lex-gql-duckdb 0.2.0 → 0.3.0
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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/src/lex-gql-duckdb.d.ts +5 -0
- package/src/lex-gql-duckdb.js +31 -0
- package/test/lex-gql-duckdb.test.js +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 45bc9df: Add `insertRecordsBatch()` for bulk inserts
|
|
8
|
+
|
|
9
|
+
- New `insertRecordsBatch(records)` method that uses a single INSERT statement with multiple VALUES
|
|
10
|
+
- Fixes memory leak in relay example caused by unbounded `pendingWrites` queue
|
|
11
|
+
|
|
3
12
|
## 0.2.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/package.json
CHANGED
package/src/lex-gql-duckdb.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function setupSchema(conn: DuckDBConnection): Promise<void>;
|
|
|
27
27
|
/**
|
|
28
28
|
* @typedef {Object} Writer
|
|
29
29
|
* @property {(record: RecordInput) => Promise<void>} insertRecord - Insert or replace a record
|
|
30
|
+
* @property {(records: RecordInput[]) => Promise<void>} insertRecordsBatch - Insert multiple records in a single statement
|
|
30
31
|
* @property {(uri: string) => Promise<void>} deleteRecord - Delete a record by URI
|
|
31
32
|
* @property {(did: string, handle: string) => Promise<void>} upsertActor - Insert or replace an actor
|
|
32
33
|
*/
|
|
@@ -109,6 +110,10 @@ export type Writer = {
|
|
|
109
110
|
* - Insert or replace a record
|
|
110
111
|
*/
|
|
111
112
|
insertRecord: (record: RecordInput) => Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* - Insert multiple records in a single statement
|
|
115
|
+
*/
|
|
116
|
+
insertRecordsBatch: (records: RecordInput[]) => Promise<void>;
|
|
112
117
|
/**
|
|
113
118
|
* - Delete a record by URI
|
|
114
119
|
*/
|
package/src/lex-gql-duckdb.js
CHANGED
|
@@ -103,6 +103,7 @@ function parseAtUri(uri) {
|
|
|
103
103
|
/**
|
|
104
104
|
* @typedef {Object} Writer
|
|
105
105
|
* @property {(record: RecordInput) => Promise<void>} insertRecord - Insert or replace a record
|
|
106
|
+
* @property {(records: RecordInput[]) => Promise<void>} insertRecordsBatch - Insert multiple records in a single statement
|
|
106
107
|
* @property {(uri: string) => Promise<void>} deleteRecord - Delete a record by URI
|
|
107
108
|
* @property {(did: string, handle: string) => Promise<void>} upsertActor - Insert or replace an actor
|
|
108
109
|
*/
|
|
@@ -141,6 +142,36 @@ export function createWriter(conn) {
|
|
|
141
142
|
);
|
|
142
143
|
},
|
|
143
144
|
|
|
145
|
+
insertRecordsBatch: async (records) => {
|
|
146
|
+
if (records.length === 0) return;
|
|
147
|
+
|
|
148
|
+
const now = new Date().toISOString();
|
|
149
|
+
const rows = records.map(({ uri, cid, record, indexedAt }) => {
|
|
150
|
+
const { did, collection, rkey } = parseAtUri(uri);
|
|
151
|
+
const recordJson = typeof record === 'string' ? record : JSON.stringify(record);
|
|
152
|
+
return [uri, did, collection, rkey, cid || null, recordJson, indexedAt || now];
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Build a single INSERT with multiple VALUES for ~10x faster performance
|
|
156
|
+
const placeholders = rows.map(() => '(?, ?, ?, ?, ?, ?, ?)').join(', ');
|
|
157
|
+
const params = rows.flat();
|
|
158
|
+
|
|
159
|
+
await conn.run(
|
|
160
|
+
`
|
|
161
|
+
INSERT INTO records (uri, did, collection, rkey, cid, record, indexed_at)
|
|
162
|
+
VALUES ${placeholders}
|
|
163
|
+
ON CONFLICT (uri) DO UPDATE SET
|
|
164
|
+
did = EXCLUDED.did,
|
|
165
|
+
collection = EXCLUDED.collection,
|
|
166
|
+
rkey = EXCLUDED.rkey,
|
|
167
|
+
cid = EXCLUDED.cid,
|
|
168
|
+
record = EXCLUDED.record,
|
|
169
|
+
indexed_at = EXCLUDED.indexed_at
|
|
170
|
+
`,
|
|
171
|
+
...params
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
|
|
144
175
|
deleteRecord: async (uri) => {
|
|
145
176
|
await conn.run('DELETE FROM records WHERE uri = ?', uri);
|
|
146
177
|
},
|
|
@@ -109,6 +109,43 @@ describe('createWriter', () => {
|
|
|
109
109
|
expect(rows).toHaveLength(1);
|
|
110
110
|
expect(rows[0].handle).toBe('alice.example.com');
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
it('inserts multiple records in a batch', async () => {
|
|
114
|
+
await writer.insertRecordsBatch([
|
|
115
|
+
{ uri: 'at://did:plc:alice/app.bsky.feed.post/1', cid: 'cid1', record: { text: 'Post 1' } },
|
|
116
|
+
{ uri: 'at://did:plc:alice/app.bsky.feed.post/2', cid: 'cid2', record: { text: 'Post 2' } },
|
|
117
|
+
{ uri: 'at://did:plc:bob/app.bsky.feed.post/1', cid: 'cid3', record: { text: 'Post 3' } },
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
const rows = await db.all('SELECT * FROM records ORDER BY uri');
|
|
121
|
+
expect(rows).toHaveLength(3);
|
|
122
|
+
expect(JSON.parse(rows[0].record)).toEqual({ text: 'Post 1' });
|
|
123
|
+
expect(JSON.parse(rows[1].record)).toEqual({ text: 'Post 2' });
|
|
124
|
+
expect(rows[2].did).toBe('did:plc:bob');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('handles batch upsert (updates on conflict)', async () => {
|
|
128
|
+
await writer.insertRecord({
|
|
129
|
+
uri: 'at://did:plc:alice/app.bsky.feed.post/1',
|
|
130
|
+
record: { text: 'Original' },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await writer.insertRecordsBatch([
|
|
134
|
+
{ uri: 'at://did:plc:alice/app.bsky.feed.post/1', record: { text: 'Updated' } },
|
|
135
|
+
{ uri: 'at://did:plc:alice/app.bsky.feed.post/2', record: { text: 'New' } },
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const rows = await db.all('SELECT * FROM records ORDER BY uri');
|
|
139
|
+
expect(rows).toHaveLength(2);
|
|
140
|
+
expect(JSON.parse(rows[0].record)).toEqual({ text: 'Updated' });
|
|
141
|
+
expect(JSON.parse(rows[1].record)).toEqual({ text: 'New' });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles empty batch gracefully', async () => {
|
|
145
|
+
await writer.insertRecordsBatch([]);
|
|
146
|
+
const rows = await db.all('SELECT * FROM records');
|
|
147
|
+
expect(rows).toHaveLength(0);
|
|
148
|
+
});
|
|
112
149
|
});
|
|
113
150
|
|
|
114
151
|
describe('buildWhere', () => {
|