relq 1.0.48 → 1.0.50
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/dist/cjs/cli/commands/add.cjs +8 -6
- package/dist/cjs/cli/commands/import.cjs +1 -1
- package/dist/cjs/cli/commands/pull.cjs +81 -10
- package/dist/cjs/cli/commands/push.cjs +53 -15
- package/dist/cjs/cli/utils/ast-codegen.cjs +6 -5
- package/dist/cjs/cli/utils/ast-transformer.cjs +5 -1
- package/dist/cjs/cli/utils/change-tracker.cjs +16 -0
- package/dist/cjs/cli/utils/pg-parser.cjs +257 -0
- package/dist/cjs/cli/utils/schema-comparator.cjs +106 -18
- package/dist/cjs/cli/utils/schema-to-ast.cjs +243 -18
- package/dist/cjs/schema-definition/schema-builder.cjs +42 -0
- package/dist/cjs/schema-definition/table-definition.cjs +13 -2
- package/dist/config.d.ts +20 -0
- package/dist/esm/cli/commands/add.js +8 -6
- package/dist/esm/cli/commands/import.js +1 -1
- package/dist/esm/cli/commands/pull.js +83 -12
- package/dist/esm/cli/commands/push.js +53 -15
- package/dist/esm/cli/utils/ast-codegen.js +6 -5
- package/dist/esm/cli/utils/ast-transformer.js +5 -1
- package/dist/esm/cli/utils/change-tracker.js +16 -0
- package/dist/esm/cli/utils/pg-parser.js +244 -1
- package/dist/esm/cli/utils/schema-comparator.js +106 -18
- package/dist/esm/cli/utils/schema-to-ast.js +239 -18
- package/dist/esm/schema-definition/schema-builder.js +38 -0
- package/dist/esm/schema-definition/table-definition.js +14 -3
- package/dist/index.d.ts +21 -1
- package/dist/schema-builder.d.ts +7 -0
- package/package.json +1 -1
|
@@ -271,31 +271,33 @@ function parseSchemaFileForComparison(schemaPath) {
|
|
|
271
271
|
definition: tableCheckMatch[2].trim(),
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
|
-
const checkRegexNew = /check\.constraint\s*\(\s*['"]([^'"]+)['"]/g;
|
|
274
|
+
const checkRegexNew = /check\.constraint\s*\(\s*['"]([^'"]+)['"]\s*,\s*(?:sql`([^`]*)`|([^)]+\)))/g;
|
|
275
275
|
let newCheckMatch;
|
|
276
276
|
while ((newCheckMatch = checkRegexNew.exec(optionsBlock)) !== null) {
|
|
277
277
|
const constraintName = newCheckMatch[1];
|
|
278
|
+
const expression = (newCheckMatch[2] || newCheckMatch[3] || '').trim();
|
|
278
279
|
if (!constraints.some(c => c.name === constraintName)) {
|
|
279
280
|
constraints.push({
|
|
280
281
|
name: constraintName,
|
|
281
282
|
type: 'CHECK',
|
|
282
283
|
columns: [],
|
|
283
|
-
definition: '',
|
|
284
|
+
definition: expression ? `CHECK (${expression})` : '',
|
|
284
285
|
});
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
288
|
const checkConstraintsBlockMatch = optionsBlock.match(/checkConstraints:\s*\([^)]+\)\s*=>\s*\[([^\]]+)\]/s);
|
|
288
289
|
if (checkConstraintsBlockMatch) {
|
|
289
290
|
const checkBlock = checkConstraintsBlockMatch[1];
|
|
290
|
-
const
|
|
291
|
-
for (const match of
|
|
291
|
+
const constraintMatches = checkBlock.matchAll(/check\.constraint\s*\(\s*['"]([^'"]+)['"]\s*,\s*(?:sql`([^`]*)`|([^)]+\)))/g);
|
|
292
|
+
for (const match of constraintMatches) {
|
|
292
293
|
const constraintName = match[1];
|
|
294
|
+
const expression = (match[2] || match[3] || '').trim();
|
|
293
295
|
if (!constraints.some(c => c.name === constraintName)) {
|
|
294
296
|
constraints.push({
|
|
295
297
|
name: constraintName,
|
|
296
298
|
type: 'CHECK',
|
|
297
299
|
columns: [],
|
|
298
|
-
definition: '',
|
|
300
|
+
definition: expression ? `CHECK (${expression})` : '',
|
|
299
301
|
});
|
|
300
302
|
}
|
|
301
303
|
}
|
|
@@ -867,7 +869,7 @@ async function addCommand(context) {
|
|
|
867
869
|
const snapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
|
|
868
870
|
if (currentSchema && snapshot) {
|
|
869
871
|
const snapshotAsDbSchema = snapshotToDatabaseSchema(snapshot);
|
|
870
|
-
const schemaChanges = (0, schema_comparator_1.compareSchemas)(snapshotAsDbSchema, currentSchema);
|
|
872
|
+
const schemaChanges = await (0, schema_comparator_1.compareSchemas)(snapshotAsDbSchema, currentSchema);
|
|
871
873
|
(0, repo_manager_1.cleanupStagedChanges)(schemaChanges, projectRoot);
|
|
872
874
|
if (schemaChanges.length > 0) {
|
|
873
875
|
(0, repo_manager_1.clearUnstagedChanges)(projectRoot);
|
|
@@ -278,7 +278,7 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
278
278
|
mergedSchema = mergeSchemas(existingSnapshot, incomingSchema, replaceAll);
|
|
279
279
|
const beforeSchema = snapshotToDbSchema(existingSnapshot);
|
|
280
280
|
const afterSchema = snapshotToDbSchema(mergedSchema);
|
|
281
|
-
changes = (0, schema_comparator_1.compareSchemas)(beforeSchema, afterSchema);
|
|
281
|
+
changes = await (0, schema_comparator_1.compareSchemas)(beforeSchema, afterSchema);
|
|
282
282
|
spinner.stop();
|
|
283
283
|
if (changes.length > 0) {
|
|
284
284
|
console.log('');
|
|
@@ -180,7 +180,12 @@ async function pullCommand(context) {
|
|
|
180
180
|
const merge = flags['merge'] === true;
|
|
181
181
|
const autoCommit = flags['commit'] === true;
|
|
182
182
|
const dryRun = flags['dry-run'] === true;
|
|
183
|
+
const theirs = flags['theirs'] === true;
|
|
184
|
+
const ours = flags['ours'] === true;
|
|
183
185
|
const author = config.author || 'Relq CLI';
|
|
186
|
+
if (theirs && ours) {
|
|
187
|
+
(0, cli_utils_1.fatal)('Cannot use both --theirs and --ours', 'Choose one conflict resolution strategy.');
|
|
188
|
+
}
|
|
184
189
|
const schemaPathRaw = (0, config_loader_1.getSchemaPath)(config);
|
|
185
190
|
const schemaPath = path.resolve(projectRoot, schemaPathRaw);
|
|
186
191
|
const includeFunctions = config.includeFunctions ?? true;
|
|
@@ -239,6 +244,14 @@ async function pullCommand(context) {
|
|
|
239
244
|
if (remoteHead) {
|
|
240
245
|
(0, repo_manager_1.setFetchHead)(remoteHead, projectRoot);
|
|
241
246
|
}
|
|
247
|
+
const localCommits = (0, repo_manager_1.getAllCommits)(projectRoot);
|
|
248
|
+
const localHashes = new Set(localCommits.map(c => c.hash));
|
|
249
|
+
const missingCommits = remoteCommits.filter(c => !localHashes.has(c.hash));
|
|
250
|
+
if (missingCommits.length > 0) {
|
|
251
|
+
for (const commit of missingCommits.reverse()) {
|
|
252
|
+
(0, repo_manager_1.saveCommit)(commit, projectRoot);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
242
255
|
spinner.succeed(`Fetched ${remoteCommits.length} remote commits`);
|
|
243
256
|
console.log('');
|
|
244
257
|
console.log(cli_utils_1.colors.bold('Introspecting database...'));
|
|
@@ -418,7 +431,16 @@ async function pullCommand(context) {
|
|
|
418
431
|
comment: t.comment,
|
|
419
432
|
})),
|
|
420
433
|
functions: localSnapshot.functions || [],
|
|
421
|
-
triggers: localSnapshot.triggers || []
|
|
434
|
+
triggers: (localSnapshot.triggers || []).map(t => ({
|
|
435
|
+
name: t.name,
|
|
436
|
+
tableName: t.table,
|
|
437
|
+
event: t.events?.[0] || 'UPDATE',
|
|
438
|
+
timing: t.timing,
|
|
439
|
+
forEach: t.forEach || 'STATEMENT',
|
|
440
|
+
functionName: t.functionName,
|
|
441
|
+
definition: '',
|
|
442
|
+
isEnabled: true,
|
|
443
|
+
})),
|
|
422
444
|
};
|
|
423
445
|
const remoteForCompare = {
|
|
424
446
|
extensions: dbSchema.extensions || [],
|
|
@@ -460,7 +482,7 @@ async function pullCommand(context) {
|
|
|
460
482
|
functions: filteredFunctions || [],
|
|
461
483
|
triggers: filteredTriggers || [],
|
|
462
484
|
};
|
|
463
|
-
const allChanges = (0, schema_comparator_1.compareSchemas)(localForCompare, remoteForCompare);
|
|
485
|
+
const allChanges = await (0, schema_comparator_1.compareSchemas)(localForCompare, remoteForCompare);
|
|
464
486
|
const changeDisplays = [];
|
|
465
487
|
for (const change of allChanges) {
|
|
466
488
|
const objType = change.objectType;
|
|
@@ -642,11 +664,44 @@ async function pullCommand(context) {
|
|
|
642
664
|
console.log(` ${cli_utils_1.colors.cyan(schemaPath)}`);
|
|
643
665
|
console.log('');
|
|
644
666
|
if (!dryRun) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
667
|
+
if (theirs) {
|
|
668
|
+
console.log(`${cli_utils_1.colors.green('Using --theirs:')} Overwriting local schema with database version`);
|
|
669
|
+
console.log('');
|
|
670
|
+
}
|
|
671
|
+
else if (ours) {
|
|
672
|
+
console.log(`${cli_utils_1.colors.yellow('Using --ours:')} Keeping local schema, syncing snapshot from database`);
|
|
673
|
+
console.log('');
|
|
674
|
+
(0, repo_manager_1.saveSnapshot)(currentSchema, projectRoot);
|
|
675
|
+
console.log(`Snapshot synced from database`);
|
|
676
|
+
console.log('');
|
|
677
|
+
console.log(`hint: run ${cli_utils_1.colors.cyan("'relq add .'")} to detect differences between local schema and database`);
|
|
678
|
+
console.log('');
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
console.log('How would you like to handle this?');
|
|
683
|
+
console.log('');
|
|
684
|
+
console.log(` ${cli_utils_1.colors.cyan('--theirs')} Use database version (overwrite local schema)`);
|
|
685
|
+
console.log(` ${cli_utils_1.colors.cyan('--ours')} Keep local schema (sync snapshot only)`);
|
|
686
|
+
console.log('');
|
|
687
|
+
const choiceIndex = await (0, cli_utils_1.select)('Choose resolution strategy:', [
|
|
688
|
+
'Use database version (--theirs)',
|
|
689
|
+
'Keep local schema (--ours)',
|
|
690
|
+
'Cancel',
|
|
691
|
+
]);
|
|
692
|
+
if (choiceIndex === 2) {
|
|
693
|
+
(0, cli_utils_1.fatal)('Operation cancelled by user', `Run ${cli_utils_1.colors.cyan('relq status')} to see current state.`);
|
|
694
|
+
}
|
|
695
|
+
else if (choiceIndex === 1) {
|
|
696
|
+
(0, repo_manager_1.saveSnapshot)(currentSchema, projectRoot);
|
|
697
|
+
console.log('');
|
|
698
|
+
console.log(`Snapshot synced from database`);
|
|
699
|
+
console.log(`hint: run ${cli_utils_1.colors.cyan("'relq add .'")} to detect differences between local schema and database`);
|
|
700
|
+
console.log('');
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
console.log('');
|
|
648
704
|
}
|
|
649
|
-
console.log('');
|
|
650
705
|
}
|
|
651
706
|
}
|
|
652
707
|
else if (!schemaExists) {
|
|
@@ -788,6 +843,7 @@ async function pullCommand(context) {
|
|
|
788
843
|
}
|
|
789
844
|
}
|
|
790
845
|
const oldSnapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
|
|
846
|
+
const hadPreviousSnapshot = oldSnapshot !== null;
|
|
791
847
|
const beforeSchema = oldSnapshot ? {
|
|
792
848
|
extensions: oldSnapshot.extensions?.map(e => e.name) || [],
|
|
793
849
|
enums: oldSnapshot.enums || [],
|
|
@@ -825,7 +881,16 @@ async function pullCommand(context) {
|
|
|
825
881
|
comment: t.comment,
|
|
826
882
|
})),
|
|
827
883
|
functions: oldSnapshot.functions || [],
|
|
828
|
-
triggers: oldSnapshot.triggers || []
|
|
884
|
+
triggers: (oldSnapshot.triggers || []).map(t => ({
|
|
885
|
+
name: t.name,
|
|
886
|
+
tableName: t.table,
|
|
887
|
+
event: t.events?.[0] || 'UPDATE',
|
|
888
|
+
timing: t.timing,
|
|
889
|
+
forEach: t.forEach || 'STATEMENT',
|
|
890
|
+
functionName: t.functionName,
|
|
891
|
+
definition: '',
|
|
892
|
+
isEnabled: true,
|
|
893
|
+
})),
|
|
829
894
|
} : {
|
|
830
895
|
extensions: [],
|
|
831
896
|
enums: [],
|
|
@@ -876,22 +941,28 @@ async function pullCommand(context) {
|
|
|
876
941
|
functions: filteredFunctions || [],
|
|
877
942
|
triggers: filteredTriggers || [],
|
|
878
943
|
};
|
|
879
|
-
const schemaChanges = (0, schema_comparator_1.compareSchemas)(beforeSchema, afterSchema);
|
|
944
|
+
const schemaChanges = await (0, schema_comparator_1.compareSchemas)(beforeSchema, afterSchema);
|
|
880
945
|
(0, ast_codegen_1.copyTrackingIdsToNormalized)(parsedSchema, currentSchema);
|
|
881
946
|
(0, repo_manager_1.saveSnapshot)(currentSchema, projectRoot);
|
|
882
947
|
const duration = Date.now() - startTime;
|
|
883
948
|
if (!autoCommit) {
|
|
884
|
-
if (
|
|
949
|
+
if (remoteHead) {
|
|
950
|
+
(0, repo_manager_1.setHead)(remoteHead, projectRoot);
|
|
951
|
+
}
|
|
952
|
+
if (hadPreviousSnapshot && schemaChanges.length > 0) {
|
|
885
953
|
(0, repo_manager_1.addUnstagedChanges)(schemaChanges, projectRoot);
|
|
886
954
|
spinner.succeed(`Detected ${schemaChanges.length} schema change(s)`);
|
|
887
955
|
}
|
|
888
956
|
console.log('');
|
|
889
957
|
console.log(`Pull completed in ${(0, cli_utils_1.formatDuration)(duration)}`);
|
|
890
|
-
if (schemaChanges.length > 0) {
|
|
958
|
+
if (hadPreviousSnapshot && schemaChanges.length > 0) {
|
|
891
959
|
console.log('');
|
|
892
960
|
console.log(`${cli_utils_1.colors.green(String(schemaChanges.length))} change(s) ready to stage`);
|
|
893
961
|
console.log(`hint: run ${cli_utils_1.colors.cyan("'relq add .'")} to stage all changes`);
|
|
894
962
|
}
|
|
963
|
+
else if (!hadPreviousSnapshot) {
|
|
964
|
+
console.log('Schema synced from database');
|
|
965
|
+
}
|
|
895
966
|
else {
|
|
896
967
|
console.log('Already up to date');
|
|
897
968
|
}
|
|
@@ -328,14 +328,32 @@ async function pushCommand(context) {
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
}
|
|
331
|
+
let statementIndex = 0;
|
|
331
332
|
for (const commit of commitsToProcess) {
|
|
332
333
|
const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
|
|
333
334
|
if (fs.existsSync(commitPath)) {
|
|
334
335
|
const enhancedCommit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
335
336
|
if (enhancedCommit.sql && enhancedCommit.sql.trim()) {
|
|
336
|
-
|
|
337
|
+
const statements = enhancedCommit.sql
|
|
338
|
+
.split(';')
|
|
339
|
+
.map(s => s.trim())
|
|
340
|
+
.filter(s => s.length > 0);
|
|
341
|
+
for (const stmt of statements) {
|
|
342
|
+
statementIndex++;
|
|
343
|
+
try {
|
|
344
|
+
await client.query(stmt);
|
|
345
|
+
statementsRun++;
|
|
346
|
+
}
|
|
347
|
+
catch (stmtError) {
|
|
348
|
+
const err = new Error(stmtError.message);
|
|
349
|
+
err.failedStatement = stmt;
|
|
350
|
+
err.commitHash = commit.hash;
|
|
351
|
+
err.commitMessage = commit.message;
|
|
352
|
+
err.statementIndex = statementIndex;
|
|
353
|
+
throw err;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
337
356
|
sqlExecuted++;
|
|
338
|
-
statementsRun += enhancedCommit.sql.split(';').filter(s => s.trim()).length;
|
|
339
357
|
}
|
|
340
358
|
}
|
|
341
359
|
}
|
|
@@ -375,23 +393,29 @@ async function pushCommand(context) {
|
|
|
375
393
|
catch (error) {
|
|
376
394
|
try {
|
|
377
395
|
await client.query('ROLLBACK');
|
|
378
|
-
spinner.fail('SQL execution failed - rolled back. No commits pushed.');
|
|
379
396
|
}
|
|
380
397
|
catch {
|
|
381
|
-
spinner.fail('SQL execution failed');
|
|
382
398
|
}
|
|
383
399
|
const dbError = error?.message || String(error);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
`
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
400
|
+
let errorMsg = `${cli_utils_1.colors.red('SQL Error:')} ${dbError}\n`;
|
|
401
|
+
if (error.failedStatement) {
|
|
402
|
+
errorMsg += `\n${cli_utils_1.colors.yellow('Failed Statement:')}\n`;
|
|
403
|
+
errorMsg += ` ${error.failedStatement}\n`;
|
|
404
|
+
}
|
|
405
|
+
if (error.commitHash) {
|
|
406
|
+
errorMsg += `\n${cli_utils_1.colors.yellow('In Commit:')} ${(0, repo_manager_1.shortHash)(error.commitHash)}`;
|
|
407
|
+
if (error.commitMessage) {
|
|
408
|
+
errorMsg += ` - ${error.commitMessage}`;
|
|
409
|
+
}
|
|
410
|
+
errorMsg += `\n`;
|
|
411
|
+
}
|
|
412
|
+
if (error.statementIndex) {
|
|
413
|
+
errorMsg += `${cli_utils_1.colors.yellow('Statement #:')} ${error.statementIndex}\n`;
|
|
414
|
+
}
|
|
415
|
+
errorMsg += `\n${cli_utils_1.colors.muted('All changes rolled back. No commits pushed.')}\n`;
|
|
416
|
+
errorMsg += `\n${cli_utils_1.colors.cyan('To fix:')} Run ${cli_utils_1.colors.yellow('relq reset --hard HEAD~1')} then ${cli_utils_1.colors.yellow('relq pull')} to resync`;
|
|
417
|
+
spinner.fail('SQL execution failed');
|
|
418
|
+
throw new Error(errorMsg);
|
|
395
419
|
}
|
|
396
420
|
finally {
|
|
397
421
|
await client.end();
|
|
@@ -407,6 +431,20 @@ async function pushCommand(context) {
|
|
|
407
431
|
spinner.succeed(`Pushed ${toPush.length} commit(s) to ${(0, repo_manager_1.getConnectionLabel)(connection)}`);
|
|
408
432
|
}
|
|
409
433
|
}
|
|
434
|
+
if (sqlApplied && !dryRun) {
|
|
435
|
+
const latestCommitPath = path.join(projectRoot, '.relq', 'commits', `${localHead}.json`);
|
|
436
|
+
if (fs.existsSync(latestCommitPath)) {
|
|
437
|
+
const latestCommit = JSON.parse(fs.readFileSync(latestCommitPath, 'utf-8'));
|
|
438
|
+
const newSnapshot = latestCommit.schema || latestCommit.snapshot;
|
|
439
|
+
if (newSnapshot) {
|
|
440
|
+
const snapshotPath = path.join(projectRoot, '.relq', 'snapshot.json');
|
|
441
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(newSnapshot, null, 2));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const { saveFileHash, hashFileContent } = await Promise.resolve().then(() => __importStar(require("../utils/repo-manager.cjs")));
|
|
445
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
446
|
+
saveFileHash(hashFileContent(schemaContent), projectRoot);
|
|
447
|
+
}
|
|
410
448
|
const oldHash = remoteHead ? (0, repo_manager_1.shortHash)(remoteHead) : '(none)';
|
|
411
449
|
const newHash = (0, repo_manager_1.shortHash)(localHead);
|
|
412
450
|
console.log(` ${oldHash}..${newHash} ${cli_utils_1.colors.muted('main -> main')}`);
|
|
@@ -12,6 +12,7 @@ const builder_1 = require("./ast/codegen/builder.cjs");
|
|
|
12
12
|
const type_map_1 = require("./ast/codegen/type-map.cjs");
|
|
13
13
|
const defaults_1 = require("./ast/codegen/defaults.cjs");
|
|
14
14
|
const constraints_1 = require("./ast/codegen/constraints.cjs");
|
|
15
|
+
const pg_parser_1 = require("./pg-parser.cjs");
|
|
15
16
|
let needsDefaultImport = false;
|
|
16
17
|
let needsSqlImport = false;
|
|
17
18
|
let trackingIdCounter = 0;
|
|
@@ -459,14 +460,14 @@ function generateTableCode(table, useCamelCase, enumNames, domainNames, columnTy
|
|
|
459
460
|
const columnChecks = new Map();
|
|
460
461
|
for (const c of table.constraints) {
|
|
461
462
|
if (c.type === 'CHECK' && c.expression) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
const extractedCol = (0, pg_parser_1.extractColumnFromCheck)(c.expression);
|
|
464
|
+
if (extractedCol) {
|
|
465
|
+
const matchingCol = table.columns.find(col => col.name.toLowerCase() === extractedCol);
|
|
466
|
+
if (matchingCol) {
|
|
465
467
|
const values = (0, constraints_1.extractEnumValues)(c.expression);
|
|
466
468
|
if (values && values.length > 0) {
|
|
467
|
-
columnChecks.set(
|
|
469
|
+
columnChecks.set(matchingCol.name, { name: c.name, values });
|
|
468
470
|
}
|
|
469
|
-
break;
|
|
470
471
|
}
|
|
471
472
|
}
|
|
472
473
|
}
|
|
@@ -137,8 +137,10 @@ async function deparseNode(node) {
|
|
|
137
137
|
}
|
|
138
138
|
async function parseColumnDef(colDef) {
|
|
139
139
|
const typeInfo = extractTypeName(colDef.typeName);
|
|
140
|
+
const colName = colDef.colname || '';
|
|
140
141
|
const column = {
|
|
141
|
-
name:
|
|
142
|
+
name: colName,
|
|
143
|
+
tsName: colName,
|
|
142
144
|
type: typeInfo.name,
|
|
143
145
|
typeParams: typeInfo.params,
|
|
144
146
|
isNullable: true,
|
|
@@ -597,6 +599,7 @@ async function introspectedToParsedSchema(schema) {
|
|
|
597
599
|
for (const c of t.columns) {
|
|
598
600
|
const col = {
|
|
599
601
|
name: c.name,
|
|
602
|
+
tsName: c.name,
|
|
600
603
|
type: normalizeTypeName(c.dataType, c.udtName),
|
|
601
604
|
typeParams: extractTypeParams(c),
|
|
602
605
|
isNullable: c.isNullable,
|
|
@@ -857,6 +860,7 @@ function normalizedToParsedSchema(schema) {
|
|
|
857
860
|
const baseType = isArray ? c.type.slice(0, -2) : c.type;
|
|
858
861
|
return {
|
|
859
862
|
name: c.name,
|
|
863
|
+
tsName: c.name,
|
|
860
864
|
type: baseType,
|
|
861
865
|
isNullable: c.nullable ?? true,
|
|
862
866
|
isPrimaryKey: c.primaryKey ?? false,
|
|
@@ -296,8 +296,24 @@ function generateConstraintSQL(change) {
|
|
|
296
296
|
return '';
|
|
297
297
|
const data = change.after;
|
|
298
298
|
if (change.type === 'CREATE' && data) {
|
|
299
|
+
if (!data.definition || data.definition.trim() === '') {
|
|
300
|
+
if (data.name.endsWith('_key')) {
|
|
301
|
+
return `-- Skipping ${data.name}: UNIQUE constraint already defined on column`;
|
|
302
|
+
}
|
|
303
|
+
return `-- Skipping ${data.name}: empty constraint definition`;
|
|
304
|
+
}
|
|
299
305
|
return `ALTER TABLE "${tableName}" ADD CONSTRAINT "${data.name}" ${data.definition};`;
|
|
300
306
|
}
|
|
307
|
+
else if (change.type === 'ALTER' && data) {
|
|
308
|
+
const beforeData = change.before;
|
|
309
|
+
const oldName = beforeData?.name || change.objectName;
|
|
310
|
+
if (!data.definition || data.definition.trim() === '') {
|
|
311
|
+
return `-- Skipping ALTER ${data.name}: empty constraint definition`;
|
|
312
|
+
}
|
|
313
|
+
const dropSQL = `ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${oldName}";`;
|
|
314
|
+
const addSQL = `ALTER TABLE "${tableName}" ADD CONSTRAINT "${data.name}" ${data.definition};`;
|
|
315
|
+
return `${dropSQL}\n${addSQL}`;
|
|
316
|
+
}
|
|
301
317
|
else if (change.type === 'DROP') {
|
|
302
318
|
return `ALTER TABLE "${tableName}" DROP CONSTRAINT IF EXISTS "${change.objectName}";`;
|
|
303
319
|
}
|
|
@@ -1 +1,258 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSQL = parseSQL;
|
|
4
|
+
exports.normalizeSQL = normalizeSQL;
|
|
5
|
+
exports.compareSQLByAST = compareSQLByAST;
|
|
6
|
+
exports.normalizeCheckConstraint = normalizeCheckConstraint;
|
|
7
|
+
exports.compareCheckConstraintsAsync = compareCheckConstraintsAsync;
|
|
8
|
+
exports.compareCheckConstraints = compareCheckConstraints;
|
|
9
|
+
exports.normalizeFunctionBodyAST = normalizeFunctionBodyAST;
|
|
10
|
+
exports.compareFunctionBodiesAsync = compareFunctionBodiesAsync;
|
|
11
|
+
exports.compareFunctionBodies = compareFunctionBodies;
|
|
12
|
+
exports.normalizeCreateTable = normalizeCreateTable;
|
|
13
|
+
exports.extractColumnFromCheck = extractColumnFromCheck;
|
|
14
|
+
exports.compareTriggers = compareTriggers;
|
|
15
|
+
const pgsql_parser_1 = require("pgsql-parser");
|
|
16
|
+
const pgsql_deparser_1 = require("pgsql-deparser");
|
|
17
|
+
async function parseSQL(sql) {
|
|
18
|
+
try {
|
|
19
|
+
const result = await (0, pgsql_parser_1.parse)(sql);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function normalizeSQL(sql) {
|
|
27
|
+
try {
|
|
28
|
+
const ast = await (0, pgsql_parser_1.parse)(sql);
|
|
29
|
+
if (!ast || ast.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
return await (0, pgsql_deparser_1.deparse)(ast);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function compareSQLByAST(sql1, sql2) {
|
|
38
|
+
try {
|
|
39
|
+
const [ast1, ast2] = await Promise.all([(0, pgsql_parser_1.parse)(sql1), (0, pgsql_parser_1.parse)(sql2)]);
|
|
40
|
+
if (!ast1 || !ast2)
|
|
41
|
+
return false;
|
|
42
|
+
const [normalized1, normalized2] = await Promise.all([(0, pgsql_deparser_1.deparse)(ast1), (0, pgsql_deparser_1.deparse)(ast2)]);
|
|
43
|
+
return normalized1 === normalized2;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function normalizeCheckConstraint(definition) {
|
|
50
|
+
if (!definition)
|
|
51
|
+
return null;
|
|
52
|
+
let sqlToparse = definition.trim();
|
|
53
|
+
if (sqlToparse.toUpperCase().startsWith('CHECK')) {
|
|
54
|
+
sqlToparse = sqlToparse.slice(5).trim();
|
|
55
|
+
}
|
|
56
|
+
while (sqlToparse.startsWith('(') && sqlToparse.endsWith(')')) {
|
|
57
|
+
let depth = 0;
|
|
58
|
+
let innerIsSame = true;
|
|
59
|
+
for (let i = 0; i < sqlToparse.length - 1; i++) {
|
|
60
|
+
if (sqlToparse[i] === '(')
|
|
61
|
+
depth++;
|
|
62
|
+
else if (sqlToparse[i] === ')') {
|
|
63
|
+
depth--;
|
|
64
|
+
if (depth === 0 && i < sqlToparse.length - 1) {
|
|
65
|
+
innerIsSame = false;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (innerIsSame) {
|
|
71
|
+
sqlToparse = sqlToparse.slice(1, -1).trim();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const wrappedSQL = `SELECT * FROM t WHERE ${sqlToparse}`;
|
|
79
|
+
const ast = await (0, pgsql_parser_1.parse)(wrappedSQL);
|
|
80
|
+
if (ast && ast.length > 0) {
|
|
81
|
+
const normalized = await (0, pgsql_deparser_1.deparse)(ast);
|
|
82
|
+
const whereIdx = normalized.toUpperCase().indexOf('WHERE ');
|
|
83
|
+
if (whereIdx >= 0) {
|
|
84
|
+
return normalized.slice(whereIdx + 6).trim();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
}
|
|
90
|
+
const wrapStrategies = [
|
|
91
|
+
`SELECT CASE WHEN ${sqlToparse} THEN 1 END`,
|
|
92
|
+
`CREATE TABLE t (x INT CHECK (${sqlToparse}))`,
|
|
93
|
+
];
|
|
94
|
+
for (const wrapped of wrapStrategies) {
|
|
95
|
+
try {
|
|
96
|
+
const ast = await (0, pgsql_parser_1.parse)(wrapped);
|
|
97
|
+
if (ast && ast.length > 0) {
|
|
98
|
+
return await (0, pgsql_deparser_1.deparse)(ast);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
async function compareCheckConstraintsAsync(def1, def2) {
|
|
108
|
+
if (!def1 && !def2)
|
|
109
|
+
return true;
|
|
110
|
+
if (!def1 || !def2)
|
|
111
|
+
return false;
|
|
112
|
+
const [norm1, norm2] = await Promise.all([
|
|
113
|
+
normalizeCheckConstraint(def1),
|
|
114
|
+
normalizeCheckConstraint(def2)
|
|
115
|
+
]);
|
|
116
|
+
if (norm1 && norm2) {
|
|
117
|
+
return norm1 === norm2;
|
|
118
|
+
}
|
|
119
|
+
return compareCheckValues(def1, def2);
|
|
120
|
+
}
|
|
121
|
+
async function compareCheckConstraints(def1, def2) {
|
|
122
|
+
if (!def1 && !def2)
|
|
123
|
+
return true;
|
|
124
|
+
if (!def1 || !def2)
|
|
125
|
+
return false;
|
|
126
|
+
const [norm1, norm2] = await Promise.all([
|
|
127
|
+
normalizeCheckConstraint(def1),
|
|
128
|
+
normalizeCheckConstraint(def2)
|
|
129
|
+
]);
|
|
130
|
+
if (norm1 && norm2) {
|
|
131
|
+
return norm1 === norm2;
|
|
132
|
+
}
|
|
133
|
+
return compareCheckValues(def1, def2);
|
|
134
|
+
}
|
|
135
|
+
function compareCheckValues(def1, def2) {
|
|
136
|
+
const values1 = extractCheckValues(def1);
|
|
137
|
+
const values2 = extractCheckValues(def2);
|
|
138
|
+
if (values1.length !== values2.length)
|
|
139
|
+
return false;
|
|
140
|
+
const sorted1 = [...values1].sort();
|
|
141
|
+
const sorted2 = [...values2].sort();
|
|
142
|
+
return sorted1.every((v, i) => v === sorted2[i]);
|
|
143
|
+
}
|
|
144
|
+
function extractCheckValues(definition) {
|
|
145
|
+
if (!definition)
|
|
146
|
+
return [];
|
|
147
|
+
const arrayMatch = definition.match(/ARRAY\[([^\]]+)\]/i);
|
|
148
|
+
if (arrayMatch) {
|
|
149
|
+
const valuesStr = arrayMatch[1];
|
|
150
|
+
const values = valuesStr.match(/'([^']+)'/g)?.map(v => v.replace(/'/g, '').toLowerCase()) || [];
|
|
151
|
+
return values;
|
|
152
|
+
}
|
|
153
|
+
const inMatch = definition.match(/IN\s*\(([^)]+)\)/i);
|
|
154
|
+
if (inMatch) {
|
|
155
|
+
const valuesStr = inMatch[1];
|
|
156
|
+
const values = valuesStr.match(/'([^']+)'/g)?.map(v => v.replace(/'/g, '').toLowerCase()) || [];
|
|
157
|
+
return values;
|
|
158
|
+
}
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
async function normalizeFunctionBodyAST(body) {
|
|
162
|
+
if (!body)
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
const asBlock = `DO $$ ${body} $$`;
|
|
166
|
+
const ast = await (0, pgsql_parser_1.parse)(asBlock);
|
|
167
|
+
if (ast && ast.length > 0) {
|
|
168
|
+
return await (0, pgsql_deparser_1.deparse)(ast);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const ast = await (0, pgsql_parser_1.parse)(body);
|
|
175
|
+
if (ast && ast.length > 0) {
|
|
176
|
+
return await (0, pgsql_deparser_1.deparse)(ast);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const asFunc = `CREATE FUNCTION _tmp() RETURNS void AS $$ ${body} $$ LANGUAGE plpgsql`;
|
|
183
|
+
const ast = await (0, pgsql_parser_1.parse)(asFunc);
|
|
184
|
+
if (ast && ast.length > 0) {
|
|
185
|
+
return await (0, pgsql_deparser_1.deparse)(ast);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
async function compareFunctionBodiesAsync(body1, body2) {
|
|
193
|
+
if (!body1 && !body2)
|
|
194
|
+
return true;
|
|
195
|
+
if (!body1 || !body2)
|
|
196
|
+
return false;
|
|
197
|
+
const [norm1, norm2] = await Promise.all([
|
|
198
|
+
normalizeFunctionBodyAST(body1),
|
|
199
|
+
normalizeFunctionBodyAST(body2)
|
|
200
|
+
]);
|
|
201
|
+
if (norm1 && norm2) {
|
|
202
|
+
return norm1 === norm2;
|
|
203
|
+
}
|
|
204
|
+
return normalizeString(body1) === normalizeString(body2);
|
|
205
|
+
}
|
|
206
|
+
async function compareFunctionBodies(body1, body2) {
|
|
207
|
+
if (!body1 && !body2)
|
|
208
|
+
return true;
|
|
209
|
+
if (!body1 || !body2)
|
|
210
|
+
return false;
|
|
211
|
+
const [norm1, norm2] = await Promise.all([
|
|
212
|
+
normalizeFunctionBodyAST(body1),
|
|
213
|
+
normalizeFunctionBodyAST(body2)
|
|
214
|
+
]);
|
|
215
|
+
if (norm1 && norm2) {
|
|
216
|
+
return norm1 === norm2;
|
|
217
|
+
}
|
|
218
|
+
return normalizeString(body1) === normalizeString(body2);
|
|
219
|
+
}
|
|
220
|
+
function normalizeString(str) {
|
|
221
|
+
if (!str)
|
|
222
|
+
return '';
|
|
223
|
+
return str.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
224
|
+
}
|
|
225
|
+
async function normalizeCreateTable(sql) {
|
|
226
|
+
try {
|
|
227
|
+
const ast = await (0, pgsql_parser_1.parse)(sql);
|
|
228
|
+
if (ast && ast.length > 0) {
|
|
229
|
+
return await (0, pgsql_deparser_1.deparse)(ast);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function extractColumnFromCheck(definition) {
|
|
238
|
+
if (!definition)
|
|
239
|
+
return null;
|
|
240
|
+
const enumMatch = definition.match(/\((\w+)\)::text\s*=\s*ANY/i);
|
|
241
|
+
if (enumMatch)
|
|
242
|
+
return enumMatch[1].toLowerCase();
|
|
243
|
+
const inMatch = definition.match(/["\(](\w+)["]\s+IN\s*\(/i);
|
|
244
|
+
if (inMatch)
|
|
245
|
+
return inMatch[1].toLowerCase();
|
|
246
|
+
const compMatch = definition.match(/\(\(?(\w+)\s*(?:>=?|<=?|<>|!=|=)/i);
|
|
247
|
+
if (compMatch)
|
|
248
|
+
return compMatch[1].toLowerCase();
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function compareTriggers(trigger1, trigger2) {
|
|
252
|
+
if (!trigger1 || !trigger2)
|
|
253
|
+
return false;
|
|
254
|
+
return (normalizeString(trigger1.timing) === normalizeString(trigger2.timing) &&
|
|
255
|
+
normalizeString(trigger1.event) === normalizeString(trigger2.event) &&
|
|
256
|
+
normalizeString(trigger1.functionName) === normalizeString(trigger2.functionName) &&
|
|
257
|
+
normalizeString(trigger1.tableName) === normalizeString(trigger2.tableName));
|
|
258
|
+
}
|