@dbml/connector 3.7.4 → 3.8.1
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/__tests__/connector.test.ts +2 -2
- package/__tests__/connectors/postgres/expect-out-files/schema.json +208 -0
- package/dist/connectors/bigqueryConnector.d.ts +3 -0
- package/dist/connectors/bigqueryConnector.js +276 -0
- package/dist/connectors/connector.d.ts +1 -1
- package/dist/connectors/connector.js +9 -3
- package/dist/connectors/mssqlConnector.d.ts +1 -1
- package/dist/connectors/mssqlConnector.js +100 -70
- package/dist/connectors/postgresConnector.d.ts +1 -1
- package/dist/connectors/postgresConnector.js +28 -20
- package/dist/connectors/snowflakeConnector.d.ts +3 -0
- package/dist/connectors/snowflakeConnector.js +292 -0
- package/dist/connectors/types.d.ts +11 -1
- package/dist/utils/credential-loader.d.ts +3 -0
- package/dist/utils/credential-loader.js +53 -0
- package/dist/utils/helpers.d.ts +7 -0
- package/dist/utils/helpers.js +50 -0
- package/dist/utils/parseSchema.d.ts +10 -0
- package/dist/utils/parseSchema.js +59 -0
- package/package.json +5 -3
- package/src/connectors/bigqueryConnector.ts +389 -0
- package/src/connectors/connector.ts +9 -3
- package/src/connectors/mssqlConnector.ts +107 -77
- package/src/connectors/postgresConnector.ts +29 -21
- package/src/connectors/snowflakeConnector.ts +370 -0
- package/src/connectors/types.ts +12 -0
- package/src/ddl_samples/bigquery.sql +71 -0
- package/src/ddl_samples/snowflake.sql +131 -0
- package/src/utils/credential-loader.ts +64 -0
- package/src/utils/helpers.ts +73 -0
- package/src/utils/parseSchema.ts +60 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable camelcase */
|
|
2
2
|
import { Client } from 'pg';
|
|
3
|
+
import { buildSchemaQuery, parseConnectionString } from '../utils/parseSchema';
|
|
3
4
|
import {
|
|
4
5
|
DatabaseSchema,
|
|
5
6
|
DefaultInfo,
|
|
@@ -113,14 +114,14 @@ 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
|
}> => {
|
|
120
121
|
const fields: FieldsDictionary = {};
|
|
121
122
|
const tablesAndFieldsSql = `
|
|
122
123
|
WITH comments AS (
|
|
123
|
-
SELECT
|
|
124
|
+
SELECT DISTINCT ON (pc.relname, pn.nspname, pa.attname)
|
|
124
125
|
pc.relname AS table_name,
|
|
125
126
|
pn.nspname AS table_schema,
|
|
126
127
|
pa.attname AS column_name,
|
|
@@ -150,6 +151,7 @@ const generateTablesAndFields = async (client: Client): Promise<{
|
|
|
150
151
|
c.identity_increment,
|
|
151
152
|
c.is_nullable,
|
|
152
153
|
c.column_default,
|
|
154
|
+
c.ordinal_position,
|
|
153
155
|
CASE
|
|
154
156
|
WHEN c.column_default IS NULL THEN NULL
|
|
155
157
|
WHEN c.column_default LIKE 'nextval(%' THEN 'increment'
|
|
@@ -158,8 +160,8 @@ const generateTablesAndFields = async (client: Client): Promise<{
|
|
|
158
160
|
WHEN c.column_default ~ '^-?[0-9]+(.[0-9]+)?$' THEN 'number'
|
|
159
161
|
ELSE 'expression'
|
|
160
162
|
END AS default_type,
|
|
161
|
-
(SELECT description FROM comments WHERE table_name = t.table_name AND table_schema = t.table_schema AND column_name IS NULL) AS table_comment,
|
|
162
|
-
(SELECT description FROM comments WHERE table_name = t.table_name AND table_schema = t.table_schema AND column_name = c.column_name) AS column_comment
|
|
163
|
+
(SELECT description FROM comments WHERE table_name = t.table_name AND table_schema = t.table_schema AND column_name IS NULL LIMIT 1) AS table_comment,
|
|
164
|
+
(SELECT description FROM comments WHERE table_name = t.table_name AND table_schema = t.table_schema AND column_name = c.column_name LIMIT 1) AS column_comment
|
|
163
165
|
FROM
|
|
164
166
|
information_schema.columns c
|
|
165
167
|
JOIN
|
|
@@ -167,6 +169,7 @@ const generateTablesAndFields = async (client: Client): Promise<{
|
|
|
167
169
|
WHERE
|
|
168
170
|
t.table_type = 'BASE TABLE'
|
|
169
171
|
AND t.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
172
|
+
${buildSchemaQuery('t.table_schema', schemas)}
|
|
170
173
|
ORDER BY
|
|
171
174
|
t.table_schema,
|
|
172
175
|
t.table_name,
|
|
@@ -200,7 +203,7 @@ const generateTablesAndFields = async (client: Client): Promise<{
|
|
|
200
203
|
};
|
|
201
204
|
};
|
|
202
205
|
|
|
203
|
-
const generateRawRefs = async (client: Client): Promise<Ref[]> => {
|
|
206
|
+
const generateRawRefs = async (client: Client, schemas: string[]): Promise<Ref[]> => {
|
|
204
207
|
const refs: Ref[] = [];
|
|
205
208
|
|
|
206
209
|
const refsListSql = `
|
|
@@ -226,6 +229,7 @@ const generateRawRefs = async (client: Client): Promise<Ref[]> => {
|
|
|
226
229
|
AND tc.table_schema = rc.constraint_schema
|
|
227
230
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
228
231
|
AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
232
|
+
${buildSchemaQuery('tc.table_schema', schemas)}
|
|
229
233
|
GROUP BY
|
|
230
234
|
tc.table_schema,
|
|
231
235
|
tc.table_name,
|
|
@@ -279,8 +283,7 @@ const generateRawRefs = async (client: Client): Promise<Ref[]> => {
|
|
|
279
283
|
return refs;
|
|
280
284
|
};
|
|
281
285
|
|
|
282
|
-
const generateIndexes = async (client: Client) => {
|
|
283
|
-
// const tableConstraints = {};
|
|
286
|
+
const generateIndexes = async (client: Client, schemas: string[]) => {
|
|
284
287
|
const indexListSql = `
|
|
285
288
|
WITH user_tables AS (
|
|
286
289
|
SELECT
|
|
@@ -293,6 +296,7 @@ const generateIndexes = async (client: Client) => {
|
|
|
293
296
|
),
|
|
294
297
|
index_info AS (
|
|
295
298
|
SELECT
|
|
299
|
+
t.relnamespace::regnamespace::text AS table_schema,
|
|
296
300
|
t.relname AS table_name,
|
|
297
301
|
i.relname AS index_name,
|
|
298
302
|
ix.indisunique AS is_unique,
|
|
@@ -317,7 +321,7 @@ const generateIndexes = async (client: Client) => {
|
|
|
317
321
|
AND t.relname NOT LIKE 'pg_%'
|
|
318
322
|
AND t.relname NOT LIKE 'sql_%'
|
|
319
323
|
GROUP BY
|
|
320
|
-
t.relname, i.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indexprs, ix.indrelid
|
|
324
|
+
t.relnamespace, t.relname, i.relname, ix.indisunique, ix.indisprimary, am.amname, ix.indexprs, ix.indrelid
|
|
321
325
|
)
|
|
322
326
|
SELECT
|
|
323
327
|
ut.tableschema AS table_schema,
|
|
@@ -328,12 +332,13 @@ const generateIndexes = async (client: Client) => {
|
|
|
328
332
|
ii.index_type,
|
|
329
333
|
ii.columns,
|
|
330
334
|
ii.expressions,
|
|
331
|
-
ii.constraint_type --
|
|
335
|
+
ii.constraint_type -- Retained constraint type here
|
|
332
336
|
FROM
|
|
333
337
|
user_tables ut
|
|
334
338
|
LEFT JOIN
|
|
335
|
-
index_info ii ON ut.tablename = ii.table_name
|
|
339
|
+
index_info ii ON ut.tableschema = ii.table_schema AND ut.tablename = ii.table_name
|
|
336
340
|
WHERE ii.columns IS NOT NULL
|
|
341
|
+
${buildSchemaQuery('ut.tableschema', schemas)}
|
|
337
342
|
ORDER BY
|
|
338
343
|
ut.tablename,
|
|
339
344
|
ii.constraint_type,
|
|
@@ -427,9 +432,9 @@ const generateIndexes = async (client: Client) => {
|
|
|
427
432
|
};
|
|
428
433
|
};
|
|
429
434
|
|
|
430
|
-
const generateRawEnums = async (client: Client): Promise<Enum[]> => {
|
|
435
|
+
const generateRawEnums = async (client: Client, schemas: string[]): Promise<Enum[]> => {
|
|
431
436
|
const enumListSql = `
|
|
432
|
-
SELECT
|
|
437
|
+
SELECT DISTINCT
|
|
433
438
|
n.nspname AS schema_name,
|
|
434
439
|
t.typname AS enum_type,
|
|
435
440
|
e.enumlabel AS enum_value,
|
|
@@ -440,6 +445,7 @@ const generateRawEnums = async (client: Client): Promise<Enum[]> => {
|
|
|
440
445
|
pg_type t ON e.enumtypid = t.oid
|
|
441
446
|
JOIN
|
|
442
447
|
pg_namespace n ON t.typnamespace = n.oid
|
|
448
|
+
${buildSchemaQuery('n.nspname', schemas, 'WHERE')}
|
|
443
449
|
ORDER BY
|
|
444
450
|
schema_name,
|
|
445
451
|
enum_type,
|
|
@@ -449,15 +455,16 @@ const generateRawEnums = async (client: Client): Promise<Enum[]> => {
|
|
|
449
455
|
const enumListResult = await client.query(enumListSql);
|
|
450
456
|
const enums = enumListResult.rows.reduce((acc, row) => {
|
|
451
457
|
const { schema_name, enum_type, enum_value } = row;
|
|
458
|
+
const key = `${schema_name}.${enum_type}`;
|
|
452
459
|
|
|
453
|
-
if (!acc[
|
|
454
|
-
acc[
|
|
460
|
+
if (!acc[key]) {
|
|
461
|
+
acc[key] = {
|
|
455
462
|
name: enum_type,
|
|
456
463
|
schemaName: schema_name,
|
|
457
464
|
values: [],
|
|
458
465
|
};
|
|
459
466
|
}
|
|
460
|
-
acc[
|
|
467
|
+
acc[key].values.push({
|
|
461
468
|
name: enum_value,
|
|
462
469
|
});
|
|
463
470
|
return acc;
|
|
@@ -467,12 +474,13 @@ const generateRawEnums = async (client: Client): Promise<Enum[]> => {
|
|
|
467
474
|
};
|
|
468
475
|
|
|
469
476
|
const fetchSchemaJson = async (connection: string): Promise<DatabaseSchema> => {
|
|
470
|
-
const
|
|
477
|
+
const { connectionString, schemas } = parseConnectionString(connection, 'jdbc');
|
|
478
|
+
const client = await getValidatedClient(connectionString);
|
|
471
479
|
|
|
472
|
-
const tablesAndFieldsRes = generateTablesAndFields(client);
|
|
473
|
-
const indexesRes = generateIndexes(client);
|
|
474
|
-
const refsRes = generateRawRefs(client);
|
|
475
|
-
const enumsRes = generateRawEnums(client);
|
|
480
|
+
const tablesAndFieldsRes = generateTablesAndFields(client, schemas);
|
|
481
|
+
const indexesRes = generateIndexes(client, schemas);
|
|
482
|
+
const refsRes = generateRawRefs(client, schemas);
|
|
483
|
+
const enumsRes = generateRawEnums(client, schemas);
|
|
476
484
|
|
|
477
485
|
const res = await Promise.all([
|
|
478
486
|
tablesAndFieldsRes,
|
|
@@ -498,5 +506,5 @@ const fetchSchemaJson = async (connection: string): Promise<DatabaseSchema> => {
|
|
|
498
506
|
};
|
|
499
507
|
|
|
500
508
|
export {
|
|
501
|
-
fetchSchemaJson
|
|
509
|
+
fetchSchemaJson
|
|
502
510
|
};
|
|
@@ -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
|
+
};
|
package/src/connectors/types.ts
CHANGED
|
@@ -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);
|