• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / loaders.gl / 25798238260

13 May 2026 12:10PM UTC coverage: 60.607% (+0.3%) from 60.27%
25798238260

push

github

web-flow
feat(json) GeoJSON -> geoarrow, schema, logging  (#3399)

13466 of 24516 branches covered (54.93%)

Branch coverage included in aggregate %.

448 of 541 new or added lines in 12 files covered. (82.81%)

1264 existing lines in 117 files now uncovered.

27516 of 43103 relevant lines covered (63.84%)

15056.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.41
/modules/sql/src/duckdb-sql-source.ts
1
// loaders.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {Schema} from '@loaders.gl/schema';
6
import type {SourceLoader, CoreAPI} from '@loaders.gl/loader-utils';
7
import {convertArrowToTable} from '@loaders.gl/schema-utils';
8

9
import {SQLDataSource, SQL_SOURCE_DEFAULT_OPTIONS} from './sql-source';
10
import type {
11
  SQLAdapter,
12
  SQLAdapterFactoryContext,
13
  SQLCatalogInfo,
14
  SQLColumnInfo,
15
  SQLQueryOptions,
16
  SQLSourceOptions,
17
  SQLTableInfo
18
} from './sql-types';
19
import {
20
  convertRowsToArrowTable,
21
  convertSQLColumnsToSchema,
22
  escapeSqlString,
23
  isNodeRuntime
24
} from './sql-utils';
25

26
// __VERSION__ is injected by babel-plugin-version-inline
27
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
28
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
5!
29

30
/** DuckDB-backed SQL source. */
31
export const DuckDBSQLSource = {
5✔
32
  dataType: null as unknown as DuckDBSQLDataSource,
33
  batchType: null as never,
34
  name: 'DuckDBSQLSource',
35
  id: 'duckdb-sql',
36
  module: 'sql',
37
  version: VERSION,
38
  extensions: ['duckdb', 'db'],
39
  mimeTypes: [],
40
  type: 'duckdb-sql',
41
  fromUrl: true,
42
  fromBlob: false,
43
  options: {
44
    sql: {
45
      resultFormat: 'auto',
46
      adapterOptions: {},
47
      browserAdapterFactory: undefined,
48
      nodeAdapterFactory: undefined
49
    },
50
    duckdb: {
51
      accessMode: 'auto',
52
      bundles: undefined,
53
      databasePath: undefined,
54
      remoteUrl: undefined,
55
      workerUrl: undefined
56
    },
57
    snowflake: {
58
      account: undefined,
59
      database: undefined,
60
      schema: undefined,
61
      token: undefined,
62
      warehouse: undefined,
63
      role: undefined,
64
      authenticator: undefined
65
    }
66
  },
67
  defaultOptions: SQL_SOURCE_DEFAULT_OPTIONS,
68
  testURL: (url: string): boolean => /^duckdb:\/\//i.test(url),
×
69
  createDataSource: (
70
    data: string,
71
    options: SQLSourceOptions,
72
    coreApi?: CoreAPI
UNCOV
73
  ): DuckDBSQLDataSource => new DuckDBSQLDataSource(data, options, coreApi)
2✔
74
} as const satisfies SourceLoader<DuckDBSQLDataSource>;
75

76
/** SQLDataSource specialization for DuckDB. */
77
export class DuckDBSQLDataSource extends SQLDataSource {
78
  constructor(data: string, options: SQLSourceOptions, coreApi?: CoreAPI) {
UNCOV
79
    super(
2✔
80
      data,
81
      options,
82
      DuckDBSQLSource.type,
83
      SQL_SOURCE_DEFAULT_OPTIONS,
84
      createDuckDBAdapter,
85
      coreApi
86
    );
87
  }
88
}
89

90
async function createDuckDBAdapter(context: SQLAdapterFactoryContext): Promise<SQLAdapter> {
UNCOV
91
  if (context.options.duckdb?.remoteUrl) {
1!
92
    throw new Error('Remote DuckDB adapters are not implemented yet.');
×
93
  }
UNCOV
94
  if (isNodeRuntime()) {
1!
UNCOV
95
    return await createDuckDBNodeAdapter(context);
1✔
96
  }
97
  return await createDuckDBBrowserAdapter(context);
×
98
}
99

100
async function createDuckDBNodeAdapter(context: SQLAdapterFactoryContext): Promise<SQLAdapter> {
UNCOV
101
  const duckdb = await import('@duckdb/node-api');
1✔
102

UNCOV
103
  const databasePath = getDuckDBDatabasePath(context.url, context.options);
1✔
104
  const configuration =
UNCOV
105
    context.options.duckdb?.accessMode && context.options.duckdb.accessMode !== 'auto'
1!
106
      ? {access_mode: context.options.duckdb.accessMode}
107
      : undefined;
108

109
  let connection: any;
110

UNCOV
111
  const executeRows = async (
1✔
112
    sqlText: string,
113
    options: SQLQueryOptions = {}
5✔
114
  ): Promise<Record<string, unknown>[]> => {
UNCOV
115
    if (!connection) {
5!
116
      const instance = await duckdb.DuckDBInstance.create(databasePath, configuration);
×
117
      connection = await instance.connect();
×
118
    }
UNCOV
119
    const reader = await connection.runAndReadAll(sqlText, options.parameters);
5✔
UNCOV
120
    return reader.getRowObjectsJson();
5✔
121
  };
122

UNCOV
123
  return {
1✔
124
    capabilities: {
125
      supportsArrow: true,
126
      supportsMetadata: true,
127
      runtime: 'node',
128
      isDynamic: true
129
    },
130
    async connect(): Promise<void> {
UNCOV
131
      if (!connection) {
1!
UNCOV
132
        const instance = await duckdb.DuckDBInstance.create(databasePath, configuration);
1✔
UNCOV
133
        connection = await instance.connect();
1✔
134
      }
135
    },
136
    async close(): Promise<void> {
UNCOV
137
      if (connection?.closeSync) {
1!
UNCOV
138
        connection.closeSync();
1✔
139
      }
UNCOV
140
      connection = null;
1✔
141
    },
142
    async listCatalogs(): Promise<SQLCatalogInfo[]> {
143
      const rows = await executeRows(`
×
144
        SELECT DISTINCT catalog_name
145
        FROM information_schema.schemata
146
        ORDER BY catalog_name
147
      `);
148
      return rows.map(row => ({catalogName: String(row.catalog_name)}));
×
149
    },
150
    async listSchemas(catalogName?: string) {
151
      const filter = catalogName ? `WHERE catalog_name = '${escapeSqlString(catalogName)}'` : '';
×
152
      const rows = await executeRows(`
×
153
        SELECT catalog_name, schema_name
154
        FROM information_schema.schemata
155
        ${filter}
156
        ORDER BY catalog_name, schema_name
157
      `);
158
      return rows.map(row => ({
×
159
        catalogName: row.catalog_name ? String(row.catalog_name) : undefined,
×
160
        schemaName: String(row.schema_name)
161
      }));
162
    },
163
    async listTables(options?: {
164
      catalogName?: string;
165
      schemaName?: string;
166
    }): Promise<SQLTableInfo[]> {
UNCOV
167
      const filters = [
1✔
168
        options?.catalogName
1!
169
          ? `table_catalog = '${escapeSqlString(options.catalogName)}'`
170
          : undefined,
171
        options?.schemaName ? `table_schema = '${escapeSqlString(options.schemaName)}'` : undefined,
1!
172
        `table_schema NOT IN ('information_schema', 'pg_catalog')`
173
      ].filter(Boolean);
UNCOV
174
      const whereClause = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
1!
UNCOV
175
      const rows = await executeRows(`
1✔
176
        SELECT table_catalog, table_schema, table_name, table_type
177
        FROM information_schema.tables
178
        ${whereClause}
179
        ORDER BY table_catalog, table_schema, table_name
180
      `);
UNCOV
181
      return rows.map(row => ({
1✔
182
        catalogName: row.table_catalog ? String(row.table_catalog) : undefined,
1!
183
        schemaName: row.table_schema ? String(row.table_schema) : undefined,
1!
184
        tableName: String(row.table_name),
185
        tableType: row.table_type ? String(row.table_type) : undefined
1!
186
      }));
187
    },
188
    async getTableSchema(options: {
189
      catalogName?: string;
190
      schemaName?: string;
191
      tableName: string;
192
    }): Promise<Schema> {
UNCOV
193
      const filters = [
1✔
194
        options.catalogName
1!
195
          ? `table_catalog = '${escapeSqlString(options.catalogName)}'`
196
          : undefined,
197
        options.schemaName ? `table_schema = '${escapeSqlString(options.schemaName)}'` : undefined,
1!
198
        `table_name = '${escapeSqlString(options.tableName)}'`
199
      ].filter(Boolean);
UNCOV
200
      const rows = await executeRows(`
1✔
201
        SELECT table_catalog, table_schema, table_name, column_name, data_type, is_nullable, ordinal_position
202
        FROM information_schema.columns
203
        WHERE ${filters.join(' AND ')}
204
        ORDER BY ordinal_position
205
      `);
UNCOV
206
      const columns = rows.map(
1✔
207
        row =>
UNCOV
208
          ({
1✔
209
            catalogName: row.table_catalog ? String(row.table_catalog) : undefined,
1!
210
            schemaName: row.table_schema ? String(row.table_schema) : undefined,
1!
211
            tableName: row.table_name ? String(row.table_name) : undefined,
1!
212
            columnName: String(row.column_name),
213
            sqlType: String(row.data_type),
214
            nullable: row.is_nullable === 'YES',
215
            ordinalPosition: Number(row.ordinal_position)
216
          }) satisfies SQLColumnInfo
217
      );
UNCOV
218
      return convertSQLColumnsToSchema(columns);
1✔
219
    },
220
    async executeRows(
221
      sqlText: string,
222
      options: SQLQueryOptions = {}
2✔
223
    ): Promise<Record<string, unknown>[]> {
UNCOV
224
      return await executeRows(sqlText, options);
2✔
225
    },
226
    async executeArrow(sqlText: string, options: SQLQueryOptions = {}) {
1✔
UNCOV
227
      const rows = await executeRows(sqlText, options);
1✔
UNCOV
228
      return convertRowsToArrowTable(rows);
1✔
229
    }
230
  };
231
}
232

233
async function createDuckDBBrowserAdapter(context: SQLAdapterFactoryContext): Promise<SQLAdapter> {
234
  const duckdb = await import('@duckdb/duckdb-wasm');
×
235

236
  const logger = new duckdb.ConsoleLogger();
×
237
  const selectedBundles = await duckdb.selectBundle(duckdb.getJsDelivrBundles());
×
238
  const bundles = {
×
239
    ...selectedBundles,
240
    ...context.options.duckdb?.bundles
241
  };
242
  const workerUrl = context.options.duckdb?.workerUrl || selectedBundles.mainWorker;
×
243
  if (!workerUrl) {
×
244
    throw new Error('DuckDBSQLSource could not resolve a worker bundle URL.');
×
245
  }
246
  const worker = await duckdb.createWorker(workerUrl);
×
247
  const database = new duckdb.AsyncDuckDB(logger, worker);
×
248
  await database.instantiate(bundles.mainModule, bundles.pthreadWorker);
×
249
  const connection = await database.connect();
×
250

251
  const executeRows = async (sqlText: string): Promise<Record<string, unknown>[]> => {
×
252
    const result = await connection.query(sqlText);
×
253
    return convertArrowToTable(result, 'object-row-table').data;
×
254
  };
255

256
  return {
×
257
    capabilities: {
258
      supportsArrow: true,
259
      supportsMetadata: true,
260
      runtime: 'browser',
261
      isDynamic: true
262
    },
263
    async connect(): Promise<void> {},
264
    async close(): Promise<void> {
265
      await connection.close();
×
266
      await database.terminate();
×
267
      worker.terminate();
×
268
    },
269
    async listCatalogs(): Promise<SQLCatalogInfo[]> {
270
      const rows = await executeRows(`
×
271
        SELECT DISTINCT catalog_name
272
        FROM information_schema.schemata
273
        ORDER BY catalog_name
274
      `);
275
      return rows.map(row => ({catalogName: String(row.catalog_name)}));
×
276
    },
277
    async listSchemas(catalogName?: string) {
278
      const filter = catalogName ? `WHERE catalog_name = '${escapeSqlString(catalogName)}'` : '';
×
279
      const rows = await executeRows(`
×
280
        SELECT catalog_name, schema_name
281
        FROM information_schema.schemata
282
        ${filter}
283
        ORDER BY catalog_name, schema_name
284
      `);
285
      return rows.map(row => ({
×
286
        catalogName: row.catalog_name ? String(row.catalog_name) : undefined,
×
287
        schemaName: String(row.schema_name)
288
      }));
289
    },
290
    async listTables(options?: {
291
      catalogName?: string;
292
      schemaName?: string;
293
    }): Promise<SQLTableInfo[]> {
294
      const filters = [
×
295
        options?.catalogName
×
296
          ? `table_catalog = '${escapeSqlString(options.catalogName)}'`
297
          : undefined,
298
        options?.schemaName ? `table_schema = '${escapeSqlString(options.schemaName)}'` : undefined,
×
299
        `table_schema NOT IN ('information_schema', 'pg_catalog')`
300
      ].filter(Boolean);
301
      const whereClause = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
×
302
      const rows = await executeRows(`
×
303
        SELECT table_catalog, table_schema, table_name, table_type
304
        FROM information_schema.tables
305
        ${whereClause}
306
        ORDER BY table_catalog, table_schema, table_name
307
      `);
308
      return rows.map(row => ({
×
309
        catalogName: row.table_catalog ? String(row.table_catalog) : undefined,
×
310
        schemaName: row.table_schema ? String(row.table_schema) : undefined,
×
311
        tableName: String(row.table_name),
312
        tableType: row.table_type ? String(row.table_type) : undefined
×
313
      }));
314
    },
315
    async getTableSchema(options: {
316
      catalogName?: string;
317
      schemaName?: string;
318
      tableName: string;
319
    }): Promise<Schema> {
320
      const filters = [
×
321
        options.catalogName
×
322
          ? `table_catalog = '${escapeSqlString(options.catalogName)}'`
323
          : undefined,
324
        options.schemaName ? `table_schema = '${escapeSqlString(options.schemaName)}'` : undefined,
×
325
        `table_name = '${escapeSqlString(options.tableName)}'`
326
      ].filter(Boolean);
327
      const rows = await executeRows(`
×
328
        SELECT table_catalog, table_schema, table_name, column_name, data_type, is_nullable, ordinal_position
329
        FROM information_schema.columns
330
        WHERE ${filters.join(' AND ')}
331
        ORDER BY ordinal_position
332
      `);
333
      const columns = rows.map(
×
334
        row =>
335
          ({
×
336
            catalogName: row.table_catalog ? String(row.table_catalog) : undefined,
×
337
            schemaName: row.table_schema ? String(row.table_schema) : undefined,
×
338
            tableName: row.table_name ? String(row.table_name) : undefined,
×
339
            columnName: String(row.column_name),
340
            sqlType: String(row.data_type),
341
            nullable: row.is_nullable === 'YES',
342
            ordinalPosition: Number(row.ordinal_position)
343
          }) satisfies SQLColumnInfo
344
      );
345
      return convertSQLColumnsToSchema(columns);
×
346
    },
347
    async executeRows(sqlText: string): Promise<Record<string, unknown>[]> {
348
      return await executeRows(sqlText);
×
349
    },
350
    async executeArrow(sqlText: string) {
351
      const result = await connection.query(sqlText);
×
352
      return convertArrowToTable(result, 'arrow-table');
×
353
    }
354
  };
355
}
356

357
function getDuckDBDatabasePath(url: string, options: SQLSourceOptions): string {
UNCOV
358
  if (options.duckdb?.databasePath) {
1!
359
    return options.duckdb.databasePath;
×
360
  }
361

UNCOV
362
  const parsedUrl = new URL(url);
1✔
UNCOV
363
  const pathname = decodeURIComponent(parsedUrl.pathname || '');
1!
UNCOV
364
  if (!pathname || pathname === '/') {
1!
365
    return ':memory:';
×
366
  }
UNCOV
367
  return pathname.slice(1) || ':memory:';
1!
368
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc