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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lex-gql-duckdb",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "DuckDB adapter for lex-gql - optimized for analytics queries",
5
5
  "type": "module",
6
6
  "main": "src/lex-gql-duckdb.js",
@@ -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
  */
@@ -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', () => {