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

visgl / loaders.gl / 24793222805

22 Apr 2026 05:38PM UTC coverage: 59.007% (+1.9%) from 57.099%
24793222805

push

github

web-flow
chore: Run full tests on node (#3397)

11088 of 20586 branches covered (53.86%)

Branch coverage included in aggregate %.

22864 of 36953 relevant lines covered (61.87%)

16255.47 hits per line

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

49.25
/modules/sql/src/snowflake-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

8
import {SQLDataSource, SQL_SOURCE_DEFAULT_OPTIONS} from './sql-source';
9
import type {
10
  SQLAdapter,
11
  SQLAdapterFactoryContext,
12
  SQLQueryOptions,
13
  SQLSourceOptions,
14
  SQLTableInfo
15
} from './sql-types';
16
import {
17
  convertRowsToArrowTable,
18
  convertSQLColumnsToSchema,
19
  getQualifiedTableName
20
} from './sql-utils';
21

22
// __VERSION__ is injected by babel-plugin-version-inline
23
// @ts-ignore TS2304: Cannot find name '__VERSION__'.
24
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
5!
25

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

72
/** SQLDataSource specialization for Snowflake. */
73
export class SnowflakeSQLDataSource extends SQLDataSource {
74
  constructor(data: string, options: SQLSourceOptions, coreApi?: CoreAPI) {
75
    super(
4✔
76
      data,
77
      options,
78
      SnowflakeSQLSource.type,
79
      SQL_SOURCE_DEFAULT_OPTIONS,
80
      createSnowflakeAdapter,
81
      coreApi
82
    );
83
  }
84
}
85

86
async function createSnowflakeAdapter(context: SQLAdapterFactoryContext): Promise<SQLAdapter> {
87
  const baseStatementUrl = getSnowflakeStatementsUrl(context.url, context.options);
2✔
88
  const authorization = context.options.snowflake?.token;
2✔
89
  if (!authorization) {
2!
90
    throw new Error('SnowflakeSQLSource requires snowflake.token.');
×
91
  }
92

93
  const executeRows = async (
2✔
94
    sqlText: string,
95
    options: SQLQueryOptions = {}
4✔
96
  ): Promise<Record<string, unknown>[]> => {
97
    const initialResponse = await executeSnowflakeStatement(
4✔
98
      context,
99
      baseStatementUrl,
100
      authorization,
101
      sqlText,
102
      options
103
    );
104
    const responses = [initialResponse];
4✔
105

106
    const partitionInfo = initialResponse.resultSetMetaData?.partitionInfo || [];
4!
107
    for (let partitionIndex = 1; partitionIndex < partitionInfo.length; partitionIndex++) {
4✔
108
      responses.push(
4✔
109
        await fetchSnowflakeJson(
110
          context,
111
          `${baseStatementUrl}/${initialResponse.statementHandle}?partition=${partitionIndex}`,
112
          authorization,
113
          {method: 'GET', signal: options.signal}
114
        )
115
      );
116
    }
117

118
    const rowType = initialResponse.resultSetMetaData?.rowType || [];
4!
119
    const rowNames = rowType.map((row: {name: string}) => row.name);
8✔
120
    return responses.flatMap((response: {data?: unknown[][]}) =>
8✔
121
      (response.data || []).map((values: unknown[]) => {
8!
122
        const row: Record<string, unknown> = {};
8✔
123
        rowNames.forEach((name, index) => {
8✔
124
          row[name] = values[index];
16✔
125
        });
126
        return row;
8✔
127
      })
128
    );
129
  };
130

131
  return {
2✔
132
    capabilities: {
133
      supportsArrow: false,
134
      supportsMetadata: true,
135
      runtime: 'both',
136
      isDynamic: false
137
    },
138
    async connect(): Promise<void> {
139
      if (!authorization) {
2!
140
        throw new Error('SnowflakeSQLSource requires snowflake.token.');
×
141
      }
142
    },
143
    async close(): Promise<void> {},
144
    async listCatalogs() {
145
      const rows = await executeRows('SHOW DATABASES');
×
146
      return rows.map(row => ({
×
147
        catalogName: String(row.name || row.NAME)
×
148
      }));
149
    },
150
    async listSchemas() {
151
      const rows = await executeRows('SHOW SCHEMAS');
×
152
      return rows.map(row => ({
×
153
        catalogName: row.database_name ? String(row.database_name) : undefined,
×
154
        schemaName: String(row.name || row.NAME)
×
155
      }));
156
    },
157
    async listTables(): Promise<SQLTableInfo[]> {
158
      const rows = await executeRows('SHOW TABLES');
×
159
      return rows.map(row => ({
×
160
        catalogName: row.database_name ? String(row.database_name) : undefined,
×
161
        schemaName: row.schema_name ? String(row.schema_name) : undefined,
×
162
        tableName: String(row.name || row.NAME),
×
163
        tableType: row.kind ? String(row.kind) : undefined
×
164
      }));
165
    },
166
    async getTableSchema(options: {
167
      catalogName?: string;
168
      schemaName?: string;
169
      tableName: string;
170
    }): Promise<Schema> {
171
      const qualifiedTableName = getQualifiedTableName(options);
×
172
      const rows = await executeRows(`DESCRIBE TABLE ${qualifiedTableName}`);
×
173
      return convertSQLColumnsToSchema(
×
174
        rows.map((row, index) => ({
×
175
          columnName: String(row.name || row.NAME || `column_${index + 1}`),
×
176
          sqlType: String(row.type || row.TYPE || 'VARCHAR'),
×
177
          nullable: String(row.null || row.NULL || '').toUpperCase() === 'Y',
×
178
          ordinalPosition: index + 1
179
        }))
180
      );
181
    },
182
    async executeRows(
183
      sqlText: string,
184
      options: SQLQueryOptions = {}
2✔
185
    ): Promise<Record<string, unknown>[]> {
186
      return await executeRows(sqlText, options);
2✔
187
    },
188
    async executeArrow(sqlText: string, options: SQLQueryOptions = {}) {
2✔
189
      const rows = await executeRows(sqlText, options);
2✔
190
      return convertRowsToArrowTable(rows);
2✔
191
    }
192
  };
193
}
194

195
async function executeSnowflakeStatement(
196
  context: SQLAdapterFactoryContext,
197
  baseStatementUrl: string,
198
  authorization: string,
199
  sqlText: string,
200
  options: SQLQueryOptions
201
): Promise<any> {
202
  let response = await fetchSnowflakeJson(context, baseStatementUrl, authorization, {
4✔
203
    method: 'POST',
204
    headers: {
205
      'Content-Type': 'application/json'
206
    },
207
    body: JSON.stringify({
208
      statement: sqlText,
209
      timeout: 60,
210
      bindings: normalizeSnowflakeBindings(options.parameters),
211
      warehouse: context.options.snowflake?.warehouse,
212
      database: context.options.snowflake?.database,
213
      schema: context.options.snowflake?.schema,
214
      role: context.options.snowflake?.role
215
    }),
216
    signal: options.signal
217
  });
218

219
  while (response?.statementStatusUrl && !response?.data) {
4!
220
    const pollUrl = `${getSnowflakeBaseUrl(context.url, context.options)}${response.statementStatusUrl}`;
×
221
    response = await fetchSnowflakeJson(context, pollUrl, authorization, {
×
222
      method: 'GET',
223
      signal: options.signal
224
    });
225
  }
226

227
  if (response?.code || response?.message) {
4!
228
    throw new Error(response.message || `Snowflake SQL API error ${response.code}`);
×
229
  }
230

231
  return response;
4✔
232
}
233

234
async function fetchSnowflakeJson(
235
  context: SQLAdapterFactoryContext,
236
  url: string,
237
  authorization: string,
238
  requestInit: RequestInit
239
): Promise<any> {
240
  const customFetch = context.options.core?.loadOptions?.core?.fetch;
8✔
241
  const headers = new Headers(requestInit.headers);
8✔
242
  headers.set(
8✔
243
    'Authorization',
244
    authorization.startsWith('Bearer ') ? authorization : `Bearer ${authorization}`
8!
245
  );
246
  headers.set('Accept', 'application/json');
8✔
247

248
  const response =
249
    typeof customFetch === 'function'
8!
250
      ? await customFetch(url, {
251
          ...requestInit,
252
          headers
253
        })
254
      : await context.coreApi.fetchFile(url, {
255
          ...requestInit,
256
          headers
257
        });
258
  const json = await response.json();
×
259
  if (!response.ok) {
8!
260
    throw new Error(json?.message || `${response.status} ${response.statusText}`);
×
261
  }
262
  return json;
8✔
263
}
264

265
function normalizeSnowflakeBindings(
266
  parameters: SQLQueryOptions['parameters']
267
): Record<string, {type: string; value: unknown}> | undefined {
268
  if (!parameters || Array.isArray(parameters)) {
4!
269
    return undefined;
4✔
270
  }
271
  const bindings: Record<string, {type: string; value: unknown}> = {};
×
272
  for (const [key, value] of Object.entries(parameters)) {
×
273
    bindings[key] = {
×
274
      type: typeof value === 'number' ? 'FIXED' : 'TEXT',
×
275
      value
276
    };
277
  }
278
  return bindings;
×
279
}
280

281
function getSnowflakeStatementsUrl(url: string, options: SQLSourceOptions): string {
282
  return `${getSnowflakeBaseUrl(url, options)}/api/v2/statements`;
2✔
283
}
284

285
function getSnowflakeBaseUrl(url: string, options: SQLSourceOptions): string {
286
  if (/^https?:\/\//i.test(url)) {
2!
287
    return url.replace(/\/api\/v2\/statements.*$/, '');
×
288
  }
289

290
  const parsedUrl = new URL(url);
2✔
291
  const account = options.snowflake?.account || parsedUrl.hostname;
2✔
292
  if (!account) {
2!
293
    throw new Error('SnowflakeSQLSource requires a Snowflake account URL or snowflake.account.');
×
294
  }
295
  return `https://${account}.snowflakecomputing.com`;
2✔
296
}
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