@everystack/cli 0.4.99 → 0.4.100
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 +14 -0
- package/package.json +2 -2
- package/src/cli/apply-execute.ts +15 -1
- package/src/cli/catalog-rename.ts +115 -0
- package/src/cli/model-render.ts +19 -3
- package/src/cli/schema-compile.ts +22 -1
- package/src/cli/schema-diff.ts +18 -10
- package/src/cli/schema-introspect.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to `@everystack/cli` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.100] - 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- check() takes an explicit constraint name; db:pull keeps a live name
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- db:apply runs a rename-only plan
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
- a check folded into .validate() takes the field check name
|
|
20
|
+
|
|
7
21
|
## [0.4.99] - 2026-10-14
|
|
8
22
|
|
|
9
23
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.100",
|
|
4
4
|
"description": "CLI and OTA updates for Expo apps on everystack",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"structured-headers": "1.0.1",
|
|
146
146
|
"tsx": "4.21.0",
|
|
147
147
|
"typescript": "5.9.3",
|
|
148
|
-
"@everystack/model": "0.4.
|
|
148
|
+
"@everystack/model": "0.4.22"
|
|
149
149
|
},
|
|
150
150
|
"peerDependencies": {
|
|
151
151
|
"@everystack/server": ">=0.4.0",
|
package/src/cli/apply-execute.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
import { validationPendingReason, pendingValidationsAreSafe } from './apply-refusals';
|
|
74
74
|
import { ENSURE_RECONCILER_SQL, renderSchemaLogInsert, renderSchemaLogFingerprintUpdate } from './derived-apply';
|
|
75
75
|
import { checkPlanPrecondition, planHash, defaultAclHash, type EdgePlan } from './edge-plan';
|
|
76
|
+
import { renameEdgeVerdict } from './catalog-rename';
|
|
76
77
|
import type { DescentLedgerRow, LedgerRead } from './git-descent';
|
|
77
78
|
|
|
78
79
|
/**
|
|
@@ -477,7 +478,20 @@ export async function executeApplyPlan(
|
|
|
477
478
|
}
|
|
478
479
|
|
|
479
480
|
if (live === plan.toFingerprint) {
|
|
480
|
-
|
|
481
|
+
// The hash is blind to a catalog rename (a check is keyed by predicate, an index or key by
|
|
482
|
+
// content), so a plan of renames alone has from == to == live whether or not it ran. Only
|
|
483
|
+
// then do the names decide, read from the same snapshot as the lock. Not landed: fall
|
|
484
|
+
// through, and every gate below runs. Half landed, or in a shape the plan never predicted:
|
|
485
|
+
// stale. Everything else is today's idempotent re-apply.
|
|
486
|
+
const verdict = plan.fromFingerprint === plan.toFingerprint
|
|
487
|
+
? renameEdgeVerdict(plan.statements, before)
|
|
488
|
+
: { verdict: 'not-renames' as const };
|
|
489
|
+
if (verdict.verdict === 'stale') {
|
|
490
|
+
const reason = `this plan is stale: ${verdict.reason}. Re-mint with db:plan.`;
|
|
491
|
+
await recordRefusal(runner, plan, live, 'rename edge', reason, options);
|
|
492
|
+
return { status: 'refused', liveFingerprint: live, reason };
|
|
493
|
+
}
|
|
494
|
+
if (verdict.verdict !== 'unsettled') return { status: 'already-applied', liveFingerprint: live };
|
|
481
495
|
}
|
|
482
496
|
const pre = checkPlanPrecondition(plan, live);
|
|
483
497
|
if (!pre.ok) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalog-rename — the catalog-only renames the planner emits, their parser, and the verdict
|
|
3
|
+
* db:apply reads from them.
|
|
4
|
+
*
|
|
5
|
+
* A rename of a check, a foreign key or an index changes no fingerprint: a check is hashed by its
|
|
6
|
+
* predicate and an index or key by its content. So a plan whose ONLY change is such a rename has
|
|
7
|
+
* from == to == live, and the endpoint cannot tell a rename that ran from one that did not. The
|
|
8
|
+
* live names can. The renderer and the parser live side by side here so they cannot drift; the
|
|
9
|
+
* parser accepts exactly what the renderer writes and nothing else.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SchemaSnapshot } from './schema-introspect';
|
|
13
|
+
import { isPureComment } from './sql-statement';
|
|
14
|
+
|
|
15
|
+
/** The same quoting schema-diff uses for every identifier it emits. */
|
|
16
|
+
const quote = (name: string): string => `"${name}"`;
|
|
17
|
+
|
|
18
|
+
/** `public.orders` → `public.`; an unqualified table carries no qualifier. */
|
|
19
|
+
const schemaPrefix = (table: string): string => (table.includes('.') ? `${table.split('.')[0]}.` : '');
|
|
20
|
+
|
|
21
|
+
export interface RenameChange {
|
|
22
|
+
/** Schema-qualified, as the snapshot spells it (`public.orders`). */
|
|
23
|
+
table: string;
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `ALTER TABLE … RENAME CONSTRAINT` — catalog-only and instant; the constraint keeps its identity. */
|
|
29
|
+
export function renderConstraintRename(change: RenameChange): string {
|
|
30
|
+
return `ALTER TABLE ${change.table} RENAME CONSTRAINT ${quote(change.from)} TO ${quote(change.to)};`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `ALTER INDEX … RENAME TO` — no rebuild. The schema qualifies the OLD name only. */
|
|
34
|
+
export function renderIndexRename(change: RenameChange): string {
|
|
35
|
+
return `ALTER INDEX ${schemaPrefix(change.table)}${quote(change.from)} RENAME TO ${quote(change.to)};`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type CatalogRename =
|
|
39
|
+
| { kind: 'constraint'; table: string; from: string; to: string }
|
|
40
|
+
/** ALTER INDEX names no table — an index name is unique within its schema, so the schema is the scope. */
|
|
41
|
+
| { kind: 'index'; schema: string; from: string; to: string };
|
|
42
|
+
|
|
43
|
+
const IDENT = '"([^"]*)"';
|
|
44
|
+
const CONSTRAINT_RE = new RegExp(`^ALTER TABLE (\\S+) RENAME CONSTRAINT ${IDENT} TO ${IDENT};$`);
|
|
45
|
+
const INDEX_RE = new RegExp(`^ALTER INDEX (?:([^\\s."]+)\\.)?${IDENT} RENAME TO ${IDENT};$`);
|
|
46
|
+
|
|
47
|
+
/** The rename a statement is, if it is exactly one the renderers above produce; otherwise null. */
|
|
48
|
+
export function parseCatalogRename(sql: string): CatalogRename | null {
|
|
49
|
+
const c = CONSTRAINT_RE.exec(sql);
|
|
50
|
+
if (c) return { kind: 'constraint', table: c[1], from: c[2], to: c[3] };
|
|
51
|
+
const i = INDEX_RE.exec(sql);
|
|
52
|
+
// An unqualified ALTER INDEX resolves through search_path; the renderer only writes one for an
|
|
53
|
+
// unqualified table, which the snapshot spells as `public`.
|
|
54
|
+
if (i) return { kind: 'index', schema: i[1] ?? 'public', from: i[2], to: i[3] };
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type RenameEdgeVerdict =
|
|
59
|
+
/** Empty, or some statement is not a recognized rename — the names have nothing to say. */
|
|
60
|
+
| { verdict: 'not-renames' }
|
|
61
|
+
/** Every rename already landed: `to` present, `from` absent. */
|
|
62
|
+
| { verdict: 'settled' }
|
|
63
|
+
/** No rename has landed: `from` present, `to` absent. The edge still has to run. */
|
|
64
|
+
| { verdict: 'unsettled' }
|
|
65
|
+
/** Anything else — the plan describes neither the database before it nor after it. */
|
|
66
|
+
| { verdict: 'stale'; reason: string };
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Where each rename in the edge stands in the BEFORE snapshot (the one the lock was computed
|
|
70
|
+
* from). Only an edge made entirely of recognized renames gets a verdict other than
|
|
71
|
+
* `not-renames`; one rename in an odd state, or a mix of landed and not, makes the plan stale.
|
|
72
|
+
*/
|
|
73
|
+
export function renameEdgeVerdict(statements: readonly string[], before: SchemaSnapshot): RenameEdgeVerdict {
|
|
74
|
+
// A comment is never run, and the planner writes them beside real changes (an inert
|
|
75
|
+
// renamedFrom notice, a held drop). They must not hide a rename edge from the names.
|
|
76
|
+
const real = statements.filter((sql) => !isPureComment(sql));
|
|
77
|
+
if (real.length === 0) return { verdict: 'not-renames' };
|
|
78
|
+
const renames: CatalogRename[] = [];
|
|
79
|
+
for (const sql of real) {
|
|
80
|
+
const rename = parseCatalogRename(sql);
|
|
81
|
+
if (!rename) return { verdict: 'not-renames' };
|
|
82
|
+
renames.push(rename);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const settled: string[] = [];
|
|
86
|
+
const unsettled: string[] = [];
|
|
87
|
+
for (const rename of renames) {
|
|
88
|
+
let present: Set<string>;
|
|
89
|
+
let where: string;
|
|
90
|
+
if (rename.kind === 'constraint') {
|
|
91
|
+
const table = before.tables.find((t) => t.table === rename.table);
|
|
92
|
+
if (!table) return { verdict: 'stale', reason: `table ${rename.table} does not exist` };
|
|
93
|
+
present = new Set([...table.checks, ...table.foreignKeys, ...table.uniques].map((c) => c.name));
|
|
94
|
+
where = `on ${rename.table}`;
|
|
95
|
+
} else {
|
|
96
|
+
present = new Set(before.tables
|
|
97
|
+
.filter((t) => t.table.startsWith(`${rename.schema}.`))
|
|
98
|
+
.flatMap((t) => t.indexes.map((ix) => ix.name)));
|
|
99
|
+
where = `in schema ${rename.schema}`;
|
|
100
|
+
}
|
|
101
|
+
const hasFrom = present.has(rename.from);
|
|
102
|
+
const hasTo = present.has(rename.to);
|
|
103
|
+
const label = `"${rename.from}" → "${rename.to}"`;
|
|
104
|
+
if (hasTo && !hasFrom) settled.push(label);
|
|
105
|
+
else if (hasFrom && !hasTo) unsettled.push(label);
|
|
106
|
+
else if (hasFrom && hasTo) return { verdict: 'stale', reason: `both "${rename.from}" and "${rename.to}" exist ${where}` };
|
|
107
|
+
else return { verdict: 'stale', reason: `neither "${rename.from}" nor "${rename.to}" exists ${where}` };
|
|
108
|
+
}
|
|
109
|
+
if (unsettled.length === 0) return { verdict: 'settled' };
|
|
110
|
+
if (settled.length === 0) return { verdict: 'unsettled' };
|
|
111
|
+
return {
|
|
112
|
+
verdict: 'stale',
|
|
113
|
+
reason: `some of its renames are already applied (${settled.join(', ')}) and some are not (${unsettled.join(', ')})`,
|
|
114
|
+
};
|
|
115
|
+
}
|
package/src/cli/model-render.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { generatedIndexName, generatedForeignKeyName } from './schema-compile';
|
|
|
24
24
|
// The soft-delete RULE itself — one boolean combination, shared with `defineModel`'s refusal.
|
|
25
25
|
// Only the FACT EXTRACTION is duplicated here (this renderer holds text, not descriptors);
|
|
26
26
|
// the rule is not, so the two surfaces cannot drift on when a declaration is required.
|
|
27
|
-
import { needsSoftDeleteDeclaration, privilegesGrantDelete } from '@everystack/model';
|
|
27
|
+
import { needsSoftDeleteDeclaration, privilegesGrantDelete, positionalCheckName } from '@everystack/model';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Reverse a CHECK predicate back into the `.validate(z…)` that produced it — the inverse of
|
|
@@ -599,8 +599,18 @@ function renderConstraintsBlock(table: TableSchema, checks: CheckConstraint[], k
|
|
|
599
599
|
for (const u of table.uniques) {
|
|
600
600
|
if (u.columns.length > 1) items.push(`unique(${u.columns.map((c) => tsLiteral(toCamelCase(c))).join(', ')})`);
|
|
601
601
|
}
|
|
602
|
+
// R9 — a check keeps the name the database has. Said only when it is NOT the positional name
|
|
603
|
+
// the compiler would give it: position counts only the checks rendered bare, exactly as the
|
|
604
|
+
// compiler counts only unnamed ones, so every pulled check compiles back to its live name.
|
|
605
|
+
let position = 0;
|
|
602
606
|
for (const ck of checks) {
|
|
603
|
-
|
|
607
|
+
const predicate = `sql\`${normalizeCheck(ck.expr)}\``;
|
|
608
|
+
if (ck.name === positionalCheckName(bareName(table.table), position)) {
|
|
609
|
+
items.push(`check(${predicate})`);
|
|
610
|
+
position++;
|
|
611
|
+
} else {
|
|
612
|
+
items.push(`check(${predicate}, { name: ${tsLiteral(ck.name)} })`);
|
|
613
|
+
}
|
|
604
614
|
}
|
|
605
615
|
for (const ix of table.indexes) {
|
|
606
616
|
// Plain entries render as field keys; canonical-text entries (expressions, DESC,
|
|
@@ -655,9 +665,15 @@ export function renderModelBlock(table: TableSchema, known: Set<string>, enums:
|
|
|
655
665
|
const validates = new Map<string, string>();
|
|
656
666
|
const columnNames = new Set(table.columns.map((c) => c.name));
|
|
657
667
|
const tableChecks: CheckConstraint[] = [];
|
|
668
|
+
// A `.validate()` compiles to a CHECK named `<table>_<column>_check`. When a DIFFERENT live
|
|
669
|
+
// check already holds that name, folding this one into the field would give the model two
|
|
670
|
+
// constraints under one name, so it stays a table-level check and keeps its own.
|
|
671
|
+
const liveCheckNames = new Set(table.checks.map((ck) => ck.name));
|
|
658
672
|
for (const ck of table.checks) {
|
|
659
673
|
const v = checkToValidate(ck.expr);
|
|
660
|
-
|
|
674
|
+
const fieldCheckName = v ? `${bareName(table.table)}_${v.column}_check` : '';
|
|
675
|
+
const nameFree = ck.name === fieldCheckName || !liveCheckNames.has(fieldCheckName);
|
|
676
|
+
if (v && columnNames.has(v.column) && !validates.has(v.column) && nameFree) validates.set(v.column, v.zod);
|
|
661
677
|
else tableChecks.push(ck);
|
|
662
678
|
}
|
|
663
679
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import type { ModelDescriptor, FieldSpec, SequenceDescriptor } from '@everystack/model';
|
|
23
|
+
import { positionalCheckName } from '@everystack/model';
|
|
23
24
|
import type { TableSchema, EnumType, SequenceSchema, UniqueConstraint, CheckConstraint, IndexSchema, ForeignKey } from './schema-introspect';
|
|
24
25
|
import { nextvalSequence, identityClause, type RenameMap } from './schema-diff';
|
|
25
26
|
import { normalizeDeparsedExpr } from './deparse-normal';
|
|
@@ -59,6 +60,13 @@ export function modelConstraintSpecs(model: ModelDescriptor): { uniques: UniqueC
|
|
|
59
60
|
const indexes: IndexSchema[] = [];
|
|
60
61
|
let checkIndex = 0;
|
|
61
62
|
const usedIndexNames = new Set<string>();
|
|
63
|
+
// A field's `.validate()` CHECK is named `<table>_<column>_check`; a declared table check may
|
|
64
|
+
// not take that name, or CREATE TABLE fails with two constraints under one name.
|
|
65
|
+
const fieldCheckNames = new Map<string, string>();
|
|
66
|
+
for (const [key, field] of Object.entries(model.fields)) {
|
|
67
|
+
const col = toSnakeCase(key);
|
|
68
|
+
if (fieldCheckPredicate(col, field.spec)) fieldCheckNames.set(`${model.table}_${col}_check`, key);
|
|
69
|
+
}
|
|
62
70
|
for (const con of model.constraints) {
|
|
63
71
|
if (con.kind === 'unique') {
|
|
64
72
|
const cols = con.columns.map(toSnakeCase);
|
|
@@ -67,7 +75,20 @@ export function modelConstraintSpecs(model: ModelDescriptor): { uniques: UniqueC
|
|
|
67
75
|
// Normalized like the live producer (schema-introspect): a model that transcribes a
|
|
68
76
|
// pulled predicate must compare equal to the database it came from, whichever of the
|
|
69
77
|
// two equivalent deparse spellings each side happens to hold. See deparse-normal.ts.
|
|
70
|
-
|
|
78
|
+
//
|
|
79
|
+
// A DECLARED name is used verbatim and takes no position, so naming one check never
|
|
80
|
+
// renames its unnamed siblings; defineModel already refused duplicates and collisions
|
|
81
|
+
// with a positional name.
|
|
82
|
+
if (con.name !== undefined && fieldCheckNames.has(con.name)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`model '${model.table}': check(sql\`${con.predicate}\`) is named '${con.name}', the name of the CHECK that field '${fieldCheckNames.get(con.name)}'.validate() compiles to. Choose another name.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
checks.push({
|
|
88
|
+
name: con.name ?? positionalCheckName(model.table, checkIndex++),
|
|
89
|
+
expr: normalizeDeparsedExpr(con.predicate),
|
|
90
|
+
...(con.name !== undefined ? { nameDeclared: true } : {}),
|
|
91
|
+
});
|
|
71
92
|
} else if (con.kind === 'index') {
|
|
72
93
|
// Plain entries are field keys → snake_cased; raw sql`` entries (expressions,
|
|
73
94
|
// DESC, opclass) pass verbatim — they are already SQL (Brick E).
|
package/src/cli/schema-diff.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
IndexSchema,
|
|
34
34
|
SequenceSchema,
|
|
35
35
|
} from './schema-introspect';
|
|
36
|
+
import { renderConstraintRename, renderIndexRename } from './catalog-rename';
|
|
36
37
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
38
39
|
// The change set.
|
|
@@ -849,8 +850,12 @@ export function foreignKeyContentKey(
|
|
|
849
850
|
* same predicate is the same check no matter what it's called, so a pulled check (whose DB
|
|
850
851
|
* name we didn't choose) re-generates clean. A changed predicate drops the old (by its real
|
|
851
852
|
* name) and adds the new.
|
|
853
|
+
*
|
|
854
|
+
* R9, the CHECK half of B3/A8: a predicate match whose DECLARED name disagrees with the live
|
|
855
|
+
* one is the same check under another name — `RENAME CONSTRAINT`, catalog-only, no re-scan.
|
|
856
|
+
* An undeclared (positional) name never renames the database.
|
|
852
857
|
*/
|
|
853
|
-
function diffChecks(d: TableSchema, c: TableSchema, dropConstraints: SchemaChange[], addConstraints: SchemaChange[]): void {
|
|
858
|
+
function diffChecks(d: TableSchema, c: TableSchema, dropConstraints: SchemaChange[], addConstraints: SchemaChange[], renames: SchemaChange[]): void {
|
|
854
859
|
const desiredByPred = new Map(d.checks.map((ck) => [normalizeCheck(ck.expr), ck]));
|
|
855
860
|
const currentByPred = new Map(c.checks.map((ck) => [normalizeCheck(ck.expr), ck]));
|
|
856
861
|
// Paren-blind lookup, diagnostic only (see diffIndexes): a compound CHECK churns for the
|
|
@@ -860,7 +865,11 @@ function diffChecks(d: TableSchema, c: TableSchema, dropConstraints: SchemaChang
|
|
|
860
865
|
if (!desiredByPred.has(pred)) dropConstraints.push({ kind: 'dropConstraint', table: d.table, name: ck.name });
|
|
861
866
|
}
|
|
862
867
|
for (const [pred, ck] of desiredByPred) {
|
|
863
|
-
|
|
868
|
+
const live = currentByPred.get(pred);
|
|
869
|
+
if (live) {
|
|
870
|
+
if (ck.nameDeclared && live.name !== ck.name) renames.push({ kind: 'renameConstraint', table: d.table, from: live.name, to: ck.name });
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
864
873
|
const twin = currentByParenBlind.get(stripAllParens(ck.expr));
|
|
865
874
|
// Annotate only a twin that is itself being dropped (no exact desired match) — a real churn.
|
|
866
875
|
const churning = twin && !desiredByPred.has(normalizeCheck(twin.expr));
|
|
@@ -921,7 +930,7 @@ function diffConstraints(d: TableSchema, c: TableSchema, dropConstraints: Schema
|
|
|
921
930
|
for (const cur of bucket) dropConstraints.push({ kind: 'dropConstraint', table: d.table, name: cur.name });
|
|
922
931
|
}
|
|
923
932
|
|
|
924
|
-
diffChecks(d, c, dropConstraints, addConstraints);
|
|
933
|
+
diffChecks(d, c, dropConstraints, addConstraints, renames);
|
|
925
934
|
|
|
926
935
|
// Primary key: a first-time add is safe; any other change needs the existing
|
|
927
936
|
// constraint's name to drop it (which the snapshot discards), so it is surfaced.
|
|
@@ -1170,18 +1179,17 @@ function emitOne(change: SchemaChange, opts: EmitOptions): string | string[] {
|
|
|
1170
1179
|
}
|
|
1171
1180
|
case 'renameConstraint':
|
|
1172
1181
|
// Catalog-only and instant — the constraint and its backing index keep their identity.
|
|
1173
|
-
|
|
1182
|
+
// Rendered beside its parser: db:apply reads this exact text back (catalog-rename).
|
|
1183
|
+
return renderConstraintRename(change);
|
|
1174
1184
|
case 'redefineIndex': {
|
|
1175
1185
|
// Same statement as a removal; a DIFFERENT decision, which is why it is a different kind.
|
|
1176
1186
|
const schema = change.table.includes('.') ? `${change.table.split('.')[0]}.` : '';
|
|
1177
1187
|
return `DROP INDEX ${schema}${quote(change.name)};`;
|
|
1178
1188
|
}
|
|
1179
|
-
case 'renameIndex':
|
|
1180
|
-
// Non-destructive and instant — a catalog rename, no rebuild.
|
|
1181
|
-
//
|
|
1182
|
-
|
|
1183
|
-
return `ALTER INDEX ${schema}${quote(change.from)} RENAME TO ${quote(change.to)};`;
|
|
1184
|
-
}
|
|
1189
|
+
case 'renameIndex':
|
|
1190
|
+
// Non-destructive and instant — a catalog rename, no rebuild. Rendered beside its parser:
|
|
1191
|
+
// db:apply reads this exact text back (catalog-rename).
|
|
1192
|
+
return renderIndexRename(change);
|
|
1185
1193
|
case 'enumValuesChanged':
|
|
1186
1194
|
return `-- NOTICE: enum ${quote(change.name)} values changed ([${change.from.join(', ')}] → [${change.to.join(', ')}]) in a way Postgres can't apply in place (a removal, reorder, or rename). ADD VALUE only appends; reshaping an enum needs a manual type swap (create new type, ALTER COLUMN ... TYPE using a cast, drop old).`;
|
|
1187
1195
|
case 'createSequence':
|
|
@@ -88,6 +88,8 @@ export interface CheckConstraint {
|
|
|
88
88
|
name: string;
|
|
89
89
|
/** The predicate text, as `pg_get_constraintdef` deparses it. */
|
|
90
90
|
expr: string;
|
|
91
|
+
/** The name was DECLARED (`check(sql, { name })`), not positional — same rule as {@link IndexSchema.nameDeclared}. */
|
|
92
|
+
nameDeclared?: boolean;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/** A standalone index (NOT one backing a PK or UNIQUE constraint — those are excluded). */
|