@dbsp/cli 2.1.4 → 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 +570 -16
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1313
|
+
const adapter = createPgsqlAdapter3(pool);
|
|
796
1314
|
const compareOptions = {
|
|
797
1315
|
...options.schemaName ? { schema: options.schemaName } : {},
|
|
798
1316
|
...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {},
|
|
799
|
-
|
|
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
|
|
937
|
-
var replCommand = new
|
|
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
|
|
1579
|
+
createPgsqlAdapter as createPgsqlAdapter4,
|
|
1580
|
+
escapeDiagnosticText as escapeDiagnosticText4
|
|
1060
1581
|
} from "@dbsp/adapter-pgsql";
|
|
1061
|
-
import { Command as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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}`);
|