relq 1.0.5 → 1.0.7

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 (86) hide show
  1. package/dist/cjs/cli/commands/add.cjs +257 -17
  2. package/dist/cjs/cli/commands/commit.cjs +13 -2
  3. package/dist/cjs/cli/commands/export.cjs +25 -19
  4. package/dist/cjs/cli/commands/import.cjs +219 -100
  5. package/dist/cjs/cli/commands/init.cjs +86 -14
  6. package/dist/cjs/cli/commands/pull.cjs +104 -23
  7. package/dist/cjs/cli/commands/push.cjs +38 -3
  8. package/dist/cjs/cli/index.cjs +9 -1
  9. package/dist/cjs/cli/utils/ast/codegen/builder.cjs +297 -0
  10. package/dist/cjs/cli/utils/ast/codegen/constraints.cjs +185 -0
  11. package/dist/cjs/cli/utils/ast/codegen/defaults.cjs +311 -0
  12. package/dist/cjs/cli/utils/ast/codegen/index.cjs +24 -0
  13. package/dist/cjs/cli/utils/ast/codegen/type-map.cjs +116 -0
  14. package/dist/cjs/cli/utils/ast/codegen/utils.cjs +69 -0
  15. package/dist/cjs/cli/utils/ast/index.cjs +19 -0
  16. package/dist/cjs/cli/utils/ast/transformer/helpers.cjs +154 -0
  17. package/dist/cjs/cli/utils/ast/transformer/index.cjs +25 -0
  18. package/dist/cjs/cli/utils/ast/types.cjs +2 -0
  19. package/dist/cjs/cli/utils/ast-codegen.cjs +949 -0
  20. package/dist/cjs/cli/utils/ast-transformer.cjs +916 -0
  21. package/dist/cjs/cli/utils/change-tracker.cjs +50 -1
  22. package/dist/cjs/cli/utils/cli-utils.cjs +151 -0
  23. package/dist/cjs/cli/utils/fast-introspect.cjs +149 -23
  24. package/dist/cjs/cli/utils/pg-parser.cjs +1 -0
  25. package/dist/cjs/cli/utils/repo-manager.cjs +121 -4
  26. package/dist/cjs/cli/utils/schema-comparator.cjs +98 -14
  27. package/dist/cjs/cli/utils/schema-introspect.cjs +56 -19
  28. package/dist/cjs/cli/utils/snapshot-manager.cjs +0 -1
  29. package/dist/cjs/cli/utils/sql-generator.cjs +353 -64
  30. package/dist/cjs/cli/utils/type-generator.cjs +114 -15
  31. package/dist/cjs/config/config.cjs +29 -10
  32. package/dist/cjs/core/relq-client.cjs +22 -6
  33. package/dist/cjs/schema-definition/column-types.cjs +149 -13
  34. package/dist/cjs/schema-definition/defaults.cjs +72 -0
  35. package/dist/cjs/schema-definition/index.cjs +15 -1
  36. package/dist/cjs/schema-definition/introspection.cjs +7 -3
  37. package/dist/cjs/schema-definition/pg-relations.cjs +169 -0
  38. package/dist/cjs/schema-definition/pg-view.cjs +30 -0
  39. package/dist/cjs/schema-definition/table-definition.cjs +110 -4
  40. package/dist/cjs/types/config-types.cjs +13 -4
  41. package/dist/cjs/utils/aws-dsql.cjs +177 -0
  42. package/dist/config.d.ts +147 -2
  43. package/dist/esm/cli/commands/add.js +255 -18
  44. package/dist/esm/cli/commands/commit.js +13 -2
  45. package/dist/esm/cli/commands/export.js +25 -19
  46. package/dist/esm/cli/commands/import.js +221 -102
  47. package/dist/esm/cli/commands/init.js +86 -14
  48. package/dist/esm/cli/commands/pull.js +106 -25
  49. package/dist/esm/cli/commands/push.js +39 -4
  50. package/dist/esm/cli/index.js +9 -1
  51. package/dist/esm/cli/utils/ast/codegen/builder.js +291 -0
  52. package/dist/esm/cli/utils/ast/codegen/constraints.js +176 -0
  53. package/dist/esm/cli/utils/ast/codegen/defaults.js +305 -0
  54. package/dist/esm/cli/utils/ast/codegen/index.js +6 -0
  55. package/dist/esm/cli/utils/ast/codegen/type-map.js +111 -0
  56. package/dist/esm/cli/utils/ast/codegen/utils.js +60 -0
  57. package/dist/esm/cli/utils/ast/index.js +3 -0
  58. package/dist/esm/cli/utils/ast/transformer/helpers.js +141 -0
  59. package/dist/esm/cli/utils/ast/transformer/index.js +2 -0
  60. package/dist/esm/cli/utils/ast/types.js +1 -0
  61. package/dist/esm/cli/utils/ast-codegen.js +945 -0
  62. package/dist/esm/cli/utils/ast-transformer.js +907 -0
  63. package/dist/esm/cli/utils/change-tracker.js +50 -1
  64. package/dist/esm/cli/utils/cli-utils.js +147 -0
  65. package/dist/esm/cli/utils/fast-introspect.js +149 -23
  66. package/dist/esm/cli/utils/pg-parser.js +1 -0
  67. package/dist/esm/cli/utils/repo-manager.js +114 -4
  68. package/dist/esm/cli/utils/schema-comparator.js +98 -14
  69. package/dist/esm/cli/utils/schema-introspect.js +56 -19
  70. package/dist/esm/cli/utils/snapshot-manager.js +0 -1
  71. package/dist/esm/cli/utils/sql-generator.js +353 -64
  72. package/dist/esm/cli/utils/type-generator.js +114 -15
  73. package/dist/esm/config/config.js +29 -10
  74. package/dist/esm/core/relq-client.js +23 -7
  75. package/dist/esm/schema-definition/column-types.js +146 -12
  76. package/dist/esm/schema-definition/defaults.js +69 -0
  77. package/dist/esm/schema-definition/index.js +3 -0
  78. package/dist/esm/schema-definition/introspection.js +7 -3
  79. package/dist/esm/schema-definition/pg-relations.js +161 -0
  80. package/dist/esm/schema-definition/pg-view.js +24 -0
  81. package/dist/esm/schema-definition/table-definition.js +110 -4
  82. package/dist/esm/types/config-types.js +12 -4
  83. package/dist/esm/utils/aws-dsql.js +139 -0
  84. package/dist/index.d.ts +159 -1
  85. package/dist/schema-builder.d.ts +1314 -32
  86. package/package.json +1 -1
@@ -1,16 +1,18 @@
1
1
  import * as fs from 'fs';
2
+ import stripComments from 'strip-comments';
2
3
  import { colors, fatal, warning } from "../utils/spinner.js";
3
4
  import { loadRelqignore, isIgnored, } from "../utils/relqignore.js";
4
5
  import { loadConfig } from "../../config/config.js";
5
6
  import * as path from 'path';
6
- import { isInitialized, loadSnapshot, getUnstagedChanges, getStagedChanges, stageChanges, detectFileChanges, addUnstagedChanges, clearUnstagedChanges, } from "../utils/repo-manager.js";
7
+ import { isInitialized, loadSnapshot, getUnstagedChanges, getStagedChanges, stageChanges, detectFileChanges, addUnstagedChanges, clearUnstagedChanges, cleanupStagedChanges, } from "../utils/repo-manager.js";
7
8
  import { getChangeDisplayName } from "../utils/change-tracker.js";
8
9
  import { compareSchemas } from "../utils/schema-comparator.js";
9
10
  function parseSchemaFileForComparison(schemaPath) {
10
11
  if (!fs.existsSync(schemaPath)) {
11
12
  return null;
12
13
  }
13
- const content = fs.readFileSync(schemaPath, 'utf-8');
14
+ const rawContent = fs.readFileSync(schemaPath, 'utf-8');
15
+ const content = stripComments(rawContent);
14
16
  const tables = [];
15
17
  const tableStartRegex = /defineTable\s*\(\s*['"]([^'"]+)['"],\s*\{/g;
16
18
  let tableStartMatch;
@@ -50,15 +52,21 @@ function parseSchemaFileForComparison(schemaPath) {
50
52
  const tsToDbNameMap = new Map();
51
53
  const lines = columnsBlock.split('\n');
52
54
  let currentColDef = '';
55
+ let pendingJsDocComment = null;
53
56
  for (const line of lines) {
54
57
  const trimmed = line.trim();
58
+ const jsDocMatch = trimmed.match(/^\/\*\*\s*(.*?)\s*\*\/$/);
59
+ if (jsDocMatch) {
60
+ pendingJsDocComment = jsDocMatch[1];
61
+ continue;
62
+ }
55
63
  if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
56
64
  continue;
57
65
  currentColDef += ' ' + trimmed;
58
66
  if (trimmed.endsWith(',') || trimmed.endsWith(')')) {
59
67
  const colDef = currentColDef.trim();
60
68
  currentColDef = '';
61
- const typePattern = 'varchar|text|uuid|integer|bigint|boolean|timestamp|date|jsonb|json|numeric|serial|bigserial|smallserial|tsvector|smallint|real|doublePrecision|char|inet|cidr|macaddr|macaddr8|interval|time|point|line|lseg|box|path|polygon|circle|bytea|bit|varbit|money|xml|oid';
69
+ const typePattern = 'varchar|text|uuid|integer|bigint|boolean|timestamptz|timestamp|date|jsonb|json|numeric|serial|bigserial|smallserial|tsvector|smallint|real|doublePrecision|char|inet|cidr|macaddr|macaddr8|interval|timetz|time|point|line|lseg|box|path|polygon|circle|bytea|bit|varbit|money|xml|oid|enumType|domainType';
62
70
  const colMatch = colDef.match(new RegExp(`^(\\w+):\\s*(${typePattern})`));
63
71
  if (!colMatch)
64
72
  continue;
@@ -74,13 +82,17 @@ function parseSchemaFileForComparison(schemaPath) {
74
82
  if (bigintMatch) {
75
83
  defaultValue = bigintMatch[1];
76
84
  }
77
- const funcDefaultMatch = !defaultValue && colDef.match(/\.default\(\s*(genRandomUuid|now|currentDate|currentTimestamp|emptyArray|emptyObject)\s*\(\s*\)\s*\)/);
85
+ const bigintLiteralMatch = !defaultValue && colDef.match(/\.default\(\s*(-?\d+)n\s*\)/);
86
+ if (bigintLiteralMatch) {
87
+ defaultValue = bigintLiteralMatch[1];
88
+ }
89
+ const funcDefaultMatch = !defaultValue && colDef.match(/\.default\(\s*(?:DEFAULT\.)?(genRandomUuid|now|currentDate|currentTimestamp|emptyArray|emptyObject|emptyJsonb)\s*\(\s*\)\s*\)/);
78
90
  if (funcDefaultMatch) {
79
91
  const funcName = funcDefaultMatch[1];
80
92
  if (funcName === 'emptyArray') {
81
93
  defaultValue = isJsonbColumn ? "'[]'::jsonb" : "'{}'::text[]";
82
94
  }
83
- else if (funcName === 'emptyObject') {
95
+ else if (funcName === 'emptyObject' || funcName === 'emptyJsonb') {
84
96
  defaultValue = "'{}'::jsonb";
85
97
  }
86
98
  else {
@@ -145,10 +157,22 @@ function parseSchemaFileForComparison(schemaPath) {
145
157
  if (commentMatch) {
146
158
  comment = commentMatch[2];
147
159
  }
160
+ else if (pendingJsDocComment) {
161
+ comment = pendingJsDocComment;
162
+ }
163
+ pendingJsDocComment = null;
148
164
  if (colDef.includes('.identity()')) {
149
165
  defaultValue = defaultValue || 'GENERATED BY DEFAULT AS IDENTITY';
150
166
  }
151
- const isArray = colDef.includes('.array()');
167
+ let trackingId = undefined;
168
+ const trackingIdMatch = colDef.match(/\.\$id\(\s*['"]([^'"]+)['"]\s*\)/);
169
+ if (trackingIdMatch) {
170
+ trackingId = trackingIdMatch[1];
171
+ }
172
+ const hasArrayModifier = colDef.includes('.array()');
173
+ const hasEmptyArrayDefault = colDef.includes('emptyArray()');
174
+ const isJsonbType = type === 'jsonb' || type === 'json';
175
+ const isArray = hasArrayModifier || (hasEmptyArrayDefault && !isJsonbType);
152
176
  columns.push({
153
177
  name: dbColName,
154
178
  dataType: isArray ? `${type}[]` : type,
@@ -161,6 +185,7 @@ function parseSchemaFileForComparison(schemaPath) {
161
185
  scale: null,
162
186
  references: null,
163
187
  comment,
188
+ trackingId,
164
189
  });
165
190
  }
166
191
  }
@@ -173,8 +198,13 @@ function parseSchemaFileForComparison(schemaPath) {
173
198
  const tsColName = c.trim().replace(/table\.\s*/, '');
174
199
  return tsToDbNameMap.get(tsColName) || tsColName;
175
200
  });
176
- const isUnique = optionsBlock.includes(`index('${indexName}')`) &&
177
- optionsBlock.substring(optionsBlock.indexOf(`index('${indexName}')`)).split('\n')[0].includes('.unique()');
201
+ const indexStart = optionsBlock.indexOf(`index('${indexName}')`);
202
+ const indexLine = indexStart >= 0 ? optionsBlock.substring(indexStart).split('\n')[0] : '';
203
+ const isUnique = indexLine.includes('.unique()');
204
+ const commentMatch = indexLine.match(/\.comment\(\s*(['"])([^'"]*)\1\s*\)/);
205
+ const comment = commentMatch ? commentMatch[2] : null;
206
+ const trackingIdMatch = indexLine.match(/\.\$id\(\s*(['"])([^'"]+)\1\s*\)/);
207
+ const trackingId = trackingIdMatch ? trackingIdMatch[2] : undefined;
178
208
  indexes.push({
179
209
  name: indexName,
180
210
  columns: indexCols,
@@ -184,6 +214,8 @@ function parseSchemaFileForComparison(schemaPath) {
184
214
  definition: '',
185
215
  whereClause: null,
186
216
  expression: null,
217
+ comment,
218
+ trackingId,
187
219
  });
188
220
  }
189
221
  const checkRegexLegacy = /check\s*\(\s*['"]([^'"]+)['"]\s*,\s*sql`([^`]+)`\s*\)/g;
@@ -234,6 +266,83 @@ function parseSchemaFileForComparison(schemaPath) {
234
266
  const dbPartitionKey = tsToDbNameMap.get(tsPartitionKey) || tsPartitionKey;
235
267
  partitionKey = [dbPartitionKey];
236
268
  }
269
+ const pkColumns = columns.filter(c => c.isPrimaryKey).map(c => c.name);
270
+ if (pkColumns.length > 0) {
271
+ const pkName = `${tableName}_pkey`;
272
+ if (!constraints.some(c => c.type === 'PRIMARY KEY')) {
273
+ constraints.push({
274
+ name: pkName,
275
+ type: 'PRIMARY KEY',
276
+ columns: pkColumns,
277
+ definition: '',
278
+ });
279
+ }
280
+ }
281
+ for (const col of columns) {
282
+ if (col.isUnique && !col.isPrimaryKey) {
283
+ const uniqueName = `${tableName}_${col.name}_key`;
284
+ col.isUnique = false;
285
+ if (!constraints.some(c => c.name === uniqueName)) {
286
+ constraints.push({
287
+ name: uniqueName,
288
+ type: 'UNIQUE',
289
+ columns: [col.name],
290
+ definition: '',
291
+ });
292
+ }
293
+ }
294
+ }
295
+ const uniqueConstraintRegex = /constraint\.unique\s*\(\s*\{\s*name:\s*['"]([^'"]+)['"]\s*,\s*columns:\s*\[([^\]]+)\]/g;
296
+ let uniqueMatch;
297
+ while ((uniqueMatch = uniqueConstraintRegex.exec(optionsBlock)) !== null) {
298
+ const constraintName = uniqueMatch[1];
299
+ const colsStr = uniqueMatch[2];
300
+ const uniqueCols = colsStr.split(',').map(c => {
301
+ const tsColName = c.trim().replace(/table\.\s*/, '');
302
+ return tsToDbNameMap.get(tsColName) || tsColName;
303
+ });
304
+ if (!constraints.some(c => c.name === constraintName)) {
305
+ constraints.push({
306
+ name: constraintName,
307
+ type: 'UNIQUE',
308
+ columns: uniqueCols,
309
+ definition: '',
310
+ });
311
+ }
312
+ }
313
+ const pkConstraintRegex = /constraint\.primaryKey\s*\(\s*\{\s*name:\s*['"]([^'"]+)['"]\s*,\s*columns:\s*\[([^\]]+)\]/g;
314
+ let pkMatch;
315
+ while ((pkMatch = pkConstraintRegex.exec(optionsBlock)) !== null) {
316
+ const constraintName = pkMatch[1];
317
+ const colsStr = pkMatch[2];
318
+ const pkCols = colsStr.split(',').map(c => {
319
+ const tsColName = c.trim().replace(/table\.\s*/, '');
320
+ return tsToDbNameMap.get(tsColName) || tsColName;
321
+ });
322
+ const existingPkIdx = constraints.findIndex(c => c.type === 'PRIMARY KEY');
323
+ if (existingPkIdx >= 0) {
324
+ constraints[existingPkIdx] = {
325
+ name: constraintName,
326
+ type: 'PRIMARY KEY',
327
+ columns: pkCols,
328
+ definition: '',
329
+ };
330
+ }
331
+ else {
332
+ constraints.push({
333
+ name: constraintName,
334
+ type: 'PRIMARY KEY',
335
+ columns: pkCols,
336
+ definition: '',
337
+ });
338
+ }
339
+ for (const dbColName of pkCols) {
340
+ const col = columns.find(c => c.name === dbColName);
341
+ if (col) {
342
+ col.isPrimaryKey = true;
343
+ }
344
+ }
345
+ }
237
346
  tables.push({
238
347
  name: tableName,
239
348
  schema: 'public',
@@ -269,6 +378,80 @@ function parseSchemaFileForComparison(schemaPath) {
269
378
  }
270
379
  }
271
380
  }
381
+ const toSnakeCase = (str) => {
382
+ return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
383
+ };
384
+ const pgRelationsStart = content.indexOf('pgRelations(');
385
+ let relationsBlock = '';
386
+ if (pgRelationsStart !== -1) {
387
+ const arrowStart = content.indexOf('=> ({', pgRelationsStart);
388
+ if (arrowStart !== -1) {
389
+ const blockStart = arrowStart + 5;
390
+ let depth = 1;
391
+ let i = blockStart;
392
+ while (i < content.length && depth > 0) {
393
+ if (content[i] === '{')
394
+ depth++;
395
+ else if (content[i] === '}')
396
+ depth--;
397
+ i++;
398
+ }
399
+ relationsBlock = content.slice(blockStart, i - 1);
400
+ }
401
+ }
402
+ if (relationsBlock) {
403
+ const tableEntryRegex = /(\w+):\s*tables\.\1\s*\(\s*\([^)]*\)\s*=>\s*\(\{/g;
404
+ let tableMatch;
405
+ while ((tableMatch = tableEntryRegex.exec(relationsBlock)) !== null) {
406
+ const tableTsName = tableMatch[1];
407
+ const tableDbName = toSnakeCase(tableTsName);
408
+ const entryStart = tableMatch.index + tableMatch[0].length;
409
+ let depth = 1;
410
+ let j = entryStart;
411
+ while (j < relationsBlock.length && depth > 0) {
412
+ if (relationsBlock[j] === '{')
413
+ depth++;
414
+ else if (relationsBlock[j] === '}')
415
+ depth--;
416
+ j++;
417
+ }
418
+ const tableRelationsContent = relationsBlock.slice(entryStart, j - 1);
419
+ const table = tables.find(t => t.name === tableDbName);
420
+ if (!table)
421
+ continue;
422
+ const fkRegex = /(\w+):\s*r\.referenceTo\.(\w+)\s*\(\s*\w*\s*=>\s*\(\{([^}]*)\}\)/g;
423
+ let fkMatch;
424
+ while ((fkMatch = fkRegex.exec(tableRelationsContent)) !== null) {
425
+ const colTsName = fkMatch[1];
426
+ const refTableTsName = fkMatch[2];
427
+ const fkOptionsStr = fkMatch[3];
428
+ const colDbName = toSnakeCase(colTsName);
429
+ const refTableDbName = toSnakeCase(refTableTsName);
430
+ let onDelete;
431
+ let onUpdate;
432
+ const nameMatch = fkOptionsStr.match(/name:\s*['"]([^'"]+)['"]/);
433
+ const onDeleteMatch = fkOptionsStr.match(/onDelete:\s*['"]([^'"]+)['"]/);
434
+ const onUpdateMatch = fkOptionsStr.match(/onUpdate:\s*['"]([^'"]+)['"]/);
435
+ if (onDeleteMatch)
436
+ onDelete = onDeleteMatch[1];
437
+ if (onUpdateMatch)
438
+ onUpdate = onUpdateMatch[1];
439
+ const fkName = nameMatch ? nameMatch[1] : `${tableDbName}_${colDbName}_fkey`;
440
+ if (!table.constraints.some(c => c.name === fkName || (c.type === 'FOREIGN KEY' && c.columns?.includes(colDbName)))) {
441
+ table.constraints.push({
442
+ name: fkName,
443
+ type: 'FOREIGN KEY',
444
+ columns: [colDbName],
445
+ definition: '',
446
+ referencedTable: refTableDbName,
447
+ referencedColumns: ['id'],
448
+ onDelete,
449
+ onUpdate,
450
+ });
451
+ }
452
+ }
453
+ }
454
+ }
272
455
  return {
273
456
  tables,
274
457
  enums,
@@ -301,6 +484,7 @@ function snapshotToDatabaseSchema(snapshot) {
301
484
  scale: null,
302
485
  references: null,
303
486
  comment: c.comment || null,
487
+ trackingId: c.trackingId,
304
488
  })),
305
489
  indexes: (t.indexes || []).map(i => ({
306
490
  name: i.name,
@@ -311,6 +495,7 @@ function snapshotToDatabaseSchema(snapshot) {
311
495
  definition: i.definition || '',
312
496
  whereClause: i.whereClause || null,
313
497
  expression: null,
498
+ trackingId: i.trackingId,
314
499
  })),
315
500
  constraints: (t.constraints || []).map(c => ({
316
501
  name: c.name,
@@ -339,6 +524,57 @@ function snapshotToDatabaseSchema(snapshot) {
339
524
  extensions: (snapshot.extensions || []).map(e => typeof e === 'string' ? e : e.name),
340
525
  };
341
526
  }
527
+ let trackingIdCounter = 0;
528
+ function generateTrackingId(prefix) {
529
+ trackingIdCounter++;
530
+ const base = (Date.now().toString(36) + trackingIdCounter.toString(36)).slice(-5);
531
+ return prefix + base.padStart(5, '0');
532
+ }
533
+ function injectTrackingIds(schemaPath) {
534
+ if (!fs.existsSync(schemaPath)) {
535
+ return 0;
536
+ }
537
+ let content = fs.readFileSync(schemaPath, 'utf-8');
538
+ let injectedCount = 0;
539
+ const columnLineRegex = /^(\s*)(\w+):\s*(varchar|text|uuid|integer|bigint|boolean|timestamptz|timestamp|date|jsonb|json|numeric|serial|bigserial|smallserial|tsvector|smallint|real|doublePrecision|char|inet|cidr|macaddr|macaddr8|interval|timetz|time|point|line|lseg|box|path|polygon|circle|bytea|bit|varbit|money|xml|oid|enumType|domainType)\s*\([^)]*\)([^,\n]*)(,?)$/gm;
540
+ content = content.replace(columnLineRegex, (match, indent, colName, type, modifiers, comma) => {
541
+ if (modifiers.includes('.$id(')) {
542
+ return match;
543
+ }
544
+ const commentMatch = modifiers.match(/^(.*)(\.\s*comment\s*\([^)]+\))$/);
545
+ if (commentMatch) {
546
+ const beforeComment = commentMatch[1];
547
+ const comment = commentMatch[2];
548
+ const trackingId = generateTrackingId('c');
549
+ injectedCount++;
550
+ return `${indent}${colName}: ${type}(${match.split('(')[1].split(')')[0]})${beforeComment}.$id('${trackingId}')${comment}${comma}`;
551
+ }
552
+ const trackingId = generateTrackingId('c');
553
+ injectedCount++;
554
+ return `${indent}${colName}: ${type}(${match.split('(')[1].split(')')[0]})${modifiers}.$id('${trackingId}')${comma}`;
555
+ });
556
+ const indexLineRegex = /^(\s*)(index\s*\(\s*['"][^'"]+['"]\s*\)\s*\.on\s*\([^)]+\)[^,\n]*)(,?)$/gm;
557
+ content = content.replace(indexLineRegex, (match, indent, indexDef, comma) => {
558
+ if (indexDef.includes('.$id(')) {
559
+ return match;
560
+ }
561
+ const commentMatch = indexDef.match(/^(.*)(\.\s*comment\s*\([^)]+\))$/);
562
+ if (commentMatch) {
563
+ const beforeComment = commentMatch[1];
564
+ const comment = commentMatch[2];
565
+ const trackingId = generateTrackingId('i');
566
+ injectedCount++;
567
+ return `${indent}${beforeComment}.$id('${trackingId}')${comment}${comma}`;
568
+ }
569
+ const trackingId = generateTrackingId('i');
570
+ injectedCount++;
571
+ return `${indent}${indexDef}.$id('${trackingId}')${comma}`;
572
+ });
573
+ if (injectedCount > 0) {
574
+ fs.writeFileSync(schemaPath, content, 'utf-8');
575
+ }
576
+ return injectedCount;
577
+ }
342
578
  export async function addCommand(context) {
343
579
  const { args, projectRoot } = context;
344
580
  console.log('');
@@ -347,8 +583,12 @@ export async function addCommand(context) {
347
583
  }
348
584
  const ignorePatterns = loadRelqignore(projectRoot);
349
585
  const config = await loadConfig();
350
- const schemaPathRaw = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
586
+ const schemaPathRaw = typeof config?.schema === 'string' ? config.schema : './db/schema.ts';
351
587
  const schemaPath = path.resolve(projectRoot, schemaPathRaw);
588
+ const injectedCount = injectTrackingIds(schemaPath);
589
+ if (injectedCount > 0) {
590
+ console.log(colors.muted(`Injected ${injectedCount} tracking ID(s) into schema.ts`));
591
+ }
352
592
  const fileChange = detectFileChanges(schemaPath, projectRoot);
353
593
  if (fileChange) {
354
594
  const currentSchema = parseSchemaFileForComparison(schemaPath);
@@ -356,16 +596,13 @@ export async function addCommand(context) {
356
596
  if (currentSchema && snapshot) {
357
597
  const snapshotAsDbSchema = snapshotToDatabaseSchema(snapshot);
358
598
  const schemaChanges = compareSchemas(snapshotAsDbSchema, currentSchema);
599
+ cleanupStagedChanges(schemaChanges, projectRoot);
359
600
  if (schemaChanges.length > 0) {
360
601
  clearUnstagedChanges(projectRoot);
361
602
  addUnstagedChanges(schemaChanges, projectRoot);
362
603
  }
363
604
  else {
364
- const existingUnstaged = getUnstagedChanges(projectRoot);
365
- const hasFileChange = existingUnstaged.some(c => c.objectType === 'SCHEMA_FILE');
366
- if (!hasFileChange) {
367
- addUnstagedChanges([fileChange], projectRoot);
368
- }
605
+ clearUnstagedChanges(projectRoot);
369
606
  }
370
607
  }
371
608
  }
@@ -377,16 +614,16 @@ export async function addCommand(context) {
377
614
  if (result.ignored) {
378
615
  return false;
379
616
  }
380
- if (['FUNCTION', 'PROCEDURE'].includes(change.objectType) && !config.includeFunctions) {
617
+ if (['FUNCTION', 'PROCEDURE'].includes(change.objectType) && !config?.includeFunctions) {
381
618
  return false;
382
619
  }
383
- if (change.objectType === 'TRIGGER' && !config.includeTriggers) {
620
+ if (change.objectType === 'TRIGGER' && !config?.includeTriggers) {
384
621
  return false;
385
622
  }
386
- if (['VIEW', 'MATERIALIZED_VIEW'].includes(change.objectType) && !config.includeViews) {
623
+ if (['VIEW', 'MATERIALIZED_VIEW'].includes(change.objectType) && !config?.includeViews) {
387
624
  return false;
388
625
  }
389
- if (change.objectType === 'FOREIGN_TABLE' && !config.includeFDW) {
626
+ if (change.objectType === 'FOREIGN_TABLE' && !config?.includeFDW) {
390
627
  return false;
391
628
  }
392
629
  return true;
@@ -39,6 +39,7 @@ export async function commitCommand(context) {
39
39
  const creates = staged.filter(c => c.type === 'CREATE').length;
40
40
  const alters = staged.filter(c => c.type === 'ALTER').length;
41
41
  const drops = staged.filter(c => c.type === 'DROP').length;
42
+ const renames = staged.filter(c => c.type === 'RENAME').length;
42
43
  const parentHash = getHead(projectRoot);
43
44
  const hashInput = JSON.stringify({
44
45
  changes: staged.map(c => c.id),
@@ -60,6 +61,7 @@ export async function commitCommand(context) {
60
61
  creates,
61
62
  alters,
62
63
  drops,
64
+ renames,
63
65
  total: staged.length,
64
66
  },
65
67
  };
@@ -84,7 +86,7 @@ export async function commitCommand(context) {
84
86
  }
85
87
  }
86
88
  const commitConfig = await loadConfig();
87
- const schemaPathRaw = typeof commitConfig.schema === 'string' ? commitConfig.schema : './db/schema.ts';
89
+ const schemaPathRaw = typeof commitConfig?.schema === 'string' ? commitConfig.schema : './db/schema.ts';
88
90
  const schemaFilePath = path.resolve(projectRoot, schemaPathRaw);
89
91
  if (fs.existsSync(schemaFilePath)) {
90
92
  const currentContent = fs.readFileSync(schemaFilePath, 'utf-8');
@@ -92,7 +94,16 @@ export async function commitCommand(context) {
92
94
  saveFileHash(currentHash, projectRoot);
93
95
  }
94
96
  console.log(`[${shortHash(hash)}] ${message}`);
95
- console.log(` ${creates} create(s), ${alters} alter(s), ${drops} drop(s)`);
97
+ const statsParts = [];
98
+ if (creates > 0)
99
+ statsParts.push(`${creates} create(s)`);
100
+ if (alters > 0)
101
+ statsParts.push(`${alters} alter(s)`);
102
+ if (drops > 0)
103
+ statsParts.push(`${drops} drop(s)`);
104
+ if (renames > 0)
105
+ statsParts.push(`${renames} rename(s)`);
106
+ console.log(` ${statsParts.length > 0 ? statsParts.join(', ') : 'no changes'}`);
96
107
  console.log('');
97
108
  hint("run 'relq push' to apply changes to database");
98
109
  hint("run 'relq export' to export as SQL file");
@@ -6,7 +6,6 @@ import { isInitialized, getStagedChanges, getUnstagedChanges, loadSnapshot, } fr
6
6
  import { generateCombinedSQL, sortChangesByDependency, getChangeDisplayName, } from "../utils/change-tracker.js";
7
7
  import { generateFullSchemaSQL } from "../utils/sql-generator.js";
8
8
  import { loadRelqignore, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isSequenceIgnored, isCompositeTypeIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
9
- import { loadConfig } from "../../config/config.js";
10
9
  export async function exportCommand(context) {
11
10
  const spinner = createSpinner();
12
11
  const { args, flags, projectRoot } = context;
@@ -28,9 +27,9 @@ export async function exportCommand(context) {
28
27
  if (fromDb) {
29
28
  return exportFromDatabase(context, absoluteOutputPath, options);
30
29
  }
31
- return exportFromSnapshot(projectRoot, absoluteOutputPath, options);
30
+ return exportFromSnapshot(projectRoot, absoluteOutputPath, options, context.config ?? undefined);
32
31
  }
33
- async function exportFromSnapshot(projectRoot, absoluteOutputPath, options) {
32
+ async function exportFromSnapshot(projectRoot, absoluteOutputPath, options, config) {
34
33
  const spinner = createSpinner();
35
34
  if (!isInitialized(projectRoot)) {
36
35
  console.log(`${colors.red('error:')} relq not initialized`);
@@ -47,13 +46,13 @@ async function exportFromSnapshot(projectRoot, absoluteOutputPath, options) {
47
46
  return;
48
47
  }
49
48
  spinner.succeed('Loaded snapshot');
50
- const config = await loadConfig();
49
+ const cfg = config || {};
51
50
  const ignorePatterns = loadRelqignore(projectRoot);
52
51
  const filteredSnapshot = filterNormalizedSchema(snapshot, ignorePatterns, {
53
- includeFunctions: config.includeFunctions ?? options.includeFunctions ?? false,
54
- includeTriggers: config.includeTriggers ?? options.includeTriggers ?? false,
55
- includeViews: config.includeViews ?? false,
56
- includeFDW: config.includeFDW ?? false,
52
+ includeFunctions: cfg.includeFunctions ?? options.includeFunctions ?? true,
53
+ includeTriggers: cfg.includeTriggers ?? options.includeTriggers ?? true,
54
+ includeViews: cfg.includeViews ?? false,
55
+ includeFDW: cfg.includeFDW ?? false,
57
56
  });
58
57
  const schema = normalizedToDbSchema(filteredSnapshot);
59
58
  const ignoredCount = (snapshot.tables.length - filteredSnapshot.tables.length) +
@@ -77,7 +76,12 @@ async function exportFromSnapshot(projectRoot, absoluteOutputPath, options) {
77
76
  console.log(` ${colors.muted(`${ignoredCount} object(s) filtered by .relqignore`)}`);
78
77
  }
79
78
  spinner.start('Generating SQL statements');
80
- const sqlContent = generateFullSQL(schema, options);
79
+ const exportOptions = {
80
+ ...options,
81
+ includeFunctions: cfg.includeFunctions ?? options.includeFunctions ?? true,
82
+ includeTriggers: cfg.includeTriggers ?? options.includeTriggers ?? true,
83
+ };
84
+ const sqlContent = generateFullSQL(schema, exportOptions);
81
85
  spinner.succeed('Generated SQL statements');
82
86
  const outputDir = path.dirname(absoluteOutputPath);
83
87
  if (!fs.existsSync(outputDir)) {
@@ -147,15 +151,9 @@ function normalizedToDbSchema(normalized) {
147
151
  defaultValue: c.default,
148
152
  isPrimaryKey: c.primaryKey,
149
153
  isUnique: c.unique,
150
- maxLength: null,
151
- precision: null,
152
- scale: null,
153
- references: c.references ? {
154
- table: c.references.table,
155
- column: c.references.column,
156
- onDelete: c.references.onDelete,
157
- onUpdate: c.references.onUpdate,
158
- } : null,
154
+ maxLength: c.maxLength ?? null,
155
+ precision: c.precision ?? null,
156
+ scale: c.scale ?? null,
159
157
  check: c.check ?? undefined,
160
158
  comment: c.comment ?? undefined,
161
159
  isGenerated: c.isGenerated || false,
@@ -171,6 +169,7 @@ function normalizedToDbSchema(normalized) {
171
169
  definition: i.definition,
172
170
  whereClause: i.whereClause ?? null,
173
171
  expression: i.columnDefs?.find(cd => cd.expression)?.expression ?? null,
172
+ comment: i.comment ?? null,
174
173
  })),
175
174
  constraints: t.constraints.map(c => ({
176
175
  name: c.name,
@@ -185,6 +184,7 @@ function normalizedToDbSchema(normalized) {
185
184
  isPartitioned: t.isPartitioned || t.partitionType !== undefined,
186
185
  partitionType: t.partitionType,
187
186
  partitionKey: t.partitionKey,
187
+ comment: t.comment ?? null,
188
188
  })),
189
189
  enums: normalized.enums.map(e => ({
190
190
  name: e.name,
@@ -233,7 +233,13 @@ function normalizedToDbSchema(normalized) {
233
233
  definition: '',
234
234
  isEnabled: t.isEnabled !== false,
235
235
  })),
236
- partitions: [],
236
+ partitions: normalized.tables.flatMap(t => (t.partitions || []).map(p => ({
237
+ name: p.name,
238
+ parentTable: t.name,
239
+ partitionBound: p.bound || '',
240
+ partitionType: (p.boundType || t.partitionType || 'LIST'),
241
+ partitionKey: t.partitionKey || [],
242
+ }))),
237
243
  policies: [],
238
244
  foreignServers: [],
239
245
  foreignTables: [],