@dbml/connector 5.2.0 → 5.3.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.
Files changed (46) hide show
  1. package/__tests__/connector.test.ts +1 -1
  2. package/__tests__/connectors/mssql/out-files/schema.json +37 -253
  3. package/__tests__/connectors/mysql/expect-out-files/schema.json +1 -1
  4. package/__tests__/connectors/oracle/connection.json +4 -0
  5. package/__tests__/connectors/oracle/expect-out-files/schema.json +3866 -0
  6. package/dist/connectors/{bigqueryConnector.d.ts → bigquery/index.d.ts} +1 -1
  7. package/dist/connectors/{bigqueryConnector.js → bigquery/index.js} +2 -2
  8. package/dist/connectors/connector.js +13 -10
  9. package/dist/connectors/{mssqlConnector.d.ts → mssql/index.d.ts} +1 -1
  10. package/dist/connectors/{mssqlConnector.js → mssql/index.js} +1 -1
  11. package/dist/connectors/{mysqlConnector.d.ts → mysql/index.d.ts} +1 -1
  12. package/dist/connectors/oracle/constraints.d.ts +7 -0
  13. package/dist/connectors/oracle/constraints.js +112 -0
  14. package/dist/connectors/oracle/index.d.ts +3 -0
  15. package/dist/connectors/oracle/index.js +68 -0
  16. package/dist/connectors/oracle/indexes.d.ts +3 -0
  17. package/dist/connectors/oracle/indexes.js +65 -0
  18. package/dist/connectors/oracle/refs.d.ts +3 -0
  19. package/dist/connectors/oracle/refs.js +67 -0
  20. package/dist/connectors/oracle/tables.d.ts +7 -0
  21. package/dist/connectors/oracle/tables.js +280 -0
  22. package/dist/connectors/oracle/utils.d.ts +9 -0
  23. package/dist/connectors/oracle/utils.js +27 -0
  24. package/dist/connectors/{snowflakeConnector.d.ts → postgres/index.d.ts} +1 -1
  25. package/dist/connectors/{postgresConnector.js → postgres/index.js} +1 -1
  26. package/dist/connectors/{postgresConnector.d.ts → snowflake/index.d.ts} +1 -1
  27. package/dist/connectors/{snowflakeConnector.js → snowflake/index.js} +1 -1
  28. package/dist/connectors/types.d.ts +1 -1
  29. package/package.json +5 -3
  30. package/src/connectors/{bigqueryConnector.ts → bigquery/index.ts} +3 -3
  31. package/src/connectors/connector.ts +8 -5
  32. package/src/connectors/{mssqlConnector.ts → mssql/index.ts} +2 -2
  33. package/src/connectors/{mysqlConnector.ts → mysql/index.ts} +1 -1
  34. package/src/connectors/oracle/constraints.ts +113 -0
  35. package/src/connectors/oracle/index.ts +76 -0
  36. package/src/connectors/oracle/indexes.ts +71 -0
  37. package/src/connectors/oracle/refs.ts +73 -0
  38. package/src/connectors/oracle/tables.ts +305 -0
  39. package/src/connectors/oracle/utils.ts +23 -0
  40. package/src/connectors/{postgresConnector.ts → postgres/index.ts} +2 -2
  41. package/src/connectors/{snowflakeConnector.ts → snowflake/index.ts} +2 -2
  42. package/src/connectors/types.ts +1 -0
  43. package/__tests__/connectors/mysql/out-files/schema.json +0 -1430
  44. package/__tests__/connectors/postgres/out-files/schema.json +0 -1719
  45. package/test-output.log +0 -18
  46. /package/dist/connectors/{mysqlConnector.js → mysql/index.js} +0 -0
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateTablesAndFields = generateTablesAndFields;
4
+ exports.getFieldType = getFieldType;
5
+ const utils_1 = require("./utils");
6
+ async function generateTablesAndFields(client) {
7
+ const fields = {};
8
+ // Note: DATA_DEFAULT is LONG type which doesn't support SQL string operations
9
+ // The oracledb driver fetches LONG as string, so we detect default type in JavaScript
10
+ const tablesAndFieldsSql = `
11
+ SELECT
12
+ cols.TABLE_NAME AS "table_name",
13
+ cols.COLUMN_NAME AS "column_name",
14
+ cols.COLUMN_ID AS "column_id",
15
+ cols.DATA_TYPE AS "data_type",
16
+ cols.DATA_LENGTH AS "data_length",
17
+ cols.DATA_PRECISION AS "data_precision",
18
+ cols.DATA_SCALE AS "data_scale",
19
+ CASE WHEN cols.IDENTITY_COLUMN = 'YES' THEN 1 ELSE 0 END AS "identity_increment",
20
+ CASE WHEN cols.NULLABLE = 'Y' THEN 1 ELSE 0 END AS "is_nullable",
21
+ cols.DATA_DEFAULT AS "data_default",
22
+ tcoms.COMMENTS AS "table_comment",
23
+ ccoms.COMMENTS AS "column_comment",
24
+ obj.CREATED AS "table_created"
25
+ FROM USER_TAB_COLUMNS cols
26
+ LEFT JOIN USER_COL_COMMENTS ccoms
27
+ ON ccoms.COLUMN_NAME = cols.COLUMN_NAME
28
+ AND ccoms.TABLE_NAME = cols.TABLE_NAME
29
+ LEFT JOIN USER_TAB_COMMENTS tcoms
30
+ ON tcoms.TABLE_NAME = cols.TABLE_NAME
31
+ LEFT JOIN USER_OBJECTS obj
32
+ ON obj.OBJECT_NAME = cols.TABLE_NAME
33
+ AND obj.OBJECT_TYPE = 'TABLE'
34
+ ORDER BY obj.CREATED, obj.OBJECT_ID, cols.COLUMN_ID
35
+ `;
36
+ const tablesAndFieldsResult = await client.execute(tablesAndFieldsSql, [], utils_1.EXECUTE_OPTIONS);
37
+ const tableMap = tablesAndFieldsResult.rows?.reduce((acc, row) => {
38
+ const { table_name, table_comment } = row;
39
+ if (!acc[table_name]) {
40
+ acc[table_name] = {
41
+ name: table_name,
42
+ schemaName: '',
43
+ note: table_comment ? { value: table_comment } : { value: '' },
44
+ };
45
+ }
46
+ if (!fields[table_name])
47
+ fields[table_name] = [];
48
+ const field = generateField(row);
49
+ fields[table_name].push(field);
50
+ return acc;
51
+ }, {}) || {};
52
+ return {
53
+ tables: Object.values(tableMap),
54
+ fields,
55
+ };
56
+ }
57
+ // Field utils
58
+ function getDefaultType(columnDefault) {
59
+ if (columnDefault === null || columnDefault === undefined) {
60
+ return 'expression';
61
+ }
62
+ const trimmed = columnDefault.trim();
63
+ // String defaults are wrapped in single quotes: 'value'
64
+ // Example: `'string'`, `'string with quotes'''`
65
+ if (/^'.*'$/.test(trimmed)) {
66
+ return 'string';
67
+ }
68
+ // Number defaults are integers or non-scientific floats
69
+ // Example: `123`, `0.12`, `.12`, `12.`
70
+ if (/^[0-9]+$/.test(trimmed) || /^[0-9]*\.[0-9]*$/.test(trimmed)) {
71
+ return 'number';
72
+ }
73
+ // Example: Strings that are not enclosed in `'` or do not contain only numbers and `.`
74
+ // * CURRENT_TIMESTAMP
75
+ // * SYSDATE
76
+ // * "number-column" + 2
77
+ return 'expression';
78
+ }
79
+ function generateField(row) {
80
+ const { column_name, data_type, data_length, data_precision, data_scale, identity_increment, is_nullable, data_default, column_comment, } = row;
81
+ const defaultType = getDefaultType(data_default);
82
+ const dbdefault = data_default && !identity_increment ? getDbdefault(data_default, defaultType) : null;
83
+ const fieldType = {
84
+ type_name: getFieldType(data_type, data_length, data_precision, data_scale),
85
+ schemaName: null,
86
+ };
87
+ return {
88
+ name: column_name,
89
+ type: fieldType,
90
+ dbdefault,
91
+ not_null: !is_nullable,
92
+ increment: !!identity_increment,
93
+ note: column_comment ? { value: column_comment } : { value: '' },
94
+ };
95
+ }
96
+ // Field default utils
97
+ function getDbdefault(columnDefault, defaultType) {
98
+ if (columnDefault === null) {
99
+ return null;
100
+ }
101
+ if (defaultType === 'string') {
102
+ const trimmed = columnDefault.trim();
103
+ return {
104
+ value: trimmed.slice(1, -1) // slice off single quotes at the start and end of the value
105
+ .replaceAll('\'\'', '\''), // escape sequences are preserved: in oracle, double single quotes = escape a single quote
106
+ type: defaultType,
107
+ };
108
+ }
109
+ return { value: columnDefault, type: defaultType };
110
+ }
111
+ // Field type utils
112
+ // These datatypes are never qualified: data size, data scale and data precision are all ignore.
113
+ // Ref: https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Data-Types.html
114
+ const UNQUALIFIED_DATA_TYPES = new Set([
115
+ // datetime
116
+ 'date',
117
+ // number
118
+ 'binary_float',
119
+ 'binary_double',
120
+ // long and raw
121
+ 'long',
122
+ 'long raw',
123
+ // large object
124
+ 'blob',
125
+ 'clob',
126
+ 'nclob',
127
+ 'bfile',
128
+ // rowid
129
+ 'rowid',
130
+ // ansi supported
131
+ 'integer',
132
+ 'int',
133
+ 'smallint',
134
+ 'double precision',
135
+ 'real',
136
+ // any
137
+ 'AnyData',
138
+ 'AnyType',
139
+ 'AnyDataSet',
140
+ // xml
141
+ 'XMLType',
142
+ 'URIType',
143
+ // spatial
144
+ 'SDO_Geometry',
145
+ 'SDO_Topo_Geometry',
146
+ 'SDO_GeoRaster',
147
+ ]);
148
+ // These datatypes can be and only be qualified with data size
149
+ // For simplicity the data size in bytes is assumed
150
+ // Ref: https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Data-Types.html
151
+ const SIZED_DATA_TYPES = new Set([
152
+ // character
153
+ 'char',
154
+ 'varchar2',
155
+ 'nchar',
156
+ 'nvarchar2',
157
+ // long and raw
158
+ 'raw',
159
+ // rowid
160
+ 'urowid',
161
+ // ansi supported
162
+ 'character varying',
163
+ 'character',
164
+ 'char varying',
165
+ 'nchar varying',
166
+ 'varchar',
167
+ 'national character varying',
168
+ 'national character',
169
+ 'national char varying',
170
+ 'national char',
171
+ ]);
172
+ // These datatypes can be and only be qualified with precision
173
+ // For datetime, these are the types that only have 1 precision
174
+ // Ref: https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Data-Types.html
175
+ const PRECISION_DATA_TYPES = new Set([
176
+ // number
177
+ 'float',
178
+ ]);
179
+ // This datatype can be qualified with a fractional second precision
180
+ const TIMESTAMP_WITH_LOCAL_TIME_ZONE_DATA_TYPE = 'timestamp with local time zone';
181
+ // This datatype can be qualified with a second precision
182
+ const TIMESTAMP_WITH_TIME_ZONE_DATA_TYPE = 'timestamp with time zone';
183
+ // This datatype can be qualified with a second precision
184
+ const TIMESTAMP_DATA_TYPE = 'timestamp';
185
+ // This datatype can be qualified with a year precision
186
+ const INTERVAL_YEAR_DATA_TYPE = 'interval year to month';
187
+ // This datatype can be qualified with a day precision and a fraction second precision
188
+ const INTERVAL_DAY_DATA_TYPE = 'interval day to second';
189
+ // These datatypes can be and only be qualified with precision and scale
190
+ // For datetime, these are the types that can have 2 precisions
191
+ // Ref: https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Data-Types.html
192
+ const PRECISION_SCALED_DATA_TYPES = new Set([
193
+ // number
194
+ 'number',
195
+ // ansi supported
196
+ 'numeric',
197
+ 'decimal',
198
+ 'dec',
199
+ ]);
200
+ // Format the field type to a standard string for aesthetic when exporting to DBML (`ANYDATA`, `anydata` are all normalized to `AnyData`)
201
+ // Some types contain multiple words with arbitrary spaces, for example, `TIMESTAMP (6) WITH LOCAL TIME ZONE`, we should normalize this to allow processing to be uniform, for example when checking for equality
202
+ function normalizeFieldType(dataType) {
203
+ const normalizedDataType = dataType.split(' ').filter(Boolean).join(' ').toLowerCase();
204
+ switch (normalizedDataType) {
205
+ case 'anydata':
206
+ return 'AnyData';
207
+ case 'anytype':
208
+ return 'AnyType';
209
+ case 'anydataset':
210
+ return 'AnyDataSet';
211
+ case 'xmltype':
212
+ return 'XMLType';
213
+ case 'uritype':
214
+ return 'URIType';
215
+ case 'sdo_geometry':
216
+ return 'SDO_Geometry';
217
+ case 'sdo_topo_geometry':
218
+ return 'SDO_Topo_Geometry';
219
+ case 'sdo_georaster':
220
+ return 'SDO_GeoRaster';
221
+ default:
222
+ return normalizedDataType;
223
+ }
224
+ }
225
+ function getFieldType(dataType, characterMaximumLength, numericPrecision, numericScale) {
226
+ const normalizedDataType = normalizeFieldType(dataType);
227
+ if (normalizedDataType === TIMESTAMP_WITH_LOCAL_TIME_ZONE_DATA_TYPE) {
228
+ if (numericScale !== null)
229
+ return `timestamp (${numericScale}) with local time zone`;
230
+ return normalizedDataType;
231
+ }
232
+ if (normalizedDataType === TIMESTAMP_WITH_TIME_ZONE_DATA_TYPE) {
233
+ if (numericScale !== null)
234
+ return `timestamp (${numericScale}) with time zone`;
235
+ return normalizedDataType;
236
+ }
237
+ if (normalizedDataType === TIMESTAMP_DATA_TYPE) {
238
+ if (numericScale !== null)
239
+ return `timestamp (${numericScale})`;
240
+ return normalizedDataType;
241
+ }
242
+ if (normalizedDataType === INTERVAL_YEAR_DATA_TYPE) {
243
+ if (numericPrecision !== null)
244
+ return `interval year (${numericPrecision}) to month`;
245
+ return normalizedDataType;
246
+ }
247
+ if (normalizedDataType === INTERVAL_DAY_DATA_TYPE) {
248
+ if (numericPrecision !== null && numericScale !== null)
249
+ return `interval day (${numericPrecision}) to second (${numericScale})`;
250
+ if (numericPrecision !== null)
251
+ return `interval day (${numericPrecision}) to second`;
252
+ if (numericScale !== null)
253
+ return `interval day to second (${numericScale})`;
254
+ return normalizedDataType;
255
+ }
256
+ if (SIZED_DATA_TYPES.has(normalizedDataType)) {
257
+ if (characterMaximumLength)
258
+ return `${normalizedDataType}(${characterMaximumLength})`;
259
+ return normalizedDataType;
260
+ }
261
+ if (PRECISION_DATA_TYPES.has(normalizedDataType)) {
262
+ if (numericPrecision !== null)
263
+ return `${normalizedDataType}(${numericPrecision})`;
264
+ return normalizedDataType;
265
+ }
266
+ if (PRECISION_SCALED_DATA_TYPES.has(normalizedDataType)) {
267
+ if (numericPrecision !== null && numericScale !== null)
268
+ return `${normalizedDataType}(${numericPrecision}, ${numericScale})`;
269
+ if (numericPrecision !== null)
270
+ return `${normalizedDataType}(${numericPrecision})`;
271
+ // There's no situation where numericScale is not null but numericPrecision is null
272
+ return normalizedDataType;
273
+ }
274
+ // This is not needed since we return `normalizedDataType` either if the condition holds true or not
275
+ // if (UNQUALIFIED_DATA_TYPES.has(normalizedDataType)) {
276
+ // return normalizedDataType;
277
+ // }
278
+ return normalizedDataType;
279
+ }
280
+ ;
@@ -0,0 +1,9 @@
1
+ export declare const LIST_SEPARATOR = "@@@@@@@@";
2
+ export declare const EXECUTE_OPTIONS: {
3
+ outFormat: number;
4
+ };
5
+ export declare function processEasyConnectString(connection: string): {
6
+ username: string;
7
+ password: string;
8
+ dbidentifier: string;
9
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EXECUTE_OPTIONS = exports.LIST_SEPARATOR = void 0;
7
+ exports.processEasyConnectString = processEasyConnectString;
8
+ const oracledb_1 = __importDefault(require("oracledb"));
9
+ exports.LIST_SEPARATOR = '@@@@@@@@'; // Used to join multiple fields into a string field to simulate array column types
10
+ exports.EXECUTE_OPTIONS = { outFormat: oracledb_1.default.OUT_FORMAT_OBJECT };
11
+ // Expect an Easy Connect string format: username/password@[//]host[:port][/database]
12
+ // Explanation of the format: https://www.orafaq.com/wiki/EZCONNECT
13
+ function processEasyConnectString(connection) {
14
+ // Capturing group `username`: a string without the characters `/` `@` `:`
15
+ // Capturing group `password`: a string without the characters `/` `@` `:`
16
+ // Capturing group `dbidentifier`: a string containing the `host[:port][/database]` part
17
+ const matches = connection.match(/^(?<username>[^/@:]+)\/(?<password>[^/@:]+)@(\/\/)?(?<dbidentifier>.+)$/);
18
+ const { username, password, dbidentifier } = matches?.groups || {};
19
+ if (!username || !password || !dbidentifier) {
20
+ throw new Error('Invalid Easy Connect string. Expect a string of format \'username/password@[//]host[:port][/database]\'');
21
+ }
22
+ return {
23
+ username,
24
+ password,
25
+ dbidentifier,
26
+ };
27
+ }
@@ -1,3 +1,3 @@
1
- import { DatabaseSchema } from './types';
1
+ import { DatabaseSchema } from '../types';
2
2
  declare const fetchSchemaJson: (connection: string) => Promise<DatabaseSchema>;
3
3
  export { fetchSchemaJson, };
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fetchSchemaJson = void 0;
4
4
  const pg_1 = require("pg");
5
- const parseSchema_1 = require("../utils/parseSchema");
5
+ const parseSchema_1 = require("../../utils/parseSchema");
6
6
  const getValidatedClient = async (connection) => {
7
7
  const client = new pg_1.Client(connection);
8
8
  try {
@@ -1,3 +1,3 @@
1
- import { DatabaseSchema } from './types';
1
+ import { DatabaseSchema } from '../types';
2
2
  declare const fetchSchemaJson: (connection: string) => Promise<DatabaseSchema>;
3
3
  export { fetchSchemaJson, };
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.fetchSchemaJson = void 0;
14
14
  const snowflake_sdk_1 = __importDefault(require("snowflake-sdk"));
15
- const parseSchema_1 = require("../utils/parseSchema");
15
+ const parseSchema_1 = require("../../utils/parseSchema");
16
16
  const parseConnectionString = (connectionString) => {
17
17
  const params = {};
18
18
  const regex = /([^;=]+)=([^;]*)/g;
@@ -96,4 +96,4 @@ interface BigQueryCredentials {
96
96
  };
97
97
  datasets: string[];
98
98
  }
99
- export { NoteInfo, TypeInfo, DefaultType, DefaultInfo, Field, Table, FieldsDictionary, TableConstraint, TableConstraintsDictionary, RefEndpoint, Ref, EnumValue, Enum, IndexColumn, Index, IndexesDictionary, DatabaseSchema, BigQueryCredentials, CheckConstraintDictionary, };
99
+ export { NoteInfo, TypeInfo, DefaultType, DefaultInfo, Field, Table, FieldsDictionary, TableConstraint, TableConstraintsDictionary, RefEndpoint, Ref, EnumValue, Enum, IndexColumn, Index, IndexesDictionary, DatabaseSchema, BigQueryCredentials, CheckConstraint, CheckConstraintDictionary, };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@dbml/connector",
4
- "version": "5.2.0",
4
+ "version": "5.3.1",
5
5
  "description": "This package was created to fetch the schema JSON from many kind of databases.",
6
6
  "author": "huy.phung.sw@gmail.com",
7
7
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
  "@types/lodash": "^4.17.16",
22
22
  "@types/mssql": "^9.1.5",
23
23
  "@types/node": "^22.2.0",
24
+ "@types/oracledb": "^6.10.0",
24
25
  "@types/pg": "^8.11.6",
25
26
  "@typescript-eslint/eslint-plugin": "^8.46.3",
26
27
  "@typescript-eslint/parser": "^8.46.3",
@@ -47,11 +48,12 @@
47
48
  "lodash": "^4.17.21",
48
49
  "mssql": "^11.0.1",
49
50
  "mysql2": "^3.11.0",
51
+ "oracledb": "^6.10.0",
50
52
  "pg": "^8.12.0",
51
- "snowflake-sdk": "^2.0.3"
53
+ "snowflake-sdk": "^2.3.3"
52
54
  },
53
55
  "engines": {
54
56
  "node": ">=18"
55
57
  },
56
- "gitHead": "20d556a6e6baad8a989c730e2afd3b65dedcea89"
58
+ "gitHead": "54ca4861d608adbfc17c86d840c2e663e9c428f9"
57
59
  }
@@ -1,5 +1,5 @@
1
1
  import { BigQuery, BigQueryOptions } from '@google-cloud/bigquery';
2
- import { loadCredentialFromFile, parseBigQueryCredential } from '../utils/credential-loader';
2
+ import { loadCredentialFromFile, parseBigQueryCredential } from '../../utils/credential-loader';
3
3
  import {
4
4
  getIntersection,
5
5
  getTableSchemaKey,
@@ -7,7 +7,7 @@ import {
7
7
  mergeIndexDictionary,
8
8
  mergeTableConstraintDictionary,
9
9
  mergeTables,
10
- } from '../utils/helpers';
10
+ } from '../../utils/helpers';
11
11
  import {
12
12
  BigQueryCredentials,
13
13
  DatabaseSchema,
@@ -19,7 +19,7 @@ import {
19
19
  IndexesDictionary,
20
20
  Table,
21
21
  TableConstraintsDictionary,
22
- } from './types';
22
+ } from '../types';
23
23
 
24
24
  // BigQuery client authentication strategy:
25
25
  // 1. Start with environment/default authentication:
@@ -1,9 +1,10 @@
1
1
  import { DatabaseSchema } from './types';
2
- import { fetchSchemaJson as fetchPostgresSchemaJson } from './postgresConnector';
3
- import { fetchSchemaJson as fetchMssqlSchemaJson } from './mssqlConnector';
4
- import { fetchSchemaJson as fetchMysqlSchemaJson } from './mysqlConnector';
5
- import { fetchSchemaJson as fetchSnowflakeSchemaJson } from './snowflakeConnector';
6
- import { fetchSchemaJson as fetchBigQuerySchemaJson } from './bigqueryConnector';
2
+ import { fetchSchemaJson as fetchPostgresSchemaJson } from './postgres';
3
+ import { fetchSchemaJson as fetchMssqlSchemaJson } from './mssql';
4
+ import { fetchSchemaJson as fetchMysqlSchemaJson } from './mysql';
5
+ import { fetchSchemaJson as fetchSnowflakeSchemaJson } from './snowflake';
6
+ import { fetchSchemaJson as fetchBigQuerySchemaJson } from './bigquery';
7
+ import { fetchSchemaJson as fetchOracleSchemaJson } from './oracle';
7
8
 
8
9
  const fetchSchemaJson = async (connection: string, databaseType: string): Promise<DatabaseSchema> => {
9
10
  switch (databaseType) {
@@ -17,6 +18,8 @@ const fetchSchemaJson = async (connection: string, databaseType: string): Promis
17
18
  return fetchSnowflakeSchemaJson(connection);
18
19
  case 'bigquery':
19
20
  return fetchBigQuerySchemaJson(connection);
21
+ case 'oracle':
22
+ return fetchOracleSchemaJson(connection);
20
23
  default:
21
24
  throw new Error(`Unsupported database type: ${databaseType}`);
22
25
  }
@@ -4,7 +4,7 @@
4
4
  * - Since DBML 4.0.0, we have supported CHECK constraints, so we no longer extract ENUMS out from CHECK constraints but rather keep them as CHECK constraints.
5
5
  * */
6
6
  import sql from 'mssql';
7
- import { buildSchemaQuery, parseConnectionString } from '../utils/parseSchema';
7
+ import { buildSchemaQuery, parseConnectionString } from '../../utils/parseSchema';
8
8
  import {
9
9
  CheckConstraintDictionary,
10
10
  DatabaseSchema,
@@ -17,7 +17,7 @@ import {
17
17
  RefEndpoint,
18
18
  Table,
19
19
  TableConstraintsDictionary,
20
- } from './types';
20
+ } from '../types';
21
21
 
22
22
  // https://learn.microsoft.com/en-us/sql/t-sql/data-types/date-and-time-types?view=sql-server-ver15
23
23
  const MSSQL_DATE_TYPES = [
@@ -2,7 +2,7 @@ import { createConnection, Connection } from 'mysql2/promise';
2
2
  import { flatten } from 'lodash';
3
3
  import {
4
4
  CheckConstraintDictionary, DefaultInfo, DatabaseSchema, DefaultType, Field, IndexColumn, FieldsDictionary, Table, Enum, IndexesDictionary, TableConstraintsDictionary, Index, Ref,
5
- } from './types';
5
+ } from '../types';
6
6
 
7
7
  const NUMBER_REGEX = '^-?[0-9]+(.[0-9]+)?$';
8
8
 
@@ -0,0 +1,113 @@
1
+ import { Connection } from 'oracledb';
2
+ import { CheckConstraint, CheckConstraintDictionary, Index, IndexesDictionary, TableConstraintsDictionary } from '../types';
3
+ import { EXECUTE_OPTIONS, LIST_SEPARATOR } from './utils';
4
+
5
+ export async function generateConstraints (client: Connection): Promise<{
6
+ indexes: IndexesDictionary;
7
+ tableConstraints: TableConstraintsDictionary;
8
+ checks: CheckConstraintDictionary;
9
+ }> {
10
+ const query = `
11
+ WITH all_constraints AS (
12
+ SELECT
13
+ c.CONSTRAINT_NAME,
14
+ c.TABLE_NAME,
15
+ c.CONSTRAINT_TYPE,
16
+ c.SEARCH_CONDITION_VC AS CHECK_EXPRESSION
17
+ FROM USER_CONSTRAINTS c
18
+ WHERE c.CONSTRAINT_TYPE IN ('P', 'U', 'C') -- P stands for primary key, U stands for unique and C stands for check
19
+ AND (c.CONSTRAINT_TYPE != 'C' OR UPPER(c.SEARCH_CONDITION_VC) NOT LIKE '%IS NOT NULL')
20
+ ),
21
+ constraint_columns AS (
22
+ SELECT
23
+ cc.CONSTRAINT_NAME,
24
+ LISTAGG(cc.COLUMN_NAME, '${LIST_SEPARATOR}') WITHIN GROUP (ORDER BY cc.POSITION) AS COLUMNS,
25
+ COUNT(cc.COLUMN_NAME) AS COLUMN_COUNT
26
+ FROM USER_CONS_COLUMNS cc
27
+ GROUP BY cc.CONSTRAINT_NAME
28
+ )
29
+ SELECT
30
+ ac.CONSTRAINT_NAME AS "constraint_name",
31
+ ac.TABLE_NAME AS "table_name",
32
+ cc.COLUMNS AS "columns",
33
+ cc.COLUMN_COUNT AS "column_count",
34
+ ac.CHECK_EXPRESSION AS "check_expression",
35
+ ac.CONSTRAINT_TYPE AS "constraint_type"
36
+ FROM all_constraints ac
37
+ LEFT JOIN constraint_columns cc
38
+ ON ac.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
39
+ LEFT JOIN USER_OBJECTS obj
40
+ ON obj.OBJECT_NAME = ac.TABLE_NAME
41
+ AND obj.OBJECT_TYPE = 'TABLE'
42
+ ORDER BY
43
+ obj.CREATED,
44
+ obj.OBJECT_ID,
45
+ ac.CONSTRAINT_NAME
46
+ `;
47
+
48
+ const indexes: IndexesDictionary = {};
49
+ const tableConstraints: TableConstraintsDictionary = {};
50
+ const checks: CheckConstraintDictionary = {};
51
+
52
+ const res = await client.execute(query, [], EXECUTE_OPTIONS);
53
+ res.rows?.forEach((row) => {
54
+ const { constraint_name, table_name, columns: columnsStr, column_count, check_expression, constraint_type } = row as Record<string, unknown>;
55
+ const tableName = table_name as string;
56
+ const constraintName = (constraint_name as string) || '';
57
+ const columns = ((columnsStr as string) || '').split(LIST_SEPARATOR);
58
+ const columnCount = (column_count as number) || 1;
59
+ const constraintType = constraint_type as 'P' | 'U' | 'C';
60
+ const checkExpression = (check_expression || null) as string | null;
61
+
62
+ // Table-level check constraints
63
+ if (constraintType === 'C' && checkExpression && columnCount > 1) {
64
+ if (!checks[tableName]) checks[tableName] = [];
65
+ const check: CheckConstraint = {
66
+ name: constraintName,
67
+ expression: checkExpression,
68
+ };
69
+ checks[tableName].push(check);
70
+ return;
71
+ }
72
+ // Column-level check constraints
73
+ if (constraintType === 'C' && checkExpression && columnCount === 1 && columns.length === 1) {
74
+ if (!tableConstraints[tableName]) tableConstraints[tableName] = {};
75
+ const column = columns[0];
76
+ if (!tableConstraints[tableName][column]) tableConstraints[tableName][column] = { checks: [] };
77
+ tableConstraints[tableName][column].checks.push({
78
+ name: constraintName,
79
+ expression: checkExpression,
80
+ });
81
+ }
82
+ // Column-level unique or primary indexes
83
+ if (['P', 'U'].includes(constraintType) && columnCount === 1 && columns.length === 1) {
84
+ if (!tableConstraints[tableName]) tableConstraints[tableName] = {};
85
+ const column = columns[0];
86
+ if (!tableConstraints[tableName][column]) tableConstraints[tableName][column] = { checks: [] };
87
+ tableConstraints[tableName][column].pk ||= constraintType === 'P';
88
+ tableConstraints[tableName][column].unique ||= constraintType === 'U';
89
+ }
90
+ // Table-level unique or primary indexes
91
+ if (['P', 'U'].includes(constraintType) && columnCount > 1) {
92
+ if (!tableConstraints[tableName]) tableConstraints[tableName] = {};
93
+ if (!indexes[tableName]) indexes[tableName] = [];
94
+ const index: Index = {
95
+ name: constraintName,
96
+ type: '',
97
+ unique: true,
98
+ pk: constraintType === 'P',
99
+ columns: columns.map((c) => ({
100
+ type: 'column',
101
+ value: c,
102
+ })),
103
+ };
104
+ indexes[tableName].push(index);
105
+ }
106
+ });
107
+
108
+ return {
109
+ indexes,
110
+ tableConstraints,
111
+ checks,
112
+ };
113
+ }
@@ -0,0 +1,76 @@
1
+ import { getConnection, Connection } from 'oracledb';
2
+ import { DatabaseSchema } from '../types';
3
+ import { generateTablesAndFields } from './tables';
4
+ import { generateRawRefs } from './refs';
5
+ import { generateConstraints } from './constraints';
6
+ import { generateIndexes } from './indexes';
7
+ import { processEasyConnectString } from './utils';
8
+
9
+ const getValidatedClient = async (username: string, password: string, dbidentifier: string): Promise<Connection> => {
10
+ const client = await getConnection({
11
+ username,
12
+ password,
13
+ connectionString: dbidentifier,
14
+ });
15
+ try {
16
+ await client.execute('SELECT 1 FROM DUAL');
17
+
18
+ return client;
19
+ } catch (error) {
20
+ await client.close();
21
+ if (error instanceof Error) {
22
+ throw new Error(`OracleSQL connection error: ${error.message}`);
23
+ }
24
+
25
+ throw error;
26
+ }
27
+ };
28
+
29
+ // Expect an Easy Connect string format: username/password@[//]host[:port][/database]
30
+ // `username` and `password` are mandatory
31
+ // `port` and `database` (and the parts enclosed in `[]`) are optional
32
+ // Explanation of the format: https://www.orafaq.com/wiki/EZCONNECT
33
+ async function fetchSchemaJson (connection: string): Promise<DatabaseSchema> {
34
+ const { username, password, dbidentifier } = processEasyConnectString(connection);
35
+
36
+ const client = await getValidatedClient(username, password, dbidentifier);
37
+
38
+ const tablesAndFieldsRes = generateTablesAndFields(client);
39
+ const constraintsRes = generateConstraints(client);
40
+ const refsRes = generateRawRefs(client);
41
+ const indexesRes = generateIndexes(client);
42
+
43
+ const res = await Promise.all([
44
+ tablesAndFieldsRes,
45
+ constraintsRes,
46
+ refsRes,
47
+ indexesRes,
48
+ ]);
49
+
50
+ const { tables, fields } = res[0];
51
+ const { indexes: uniqueIndexes, tableConstraints, checks } = res[1];
52
+ const refs = res[2];
53
+ const indexes = res[3];
54
+ Object.keys(uniqueIndexes).forEach((key) => {
55
+ if (!indexes[key]) indexes[key] = uniqueIndexes[key];
56
+ else indexes[key].push(...uniqueIndexes[key]);
57
+ });
58
+
59
+ await client.close();
60
+
61
+ return {
62
+ tables,
63
+ fields,
64
+ refs,
65
+ // As of oracle 19c, there's no native ENUM data type
66
+ // ENUM is introduced in oracle 23ai
67
+ enums: [],
68
+ indexes,
69
+ tableConstraints,
70
+ checks,
71
+ };
72
+ };
73
+
74
+ export {
75
+ fetchSchemaJson,
76
+ };