@everystack/cli 0.4.105 → 0.4.107
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/CHANGELOG.md +34 -0
- package/package.json +3 -3
- package/src/cli/alter-type-dependents.ts +199 -0
- package/src/cli/apply-execute.ts +18 -0
- package/src/cli/commands/db-apply.ts +54 -7
- package/src/cli/commands/db-branch.ts +2 -0
- package/src/cli/commands/db-build.ts +2 -1
- package/src/cli/commands/db-check.ts +12 -1
- package/src/cli/commands/db-plan.ts +31 -12
- package/src/cli/commands/db-reconcile.ts +11 -2
- package/src/cli/commands/db-sync.ts +24 -8
- package/src/cli/db-build.ts +7 -4
- package/src/cli/declared-derived.ts +11 -1
- package/src/cli/derived-catchup.ts +11 -3
- package/src/cli/edge-plan.ts +34 -2
- package/src/cli/lifecycle-preflight.ts +422 -52
- package/src/cli/state-apply.ts +53 -21
package/src/cli/state-apply.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { introspectDerived } from './derived-introspect';
|
|
|
37
37
|
import { objectGrantDelta } from './derived-plan';
|
|
38
38
|
import { isPureComment, statementBody, statementHead } from './sql-statement';
|
|
39
39
|
import { splitFunctionIdentity } from './pg-argtypes';
|
|
40
|
-
import type { StatePrerequisite } from './edge-plan';
|
|
40
|
+
import type { StateDependent, StatePrerequisite } from './edge-plan';
|
|
41
41
|
|
|
42
42
|
const NOTICE_PREFIX = '-- NOTICE';
|
|
43
43
|
export const STATE_PREREQUISITE_DRIFT_PREFIX = 'state prerequisite drift:';
|
|
@@ -366,6 +366,11 @@ export interface StateApplyOptions {
|
|
|
366
366
|
* back both, rather than leaving a derived object ahead of the state it serves.
|
|
367
367
|
*/
|
|
368
368
|
statePrerequisites?: readonly StatePrerequisite[];
|
|
369
|
+
/**
|
|
370
|
+
* Reviewed declared views and matviews a type change in this batch binds: dropped (dependents
|
|
371
|
+
* first) before it and re-created after it, in the same transaction, so they are never absent.
|
|
372
|
+
*/
|
|
373
|
+
stateDependents?: readonly StateDependent[];
|
|
369
374
|
/**
|
|
370
375
|
* An explicit write transaction over one pinned connection. Prerequisites need this because
|
|
371
376
|
* their DDL, the state edge, and their catalog-derived provenance must commit or roll back
|
|
@@ -518,6 +523,7 @@ export async function applyGeneratedStatements(
|
|
|
518
523
|
): Promise<StateApplyResult> {
|
|
519
524
|
const classified = classifyGeneratedStatements(statements);
|
|
520
525
|
const prerequisites = options.statePrerequisites ?? [];
|
|
526
|
+
const dependents = options.stateDependents ?? [];
|
|
521
527
|
// Checks the caller withheld before calling. They are outstanding from the first line of this
|
|
522
528
|
// function: the outcome written before the batch already counts them, and they come back in the
|
|
523
529
|
// result whether or not any check actually ran here.
|
|
@@ -583,7 +589,7 @@ export async function applyGeneratedStatements(
|
|
|
583
589
|
const validationSql = validations.join('\n;\n');
|
|
584
590
|
const started = now();
|
|
585
591
|
try {
|
|
586
|
-
if (prerequisites.length > 0) {
|
|
592
|
+
if (prerequisites.length > 0 || dependents.length > 0) {
|
|
587
593
|
if (!options.transaction) {
|
|
588
594
|
throw new Error('state prerequisites require a pinned write transaction; this apply lane cannot prove atomic provenance.');
|
|
589
595
|
}
|
|
@@ -591,39 +597,65 @@ export async function applyGeneratedStatements(
|
|
|
591
597
|
const [settings] = await transactionRunner("SELECT pg_catalog.current_setting('search_path') AS search_path");
|
|
592
598
|
const originalPath = settings?.search_path;
|
|
593
599
|
if (typeof originalPath !== 'string') throw new Error('could not capture prerequisite search_path; no state was applied.');
|
|
600
|
+
const reread = async () => new Map((await introspectDerived(borrowedSessionRunner(transactionRunner)))
|
|
601
|
+
.objects.map((object) => [object.identity, object]));
|
|
602
|
+
// A dependent is re-created from the reviewed SQL, so it must still be the object the mint
|
|
603
|
+
// read. Checked inside the transaction, before any write.
|
|
604
|
+
if (dependents.length > 0) {
|
|
605
|
+
const before = await reread();
|
|
606
|
+
for (const dependent of dependents) {
|
|
607
|
+
if (before.get(dependent.identity)?.defHash !== dependent.expectedDefHash) {
|
|
608
|
+
throw new Error(`${STATE_PREREQUISITE_DRIFT_PREFIX} ${dependent.kind} ${dependent.identity} is not the one this plan was minted against; the target changed since minting. Re-mint with db:plan.`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
594
612
|
// Defaults and SQL-standard bodies bind names at CREATE time. Use the full declared
|
|
595
613
|
// path recorded at mint, then restore the exact connection setting; the state DDL
|
|
596
|
-
// that follows pins the canonical path itself.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
...(
|
|
600
|
-
prerequisite
|
|
601
|
-
|
|
602
|
-
|
|
614
|
+
// that follows pins the canonical path itself. Dependents drop before the edge, children
|
|
615
|
+
// first, and come back after it, parents first, under their declared path.
|
|
616
|
+
batchSql = [
|
|
617
|
+
...(prerequisites.length > 0 ? ['SET LOCAL check_function_bodies = off'] : []),
|
|
618
|
+
...prerequisites.flatMap((prerequisite) => [
|
|
619
|
+
renderStatePrerequisiteGuard(prerequisite),
|
|
620
|
+
...(prerequisite.searchPath === null ? [] : [renderLocalSearchPath(prerequisite.searchPath)]),
|
|
621
|
+
prerequisite.sql,
|
|
622
|
+
...(prerequisite.searchPath === null ? [] : [renderLocalSearchPath(originalPath)]),
|
|
623
|
+
]),
|
|
624
|
+
...[...dependents].reverse().map((dependent) => dependent.dropSql),
|
|
625
|
+
...stateBatch,
|
|
626
|
+
...dependents.flatMap((dependent) => [renderLocalSearchPath(dependent.searchPath ?? originalPath), dependent.sql]),
|
|
627
|
+
...(dependents.length > 0 ? [renderLocalSearchPath(originalPath)] : []),
|
|
628
|
+
].join('\n;\n');
|
|
603
629
|
await transactionRunner(batchSql);
|
|
604
630
|
// `defHash` is PostgreSQL's canonical deparse, so source text cannot predict it. Read it
|
|
605
631
|
// after the CREATE through the OPEN transaction, then write provenance before COMMIT.
|
|
606
|
-
const
|
|
607
|
-
|
|
632
|
+
const created: Array<{ identity: string; kind: 'function' | StateDependent['kind']; grantContract: StatePrerequisite['grantContract'];
|
|
633
|
+
provenance: { srcHash: string; bodyHash?: string; kind: string; dropSql?: string } }> = [
|
|
634
|
+
...prerequisites.map((p) => ({ identity: p.identity, kind: 'function' as const, grantContract: p.grantContract, provenance: p.provenance })),
|
|
635
|
+
...dependents.map((d) => ({ identity: d.identity, kind: d.kind, grantContract: d.grantContract, provenance: d.provenance })),
|
|
636
|
+
];
|
|
608
637
|
let liveByIdentity = await reread();
|
|
609
|
-
|
|
638
|
+
// A function is a state prerequisite; a re-created dependent is named as the view it is.
|
|
639
|
+
const objectLabel = (object: (typeof created)[number]): string =>
|
|
640
|
+
`${object.kind === 'function' ? 'state prerequisite' : object.kind} ${object.identity}`;
|
|
641
|
+
const grantDelta = (prerequisite: (typeof created)[number]) => {
|
|
610
642
|
const live = liveByIdentity.get(prerequisite.identity);
|
|
611
643
|
if (!live || live.grants === undefined) {
|
|
612
|
-
throw new Error(
|
|
644
|
+
throw new Error(`${objectLabel(prerequisite)} has no readable grant catalog; no provenance was written.`);
|
|
613
645
|
}
|
|
614
646
|
const contract = prerequisite.grantContract;
|
|
615
647
|
if (contract.owner && live.owner !== contract.owner) {
|
|
616
|
-
throw new Error(
|
|
648
|
+
throw new Error(`${objectLabel(prerequisite)} owner did not converge to ${contract.owner}; no provenance was written.`);
|
|
617
649
|
}
|
|
618
|
-
const delta = objectGrantDelta({ ...contract, identity: prerequisite.identity, kind:
|
|
650
|
+
const delta = objectGrantDelta({ ...contract, identity: prerequisite.identity, kind: prerequisite.kind }, live);
|
|
619
651
|
if (!delta || delta.columnScoped || delta.suppressed.length > 0) {
|
|
620
|
-
throw new Error(
|
|
652
|
+
throw new Error(`${objectLabel(prerequisite)} grants cannot be verified; no provenance was written.`);
|
|
621
653
|
}
|
|
622
654
|
return delta.statements;
|
|
623
655
|
};
|
|
624
656
|
// CREATE inherits default ACLs, and ALTER OWNER can remove a declared builder grant.
|
|
625
657
|
// Settle from the actual catalog using the same grant diff as derived reconcile.
|
|
626
|
-
const settle =
|
|
658
|
+
const settle = created.flatMap((prerequisite) => {
|
|
627
659
|
const statements = grantDelta(prerequisite);
|
|
628
660
|
const { owner, ownerMode } = prerequisite.grantContract;
|
|
629
661
|
return statements.length > 0 && owner && ownerMode === 'set-role'
|
|
@@ -636,16 +668,16 @@ export async function applyGeneratedStatements(
|
|
|
636
668
|
liveByIdentity = await reread();
|
|
637
669
|
}
|
|
638
670
|
// A successful REVOKE can be a warning-only no-op. Only a second catalog read proves it.
|
|
639
|
-
for (const prerequisite of
|
|
671
|
+
for (const prerequisite of created) {
|
|
640
672
|
const remaining = grantDelta(prerequisite);
|
|
641
673
|
if (remaining.length > 0) {
|
|
642
|
-
throw new Error(
|
|
674
|
+
throw new Error(`${objectLabel(prerequisite)} grants did not converge: still needs ${remaining.join('; ')}. No provenance was written.`);
|
|
643
675
|
}
|
|
644
676
|
}
|
|
645
|
-
const provenanceSql =
|
|
677
|
+
const provenanceSql = created.map((prerequisite) => {
|
|
646
678
|
const liveObject = liveByIdentity.get(prerequisite.identity);
|
|
647
679
|
if (!liveObject) {
|
|
648
|
-
throw new Error(
|
|
680
|
+
throw new Error(`${objectLabel(prerequisite)} was created but is not introspectable before commit; provenance was not recorded.`);
|
|
649
681
|
}
|
|
650
682
|
return renderProvenanceUpsert(
|
|
651
683
|
prerequisite.identity,
|