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
@@ -2,11 +2,12 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { requireValidConfig } from "../utils/config-loader.js";
4
4
  import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
5
- import { generateTypeScript } from "../utils/type-generator.js";
5
+ import { introspectedToParsedSchema } from "../utils/ast-transformer.js";
6
+ import { generateTypeScriptFromAST } from "../utils/ast-codegen.js";
6
7
  import { getConnectionDescription } from "../utils/env-loader.js";
7
- import { createSpinner, colors, formatBytes, formatDuration, fatal, confirm, warning } from "../utils/cli-utils.js";
8
+ import { createSpinner, colors, formatBytes, formatDuration, fatal, confirm, warning, createMultiProgress } from "../utils/cli-utils.js";
8
9
  import { loadRelqignore, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isCompositeTypeIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
9
- import { isInitialized, initRepository, getHead, saveSnapshot, loadSnapshot, createCommit, shortHash, fetchRemoteCommits, ensureRemoteTable, setFetchHead, addUnstagedChanges, getStagedChanges, getUnstagedChanges, clearWorkingState, hashFileContent, saveFileHash, } from "../utils/repo-manager.js";
10
+ import { isInitialized, initRepository, getHead, saveSnapshot, loadSnapshot, createCommit, shortHash, fetchRemoteCommits, ensureRemoteTable, setFetchHead, addUnstagedChanges, getStagedChanges, getUnstagedChanges, clearWorkingState, hashFileContent, saveFileHash, markCommitAsPulled, } from "../utils/repo-manager.js";
10
11
  import { compareSchemas } from "../utils/schema-comparator.js";
11
12
  function toCamelCase(str) {
12
13
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
@@ -145,8 +146,8 @@ export async function pullCommand(context) {
145
146
  const author = config.author || 'Relq CLI';
146
147
  const schemaPathRaw = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
147
148
  const schemaPath = path.resolve(projectRoot, schemaPathRaw);
148
- const includeFunctions = config.generate?.includeFunctions ?? false;
149
- const includeTriggers = config.generate?.includeTriggers ?? false;
149
+ const includeFunctions = config.includeFunctions ?? true;
150
+ const includeTriggers = config.includeTriggers ?? true;
150
151
  const spinner = createSpinner();
151
152
  const startTime = Date.now();
152
153
  console.log('');
@@ -202,12 +203,36 @@ export async function pullCommand(context) {
202
203
  setFetchHead(remoteHead, projectRoot);
203
204
  }
204
205
  spinner.succeed(`Fetched ${remoteCommits.length} remote commits`);
205
- spinner.start('Introspecting database...');
206
+ console.log('');
207
+ console.log(colors.bold('Introspecting database...'));
208
+ console.log('');
209
+ const progress = createMultiProgress();
210
+ const progressItems = [
211
+ { id: 'tables', label: 'tables', status: 'pending', count: 0 },
212
+ { id: 'columns', label: 'columns', status: 'pending', count: 0 },
213
+ { id: 'constraints', label: 'constraints', status: 'pending', count: 0 },
214
+ { id: 'indexes', label: 'indexes', status: 'pending', count: 0 },
215
+ { id: 'checks', label: 'checks', status: 'pending', count: 0 },
216
+ { id: 'enums', label: 'enums', status: 'pending', count: 0 },
217
+ { id: 'partitions', label: 'partitions', status: 'pending', count: 0 },
218
+ { id: 'extensions', label: 'extensions', status: 'pending', count: 0 },
219
+ ...(includeFunctions ? [{ id: 'functions', label: 'functions', status: 'pending', count: 0 }] : []),
220
+ ...(includeTriggers ? [{ id: 'triggers', label: 'triggers', status: 'pending', count: 0 }] : []),
221
+ { id: 'collations', label: 'collations', status: 'pending', count: 0 },
222
+ { id: 'foreign_servers', label: 'foreign servers', status: 'pending', count: 0 },
223
+ { id: 'foreign_tables', label: 'foreign tables', status: 'pending', count: 0 },
224
+ ];
225
+ progress.setItems(progressItems);
226
+ progress.start();
206
227
  const dbSchema = await fastIntrospectDatabase(connection, undefined, {
207
228
  includeFunctions,
208
229
  includeTriggers,
230
+ onDetailedProgress: (update) => {
231
+ progress.updateItem(update.step, { status: update.status, count: update.count });
232
+ },
209
233
  });
210
- spinner.succeed(`Found ${dbSchema.tables.length} tables`);
234
+ progress.complete();
235
+ console.log('');
211
236
  const ignorePatterns = loadRelqignore(projectRoot);
212
237
  const filteredTables = dbSchema.tables
213
238
  .filter(t => !isTableIgnored(t.name, ignorePatterns).ignored)
@@ -241,12 +266,21 @@ export async function pullCommand(context) {
241
266
  default: c.defaultValue,
242
267
  primaryKey: c.isPrimaryKey,
243
268
  unique: c.isUnique,
269
+ maxLength: c.maxLength ?? null,
270
+ precision: c.precision ?? null,
271
+ scale: c.scale ?? null,
272
+ comment: c.comment || undefined,
273
+ isGenerated: c.isGenerated || false,
274
+ generationExpression: c.generationExpression ?? null,
244
275
  })),
245
276
  indexes: t.indexes.map(i => ({
246
277
  name: i.name,
247
278
  columns: i.columns,
248
279
  unique: i.isUnique,
249
280
  type: i.type,
281
+ definition: i.definition || undefined,
282
+ whereClause: i.whereClause || undefined,
283
+ comment: i.comment || undefined,
250
284
  })),
251
285
  constraints: t.constraints.map(c => ({
252
286
  name: c.name,
@@ -256,6 +290,12 @@ export async function pullCommand(context) {
256
290
  isPartitioned: t.isPartitioned,
257
291
  partitionType: t.partitionType,
258
292
  partitionKey: normalizePartitionKey(t.partitionKey),
293
+ comment: t.comment || undefined,
294
+ partitions: t.childPartitions?.map((p) => ({
295
+ name: p.name,
296
+ bound: p.partitionBound || '',
297
+ boundType: p.partitionType,
298
+ })) || undefined,
259
299
  })),
260
300
  enums: filteredEnums.map(e => ({
261
301
  name: e.name,
@@ -359,24 +399,43 @@ export async function pullCommand(context) {
359
399
  }
360
400
  }
361
401
  else if (!schemaExists) {
362
- console.log('First pull - creating schema');
402
+ console.log(colors.bold('Creating schema') + colors.dim(' (first pull)'));
363
403
  console.log('');
404
+ const tableCount = filteredTables.length;
405
+ const columnCount = filteredTables.reduce((sum, t) => sum + t.columns.length, 0);
364
406
  const indexCount = filteredTables.reduce((sum, t) => sum + (t.indexes?.filter(i => !i.isPrimary).length || 0), 0);
365
- const partitionCount = filteredTables.filter(t => t.isPartitioned).length;
407
+ const partitionedCount = filteredTables.filter(t => t.isPartitioned).length;
408
+ const childPartitionCount = filteredTables.reduce((sum, t) => sum + (t.childPartitions?.length || 0), 0);
366
409
  const tableCommentCount = filteredTables.filter(t => t.comment).length;
367
410
  const columnCommentCount = filteredTables.reduce((sum, t) => sum + t.columns.filter(c => c.comment).length, 0);
368
- console.log('Schema Summary:');
369
- console.log(` ${colors.green(String(filteredTables.length))} tables`);
411
+ const indexCommentCount = filteredTables.reduce((sum, t) => sum + t.indexes.filter(i => i.comment).length, 0);
412
+ const summaryItems = [];
413
+ summaryItems.push({ count: tableCount, label: 'tables' });
414
+ summaryItems.push({ count: columnCount, label: 'columns' });
370
415
  if (indexCount > 0)
371
- console.log(` ${colors.green(String(indexCount))} indexes`);
372
- if (partitionCount > 0)
373
- console.log(` ${colors.green(String(partitionCount))} partitioned tables`);
374
- if (tableCommentCount > 0)
375
- console.log(` ${colors.green(String(tableCommentCount))} table comments`);
376
- if (columnCommentCount > 0)
377
- console.log(` ${colors.green(String(columnCommentCount))} column comments`);
416
+ summaryItems.push({ count: indexCount, label: 'indexes' });
417
+ if (partitionedCount > 0) {
418
+ const partLabel = childPartitionCount > 0
419
+ ? `partitioned (${childPartitionCount} child partitions)`
420
+ : 'partitioned';
421
+ summaryItems.push({ count: partitionedCount, label: partLabel });
422
+ }
423
+ if (tableCommentCount > 0 || columnCommentCount > 0 || indexCommentCount > 0) {
424
+ const commentParts = [];
425
+ if (tableCommentCount > 0)
426
+ commentParts.push(`${tableCommentCount} table`);
427
+ if (columnCommentCount > 0)
428
+ commentParts.push(`${columnCommentCount} column`);
429
+ if (indexCommentCount > 0)
430
+ commentParts.push(`${indexCommentCount} index`);
431
+ summaryItems.push({ count: tableCommentCount + columnCommentCount + indexCommentCount, label: `comments (${commentParts.join(', ')})` });
432
+ }
378
433
  if (dbSchema.extensions.length > 0)
379
- console.log(` ${colors.green(String(dbSchema.extensions.length))} extensions`);
434
+ summaryItems.push({ count: dbSchema.extensions.length, label: 'extensions' });
435
+ for (const item of summaryItems) {
436
+ const countStr = String(item.count).padStart(4);
437
+ console.log(`${colors.green('[+]')} ${colors.green(countStr)} ${item.label}`);
438
+ }
380
439
  console.log('');
381
440
  }
382
441
  if (dryRun) {
@@ -385,13 +444,10 @@ export async function pullCommand(context) {
385
444
  return;
386
445
  }
387
446
  spinner.start('Generating TypeScript schema...');
388
- const typescript = generateTypeScript(dbSchema, {
389
- includeDefineTables: true,
390
- includeSchema: true,
391
- includeIndexes: true,
392
- includeFunctions,
393
- includeTriggers,
447
+ const parsedSchema = await introspectedToParsedSchema(dbSchema);
448
+ const typescript = generateTypeScriptFromAST(parsedSchema, {
394
449
  camelCase: config.generate?.camelCase ?? true,
450
+ importPath: 'relq/schema-builder',
395
451
  });
396
452
  spinner.succeed('Generated TypeScript schema');
397
453
  const schemaDir = path.dirname(schemaPath);
@@ -494,6 +550,7 @@ export async function pullCommand(context) {
494
550
  triggers: filteredTriggers || [],
495
551
  };
496
552
  const schemaChanges = compareSchemas(beforeSchema, afterSchema);
553
+ applyTrackingIdsToSnapshot(typescript, currentSchema);
497
554
  saveSnapshot(currentSchema, projectRoot);
498
555
  const duration = Date.now() - startTime;
499
556
  if (noCommit) {
@@ -516,6 +573,7 @@ export async function pullCommand(context) {
516
573
  const connectionDesc = getConnectionDescription(connection);
517
574
  const commitMessage = `pull: sync from ${connectionDesc}`;
518
575
  const commit = createCommit(currentSchema, author, commitMessage, projectRoot);
576
+ markCommitAsPulled(commit.hash, connection, projectRoot);
519
577
  clearWorkingState(projectRoot);
520
578
  console.log('');
521
579
  console.log(`Pull completed in ${formatDuration(duration)}`);
@@ -598,3 +656,26 @@ function detectObjectConflicts(local, remote) {
598
656
  }
599
657
  return conflicts;
600
658
  }
659
+ function applyTrackingIdsToSnapshot(typescript, snapshot) {
660
+ for (const table of snapshot.tables) {
661
+ const tablePattern = new RegExp(`defineTable\\s*\\(\\s*['"]${table.name}['"]\\s*,\\s*\\{[^}]+\\}\\s*,\\s*\\{[^}]*\\$trackingId:\\s*['"]([^'"]+)['"]`, 's');
662
+ const tableMatch = typescript.match(tablePattern);
663
+ if (tableMatch) {
664
+ table.trackingId = tableMatch[1];
665
+ }
666
+ for (const col of table.columns) {
667
+ const colPattern = new RegExp(`(?:${col.tsName}|${col.name}):\\s*\\w+\\([^)]*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
668
+ const colMatch = typescript.match(colPattern);
669
+ if (colMatch) {
670
+ col.trackingId = colMatch[1];
671
+ }
672
+ }
673
+ for (const idx of table.indexes) {
674
+ const idxPattern = new RegExp(`index\\s*\\(\\s*['"]${idx.name}['"]\\s*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
675
+ const idxMatch = typescript.match(idxPattern);
676
+ if (idxMatch) {
677
+ idx.trackingId = idxMatch[1];
678
+ }
679
+ }
680
+ }
681
+ }
@@ -5,7 +5,7 @@ import { getConnectionDescription } from "../utils/env-loader.js";
5
5
  import { colors, createSpinner, fatal, confirm, warning } from "../utils/cli-utils.js";
6
6
  import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
7
7
  import { loadRelqignore, isTableIgnored, isColumnIgnored, isEnumIgnored, isDomainIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
8
- import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, } from "../utils/repo-manager.js";
8
+ import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, isCommitSyncedWith, markCommitAsPushed, getConnectionLabel, } from "../utils/repo-manager.js";
9
9
  export async function pushCommand(context) {
10
10
  const { config, flags } = context;
11
11
  if (!config) {
@@ -41,7 +41,15 @@ export async function pushCommand(context) {
41
41
  const localCommits = getAllCommits(projectRoot);
42
42
  const remoteHashes = new Set(remoteCommits.map(c => c.hash));
43
43
  const localHashes = new Set(localCommits.map(c => c.hash));
44
- const toPush = localCommits.filter(c => !remoteHashes.has(c.hash));
44
+ const toPush = localCommits.filter(c => {
45
+ if (remoteHashes.has(c.hash))
46
+ return false;
47
+ const syncStatus = isCommitSyncedWith(c, connection);
48
+ if (syncStatus.synced) {
49
+ return false;
50
+ }
51
+ return true;
52
+ });
45
53
  const remoteMissing = remoteCommits.filter(c => !localHashes.has(c.hash));
46
54
  spinner.succeed('Checked remote commits');
47
55
  spinner.start('Introspecting remote database...');
@@ -131,9 +139,10 @@ export async function pushCommand(context) {
131
139
  const commitsToProcess = [...toPush].reverse();
132
140
  spinner.start(`Pushing ${toPush.length} commit(s)...`);
133
141
  for (const commit of commitsToProcess) {
134
- await pushCommit(connection, commit);
142
+ await pushCommit(connection, commit, projectRoot);
143
+ markCommitAsPushed(commit.hash, connection, projectRoot);
135
144
  }
136
- spinner.succeed(`Pushed ${toPush.length} commit(s)`);
145
+ spinner.succeed(`Pushed ${toPush.length} commit(s) to ${getConnectionLabel(connection)}`);
137
146
  }
138
147
  if (applySQL) {
139
148
  spinner.start('Applying SQL changes...');
@@ -173,6 +182,32 @@ export async function pushCommand(context) {
173
182
  }
174
183
  await client.query('COMMIT');
175
184
  spinner.succeed(`Applied ${statementsRun} statement(s) from ${sqlExecuted} commit(s)`);
185
+ let hasRenameOperations = false;
186
+ for (const commit of commitsToProcess) {
187
+ const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
188
+ if (fs.existsSync(commitPath)) {
189
+ const enhancedCommit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
190
+ if (enhancedCommit.changes?.some((c) => c.type === 'RENAME')) {
191
+ hasRenameOperations = true;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ if (hasRenameOperations) {
197
+ spinner.start('Updating snapshot after RENAME...');
198
+ try {
199
+ const updatedSchema = await fastIntrospectDatabase(connection, undefined, {
200
+ includeFunctions,
201
+ includeTriggers,
202
+ });
203
+ const snapshotPath = path.join(projectRoot, '.relq', 'snapshot.json');
204
+ fs.writeFileSync(snapshotPath, JSON.stringify(updatedSchema, null, 2));
205
+ spinner.succeed('Snapshot updated with renamed objects');
206
+ }
207
+ catch (e) {
208
+ spinner.warn('Could not update snapshot after RENAME - run "relq pull" manually');
209
+ }
210
+ }
176
211
  }
177
212
  catch (error) {
178
213
  try {
@@ -62,7 +62,14 @@ function parseArgs(argv) {
62
62
  flags[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1);
63
63
  }
64
64
  else {
65
- flags[arg.slice(2)] = true;
65
+ const nextArg = argv[i + 1];
66
+ if (nextArg && !nextArg.startsWith('-')) {
67
+ flags[arg.slice(2)] = nextArg;
68
+ i++;
69
+ }
70
+ else {
71
+ flags[arg.slice(2)] = true;
72
+ }
66
73
  }
67
74
  }
68
75
  else if (arg.startsWith('-') && arg.length === 2) {
@@ -175,6 +182,7 @@ async function main() {
175
182
  printHelp();
176
183
  process.exit(1);
177
184
  }
185
+ console.log('Command:', command);
178
186
  let config = null;
179
187
  let resolvedProjectRoot = process.cwd();
180
188
  if (requiresConfig(command)) {
@@ -0,0 +1,291 @@
1
+ import { toCamelCase, escapeString } from "./utils.js";
2
+ export const KNOWN_BUILDER_FUNCTIONS = {
3
+ 'setweight': 'setweight',
4
+ 'to_tsvector': 'toTsvector',
5
+ 'to_tsquery': 'toTsquery',
6
+ 'plainto_tsquery': 'plainToTsquery',
7
+ 'phraseto_tsquery': 'phraseToTsquery',
8
+ 'websearch_to_tsquery': 'websearchToTsquery',
9
+ 'ts_headline': 'tsHeadline',
10
+ 'ts_rank': 'tsRank',
11
+ 'ts_rank_cd': 'tsRankCd',
12
+ 'coalesce': 'coalesce',
13
+ 'nullif': 'nullif',
14
+ 'greatest': 'greatest',
15
+ 'least': 'least',
16
+ 'concat': 'concat',
17
+ 'concat_ws': 'concatWs',
18
+ 'lower': 'lower',
19
+ 'upper': 'upper',
20
+ 'trim': 'trim',
21
+ 'ltrim': 'ltrim',
22
+ 'rtrim': 'rtrim',
23
+ 'length': 'length',
24
+ 'substring': 'substring',
25
+ 'replace': 'replace',
26
+ 'regexp_replace': 'regexpReplace',
27
+ 'split_part': 'splitPart',
28
+ 'now': 'now',
29
+ 'current_timestamp': 'currentTimestamp',
30
+ 'current_date': 'currentDate',
31
+ 'current_time': 'currentTime',
32
+ 'date_trunc': 'dateTrunc',
33
+ 'date_part': 'datePart',
34
+ 'extract': 'extract',
35
+ 'age': 'age',
36
+ 'gen_random_uuid': 'genRandomUuid',
37
+ 'uuid_generate_v4': 'uuidGenerateV4',
38
+ 'jsonb_build_object': 'jsonbBuildObject',
39
+ 'jsonb_agg': 'jsonbAgg',
40
+ 'json_agg': 'jsonAgg',
41
+ 'array_agg': 'arrayAgg',
42
+ 'string_agg': 'stringAgg',
43
+ 'count': 'count',
44
+ 'sum': 'sum',
45
+ 'avg': 'avg',
46
+ 'min': 'min',
47
+ 'max': 'max',
48
+ 'abs': 'abs',
49
+ 'ceil': 'ceil',
50
+ 'floor': 'floor',
51
+ 'round': 'round',
52
+ 'sqrt': 'sqrt',
53
+ 'power': 'power',
54
+ 'mod': 'mod',
55
+ 'random': 'random',
56
+ };
57
+ export function mapFunctionToBuilder(funcName) {
58
+ return KNOWN_BUILDER_FUNCTIONS[funcName] || null;
59
+ }
60
+ export function astToBuilder(node, prefixOrOptions = 'g') {
61
+ const opts = typeof prefixOrOptions === 'string'
62
+ ? { prefix: prefixOrOptions }
63
+ : prefixOrOptions;
64
+ const { prefix = 'g', useCamelCase = false, useTableRef = false } = opts;
65
+ if (!node)
66
+ return "''";
67
+ if (node.FuncCall) {
68
+ const func = node.FuncCall;
69
+ const funcName = func.funcname?.map((n) => n.String?.sval).filter(Boolean).join('.') || '';
70
+ const args = (func.args || []).map((a) => astToBuilder(a, opts));
71
+ const builderMethod = mapFunctionToBuilder(funcName.toLowerCase());
72
+ if (builderMethod) {
73
+ return `${prefix}.${builderMethod}(${args.join(', ')})`;
74
+ }
75
+ throw new Error(`Unsupported function in generated expression: "${funcName}". Add this function to KNOWN_BUILDER_FUNCTIONS in astToBuilder.`);
76
+ }
77
+ if (node.CoalesceExpr) {
78
+ const args = (node.CoalesceExpr.args || []).map((a) => astToBuilder(a, opts));
79
+ return `${prefix}.coalesce(${args.join(', ')})`;
80
+ }
81
+ if (node.A_Expr) {
82
+ const expr = node.A_Expr;
83
+ const op = expr.name?.[0]?.String?.sval || '';
84
+ const left = astToBuilder(expr.lexpr, opts);
85
+ const right = astToBuilder(expr.rexpr, opts);
86
+ switch (op) {
87
+ case '||':
88
+ return `__CONCAT__[${left}, ${right}]`;
89
+ case '->>':
90
+ return `${prefix}.jsonbExtractText(${left}, ${right})`;
91
+ case '->':
92
+ return `${prefix}.jsonbExtract(${left}, ${right})`;
93
+ case '@@':
94
+ return `${prefix}.tsMatch(${left}, ${right})`;
95
+ case '+':
96
+ return `${prefix}.add(${left}, ${right})`;
97
+ case '-':
98
+ return `${prefix}.subtract(${left}, ${right})`;
99
+ case '*':
100
+ return `${prefix}.multiply(${left}, ${right})`;
101
+ case '/':
102
+ return `${prefix}.divide(${left}, ${right})`;
103
+ case '%':
104
+ return `${prefix}.mod(${left}, ${right})`;
105
+ case '=':
106
+ case '<>':
107
+ case '!=':
108
+ case '<':
109
+ case '>':
110
+ case '<=':
111
+ case '>=':
112
+ if (useTableRef) {
113
+ const methodMap = {
114
+ '=': 'eq', '<>': 'ne', '!=': 'neq',
115
+ '<': 'lt', '>': 'gt', '<=': 'lte', '>=': 'gte'
116
+ };
117
+ return `${left}.${methodMap[op]}(${right})`;
118
+ }
119
+ return `${prefix}.compare(${left}, '${op}', ${right})`;
120
+ case '~':
121
+ case '~*':
122
+ case '!~':
123
+ case '!~*':
124
+ return `${prefix}.regex(${left}, '${op}', ${right})`;
125
+ default:
126
+ throw new Error(`Unsupported operator in generated expression: "${op}". Add explicit handling for this operator in astToBuilder.`);
127
+ }
128
+ }
129
+ if (node.ColumnRef) {
130
+ const fields = (node.ColumnRef.fields || [])
131
+ .map((f) => f.String?.sval)
132
+ .filter(Boolean);
133
+ if (fields.length === 1) {
134
+ const colName = useCamelCase ? toCamelCase(fields[0]) : fields[0];
135
+ return `table.${colName}`;
136
+ }
137
+ else if (fields.length === 2) {
138
+ const colName = useCamelCase ? toCamelCase(fields[1]) : fields[1];
139
+ return `table.${colName}`;
140
+ }
141
+ const colName = useCamelCase ? toCamelCase(fields[fields.length - 1]) : fields[fields.length - 1];
142
+ return `table.${colName}`;
143
+ }
144
+ if (node.A_Const) {
145
+ const val = node.A_Const;
146
+ if (val.sval !== undefined) {
147
+ const s = val.sval?.sval ?? val.sval;
148
+ if (s === '' || s === ' ') {
149
+ return s === '' ? `${prefix}.asText()` : `${prefix}.asText(' ')`;
150
+ }
151
+ return `'${escapeString(String(s))}'`;
152
+ }
153
+ else if (val.ival !== undefined) {
154
+ const i = val.ival?.ival ?? val.ival;
155
+ return String(i);
156
+ }
157
+ else if (val.fval !== undefined) {
158
+ const f = val.fval?.fval ?? val.fval;
159
+ return String(f);
160
+ }
161
+ else if (val.boolval !== undefined) {
162
+ return val.boolval?.boolval ? 'true' : 'false';
163
+ }
164
+ return `${prefix}.asText()`;
165
+ }
166
+ if (node.TypeCast) {
167
+ const arg = astToBuilder(node.TypeCast.arg, opts);
168
+ const typeName = node.TypeCast.typeName?.names
169
+ ?.map((n) => n.String?.sval)
170
+ .filter(Boolean)
171
+ .join('.') || '';
172
+ if (typeName === 'regconfig' || typeName === 'pg_catalog.regconfig') {
173
+ return arg;
174
+ }
175
+ if (typeName === 'text' || typeName === 'pg_catalog.text') {
176
+ return `${prefix}.asText(${arg})`;
177
+ }
178
+ if (typeName === 'varchar' || typeName === 'pg_catalog.varchar' || typeName.includes('character varying')) {
179
+ return arg;
180
+ }
181
+ if (typeName === 'char' || typeName === 'bpchar' || typeName === 'pg_catalog.bpchar') {
182
+ return arg;
183
+ }
184
+ if (typeName === 'numeric' || typeName === 'pg_catalog.numeric') {
185
+ return arg;
186
+ }
187
+ if (typeName === 'int4' || typeName === 'integer' || typeName === 'pg_catalog.int4') {
188
+ return arg;
189
+ }
190
+ return `${prefix}.cast(${arg}, '${typeName}')`;
191
+ }
192
+ if (node.NullTest) {
193
+ const arg = astToBuilder(node.NullTest.arg, opts);
194
+ const isNull = node.NullTest.nulltesttype === 'IS_NULL';
195
+ return isNull ? `${prefix}.isNull(${arg})` : `${prefix}.isNotNull(${arg})`;
196
+ }
197
+ if (node.BoolExpr) {
198
+ const boolexpr = node.BoolExpr;
199
+ const args = (boolexpr.args || []).map((a) => astToBuilder(a, opts));
200
+ if (useTableRef && args.length >= 2) {
201
+ switch (boolexpr.boolop) {
202
+ case 'AND_EXPR': return args.reduce((acc, arg) => `${acc}.and(${arg})`);
203
+ case 'OR_EXPR': return args.reduce((acc, arg) => `${acc}.or(${arg})`);
204
+ case 'NOT_EXPR': return `${args[0]}.not()`;
205
+ }
206
+ }
207
+ switch (boolexpr.boolop) {
208
+ case 'AND_EXPR': return `${prefix}.and(${args.join(', ')})`;
209
+ case 'OR_EXPR': return `${prefix}.or(${args.join(', ')})`;
210
+ case 'NOT_EXPR': return `${prefix}.not(${args[0]})`;
211
+ }
212
+ }
213
+ if (node.CaseExpr) {
214
+ const caseExpr = node.CaseExpr;
215
+ let chain = `${prefix}.case()`;
216
+ for (const w of (caseExpr.args || [])) {
217
+ if (w.CaseWhen) {
218
+ const condition = astToBuilder(w.CaseWhen.expr, opts);
219
+ const result = astToBuilder(w.CaseWhen.result, opts);
220
+ chain += `.when(${condition}, ${result})`;
221
+ }
222
+ }
223
+ if (caseExpr.defresult) {
224
+ const defResult = astToBuilder(caseExpr.defresult, opts);
225
+ chain += `.else(${defResult})`;
226
+ }
227
+ else {
228
+ chain += '.end()';
229
+ }
230
+ return chain;
231
+ }
232
+ const nodeType = Object.keys(node)[0];
233
+ throw new Error(`Unsupported AST node type in generated expression: "${nodeType}". Add explicit handling for this node type in astToBuilder.`);
234
+ }
235
+ function extractConcatItems(expr) {
236
+ if (expr.startsWith('__CONCAT__[')) {
237
+ const inner = expr.slice('__CONCAT__['.length, -1);
238
+ const items = [];
239
+ let depth = 0;
240
+ let current = '';
241
+ let inQuote = false;
242
+ for (let i = 0; i < inner.length; i++) {
243
+ const c = inner[i];
244
+ if (c === "'" && inner[i - 1] !== '\\') {
245
+ inQuote = !inQuote;
246
+ }
247
+ if (!inQuote) {
248
+ if (c === '[' || c === '(')
249
+ depth++;
250
+ if (c === ']' || c === ')')
251
+ depth--;
252
+ if (c === ',' && depth === 0) {
253
+ items.push(current.trim());
254
+ current = '';
255
+ continue;
256
+ }
257
+ }
258
+ current += c;
259
+ }
260
+ if (current.trim())
261
+ items.push(current.trim());
262
+ return items.flatMap(item => extractConcatItems(item));
263
+ }
264
+ return [expr];
265
+ }
266
+ function cleanAllConcats(expr) {
267
+ let result = expr;
268
+ let iterations = 0;
269
+ const maxIterations = 50;
270
+ while (result.includes('__CONCAT__[') && iterations < maxIterations) {
271
+ const match = result.match(/__CONCAT__\[([^\[\]]*)\]/);
272
+ if (match) {
273
+ const replacement = `F.concat(${match[1]})`;
274
+ result = result.replace(match[0], replacement);
275
+ }
276
+ else {
277
+ break;
278
+ }
279
+ iterations++;
280
+ }
281
+ return result;
282
+ }
283
+ export function formatGeneratedExpression(expr) {
284
+ if (expr.startsWith('__CONCAT__[')) {
285
+ const items = extractConcatItems(expr);
286
+ const cleanedItems = items.map(item => cleanAllConcats(item));
287
+ return { isArray: true, items: cleanedItems };
288
+ }
289
+ const cleaned = cleanAllConcats(expr);
290
+ return { isArray: false, items: [cleaned] };
291
+ }