@enbox/dwn-sql-store 0.0.7 → 0.0.9
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.
- package/dist/esm/src/blockstore-sql.js +117 -0
- package/dist/esm/src/blockstore-sql.js.map +1 -0
- package/dist/esm/src/data-store-s3.js +243 -0
- package/dist/esm/src/data-store-s3.js.map +1 -0
- package/dist/esm/src/data-store-sql.js +175 -59
- package/dist/esm/src/data-store-sql.js.map +1 -1
- package/dist/esm/src/main.js +4 -0
- package/dist/esm/src/main.js.map +1 -1
- package/dist/esm/src/message-store-sql.js +1 -0
- package/dist/esm/src/message-store-sql.js.map +1 -1
- package/dist/esm/src/migration-runner.js +99 -0
- package/dist/esm/src/migration-runner.js.map +1 -0
- package/dist/esm/src/migrations/001-initial-schema.js +163 -0
- package/dist/esm/src/migrations/001-initial-schema.js.map +1 -0
- package/dist/esm/src/migrations/002-content-addressed-datastore.js +126 -0
- package/dist/esm/src/migrations/002-content-addressed-datastore.js.map +1 -0
- package/dist/esm/src/migrations/003-add-squash-column.js +17 -0
- package/dist/esm/src/migrations/003-add-squash-column.js.map +1 -0
- package/dist/esm/src/migrations/index.js +13 -0
- package/dist/esm/src/migrations/index.js.map +1 -0
- package/dist/esm/src/state-index-sql.js +4 -3
- package/dist/esm/src/state-index-sql.js.map +1 -1
- package/dist/types/src/blockstore-sql.d.ts +36 -0
- package/dist/types/src/blockstore-sql.d.ts.map +1 -0
- package/dist/types/src/data-store-s3.d.ts +53 -0
- package/dist/types/src/data-store-s3.d.ts.map +1 -0
- package/dist/types/src/data-store-sql.d.ts +12 -0
- package/dist/types/src/data-store-sql.d.ts.map +1 -1
- package/dist/types/src/main.d.ts +4 -0
- package/dist/types/src/main.d.ts.map +1 -1
- package/dist/types/src/message-store-sql.d.ts.map +1 -1
- package/dist/types/src/migration-runner.d.ts +50 -0
- package/dist/types/src/migration-runner.d.ts.map +1 -0
- package/dist/types/src/migrations/001-initial-schema.d.ts +10 -0
- package/dist/types/src/migrations/001-initial-schema.d.ts.map +1 -0
- package/dist/types/src/migrations/002-content-addressed-datastore.d.ts +28 -0
- package/dist/types/src/migrations/002-content-addressed-datastore.d.ts.map +1 -0
- package/dist/types/src/migrations/003-add-squash-column.d.ts +10 -0
- package/dist/types/src/migrations/003-add-squash-column.d.ts.map +1 -0
- package/dist/types/src/migrations/index.d.ts +7 -0
- package/dist/types/src/migrations/index.d.ts.map +1 -0
- package/dist/types/src/state-index-sql.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +26 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/src/blockstore-sql.ts +142 -0
- package/src/data-store-s3.ts +338 -0
- package/src/data-store-sql.ts +208 -79
- package/src/main.ts +4 -0
- package/src/message-store-sql.ts +1 -0
- package/src/migration-runner.ts +137 -0
- package/src/migrations/001-initial-schema.ts +190 -0
- package/src/migrations/002-content-addressed-datastore.ts +140 -0
- package/src/migrations/003-add-squash-column.ts +21 -0
- package/src/migrations/index.ts +15 -0
- package/src/state-index-sql.ts +4 -3
- package/src/types.ts +30 -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
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.
|
|
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 (!
|
|
49
|
+
if (!ref) {
|
|
56
50
|
return undefined;
|
|
57
51
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
163
|
+
throw new Error(`Connection to database not open. Call \`open\` before using \`${method}\`.`);
|
|
97
164
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|