@sqlrooms/duckdb 0.29.0-rc.1 → 0.29.0-rc.11

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 (40) hide show
  1. package/LICENSE.md +2 -1
  2. package/README.md +84 -4
  3. package/dist/DuckDbSlice.d.ts +56 -14
  4. package/dist/DuckDbSlice.d.ts.map +1 -1
  5. package/dist/DuckDbSlice.js +299 -164
  6. package/dist/DuckDbSlice.js.map +1 -1
  7. package/dist/connectors/WasmDuckDbConnector.d.ts.map +1 -1
  8. package/dist/connectors/WasmDuckDbConnector.js +27 -0
  9. package/dist/connectors/WasmDuckDbConnector.js.map +1 -1
  10. package/dist/connectors/WebSocketDuckDbConnector.d.ts +23 -0
  11. package/dist/connectors/WebSocketDuckDbConnector.d.ts.map +1 -1
  12. package/dist/connectors/WebSocketDuckDbConnector.js +62 -0
  13. package/dist/connectors/WebSocketDuckDbConnector.js.map +1 -1
  14. package/dist/connectors/createDuckDbConnector.d.ts +1 -1
  15. package/dist/connectors/createDuckDbConnector.d.ts.map +1 -1
  16. package/dist/connectors/createDuckDbConnector.js.map +1 -1
  17. package/dist/index.d.ts +8 -5
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/loadTableSchemas.d.ts +40 -0
  22. package/dist/loadTableSchemas.d.ts.map +1 -0
  23. package/dist/loadTableSchemas.js +260 -0
  24. package/dist/loadTableSchemas.js.map +1 -0
  25. package/dist/use-copy-as-tsv.d.ts +13 -0
  26. package/dist/use-copy-as-tsv.d.ts.map +1 -0
  27. package/dist/use-copy-as-tsv.js +68 -0
  28. package/dist/use-copy-as-tsv.js.map +1 -0
  29. package/dist/use-export-to-csv.d.ts +5 -0
  30. package/dist/use-export-to-csv.d.ts.map +1 -0
  31. package/dist/{exportToCsv.js → use-export-to-csv.js} +1 -1
  32. package/dist/use-export-to-csv.js.map +1 -0
  33. package/dist/useDataTable.d.ts +14 -0
  34. package/dist/useDataTable.d.ts.map +1 -0
  35. package/dist/useDataTable.js +21 -0
  36. package/dist/useDataTable.js.map +1 -0
  37. package/package.json +27 -27
  38. package/dist/exportToCsv.d.ts +0 -4
  39. package/dist/exportToCsv.d.ts.map +0 -1
  40. package/dist/exportToCsv.js.map +0 -1
@@ -1,11 +1,55 @@
1
- import { createDbSchemaTrees, escapeVal, getColValAsNumber, isQualifiedTableName, joinStatements, makeQualifiedTableName, separateLastStatement, } from '@sqlrooms/duckdb-core';
1
+ import { createDbSchemaTrees, escapeVal, getColValAsNumber, getRawSqlTableReference, getTableIdentity, isQualifiedTableName, joinStatements, makeQualifiedTableName, parseQualifiedSqlIdentifier, separateLastStatement, } from '@sqlrooms/duckdb-core';
2
2
  import { createSlice, registerCommandsForOwner, unregisterCommandsForOwner, useBaseRoomStore, } from '@sqlrooms/room-store';
3
3
  import * as arrow from 'apache-arrow';
4
4
  import deepEquals from 'fast-deep-equal';
5
5
  import { produce } from 'immer';
6
6
  import { z } from 'zod';
7
7
  import { createWasmDuckDbConnector } from './connectors/createDuckDbConnector';
8
+ import { loadSchemaCatalog, loadTableSchemas, } from './loadTableSchemas';
8
9
  const DUCKDB_COMMAND_OWNER = '@sqlrooms/duckdb';
10
+ const INTERNAL_SQLROOMS_PREFIX = '__sqlrooms';
11
+ /** DuckDB's temporary database catalog; never show in the default data source tree. */
12
+ const DUCKDB_TEMP_DATABASE = 'temp';
13
+ /**
14
+ * Default predicate: which tables/schemas/databases appear in the data source panel.
15
+ * Hides `__sqlrooms_*` names and DuckDB's `temp` database.
16
+ */
17
+ export const defaultLoadTableSchemasFilter = (table) => {
18
+ if (table.table.startsWith(INTERNAL_SQLROOMS_PREFIX) ||
19
+ table.database?.startsWith(INTERNAL_SQLROOMS_PREFIX) ||
20
+ table.schema?.startsWith(INTERNAL_SQLROOMS_PREFIX)) {
21
+ return false;
22
+ }
23
+ if (table.database === DUCKDB_TEMP_DATABASE) {
24
+ return false;
25
+ }
26
+ return true;
27
+ };
28
+ /**
29
+ * Factory returning {@link defaultLoadTableSchemasFilter}.
30
+ * Hides `__sqlrooms_*` names and DuckDB's `temp` database.
31
+ * Apps can pass {@link CreateDuckDbSliceProps.loadTableSchemasFilter} to add more rules.
32
+ */
33
+ export function createDefaultLoadTableSchemasFilter() {
34
+ return defaultLoadTableSchemasFilter;
35
+ }
36
+ /**
37
+ * Default catalog visibility predicate for the data source panel.
38
+ * Hides SQLRooms internal objects and DuckDB's temporary database catalog.
39
+ */
40
+ export const defaultLoadSchemaCatalogFilter = (entry) => {
41
+ switch (entry.type) {
42
+ case 'database':
43
+ return (!entry.database.startsWith(INTERNAL_SQLROOMS_PREFIX) &&
44
+ entry.database !== DUCKDB_TEMP_DATABASE);
45
+ case 'schema':
46
+ return (!entry.database.startsWith(INTERNAL_SQLROOMS_PREFIX) &&
47
+ entry.database !== DUCKDB_TEMP_DATABASE &&
48
+ !entry.schema.startsWith(INTERNAL_SQLROOMS_PREFIX));
49
+ case 'table':
50
+ return defaultLoadTableSchemasFilter(entry.table);
51
+ }
52
+ };
9
53
  const DropTableCommandInput = z.object({
10
54
  tableName: z.string().describe('Name of the table to drop.'),
11
55
  });
@@ -33,14 +77,74 @@ const CreateTableFromQueryCommandInput = z.object({
33
77
  .default(false)
34
78
  .describe('Allow multiple SQL statements where the final one is SELECT.'),
35
79
  });
36
- function isDuckDbPlaceholderViewColumn(columnName, columnType) {
37
- return columnName === '__' && columnType.toUpperCase() === 'UNKNOWN';
38
- }
39
80
  /**
40
81
  * Create a DuckDB slice for managing the connector
41
82
  */
42
- export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } = {}) {
83
+ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), loadTableSchemasFilter = defaultLoadTableSchemasFilter, loadSchemaCatalogFilter, } = {}) {
84
+ let refreshPromise = null;
85
+ let pendingSchemaRefresh = false;
86
+ const effectiveSchemaCatalogFilter = loadSchemaCatalogFilter ??
87
+ (loadTableSchemasFilter === null
88
+ ? null
89
+ : ((entry) => {
90
+ if (entry.type === 'table') {
91
+ return loadTableSchemasFilter
92
+ ? loadTableSchemasFilter(entry.table)
93
+ : true;
94
+ }
95
+ return defaultLoadSchemaCatalogFilter(entry);
96
+ }));
43
97
  return createSlice((set, get, store) => {
98
+ const parseTableReferenceParts = (tableName) => {
99
+ const parsed = parseQualifiedSqlIdentifier(tableName);
100
+ return parsed?.table
101
+ ? {
102
+ database: parsed.database,
103
+ schema: parsed.schema,
104
+ table: parsed.table,
105
+ }
106
+ : { table: tableName };
107
+ };
108
+ /**
109
+ * Internal helper to load a table schema by exact name, bypassing the visibility filter.
110
+ * Used when performing exact lookups (e.g., checking if a specific table exists).
111
+ */
112
+ const loadTableSchemaByName = async (tableName) => {
113
+ const tableNameParts = typeof tableName === 'string'
114
+ ? parseTableReferenceParts(tableName)
115
+ : tableName;
116
+ const qualifiedName = get().db.qualifyTableName(tableNameParts);
117
+ const connector = await get().db.getConnector();
118
+ const [table] = await loadTableSchemas(connector, {
119
+ database: qualifiedName.database,
120
+ schema: qualifiedName.schema,
121
+ table: qualifiedName.table,
122
+ defaultDatabase: get().db.currentDatabase,
123
+ });
124
+ return table &&
125
+ table.table.table === qualifiedName.table &&
126
+ (!qualifiedName.schema ||
127
+ table.table.schema === qualifiedName.schema) &&
128
+ (!qualifiedName.database ||
129
+ table.table.database === qualifiedName.database)
130
+ ? table
131
+ : undefined;
132
+ };
133
+ const throwIfUnresolvedExplicitDatabaseReference = (tableName, table) => {
134
+ if (table) {
135
+ return;
136
+ }
137
+ const database = typeof tableName === 'string'
138
+ ? parseTableReferenceParts(tableName).database
139
+ : tableName.database;
140
+ if (!database) {
141
+ return;
142
+ }
143
+ const qualifiedName = isQualifiedTableName(tableName)
144
+ ? get().db.qualifyTableName(tableName)
145
+ : get().db.qualifyTableName(parseTableReferenceParts(tableName));
146
+ throw new Error(`Relation "${getRawSqlTableReference(qualifiedName)}" not found.`);
147
+ };
44
148
  return {
45
149
  db: {
46
150
  connector, // Will be initialized during init
@@ -52,6 +156,16 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
52
156
  tableRowCounts: {},
53
157
  schemaTrees: undefined,
54
158
  queryCache: {},
159
+ qualifyTableName(tableName) {
160
+ const { currentDatabase, currentSchema } = get().db;
161
+ const parts = typeof tableName === 'string' ? { table: tableName } : tableName;
162
+ return makeQualifiedTableName({
163
+ database: parts.database ?? currentDatabase,
164
+ schema: parts.schema ?? currentSchema,
165
+ table: parts.table,
166
+ defaultDatabase: parts.defaultDatabase ?? currentDatabase,
167
+ });
168
+ },
55
169
  setConnector: (connector) => {
56
170
  set(produce((state) => {
57
171
  state.config.dataSources = [];
@@ -60,7 +174,9 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
60
174
  },
61
175
  initialize: async () => {
62
176
  await get().db.connector.initialize();
63
- await get().db.refreshTableSchemas();
177
+ // No await here, we want to continue initializing the room even
178
+ // if the table schemas are not refreshed yet
179
+ get().db.refreshTableSchemas();
64
180
  registerCommandsForOwner(store, DUCKDB_COMMAND_OWNER, createDuckDbCommands());
65
181
  },
66
182
  getConnector: async () => {
@@ -69,6 +185,18 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
69
185
  },
70
186
  destroy: async () => {
71
187
  unregisterCommandsForOwner(store, DUCKDB_COMMAND_OWNER);
188
+ // Wait for any in-flight refreshTableSchemas() to finish before
189
+ // destroying the connector. initialize() fires it without await,
190
+ // so it may still be querying the native DuckDB instance; tearing
191
+ // that down mid-flight causes a use-after-free segfault.
192
+ if (refreshPromise) {
193
+ try {
194
+ await refreshPromise;
195
+ }
196
+ catch {
197
+ // ignore – we're shutting down
198
+ }
199
+ }
72
200
  try {
73
201
  if (get().db.connector) {
74
202
  await get().db.connector.destroy();
@@ -92,14 +220,13 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
92
220
  async createTableFromQuery(tableName, query, options) {
93
221
  const { replace = true, temp = false, view = false, allowMultipleStatements = false, abortSignal, } = options || {};
94
222
  // For temp tables/views, DuckDB requires the "temp" database
95
- const baseQualifiedName = isQualifiedTableName(tableName)
96
- ? tableName
97
- : makeQualifiedTableName({ table: tableName });
223
+ const baseQualifiedName = get().db.qualifyTableName(tableName);
98
224
  const qualifiedName = temp
99
225
  ? makeQualifiedTableName({
100
226
  table: baseQualifiedName.table,
101
227
  schema: baseQualifiedName.schema,
102
228
  database: 'temp',
229
+ defaultDatabase: get().db.currentDatabase,
103
230
  })
104
231
  : baseQualifiedName;
105
232
  const connector = await get().db.getConnector();
@@ -121,7 +248,7 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
121
248
  ]
122
249
  .filter(Boolean)
123
250
  .join(' ');
124
- const createStatement = `${createKeyword} ${qualifiedName} AS (
251
+ const createStatement = `${createKeyword} ${getRawSqlTableReference(qualifiedName)} AS (
125
252
  ${lastStatement}
126
253
  )`;
127
254
  const fullQuery = joinStatements(precedingStatements, createStatement);
@@ -153,18 +280,19 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
153
280
  * @deprecated Use .loadTableRowCount() instead
154
281
  */
155
282
  async getTableRowCount(table, schema = 'main') {
156
- return get().db.loadTableRowCount({ table, schema });
283
+ return get().db.loadTableRowCount(get().db.qualifyTableName({ table, schema }));
157
284
  },
158
285
  async loadTableRowCount(tableName) {
159
286
  const { schema, database, table } = typeof tableName === 'string'
160
- ? { table: tableName }
161
- : tableName || {};
287
+ ? get().db.qualifyTableName(parseTableReferenceParts(tableName))
288
+ : get().db.qualifyTableName(tableName);
162
289
  const connector = await get().db.getConnector();
163
- const result = await connector.query(`SELECT COUNT(*) FROM ${makeQualifiedTableName({
290
+ const qualifiedName = get().db.qualifyTableName({
164
291
  schema,
165
292
  database,
166
293
  table,
167
- })}`);
294
+ });
295
+ const result = await connector.query(`SELECT COUNT(*) FROM ${getRawSqlTableReference(qualifiedName)}`);
168
296
  return getColValAsNumber(result);
169
297
  },
170
298
  /**
@@ -174,188 +302,182 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
174
302
  return await get().db.loadTableSchemas({ schema });
175
303
  },
176
304
  async loadTableSchemas(filter) {
177
- const { schema, database, table } = filter || {};
178
- const sql = `WITH tables_and_views AS (
179
- FROM duckdb_tables() SELECT
180
- database_name AS database,
181
- schema_name AS schema,
182
- table_name AS name,
183
- sql,
184
- comment,
185
- estimated_size,
186
- FALSE AS isView
187
- UNION
188
- FROM duckdb_views() SELECT
189
- database_name AS database,
190
- schema_name AS schema,
191
- view_name AS name,
192
- sql,
193
- comment,
194
- NULL estimated_size,
195
- TRUE AS isView
196
- )
197
- SELECT
198
- isView,
199
- database, schema,
200
- name, column_names, column_types,
201
- sql, comment,
202
- estimated_size
203
- FROM (DESCRIBE)
204
- LEFT OUTER JOIN tables_and_views USING (database, schema, name)
205
- ${schema || database || table
206
- ? `WHERE ${[
207
- schema ? `schema = ${escapeVal(schema)}` : '',
208
- database ? `database = ${escapeVal(database)}` : '',
209
- table ? `name = ${escapeVal(table)}` : '',
210
- ]
211
- .filter(Boolean)
212
- .join(' AND ')}`
213
- : ''}`;
214
- const describeResults = await connector.query(sql);
215
- const newTables = [];
216
- for (let i = 0; i < describeResults.numRows; i++) {
217
- const isView = describeResults.getChild('isView')?.get(i);
218
- const database = describeResults.getChild('database')?.get(i);
219
- const schema = describeResults.getChild('schema')?.get(i);
220
- const table = describeResults.getChild('name')?.get(i);
221
- const sql = describeResults.getChild('sql')?.get(i);
222
- const comment = describeResults.getChild('comment')?.get(i);
223
- const estimatedSize = describeResults
224
- .getChild('estimated_size')
225
- ?.get(i);
226
- const columnNames = describeResults
227
- .getChild('column_names')
228
- ?.get(i);
229
- const columnTypes = describeResults
230
- .getChild('column_types')
231
- ?.get(i);
232
- const columns = [];
233
- for (let di = 0; di < (columnNames?.length ?? 0); di++) {
234
- const columnName = String(columnNames.get(di));
235
- const columnType = String(columnTypes?.get(di));
236
- if (isDuckDbPlaceholderViewColumn(columnName, columnType)) {
237
- continue;
238
- }
239
- columns.push({
240
- name: columnName,
241
- type: columnType,
242
- });
243
- }
244
- newTables.push({
245
- table: makeQualifiedTableName({ database, schema, table }),
246
- database,
247
- schema,
248
- tableName: table,
249
- columns,
250
- sql,
251
- comment,
252
- isView: Boolean(isView),
253
- rowCount: typeof estimatedSize === 'bigint'
254
- ? Number(estimatedSize)
255
- : estimatedSize === null
256
- ? undefined
257
- : estimatedSize,
258
- });
259
- }
260
- return newTables;
305
+ const connector = await get().db.getConnector();
306
+ return loadTableSchemas(connector, {
307
+ ...filter,
308
+ filterFunction: loadTableSchemasFilter,
309
+ defaultDatabase: get().db.currentDatabase,
310
+ });
261
311
  },
262
312
  async checkTableExists(tableName) {
263
- const qualifiedName = isQualifiedTableName(tableName)
264
- ? tableName
265
- : makeQualifiedTableName({ table: tableName });
266
- const table = (await get().db.loadTableSchemas(qualifiedName))[0];
267
- if (!table) {
268
- return false;
313
+ const table = get().db.findTable(tableName) ??
314
+ (await loadTableSchemaByName(tableName));
315
+ return Boolean(table);
316
+ },
317
+ async dropRelation(tableName) {
318
+ const connector = await get().db.getConnector();
319
+ const table = get().db.findTable(tableName) ??
320
+ (await loadTableSchemaByName(tableName));
321
+ throwIfUnresolvedExplicitDatabaseReference(tableName, table);
322
+ const qualifiedTable = table?.table ??
323
+ (isQualifiedTableName(tableName)
324
+ ? get().db.qualifyTableName(tableName)
325
+ : get().db.qualifyTableName(parseTableReferenceParts(tableName)));
326
+ const isView = table?.isView;
327
+ const qualifiedTableReference = getRawSqlTableReference(qualifiedTable);
328
+ if (isView) {
329
+ await connector.query(`DROP VIEW IF EXISTS ${qualifiedTableReference};`);
330
+ }
331
+ else {
332
+ await connector.query(`DROP TABLE IF EXISTS ${qualifiedTableReference};`);
269
333
  }
270
- return true;
334
+ get().db.refreshTableSchemas();
271
335
  },
272
336
  async dropTable(tableName) {
273
337
  const connector = await get().db.getConnector();
274
- const qualifiedTable = isQualifiedTableName(tableName)
275
- ? tableName
276
- : makeQualifiedTableName({ table: tableName });
277
- await connector.query(`DROP TABLE IF EXISTS ${qualifiedTable};`);
278
- await get().db.refreshTableSchemas();
338
+ const table = get().db.findTable(tableName) ??
339
+ (await loadTableSchemaByName(tableName));
340
+ throwIfUnresolvedExplicitDatabaseReference(tableName, table);
341
+ const qualifiedTable = table?.table ??
342
+ (isQualifiedTableName(tableName)
343
+ ? get().db.qualifyTableName(tableName)
344
+ : get().db.qualifyTableName(parseTableReferenceParts(tableName)));
345
+ if (table?.isView) {
346
+ throw new Error(`"${getRawSqlTableReference(qualifiedTable)}" is a view. Use dropRelation() to remove views.`);
347
+ }
348
+ await connector.query(`DROP TABLE IF EXISTS ${getRawSqlTableReference(qualifiedTable)};`);
349
+ get().db.refreshTableSchemas();
279
350
  },
280
351
  async addTable(tableName, data) {
281
- const qualifiedName = isQualifiedTableName(tableName)
282
- ? tableName
283
- : makeQualifiedTableName({ table: tableName });
352
+ const qualifiedName = get().db.qualifyTableName(tableName);
284
353
  const { db } = get();
354
+ const tableReference = getRawSqlTableReference(qualifiedName);
285
355
  if (data instanceof arrow.Table) {
286
356
  // TODO: make sure the table is replaced
287
- await db.connector.loadArrow(data, qualifiedName.toString());
357
+ await db.connector.loadArrow(data, tableReference);
288
358
  }
289
359
  else {
290
- await db.connector.loadObjects(data, qualifiedName.toString(), {
360
+ await db.connector.loadObjects(data, tableReference, {
291
361
  replace: true,
292
362
  });
293
363
  }
294
- const newTable = (await db.loadTableSchemas(qualifiedName))[0];
364
+ const newTable = await loadTableSchemaByName(qualifiedName);
295
365
  if (!newTable) {
296
366
  throw new Error('Failed to add table');
297
367
  }
298
368
  set((state) => produce(state, (draft) => {
299
369
  draft.db.tables.push(newTable);
300
370
  }));
301
- await get().db.refreshTableSchemas();
371
+ get().db.refreshTableSchemas();
302
372
  return newTable;
303
373
  },
304
374
  async setTableRowCount(tableName, rowCount) {
305
- const qualifiedName = isQualifiedTableName(tableName)
306
- ? tableName
307
- : makeQualifiedTableName({ table: tableName });
375
+ const qualifiedName = get().db.qualifyTableName(tableName);
308
376
  set((state) => produce(state, (draft) => {
309
- draft.db.tableRowCounts[qualifiedName.toString()] = rowCount;
377
+ draft.db.tableRowCounts[getTableIdentity(qualifiedName)] =
378
+ rowCount;
310
379
  }));
311
380
  },
312
381
  getTable(tableName) {
313
- return get().db.findTableByName(tableName);
382
+ return get().db.findTable(tableName);
314
383
  },
315
- findTableByName(tableName) {
316
- const { table, schema, database } = {
317
- schema: get().db.currentSchema,
318
- database: get().db.currentDatabase,
319
- ...(typeof tableName === 'string'
320
- ? { table: tableName }
321
- : tableName),
384
+ findTable(tableName) {
385
+ const { currentSchema, currentDatabase, tables } = get().db;
386
+ const findMatchingTable = ({ table, schema, database, }) => table
387
+ ? tables.find((t) => t.table.table === table &&
388
+ (!schema || t.table.schema === schema) &&
389
+ (!database || t.table.database === database))
390
+ : undefined;
391
+ const findUniqueMatchingTable = ({ table, schema, }) => {
392
+ if (!table)
393
+ return undefined;
394
+ const matches = tables.filter((t) => t.table.table === table &&
395
+ (!schema || t.table.schema === schema));
396
+ return matches.length === 1 ? matches[0] : undefined;
397
+ };
398
+ const resolvedTableName = typeof tableName === 'string'
399
+ ? (parseQualifiedSqlIdentifier(tableName) ?? {})
400
+ : tableName;
401
+ const { table, schema, database, defaultDatabase } = {
402
+ schema: currentSchema,
403
+ database: currentDatabase,
404
+ ...resolvedTableName,
322
405
  };
323
- return get().db.tables.find((t) => t.table.table === table &&
324
- (!schema || t.table.schema === schema) &&
325
- (!database || t.table.database === database));
406
+ const exactMatch = findMatchingTable({ table, schema, database });
407
+ if (exactMatch)
408
+ return exactMatch;
409
+ const isStaleDefaultDatabaseReference = typeof tableName !== 'string' &&
410
+ database &&
411
+ defaultDatabase &&
412
+ database === defaultDatabase &&
413
+ database !== currentDatabase;
414
+ if (isStaleDefaultDatabaseReference) {
415
+ return findUniqueMatchingTable({ table, schema });
416
+ }
417
+ return undefined;
418
+ },
419
+ findTableByName(tableName) {
420
+ return get().db.findTable(tableName);
326
421
  },
327
422
  async refreshTableSchemas() {
423
+ if (refreshPromise) {
424
+ pendingSchemaRefresh = true;
425
+ return refreshPromise;
426
+ }
328
427
  set((state) => produce(state, (draft) => {
329
428
  draft.db.isRefreshingTableSchemas = true;
330
429
  }));
331
- try {
332
- const connector = await get().db.getConnector();
333
- const result = await connector.query(`SELECT current_schema() AS schema, current_database() AS database`);
334
- set((state) => produce(state, (draft) => {
335
- draft.db.currentSchema = result.getChild('schema')?.get(0);
336
- draft.db.currentDatabase = result
337
- .getChild('database')
338
- ?.get(0);
339
- }));
340
- const newTables = await get().db.loadTableSchemas();
341
- // Only update if there's an actual change in the schemas
342
- if (!deepEquals(newTables, get().db.tables)) {
430
+ refreshPromise = (async () => {
431
+ try {
432
+ let schemasWithTables = [];
433
+ do {
434
+ pendingSchemaRefresh = false;
435
+ const connector = await get().db.getConnector();
436
+ const result = await connector.query(`SELECT current_schema() AS schema, current_database() AS database`);
437
+ const currentSchemaValue = result.getChild('schema')?.get(0);
438
+ const currentDatabaseValue = result
439
+ .getChild('database')
440
+ ?.get(0);
441
+ const currentSchema = currentSchemaValue == null
442
+ ? undefined
443
+ : String(currentSchemaValue);
444
+ const currentDatabase = currentDatabaseValue == null
445
+ ? undefined
446
+ : String(currentDatabaseValue);
447
+ set((state) => produce(state, (draft) => {
448
+ draft.db.currentSchema = currentSchema;
449
+ draft.db.currentDatabase = currentDatabase;
450
+ }));
451
+ schemasWithTables = await loadSchemaCatalog(connector, {
452
+ filterFunction: effectiveSchemaCatalogFilter,
453
+ defaultDatabase: currentDatabase,
454
+ });
455
+ } while (pendingSchemaRefresh);
456
+ const newTables = schemasWithTables.flatMap((s) => s.tables);
457
+ const currentTables = get().db.tables;
458
+ const currentSchemaTrees = get().db.schemaTrees;
459
+ const newSchemaTrees = createDbSchemaTrees(schemasWithTables);
460
+ if (!deepEquals(newTables, currentTables) ||
461
+ !deepEquals(newSchemaTrees, currentSchemaTrees)) {
462
+ set((state) => produce(state, (draft) => {
463
+ draft.db.tables = newTables;
464
+ draft.db.schemaTrees = newSchemaTrees;
465
+ }));
466
+ }
467
+ return newTables;
468
+ }
469
+ catch (err) {
470
+ get().room.captureException(err);
471
+ return [];
472
+ }
473
+ finally {
474
+ refreshPromise = null;
343
475
  set((state) => produce(state, (draft) => {
344
- draft.db.tables = newTables;
345
- draft.db.schemaTrees = createDbSchemaTrees(newTables);
476
+ draft.db.isRefreshingTableSchemas = false;
346
477
  }));
347
478
  }
348
- return newTables;
349
- }
350
- catch (err) {
351
- get().room.captureException(err);
352
- return [];
353
- }
354
- finally {
355
- set((state) => produce(state, (draft) => {
356
- draft.db.isRefreshingTableSchemas = false;
357
- }));
358
- }
479
+ })();
480
+ return refreshPromise;
359
481
  },
360
482
  async sqlSelectToJson(sql) {
361
483
  const connector = await get().db.getConnector();
@@ -378,7 +500,12 @@ export function createDuckDbSlice({ connector = createWasmDuckDbConnector(), } =
378
500
  set((state) => produce(state, (draft) => {
379
501
  draft.db.queryCache[queryKey] = queryHandle;
380
502
  }));
381
- queryHandle.result.finally(() => {
503
+ queryHandle.result
504
+ .catch(() => {
505
+ // Prevent unhandled promise rejection warnings.
506
+ // Callers handle errors via await/try-catch.
507
+ })
508
+ .finally(() => {
382
509
  // remove from cache after completion
383
510
  set((state) => produce(state, (draft) => {
384
511
  delete draft.db.queryCache[queryKey];
@@ -415,10 +542,18 @@ function createDuckDbCommands() {
415
542
  },
416
543
  {
417
544
  id: 'db.drop-table',
418
- name: 'Drop table',
419
- description: 'Drop a table from DuckDB by name',
545
+ name: 'Drop relation',
546
+ description: 'Drop a table or view from DuckDB by name',
420
547
  group: 'Database',
421
- keywords: ['duckdb', 'database', 'drop', 'table', 'delete'],
548
+ keywords: [
549
+ 'duckdb',
550
+ 'database',
551
+ 'drop',
552
+ 'table',
553
+ 'view',
554
+ 'relation',
555
+ 'delete',
556
+ ],
422
557
  inputSchema: DropTableCommandInput,
423
558
  inputDescription: 'Provide a tableName to remove from DuckDB.',
424
559
  metadata: {
@@ -436,11 +571,11 @@ function createDuckDbCommands() {
436
571
  },
437
572
  execute: async ({ getState }, input) => {
438
573
  const { tableName } = input;
439
- await getState().db.dropTable(tableName);
574
+ await getState().db.dropRelation(tableName);
440
575
  return {
441
576
  success: true,
442
577
  commandId: 'db.drop-table',
443
- message: `Dropped table "${tableName}".`,
578
+ message: `Dropped relation "${tableName}".`,
444
579
  };
445
580
  },
446
581
  },