@everystack/cli 0.4.102 → 0.4.104

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/package.json +3 -3
  3. package/src/cli/authz-compile.ts +48 -11
  4. package/src/cli/authz-contract.ts +6 -0
  5. package/src/cli/authz-derive.ts +3 -1
  6. package/src/cli/commands/db-apply.ts +34 -10
  7. package/src/cli/commands/db-backup.ts +62 -26
  8. package/src/cli/commands/db-fork.ts +2 -1
  9. package/src/cli/commands/db-plan.ts +5 -0
  10. package/src/cli/commands/db-pull.ts +48 -12
  11. package/src/cli/commands/db-reconcile.ts +34 -5
  12. package/src/cli/commands/db-sync.ts +32 -3
  13. package/src/cli/commands/db.ts +46 -21
  14. package/src/cli/db-build.ts +11 -1
  15. package/src/cli/declared-diff.ts +3 -2
  16. package/src/cli/derived-apply.ts +5 -1
  17. package/src/cli/derived-catchup.ts +4 -0
  18. package/src/cli/derived-compile.ts +2 -1
  19. package/src/cli/derived-introspect.ts +22 -1
  20. package/src/cli/derived-plan.ts +64 -0
  21. package/src/cli/derived-render.ts +1 -0
  22. package/src/cli/edge-plan.ts +35 -0
  23. package/src/cli/index-lock.ts +65 -0
  24. package/src/cli/index.ts +1 -1
  25. package/src/cli/lifecycle-canon.ts +2 -1
  26. package/src/cli/lifecycle-derived.ts +334 -45
  27. package/src/cli/lifecycle-preflight.ts +498 -0
  28. package/src/cli/lifecycle-pull.ts +119 -11
  29. package/src/cli/migration-compile.ts +4 -3
  30. package/src/cli/migration-generate.ts +3 -2
  31. package/src/cli/model-api.ts +9 -0
  32. package/src/cli/model-render.ts +80 -8
  33. package/src/cli/plain-output.ts +14 -1
  34. package/src/cli/schema-compile.ts +34 -67
  35. package/src/cli/schema-fingerprint.ts +3 -2
  36. package/src/cli/schema-introspect.ts +33 -2
  37. package/src/cli/state-function-deps.ts +2 -1
  38. package/src/cli/lifecycle-verify.ts +0 -232
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ⚠ LOCK — which of a plan's statements build an index on a table that already holds rows.
3
+ *
4
+ * db:apply runs a plain `CREATE INDEX` inside its one transaction. PostgreSQL takes a SHARE lock
5
+ * on the table for the whole build, whatever its size: reads go on, INSERT/UPDATE/DELETE wait until
6
+ * the build finishes and the transaction commits. On a large table that is a write outage, so the
7
+ * plan says so, with the catalog's row estimate (`pg_class.reltuples`) to let the reader judge.
8
+ *
9
+ * Informational only: no --confirm, no exit code, and the plan is still "Safe to apply". Applying
10
+ * is unchanged. The estimate rides beside the snapshot (`rowEstimates`), never inside a table, so
11
+ * no fingerprint reads it.
12
+ *
13
+ * A table the same plan creates is not live, so it gets no line; an estimate of 0 is an empty table,
14
+ * no line; -1 is a table with data that was never analyzed, "row count unknown"; a read without
15
+ * estimates (an older ops Lambda) still warns, without a count.
16
+ */
17
+
18
+ import { statementBody } from './sql-statement';
19
+ import type { SchemaSnapshot } from './schema-introspect';
20
+
21
+ export interface LockWarning {
22
+ index: string;
23
+ /** Schema-qualified, as the snapshot names it. */
24
+ table: string;
25
+ /** The estimate; null when the table has data but was never analyzed; absent when not read. */
26
+ rows?: number | null;
27
+ }
28
+
29
+ const IDENT = '"(?:[^"]|"")+"|[A-Za-z_][\\w$]*';
30
+ const CREATE_INDEX = new RegExp(
31
+ `^CREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?!CONCURRENTLY\\b)(?:IF\\s+NOT\\s+EXISTS\\s+)?(${IDENT})\\s+ON\\s+(?:ONLY\\s+)?((?:${IDENT})(?:\\.(?:${IDENT}))?)`,
32
+ 'i',
33
+ );
34
+ const unquote = (part: string): string => (part.startsWith('"') ? part.slice(1, -1).replace(/""/g, '"') : part);
35
+ const qualified = (name: string): string => {
36
+ const parts = name.match(new RegExp(IDENT, 'g')) ?? [name];
37
+ const clean = parts.map(unquote);
38
+ return clean.length === 1 ? `public.${clean[0]}` : clean.join('.');
39
+ };
40
+
41
+ export function indexLockWarnings(statements: readonly string[], live: SchemaSnapshot): LockWarning[] {
42
+ const liveTables = new Set(live.tables.map((t) => t.table));
43
+ const out: LockWarning[] = [];
44
+ for (const statement of statements) {
45
+ const m = CREATE_INDEX.exec(statementBody(statement));
46
+ if (!m) continue;
47
+ const table = qualified(m[2]);
48
+ if (!liveTables.has(table)) continue;
49
+ const estimate = live.rowEstimates?.[table];
50
+ if (estimate === 0) continue;
51
+ out.push({ index: unquote(m[1]), table, ...(estimate === undefined ? {} : { rows: estimate < 0 ? null : estimate }) });
52
+ }
53
+ return out;
54
+ }
55
+
56
+ const display = (table: string): string => (table.startsWith('public.') ? table.slice('public.'.length) : table);
57
+
58
+ export function lockLines(warnings: readonly LockWarning[]): string[] {
59
+ return warnings.map((w) => {
60
+ const count = w.rows === undefined ? ''
61
+ : w.rows === null ? ' (row count unknown)'
62
+ : ` (about ${w.rows.toLocaleString('en-US')} ${w.rows === 1 ? 'row' : 'rows'})`;
63
+ return `⚠ LOCK — CREATE INDEX ${w.index} on ${display(w.table)} blocks writes to that table until it is built${count}.`;
64
+ });
65
+ }
package/src/cli/index.ts CHANGED
@@ -312,7 +312,7 @@ Usage:
312
312
  everystack db:reset [--stage <name>] Drop all schemas + re-run migrations (dev only)
313
313
  everystack db:psql --stage <name> Interactive ADMIN psql (IAM-gated; resolves the admin URL in-process)
314
314
  everystack db:psql [--stage <name>] -c <command> Run one query via Lambda (read-only). Runs as the SELECT-only 'reader' role when the stage has one (db:provision --with-reader + QUERY_DATABASE_URL wired to dbPlugin's queryDatabaseUrl); WITHOUT one it runs on the OPERATOR connection, fenced only by a read-only transaction — see docs/cli.md
315
- everystack db:doctor [--purge-after <days>] [--models <barrel>] [--stage <name>] [--direct | --database-url <api> [--admin-database-url <ops>]] Check the DB is least-privilege + RLS-subject (api vs operator connection). No flag = ops-Lambda venue (auto-falls to direct via the stage secrets if the handler has no dbPlugin). --direct = probe both connections from the stage's DATABASE_URL + ADMIN_DATABASE_URL secrets; --database-url = explicit local venue (a lone URL is probed as both)
315
+ everystack db:doctor [--destroy-after <days>] [--models <barrel>] [--stage <name>] [--direct | --database-url <api> [--admin-database-url <ops>]] Check the DB is least-privilege + RLS-subject (api vs operator connection). No flag = ops-Lambda venue (auto-falls to direct via the stage secrets if the handler has no dbPlugin). --direct = probe both connections from the stage's DATABASE_URL + ADMIN_DATABASE_URL secrets; --database-url = explicit local venue (a lone URL is probed as both)
316
316
  everystack db:provision --stage <name> [--direct | --database-url <url>] [--rotation-pairs] [--with-reader] Create the least-privilege role chain (idempotent). --rotation-pairs creates A/B pairs for live rotation. --with-reader adds the SELECT-only 'reader' login db:query runs as, grants it read on the declared schemas, and writes QUERY_DATABASE_URL (opt-in once, then kept converged on every re-provision)
317
317
  everystack db:provision --rotate --stage <name> [--slot api|ops|both] [--grace 60] Rotate credentials live (A/B role swap, no deploy, no downtime)
318
318
  everystack db:snapshot [--stage <name>] [--instance <id>] Take a physical RDS snapshot (instant DR point; RDS only — use db:backup for portable logical backups)
@@ -29,7 +29,8 @@ export function canonOfFunction(fn: FunctionDescriptor): FunctionCanon {
29
29
  language: fn.language, secdef: fn.security === 'definer',
30
30
  volatility: { immutable: 'i', stable: 's', volatile: 'v' }[fn.volatility],
31
31
  strict: false, leakproof: false,
32
- config: Array.isArray(fn.searchPath) ? [`search_path=${fn.searchPath.join(', ')}`] : [],
32
+ config: [...(Array.isArray(fn.searchPath) ? [`search_path=${fn.searchPath.join(', ')}`] : []),
33
+ ...(fn.lockTimeout === undefined ? [] : [`lock_timeout=${fn.lockTimeout}`])],
33
34
  owner: fn.owner ?? '(ambient)', execute: [...execute].sort(),
34
35
  // renderFunction adds exactly one newline on each side of the authored body.
35
36
  body: `\n${fn.body}\n`,