@dbml/connector 3.7.4 → 3.8.0

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.
@@ -13,6 +13,7 @@ import {
13
13
  Table,
14
14
  TableConstraintsDictionary,
15
15
  } from './types';
16
+ import { parseConnectionString, buildSchemaQuery } from '../utils/parseSchema';
16
17
 
17
18
  const getValidatedClient = async (connection: string): Promise<Client> => {
18
19
  const client = new Client(connection);
@@ -113,7 +114,7 @@ const generateField = (row: Record<string, any>): Field => {
113
114
  };
114
115
  };
115
116
 
116
- const generateTablesAndFields = async (client: Client): Promise<{
117
+ const generateTablesAndFields = async (client: Client, schemas: string[]): Promise<{
117
118
  tables: Table[],
118
119
  fields: FieldsDictionary,
119
120
  }> => {
@@ -167,6 +168,7 @@ const generateTablesAndFields = async (client: Client): Promise<{
167
168
  WHERE
168
169
  t.table_type = 'BASE TABLE'
169
170
  AND t.table_schema NOT IN ('pg_catalog', 'information_schema')
171
+ ${buildSchemaQuery('t.table_schema', schemas)}
170
172
  ORDER BY
171
173
  t.table_schema,
172
174
  t.table_name,
@@ -200,7 +202,7 @@ const generateTablesAndFields = async (client: Client): Promise<{
200
202
  };
201
203
  };
202
204
 
203
- const generateRawRefs = async (client: Client): Promise<Ref[]> => {
205
+ const generateRawRefs = async (client: Client, schemas: string[]): Promise<Ref[]> => {
204
206
  const refs: Ref[] = [];
205
207
 
206
208
  const refsListSql = `
@@ -226,6 +228,7 @@ const generateRawRefs = async (client: Client): Promise<Ref[]> => {
226
228
  AND tc.table_schema = rc.constraint_schema
227
229
  WHERE tc.constraint_type = 'FOREIGN KEY'
228
230
  AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
231
+ ${buildSchemaQuery('tc.table_schema', schemas)}
229
232
  GROUP BY
230
233
  tc.table_schema,
231
234
  tc.table_name,
@@ -279,8 +282,7 @@ const generateRawRefs = async (client: Client): Promise<Ref[]> => {
279
282
  return refs;
280
283
  };
281
284
 
282
- const generateIndexes = async (client: Client) => {
283
- // const tableConstraints = {};
285
+ const generateIndexes = async (client: Client, schemas: string[]) => {
284
286
  const indexListSql = `
285
287
  WITH user_tables AS (
286
288
  SELECT
@@ -293,6 +295,7 @@ const generateIndexes = async (client: Client) => {
293
295
  ),
294
296
  index_info AS (
295
297
  SELECT
298
+ t.relnamespace::regnamespace::text AS table_schema,
296
299
  t.relname AS table_name,
297
300
  i.relname AS index_name,
298
301
  ix.indisunique AS is_unique,
@@ -317,7 +320,7 @@ const generateIndexes = async (client: Client) => {
317
320
  AND t.relname NOT LIKE 'pg_%'
318
321
  AND t.relname NOT LIKE 'sql_%'
319
322
  GROUP BY
320
- t.relname, i.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indexprs, ix.indrelid
323
+ t.relnamespace, t.relname, i.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indexprs, ix.indrelid
321
324
  )
322
325
  SELECT
323
326
  ut.tableschema AS table_schema,
@@ -328,12 +331,13 @@ const generateIndexes = async (client: Client) => {
328
331
  ii.index_type,
329
332
  ii.columns,
330
333
  ii.expressions,
331
- ii.constraint_type -- Added constraint type
334
+ ii.constraint_type -- Retained constraint type here
332
335
  FROM
333
336
  user_tables ut
334
337
  LEFT JOIN
335
- index_info ii ON ut.tablename = ii.table_name
338
+ index_info ii ON ut.tableschema = ii.table_schema AND ut.tablename = ii.table_name
336
339
  WHERE ii.columns IS NOT NULL
340
+ ${buildSchemaQuery('ut.tableschema', schemas)}
337
341
  ORDER BY
338
342
  ut.tablename,
339
343
  ii.constraint_type,
@@ -427,7 +431,7 @@ const generateIndexes = async (client: Client) => {
427
431
  };
428
432
  };
429
433
 
430
- const generateRawEnums = async (client: Client): Promise<Enum[]> => {
434
+ const generateRawEnums = async (client: Client, schemas: string[]): Promise<Enum[]> => {
431
435
  const enumListSql = `
432
436
  SELECT
433
437
  n.nspname AS schema_name,
@@ -440,6 +444,7 @@ const generateRawEnums = async (client: Client): Promise<Enum[]> => {
440
444
  pg_type t ON e.enumtypid = t.oid
441
445
  JOIN
442
446
  pg_namespace n ON t.typnamespace = n.oid
447
+ ${buildSchemaQuery('n.nspname', schemas, 'WHERE')}
443
448
  ORDER BY
444
449
  schema_name,
445
450
  enum_type,
@@ -467,12 +472,13 @@ const generateRawEnums = async (client: Client): Promise<Enum[]> => {
467
472
  };
468
473
 
469
474
  const fetchSchemaJson = async (connection: string): Promise<DatabaseSchema> => {
470
- const client = await getValidatedClient(connection);
475
+ const { connectionString, schemas } = parseConnectionString(connection, 'jdbc');
476
+ const client = await getValidatedClient(connectionString);
471
477
 
472
- const tablesAndFieldsRes = generateTablesAndFields(client);
473
- const indexesRes = generateIndexes(client);
474
- const refsRes = generateRawRefs(client);
475
- const enumsRes = generateRawEnums(client);
478
+ const tablesAndFieldsRes = generateTablesAndFields(client, schemas);
479
+ const indexesRes = generateIndexes(client, schemas);
480
+ const refsRes = generateRawRefs(client, schemas);
481
+ const enumsRes = generateRawEnums(client, schemas);
476
482
 
477
483
  const res = await Promise.all([
478
484
  tablesAndFieldsRes,
@@ -0,0 +1,370 @@
1
+ /* eslint-disable camelcase */
2
+ //
3
+ // Description:
4
+ // Snowflake info schema lacks reference direction for relationships.
5
+ // Constraints and uniqueness cannot be auto-generated from Snowflake info schema.
6
+ // Snowflake does not support ENUMs.
7
+ // Snowflake does not support indexes for standard tables.
8
+ //
9
+
10
+ import snowflake, { Connection, LogLevel } from 'snowflake-sdk';
11
+ import { parseSchema } from '../utils/parseSchema';
12
+ import {
13
+ DatabaseSchema,
14
+ DefaultInfo,
15
+ DefaultType,
16
+ Field,
17
+ FieldsDictionary,
18
+ Index,
19
+ IndexesDictionary,
20
+ Table,
21
+ TableConstraintsDictionary,
22
+ } from './types';
23
+
24
+ type ConstraintRow = {
25
+ schemaName: string;
26
+ tableName: string;
27
+ constraintName: string;
28
+ columnNames: string[];
29
+ type: string;
30
+ primary?: boolean;
31
+ unique?: boolean;
32
+ };
33
+
34
+ type GeneratedIndexes = {
35
+ indexes: IndexesDictionary;
36
+ tableConstraints: TableConstraintsDictionary;
37
+ };
38
+
39
+ const parseConnectionString = (connectionString: string): Record<string, string> => {
40
+ const params: Record<string, string> = {};
41
+ const regex = /([^;=]+)=([^;]*)/g;
42
+ let match;
43
+
44
+ while ((match = regex.exec(connectionString)) !== null) {
45
+ params[match[1].trim()] = match[2].trim();
46
+ }
47
+
48
+ return params;
49
+ }
50
+
51
+
52
+ const connect = async (connection: Connection): Promise<void> => {
53
+ return new Promise((resolve, reject) => {
54
+ console.log('Attempting to connect...');
55
+ connection.connect((err) => {
56
+ if (err) {
57
+ reject(err);
58
+ } else {
59
+ resolve();
60
+ }
61
+ });
62
+ });
63
+ }
64
+
65
+ const executeQuery = (connection: Connection, sqlText: string): Promise<any[]> => {
66
+ return new Promise((resolve, reject) => {
67
+ console.log('Executing query...');
68
+ connection.execute({
69
+ sqlText: sqlText,
70
+ complete: (err, stmt, rows) => {
71
+ if (err) {
72
+ reject(err);
73
+ } else {
74
+ console.log('Query executed successfully.');
75
+ if (rows) {
76
+ resolve(rows);
77
+ }
78
+ }
79
+ }
80
+ });
81
+ });
82
+ }
83
+
84
+ const isLogLevel = (value: string): value is LogLevel => {
85
+ return ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'].includes(value as LogLevel)
86
+ }
87
+
88
+ const connectToSnowflake = async (config: Record<string, string>): Promise<Connection> => {
89
+ const logLevel = isLogLevel(config.LOG_LEVEL) ? config.LOG_LEVEL : 'INFO';
90
+ const isDebugMode = config.IS_DEBUG_MODE === 'true';
91
+ snowflake.configure({
92
+ logLevel,
93
+ additionalLogToConsole: isDebugMode,
94
+ });
95
+
96
+ // "SERVER=myaccount.snowflakecomputing.com;UID=myusername;PWD=mypassword;DATABASE=mydatabase;WAREHOUSE=mywarehouse;ROLE=myrole";
97
+ const connection = snowflake.createConnection({
98
+ account: config.SERVER,
99
+ username: config.UID,
100
+ password: config.PWD,
101
+ database: config.DATABASE,
102
+ warehouse: config.WAREHOUSE,
103
+ sfRetryMaxLoginRetries: 3,
104
+ timeout: 10000,
105
+ });
106
+
107
+ try {
108
+ // Connect to Snowflake
109
+ await connect(connection);
110
+
111
+ // Execute the query
112
+ await executeQuery(connection, 'SELECT CURRENT_VERSION();');
113
+
114
+ return connection;
115
+ } catch (err) {
116
+ throw err;
117
+ }
118
+ }
119
+
120
+
121
+ const convertQueryBoolean = (val: string | null) => val === 'YES';
122
+
123
+ const getFieldType = (data_type: string, character_maximum_length: number, numeric_precision: number, numeric_scale: number): string => {
124
+ if (character_maximum_length) {
125
+ return `${data_type}(${character_maximum_length})`;
126
+ }
127
+ if (numeric_precision && numeric_scale) {
128
+ return `${data_type}(${numeric_precision},${numeric_scale})`;
129
+ }
130
+ return data_type;
131
+ };
132
+
133
+ const getDbdefault = (column_default: string, default_type: DefaultType): DefaultInfo => {
134
+ if (default_type === 'string') {
135
+ return {
136
+ type: default_type,
137
+ value: column_default.slice(1, -1),
138
+ };
139
+ }
140
+ return {
141
+ type: default_type,
142
+ value: column_default,
143
+ };
144
+ };
145
+
146
+ const generateField = (row: Record<string, any>): Field => {
147
+ const {
148
+ COLUMN_NAME: column_name,
149
+ DATA_TYPE: data_type,
150
+ CHARACTER_MAXIMUM_LENGTH: character_maximum_length,
151
+ NUMERIC_PRECISION: numeric_precision,
152
+ NUMERIC_SCALE: numeric_scale,
153
+ IDENTITY_INCREMENT: identity_increment,
154
+ IS_NULLABLE: is_nullable,
155
+ COLUMN_DEFAULT: column_default,
156
+ DEFAULT_TYPE: default_type,
157
+ COLUMN_COMMENT: column_comment,
158
+ } = row;
159
+
160
+ const dbdefault = column_default ? getDbdefault(column_default, default_type) : null;
161
+
162
+ const fieldType = {
163
+ type_name: getFieldType(data_type, character_maximum_length, numeric_precision, numeric_scale),
164
+ schemaName: null,
165
+ };
166
+
167
+ return {
168
+ name: column_name,
169
+ type: fieldType,
170
+ dbdefault,
171
+ not_null: !convertQueryBoolean(is_nullable),
172
+ increment: !!identity_increment,
173
+ note: column_comment ? { value: column_comment } : { value: '' },
174
+ };
175
+ };
176
+
177
+ const generateTablesAndFields = async (conn: Connection, schemas: string[]): Promise<{
178
+ tables: Table[],
179
+ fields: FieldsDictionary,
180
+ }> => {
181
+ const fields: FieldsDictionary = {};
182
+ const schemaSql = schemas.length > 0 ? `AND c.table_schema IN (${schemas.map((schema) => `'${schema}'`).join(',')})` : '';
183
+ const tablesAndFieldsSql = `
184
+ SELECT
185
+ c.table_schema,
186
+ c.table_name,
187
+ c.column_name,
188
+ c.data_type,
189
+ c.character_maximum_length,
190
+ c.numeric_precision,
191
+ c.numeric_scale,
192
+ c.is_nullable,
193
+ c.column_default,
194
+ CASE
195
+ WHEN c.column_default IS NULL THEN NULL
196
+ WHEN c.column_default LIKE '''%' THEN 'string'
197
+ WHEN LOWER(c.column_default) = 'true' OR LOWER(c.column_default) = 'false' THEN 'boolean'
198
+ WHEN c.column_default REGEXP '^-?[0-9]+(\\.[0-9]+)?$' THEN 'number'
199
+ ELSE 'expression'
200
+ END AS default_type,
201
+ t.comment AS table_comment,
202
+ c.comment AS column_comment,
203
+ c.identity_increment
204
+ FROM
205
+ information_schema.columns c
206
+ JOIN information_schema.tables t
207
+ ON c.table_name = t.table_name
208
+ AND c.table_schema = t.table_schema
209
+ WHERE
210
+ t.table_type = 'BASE TABLE'
211
+ AND t.table_schema NOT IN ('INFORMATION_SCHEMA')
212
+ ${schemaSql}
213
+ ORDER BY
214
+ c.table_schema,
215
+ c.table_name,
216
+ c.ordinal_position;
217
+ `;
218
+
219
+ const tablesAndFieldsResult = await executeQuery(conn, tablesAndFieldsSql);
220
+ const tables = tablesAndFieldsResult.reduce((acc: Record<string, Table>, row: Record<string, string>) => {
221
+ const { TABLE_SCHEMA: table_schema, TABLE_NAME: table_name, TABLE_COMMENT: table_comment } = row;
222
+ const key = `${table_schema}.${table_name}`;
223
+
224
+ if (!acc[key]) {
225
+ acc[key] = {
226
+ name: table_name,
227
+ schemaName: table_schema,
228
+ note: table_comment ? { value: table_comment } : { value: '' },
229
+ };
230
+ }
231
+
232
+ if (!fields[key]) fields[key] = [];
233
+ const field = generateField(row);
234
+ fields[key].push(field);
235
+
236
+ return acc;
237
+ }, {});
238
+
239
+ return {
240
+ tables: Object.values(tables),
241
+ fields,
242
+ };
243
+ };
244
+
245
+ const createConstraintKeysMap = (keys: Record<string, string>[], schemas: string[], constraintType: 'primary' | 'unique'): Record<string, ConstraintRow> => {
246
+ return keys.reduce((acc: Record<string, ConstraintRow>, row: Record<string, string>) => {
247
+ const { schema_name, table_name, column_name, constraint_name } = row;
248
+ const selectedSchema = schemas.length > 0 ? schemas.includes(schema_name) : true;
249
+ if (!selectedSchema) { return acc; }
250
+
251
+ const key = `${schema_name}.${table_name}.${constraint_name}`;
252
+
253
+ if (acc[key]) {
254
+ const columnNames = acc[key].columnNames;
255
+ columnNames.push(column_name);
256
+ return acc;
257
+ }
258
+
259
+ acc[key] = {
260
+ schemaName: schema_name,
261
+ tableName: table_name,
262
+ constraintName: constraint_name,
263
+ columnNames: [column_name],
264
+ type: '',
265
+ };
266
+ acc[key][constraintType] = true;
267
+
268
+ return acc;
269
+ }, {});
270
+ };
271
+ const generateIndexes = async (conn: Connection, databaseName: string, schemas: string[]) => {
272
+
273
+
274
+ const getPrimaryKeysSql = `
275
+ SHOW PRIMARY KEYS IN DATABASE ${databaseName};
276
+ `;
277
+
278
+ const getUniqueKeysSql = `
279
+ SHOW UNIQUE KEYS IN DATABASE ${databaseName};
280
+ `;
281
+
282
+ const primaryKeys = await executeQuery(conn, getPrimaryKeysSql);
283
+ const uniqueKeys = await executeQuery(conn, getUniqueKeysSql);
284
+
285
+ const primaryKeysByConstraint: Record<string, ConstraintRow> = createConstraintKeysMap(primaryKeys, schemas, 'primary');
286
+ const uniqueKeysByConstraint: Record<string, ConstraintRow> = createConstraintKeysMap(uniqueKeys, schemas, 'unique');
287
+
288
+ const allConstraints: ConstraintRow[] = [Object.values(primaryKeysByConstraint), Object.values(uniqueKeysByConstraint)].flat();
289
+ const { indexes, tableConstraints } = allConstraints.reduce((acc: GeneratedIndexes, row: ConstraintRow): GeneratedIndexes => {
290
+ const { schemaName, tableName, constraintName, columnNames, type, primary, unique } = row;
291
+ const key = `${schemaName}.${tableName}`;
292
+
293
+ if (columnNames.length < 2) {
294
+ acc.tableConstraints[key] = {
295
+ [columnNames[0]]: {
296
+ pk: primary,
297
+ unique,
298
+ },
299
+ };
300
+ return acc;
301
+ }
302
+
303
+ const index: Index = {
304
+ name: constraintName,
305
+ type,
306
+ unique,
307
+ pk: primary,
308
+ columns: columnNames.map((columnName) => ({
309
+ type: 'column',
310
+ value: columnName,
311
+ })),
312
+ };
313
+
314
+ if (acc.indexes[key]) {
315
+ acc.indexes[key].push(index);
316
+ } else {
317
+ acc.indexes[key] = [index];
318
+ }
319
+
320
+ return acc;
321
+ }, { indexes: {}, tableConstraints: {}});
322
+
323
+ return {
324
+ indexes,
325
+ tableConstraints,
326
+ };
327
+ };
328
+
329
+ const fetchSchemaJson = async (connection: string): Promise<DatabaseSchema> => {
330
+ const config = parseConnectionString(connection);
331
+ const conn = await connectToSnowflake(config);
332
+ if (conn instanceof Error) {
333
+ throw conn;
334
+ }
335
+ // Schemas: schema1,schema2,schema3
336
+ const schemas = config.SCHEMAS ? parseSchema(config.SCHEMAS) : [];
337
+ const databaseName = config.DATABASE;
338
+
339
+ const tablesAndFieldsRes = generateTablesAndFields(conn, schemas);
340
+ const indexesRes = generateIndexes(conn, databaseName, schemas);
341
+
342
+ const res = await Promise.all([
343
+ tablesAndFieldsRes,
344
+ indexesRes,
345
+ ]);
346
+
347
+ conn.destroy((err) => {
348
+ if (err) {
349
+ throw err;
350
+ } else {
351
+ console.log('Connection destroyed successfully.');
352
+ }
353
+ });
354
+
355
+ const { tables, fields } = res[0];
356
+ const { indexes, tableConstraints } = res[1];
357
+
358
+ return {
359
+ tables,
360
+ fields,
361
+ refs: [],
362
+ enums: [],
363
+ indexes,
364
+ tableConstraints,
365
+ };
366
+ };
367
+
368
+ export {
369
+ fetchSchemaJson,
370
+ };
@@ -21,6 +21,8 @@ interface Field {
21
21
  dbdefault: DefaultInfo | null;
22
22
  not_null: boolean;
23
23
  increment: boolean;
24
+ pk?: boolean;
25
+ unique?: boolean;
24
26
  note: NoteInfo;
25
27
  }
26
28
 
@@ -95,6 +97,15 @@ interface DatabaseSchema {
95
97
  indexes: IndexesDictionary; // Represents the indexes property
96
98
  }
97
99
 
100
+ interface BigQueryCredentials {
101
+ projectId: string,
102
+ credentials: {
103
+ clientEmail: string,
104
+ privateKey: string,
105
+ },
106
+ datasets: string[],
107
+ }
108
+
98
109
  export {
99
110
  NoteInfo,
100
111
  TypeInfo,
@@ -113,4 +124,5 @@ export {
113
124
  Index,
114
125
  IndexesDictionary,
115
126
  DatabaseSchema,
127
+ BigQueryCredentials,
116
128
  };
@@ -0,0 +1,71 @@
1
+ create schema if not exists dbml_test;
2
+
3
+ create table if not exists dbml_test.test_data_type_default (
4
+ t_int INT64 default 1 OPTIONS(description="Int64 column"),
5
+ t_bool_1 BOOLEAN default true,
6
+ t_bool_2 BOOLEAN default True,
7
+ t_string_1 STRING default 'string column description 1',
8
+ t_string_2 STRING default "string column description 2",
9
+ t_string_3 STRING default "NULL",
10
+ t_float FLOAT64 default 123.67,
11
+ t_decimal DECIMAL default 10,
12
+ t_date DATE default CURRENT_DATE,
13
+ t_datetime DATETIME default CURRENT_DATETIME,
14
+ t_timestamp TIMESTAMP default CURRENT_TIMESTAMP,
15
+ t_array ARRAY<INT64> default ARRAY<INT64>[1, 2, 3],
16
+ t_struct STRUCT<
17
+ street STRING,
18
+ city STRING,
19
+ zip_code INT64
20
+ > default STRUCT('123 Example St', 'Sample City', 12345),
21
+ t_interval INTERVAL,
22
+ t_geo GEOGRAPHY
23
+ ) OPTIONS(description="Table description");
24
+
25
+ create table if not exists dbml_test.test_constraint_1 (
26
+ id int64,
27
+ primary key (id) not enforced
28
+ );
29
+
30
+ create table if not exists dbml_test.test_constraint_2 (
31
+ id int64,
32
+ name string,
33
+
34
+ primary key (id, name) not enforced
35
+ );
36
+
37
+ create table if not exists dbml_test.test_constraint_3 (
38
+ id int64,
39
+ fk_id int64
40
+ );
41
+ alter table dbml_test.test_constraint_3
42
+ add foreign key (fk_id) references dbml_test.test_constraint_1 (id) not enforced;
43
+
44
+ create table if not exists dbml_test.test_constraint_4 (
45
+ fk_id int64,
46
+ fk_name string,
47
+ primary key (fk_id, fk_name) not enforced
48
+ );
49
+ alter table dbml_test.test_constraint_4
50
+ add foreign key (fk_id, fk_name) references dbml_test.test_constraint_2 (id, name) not enforced;
51
+
52
+ create table dbml_test.test_all_index (
53
+ a string,
54
+ b int64,
55
+ c struct <d int64, e array<string>, f struct<g string, h int64>>
56
+ );
57
+
58
+ create search index all_col_index
59
+ on dbml_test.test_all_index(ALL COLUMNS)
60
+ options (analyzer = 'NO_OP_ANALYZER');
61
+
62
+ create table dbml_test.test_multi_index (a string, b int64, c json, d timestamp);
63
+
64
+ create search index multi_index
65
+ on dbml_test.test_multi_index (a, b, c)
66
+ options ( data_types = ['string', 'int64', 'timestamp']);
67
+
68
+ create table dbml_test.test_index (a string, b int64);
69
+
70
+ create search index simple_index
71
+ on dbml_test.test_index (a);