@dbsp/cli 2.1.3 → 2.2.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 CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-AKVQI5ZD.js";
15
15
 
16
16
  // src/index.ts
17
- import { Command as Command7, CommanderError } from "commander";
17
+ import { Command as Command8, CommanderError } from "commander";
18
18
 
19
19
  // src/commands/generate.ts
20
20
  import { mkdirSync, writeFileSync } from "fs";
@@ -218,6 +218,7 @@ import {
218
218
  comparePgsqlDatabaseSchema,
219
219
  createPgsqlAdapter,
220
220
  ensureMigrationsTable,
221
+ escapeDiagnosticText,
221
222
  generateMigrationFile,
222
223
  generateMigrationSQL,
223
224
  getAppliedMigrations,
@@ -574,7 +575,9 @@ var devCommand = new Command3("dev").description("Generate a migration from sche
574
575
  const diff = await comparePgsqlDatabaseSchema(adapter, schemaModel, {
575
576
  ...options.schemaName ? { schema: options.schemaName } : {},
576
577
  ...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {},
577
- onWarning: (message) => console.warn(`\u26A0\uFE0F ${message}`)
578
+ onExpressionCanonicalizationWarning: (warning) => console.warn(
579
+ `\u26A0\uFE0F [${warning.kind} ${escapeDiagnosticText(warning.table)}.${escapeDiagnosticText(warning.name)}] ${warning.message}`
580
+ )
578
581
  });
579
582
  if (diff.changes.length === 0) {
580
583
  console.log("\u2705 No changes detected \u2014 database matches schema.");
@@ -691,15 +694,530 @@ var statusCommand = new Command3("status").description("Show migration status").
691
694
  );
692
695
  var migrateCommand = new Command3("migrate").description("Database migration management").addCommand(devCommand).addCommand(applyCommand).addCommand(rollbackCommand).addCommand(statusCommand);
693
696
 
697
+ // src/commands/plan.ts
698
+ import {
699
+ acquirePgTransitionClient,
700
+ createPgExecutionContract,
701
+ createPgsqlAdapter as createPgsqlAdapter2,
702
+ createPgTransitionLessor,
703
+ createPgTransitionPack,
704
+ createPgTransitionRunPersister,
705
+ ensureTransitionJournal,
706
+ escapeDiagnosticText as escapeDiagnosticText2,
707
+ PgExecutionContractDerivationError,
708
+ pgTargetIdentityMismatch,
709
+ readPgExecutionTargetFromClient,
710
+ readPgObservationContextFromLessor
711
+ } from "@dbsp/adapter-pgsql";
712
+ import {
713
+ acquireTransitionLease,
714
+ bindExecutionContract,
715
+ createComparator,
716
+ createPackRegistry,
717
+ createProver,
718
+ createTransitionRunMetadata
719
+ } from "@dbsp/core";
720
+ import { Command as Command4 } from "commander";
721
+ var PlanPersistenceIndeterminateError = class extends Error {
722
+ runId;
723
+ result;
724
+ persistenceError;
725
+ cleanup;
726
+ constructor(result, persistenceError, cleanup = noCleanupFailure) {
727
+ const cause = cleanup.kind === "failed" ? new AggregateError(
728
+ [persistenceError, cleanup.error],
729
+ "persistence and connection release both failed"
730
+ ) : persistenceError;
731
+ super(
732
+ `persistence indeterminate; inspect run ${result.runId}: ${escapeDiagnosticText2(describeThrown(persistenceError))}`,
733
+ { cause }
734
+ );
735
+ this.name = "PlanPersistenceIndeterminateError";
736
+ this.runId = result.runId ?? "";
737
+ this.result = result;
738
+ this.persistenceError = persistenceError;
739
+ this.cleanup = cleanup;
740
+ }
741
+ };
742
+ var PlanCleanupError = class extends Error {
743
+ constructor(result, cleanupError) {
744
+ super(
745
+ `connection release failed after the plan outcome was determined: ${escapeDiagnosticText2(describeThrown(cleanupError))}`,
746
+ { cause: cleanupError }
747
+ );
748
+ this.result = result;
749
+ this.cleanupError = cleanupError;
750
+ this.name = "PlanCleanupError";
751
+ }
752
+ result;
753
+ cleanupError;
754
+ };
755
+ var PlanOperationAndCleanupError = class extends AggregateError {
756
+ constructor(operationError, cleanupError) {
757
+ super(
758
+ [operationError, cleanupError],
759
+ "planning and connection release both failed",
760
+ { cause: operationError }
761
+ );
762
+ this.operationError = operationError;
763
+ this.cleanupError = cleanupError;
764
+ this.name = "PlanOperationAndCleanupError";
765
+ }
766
+ operationError;
767
+ cleanupError;
768
+ };
769
+ var noCleanupFailure = { kind: "not-failed" };
770
+ function equivalenceContext(context) {
771
+ return {
772
+ engine: context.engine,
773
+ ...context.databaseId ? { databaseId: context.databaseId } : {},
774
+ ...context.targetSchema ? { targetSchema: context.targetSchema } : {},
775
+ ...context.searchPath ? { searchPath: context.searchPath } : {},
776
+ proofObservationContext: context
777
+ };
778
+ }
779
+ function executionTargetNamespaces(plan, fallbackSchema) {
780
+ const namespaces = [
781
+ ...new Set(
782
+ plan.steps.map((step) => {
783
+ const payload = step.operation.payload;
784
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload) || typeof payload.schema !== "string")
785
+ throw new PgExecutionContractDerivationError(
786
+ step.operation.ref,
787
+ step.operation.operationKind.name,
788
+ "has no derivable target namespace"
789
+ );
790
+ return payload.schema;
791
+ })
792
+ )
793
+ ];
794
+ if (namespaces.length === 0 && fallbackSchema !== void 0)
795
+ namespaces.push(fallbackSchema);
796
+ return namespaces;
797
+ }
798
+ async function buildPgExecutionContract(pool, schema, plan, expectedTargetIdentity) {
799
+ const lease = await acquireTransitionLease(createPgTransitionLessor(pool));
800
+ try {
801
+ const target = await readPgExecutionTargetFromClient(
802
+ lease.session,
803
+ executionTargetNamespaces(plan, schema)
804
+ );
805
+ const mismatch = pgTargetIdentityMismatch(
806
+ expectedTargetIdentity,
807
+ target.identity
808
+ );
809
+ if (mismatch) {
810
+ throw new Error(
811
+ `PostgreSQL target identity changed before plan persistence: ${mismatch}`
812
+ );
813
+ }
814
+ return createPgExecutionContract(
815
+ plan,
816
+ target.identity,
817
+ target.sessionProvenance
818
+ );
819
+ } finally {
820
+ await lease.release();
821
+ }
822
+ }
823
+ var defaultPlanDeps = {
824
+ async createDbConnection(db) {
825
+ const { pool } = await createDbConnection(db);
826
+ return { pool, release: () => pool.end() };
827
+ },
828
+ loadSchema,
829
+ async ensureTransitionJournal(pool) {
830
+ const lease = await acquireTransitionLease(createPgTransitionLessor(pool));
831
+ try {
832
+ await ensureTransitionJournal(lease.session);
833
+ } finally {
834
+ await lease.release();
835
+ }
836
+ },
837
+ async captureTargetIdentity(pool, schema) {
838
+ const lease = await acquireTransitionLease(createPgTransitionLessor(pool));
839
+ try {
840
+ return (await readPgExecutionTargetFromClient(lease.session, [
841
+ schema ?? "public"
842
+ ])).identity;
843
+ } finally {
844
+ await lease.release();
845
+ }
846
+ },
847
+ async loadCurrent(pool, schema, expectedTargetIdentity) {
848
+ const lease = await acquirePgTransitionClient(pool);
849
+ try {
850
+ const observed = await readPgExecutionTargetFromClient(
851
+ lease.client,
852
+ [schema ?? "public"]
853
+ );
854
+ const mismatch = pgTargetIdentityMismatch(
855
+ expectedTargetIdentity,
856
+ observed.identity
857
+ );
858
+ if (mismatch) {
859
+ throw new Error(
860
+ `PostgreSQL introspection target identity does not match the captured target: ${mismatch}`
861
+ );
862
+ }
863
+ return await createPgsqlAdapter2(lease.client, {
864
+ borrowedClient: true
865
+ }).introspect(schema === void 0 ? {} : { schema });
866
+ } finally {
867
+ lease.release();
868
+ }
869
+ },
870
+ async readContext(pool, schema, expectedTargetIdentity) {
871
+ return readPgObservationContextFromLessor(
872
+ createPgTransitionLessor(pool),
873
+ schema,
874
+ void 0,
875
+ expectedTargetIdentity
876
+ );
877
+ },
878
+ createPlanner(loaded) {
879
+ const registry = createPackRegistry([
880
+ createPgTransitionPack({
881
+ ...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {}
882
+ })
883
+ ]);
884
+ const comparator = createComparator(registry);
885
+ const prover = createProver(registry);
886
+ return {
887
+ compare: (desired, current, context) => comparator.compare(desired, current, equivalenceContext(context)),
888
+ prove: (compare, pool, context) => prover.prove(compare, createPgTransitionLessor(pool), context),
889
+ render: (plan, context) => renderProvenPlanSql(registry, plan, context)
890
+ };
891
+ },
892
+ async persist(pool, run, plan) {
893
+ const lease = await acquireTransitionLease(createPgTransitionLessor(pool));
894
+ try {
895
+ await createPgTransitionRunPersister(lease.session).persist(run, plan);
896
+ } finally {
897
+ await lease.release();
898
+ }
899
+ },
900
+ buildExecutionContract: buildPgExecutionContract
901
+ };
902
+ function resolvedDeps(overrides) {
903
+ return { ...defaultPlanDeps, ...overrides };
904
+ }
905
+ function describeThrown(error) {
906
+ return error instanceof Error ? error.message : String(error);
907
+ }
908
+ function executionContractBlockedAssessment(error) {
909
+ return {
910
+ decision: "blocked",
911
+ assurance: "unproven",
912
+ lifecycle: "planned",
913
+ continuation: "replan-required",
914
+ reasons: [
915
+ {
916
+ code: "unsupported-transition",
917
+ changes: [],
918
+ scope: [],
919
+ detail: error.message
920
+ }
921
+ ]
922
+ };
923
+ }
924
+ function renderProvenPlanSql(registry, plan, context) {
925
+ const statements = plan.steps.map((step) => {
926
+ const resolution = registry.resolveOperation(step.operation);
927
+ if (!resolution.ok || !resolution.semantics.renderPlanSql) {
928
+ throw new Error(
929
+ `no SQL renderer is registered for proven operation ${step.operation.operationKind.name}`
930
+ );
931
+ }
932
+ return resolution.semantics.renderPlanSql(step.operation, context);
933
+ });
934
+ return [
935
+ "-- dbsp transition plan view; apply accepts a persisted run id, never this SQL.",
936
+ ...statements.map((statement) => `${statement};`)
937
+ ].join("\n");
938
+ }
939
+ async function runPlan(options, overrides) {
940
+ const deps = resolvedDeps(overrides);
941
+ const format = options.format ?? "sql";
942
+ if (format !== "sql" && format !== "json") {
943
+ throw new Error(`unsupported plan format ${format}; expected sql or json`);
944
+ }
945
+ const loaded = await deps.loadSchema(options.schemaFile);
946
+ const connection = await deps.createDbConnection(options.db);
947
+ const { pool } = connection;
948
+ let result;
949
+ let operationError;
950
+ let failed = false;
951
+ try {
952
+ const targetIdentity = await deps.captureTargetIdentity(
953
+ pool,
954
+ options.schema
955
+ );
956
+ await deps.ensureTransitionJournal(pool);
957
+ const current = await deps.loadCurrent(
958
+ pool,
959
+ options.schema,
960
+ targetIdentity
961
+ );
962
+ const context = await deps.readContext(
963
+ pool,
964
+ options.schema,
965
+ targetIdentity
966
+ );
967
+ const planner = deps.createPlanner(loaded);
968
+ const compare = planner.compare(loaded.model, current, context);
969
+ const prove = await planner.prove(compare, pool, context);
970
+ if (prove.kind === "proven") {
971
+ let executionContract;
972
+ try {
973
+ executionContract = await deps.buildExecutionContract(
974
+ pool,
975
+ options.schema,
976
+ prove.plan,
977
+ targetIdentity
978
+ );
979
+ } catch (error) {
980
+ if (!(error instanceof PgExecutionContractDerivationError)) throw error;
981
+ result = {
982
+ compareKind: compare.kind,
983
+ proveKind: "blocked",
984
+ assessment: executionContractBlockedAssessment(error),
985
+ persisted: false,
986
+ runId: null,
987
+ planDigest: null
988
+ };
989
+ }
990
+ if (executionContract !== void 0) {
991
+ const durablePlan = bindExecutionContract(
992
+ prove.plan,
993
+ executionContract
994
+ );
995
+ const run = createTransitionRunMetadata(durablePlan);
996
+ const proofContext = prove.plan.observations.find(
997
+ (observation) => observation.role === "evidence"
998
+ )?.context;
999
+ if (!proofContext) {
1000
+ throw new Error(
1001
+ "internal error: minted proven plan has no evidence observation context"
1002
+ );
1003
+ }
1004
+ const sql = planner.render(durablePlan, proofContext);
1005
+ if (!options.dryRun) {
1006
+ try {
1007
+ await deps.persist(pool, run, durablePlan);
1008
+ } catch (error) {
1009
+ throw new PlanPersistenceIndeterminateError(
1010
+ {
1011
+ compareKind: compare.kind,
1012
+ proveKind: prove.kind,
1013
+ assessment: prove.assessment,
1014
+ persisted: "indeterminate",
1015
+ runId: run.runId,
1016
+ planDigest: run.planDigest
1017
+ },
1018
+ error
1019
+ );
1020
+ }
1021
+ }
1022
+ result = {
1023
+ compareKind: compare.kind,
1024
+ proveKind: prove.kind,
1025
+ assessment: prove.assessment,
1026
+ persisted: !options.dryRun,
1027
+ runId: options.dryRun ? null : run.runId,
1028
+ planDigest: run.planDigest,
1029
+ plan: durablePlan,
1030
+ sql
1031
+ };
1032
+ }
1033
+ }
1034
+ if (prove.kind === "no-drift") {
1035
+ result = {
1036
+ compareKind: compare.kind,
1037
+ proveKind: prove.kind,
1038
+ assessment: prove.assessment,
1039
+ persisted: false,
1040
+ runId: null,
1041
+ planDigest: null
1042
+ };
1043
+ } else if (prove.kind !== "proven") {
1044
+ result = {
1045
+ compareKind: compare.kind,
1046
+ proveKind: prove.kind,
1047
+ assessment: prove.assessment,
1048
+ persisted: false,
1049
+ runId: null,
1050
+ planDigest: null
1051
+ };
1052
+ }
1053
+ } catch (error) {
1054
+ failed = true;
1055
+ operationError = error;
1056
+ }
1057
+ let cleanup = noCleanupFailure;
1058
+ try {
1059
+ await connection.release();
1060
+ } catch (error) {
1061
+ cleanup = { kind: "failed", error };
1062
+ }
1063
+ if (failed) {
1064
+ if (operationError instanceof PlanPersistenceIndeterminateError) {
1065
+ throw new PlanPersistenceIndeterminateError(
1066
+ operationError.result,
1067
+ operationError.persistenceError,
1068
+ cleanup
1069
+ );
1070
+ }
1071
+ if (cleanup.kind === "failed") {
1072
+ throw new PlanOperationAndCleanupError(operationError, cleanup.error);
1073
+ }
1074
+ throw operationError;
1075
+ }
1076
+ if (cleanup.kind === "failed") {
1077
+ throw new PlanCleanupError(result, cleanup.error);
1078
+ }
1079
+ return result;
1080
+ }
1081
+ function formatPlanJson(result, dryRun) {
1082
+ return {
1083
+ compareKind: result.compareKind,
1084
+ proveKind: result.proveKind,
1085
+ assessment: result.assessment,
1086
+ persisted: result.persisted,
1087
+ runId: result.runId,
1088
+ planDigest: result.planDigest,
1089
+ ...result.plan ? { plan: result.plan } : {},
1090
+ ...result.sql ? { sql: result.sql } : {},
1091
+ ...dryRun && result.proveKind === "proven" ? { preview: "non-executable; no durable dbsp record" } : {}
1092
+ };
1093
+ }
1094
+ function formatPlanHuman(result, dryRun) {
1095
+ if (result.proveKind === "proven") {
1096
+ return [
1097
+ dryRun ? "Proven transition preview (non-executable; no durable dbsp record):" : "Proven transition plan:",
1098
+ // Every operation renderer converges on result.sql before this human-only
1099
+ // view, so database-sourced SQL cannot bypass terminal escaping here.
1100
+ ...result.sql === void 0 ? [] : [escapeDiagnosticText2(result.sql)],
1101
+ ...result.plan?.executionContract ? [
1102
+ "Execution contract:",
1103
+ ...result.plan.executionContract.requirements.map(
1104
+ (requirement) => ` ${escapeDiagnosticText2(JSON.stringify(requirement))}`
1105
+ )
1106
+ ] : [],
1107
+ dryRun ? `Preview digest: ${result.planDigest}` : `Run id: ${result.runId}
1108
+ Plan digest: ${result.planDigest}`
1109
+ ].filter((line) => line !== void 0).join("\n");
1110
+ }
1111
+ if (result.proveKind === "no-drift") {
1112
+ return "Database already matches the target; nothing was persisted.";
1113
+ }
1114
+ return `No durable plan was created.
1115
+ ${escapeDiagnosticText2(JSON.stringify(result.assessment, null, 2))}`;
1116
+ }
1117
+ function preservedPlanResult(error, fallback) {
1118
+ if (error instanceof PlanPersistenceIndeterminateError) return error.result;
1119
+ if (error instanceof PlanCleanupError) return error.result;
1120
+ return fallback;
1121
+ }
1122
+ function primaryPlanError(error) {
1123
+ if (error instanceof PlanPersistenceIndeterminateError) {
1124
+ return error.persistenceError;
1125
+ }
1126
+ if (error instanceof PlanCleanupError) return error.cleanupError;
1127
+ if (error instanceof PlanOperationAndCleanupError) {
1128
+ return error.operationError;
1129
+ }
1130
+ return error;
1131
+ }
1132
+ function cleanupFailure(error) {
1133
+ if (error instanceof PlanPersistenceIndeterminateError) {
1134
+ return error.cleanup;
1135
+ }
1136
+ if (error instanceof PlanCleanupError) {
1137
+ return { kind: "failed", error: error.cleanupError };
1138
+ }
1139
+ if (error instanceof PlanOperationAndCleanupError) {
1140
+ return { kind: "failed", error: error.cleanupError };
1141
+ }
1142
+ return noCleanupFailure;
1143
+ }
1144
+ function formatPlanFailureJson(error, fallback) {
1145
+ const result = preservedPlanResult(error, fallback);
1146
+ const cleanupError = cleanupFailure(error);
1147
+ return {
1148
+ compareKind: result?.compareKind ?? null,
1149
+ proveKind: result?.proveKind ?? null,
1150
+ assessment: result?.assessment ?? null,
1151
+ persisted: result?.persisted ?? false,
1152
+ runId: result?.runId ?? null,
1153
+ planDigest: result?.planDigest ?? null,
1154
+ error: describeThrown(primaryPlanError(error)),
1155
+ ...cleanupError.kind === "not-failed" ? {} : { cleanupError: describeThrown(cleanupError.error) }
1156
+ };
1157
+ }
1158
+ function formatPlanFailureHuman(error, fallback) {
1159
+ const result = preservedPlanResult(error, fallback);
1160
+ const fields = result ? [
1161
+ result.persisted === true ? "A durable plan was created before this failure." : result.persisted === "indeterminate" ? "Persistence is indeterminate; inspect the minted run before retrying." : "No durable plan was created.",
1162
+ ...result.runId ? [`Run id: ${result.runId}`] : [],
1163
+ ...result.planDigest ? [`Plan digest: ${result.planDigest}`] : []
1164
+ ] : [];
1165
+ const cleanupError = cleanupFailure(error);
1166
+ return [
1167
+ `\u274C ${escapeDiagnosticText2(describeThrown(primaryPlanError(error)))}`,
1168
+ ...fields,
1169
+ ...cleanupError.kind === "not-failed" ? [] : [
1170
+ `Connection release also failed: ${escapeDiagnosticText2(describeThrown(cleanupError.error))}`
1171
+ ]
1172
+ ].join("\n");
1173
+ }
1174
+ function printHumanResult(result, dryRun) {
1175
+ console.log(formatPlanHuman(result, dryRun));
1176
+ }
1177
+ function exitCodeForPlanResult(result) {
1178
+ return result.proveKind === "blocked" || result.proveKind === "inapplicable" ? 1 : 0;
1179
+ }
1180
+ var planCommand = new Command4("plan").description("Prove and persist a transition plan without applying it").argument("<schema-file>", "Schema DSL file").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema <name>", "Database schema name", "public").option("--dry-run", "Prove and render without persisting a transition run").option("--format <format>", "Output format: sql or json", "sql").exitOverride().configureOutput({ writeErr: () => {
1181
+ } }).action(
1182
+ async (schemaFile, options) => {
1183
+ let result;
1184
+ try {
1185
+ result = await runPlan({ ...options, schemaFile });
1186
+ if (options.format === "json") {
1187
+ console.log(
1188
+ JSON.stringify(
1189
+ formatPlanJson(result, options.dryRun === true),
1190
+ null,
1191
+ 2
1192
+ )
1193
+ );
1194
+ } else {
1195
+ printHumanResult(result, options.dryRun === true);
1196
+ }
1197
+ process.exitCode = exitCodeForPlanResult(result);
1198
+ } catch (error) {
1199
+ if (options.format === "json") {
1200
+ console.log(
1201
+ JSON.stringify(formatPlanFailureJson(error, result), null, 2)
1202
+ );
1203
+ } else {
1204
+ console.error(formatPlanFailureHuman(error, result));
1205
+ }
1206
+ process.exitCode = 1;
1207
+ }
1208
+ }
1209
+ );
1210
+
694
1211
  // src/commands/push.ts
695
1212
  import {
696
1213
  comparePgsqlDatabaseSchema as comparePgsqlDatabaseSchema2,
697
- createPgsqlAdapter as createPgsqlAdapter2,
1214
+ createPgsqlAdapter as createPgsqlAdapter3,
1215
+ escapeDiagnosticText as escapeDiagnosticText3,
698
1216
  generateDDL,
699
1217
  generateMigrationSQL as generateMigrationSQL2,
700
1218
  getNamingPluginForDbCasing
701
1219
  } from "@dbsp/adapter-pgsql";
702
- import { Command as Command4 } from "commander";
1220
+ import { Command as Command5 } from "commander";
703
1221
 
704
1222
  // src/ddl-executor.ts
705
1223
  async function executeDdl(pool, statements, options) {
@@ -732,7 +1250,7 @@ async function executeDdl(pool, statements, options) {
732
1250
 
733
1251
  // src/commands/push.ts
734
1252
  var MIGRATIONS_TABLE = "_dbsp_migrations";
735
- var pushCommand = new Command4("push").description("Push schema to database (additive by default)").option(
1253
+ var pushCommand = new Command5("push").description("Push schema to database (additive by default)").option(
736
1254
  "-s, --schema <path>",
737
1255
  "Path to schema file (default: dbsp.schema.ts)"
738
1256
  ).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(
@@ -792,11 +1310,13 @@ var pushCommand = new Command4("push").description("Push schema to database (add
792
1310
  );
793
1311
  }
794
1312
  } else {
795
- const adapter = createPgsqlAdapter2(pool);
1313
+ const adapter = createPgsqlAdapter3(pool);
796
1314
  const compareOptions = {
797
1315
  ...options.schemaName ? { schema: options.schemaName } : {},
798
1316
  ...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {},
799
- onWarning: (message) => console.warn(`\u26A0\uFE0F ${message}`)
1317
+ onExpressionCanonicalizationWarning: (warning) => console.warn(
1318
+ `\u26A0\uFE0F [${warning.kind} ${escapeDiagnosticText3(warning.table)}.${escapeDiagnosticText3(warning.name)}] ${warning.message}`
1319
+ )
800
1320
  };
801
1321
  const diff = await comparePgsqlDatabaseSchema2(
802
1322
  adapter,
@@ -933,8 +1453,8 @@ function formatOutstandingChange(change) {
933
1453
 
934
1454
  // src/commands/repl.ts
935
1455
  import { readFileSync as readFileSync2 } from "fs";
936
- import { Command as Command5 } from "commander";
937
- 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(
1456
+ import { Command as Command6 } from "commander";
1457
+ var replCommand = new Command6("repl").description("Launch interactive REPL for exploring schema and queries").option("-s, --schema <path>", "Path to schema file (default: auto-detect)").option(
938
1458
  "-d, --db <url>",
939
1459
  "PostgreSQL connection URL for execution mode (e.g., postgres://localhost/mydb)"
940
1460
  ).option("-e, --eval <query>", "Execute a single query and exit (batch mode)").option(
@@ -1056,9 +1576,10 @@ function mapReplCasingOption(casing) {
1056
1576
  // src/commands/verify.ts
1057
1577
  import {
1058
1578
  comparePgsqlDatabaseSchema as comparePgsqlDatabaseSchema3,
1059
- createPgsqlAdapter as createPgsqlAdapter3
1579
+ createPgsqlAdapter as createPgsqlAdapter4,
1580
+ escapeDiagnosticText as escapeDiagnosticText4
1060
1581
  } from "@dbsp/adapter-pgsql";
1061
- import { Command as Command6 } from "commander";
1582
+ import { Command as Command7 } from "commander";
1062
1583
 
1063
1584
  // src/verifier.ts
1064
1585
  var CHANGE_TO_DRIFT = {
@@ -1182,7 +1703,7 @@ function formatVerifyResult(result) {
1182
1703
  }
1183
1704
 
1184
1705
  // src/commands/verify.ts
1185
- var verifyCommand = new Command6("verify").description("Compare schema vs real database (drift detection)").option(
1706
+ var verifyCommand = new Command7("verify").description("Compare schema vs real database (drift detection)").option(
1186
1707
  "-s, --schema <path>",
1187
1708
  "Path to schema file (default: dbsp.schema.ts)"
1188
1709
  ).requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema-name <name>", "Database schema name (default: public)").option("--json", "Output as JSON").action(
@@ -1202,11 +1723,13 @@ var verifyCommand = new Command6("verify").description("Compare schema vs real d
1202
1723
  const schemaModel = loaded.model;
1203
1724
  const { pool } = await createDbConnection(options.db);
1204
1725
  try {
1205
- const adapter = createPgsqlAdapter3(pool);
1726
+ const adapter = createPgsqlAdapter4(pool);
1206
1727
  const diff = await comparePgsqlDatabaseSchema3(adapter, schemaModel, {
1207
1728
  ...options.schemaName ? { schema: options.schemaName } : {},
1208
1729
  ...loaded.dbCasing !== void 0 ? { dbCasing: loaded.dbCasing } : {},
1209
- onWarning: (message) => console.warn(`\u26A0\uFE0F ${message}`)
1730
+ onExpressionCanonicalizationWarning: (warning) => console.warn(
1731
+ `\u26A0\uFE0F [${warning.kind} ${escapeDiagnosticText4(warning.table)}.${escapeDiagnosticText4(warning.name)}] ${warning.message}`
1732
+ )
1210
1733
  });
1211
1734
  const dbModel = await adapter.introspect({
1212
1735
  ...options.schemaName ? { schema: options.schemaName } : {}
@@ -1250,14 +1773,45 @@ var verifyCommand = new Command6("verify").description("Compare schema vs real d
1250
1773
  );
1251
1774
 
1252
1775
  // src/index.ts
1253
- var program = new Command7();
1776
+ var program = new Command8();
1777
+ function selectedCommand(argv, commands) {
1778
+ for (let index = 0; index < argv.length; index += 1) {
1779
+ const argument = argv[index];
1780
+ if (argument === "--") return void 0;
1781
+ if (argument?.startsWith("-")) continue;
1782
+ const command = commands.find((candidate) => candidate.name() === argument);
1783
+ return command === void 0 ? void 0 : { command, index };
1784
+ }
1785
+ return void 0;
1786
+ }
1787
+ function requestsPlanJson(argv, commands) {
1788
+ const selected = selectedCommand(argv, commands);
1789
+ if (selected?.command !== planCommand) return false;
1790
+ for (let index = selected.index + 1; index < argv.length; index += 1) {
1791
+ const argument = argv[index];
1792
+ if (argument === "--") break;
1793
+ if (argument === "--format" && argv[index + 1] === "json" || argument === "--format=json") {
1794
+ return true;
1795
+ }
1796
+ }
1797
+ return false;
1798
+ }
1254
1799
  program.name("dbsp").description("Schema-first code generation for db-semantic-planner").version("0.0.1");
1255
1800
  program.addCommand(generateCommand);
1256
1801
  program.addCommand(introspectCommand);
1257
1802
  program.addCommand(migrateCommand);
1803
+ program.addCommand(planCommand);
1258
1804
  program.addCommand(pushCommand);
1259
1805
  program.addCommand(replCommand);
1260
1806
  program.addCommand(verifyCommand);
1807
+ var planJsonRequested = requestsPlanJson(
1808
+ process.argv.slice(2),
1809
+ program.commands
1810
+ );
1811
+ if (planJsonRequested) {
1812
+ program.configureOutput({ writeErr: () => {
1813
+ } });
1814
+ }
1261
1815
  program.exitOverride();
1262
1816
  try {
1263
1817
  program.parse();
@@ -1266,7 +1820,7 @@ try {
1266
1820
  process.exit(0);
1267
1821
  }
1268
1822
  const message = err instanceof Error ? err.message : "Command parse error";
1269
- if (process.argv.includes("--json")) {
1823
+ if (process.argv.includes("--json") || planJsonRequested) {
1270
1824
  console.log(JSON.stringify({ status: "error", error: message }, null, 2));
1271
1825
  } else {
1272
1826
  console.error(`\u274C ${message}`);