@sqlrooms/duckdb 0.29.0-rc.1 → 0.29.0-rc.10
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/LICENSE.md +2 -1
- package/README.md +84 -4
- package/dist/DuckDbSlice.d.ts +56 -14
- package/dist/DuckDbSlice.d.ts.map +1 -1
- package/dist/DuckDbSlice.js +299 -164
- package/dist/DuckDbSlice.js.map +1 -1
- package/dist/connectors/WasmDuckDbConnector.d.ts.map +1 -1
- package/dist/connectors/WasmDuckDbConnector.js +27 -0
- package/dist/connectors/WasmDuckDbConnector.js.map +1 -1
- package/dist/connectors/WebSocketDuckDbConnector.d.ts +23 -0
- package/dist/connectors/WebSocketDuckDbConnector.d.ts.map +1 -1
- package/dist/connectors/WebSocketDuckDbConnector.js +62 -0
- package/dist/connectors/WebSocketDuckDbConnector.js.map +1 -1
- package/dist/connectors/createDuckDbConnector.d.ts +1 -1
- package/dist/connectors/createDuckDbConnector.d.ts.map +1 -1
- package/dist/connectors/createDuckDbConnector.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/loadTableSchemas.d.ts +40 -0
- package/dist/loadTableSchemas.d.ts.map +1 -0
- package/dist/loadTableSchemas.js +260 -0
- package/dist/loadTableSchemas.js.map +1 -0
- package/dist/use-copy-as-tsv.d.ts +13 -0
- package/dist/use-copy-as-tsv.d.ts.map +1 -0
- package/dist/use-copy-as-tsv.js +68 -0
- package/dist/use-copy-as-tsv.js.map +1 -0
- package/dist/use-export-to-csv.d.ts +5 -0
- package/dist/use-export-to-csv.d.ts.map +1 -0
- package/dist/{exportToCsv.js → use-export-to-csv.js} +1 -1
- package/dist/use-export-to-csv.js.map +1 -0
- package/dist/useDataTable.d.ts +14 -0
- package/dist/useDataTable.d.ts.map +1 -0
- package/dist/useDataTable.js +21 -0
- package/dist/useDataTable.js.map +1 -0
- package/package.json +27 -27
- package/dist/exportToCsv.d.ts +0 -4
- package/dist/exportToCsv.d.ts.map +0 -1
- package/dist/exportToCsv.js.map +0 -1
package/dist/DuckDbSlice.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
?
|
|
161
|
-
: tableName
|
|
287
|
+
? get().db.qualifyTableName(parseTableReferenceParts(tableName))
|
|
288
|
+
: get().db.qualifyTableName(tableName);
|
|
162
289
|
const connector = await get().db.getConnector();
|
|
163
|
-
const
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
334
|
+
get().db.refreshTableSchemas();
|
|
271
335
|
},
|
|
272
336
|
async dropTable(tableName) {
|
|
273
337
|
const connector = await get().db.getConnector();
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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 =
|
|
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,
|
|
357
|
+
await db.connector.loadArrow(data, tableReference);
|
|
288
358
|
}
|
|
289
359
|
else {
|
|
290
|
-
await db.connector.loadObjects(data,
|
|
360
|
+
await db.connector.loadObjects(data, tableReference, {
|
|
291
361
|
replace: true,
|
|
292
362
|
});
|
|
293
363
|
}
|
|
294
|
-
const newTable =
|
|
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
|
-
|
|
371
|
+
get().db.refreshTableSchemas();
|
|
302
372
|
return newTable;
|
|
303
373
|
},
|
|
304
374
|
async setTableRowCount(tableName, rowCount) {
|
|
305
|
-
const qualifiedName =
|
|
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
|
|
377
|
+
draft.db.tableRowCounts[getTableIdentity(qualifiedName)] =
|
|
378
|
+
rowCount;
|
|
310
379
|
}));
|
|
311
380
|
},
|
|
312
381
|
getTable(tableName) {
|
|
313
|
-
return get().db.
|
|
382
|
+
return get().db.findTable(tableName);
|
|
314
383
|
},
|
|
315
|
-
|
|
316
|
-
const {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
.
|
|
338
|
-
?.get(0);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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.
|
|
345
|
-
draft.db.schemaTrees = createDbSchemaTrees(newTables);
|
|
476
|
+
draft.db.isRefreshingTableSchemas = false;
|
|
346
477
|
}));
|
|
347
478
|
}
|
|
348
|
-
|
|
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
|
|
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
|
|
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: [
|
|
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.
|
|
574
|
+
await getState().db.dropRelation(tableName);
|
|
440
575
|
return {
|
|
441
576
|
success: true,
|
|
442
577
|
commandId: 'db.drop-table',
|
|
443
|
-
message: `Dropped
|
|
578
|
+
message: `Dropped relation "${tableName}".`,
|
|
444
579
|
};
|
|
445
580
|
},
|
|
446
581
|
},
|