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.
- package/dist/cjs/cli/commands/add.cjs +257 -17
- package/dist/cjs/cli/commands/commit.cjs +13 -2
- package/dist/cjs/cli/commands/export.cjs +25 -19
- package/dist/cjs/cli/commands/import.cjs +219 -100
- package/dist/cjs/cli/commands/init.cjs +86 -14
- package/dist/cjs/cli/commands/pull.cjs +104 -23
- package/dist/cjs/cli/commands/push.cjs +38 -3
- package/dist/cjs/cli/index.cjs +9 -1
- package/dist/cjs/cli/utils/ast/codegen/builder.cjs +297 -0
- package/dist/cjs/cli/utils/ast/codegen/constraints.cjs +185 -0
- package/dist/cjs/cli/utils/ast/codegen/defaults.cjs +311 -0
- package/dist/cjs/cli/utils/ast/codegen/index.cjs +24 -0
- package/dist/cjs/cli/utils/ast/codegen/type-map.cjs +116 -0
- package/dist/cjs/cli/utils/ast/codegen/utils.cjs +69 -0
- package/dist/cjs/cli/utils/ast/index.cjs +19 -0
- package/dist/cjs/cli/utils/ast/transformer/helpers.cjs +154 -0
- package/dist/cjs/cli/utils/ast/transformer/index.cjs +25 -0
- package/dist/cjs/cli/utils/ast/types.cjs +2 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +949 -0
- package/dist/cjs/cli/utils/ast-transformer.cjs +916 -0
- package/dist/cjs/cli/utils/change-tracker.cjs +50 -1
- package/dist/cjs/cli/utils/cli-utils.cjs +151 -0
- package/dist/cjs/cli/utils/fast-introspect.cjs +149 -23
- package/dist/cjs/cli/utils/pg-parser.cjs +1 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +121 -4
- package/dist/cjs/cli/utils/schema-comparator.cjs +98 -14
- package/dist/cjs/cli/utils/schema-introspect.cjs +56 -19
- package/dist/cjs/cli/utils/snapshot-manager.cjs +0 -1
- package/dist/cjs/cli/utils/sql-generator.cjs +353 -64
- package/dist/cjs/cli/utils/type-generator.cjs +114 -15
- package/dist/cjs/config/config.cjs +29 -10
- package/dist/cjs/core/relq-client.cjs +22 -6
- package/dist/cjs/schema-definition/column-types.cjs +149 -13
- package/dist/cjs/schema-definition/defaults.cjs +72 -0
- package/dist/cjs/schema-definition/index.cjs +15 -1
- package/dist/cjs/schema-definition/introspection.cjs +7 -3
- package/dist/cjs/schema-definition/pg-relations.cjs +169 -0
- package/dist/cjs/schema-definition/pg-view.cjs +30 -0
- package/dist/cjs/schema-definition/table-definition.cjs +110 -4
- package/dist/cjs/types/config-types.cjs +13 -4
- package/dist/cjs/utils/aws-dsql.cjs +177 -0
- package/dist/config.d.ts +147 -2
- package/dist/esm/cli/commands/add.js +255 -18
- package/dist/esm/cli/commands/commit.js +13 -2
- package/dist/esm/cli/commands/export.js +25 -19
- package/dist/esm/cli/commands/import.js +221 -102
- package/dist/esm/cli/commands/init.js +86 -14
- package/dist/esm/cli/commands/pull.js +106 -25
- package/dist/esm/cli/commands/push.js +39 -4
- package/dist/esm/cli/index.js +9 -1
- package/dist/esm/cli/utils/ast/codegen/builder.js +291 -0
- package/dist/esm/cli/utils/ast/codegen/constraints.js +176 -0
- package/dist/esm/cli/utils/ast/codegen/defaults.js +305 -0
- package/dist/esm/cli/utils/ast/codegen/index.js +6 -0
- package/dist/esm/cli/utils/ast/codegen/type-map.js +111 -0
- package/dist/esm/cli/utils/ast/codegen/utils.js +60 -0
- package/dist/esm/cli/utils/ast/index.js +3 -0
- package/dist/esm/cli/utils/ast/transformer/helpers.js +141 -0
- package/dist/esm/cli/utils/ast/transformer/index.js +2 -0
- package/dist/esm/cli/utils/ast/types.js +1 -0
- package/dist/esm/cli/utils/ast-codegen.js +945 -0
- package/dist/esm/cli/utils/ast-transformer.js +907 -0
- package/dist/esm/cli/utils/change-tracker.js +50 -1
- package/dist/esm/cli/utils/cli-utils.js +147 -0
- package/dist/esm/cli/utils/fast-introspect.js +149 -23
- package/dist/esm/cli/utils/pg-parser.js +1 -0
- package/dist/esm/cli/utils/repo-manager.js +114 -4
- package/dist/esm/cli/utils/schema-comparator.js +98 -14
- package/dist/esm/cli/utils/schema-introspect.js +56 -19
- package/dist/esm/cli/utils/snapshot-manager.js +0 -1
- package/dist/esm/cli/utils/sql-generator.js +353 -64
- package/dist/esm/cli/utils/type-generator.js +114 -15
- package/dist/esm/config/config.js +29 -10
- package/dist/esm/core/relq-client.js +23 -7
- package/dist/esm/schema-definition/column-types.js +146 -12
- package/dist/esm/schema-definition/defaults.js +69 -0
- package/dist/esm/schema-definition/index.js +3 -0
- package/dist/esm/schema-definition/introspection.js +7 -3
- package/dist/esm/schema-definition/pg-relations.js +161 -0
- package/dist/esm/schema-definition/pg-view.js +24 -0
- package/dist/esm/schema-definition/table-definition.js +110 -4
- package/dist/esm/types/config-types.js +12 -4
- package/dist/esm/utils/aws-dsql.js +139 -0
- package/dist/index.d.ts +159 -1
- package/dist/schema-builder.d.ts +1314 -32
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
177
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
617
|
+
if (['FUNCTION', 'PROCEDURE'].includes(change.objectType) && !config?.includeFunctions) {
|
|
381
618
|
return false;
|
|
382
619
|
}
|
|
383
|
-
if (change.objectType === 'TRIGGER' && !config
|
|
620
|
+
if (change.objectType === 'TRIGGER' && !config?.includeTriggers) {
|
|
384
621
|
return false;
|
|
385
622
|
}
|
|
386
|
-
if (['VIEW', 'MATERIALIZED_VIEW'].includes(change.objectType) && !config
|
|
623
|
+
if (['VIEW', 'MATERIALIZED_VIEW'].includes(change.objectType) && !config?.includeViews) {
|
|
387
624
|
return false;
|
|
388
625
|
}
|
|
389
|
-
if (change.objectType === 'FOREIGN_TABLE' && !config
|
|
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
|
|
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
|
-
|
|
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
|
|
49
|
+
const cfg = config || {};
|
|
51
50
|
const ignorePatterns = loadRelqignore(projectRoot);
|
|
52
51
|
const filteredSnapshot = filterNormalizedSchema(snapshot, ignorePatterns, {
|
|
53
|
-
includeFunctions:
|
|
54
|
-
includeTriggers:
|
|
55
|
-
includeViews:
|
|
56
|
-
includeFDW:
|
|
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
|
|
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: [],
|