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

visgl / loaders.gl / 24509568695

16 Apr 2026 12:13PM UTC coverage: 56.692% (+0.03%) from 56.662%
24509568695

push

github

web-flow
feat(sql) New SQL module with SQLDataSource and DuckDB connector (#3389)

10064 of 19243 branches covered (52.3%)

Branch coverage included in aggregate %.

159 of 239 new or added lines in 4 files covered. (66.53%)

20686 of 34997 relevant lines covered (59.11%)

5007.32 hits per line

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

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

5
import * as arrow from 'apache-arrow';
6
import type {ArrowTable, DataType, Field, ObjectRowTable, Schema} from '@loaders.gl/schema';
7
import {makeTableFromData, getDataTypeFromArray} from '@loaders.gl/schema-utils';
8

9
import type {SQLColumnInfo} from './sql-types';
10

11
/** Returns true when the current runtime is Node.js. */
12
export function isNodeRuntime(): boolean {
13
  return typeof process !== 'undefined' && Boolean(process.versions?.node);
6✔
14
}
15

16
/** Escapes a SQL string literal using single-quote doubling. */
17
export function escapeSqlString(value: string): string {
18
  return value.replace(/'/g, "''");
3✔
19
}
20

21
/** Wraps an identifier in double quotes, escaping embedded quotes. */
22
export function quoteSqlIdentifier(identifier: string): string {
23
  return `"${identifier.replace(/"/g, '""')}"`;
4✔
24
}
25

26
/** Converts object rows into a loaders.gl Arrow table. */
27
export function convertRowsToArrowTable(rows: Record<string, unknown>[]): ArrowTable {
28
  const table: ObjectRowTable =
29
    rows.length > 0
5✔
30
      ? makeTableFromData(rows)
31
      : {shape: 'object-row-table', schema: {fields: [], metadata: {}}, data: rows};
32
  const schema = deducePrimitiveAwareSchema(table);
5✔
33

34
  if (table.data.length === 0 || schema.fields.length === 0) {
5✔
35
    return {
1✔
36
      shape: 'arrow-table',
37
      schema,
38
      data: new arrow.Table(new arrow.Schema([]))
39
    };
40
  }
41

42
  return {
4✔
43
    shape: 'arrow-table',
44
    schema,
45
    data: arrow.tableFromJSON(table.data)
46
  };
47
}
48

49
/** Converts normalized SQL columns into a loaders.gl schema. */
50
export function convertSQLColumnsToSchema(columns: SQLColumnInfo[]): Schema {
51
  return {
2✔
52
    metadata: {},
53
    fields: columns
54
      .sort((left, right) => (left.ordinalPosition || 0) - (right.ordinalPosition || 0))
2!
55
      .map(column => ({
4✔
56
        name: column.columnName,
57
        type: mapSQLTypeToDataType(column.sqlType),
58
        nullable: column.nullable,
59
        metadata: {
60
          sqlType: column.sqlType
61
        }
62
      }))
63
  };
64
}
65

66
function mapSQLTypeToDataType(sqlType: string): DataType {
67
  const normalizedType = sqlType.toUpperCase();
4✔
68
  if (
4✔
69
    normalizedType.includes('BIGINT') ||
10✔
70
    normalizedType === 'HUGEINT' ||
71
    normalizedType === 'LONG'
72
  ) {
73
    return 'int64';
1✔
74
  }
75
  if (
3✔
76
    normalizedType.includes('INT') ||
9✔
77
    normalizedType === 'INTEGER' ||
78
    normalizedType === 'SMALLINT' ||
79
    normalizedType === 'TINYINT'
80
  ) {
81
    return 'int32';
1✔
82
  }
83
  if (
2!
84
    normalizedType.includes('DOUBLE') ||
6✔
85
    normalizedType.includes('FLOAT8') ||
86
    normalizedType.includes('REAL')
87
  ) {
NEW
88
    return 'float64';
×
89
  }
90
  if (
2!
91
    normalizedType.includes('FLOAT') ||
6✔
92
    normalizedType.includes('DECIMAL') ||
93
    normalizedType.includes('NUMERIC')
94
  ) {
NEW
95
    return 'float64';
×
96
  }
97
  if (normalizedType.includes('BOOL')) {
2!
NEW
98
    return 'bool';
×
99
  }
100
  if (normalizedType.includes('DATE')) {
2!
NEW
101
    return 'date-day';
×
102
  }
103
  if (normalizedType.includes('TIME') || normalizedType.includes('TIMESTAMP')) {
2✔
104
    return 'timestamp-millisecond';
1✔
105
  }
106
  if (normalizedType.includes('BLOB') || normalizedType.includes('BINARY')) {
1!
107
    return 'binary';
1✔
108
  }
NEW
109
  return 'utf8';
×
110
}
111

112
function deducePrimitiveAwareSchema(table: ObjectRowTable): Schema {
113
  return {
5✔
114
    ...table.schema!,
115
    fields: table.schema!.fields.map((field): Field => {
116
      if (field.type !== 'null') {
5✔
117
        return field;
3✔
118
      }
119

120
      const columnValues = table.data.map(row => row[field.name]);
4✔
121
      const inferredType = getDataTypeFromArray(columnValues);
4✔
122
      return {...field, type: inferredType.type, nullable: inferredType.nullable};
2✔
123
    })
124
  };
125
}
126

127
/** Returns a fully qualified SQL object name. */
128
export function getQualifiedTableName(options: {
129
  catalogName?: string;
130
  schemaName?: string;
131
  tableName: string;
132
}): string {
133
  const identifiers = [options.catalogName, options.schemaName, options.tableName].filter(
1✔
134
    (value): value is string => Boolean(value)
3✔
135
  );
136
  return identifiers.map(identifier => quoteSqlIdentifier(identifier)).join('.');
3✔
137
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc