@dbsp/cli 1.0.0

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/index.js ADDED
@@ -0,0 +1,1138 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createDbConnection,
4
+ generateSchemaFile,
5
+ redactDbUrl
6
+ } from "./chunk-UZEMCTNH.js";
7
+ import {
8
+ loadSchema,
9
+ loadSchemaFromCwd
10
+ } from "./chunk-AQC34IO5.js";
11
+ import {
12
+ config
13
+ } from "./chunk-U5DSGBS2.js";
14
+
15
+ // src/index.ts
16
+ import { Command as Command7, CommanderError } from "commander";
17
+
18
+ // src/commands/generate.ts
19
+ import { mkdirSync, writeFileSync } from "fs";
20
+ import { dirname, resolve } from "path";
21
+ import { Command } from "commander";
22
+ var generateCommand = new Command("generate").description("Generate code from schema").argument("<target>", "Target to generate: ddl").option("-s, --schema <path>", "Path to schema file (default: auto-detect)").option("-o, --out <dir>", "Output directory (default: ./generated/<target>)").option("--output <dir>", "Output directory (alias for --out)").option("--drop", "Include DROP TABLE IF EXISTS statements (ddl only)").option("--schema-name <name>", "Database schema name (ddl only)").option(
23
+ "--dialect <name>",
24
+ "Database dialect: postgresql | mysql | sqlite | mssql (default: postgresql)"
25
+ ).option(
26
+ "--casing <type>",
27
+ "Column naming: snake | camel | none (default: based on dialect)"
28
+ ).action(
29
+ async (target, options) => {
30
+ try {
31
+ let schema;
32
+ let schemaPath;
33
+ if (options.schema) {
34
+ schema = await loadSchema(options.schema);
35
+ schemaPath = options.schema;
36
+ } else {
37
+ const result = await loadSchemaFromCwd();
38
+ schema = result.schema;
39
+ schemaPath = result.path;
40
+ }
41
+ const outputPath = options.out ?? options.output;
42
+ const useStdout = target === "ddl" && !outputPath;
43
+ const log = useStdout ? console.error : console.log;
44
+ log(`\u{1F4C4} Loaded schema from: ${schemaPath}`);
45
+ const dialect = options.dialect ?? "postgresql";
46
+ if (dialect !== "postgresql") {
47
+ console.error(
48
+ `\u26A0\uFE0F Warning: Only 'postgresql' dialect is currently supported. Using postgresql.`
49
+ );
50
+ }
51
+ switch (target) {
52
+ case "ddl": {
53
+ const casing = options.casing ?? "snake";
54
+ const { createPgsqlCompileOnlyAdapter } = await import("@dbsp/adapter-pgsql");
55
+ const dbCasing = casing === "snake" ? "snake_case" : "preserve";
56
+ const adapter = createPgsqlCompileOnlyAdapter({
57
+ dbCasing,
58
+ ...options.schemaName ? { schemaName: options.schemaName } : {}
59
+ });
60
+ {
61
+ const ddlStatements = adapter.generateDDL(schema.model, {
62
+ ...options.drop !== void 0 && {
63
+ includeDropStatements: options.drop
64
+ }
65
+ });
66
+ const ddlContent = ddlStatements.join("\n\n");
67
+ const outputPath2 = options.out ?? options.output;
68
+ if (outputPath2) {
69
+ const outPath = outputPath2.endsWith(".sql") ? resolve(process.cwd(), outputPath2) : resolve(process.cwd(), outputPath2, "schema.sql");
70
+ mkdirSync(dirname(outPath), { recursive: true });
71
+ writeFileSync(outPath, ddlContent, "utf-8");
72
+ console.log(`\u2705 Generated DDL: ${outPath}`);
73
+ console.log(` Tables: ${schema.tableNames.length}`);
74
+ console.log(` Statements: ${ddlStatements.length}`);
75
+ console.log(` Casing: ${casing}`);
76
+ if (options.drop) {
77
+ console.log(` Includes DROP statements`);
78
+ }
79
+ if (options.schemaName) {
80
+ console.log(` Schema: ${options.schemaName}`);
81
+ }
82
+ } else {
83
+ console.log(ddlContent);
84
+ }
85
+ }
86
+ break;
87
+ }
88
+ case "manifest":
89
+ case "kysely":
90
+ throw new Error(
91
+ `Target '${target}' has been removed in ARCH-005. These generators required the legacy defineSchema() format. Use 'ddl' target for SQL generation.`
92
+ );
93
+ default:
94
+ throw new Error(
95
+ `Unknown target: ${target}. Available targets: ddl`
96
+ );
97
+ }
98
+ } catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ console.error(`\u274C ${message}`);
101
+ process.exit(1);
102
+ }
103
+ }
104
+ );
105
+
106
+ // src/commands/introspect.ts
107
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
108
+ import { dirname as dirname2, resolve as resolve2 } from "path";
109
+ import { Command as Command2 } from "commander";
110
+ var introspectCommand = new Command2("introspect").description("Generate schema.ts from database introspection").requiredOption("-d, --db <url>", "Database connection URL (required)").option("-o, --out <file>", "Output schema file", "./dbsp.schema.ts").option("--schema-name <name>", "Database schema name", "public").option(
111
+ "--exclude <patterns>",
112
+ "Tables to exclude (comma-separated glob patterns)",
113
+ "_migrations,_prisma*,pg_*"
114
+ ).option("--include <patterns>", "Tables to include (comma-separated)").option("--no-db-type-comments", "Omit original DB type comments").option(
115
+ "--db-casing <casing>",
116
+ "Database column casing (snake_case \u2192 camelCase in generated code)",
117
+ "snake_case"
118
+ ).action(
119
+ async (options) => {
120
+ const redactedUrl = redactDbUrl(options.db);
121
+ console.log(`\u{1F50D} Introspecting database: ${redactedUrl}`);
122
+ console.log(` Schema: ${options.schemaName}`);
123
+ if (options.exclude) {
124
+ console.log(` Excluding: ${options.exclude}`);
125
+ }
126
+ console.log("");
127
+ try {
128
+ const { pool } = await createDbConnection(options.db);
129
+ try {
130
+ const { introspect: introspect4 } = await import("@dbsp/adapter-pgsql");
131
+ const excludePatterns = options.exclude ? options.exclude.split(",").map((s) => s.trim()) : void 0;
132
+ const includePatterns = options.include ? options.include.split(",").map((s) => s.trim()) : void 0;
133
+ const model = await introspect4(pool, {
134
+ schema: options.schemaName,
135
+ ...excludePatterns ? { exclude: excludePatterns } : {},
136
+ ...includePatterns ? { include: includePatterns } : {}
137
+ });
138
+ const tableCount = model.tables.size;
139
+ const relationCount = model.relations.size;
140
+ const hierarchyCount = model.hierarchies?.length ?? 0;
141
+ console.log(
142
+ `\u{1F4CA} Found ${tableCount} tables, ${relationCount} relations, ${hierarchyCount} hierarchies`
143
+ );
144
+ if (model.warnings?.length) {
145
+ for (const w of model.warnings) {
146
+ console.log(` \u26A0\uFE0F ${w}`);
147
+ }
148
+ }
149
+ console.log("");
150
+ const codegenOptions = {
151
+ sourceUrl: options.db,
152
+ includeDbTypeComments: options.dbTypeComments,
153
+ warnings: model.warnings,
154
+ introspectedAt: model.introspectedAt,
155
+ dbCasing: options.dbCasing
156
+ };
157
+ const schemaCode = generateSchemaFile(model, codegenOptions);
158
+ const outPath = resolve2(process.cwd(), options.out);
159
+ mkdirSync2(dirname2(outPath), { recursive: true });
160
+ writeFileSync2(outPath, schemaCode, "utf-8");
161
+ console.log(`\u2705 Generated schema: ${outPath}`);
162
+ console.log(` Tables: ${tableCount}`);
163
+ console.log(` Relations: ${relationCount}`);
164
+ } finally {
165
+ await pool.end();
166
+ }
167
+ } catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ console.error(`\u274C ${message}`);
170
+ process.exit(1);
171
+ }
172
+ }
173
+ );
174
+
175
+ // src/commands/migrate.ts
176
+ import {
177
+ compareSchemata,
178
+ ensureMigrationsTable,
179
+ generateMigrationFile,
180
+ generateMigrationSQL,
181
+ getAppliedMigrations,
182
+ getNextSchemaVersion,
183
+ introspect,
184
+ isDestructiveDown,
185
+ parseMigrationFile,
186
+ recordMigration,
187
+ removeMigrationRecord,
188
+ withMigrationLock
189
+ } from "@dbsp/adapter-pgsql";
190
+ import { Command as Command3 } from "commander";
191
+
192
+ // src/migration-file.ts
193
+ import { createHash } from "crypto";
194
+ import {
195
+ existsSync,
196
+ mkdirSync as mkdirSync3,
197
+ readdirSync,
198
+ readFileSync,
199
+ writeFileSync as writeFileSync3
200
+ } from "fs";
201
+ import { join, resolve as resolve3 } from "path";
202
+ var DEFAULT_MIGRATIONS_DIR = "migrations";
203
+ var MIGRATION_FILENAME_PATTERN = /^\d{4}_[\w-]+\.sql$/;
204
+ function computeChecksum(content) {
205
+ return createHash("sha256").update(content, "utf-8").digest("hex");
206
+ }
207
+ function generateMigrationFilename(existingFiles, description) {
208
+ const maxNum = existingFiles.reduce((max, f) => {
209
+ const match = f.match(/^(\d{4})/);
210
+ return match?.[1] ? Math.max(max, Number.parseInt(match[1], 10)) : max;
211
+ }, 0);
212
+ const nextNum = String(maxNum + 1).padStart(4, "0");
213
+ const sanitized = description.toLowerCase().replace(/[^a-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
214
+ return `${nextNum}_${sanitized || "migration"}.sql`;
215
+ }
216
+ function ensureMigrationsDir(dir) {
217
+ const fullPath = resolve3(dir);
218
+ if (!existsSync(fullPath)) {
219
+ mkdirSync3(fullPath, { recursive: true });
220
+ }
221
+ return fullPath;
222
+ }
223
+ function scanMigrationFiles(dir) {
224
+ const fullPath = resolve3(dir);
225
+ if (!existsSync(fullPath)) {
226
+ return [];
227
+ }
228
+ const entries = readdirSync(fullPath).filter((f) => MIGRATION_FILENAME_PATTERN.test(f)).sort();
229
+ return entries.map((name) => {
230
+ const filePath = join(fullPath, name);
231
+ const content = readFileSync(filePath, "utf-8");
232
+ return {
233
+ name,
234
+ path: filePath,
235
+ content,
236
+ checksum: computeChecksum(content)
237
+ };
238
+ });
239
+ }
240
+ function writeMigrationFile(dir, filename, content) {
241
+ const fullDir = ensureMigrationsDir(dir);
242
+ const filePath = join(fullDir, filename);
243
+ writeFileSync3(filePath, content, "utf-8");
244
+ return {
245
+ name: filename,
246
+ path: filePath,
247
+ content,
248
+ checksum: computeChecksum(content)
249
+ };
250
+ }
251
+
252
+ // src/commands/migrate.ts
253
+ var MigrationError = class extends Error {
254
+ constructor(message) {
255
+ super(message);
256
+ this.name = "MigrationError";
257
+ }
258
+ };
259
+ function sanitizePgError(err) {
260
+ if (err instanceof Error) {
261
+ const code = err.code;
262
+ if (typeof code === "string" && /^[0-9A-Z]{5}$/.test(code)) {
263
+ if (process.env.DEBUG?.includes("dbsp")) {
264
+ console.error(`[DEBUG] pg error detail: ${err.message}`);
265
+ }
266
+ return new MigrationError(`Migration failed: database error ${code}`);
267
+ }
268
+ return err;
269
+ }
270
+ return new Error(String(err));
271
+ }
272
+ async function withMigratePool(dbUrl, fn) {
273
+ const { pool } = await createDbConnection(dbUrl);
274
+ try {
275
+ return await fn(pool);
276
+ } finally {
277
+ let endError;
278
+ try {
279
+ await pool.end();
280
+ } catch (e) {
281
+ endError = e;
282
+ }
283
+ if (endError !== void 0) {
284
+ console.error(
285
+ `Warning: pool.end() failed: ${endError instanceof Error ? endError.message : String(endError)}`
286
+ );
287
+ }
288
+ }
289
+ }
290
+ async function runMigrateAction(fn) {
291
+ try {
292
+ await fn();
293
+ } catch (error) {
294
+ if (error instanceof Error) {
295
+ console.error(`\u274C Error: ${error.message}`);
296
+ } else {
297
+ console.error("\u274C Unknown error occurred");
298
+ }
299
+ process.exit(1);
300
+ }
301
+ }
302
+ var devCommand = new Command3("dev").description("Generate a migration from schema changes").option(
303
+ "-s, --schema <path>",
304
+ "Path to schema file (default: dbsp.schema.ts)"
305
+ ).requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema-name <name>", "Database schema name (default: public)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("-n, --name <description>", "Migration description", "migration").option("--allow-destructive", "Include destructive changes (drops)").action(
306
+ (options) => runMigrateAction(async () => {
307
+ const schemaPath = options.schema ?? "dbsp.schema.ts";
308
+ console.log(`\u{1F4DD} Generating migration: ${schemaPath}`);
309
+ console.log(` Database: ${redactDbUrl(options.db)}`);
310
+ console.log("");
311
+ const loaded = await loadSchema(schemaPath);
312
+ const schemaModel = loaded.model;
313
+ await withMigratePool(options.db, async (pool) => {
314
+ const dbModel = await introspect(pool, {
315
+ ...options.schemaName ? { schema: options.schemaName } : {}
316
+ });
317
+ const diff = compareSchemata(schemaModel, dbModel);
318
+ if (diff.changes.length === 0) {
319
+ console.log("\u2705 No changes detected \u2014 database matches schema.");
320
+ return;
321
+ }
322
+ if (diff.hasDestructive && !options.allowDestructive) {
323
+ const destructive = diff.changes.filter((c) => c.destructive);
324
+ throw new MigrationError(
325
+ `${destructive.length} destructive change(s) detected:
326
+ ` + destructive.map((c) => ` - ${c.details}`).join("\n") + "\n\nUse --allow-destructive to include these changes."
327
+ );
328
+ }
329
+ const sqlOptions = {
330
+ includeDestructive: options.allowDestructive ?? false,
331
+ ...options.schemaName ? { schemaName: options.schemaName } : {}
332
+ };
333
+ const statements = generateMigrationSQL(diff, sqlOptions);
334
+ if (statements.length === 0) {
335
+ console.log(
336
+ "\u2705 No migration needed \u2014 all changes are non-actionable."
337
+ );
338
+ return;
339
+ }
340
+ const existingFiles = scanMigrationFiles(options.dir).map(
341
+ (f) => f.name
342
+ );
343
+ const filename = generateMigrationFilename(
344
+ existingFiles,
345
+ options.name
346
+ );
347
+ const content = generateMigrationFile(diff, {
348
+ ...sqlOptions,
349
+ name: filename
350
+ });
351
+ const file = writeMigrationFile(options.dir, filename, content);
352
+ console.log(`\u2705 Migration created: ${file.path}`);
353
+ console.log(` Statements: ${statements.length}`);
354
+ console.log(` Checksum: ${file.checksum.slice(0, 12)}...`);
355
+ });
356
+ })
357
+ );
358
+ var applyCommand = new Command3("apply").description("Apply pending migrations").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("--dry-run", "Show pending migrations without applying").action(
359
+ (options) => runMigrateAction(async () => {
360
+ console.log("\u{1F504} Applying migrations");
361
+ console.log(` Database: ${redactDbUrl(options.db)}`);
362
+ console.log(` Directory: ${options.dir}`);
363
+ console.log("");
364
+ await withMigratePool(options.db, async (pool) => {
365
+ await ensureMigrationsTable(pool);
366
+ await withMigrationLock(pool, async (client) => {
367
+ const applied = await getAppliedMigrations(client);
368
+ const appliedMap = new Map(applied.map((m) => [m.name, m.checksum]));
369
+ const files = scanMigrationFiles(options.dir);
370
+ if (files.length === 0) {
371
+ console.log(`No migration files found in ${options.dir}`);
372
+ return;
373
+ }
374
+ for (const file of files) {
375
+ const existingChecksum = appliedMap.get(file.name);
376
+ if (existingChecksum !== void 0 && existingChecksum !== file.checksum) {
377
+ throw new MigrationError(
378
+ `Checksum mismatch for ${file.name}
379
+ Expected: ${existingChecksum}
380
+ Got: ${file.checksum}
381
+
382
+ Migration file has been tampered with after being applied.`
383
+ );
384
+ }
385
+ }
386
+ const pending = files.filter((f) => !appliedMap.has(f.name));
387
+ if (pending.length === 0) {
388
+ console.log("\u2705 All migrations already applied.");
389
+ return;
390
+ }
391
+ if (options.dryRun) {
392
+ console.log(`${pending.length} pending migration(s):`);
393
+ for (const file of pending) {
394
+ console.log(` - ${file.name}`);
395
+ }
396
+ return;
397
+ }
398
+ let appliedCount = 0;
399
+ for (const file of pending) {
400
+ console.log(` Applying: ${file.name}...`);
401
+ const parsed = parseMigrationFile(file.content);
402
+ const statements = parsed.upStatements.filter(
403
+ (s) => s.length > 0 && !s.startsWith("-- ")
404
+ );
405
+ const version = await getNextSchemaVersion(
406
+ client
407
+ );
408
+ const destructive = isDestructiveDown(parsed.downStatements);
409
+ try {
410
+ await client.query("BEGIN");
411
+ for (const stmt of statements) {
412
+ await client.query(stmt);
413
+ }
414
+ await recordMigration(
415
+ client,
416
+ file.name,
417
+ file.checksum,
418
+ version,
419
+ destructive
420
+ );
421
+ await client.query("COMMIT");
422
+ } catch (applyError) {
423
+ let rollbackError;
424
+ try {
425
+ await client.query("ROLLBACK");
426
+ } catch (e) {
427
+ rollbackError = e;
428
+ }
429
+ const primary = sanitizePgError(applyError);
430
+ if (rollbackError !== void 0) {
431
+ console.error(
432
+ ` Note: rollback also failed: ${sanitizePgError(rollbackError).message}`
433
+ );
434
+ }
435
+ throw primary;
436
+ }
437
+ appliedCount++;
438
+ console.log(` \u2705 Applied: ${file.name}`);
439
+ }
440
+ console.log(
441
+ `
442
+ \u2705 ${appliedCount} migration(s) applied successfully.`
443
+ );
444
+ });
445
+ });
446
+ })
447
+ );
448
+ var rollbackCommand = new Command3("rollback").description("Roll back applied migrations").argument("<count>", "Number of migrations to roll back").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("--force", "Force rollback of destructive or empty DOWN migrations").action(
449
+ (countArg, options) => {
450
+ const count = Number.parseInt(countArg, 10);
451
+ if (Number.isNaN(count) || count < 1) {
452
+ console.error("\u274C Count must be a positive integer");
453
+ process.exit(1);
454
+ }
455
+ return runMigrateAction(async () => {
456
+ console.log(`\u23EA Rolling back ${count} migration(s)`);
457
+ console.log(` Database: ${redactDbUrl(options.db)}`);
458
+ console.log(` Directory: ${options.dir}`);
459
+ console.log("");
460
+ await withMigratePool(options.db, async (pool) => {
461
+ await ensureMigrationsTable(pool);
462
+ await withMigrationLock(pool, async (client) => {
463
+ const applied = await getAppliedMigrations(
464
+ client
465
+ );
466
+ const sortedDesc = [...applied].sort(
467
+ (a, b) => b.name.localeCompare(a.name)
468
+ );
469
+ if (count > sortedDesc.length) {
470
+ throw new MigrationError(
471
+ `Cannot roll back ${count} migration(s) \u2014 only ${sortedDesc.length} applied`
472
+ );
473
+ }
474
+ const toRollback = sortedDesc.slice(0, count);
475
+ const files = scanMigrationFiles(options.dir);
476
+ const fileMap = new Map(files.map((f) => [f.name, f]));
477
+ let rolledBack = 0;
478
+ for (const record of toRollback) {
479
+ const file = fileMap.get(record.name);
480
+ if (!file) {
481
+ throw new MigrationError(
482
+ `Migration file not found on disk: ${record.name}`
483
+ );
484
+ }
485
+ if (file.checksum !== record.checksum) {
486
+ throw new MigrationError(
487
+ `Checksum mismatch for ${record.name}
488
+ Expected: ${record.checksum}
489
+ Got: ${file.checksum}
490
+
491
+ Migration file has been modified since it was applied.`
492
+ );
493
+ }
494
+ const parsed = parseMigrationFile(file.content);
495
+ if (!parsed.hasDown) {
496
+ throw new MigrationError(
497
+ `Migration ${record.name} has no DOWN section
498
+ Cannot roll back a migration without a DOWN section.`
499
+ );
500
+ }
501
+ const downStmts = parsed.downStatements.filter(
502
+ (s) => s.length > 0 && !s.startsWith("-- ")
503
+ );
504
+ if (downStmts.length === 0 && !options.force) {
505
+ throw new MigrationError(
506
+ `Migration ${record.name} has an empty DOWN section
507
+ Use --force to roll back anyway.`
508
+ );
509
+ }
510
+ if (isDestructiveDown(parsed.downStatements) && !options.force) {
511
+ throw new MigrationError(
512
+ `Migration ${record.name} has destructive DOWN operations
513
+ Use --force to proceed with destructive rollback.`
514
+ );
515
+ }
516
+ if (downStmts.length > 0 || options.force) {
517
+ console.log(` Rolling back: ${record.name}...`);
518
+ }
519
+ try {
520
+ await client.query("BEGIN");
521
+ for (const stmt of downStmts) {
522
+ await client.query(stmt);
523
+ }
524
+ await removeMigrationRecord(
525
+ client,
526
+ record.name
527
+ );
528
+ await client.query("COMMIT");
529
+ } catch (rollbackError) {
530
+ let rollbackCleanupError;
531
+ try {
532
+ await client.query("ROLLBACK");
533
+ } catch (e) {
534
+ rollbackCleanupError = e;
535
+ }
536
+ const primary = sanitizePgError(rollbackError);
537
+ if (rollbackCleanupError !== void 0) {
538
+ console.error(
539
+ ` Note: rollback also failed: ${sanitizePgError(rollbackCleanupError).message}`
540
+ );
541
+ }
542
+ throw primary;
543
+ }
544
+ rolledBack++;
545
+ console.log(` \u2705 Rolled back: ${record.name}`);
546
+ }
547
+ console.log(
548
+ `
549
+ \u2705 ${rolledBack} migration(s) rolled back successfully.`
550
+ );
551
+ });
552
+ });
553
+ });
554
+ }
555
+ );
556
+ var statusCommand = new Command3("status").description("Show migration status").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--dir <path>", "Migrations directory", DEFAULT_MIGRATIONS_DIR).option("--json", "Output as JSON").action(
557
+ (options) => runMigrateAction(async () => {
558
+ await withMigratePool(options.db, async (pool) => {
559
+ await ensureMigrationsTable(pool);
560
+ const applied = await getAppliedMigrations(pool);
561
+ const appliedMap = new Map(applied.map((m) => [m.name, m]));
562
+ const files = scanMigrationFiles(options.dir);
563
+ const statuses = files.map((file) => {
564
+ const record = appliedMap.get(file.name);
565
+ if (record === void 0) {
566
+ return {
567
+ name: file.name,
568
+ status: "pending"
569
+ };
570
+ }
571
+ if (record.checksum !== file.checksum) {
572
+ return {
573
+ name: file.name,
574
+ status: "checksum_mismatch",
575
+ appliedAt: record.appliedAt
576
+ };
577
+ }
578
+ return {
579
+ name: file.name,
580
+ status: "applied",
581
+ appliedAt: record.appliedAt
582
+ };
583
+ });
584
+ for (const record of applied) {
585
+ if (!files.some((f) => f.name === record.name)) {
586
+ statuses.push({
587
+ name: record.name,
588
+ status: "missing_file",
589
+ appliedAt: record.appliedAt
590
+ });
591
+ }
592
+ }
593
+ if (options.json) {
594
+ console.log(JSON.stringify(statuses, null, 2));
595
+ } else {
596
+ console.log("Migration Status");
597
+ console.log(` Database: ${redactDbUrl(options.db)}`);
598
+ console.log(` Directory: ${options.dir}`);
599
+ console.log("");
600
+ if (statuses.length === 0) {
601
+ console.log("No migrations found.");
602
+ } else {
603
+ for (const s of statuses) {
604
+ const icon = s.status === "applied" ? "\u2705" : s.status === "pending" ? "\u23F3" : s.status === "checksum_mismatch" ? "\u26A0\uFE0F" : "\u2753";
605
+ const detail = "appliedAt" in s && s.appliedAt ? ` (applied: ${s.appliedAt.toISOString()})` : "";
606
+ console.log(` ${icon} ${s.name} \u2014 ${s.status}${detail}`);
607
+ }
608
+ const pendingCount = statuses.filter(
609
+ (s) => s.status === "pending"
610
+ ).length;
611
+ const appliedCount = statuses.filter(
612
+ (s) => s.status === "applied"
613
+ ).length;
614
+ console.log("");
615
+ console.log(
616
+ `Total: ${statuses.length} | Applied: ${appliedCount} | Pending: ${pendingCount}`
617
+ );
618
+ }
619
+ }
620
+ });
621
+ })
622
+ );
623
+ var migrateCommand = new Command3("migrate").description("Database migration management").addCommand(devCommand).addCommand(applyCommand).addCommand(rollbackCommand).addCommand(statusCommand);
624
+
625
+ // src/commands/push.ts
626
+ import {
627
+ compareSchemata as compareSchemata2,
628
+ generateDDL,
629
+ generateMigrationSQL as generateMigrationSQL2,
630
+ introspect as introspect2
631
+ } from "@dbsp/adapter-pgsql";
632
+ import { Command as Command4 } from "commander";
633
+
634
+ // src/ddl-executor.ts
635
+ async function executeDdl(pool, statements, options) {
636
+ if (statements.length === 0) {
637
+ return { statementsExecuted: 0, dryRun: options?.dryRun ?? false };
638
+ }
639
+ if (options?.dryRun) {
640
+ return { statementsExecuted: statements.length, dryRun: true };
641
+ }
642
+ let client;
643
+ try {
644
+ client = await pool.connect();
645
+ await client.query("BEGIN");
646
+ for (const stmt of statements) {
647
+ await client.query(stmt);
648
+ }
649
+ await client.query("COMMIT");
650
+ return { statementsExecuted: statements.length, dryRun: false };
651
+ } catch (error) {
652
+ if (client) {
653
+ await client.query("ROLLBACK");
654
+ }
655
+ throw error;
656
+ } finally {
657
+ if (client) {
658
+ client.release();
659
+ }
660
+ }
661
+ }
662
+
663
+ // src/commands/push.ts
664
+ var MIGRATIONS_TABLE = "_dbsp_migrations";
665
+ var pushCommand = new Command4("push").description("Push schema to database (additive by default)").option(
666
+ "-s, --schema <path>",
667
+ "Path to schema file (default: dbsp.schema.ts)"
668
+ ).requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema-name <name>", "Database schema name (default: public)").option("--drop", "Drop and recreate all objects (preserves migrations)").option("--dry-run", "Print SQL without executing").option("--json", "Output as JSON").action(
669
+ async (options) => {
670
+ const schemaPath = options.schema ?? "dbsp.schema.ts";
671
+ const redactedUrl = redactDbUrl(options.db);
672
+ if (!options.json) {
673
+ console.log(
674
+ `\u{1F680} Pushing schema: ${schemaPath}${options.drop ? " (with --drop)" : ""}`
675
+ );
676
+ console.log(` Database: ${redactedUrl}`);
677
+ if (options.schemaName) {
678
+ console.log(` Schema: ${options.schemaName}`);
679
+ }
680
+ if (options.dryRun) {
681
+ console.log(` Mode: DRY RUN (no changes will be applied)`);
682
+ }
683
+ console.log("");
684
+ }
685
+ try {
686
+ const loaded = await loadSchema(schemaPath);
687
+ const schemaModel = loaded.model;
688
+ const { pool } = await createDbConnection(options.db);
689
+ try {
690
+ if (options.drop) {
691
+ const statements = generateDDL(schemaModel, {
692
+ includeDropStatements: true,
693
+ ...options.schemaName ? { schemaName: options.schemaName } : {}
694
+ });
695
+ const escapedTable = MIGRATIONS_TABLE.replace(
696
+ /[.*+?^${}()|[\]\\]/g,
697
+ "\\$&"
698
+ );
699
+ const migrationsPattern = new RegExp(
700
+ `DROP\\s+TABLE(?:\\s+IF\\s+EXISTS)?(?:\\s+"[^"]*"\\s*\\.)?\\s*"${escapedTable}"`,
701
+ "i"
702
+ );
703
+ const filtered = statements.filter(
704
+ (stmt) => !migrationsPattern.test(stmt)
705
+ );
706
+ outputResult(filtered, options);
707
+ const result = await executeDdl(pool, filtered, {
708
+ ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {}
709
+ });
710
+ if (options.json) {
711
+ const droppedTables = statements.filter((s) => /DROP\s+TABLE/i.test(s)).filter((s) => !migrationsPattern.test(s)).map((s) => {
712
+ const m = s.match(/"([^"]+)"\s*(?:CASCADE\s*)?;?\s*$/i);
713
+ return m ? m[1] : s;
714
+ }).filter((t) => t !== void 0);
715
+ console.log(
716
+ JSON.stringify(
717
+ {
718
+ status: options.dryRun ? "dry-run" : "dropped",
719
+ tables: droppedTables,
720
+ tablesDropped: droppedTables.length,
721
+ statementsExecuted: result.statementsExecuted
722
+ },
723
+ null,
724
+ 2
725
+ )
726
+ );
727
+ } else if (!options.dryRun) {
728
+ console.log(
729
+ `
730
+ \u2705 Push complete: ${result.statementsExecuted} statements executed`
731
+ );
732
+ }
733
+ } else {
734
+ const dbModel = await introspect2(pool, {
735
+ ...options.schemaName ? { schema: options.schemaName } : {}
736
+ });
737
+ const diff = compareSchemata2(schemaModel, dbModel);
738
+ const statements = generateMigrationSQL2(diff, {
739
+ includeDestructive: false,
740
+ ...options.schemaName ? { schemaName: options.schemaName } : {}
741
+ });
742
+ const skippedChanges = diff.changes.filter((c) => c.destructive);
743
+ if (!options.json && skippedChanges.length > 0) {
744
+ console.log(
745
+ `\u26A0\uFE0F ${skippedChanges.length} non-additive change(s) skipped:`
746
+ );
747
+ for (const change of skippedChanges) {
748
+ console.log(` - ${change.details}`);
749
+ }
750
+ console.log("");
751
+ }
752
+ if (statements.length === 0) {
753
+ if (options.json) {
754
+ console.log(
755
+ JSON.stringify(
756
+ {
757
+ status: "up-to-date",
758
+ statementsExecuted: 0,
759
+ skippedChanges: skippedChanges.length
760
+ },
761
+ null,
762
+ 2
763
+ )
764
+ );
765
+ } else {
766
+ console.log("\u2705 Database is up to date \u2014 nothing to push.");
767
+ }
768
+ return;
769
+ }
770
+ outputResult(statements, options);
771
+ const result = await executeDdl(pool, statements, {
772
+ ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {}
773
+ });
774
+ if (options.json) {
775
+ console.log(
776
+ JSON.stringify(
777
+ {
778
+ status: options.dryRun ? "dry-run" : "applied",
779
+ statementsExecuted: result.statementsExecuted,
780
+ skippedChanges: skippedChanges.length
781
+ },
782
+ null,
783
+ 2
784
+ )
785
+ );
786
+ } else if (!options.dryRun) {
787
+ console.log(
788
+ `
789
+ \u2705 Push complete: ${result.statementsExecuted} statement(s) executed`
790
+ );
791
+ }
792
+ }
793
+ } finally {
794
+ await pool.end();
795
+ }
796
+ } catch (error) {
797
+ const message = error instanceof Error ? error.message : "Unknown error occurred";
798
+ if (options.json) {
799
+ console.log(
800
+ JSON.stringify({ status: "error", error: message }, null, 2)
801
+ );
802
+ } else {
803
+ console.error(`\u274C Error: ${message}`);
804
+ }
805
+ process.exit(1);
806
+ }
807
+ }
808
+ );
809
+ function outputResult(statements, options) {
810
+ if (options.dryRun && !options.json) {
811
+ console.log(`-- Dry run: ${statements.length} statement(s)
812
+ `);
813
+ for (const stmt of statements) {
814
+ console.log(`${stmt};
815
+ `);
816
+ }
817
+ }
818
+ }
819
+
820
+ // src/commands/repl.ts
821
+ import { readFileSync as readFileSync2 } from "fs";
822
+ import { Command as Command5 } from "commander";
823
+ var replCommand = new Command5("repl").description("Launch interactive REPL for exploring schema and queries").option("-s, --schema <path>", "Path to schema file (default: auto-detect)").option(
824
+ "-d, --db <url>",
825
+ "PostgreSQL connection URL for execution mode (e.g., postgres://localhost/mydb)"
826
+ ).option("-e, --eval <query>", "Execute a single query and exit (batch mode)").option(
827
+ "-i, --input <file>",
828
+ "Execute queries from file, one per line (batch mode)"
829
+ ).option(
830
+ "-f, --format <format>",
831
+ "Output format for batch mode: text (default) or json",
832
+ "text"
833
+ ).option(
834
+ "-a, --assert <file>",
835
+ "Assertion file to validate query output (requires --input)"
836
+ ).option(
837
+ "--import <files...>",
838
+ "SQL files to import before queries (equivalent to .import commands)"
839
+ ).option(
840
+ "--use <schema>",
841
+ "PostgreSQL schema to use (equivalent to .use command)"
842
+ ).option("--parse", "Start REPL with parse mode enabled (.parse toggle)").option("--exec", "Start REPL with exec mode enabled (.exec toggle)").option(
843
+ "--casing <type>",
844
+ "Column naming convention: snake (DB uses snake_case), camel (DB uses camelCase), none (preserve as-is)"
845
+ ).option(
846
+ "-c, --config <path>",
847
+ "Custom config file path (default: ~/.dbsp/config.json)"
848
+ ).action(async (options) => {
849
+ if (options.config) {
850
+ config.setConfigPath(options.config);
851
+ }
852
+ config.load();
853
+ try {
854
+ let schemaPath;
855
+ let schema;
856
+ if (options.schema) {
857
+ schema = await loadSchema(options.schema);
858
+ schemaPath = options.schema;
859
+ } else {
860
+ const result = await loadSchemaFromCwd();
861
+ schema = result.schema;
862
+ schemaPath = result.path;
863
+ }
864
+ if (options.assert && !options.input) {
865
+ throw new Error(
866
+ "--assert requires --input (assertion files validate query output from input files)"
867
+ );
868
+ }
869
+ if (options.import && !options.eval && !options.input) {
870
+ throw new Error("--import requires batch mode (--eval or --input)");
871
+ }
872
+ const dbCasing = options.casing === "snake" ? "snake_case" : options.casing === "camel" ? "camelCase" : options.casing === "none" ? "preserve" : void 0;
873
+ if (options.eval || options.input) {
874
+ const { runBatchMode } = await import("./repl/batch.js");
875
+ const queries = [];
876
+ if (options.use) {
877
+ queries.push(`.use ${options.use}`);
878
+ }
879
+ if (options.import) {
880
+ for (const file of options.import) {
881
+ queries.push(`.import ${file}`);
882
+ }
883
+ }
884
+ if (options.eval) {
885
+ queries.push(options.eval);
886
+ }
887
+ if (options.input) {
888
+ let content;
889
+ try {
890
+ content = readFileSync2(options.input, "utf-8");
891
+ } catch (err) {
892
+ const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
893
+ throw new Error(
894
+ isNotFound ? `Input file not found: ${options.input}` : `Failed to read input file: ${err instanceof Error ? err.message : String(err)}`
895
+ );
896
+ }
897
+ queries.push(...content.split("\n"));
898
+ }
899
+ await runBatchMode({
900
+ queries,
901
+ schema,
902
+ schemaPath,
903
+ format: options.format ?? "text",
904
+ ...options.db && { databaseUrl: options.db },
905
+ ...options.assert && { assertFile: options.assert },
906
+ ...dbCasing && { dbCasing }
907
+ });
908
+ return;
909
+ }
910
+ const { startRepl } = await import("./repl-4OFERLKZ.js");
911
+ await startRepl({
912
+ schema,
913
+ schemaPath,
914
+ ...options.db && { databaseUrl: options.db },
915
+ ...options.use && { initialSchemaName: options.use },
916
+ ...options.parse && { initialParseMode: true },
917
+ ...options.exec && { initialExecMode: true },
918
+ ...dbCasing && { dbCasing }
919
+ });
920
+ } catch (error) {
921
+ const message = error instanceof Error ? error.message : String(error);
922
+ console.error(`\u274C ${message}`);
923
+ process.exit(1);
924
+ }
925
+ });
926
+
927
+ // src/commands/verify.ts
928
+ import { compareSchemata as compareSchemata3, introspect as introspect3 } from "@dbsp/adapter-pgsql";
929
+ import { Command as Command6 } from "commander";
930
+
931
+ // src/verifier.ts
932
+ var CHANGE_TO_DRIFT = {
933
+ create_table: { type: "missing_table_in_db", severity: "error" },
934
+ drop_table: { type: "missing_table_in_schema", severity: "warning" },
935
+ add_column: { type: "missing_column_in_db", severity: "error" },
936
+ drop_column: { type: "missing_column_in_schema", severity: "info" },
937
+ alter_column_type: { type: "type_mismatch", severity: "error" },
938
+ alter_column_nullable: { type: "nullable_mismatch", severity: "warning" },
939
+ alter_column_default: { type: "default_mismatch", severity: "warning" },
940
+ add_primary_key: { type: "primary_key_mismatch", severity: "error" },
941
+ drop_primary_key: { type: "primary_key_mismatch", severity: "error" },
942
+ add_foreign_key: { type: "missing_fk_in_db", severity: "error" },
943
+ drop_foreign_key: { type: "missing_fk_in_schema", severity: "warning" },
944
+ alter_foreign_key: { type: "fk_on_delete_mismatch", severity: "warning" },
945
+ create_index: { type: "missing_index_in_db", severity: "warning" },
946
+ drop_index: { type: "missing_index_in_schema", severity: "info" },
947
+ // CHECK constraints
948
+ add_check_constraint: { type: "missing_check_in_db", severity: "warning" },
949
+ drop_check_constraint: { type: "missing_check_in_schema", severity: "info" },
950
+ // ENUM types
951
+ create_enum: { type: "missing_enum_in_db", severity: "error" },
952
+ alter_enum_add_value: { type: "enum_value_mismatch", severity: "warning" },
953
+ drop_enum: { type: "missing_enum_in_schema", severity: "warning" },
954
+ // Column enhancements
955
+ alter_column_collation: { type: "collation_mismatch", severity: "warning" },
956
+ alter_column_identity: { type: "identity_mismatch", severity: "warning" },
957
+ // Comments
958
+ add_comment: { type: "comment_mismatch", severity: "info" },
959
+ drop_comment: { type: "comment_mismatch", severity: "info" },
960
+ // Extensions & Sequences
961
+ create_extension: { type: "missing_extension", severity: "error" },
962
+ drop_extension: { type: "missing_extension", severity: "info" },
963
+ create_sequence: { type: "missing_sequence", severity: "warning" },
964
+ alter_sequence: { type: "sequence_mismatch", severity: "warning" },
965
+ drop_sequence: { type: "missing_sequence", severity: "info" },
966
+ // Constraints
967
+ validate_constraint: {
968
+ type: "constraint_validation_mismatch",
969
+ severity: "warning"
970
+ },
971
+ // Row-Level Security
972
+ enable_rls: { type: "rls_enabled_mismatch", severity: "warning" },
973
+ disable_rls: { type: "rls_enabled_mismatch", severity: "warning" },
974
+ create_policy: { type: "missing_policy_in_db", severity: "warning" },
975
+ drop_policy: { type: "missing_policy_in_schema", severity: "warning" }
976
+ };
977
+ function changeToDriftIssue(change) {
978
+ const mapping = CHANGE_TO_DRIFT[change.kind] ?? {
979
+ type: "type_mismatch",
980
+ severity: "warning"
981
+ };
982
+ return {
983
+ severity: mapping.severity,
984
+ type: mapping.type,
985
+ table: change.table,
986
+ ...change.column !== void 0 ? { column: change.column } : {},
987
+ message: change.details
988
+ };
989
+ }
990
+ function verifyFromDiff(diff, schemaTables, dbTables) {
991
+ const issues = diff.changes.map(changeToDriftIssue);
992
+ const severityOrder = {
993
+ error: 0,
994
+ warning: 1,
995
+ info: 2
996
+ };
997
+ issues.sort(
998
+ (a, b) => severityOrder[a.severity] - severityOrder[b.severity]
999
+ );
1000
+ const hasErrors = issues.some((i) => i.severity === "error");
1001
+ return {
1002
+ valid: !hasErrors,
1003
+ issues,
1004
+ schemaTables,
1005
+ dbTables,
1006
+ diff
1007
+ };
1008
+ }
1009
+ function formatVerifyResult(result) {
1010
+ const lines = [];
1011
+ if (result.valid) {
1012
+ lines.push("\u2705 Schema matches database");
1013
+ } else {
1014
+ lines.push("\u274C Schema drift detected");
1015
+ }
1016
+ lines.push("");
1017
+ lines.push(`Tables in schema: ${result.schemaTables.length}`);
1018
+ lines.push(`Tables in database: ${result.dbTables.length}`);
1019
+ lines.push("");
1020
+ if (result.issues.length === 0) {
1021
+ lines.push("No issues found.");
1022
+ } else {
1023
+ const errors = result.issues.filter((i) => i.severity === "error");
1024
+ const warnings = result.issues.filter((i) => i.severity === "warning");
1025
+ const infos = result.issues.filter((i) => i.severity === "info");
1026
+ if (errors.length > 0) {
1027
+ lines.push(`\u274C ${errors.length} error(s):`);
1028
+ for (const issue of errors) {
1029
+ lines.push(` ${issue.message}`);
1030
+ }
1031
+ lines.push("");
1032
+ }
1033
+ if (warnings.length > 0) {
1034
+ lines.push(`\u26A0\uFE0F ${warnings.length} warning(s):`);
1035
+ for (const issue of warnings) {
1036
+ lines.push(` ${issue.message}`);
1037
+ }
1038
+ lines.push("");
1039
+ }
1040
+ if (infos.length > 0) {
1041
+ lines.push(`\u2139\uFE0F ${infos.length} info:`);
1042
+ for (const issue of infos) {
1043
+ lines.push(` ${issue.message}`);
1044
+ }
1045
+ lines.push("");
1046
+ }
1047
+ }
1048
+ return lines.join("\n");
1049
+ }
1050
+
1051
+ // src/commands/verify.ts
1052
+ var verifyCommand = new Command6("verify").description("Compare schema vs real database (drift detection)").option(
1053
+ "-s, --schema <path>",
1054
+ "Path to schema file (default: dbsp.schema.ts)"
1055
+ ).requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema-name <name>", "Database schema name (default: public)").option("--json", "Output as JSON").action(
1056
+ async (options) => {
1057
+ const schemaPath = options.schema ?? "dbsp.schema.ts";
1058
+ const redactedUrl = redactDbUrl(options.db);
1059
+ if (!options.json) {
1060
+ console.log(`\u{1F50D} Verifying schema: ${schemaPath}`);
1061
+ console.log(` Database: ${redactedUrl}`);
1062
+ if (options.schemaName) {
1063
+ console.log(` Schema: ${options.schemaName}`);
1064
+ }
1065
+ console.log("");
1066
+ }
1067
+ try {
1068
+ const loaded = await loadSchema(schemaPath);
1069
+ const schemaModel = loaded.model;
1070
+ const { pool } = await createDbConnection(options.db);
1071
+ try {
1072
+ const dbModel = await introspect3(pool, {
1073
+ ...options.schemaName ? { schema: options.schemaName } : {}
1074
+ });
1075
+ const diff = compareSchemata3(schemaModel, dbModel);
1076
+ const schemaTables = Array.from(schemaModel.tables.keys());
1077
+ const dbTables = Array.from(dbModel.tables.keys());
1078
+ const result = verifyFromDiff(diff, schemaTables, dbTables);
1079
+ if (options.json) {
1080
+ const { diff: _diff, ...jsonResult } = result;
1081
+ console.log(
1082
+ JSON.stringify(
1083
+ {
1084
+ ...jsonResult,
1085
+ summary: diff.summary,
1086
+ hasDestructive: diff.hasDestructive
1087
+ },
1088
+ null,
1089
+ 2
1090
+ )
1091
+ );
1092
+ } else {
1093
+ console.log(formatVerifyResult(result));
1094
+ }
1095
+ process.exitCode = result.valid ? 0 : 1;
1096
+ return;
1097
+ } finally {
1098
+ await pool.end();
1099
+ }
1100
+ } catch (error) {
1101
+ const message = error instanceof Error ? error.message : "Unknown error occurred";
1102
+ if (options.json) {
1103
+ console.log(
1104
+ JSON.stringify({ status: "error", error: message }, null, 2)
1105
+ );
1106
+ } else {
1107
+ console.error(`\u274C Error: ${message}`);
1108
+ }
1109
+ process.exit(1);
1110
+ }
1111
+ }
1112
+ );
1113
+
1114
+ // src/index.ts
1115
+ var program = new Command7();
1116
+ program.name("dbsp").description("Schema-first code generation for db-semantic-planner").version("0.0.1");
1117
+ program.addCommand(generateCommand);
1118
+ program.addCommand(introspectCommand);
1119
+ program.addCommand(migrateCommand);
1120
+ program.addCommand(pushCommand);
1121
+ program.addCommand(replCommand);
1122
+ program.addCommand(verifyCommand);
1123
+ program.exitOverride();
1124
+ try {
1125
+ program.parse();
1126
+ } catch (err) {
1127
+ if (err instanceof CommanderError && err.exitCode === 0) {
1128
+ process.exit(0);
1129
+ }
1130
+ const message = err instanceof Error ? err.message : "Command parse error";
1131
+ if (process.argv.includes("--json")) {
1132
+ console.log(JSON.stringify({ status: "error", error: message }, null, 2));
1133
+ } else {
1134
+ console.error(`\u274C ${message}`);
1135
+ }
1136
+ process.exit(1);
1137
+ }
1138
+ //# sourceMappingURL=index.js.map