@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.
@@ -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
- batchSql = ['SET LOCAL check_function_bodies = off', ...prerequisites.flatMap((prerequisite) => [
598
- renderStatePrerequisiteGuard(prerequisite),
599
- ...(prerequisite.searchPath === null ? [] : [renderLocalSearchPath(prerequisite.searchPath)]),
600
- prerequisite.sql,
601
- ...(prerequisite.searchPath === null ? [] : [renderLocalSearchPath(originalPath)]),
602
- ]), ...stateBatch].join('\n;\n');
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 reread = async () => new Map((await introspectDerived(borrowedSessionRunner(transactionRunner)))
607
- .objects.map((object) => [object.identity, object]));
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
- const grantDelta = (prerequisite: StatePrerequisite) => {
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(`state prerequisite ${prerequisite.identity} has no readable grant catalog; no provenance was written.`);
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(`state prerequisite ${prerequisite.identity} owner did not converge to ${contract.owner}; no provenance was written.`);
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: 'function' }, live);
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(`state prerequisite ${prerequisite.identity} grants cannot be verified; no provenance was written.`);
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 = prerequisites.flatMap((prerequisite) => {
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 prerequisites) {
671
+ for (const prerequisite of created) {
640
672
  const remaining = grantDelta(prerequisite);
641
673
  if (remaining.length > 0) {
642
- throw new Error(`state prerequisite ${prerequisite.identity} grants did not converge: still needs ${remaining.join('; ')}. No provenance was written.`);
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 = prerequisites.map((prerequisite) => {
677
+ const provenanceSql = created.map((prerequisite) => {
646
678
  const liveObject = liveByIdentity.get(prerequisite.identity);
647
679
  if (!liveObject) {
648
- throw new Error(`state prerequisite ${prerequisite.identity} was created but is not introspectable before commit; provenance was not recorded.`);
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,