@enbox/dwn-sql-store 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/esm/src/blockstore-sql.js +117 -0
  2. package/dist/esm/src/blockstore-sql.js.map +1 -0
  3. package/dist/esm/src/data-store-s3.js +243 -0
  4. package/dist/esm/src/data-store-s3.js.map +1 -0
  5. package/dist/esm/src/data-store-sql.js +175 -59
  6. package/dist/esm/src/data-store-sql.js.map +1 -1
  7. package/dist/esm/src/main.js +4 -0
  8. package/dist/esm/src/main.js.map +1 -1
  9. package/dist/esm/src/migration-runner.js +99 -0
  10. package/dist/esm/src/migration-runner.js.map +1 -0
  11. package/dist/esm/src/migrations/001-initial-schema.js +163 -0
  12. package/dist/esm/src/migrations/001-initial-schema.js.map +1 -0
  13. package/dist/esm/src/migrations/002-content-addressed-datastore.js +126 -0
  14. package/dist/esm/src/migrations/002-content-addressed-datastore.js.map +1 -0
  15. package/dist/esm/src/migrations/index.js +11 -0
  16. package/dist/esm/src/migrations/index.js.map +1 -0
  17. package/dist/esm/src/state-index-sql.js +4 -3
  18. package/dist/esm/src/state-index-sql.js.map +1 -1
  19. package/dist/types/src/blockstore-sql.d.ts +36 -0
  20. package/dist/types/src/blockstore-sql.d.ts.map +1 -0
  21. package/dist/types/src/data-store-s3.d.ts +53 -0
  22. package/dist/types/src/data-store-s3.d.ts.map +1 -0
  23. package/dist/types/src/data-store-sql.d.ts +12 -0
  24. package/dist/types/src/data-store-sql.d.ts.map +1 -1
  25. package/dist/types/src/main.d.ts +4 -0
  26. package/dist/types/src/main.d.ts.map +1 -1
  27. package/dist/types/src/migration-runner.d.ts +50 -0
  28. package/dist/types/src/migration-runner.d.ts.map +1 -0
  29. package/dist/types/src/migrations/001-initial-schema.d.ts +10 -0
  30. package/dist/types/src/migrations/001-initial-schema.d.ts.map +1 -0
  31. package/dist/types/src/migrations/002-content-addressed-datastore.d.ts +28 -0
  32. package/dist/types/src/migrations/002-content-addressed-datastore.d.ts.map +1 -0
  33. package/dist/types/src/migrations/index.d.ts +7 -0
  34. package/dist/types/src/migrations/index.d.ts.map +1 -0
  35. package/dist/types/src/state-index-sql.d.ts.map +1 -1
  36. package/dist/types/src/types.d.ts +25 -0
  37. package/dist/types/src/types.d.ts.map +1 -1
  38. package/package.json +8 -2
  39. package/src/blockstore-sql.ts +142 -0
  40. package/src/data-store-s3.ts +338 -0
  41. package/src/data-store-sql.ts +208 -79
  42. package/src/main.ts +4 -0
  43. package/src/migration-runner.ts +137 -0
  44. package/src/migrations/001-initial-schema.ts +190 -0
  45. package/src/migrations/002-content-addressed-datastore.ts +140 -0
  46. package/src/migrations/index.ts +13 -0
  47. package/src/state-index-sql.ts +4 -3
  48. package/src/types.ts +29 -0
@@ -0,0 +1,117 @@
1
+ import { CID } from 'multiformats';
2
+ /**
3
+ * SQL-backed implementation of the `Blockstore` v5 interface, scoped to a
4
+ * single `rootDataCid`. All block operations are constrained to the blocks
5
+ * belonging to this root CID in the `dataBlocks` table.
6
+ *
7
+ * Used by `ipfs-unixfs-importer` (during `put()`) and `ipfs-unixfs-exporter`
8
+ * (during `get()`) to store and retrieve individual DAG-PB blocks.
9
+ *
10
+ * The Kysely instance and database connection are managed externally by
11
+ * `DataStoreSql`. This class does not own the connection lifecycle.
12
+ */
13
+ export class BlockstoreSql {
14
+ #db;
15
+ #rootDataCid;
16
+ constructor(db, rootDataCid) {
17
+ this.#db = db;
18
+ this.#rootDataCid = rootDataCid;
19
+ }
20
+ async open() {
21
+ // No-op: connection managed by DataStoreSql.
22
+ }
23
+ async close() {
24
+ // No-op: connection managed by DataStoreSql.
25
+ }
26
+ async put(key, val, _options) {
27
+ const blockCid = key.toString();
28
+ await this.#db
29
+ .insertInto('dataBlocks')
30
+ .values({
31
+ rootDataCid: this.#rootDataCid,
32
+ blockCid,
33
+ data: Buffer.from(val),
34
+ })
35
+ .execute();
36
+ return key;
37
+ }
38
+ async get(key, _options) {
39
+ const result = await this.#db
40
+ .selectFrom('dataBlocks')
41
+ .select('data')
42
+ .where('rootDataCid', '=', this.#rootDataCid)
43
+ .where('blockCid', '=', key.toString())
44
+ .executeTakeFirst();
45
+ if (!result) {
46
+ throw new Error(`BlockstoreSql: block not found for rootDataCid=${this.#rootDataCid}, blockCid=${key}`);
47
+ }
48
+ return new Uint8Array(result.data);
49
+ }
50
+ async has(key, _options) {
51
+ const result = await this.#db
52
+ .selectFrom('dataBlocks')
53
+ .select('blockCid')
54
+ .where('rootDataCid', '=', this.#rootDataCid)
55
+ .where('blockCid', '=', key.toString())
56
+ .executeTakeFirst();
57
+ return result !== undefined;
58
+ }
59
+ async delete(key, _options) {
60
+ await this.#db
61
+ .deleteFrom('dataBlocks')
62
+ .where('rootDataCid', '=', this.#rootDataCid)
63
+ .where('blockCid', '=', key.toString())
64
+ .execute();
65
+ }
66
+ async isEmpty(_options) {
67
+ const result = await this.#db
68
+ .selectFrom('dataBlocks')
69
+ .select('blockCid')
70
+ .where('rootDataCid', '=', this.#rootDataCid)
71
+ .executeTakeFirst();
72
+ return result === undefined;
73
+ }
74
+ async *putMany(source, options) {
75
+ for await (const entry of source) {
76
+ await this.put(entry.cid, entry.block, options);
77
+ yield entry.cid;
78
+ }
79
+ }
80
+ async *getMany(source, options) {
81
+ for await (const key of source) {
82
+ yield {
83
+ cid: key,
84
+ block: await this.get(key, options),
85
+ };
86
+ }
87
+ }
88
+ async *getAll(_options) {
89
+ const rows = await this.#db
90
+ .selectFrom('dataBlocks')
91
+ .select(['blockCid', 'data'])
92
+ .where('rootDataCid', '=', this.#rootDataCid)
93
+ .execute();
94
+ for (const row of rows) {
95
+ yield {
96
+ cid: CID.parse(row.blockCid),
97
+ block: new Uint8Array(row.data),
98
+ };
99
+ }
100
+ }
101
+ async *deleteMany(source, options) {
102
+ for await (const key of source) {
103
+ await this.delete(key, options);
104
+ yield key;
105
+ }
106
+ }
107
+ /**
108
+ * Deletes all blocks for this rootDataCid.
109
+ */
110
+ async clear() {
111
+ await this.#db
112
+ .deleteFrom('dataBlocks')
113
+ .where('rootDataCid', '=', this.#rootDataCid)
114
+ .execute();
115
+ }
116
+ }
117
+ //# sourceMappingURL=blockstore-sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blockstore-sql.js","sourceRoot":"","sources":["../../../src/blockstore-sql.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,243 @@
1
+ import { DataStream } from '@enbox/dwn-sdk-js';
2
+ import { Kysely } from 'kysely';
3
+ import { Readable } from 'stream';
4
+ import { Upload } from '@aws-sdk/lib-storage';
5
+ import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
6
+ /**
7
+ * S3-backed implementation of {@link DataStore} with SQL-based reference
8
+ * tracking for content-addressed deduplication.
9
+ *
10
+ * Data is stored as whole S3 objects keyed by `dataCid`. The same `dataCid`
11
+ * maps to a single S3 object regardless of how many (tenant, recordId) pairs
12
+ * reference it. A `dataRefs` SQL table tracks references; blocks are
13
+ * garbage-collected from S3 when the last ref is deleted.
14
+ *
15
+ * For files over `partSize` (default 5MB), the AWS SDK Upload helper
16
+ * automatically uses multipart upload with bounded memory
17
+ * (`queueSize * partSize`).
18
+ */
19
+ export class DataStoreS3 {
20
+ #dialect;
21
+ #db = null;
22
+ #s3;
23
+ #bucket;
24
+ #partSize;
25
+ #queueSize;
26
+ constructor(config) {
27
+ this.#dialect = config.dialect;
28
+ this.#bucket = config.bucket;
29
+ this.#partSize = config.partSize ?? 5 * 1024 * 1024; // 5 MB
30
+ this.#queueSize = config.queueSize ?? 4;
31
+ this.#s3 = config.s3Client ?? new S3Client({
32
+ region: config.region ?? 'us-east-1',
33
+ endpoint: config.endpoint,
34
+ forcePathStyle: config.forcePathStyle ?? false,
35
+ credentials: config.credentials,
36
+ });
37
+ }
38
+ async open() {
39
+ if (this.#db) {
40
+ return;
41
+ }
42
+ this.#db = new Kysely({ dialect: this.#dialect });
43
+ await this.#ensureRefsTable();
44
+ }
45
+ async close() {
46
+ await this.#db?.destroy();
47
+ this.#db = null;
48
+ }
49
+ async get(tenant, recordId, dataCid) {
50
+ const db = this.#getDb('get');
51
+ const ref = await db
52
+ .selectFrom('dataRefs')
53
+ .select('dataSize')
54
+ .where('tenant', '=', tenant)
55
+ .where('recordId', '=', recordId)
56
+ .where('dataCid', '=', dataCid)
57
+ .executeTakeFirst();
58
+ if (!ref) {
59
+ return undefined;
60
+ }
61
+ const response = await this.#s3.send(new GetObjectCommand({
62
+ Bucket: this.#bucket,
63
+ Key: dataCid,
64
+ }));
65
+ if (!response.Body) {
66
+ return undefined;
67
+ }
68
+ const dataStream = response.Body.transformToWebStream();
69
+ return {
70
+ dataSize: Number(ref.dataSize),
71
+ dataStream,
72
+ };
73
+ }
74
+ async put(tenant, recordId, dataCid, dataStream) {
75
+ const db = this.#getDb('put');
76
+ // Check if this exact ref already exists (idempotent put).
77
+ const existingRef = await db
78
+ .selectFrom('dataRefs')
79
+ .select('dataSize')
80
+ .where('tenant', '=', tenant)
81
+ .where('recordId', '=', recordId)
82
+ .where('dataCid', '=', dataCid)
83
+ .executeTakeFirst();
84
+ if (existingRef) {
85
+ await DataStream.toBytes(dataStream);
86
+ return { dataSize: Number(existingRef.dataSize) };
87
+ }
88
+ // Check if another ref for this dataCid already exists (dedup path).
89
+ const otherRef = await db
90
+ .selectFrom('dataRefs')
91
+ .select('dataSize')
92
+ .where('dataCid', '=', dataCid)
93
+ .executeTakeFirst();
94
+ let dataSize;
95
+ if (otherRef) {
96
+ // S3 object already exists — skip upload.
97
+ await DataStream.toBytes(dataStream);
98
+ dataSize = Number(otherRef.dataSize);
99
+ }
100
+ else {
101
+ // New data — upload to S3 with a counting passthrough.
102
+ dataSize = await this.#uploadToS3(dataCid, dataStream);
103
+ }
104
+ // Insert the reference.
105
+ await db
106
+ .insertInto('dataRefs')
107
+ .values({ tenant, recordId, dataCid, dataSize })
108
+ .execute();
109
+ return { dataSize };
110
+ }
111
+ async delete(tenant, recordId, dataCid) {
112
+ const db = this.#getDb('delete');
113
+ // Remove the reference.
114
+ await db
115
+ .deleteFrom('dataRefs')
116
+ .where('tenant', '=', tenant)
117
+ .where('recordId', '=', recordId)
118
+ .where('dataCid', '=', dataCid)
119
+ .execute();
120
+ // Garbage-collect the S3 object if no more refs point to this dataCid.
121
+ const remaining = await db
122
+ .selectFrom('dataRefs')
123
+ .select('dataCid')
124
+ .where('dataCid', '=', dataCid)
125
+ .executeTakeFirst();
126
+ if (!remaining) {
127
+ await this.#s3.send(new DeleteObjectCommand({
128
+ Bucket: this.#bucket,
129
+ Key: dataCid,
130
+ }));
131
+ }
132
+ }
133
+ async clear() {
134
+ const db = this.#getDb('clear');
135
+ // Clear the refs table.
136
+ await db.deleteFrom('dataRefs').execute();
137
+ // Delete all S3 objects in the bucket.
138
+ let continuationToken;
139
+ do {
140
+ const list = await this.#s3.send(new ListObjectsV2Command({
141
+ Bucket: this.#bucket,
142
+ ContinuationToken: continuationToken,
143
+ }));
144
+ const objects = (list.Contents ?? [])
145
+ .filter((obj) => obj.Key !== undefined)
146
+ .map((obj) => ({ Key: obj.Key }));
147
+ if (objects.length > 0) {
148
+ await this.#s3.send(new DeleteObjectsCommand({
149
+ Bucket: this.#bucket,
150
+ Delete: { Objects: objects },
151
+ }));
152
+ }
153
+ continuationToken = list.NextContinuationToken;
154
+ } while (continuationToken);
155
+ }
156
+ // ─── Private helpers ────────────────────────────────────────────────
157
+ #getDb(method) {
158
+ if (!this.#db) {
159
+ throw new Error(`Connection to database not open. Call \`open\` before using \`${method}\`.`);
160
+ }
161
+ return this.#db;
162
+ }
163
+ /**
164
+ * Uploads data to S3, counting bytes as they stream through.
165
+ * Uses multipart upload for large files via `@aws-sdk/lib-storage`.
166
+ * @returns The total number of bytes uploaded.
167
+ */
168
+ async #uploadToS3(dataCid, dataStream) {
169
+ let dataSize = 0;
170
+ // Create a Node Readable from the web ReadableStream, counting bytes.
171
+ const reader = dataStream.getReader();
172
+ const nodeStream = new Readable({
173
+ async read() {
174
+ const { done, value } = await reader.read();
175
+ if (done) {
176
+ this.push(null);
177
+ }
178
+ else {
179
+ dataSize += value.byteLength;
180
+ this.push(Buffer.from(value));
181
+ }
182
+ },
183
+ });
184
+ // For small files, a simple PutObject suffices. For large files,
185
+ // Upload handles multipart automatically with bounded memory.
186
+ if (this.#partSize > 0) {
187
+ const upload = new Upload({
188
+ client: this.#s3,
189
+ params: {
190
+ Bucket: this.#bucket,
191
+ Key: dataCid,
192
+ Body: nodeStream,
193
+ },
194
+ queueSize: this.#queueSize,
195
+ partSize: this.#partSize,
196
+ });
197
+ await upload.done();
198
+ }
199
+ else {
200
+ // Fallback: buffer entire stream (only for tiny test payloads).
201
+ const chunks = [];
202
+ for (;;) {
203
+ const { done, value } = await reader.read();
204
+ if (done) {
205
+ break;
206
+ }
207
+ dataSize += value.byteLength;
208
+ chunks.push(value);
209
+ }
210
+ const body = Buffer.concat(chunks);
211
+ await this.#s3.send(new PutObjectCommand({
212
+ Bucket: this.#bucket,
213
+ Key: dataCid,
214
+ Body: body,
215
+ }));
216
+ }
217
+ return dataSize;
218
+ }
219
+ /**
220
+ * Creates the `dataRefs` table if it doesn't already exist.
221
+ * Shares the same schema as DataStoreSql's `dataRefs` table.
222
+ */
223
+ async #ensureRefsTable() {
224
+ const db = this.#db;
225
+ if (!(await this.#dialect.hasTable(db, 'dataRefs'))) {
226
+ await db.schema
227
+ .createTable('dataRefs')
228
+ .ifNotExists()
229
+ .addColumn('tenant', 'varchar(255)', (col) => col.notNull())
230
+ .addColumn('recordId', 'varchar(60)', (col) => col.notNull())
231
+ .addColumn('dataCid', 'varchar(60)', (col) => col.notNull())
232
+ .addColumn('dataSize', 'bigint', (col) => col.notNull())
233
+ .execute();
234
+ await db.schema.createIndex('index_dataRefs_tenant_recordId_dataCid')
235
+ .on('dataRefs').columns(['tenant', 'recordId', 'dataCid']).unique().execute();
236
+ await db.schema.createIndex('index_dataRefs_dataCid')
237
+ .on('dataRefs').column('dataCid').execute();
238
+ await db.schema.createIndex('index_dataRefs_tenant')
239
+ .on('dataRefs').column('tenant').execute();
240
+ }
241
+ }
242
+ }
243
+ //# sourceMappingURL=data-store-s3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-store-s3.js","sourceRoot":"","sources":["../../../src/data-store-s3.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,21 @@
1
+ import { BlockstoreSql } from './blockstore-sql.js';
2
+ import { CID } from 'multiformats';
1
3
  import { DataStream } from '@enbox/dwn-sdk-js';
4
+ import { exporter } from 'ipfs-unixfs-exporter';
5
+ import { importer } from 'ipfs-unixfs-importer';
2
6
  import { Kysely } from 'kysely';
7
+ /**
8
+ * SQL-backed implementation of {@link DataStore} with content-addressed
9
+ * deduplication.
10
+ *
11
+ * Data is stored as DAG-PB blocks (via `ipfs-unixfs-importer`) in the
12
+ * `dataBlocks` table, keyed by `(rootDataCid, blockCid)`. A separate
13
+ * `dataRefs` table maps `(tenant, recordId, dataCid)` to content. When
14
+ * multiple records share the same `dataCid`, blocks are stored only once.
15
+ *
16
+ * On `delete()`, the ref is removed and blocks are garbage-collected only
17
+ * when the last ref to a `dataCid` is gone.
18
+ */
3
19
  export class DataStoreSql {
4
20
  #dialect;
5
21
  #db = null;
@@ -11,93 +27,193 @@ export class DataStoreSql {
11
27
  return;
12
28
  }
13
29
  this.#db = new Kysely({ dialect: this.#dialect });
14
- // if table already exists, there is no more things todo
15
- const tableName = 'dataStore';
16
- const tableExists = await this.#dialect.hasTable(this.#db, tableName);
17
- if (tableExists) {
18
- return;
19
- }
20
- // else create the table and corresponding indexes
21
- let table = this.#db.schema
22
- .createTable(tableName)
23
- .ifNotExists() // kept to show supported by all dialects in contrast to ifNotExists() below, though not needed due to hasTable() check above
24
- .addColumn('tenant', 'varchar(255)', (col) => col.notNull())
25
- .addColumn('recordId', 'varchar(60)', (col) => col.notNull())
26
- .addColumn('dataCid', 'varchar(60)', (col) => col.notNull());
27
- // Add columns that have dialect-specific constraints
28
- table = this.#dialect.addAutoIncrementingColumn(table, 'id', (col) => col.primaryKey());
29
- table = this.#dialect.addBlobColumn(table, 'data', (col) => col.notNull());
30
- await table.execute();
31
- // Add index for efficient lookups.
32
- await this.#db.schema
33
- .createIndex('tenant_recordId_dataCid')
34
- // .ifNotExists() // intentionally kept commented out code to show that it is not supported by all dialects (ie. MySQL)
35
- .on(tableName)
36
- .columns(['tenant', 'recordId', 'dataCid'])
37
- .unique()
38
- .execute();
30
+ // Create tables if they don't exist. In production the MigrationRunner
31
+ // creates these before open() is called; this fallback handles standalone
32
+ // usage (tests, plugins) that bypass the migration runner.
33
+ await this.#ensureTables();
39
34
  }
40
35
  async close() {
41
36
  await this.#db?.destroy();
42
37
  this.#db = null;
43
38
  }
44
39
  async get(tenant, recordId, dataCid) {
45
- if (!this.#db) {
46
- throw new Error('Connection to database not open. Call `open` before using `get`.');
47
- }
48
- const result = await this.#db
49
- .selectFrom('dataStore')
50
- .selectAll()
40
+ const db = this.#getDb('get');
41
+ // Look up the reference to confirm this tenant+record has this data.
42
+ const ref = await db
43
+ .selectFrom('dataRefs')
44
+ .select('dataSize')
51
45
  .where('tenant', '=', tenant)
52
46
  .where('recordId', '=', recordId)
53
47
  .where('dataCid', '=', dataCid)
54
48
  .executeTakeFirst();
55
- if (!result) {
49
+ if (!ref) {
56
50
  return undefined;
57
51
  }
58
- const dataBytes = new Uint8Array(result.data);
59
- return {
60
- dataSize: result.data.length,
61
- dataStream: new ReadableStream({
62
- start(controller) {
63
- controller.enqueue(dataBytes);
52
+ const blockstore = new BlockstoreSql(db, dataCid);
53
+ // Use ipfs-unixfs-exporter to stream data from DAG-PB blocks.
54
+ const dataDagRoot = await exporter(dataCid, blockstore);
55
+ const contentIterator = dataDagRoot.content();
56
+ const dataStream = new ReadableStream({
57
+ async pull(controller) {
58
+ const result = await contentIterator.next();
59
+ if (result.done) {
64
60
  controller.close();
65
61
  }
66
- }),
62
+ else {
63
+ controller.enqueue(result.value);
64
+ }
65
+ },
66
+ });
67
+ return {
68
+ dataSize: Number(ref.dataSize),
69
+ dataStream,
67
70
  };
68
71
  }
69
72
  async put(tenant, recordId, dataCid, dataStream) {
70
- if (!this.#db) {
71
- throw new Error('Connection to database not open. Call `open` before using `put`.');
73
+ const db = this.#getDb('put');
74
+ // Check if this exact ref already exists (idempotent put).
75
+ const existingRef = await db
76
+ .selectFrom('dataRefs')
77
+ .select('dataSize')
78
+ .where('tenant', '=', tenant)
79
+ .where('recordId', '=', recordId)
80
+ .where('dataCid', '=', dataCid)
81
+ .executeTakeFirst();
82
+ if (existingRef) {
83
+ // Drain the stream — caller expects it to be consumed.
84
+ await DataStream.toBytes(dataStream);
85
+ return { dataSize: Number(existingRef.dataSize) };
72
86
  }
73
- const bytes = await DataStream.toBytes(dataStream);
74
- const data = Buffer.from(bytes);
75
- await this.#db
76
- .insertInto('dataStore')
77
- .values({ tenant, recordId, dataCid, data })
78
- .executeTakeFirstOrThrow();
79
- return {
80
- dataSize: bytes.length
81
- };
87
+ // Check if blocks for this dataCid already exist (dedup path).
88
+ const blockstore = new BlockstoreSql(db, dataCid);
89
+ const rootCid = CID.parse(dataCid);
90
+ const blocksExist = await blockstore.has(rootCid);
91
+ let dataSize;
92
+ if (blocksExist) {
93
+ // Blocks already stored by a previous ref with the same dataCid.
94
+ // Get the data size from that existing ref.
95
+ const otherRef = await db
96
+ .selectFrom('dataRefs')
97
+ .select('dataSize')
98
+ .where('dataCid', '=', dataCid)
99
+ .executeTakeFirst();
100
+ if (otherRef) {
101
+ dataSize = Number(otherRef.dataSize);
102
+ // Drain the stream — caller expects it to be consumed.
103
+ await DataStream.toBytes(dataStream);
104
+ }
105
+ else {
106
+ // Edge case: blocks exist but no ref (interrupted previous put).
107
+ // Count bytes without full buffering.
108
+ dataSize = await DataStoreSql.#countStreamBytes(dataStream);
109
+ }
110
+ }
111
+ else {
112
+ // New data — clean up any partial blocks from interrupted imports,
113
+ // then chunk the data into DAG-PB blocks via the importer.
114
+ await blockstore.clear();
115
+ const asyncDataBlocks = importer([{ content: DataStream.asAsyncIterable(dataStream) }], blockstore, { cidVersion: 1 });
116
+ // The last block yielded contains the root CID and file size info.
117
+ let dataDagRoot;
118
+ for await (dataDagRoot of asyncDataBlocks) {
119
+ ;
120
+ }
121
+ dataSize = Number(dataDagRoot.unixfs?.fileSize() ?? dataDagRoot.size);
122
+ }
123
+ // Insert the reference.
124
+ await db
125
+ .insertInto('dataRefs')
126
+ .values({ tenant, recordId, dataCid, dataSize })
127
+ .execute();
128
+ return { dataSize };
82
129
  }
83
130
  async delete(tenant, recordId, dataCid) {
84
- if (!this.#db) {
85
- throw new Error('Connection to database not open. Call `open` before using `delete`.');
86
- }
87
- await this.#db
88
- .deleteFrom('dataStore')
131
+ const db = this.#getDb('delete');
132
+ // Remove the reference.
133
+ await db
134
+ .deleteFrom('dataRefs')
89
135
  .where('tenant', '=', tenant)
90
136
  .where('recordId', '=', recordId)
91
137
  .where('dataCid', '=', dataCid)
92
138
  .execute();
139
+ // Garbage-collect blocks if no more refs point to this dataCid.
140
+ const remaining = await db
141
+ .selectFrom('dataRefs')
142
+ .select('dataCid')
143
+ .where('dataCid', '=', dataCid)
144
+ .executeTakeFirst();
145
+ if (!remaining) {
146
+ await db
147
+ .deleteFrom('dataBlocks')
148
+ .where('rootDataCid', '=', dataCid)
149
+ .execute();
150
+ }
93
151
  }
94
152
  async clear() {
153
+ const db = this.#getDb('clear');
154
+ await db.deleteFrom('dataRefs').execute();
155
+ await db.deleteFrom('dataBlocks').execute();
156
+ }
157
+ // ─── Private helpers ────────────────────────────────────────────────
158
+ /**
159
+ * Returns the open database instance, or throws if not yet opened.
160
+ */
161
+ #getDb(method) {
95
162
  if (!this.#db) {
96
- throw new Error('Connection to database not open. Call `open` before using `clear`.');
163
+ throw new Error(`Connection to database not open. Call \`open\` before using \`${method}\`.`);
97
164
  }
98
- await this.#db
99
- .deleteFrom('dataStore')
100
- .execute();
165
+ return this.#db;
166
+ }
167
+ /**
168
+ * Creates the `dataRefs` and `dataBlocks` tables if they don't already exist.
169
+ * This is a fallback for standalone usage without the MigrationRunner.
170
+ */
171
+ async #ensureTables() {
172
+ const db = this.#db;
173
+ // ─── dataRefs ─────────────────────────────────────────────────────
174
+ if (!(await this.#dialect.hasTable(db, 'dataRefs'))) {
175
+ await db.schema
176
+ .createTable('dataRefs')
177
+ .ifNotExists()
178
+ .addColumn('tenant', 'varchar(255)', (col) => col.notNull())
179
+ .addColumn('recordId', 'varchar(60)', (col) => col.notNull())
180
+ .addColumn('dataCid', 'varchar(60)', (col) => col.notNull())
181
+ .addColumn('dataSize', 'bigint', (col) => col.notNull())
182
+ .execute();
183
+ await db.schema.createIndex('index_dataRefs_tenant_recordId_dataCid')
184
+ .on('dataRefs').columns(['tenant', 'recordId', 'dataCid']).unique().execute();
185
+ await db.schema.createIndex('index_dataRefs_dataCid')
186
+ .on('dataRefs').column('dataCid').execute();
187
+ await db.schema.createIndex('index_dataRefs_tenant')
188
+ .on('dataRefs').column('tenant').execute();
189
+ }
190
+ // ─── dataBlocks ───────────────────────────────────────────────────
191
+ if (!(await this.#dialect.hasTable(db, 'dataBlocks'))) {
192
+ let table = db.schema
193
+ .createTable('dataBlocks')
194
+ .ifNotExists()
195
+ .addColumn('rootDataCid', 'varchar(60)', (col) => col.notNull())
196
+ .addColumn('blockCid', 'varchar(60)', (col) => col.notNull());
197
+ table = this.#dialect.addBlobColumn(table, 'data', (col) => col.notNull());
198
+ await table.execute();
199
+ await db.schema.createIndex('index_dataBlocks_rootDataCid_blockCid')
200
+ .on('dataBlocks').columns(['rootDataCid', 'blockCid']).unique().execute();
201
+ }
202
+ }
203
+ /**
204
+ * Counts the number of bytes in a stream without buffering the full content.
205
+ */
206
+ static async #countStreamBytes(stream) {
207
+ const reader = stream.getReader();
208
+ let size = 0;
209
+ for (;;) {
210
+ const { done, value } = await reader.read();
211
+ if (done) {
212
+ break;
213
+ }
214
+ size += value.byteLength;
215
+ }
216
+ return size;
101
217
  }
102
218
  }
103
219
  //# sourceMappingURL=data-store-sql.js.map