biz-a-cli 2.3.79 → 2.3.80-15224
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/bin/hubEvent.js +6 -5
- package/engine/domain/appConfig.js +2342 -0
- package/engine/domain/firebird-ddl.js +1097 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
export const buildTableStructureQueries = (tableName) => {
|
|
2
|
+
const normalizedTable = normalizeIdentifier(tableName);
|
|
3
|
+
const tableLiteral = toLiteral(normalizedTable);
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
tableExistsSql: [
|
|
7
|
+
'SELECT IIF(EXISTS(',
|
|
8
|
+
' SELECT 1',
|
|
9
|
+
' FROM RDB$RELATIONS r',
|
|
10
|
+
` WHERE UPPER(TRIM(r.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
11
|
+
' AND COALESCE(r.RDB$SYSTEM_FLAG, 0) = 0',
|
|
12
|
+
' AND r.RDB$VIEW_BLR IS NULL',
|
|
13
|
+
'), 1, 0) AS TABLE_EXISTS',
|
|
14
|
+
'FROM RDB$DATABASE;',
|
|
15
|
+
].join('\n'),
|
|
16
|
+
|
|
17
|
+
columnsSql: [
|
|
18
|
+
'SELECT',
|
|
19
|
+
' TRIM(rf.RDB$FIELD_NAME) AS COLUMN_NAME,',
|
|
20
|
+
' CASE',
|
|
21
|
+
" WHEN f.RDB$FIELD_TYPE = 7 THEN 'SMALLINT'",
|
|
22
|
+
" WHEN f.RDB$FIELD_TYPE = 8 THEN 'INTEGER'",
|
|
23
|
+
" WHEN f.RDB$FIELD_TYPE = 10 THEN 'FLOAT'",
|
|
24
|
+
" WHEN f.RDB$FIELD_TYPE = 12 THEN 'DATE'",
|
|
25
|
+
" WHEN f.RDB$FIELD_TYPE = 13 THEN 'TIME'",
|
|
26
|
+
" WHEN f.RDB$FIELD_TYPE = 14 THEN 'CHAR'",
|
|
27
|
+
" WHEN f.RDB$FIELD_TYPE = 16 AND COALESCE(f.RDB$FIELD_SUB_TYPE, 0) = 0 THEN 'BIGINT'",
|
|
28
|
+
" WHEN f.RDB$FIELD_TYPE = 16 AND f.RDB$FIELD_SUB_TYPE = 1 THEN 'NUMERIC'",
|
|
29
|
+
" WHEN f.RDB$FIELD_TYPE = 16 AND f.RDB$FIELD_SUB_TYPE = 2 THEN 'DECIMAL'",
|
|
30
|
+
" WHEN f.RDB$FIELD_TYPE = 23 THEN 'SMALLINT'",
|
|
31
|
+
" WHEN f.RDB$FIELD_TYPE = 27 THEN 'DOUBLE PRECISION'",
|
|
32
|
+
" WHEN f.RDB$FIELD_TYPE = 35 THEN 'TIMESTAMP'",
|
|
33
|
+
" WHEN f.RDB$FIELD_TYPE = 37 THEN 'VARCHAR'",
|
|
34
|
+
" WHEN f.RDB$FIELD_TYPE = 261 THEN 'BLOB'",
|
|
35
|
+
" ELSE 'UNKNOWN'",
|
|
36
|
+
' END AS DATA_TYPE,',
|
|
37
|
+
' CASE',
|
|
38
|
+
' WHEN f.RDB$FIELD_TYPE IN (14, 37) THEN f.RDB$CHARACTER_LENGTH',
|
|
39
|
+
' WHEN f.RDB$FIELD_TYPE = 16 AND COALESCE(f.RDB$FIELD_SUB_TYPE, 0) IN (1, 2) THEN f.RDB$FIELD_PRECISION',
|
|
40
|
+
' ELSE NULL',
|
|
41
|
+
' END AS FIELD_SIZE,',
|
|
42
|
+
' ABS(COALESCE(f.RDB$FIELD_SCALE, 0)) AS FIELD_SCALE,',
|
|
43
|
+
' IIF(COALESCE(rf.RDB$NULL_FLAG, 0) = 1, 0, 1) AS IS_NULLABLE,',
|
|
44
|
+
' TRIM(COALESCE(CAST(rf.RDB$DEFAULT_SOURCE AS VARCHAR(4096)), CAST(f.RDB$DEFAULT_SOURCE AS VARCHAR(4096)))) AS DEFAULT_SOURCE,',
|
|
45
|
+
' TRIM(f.RDB$COMPUTED_SOURCE) AS COMPUTED_SOURCE,',
|
|
46
|
+
' rf.RDB$FIELD_POSITION AS COLUMN_POSITION',
|
|
47
|
+
'FROM RDB$RELATION_FIELDS rf',
|
|
48
|
+
'JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE',
|
|
49
|
+
`WHERE UPPER(TRIM(rf.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
50
|
+
'ORDER BY rf.RDB$FIELD_POSITION;',
|
|
51
|
+
].join('\n'),
|
|
52
|
+
|
|
53
|
+
primaryKeySql: [
|
|
54
|
+
'SELECT',
|
|
55
|
+
' TRIM(rc.RDB$CONSTRAINT_NAME) AS CONSTRAINT_NAME,',
|
|
56
|
+
' TRIM(seg.RDB$FIELD_NAME) AS COLUMN_NAME,',
|
|
57
|
+
' seg.RDB$FIELD_POSITION AS COLUMN_POSITION',
|
|
58
|
+
'FROM RDB$RELATION_CONSTRAINTS rc',
|
|
59
|
+
'JOIN RDB$INDEX_SEGMENTS seg ON seg.RDB$INDEX_NAME = rc.RDB$INDEX_NAME',
|
|
60
|
+
`WHERE UPPER(TRIM(rc.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
61
|
+
" AND rc.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'",
|
|
62
|
+
'ORDER BY seg.RDB$FIELD_POSITION;',
|
|
63
|
+
].join('\n'),
|
|
64
|
+
|
|
65
|
+
uniqueKeysSql: [
|
|
66
|
+
'SELECT',
|
|
67
|
+
' TRIM(rc.RDB$CONSTRAINT_NAME) AS CONSTRAINT_NAME,',
|
|
68
|
+
' TRIM(seg.RDB$FIELD_NAME) AS COLUMN_NAME,',
|
|
69
|
+
' seg.RDB$FIELD_POSITION AS COLUMN_POSITION',
|
|
70
|
+
'FROM RDB$RELATION_CONSTRAINTS rc',
|
|
71
|
+
'JOIN RDB$INDEX_SEGMENTS seg ON seg.RDB$INDEX_NAME = rc.RDB$INDEX_NAME',
|
|
72
|
+
`WHERE UPPER(TRIM(rc.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
73
|
+
" AND rc.RDB$CONSTRAINT_TYPE = 'UNIQUE'",
|
|
74
|
+
'ORDER BY rc.RDB$CONSTRAINT_NAME, seg.RDB$FIELD_POSITION;',
|
|
75
|
+
].join('\n'),
|
|
76
|
+
|
|
77
|
+
checkConstraintsSql: [
|
|
78
|
+
'SELECT',
|
|
79
|
+
' TRIM(rc.RDB$CONSTRAINT_NAME) AS CONSTRAINT_NAME,',
|
|
80
|
+
' TRIM(CAST(t.RDB$TRIGGER_SOURCE AS VARCHAR(4096))) AS CHECK_SOURCE',
|
|
81
|
+
'FROM RDB$RELATION_CONSTRAINTS rc',
|
|
82
|
+
'JOIN RDB$CHECK_CONSTRAINTS cc ON cc.RDB$CONSTRAINT_NAME = rc.RDB$CONSTRAINT_NAME',
|
|
83
|
+
'LEFT JOIN RDB$TRIGGERS t ON t.RDB$TRIGGER_NAME = cc.RDB$TRIGGER_NAME',
|
|
84
|
+
`WHERE UPPER(TRIM(rc.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
85
|
+
" AND rc.RDB$CONSTRAINT_TYPE = 'CHECK'",
|
|
86
|
+
'ORDER BY rc.RDB$CONSTRAINT_NAME;',
|
|
87
|
+
].join('\n'),
|
|
88
|
+
|
|
89
|
+
indexesSql: [
|
|
90
|
+
'SELECT',
|
|
91
|
+
' TRIM(i.RDB$INDEX_NAME) AS INDEX_NAME,',
|
|
92
|
+
' COALESCE(i.RDB$UNIQUE_FLAG, 0) AS IS_UNIQUE,',
|
|
93
|
+
' COALESCE(i.RDB$INDEX_TYPE, 0) AS IS_DESCENDING,',
|
|
94
|
+
' IIF(COALESCE(i.RDB$INDEX_INACTIVE, 0) = 1, 0, 1) AS IS_ACTIVE,',
|
|
95
|
+
' TRIM(i.RDB$EXPRESSION_SOURCE) AS EXPRESSION_SOURCE,',
|
|
96
|
+
' TRIM(seg.RDB$FIELD_NAME) AS COLUMN_NAME,',
|
|
97
|
+
' seg.RDB$FIELD_POSITION AS COLUMN_POSITION',
|
|
98
|
+
'FROM RDB$INDICES i',
|
|
99
|
+
'LEFT JOIN RDB$INDEX_SEGMENTS seg ON seg.RDB$INDEX_NAME = i.RDB$INDEX_NAME',
|
|
100
|
+
'LEFT JOIN RDB$RELATION_CONSTRAINTS rc ON rc.RDB$INDEX_NAME = i.RDB$INDEX_NAME',
|
|
101
|
+
`WHERE UPPER(TRIM(i.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
102
|
+
' AND rc.RDB$INDEX_NAME IS NULL',
|
|
103
|
+
'ORDER BY i.RDB$INDEX_NAME, seg.RDB$FIELD_POSITION;',
|
|
104
|
+
].join('\n'),
|
|
105
|
+
|
|
106
|
+
foreignKeysSql: [
|
|
107
|
+
'SELECT',
|
|
108
|
+
' TRIM(rc.RDB$CONSTRAINT_NAME) AS FK_NAME,',
|
|
109
|
+
' TRIM(seg.RDB$FIELD_NAME) AS COLUMN_NAME,',
|
|
110
|
+
' seg.RDB$FIELD_POSITION AS COLUMN_POSITION,',
|
|
111
|
+
' TRIM(ref.RDB$RELATION_NAME) AS REF_TABLE_NAME,',
|
|
112
|
+
' TRIM(refSeg.RDB$FIELD_NAME) AS REF_COLUMN_NAME,',
|
|
113
|
+
' TRIM(refc.RDB$DELETE_RULE) AS DELETE_RULE,',
|
|
114
|
+
' TRIM(refc.RDB$UPDATE_RULE) AS UPDATE_RULE',
|
|
115
|
+
'FROM RDB$RELATION_CONSTRAINTS rc',
|
|
116
|
+
'JOIN RDB$REF_CONSTRAINTS refc ON refc.RDB$CONSTRAINT_NAME = rc.RDB$CONSTRAINT_NAME',
|
|
117
|
+
'JOIN RDB$RELATION_CONSTRAINTS ref ON ref.RDB$CONSTRAINT_NAME = refc.RDB$CONST_NAME_UQ',
|
|
118
|
+
'JOIN RDB$INDEX_SEGMENTS seg ON seg.RDB$INDEX_NAME = rc.RDB$INDEX_NAME',
|
|
119
|
+
'JOIN RDB$INDEX_SEGMENTS refSeg',
|
|
120
|
+
' ON refSeg.RDB$INDEX_NAME = ref.RDB$INDEX_NAME',
|
|
121
|
+
' AND refSeg.RDB$FIELD_POSITION = seg.RDB$FIELD_POSITION',
|
|
122
|
+
`WHERE UPPER(TRIM(rc.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
123
|
+
" AND rc.RDB$CONSTRAINT_TYPE = 'FOREIGN KEY'",
|
|
124
|
+
'ORDER BY rc.RDB$CONSTRAINT_NAME, seg.RDB$FIELD_POSITION;',
|
|
125
|
+
].join('\n'),
|
|
126
|
+
incomingForeignKeysSql: [
|
|
127
|
+
'SELECT',
|
|
128
|
+
' TRIM(fk_rc.RDB$CONSTRAINT_NAME) AS FK_NAME,',
|
|
129
|
+
' TRIM(fk_rc.RDB$RELATION_NAME) AS FK_TABLE_NAME,',
|
|
130
|
+
' TRIM(fk_seg.RDB$FIELD_NAME) AS FK_COLUMN_NAME,',
|
|
131
|
+
' TRIM(pk_seg.RDB$FIELD_NAME) AS REF_COLUMN_NAME,',
|
|
132
|
+
' fk_seg.RDB$FIELD_POSITION AS COLUMN_POSITION',
|
|
133
|
+
'FROM RDB$RELATION_CONSTRAINTS fk_rc',
|
|
134
|
+
'JOIN RDB$REF_CONSTRAINTS refc ON refc.RDB$CONSTRAINT_NAME = fk_rc.RDB$CONSTRAINT_NAME',
|
|
135
|
+
'JOIN RDB$RELATION_CONSTRAINTS pk_rc ON pk_rc.RDB$CONSTRAINT_NAME = refc.RDB$CONST_NAME_UQ',
|
|
136
|
+
'JOIN RDB$INDEX_SEGMENTS fk_seg ON fk_seg.RDB$INDEX_NAME = fk_rc.RDB$INDEX_NAME',
|
|
137
|
+
'JOIN RDB$INDEX_SEGMENTS pk_seg ON pk_seg.RDB$INDEX_NAME = pk_rc.RDB$INDEX_NAME',
|
|
138
|
+
' AND pk_seg.RDB$FIELD_POSITION = fk_seg.RDB$FIELD_POSITION',
|
|
139
|
+
"WHERE fk_rc.RDB$CONSTRAINT_TYPE = 'FOREIGN KEY'",
|
|
140
|
+
` AND UPPER(TRIM(pk_rc.RDB$RELATION_NAME)) = ${tableLiteral}`,
|
|
141
|
+
'ORDER BY fk_rc.RDB$CONSTRAINT_NAME, fk_seg.RDB$FIELD_POSITION;',
|
|
142
|
+
].join('\n'),
|
|
143
|
+
columnDependenciesSql: [
|
|
144
|
+
'SELECT',
|
|
145
|
+
' TRIM(d.RDB$FIELD_NAME) AS COLUMN_NAME,',
|
|
146
|
+
' TRIM(d.RDB$DEPENDENT_NAME) AS DEPENDENT_NAME,',
|
|
147
|
+
' d.RDB$DEPENDENT_TYPE AS DEPENDENT_TYPE',
|
|
148
|
+
'FROM RDB$DEPENDENCIES d',
|
|
149
|
+
`WHERE UPPER(TRIM(d.RDB$DEPENDED_ON_NAME)) = ${tableLiteral}`,
|
|
150
|
+
' AND d.RDB$FIELD_NAME IS NOT NULL',
|
|
151
|
+
'ORDER BY d.RDB$FIELD_NAME, d.RDB$DEPENDENT_NAME;',
|
|
152
|
+
].join('\n'),
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const compareTableStructure = (expected, actual) => {
|
|
157
|
+
const differences = [];
|
|
158
|
+
const expectedName = normalizeIdentifier(expected.name);
|
|
159
|
+
const actualName = normalizeIdentifier(actual.tableName);
|
|
160
|
+
|
|
161
|
+
if (expectedName !== actualName) {
|
|
162
|
+
differences.push(
|
|
163
|
+
`Table name mismatch. expected "${expectedName}", actual "${actualName}".`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!actual.tableExists) {
|
|
168
|
+
differences.push(`Table "${expectedName}" does not exist.`);
|
|
169
|
+
return { isSame: false, differences };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
compareColumns(expected, actual, differences);
|
|
173
|
+
comparePrimaryKey(expected, actual, differences);
|
|
174
|
+
compareUniqueKeys(expected, actual, differences);
|
|
175
|
+
compareIndexes(expected, actual, differences);
|
|
176
|
+
compareForeignKeys(expected, actual, differences);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
isSame: differences.length === 0,
|
|
180
|
+
differences,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const buildStructureSnapshotFromRows = (input) => {
|
|
185
|
+
const tableExistsValue =
|
|
186
|
+
Number(
|
|
187
|
+
input.tableExistsRows?.[0]?.['TABLE_EXISTS'] ??
|
|
188
|
+
input.tableExistsRows?.[0]?.['table_exists'] ??
|
|
189
|
+
0,
|
|
190
|
+
) === 1;
|
|
191
|
+
|
|
192
|
+
const columns = (input.columnRows ?? []).map((row) => ({
|
|
193
|
+
name: toUpperText(row['COLUMN_NAME'] ?? row['column_name']),
|
|
194
|
+
type: toUpperText(row['DATA_TYPE'] ?? row['data_type']),
|
|
195
|
+
size: toNumberOrUndefined(row['FIELD_SIZE'] ?? row['field_size']),
|
|
196
|
+
scale: toNumberOrUndefined(row['FIELD_SCALE'] ?? row['field_scale']),
|
|
197
|
+
nullable: Number(row['IS_NULLABLE'] ?? row['is_nullable'] ?? 1) === 1,
|
|
198
|
+
defaultExpression: normalizeExpression(
|
|
199
|
+
row['DEFAULT_SOURCE'] ?? row['default_source'],
|
|
200
|
+
),
|
|
201
|
+
computedBy: normalizeExpression(
|
|
202
|
+
row['COMPUTED_SOURCE'] ?? row['computed_source'],
|
|
203
|
+
),
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
const primaryKey = groupColumnsBySingleConstraint(input.primaryKeyRows ?? []);
|
|
207
|
+
const uniqueKeys = groupColumnsByConstraint(
|
|
208
|
+
input.uniqueKeyRows ?? [],
|
|
209
|
+
'CONSTRAINT_NAME',
|
|
210
|
+
);
|
|
211
|
+
const checks = groupCheckConstraints(input.checkConstraintRows ?? []);
|
|
212
|
+
const indexes = groupIndexes(input.indexRows ?? []);
|
|
213
|
+
const foreignKeys = groupForeignKeys(input.foreignKeyRows ?? []);
|
|
214
|
+
const incomingForeignKeys = groupIncomingForeignKeys(
|
|
215
|
+
input.incomingForeignKeyRows ?? [],
|
|
216
|
+
);
|
|
217
|
+
const columnDependencies = (input.columnDependencyRows ?? []).map((row) => ({
|
|
218
|
+
columnName: toUpperText(row['COLUMN_NAME'] ?? row['column_name']),
|
|
219
|
+
dependentName: toUpperText(row['DEPENDENT_NAME'] ?? row['dependent_name']),
|
|
220
|
+
dependentType: Number(row['DEPENDENT_TYPE'] ?? row['dependent_type'] ?? 0),
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
tableName: normalizeIdentifier(input.tableName),
|
|
225
|
+
tableExists: tableExistsValue,
|
|
226
|
+
columns,
|
|
227
|
+
primaryKey,
|
|
228
|
+
uniqueKeys,
|
|
229
|
+
checks,
|
|
230
|
+
indexes,
|
|
231
|
+
foreignKeys,
|
|
232
|
+
incomingForeignKeys,
|
|
233
|
+
columnDependencies,
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const generateExecuteBlockSql = (schema) => {
|
|
238
|
+
const statements = [];
|
|
239
|
+
|
|
240
|
+
for (const table of schema.tables) {
|
|
241
|
+
const createTable = buildCreateTableStatement(table);
|
|
242
|
+
const generatorName = normalizeIdentifier(
|
|
243
|
+
table.generatorName ?? `${table.name}_GEN`,
|
|
244
|
+
);
|
|
245
|
+
const triggerName = normalizeIdentifier(
|
|
246
|
+
table.triggerName ?? `${table.name}_BI`,
|
|
247
|
+
);
|
|
248
|
+
const identityColumn = normalizeIdentifier(
|
|
249
|
+
table.identityColumnName ?? 'ID',
|
|
250
|
+
);
|
|
251
|
+
const shouldGenerateIdentityTrigger =
|
|
252
|
+
!!table.identityColumnName &&
|
|
253
|
+
hasColumnName(table.columns, identityColumn);
|
|
254
|
+
const identityValueExpression = resolveIdentityValueExpression(
|
|
255
|
+
table,
|
|
256
|
+
identityColumn,
|
|
257
|
+
generatorName,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (shouldGenerateIdentityTrigger) {
|
|
261
|
+
statements.push(`CREATE GENERATOR ${generatorName};`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
statements.push(stripIdentifierQuotes(createTable));
|
|
265
|
+
|
|
266
|
+
if (shouldGenerateIdentityTrigger) {
|
|
267
|
+
statements.push(
|
|
268
|
+
buildIdentityTriggerStatement(
|
|
269
|
+
table.name,
|
|
270
|
+
triggerName,
|
|
271
|
+
identityColumn,
|
|
272
|
+
identityValueExpression,
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const index of table.indexes ?? []) {
|
|
278
|
+
for (const statement of buildIndexStatements(table.name, index)) {
|
|
279
|
+
statements.push(stripIdentifierQuotes(statement));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const foreignKey of table.foreignKeys ?? []) {
|
|
284
|
+
statements.push(
|
|
285
|
+
stripIdentifierQuotes(
|
|
286
|
+
buildAddForeignKeyStatement(table.name, foreignKey),
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return []
|
|
293
|
+
.concat('EXECUTE BLOCK AS BEGIN')
|
|
294
|
+
.concat(
|
|
295
|
+
statements.map(
|
|
296
|
+
(statement) => `EXECUTE STATEMENT ${toLiteral(statement)};`,
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
.concat('END')
|
|
300
|
+
.join(' ');
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const generateCreateTableSql = (table, createIfNotExists = true) => {
|
|
304
|
+
const createTable = buildCreateTableStatement(table);
|
|
305
|
+
if (!createIfNotExists) {
|
|
306
|
+
return createTable;
|
|
307
|
+
}
|
|
308
|
+
return wrapCreateIfMissing(createTable);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export const generateCreateSchemaSql = (schema, createIfNotExists = true) => {
|
|
312
|
+
const createTableStatements = [];
|
|
313
|
+
const createIndexStatements = [];
|
|
314
|
+
const addForeignKeyStatements = [];
|
|
315
|
+
|
|
316
|
+
for (const table of schema.tables) {
|
|
317
|
+
createTableStatements.push(
|
|
318
|
+
generateCreateTableSql(table, createIfNotExists),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
for (const index of table.indexes ?? []) {
|
|
322
|
+
for (const statement of buildIndexStatements(table.name, index)) {
|
|
323
|
+
createIndexStatements.push(
|
|
324
|
+
createIfNotExists ? wrapCreateIfMissing(statement) : statement,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const foreignKey of table.foreignKeys ?? []) {
|
|
330
|
+
const statement = buildAddForeignKeyStatement(table.name, foreignKey);
|
|
331
|
+
addForeignKeyStatements.push(
|
|
332
|
+
createIfNotExists ? wrapCreateIfMissing(statement) : statement,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return [
|
|
338
|
+
...createTableStatements,
|
|
339
|
+
...createIndexStatements,
|
|
340
|
+
...addForeignKeyStatements,
|
|
341
|
+
].join('\n\n');
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const buildCreateTableStatement = (table) => {
|
|
345
|
+
const tableBody = table.columns.map((column) => buildColumnDefinition(column));
|
|
346
|
+
|
|
347
|
+
if (table.primaryKey && table.primaryKey.columns.length > 0) {
|
|
348
|
+
const primaryKeyName = table.primaryKey.name ?? `PK_${table.name}`;
|
|
349
|
+
tableBody.push(
|
|
350
|
+
`CONSTRAINT ${quoteIdentifier(primaryKeyName)} PRIMARY KEY (${joinColumns(table.primaryKey.columns)})`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const uniqueKey of table.uniqueKeys ?? []) {
|
|
355
|
+
tableBody.push(
|
|
356
|
+
`CONSTRAINT ${quoteIdentifier(uniqueKey.name)} UNIQUE (${joinColumns(uniqueKey.columns)})`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const checkItem of table.checks ?? []) {
|
|
361
|
+
tableBody.push(
|
|
362
|
+
`CONSTRAINT ${quoteIdentifier(checkItem.name)} CHECK (${checkItem.expression})`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return `CREATE TABLE ${quoteIdentifier(table.name)} (\n ${tableBody.join(',\n ')}\n);`;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const buildColumnDefinition = (column) => {
|
|
370
|
+
const parts = [quoteIdentifier(column.name)];
|
|
371
|
+
|
|
372
|
+
if (column.computedBy) {
|
|
373
|
+
parts.push(`COMPUTED BY (${column.computedBy})`);
|
|
374
|
+
return parts.join(' ');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
parts.push(resolveColumnType(column));
|
|
378
|
+
|
|
379
|
+
if (column.defaultExpression) {
|
|
380
|
+
parts.push(`DEFAULT ${column.defaultExpression}`);
|
|
381
|
+
} else if (column.defaultValue !== undefined) {
|
|
382
|
+
parts.push(`DEFAULT ${toLiteral(column.defaultValue)}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (column.collate) {
|
|
386
|
+
parts.push(`COLLATE ${column.collate}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (column.check) {
|
|
390
|
+
parts.push(`CHECK (${column.check})`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (column.nullable === false) {
|
|
394
|
+
parts.push('NOT NULL');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return parts.join(' ');
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const resolveColumnType = (column) => {
|
|
401
|
+
const hasSize = typeof column.size === 'number';
|
|
402
|
+
const hasScale = typeof column.scale === 'number';
|
|
403
|
+
|
|
404
|
+
const normalizedType = normalizeFb25TypeName(column.type);
|
|
405
|
+
|
|
406
|
+
if (!hasSize) {
|
|
407
|
+
return normalizedType;
|
|
408
|
+
}
|
|
409
|
+
if (!hasScale) {
|
|
410
|
+
return `${normalizedType}(${column.size})`;
|
|
411
|
+
}
|
|
412
|
+
return `${normalizedType}(${column.size},${column.scale})`;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const buildCreateIndexStatement = (tableName, index) => {
|
|
416
|
+
const mode = [index.unique ? 'UNIQUE' : '', index.descending ? 'DESC' : '']
|
|
417
|
+
.filter(Boolean)
|
|
418
|
+
.join(' ');
|
|
419
|
+
const modePart = mode.length > 0 ? `${mode} ` : '';
|
|
420
|
+
|
|
421
|
+
if (index.expression) {
|
|
422
|
+
return `CREATE ${modePart}INDEX ${quoteIdentifier(index.name)} ON ${quoteIdentifier(tableName)} COMPUTED BY (${index.expression});`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return `CREATE ${modePart}INDEX ${quoteIdentifier(index.name)} ON ${quoteIdentifier(tableName)} (${joinColumns(index.columns ?? [])});`;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const buildAlterIndexInactiveStatement = (indexName) => {
|
|
429
|
+
return `ALTER INDEX ${quoteIdentifier(indexName)} INACTIVE;`;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const buildIndexStatements = (tableName, index) => {
|
|
433
|
+
const statements = [buildCreateIndexStatement(tableName, index)];
|
|
434
|
+
if (index.active === false) {
|
|
435
|
+
statements.push(buildAlterIndexInactiveStatement(index.name));
|
|
436
|
+
}
|
|
437
|
+
return statements;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const buildAddForeignKeyStatement = (tableName, foreignKey) => {
|
|
441
|
+
const clauses = [
|
|
442
|
+
`ALTER TABLE ${quoteIdentifier(tableName)}`,
|
|
443
|
+
`ADD CONSTRAINT ${quoteIdentifier(foreignKey.name)}`,
|
|
444
|
+
`FOREIGN KEY (${joinColumns(foreignKey.columns)})`,
|
|
445
|
+
`REFERENCES ${quoteIdentifier(foreignKey.referenceTable)} (${joinColumns(foreignKey.referenceColumns)})`,
|
|
446
|
+
...(foreignKey.onDelete ? [`ON DELETE ${foreignKey.onDelete}`] : []),
|
|
447
|
+
...(foreignKey.onUpdate ? [`ON UPDATE ${foreignKey.onUpdate}`] : []),
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
return `${clauses.join(' ')};`;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const buildIdentityTriggerStatement = (
|
|
454
|
+
tableName,
|
|
455
|
+
triggerName,
|
|
456
|
+
identityColumn,
|
|
457
|
+
valueExpression,
|
|
458
|
+
) => {
|
|
459
|
+
const normalizedTableName = normalizeIdentifier(tableName);
|
|
460
|
+
const normalizedTriggerName = normalizeIdentifier(triggerName);
|
|
461
|
+
const normalizedIdentityColumn = normalizeIdentifier(identityColumn);
|
|
462
|
+
return [
|
|
463
|
+
`CREATE TRIGGER ${normalizedTriggerName} FOR ${normalizedTableName} ACTIVE`,
|
|
464
|
+
'BEFORE INSERT POSITION 0 AS BEGIN',
|
|
465
|
+
`IF (NEW.${normalizedIdentityColumn} IS NULL) THEN NEW.${normalizedIdentityColumn} = ${valueExpression};`,
|
|
466
|
+
'END',
|
|
467
|
+
].join(' ');
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const resolveIdentityValueExpression = (table, identityColumn, generatorName) => {
|
|
471
|
+
return `GEN_ID(${normalizeIdentifier(generatorName)}, 1)`;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const wrapCreateIfMissing = (statement) => {
|
|
475
|
+
return [
|
|
476
|
+
'EXECUTE BLOCK AS',
|
|
477
|
+
'BEGIN',
|
|
478
|
+
` EXECUTE STATEMENT ${toLiteral(statement)};`,
|
|
479
|
+
'WHEN SQLCODE -607 DO',
|
|
480
|
+
'BEGIN',
|
|
481
|
+
'END',
|
|
482
|
+
'END',
|
|
483
|
+
].join('\n');
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const joinColumns = (columns) => {
|
|
487
|
+
return columns.map((column) => quoteIdentifier(column)).join(', ');
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const quoteIdentifier = (identifier) => {
|
|
491
|
+
const normalized = normalizeIdentifier(identifier);
|
|
492
|
+
return `"${normalized.replace(/"/g, '""')}"`;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const hasColumnName = (columns, columnName) => {
|
|
496
|
+
const normalizedColumnName = normalizeIdentifier(columnName);
|
|
497
|
+
return columns.some(
|
|
498
|
+
(column) => normalizeIdentifier(column.name) === normalizedColumnName,
|
|
499
|
+
);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const stripIdentifierQuotes = (statement) => {
|
|
503
|
+
return statement.replace(/"/g, '');
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const compareColumns = (expected, actual, differences) => {
|
|
507
|
+
const expectedMap = new Map();
|
|
508
|
+
const actualMap = new Map();
|
|
509
|
+
|
|
510
|
+
for (const column of expected.columns) {
|
|
511
|
+
expectedMap.set(column.name.trim().toUpperCase(), column);
|
|
512
|
+
}
|
|
513
|
+
for (const column of actual.columns) {
|
|
514
|
+
actualMap.set(column.name.trim().toUpperCase(), column);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
for (const [columnName, expectedColumn] of expectedMap) {
|
|
518
|
+
const actualColumn = actualMap.get(columnName);
|
|
519
|
+
if (!actualColumn) {
|
|
520
|
+
differences.push(`Column "${columnName}" is missing in actual table.`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const expectedType = normalizeExpectedColumnType(expectedColumn);
|
|
525
|
+
const actualType = normalizeActualColumnType(actualColumn);
|
|
526
|
+
if (expectedType !== actualType) {
|
|
527
|
+
differences.push(
|
|
528
|
+
`Column "${columnName}" type mismatch. expected ${expectedType}, actual ${actualType}.`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const expectedNullable = expectedColumn.nullable !== false;
|
|
533
|
+
const actualNullable =
|
|
534
|
+
expectedNullable ||
|
|
535
|
+
!hasFirebird25NotNullCheck(actual, expectedColumn.name)
|
|
536
|
+
? actualColumn.nullable !== false
|
|
537
|
+
: false;
|
|
538
|
+
if (expectedNullable !== actualNullable) {
|
|
539
|
+
differences.push(
|
|
540
|
+
`Column "${columnName}" nullable mismatch. expected ${expectedNullable}, actual ${actualNullable}.`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (expectedColumn.defaultExpression) {
|
|
545
|
+
const expectedDefault = normalizeExpression(
|
|
546
|
+
expectedColumn.defaultExpression,
|
|
547
|
+
);
|
|
548
|
+
const actualDefault = normalizeExpression(actualColumn.defaultExpression);
|
|
549
|
+
if (expectedDefault !== actualDefault) {
|
|
550
|
+
differences.push(
|
|
551
|
+
`Column "${columnName}" default expression mismatch. expected "${expectedDefault}", actual "${actualDefault}".`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
} else if (expectedColumn.defaultValue !== undefined) {
|
|
555
|
+
const expectedDefault = normalizeExpression(
|
|
556
|
+
toLiteral(expectedColumn.defaultValue),
|
|
557
|
+
);
|
|
558
|
+
const actualDefault = normalizeExpression(actualColumn.defaultExpression);
|
|
559
|
+
if (expectedDefault !== actualDefault) {
|
|
560
|
+
differences.push(
|
|
561
|
+
`Column "${columnName}" default value mismatch. expected "${expectedDefault}", actual "${actualDefault}".`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (expectedColumn.computedBy) {
|
|
567
|
+
const expectedComputed = normalizeExpression(expectedColumn.computedBy);
|
|
568
|
+
const actualComputed = normalizeExpression(actualColumn.computedBy);
|
|
569
|
+
if (expectedComputed !== actualComputed) {
|
|
570
|
+
differences.push(
|
|
571
|
+
`Column "${columnName}" computed expression mismatch. expected "${expectedComputed}", actual "${actualComputed}".`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
for (const columnName of actualMap.keys()) {
|
|
578
|
+
if (!expectedMap.has(columnName)) {
|
|
579
|
+
differences.push(
|
|
580
|
+
`Column "${columnName}" exists in actual table but not in expected JSON.`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const hasFirebird25NotNullCheck = (actual, columnName) => {
|
|
587
|
+
return (actual.checks ?? []).some((check) =>
|
|
588
|
+
isFirebird25NotNullCheckForColumn(check, columnName),
|
|
589
|
+
);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const isFirebird25NotNullCheckForColumn = (check, columnName) => {
|
|
593
|
+
const expression = String(check?.expression ?? '')
|
|
594
|
+
.replace(/^CHECK\s*/i, '')
|
|
595
|
+
.replace(/"/g, '')
|
|
596
|
+
.replace(/\s+/g, '')
|
|
597
|
+
.toUpperCase();
|
|
598
|
+
const normalizedColumnName = normalizeIdentifier(columnName);
|
|
599
|
+
return (
|
|
600
|
+
expression === `${normalizedColumnName}ISNOTNULL` ||
|
|
601
|
+
expression === `(${normalizedColumnName}ISNOTNULL)`
|
|
602
|
+
);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const comparePrimaryKey = (expected, actual, differences) => {
|
|
606
|
+
const expectedPk = (expected.primaryKey?.columns ?? []).map((column) =>
|
|
607
|
+
column.trim().toUpperCase(),
|
|
608
|
+
);
|
|
609
|
+
const actualPk = (actual.primaryKey ?? []).map((column) =>
|
|
610
|
+
column.trim().toUpperCase(),
|
|
611
|
+
);
|
|
612
|
+
if (!isSameOrderedArray(expectedPk, actualPk)) {
|
|
613
|
+
differences.push(
|
|
614
|
+
`Primary key mismatch. expected [${expectedPk.join(', ')}], actual [${actualPk.join(', ')}].`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const compareUniqueKeys = (expected, actual, differences) => {
|
|
620
|
+
const expectedMap = new Map();
|
|
621
|
+
const actualMap = new Map();
|
|
622
|
+
|
|
623
|
+
for (const uniqueKey of expected.uniqueKeys ?? []) {
|
|
624
|
+
expectedMap.set(
|
|
625
|
+
uniqueKey.name.trim().toUpperCase(),
|
|
626
|
+
uniqueKey.columns.map((column) => column.trim().toUpperCase()),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
for (const uniqueKey of actual.uniqueKeys ?? []) {
|
|
630
|
+
actualMap.set(
|
|
631
|
+
uniqueKey.name.trim().toUpperCase(),
|
|
632
|
+
uniqueKey.columns.map((column) => column.trim().toUpperCase()),
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
compareNamedColumnCollections('Unique key', expectedMap, actualMap, differences);
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const compareIndexes = (expected, actual, differences) => {
|
|
640
|
+
const expectedMap = new Map();
|
|
641
|
+
const actualMap = new Map();
|
|
642
|
+
|
|
643
|
+
for (const index of expected.indexes ?? []) {
|
|
644
|
+
expectedMap.set(index.name.trim().toUpperCase(), index);
|
|
645
|
+
}
|
|
646
|
+
for (const index of actual.indexes ?? []) {
|
|
647
|
+
actualMap.set(index.name.trim().toUpperCase(), index);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
for (const [indexName, expectedIndex] of expectedMap) {
|
|
651
|
+
const actualIndex = actualMap.get(indexName);
|
|
652
|
+
if (!actualIndex) {
|
|
653
|
+
differences.push(`Index "${indexName}" is missing in actual table.`);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const expectedColumns = (expectedIndex.columns ?? []).map((column) =>
|
|
658
|
+
column.trim().toUpperCase(),
|
|
659
|
+
);
|
|
660
|
+
const actualColumns = (actualIndex.columns ?? []).map((column) =>
|
|
661
|
+
column.trim().toUpperCase(),
|
|
662
|
+
);
|
|
663
|
+
if (!isSameOrderedArray(expectedColumns, actualColumns)) {
|
|
664
|
+
differences.push(
|
|
665
|
+
`Index "${indexName}" columns mismatch. expected [${expectedColumns.join(', ')}], actual [${actualColumns.join(', ')}].`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const expectedExpression = normalizeExpression(expectedIndex.expression);
|
|
670
|
+
const actualExpression = normalizeExpression(actualIndex.expression);
|
|
671
|
+
if (expectedExpression !== actualExpression) {
|
|
672
|
+
differences.push(
|
|
673
|
+
`Index "${indexName}" expression mismatch. expected "${expectedExpression}", actual "${actualExpression}".`,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const expectedUnique = expectedIndex.unique === true;
|
|
678
|
+
const actualUnique = actualIndex.unique === true;
|
|
679
|
+
if (expectedUnique !== actualUnique) {
|
|
680
|
+
differences.push(
|
|
681
|
+
`Index "${indexName}" unique flag mismatch. expected ${expectedUnique}, actual ${actualUnique}.`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const expectedDescending = expectedIndex.descending === true;
|
|
686
|
+
const actualDescending = actualIndex.descending === true;
|
|
687
|
+
if (expectedDescending !== actualDescending) {
|
|
688
|
+
differences.push(
|
|
689
|
+
`Index "${indexName}" descending flag mismatch. expected ${expectedDescending}, actual ${actualDescending}.`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
for (const indexName of actualMap.keys()) {
|
|
695
|
+
if (!expectedMap.has(indexName)) {
|
|
696
|
+
differences.push(
|
|
697
|
+
`Index "${indexName}" exists in actual table but not in expected JSON.`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const compareForeignKeys = (expected, actual, differences) => {
|
|
704
|
+
const expectedMap = new Map();
|
|
705
|
+
const actualMap = new Map();
|
|
706
|
+
|
|
707
|
+
for (const fk of expected.foreignKeys ?? []) {
|
|
708
|
+
expectedMap.set(fk.name.trim().toUpperCase(), fk);
|
|
709
|
+
}
|
|
710
|
+
for (const fk of actual.foreignKeys ?? []) {
|
|
711
|
+
actualMap.set(fk.name.trim().toUpperCase(), fk);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
for (const [fkName, expectedFk] of expectedMap) {
|
|
715
|
+
const actualFk = actualMap.get(fkName);
|
|
716
|
+
if (!actualFk) {
|
|
717
|
+
differences.push(`Foreign key "${fkName}" is missing in actual table.`);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const expectedColumns = expectedFk.columns.map((column) =>
|
|
722
|
+
column.trim().toUpperCase(),
|
|
723
|
+
);
|
|
724
|
+
const actualColumns = actualFk.columns.map((column) =>
|
|
725
|
+
column.trim().toUpperCase(),
|
|
726
|
+
);
|
|
727
|
+
if (!isSameOrderedArray(expectedColumns, actualColumns)) {
|
|
728
|
+
differences.push(
|
|
729
|
+
`Foreign key "${fkName}" local columns mismatch. expected [${expectedColumns.join(', ')}], actual [${actualColumns.join(', ')}].`,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const expectedRefTable = expectedFk.referenceTable.trim().toUpperCase();
|
|
734
|
+
const actualRefTable = actualFk.referenceTable.trim().toUpperCase();
|
|
735
|
+
if (expectedRefTable !== actualRefTable) {
|
|
736
|
+
differences.push(
|
|
737
|
+
`Foreign key "${fkName}" reference table mismatch. expected "${expectedRefTable}", actual "${actualRefTable}".`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const expectedRefColumns = expectedFk.referenceColumns.map((column) =>
|
|
742
|
+
column.trim().toUpperCase(),
|
|
743
|
+
);
|
|
744
|
+
const actualRefColumns = actualFk.referenceColumns.map((column) =>
|
|
745
|
+
column.trim().toUpperCase(),
|
|
746
|
+
);
|
|
747
|
+
if (!isSameOrderedArray(expectedRefColumns, actualRefColumns)) {
|
|
748
|
+
differences.push(
|
|
749
|
+
`Foreign key "${fkName}" reference columns mismatch. expected [${expectedRefColumns.join(', ')}], actual [${actualRefColumns.join(', ')}].`,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const expectedDeleteRule = normalizeRule(expectedFk.onDelete);
|
|
754
|
+
const actualDeleteRule = normalizeRule(actualFk.onDelete);
|
|
755
|
+
if (expectedDeleteRule !== actualDeleteRule) {
|
|
756
|
+
differences.push(
|
|
757
|
+
`Foreign key "${fkName}" ON DELETE mismatch. expected "${expectedDeleteRule}", actual "${actualDeleteRule}".`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const expectedUpdateRule = normalizeRule(expectedFk.onUpdate);
|
|
762
|
+
const actualUpdateRule = normalizeRule(actualFk.onUpdate);
|
|
763
|
+
if (expectedUpdateRule !== actualUpdateRule) {
|
|
764
|
+
differences.push(
|
|
765
|
+
`Foreign key "${fkName}" ON UPDATE mismatch. expected "${expectedUpdateRule}", actual "${actualUpdateRule}".`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
for (const fkName of actualMap.keys()) {
|
|
771
|
+
if (!expectedMap.has(fkName)) {
|
|
772
|
+
differences.push(
|
|
773
|
+
`Foreign key "${fkName}" exists in actual table but not in expected JSON.`,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const compareNamedColumnCollections = (label, expected, actual, differences) => {
|
|
780
|
+
for (const [name, expectedColumns] of expected) {
|
|
781
|
+
const actualColumns = actual.get(name);
|
|
782
|
+
if (!actualColumns) {
|
|
783
|
+
differences.push(`${label} "${name}" is missing in actual table.`);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (!isSameOrderedArray(expectedColumns, actualColumns)) {
|
|
787
|
+
differences.push(
|
|
788
|
+
`${label} "${name}" columns mismatch. expected [${expectedColumns.join(', ')}], actual [${actualColumns.join(', ')}].`,
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
for (const name of actual.keys()) {
|
|
793
|
+
if (!expected.has(name)) {
|
|
794
|
+
differences.push(`${label} "${name}" exists in actual table but not in expected JSON.`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const groupColumnsBySingleConstraint = (rows) => {
|
|
800
|
+
return [...rows]
|
|
801
|
+
.sort(
|
|
802
|
+
(a, b) =>
|
|
803
|
+
Number(a['COLUMN_POSITION'] ?? a['column_position'] ?? 0) -
|
|
804
|
+
Number(b['COLUMN_POSITION'] ?? b['column_position'] ?? 0),
|
|
805
|
+
)
|
|
806
|
+
.map((row) => toUpperText(row['COLUMN_NAME'] ?? row['column_name']))
|
|
807
|
+
.filter((name) => name.length > 0);
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
const groupColumnsByConstraint = (rows, constraintNameKey) => {
|
|
811
|
+
const map = new Map();
|
|
812
|
+
for (const row of rows) {
|
|
813
|
+
const name = toUpperText(
|
|
814
|
+
row[constraintNameKey] ?? row[constraintNameKey.toLowerCase()],
|
|
815
|
+
);
|
|
816
|
+
if (!map.has(name)) {
|
|
817
|
+
map.set(name, []);
|
|
818
|
+
}
|
|
819
|
+
map.get(name)?.push({
|
|
820
|
+
position: Number(row['COLUMN_POSITION'] ?? row['column_position'] ?? 0),
|
|
821
|
+
column: toUpperText(row['COLUMN_NAME'] ?? row['column_name']),
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return [...map.entries()].map(([name, data]) => ({
|
|
826
|
+
name,
|
|
827
|
+
columns: data.sort((a, b) => a.position - b.position).map((item) => item.column),
|
|
828
|
+
}));
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const groupCheckConstraints = (rows) => {
|
|
832
|
+
return (rows ?? [])
|
|
833
|
+
.map((row) => ({
|
|
834
|
+
name: toUpperText(row['CONSTRAINT_NAME'] ?? row['constraint_name']),
|
|
835
|
+
expression: normalizeExpression(
|
|
836
|
+
row['CHECK_SOURCE'] ??
|
|
837
|
+
row['check_source'] ??
|
|
838
|
+
row['EXPRESSION'] ??
|
|
839
|
+
row['expression'],
|
|
840
|
+
),
|
|
841
|
+
}))
|
|
842
|
+
.filter((check) => check.name.length > 0);
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const groupIndexes = (rows) => {
|
|
846
|
+
const map = new Map();
|
|
847
|
+
|
|
848
|
+
for (const row of rows) {
|
|
849
|
+
const name = toUpperText(row['INDEX_NAME'] ?? row['index_name']);
|
|
850
|
+
if (!map.has(name)) {
|
|
851
|
+
map.set(name, {
|
|
852
|
+
unique: Number(row['IS_UNIQUE'] ?? row['is_unique'] ?? 0) === 1,
|
|
853
|
+
descending:
|
|
854
|
+
Number(row['IS_DESCENDING'] ?? row['is_descending'] ?? 0) === 1,
|
|
855
|
+
active: Number(row['IS_ACTIVE'] ?? row['is_active'] ?? 1) === 1,
|
|
856
|
+
expression: normalizeExpression(
|
|
857
|
+
row['EXPRESSION_SOURCE'] ?? row['expression_source'],
|
|
858
|
+
),
|
|
859
|
+
columns: [],
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
const columnName = toUpperText(row['COLUMN_NAME'] ?? row['column_name']);
|
|
863
|
+
if (columnName.length > 0) {
|
|
864
|
+
map.get(name)?.columns.push({
|
|
865
|
+
position: Number(row['COLUMN_POSITION'] ?? row['column_position'] ?? 0),
|
|
866
|
+
column: columnName,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return [...map.entries()].map(([name, index]) => ({
|
|
872
|
+
name,
|
|
873
|
+
unique: index.unique,
|
|
874
|
+
descending: index.descending,
|
|
875
|
+
active: index.active,
|
|
876
|
+
expression: index.expression,
|
|
877
|
+
columns: index.columns
|
|
878
|
+
.sort((a, b) => a.position - b.position)
|
|
879
|
+
.map((item) => item.column),
|
|
880
|
+
}));
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const groupForeignKeys = (rows) => {
|
|
884
|
+
const map = new Map();
|
|
885
|
+
|
|
886
|
+
for (const row of rows) {
|
|
887
|
+
const name = toUpperText(row['FK_NAME'] ?? row['fk_name']);
|
|
888
|
+
if (!map.has(name)) {
|
|
889
|
+
map.set(name, {
|
|
890
|
+
referenceTable: toUpperText(
|
|
891
|
+
row['REF_TABLE_NAME'] ?? row['ref_table_name'],
|
|
892
|
+
),
|
|
893
|
+
deleteRule: normalizeRule(row['DELETE_RULE'] ?? row['delete_rule']),
|
|
894
|
+
updateRule: normalizeRule(row['UPDATE_RULE'] ?? row['update_rule']),
|
|
895
|
+
columns: [],
|
|
896
|
+
referenceColumns: [],
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
map.get(name)?.columns.push({
|
|
901
|
+
position: Number(row['COLUMN_POSITION'] ?? row['column_position'] ?? 0),
|
|
902
|
+
column: toUpperText(row['COLUMN_NAME'] ?? row['column_name']),
|
|
903
|
+
});
|
|
904
|
+
map.get(name)?.referenceColumns.push({
|
|
905
|
+
position: Number(row['COLUMN_POSITION'] ?? row['column_position'] ?? 0),
|
|
906
|
+
column: toUpperText(row['REF_COLUMN_NAME'] ?? row['ref_column_name']),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return [...map.entries()].map(([name, fk]) => ({
|
|
911
|
+
name,
|
|
912
|
+
referenceTable: fk.referenceTable,
|
|
913
|
+
onDelete: fk.deleteRule,
|
|
914
|
+
onUpdate: fk.updateRule,
|
|
915
|
+
columns: fk.columns.sort((a, b) => a.position - b.position).map((item) => item.column),
|
|
916
|
+
referenceColumns: fk.referenceColumns
|
|
917
|
+
.sort((a, b) => a.position - b.position)
|
|
918
|
+
.map((item) => item.column),
|
|
919
|
+
}));
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const groupIncomingForeignKeys = (rows) => {
|
|
923
|
+
const map = new Map();
|
|
924
|
+
|
|
925
|
+
for (const row of rows) {
|
|
926
|
+
const fkName = toUpperText(row['FK_NAME'] ?? row['fk_name']);
|
|
927
|
+
const fkTableName = toUpperText(row['FK_TABLE_NAME'] ?? row['fk_table_name']);
|
|
928
|
+
const key = `${fkTableName}|${fkName}`;
|
|
929
|
+
if (!map.has(key)) {
|
|
930
|
+
map.set(key, {
|
|
931
|
+
name: fkName,
|
|
932
|
+
tableName: fkTableName,
|
|
933
|
+
columns: [],
|
|
934
|
+
referenceColumns: [],
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
map.get(key)?.columns.push({
|
|
939
|
+
position: Number(row['COLUMN_POSITION'] ?? row['column_position'] ?? 0),
|
|
940
|
+
column: toUpperText(row['FK_COLUMN_NAME'] ?? row['fk_column_name']),
|
|
941
|
+
});
|
|
942
|
+
map.get(key)?.referenceColumns.push({
|
|
943
|
+
position: Number(row['COLUMN_POSITION'] ?? row['column_position'] ?? 0),
|
|
944
|
+
column: toUpperText(row['REF_COLUMN_NAME'] ?? row['ref_column_name']),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return [...map.values()].map((item) => ({
|
|
949
|
+
name: item.name,
|
|
950
|
+
tableName: item.tableName,
|
|
951
|
+
columns: item.columns.sort((a, b) => a.position - b.position).map((row) => row.column),
|
|
952
|
+
referenceColumns: item.referenceColumns
|
|
953
|
+
.sort((a, b) => a.position - b.position)
|
|
954
|
+
.map((row) => row.column),
|
|
955
|
+
}));
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const normalizeExpectedColumnType = (column) => {
|
|
959
|
+
return normalizeType(column.type, column.size, column.scale);
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const normalizeActualColumnType = (column) => {
|
|
963
|
+
return normalizeType(column.type, column.size, column.scale);
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const normalizeType = (type, size, scale) => {
|
|
967
|
+
const baseType = normalizeFb25TypeName(type);
|
|
968
|
+
const supportsLength = ['CHAR', 'VARCHAR'].includes(baseType);
|
|
969
|
+
const supportsPrecision = ['NUMERIC', 'DECIMAL'].includes(baseType);
|
|
970
|
+
const hasSize = typeof size === 'number' && Number.isFinite(size) && size > 0;
|
|
971
|
+
const hasScale = typeof scale === 'number' && Number.isFinite(scale);
|
|
972
|
+
|
|
973
|
+
if (!supportsLength && !supportsPrecision) {
|
|
974
|
+
return baseType;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (!hasSize) {
|
|
978
|
+
return baseType;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (supportsLength) {
|
|
982
|
+
return `${baseType}(${size})`;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (!hasScale || scale === 0) {
|
|
986
|
+
return `${baseType}(${size})`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return `${baseType}(${size},${scale})`;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
const normalizeExpression = (expression) => {
|
|
993
|
+
if (typeof expression !== 'string') {
|
|
994
|
+
return '';
|
|
995
|
+
}
|
|
996
|
+
return expression.replace(/\s+/g, ' ').trim().toUpperCase();
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const normalizeRule = (value) => {
|
|
1000
|
+
const text = toUpperText(value);
|
|
1001
|
+
if (text.length === 0) {
|
|
1002
|
+
return undefined;
|
|
1003
|
+
}
|
|
1004
|
+
if (text === 'RESTRICT') {
|
|
1005
|
+
return 'NO ACTION';
|
|
1006
|
+
}
|
|
1007
|
+
if (
|
|
1008
|
+
text === 'NO ACTION' ||
|
|
1009
|
+
text === 'CASCADE' ||
|
|
1010
|
+
text === 'SET NULL' ||
|
|
1011
|
+
text === 'SET DEFAULT'
|
|
1012
|
+
) {
|
|
1013
|
+
return text;
|
|
1014
|
+
}
|
|
1015
|
+
return undefined;
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const isSameOrderedArray = (expected, actual) => {
|
|
1019
|
+
if (expected.length !== actual.length) {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1023
|
+
if (expected[i] !== actual[i]) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return true;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const normalizeIdentifier = (value) => {
|
|
1031
|
+
const source = String(value ?? '');
|
|
1032
|
+
// Skip the camelCase->snake_case split for already-uppercase input (e.g. a hash-suffixed
|
|
1033
|
+
// name this function itself already produced) -- otherwise a hex hash like "1E8A9B" gets
|
|
1034
|
+
// spurious underscores inserted at digit->letter boundaries ("1_E8_A9_B"), pushing an
|
|
1035
|
+
// already-valid 31-char identifier over the limit and forcing a second, different hash.
|
|
1036
|
+
const withWordBreaks = /[a-z]/.test(source) ? source.replace(/([a-z0-9])([A-Z])/g, '$1_$2') : source;
|
|
1037
|
+
const normalized = withWordBreaks
|
|
1038
|
+
.replace(/[^A-Za-z0-9_$]+/g, '_')
|
|
1039
|
+
.replace(/_+/g, '_')
|
|
1040
|
+
.replace(/^_+|_+$/g, '')
|
|
1041
|
+
.toUpperCase();
|
|
1042
|
+
const raw = normalized || 'COL';
|
|
1043
|
+
if (raw.length <= 31) {
|
|
1044
|
+
return raw;
|
|
1045
|
+
}
|
|
1046
|
+
const hash = toSimpleHash(raw).slice(0, 6);
|
|
1047
|
+
const leadingLength = 31 - hash.length - 1;
|
|
1048
|
+
// Strip a trailing underscore left by the slice so we never emit a double underscore before
|
|
1049
|
+
// the hash -- a lone trailing "_" here would otherwise get silently collapsed by any later
|
|
1050
|
+
// re-normalization pass, producing a different identifier than this function itself stored.
|
|
1051
|
+
return raw.slice(0, leadingLength).replace(/_+$/, '') + '_' + hash;
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const toSimpleHash = (input) => {
|
|
1055
|
+
let hash = 0;
|
|
1056
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1057
|
+
hash = (hash << 5) - hash + input.charCodeAt(index);
|
|
1058
|
+
hash |= 0;
|
|
1059
|
+
}
|
|
1060
|
+
return Math.abs(hash).toString(16).toUpperCase();
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
const toUpperText = (value) => {
|
|
1064
|
+
if (typeof value !== 'string') {
|
|
1065
|
+
return '';
|
|
1066
|
+
}
|
|
1067
|
+
return value.trim().toUpperCase();
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
const toNumberOrUndefined = (value) => {
|
|
1071
|
+
if (value == null || value === '') {
|
|
1072
|
+
return undefined;
|
|
1073
|
+
}
|
|
1074
|
+
const parsed = Number(value);
|
|
1075
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
const normalizeFb25TypeName = (type) => {
|
|
1079
|
+
const upperType = toUpperText(type);
|
|
1080
|
+
if (upperType === 'BOOLEAN') {
|
|
1081
|
+
return 'SMALLINT';
|
|
1082
|
+
}
|
|
1083
|
+
return upperType;
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
const toLiteral = (value) => {
|
|
1087
|
+
if (value === null) {
|
|
1088
|
+
return 'NULL';
|
|
1089
|
+
}
|
|
1090
|
+
if (typeof value === 'number') {
|
|
1091
|
+
return `${value}`;
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof value === 'boolean') {
|
|
1094
|
+
return value ? '1' : '0';
|
|
1095
|
+
}
|
|
1096
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1097
|
+
};
|