@everystack/cli 0.4.23 → 0.4.24
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/package.json +1 -1
- package/src/cli/commands/db-reconcile.ts +19 -0
- package/src/cli/derived-plan.ts +50 -0
- package/src/cli/index.ts +1 -1
package/package.json
CHANGED
|
@@ -411,6 +411,24 @@ export async function dbReconcileCommand(flags: Record<string, string>): Promise
|
|
|
411
411
|
info(`${declared.objects.length} declared derived object(s) from the models barrel.`);
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
// --only <a,b>: restrict the reconcile to the named identities (surgical). With --rebuild it is
|
|
415
|
+
// the recovery path for a self-consistent-but-wrong provenance row (a mistaken --rebaseline).
|
|
416
|
+
const only = flags.only ? flags.only.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
|
|
417
|
+
// An empty/whitespace --only would silently degrade to a FULL reconcile — refuse it, since the
|
|
418
|
+
// operator clearly meant to scope the run.
|
|
419
|
+
if (flags.only !== undefined && !only?.length) {
|
|
420
|
+
fail('--only was given but names no identities — pass full schema.name objects (e.g. --only stats_view.my_mv), or drop the flag for a full reconcile.');
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
if (only?.length) {
|
|
424
|
+
const declaredIds = new Set((declared?.objects ?? []).map((o) => o.identity));
|
|
425
|
+
const unknown = only.filter((id) => !declaredIds.has(id));
|
|
426
|
+
if (unknown.length) {
|
|
427
|
+
warn(`--only names ${unknown.length} identity(ies) not in the declared derived layer: ${unknown.join(', ')} — a full schema.name is expected (e.g. stats_view.college_player_epa_context). They will match nothing.`);
|
|
428
|
+
}
|
|
429
|
+
info(`--only: restricting the reconcile to ${only.join(', ')}${flags.rebuild === 'true' ? ' (forced rebuild from source)' : ''}.`);
|
|
430
|
+
}
|
|
431
|
+
|
|
414
432
|
const reconcileOptions = {
|
|
415
433
|
apply,
|
|
416
434
|
declared: declared?.objects,
|
|
@@ -419,6 +437,7 @@ export async function dbReconcileCommand(flags: Record<string, string>): Promise
|
|
|
419
437
|
rebaseline: flags.rebaseline === 'true',
|
|
420
438
|
rebuild: flags.rebuild === 'true',
|
|
421
439
|
overwriteDrift: flags['overwrite-drift'] === 'true',
|
|
440
|
+
only,
|
|
422
441
|
actor: process.env.USER ?? null,
|
|
423
442
|
gitRef: currentGitRef(),
|
|
424
443
|
};
|
package/src/cli/derived-plan.ts
CHANGED
|
@@ -126,6 +126,19 @@ export interface ReconcileOptions {
|
|
|
126
126
|
* their provenance identity migrates with them.
|
|
127
127
|
*/
|
|
128
128
|
renamedTables?: Record<string, string>;
|
|
129
|
+
/**
|
|
130
|
+
* Restrict the reconcile to these identities (full `schema.name`) — a surgical run that
|
|
131
|
+
* considers ONLY the named objects for create/replace/rebuild/drop/prune. Everything else is
|
|
132
|
+
* left untouched. The dependency cascade still applies: rebuilding a named relation pulls its
|
|
133
|
+
* live dependents in (they must rebuild with it) or blocks if one has no source.
|
|
134
|
+
*
|
|
135
|
+
* Combined with `rebuild`, this is the RECOVERY path for a self-consistent-but-WRONG
|
|
136
|
+
* provenance row — the state a mistaken `--rebaseline` leaves, where the source hash was
|
|
137
|
+
* re-recorded against a stale live def so both `srcChanged` and `liveChanged` read false and
|
|
138
|
+
* every normal verb no-ops. `--rebuild --only <id>` forces that object to rebuild from source
|
|
139
|
+
* regardless of the hash bookkeeping, then re-records honest provenance from the fresh object.
|
|
140
|
+
*/
|
|
141
|
+
only?: string[];
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
const isRelation = (kind: DerivedKind): boolean => kind !== 'function';
|
|
@@ -156,6 +169,12 @@ export function planReconcile(
|
|
|
156
169
|
// Every provenance consumer below reads the POST-MIGRATION view.
|
|
157
170
|
const provRows = [...provById.values()];
|
|
158
171
|
|
|
172
|
+
// --only: restrict every primary decision (create/replace/rebuild/drop/prune) to these
|
|
173
|
+
// identities. The catalog/source/provenance maps above stay FULL so the dependency cascade
|
|
174
|
+
// can still pull a named relation's dependents in — the restriction is on what the operator
|
|
175
|
+
// TARGETS, not on what the graph is allowed to see.
|
|
176
|
+
const only = options.only && options.only.length ? new Set(options.only) : null;
|
|
177
|
+
|
|
159
178
|
const skipped: string[] = [];
|
|
160
179
|
const drift: DriftFinding[] = [];
|
|
161
180
|
const needsBaseline: string[] = [];
|
|
@@ -184,6 +203,10 @@ export function planReconcile(
|
|
|
184
203
|
const extraWarnings: string[] = [];
|
|
185
204
|
|
|
186
205
|
for (const src of source.objects) {
|
|
206
|
+
// --only restricts the primary decisions to the named identities; the rest are left
|
|
207
|
+
// untouched (they are not skips — they were never in scope, so they get no plan entry).
|
|
208
|
+
if (only && !only.has(src.identity)) continue;
|
|
209
|
+
|
|
187
210
|
const liveObj = liveById.get(src.identity);
|
|
188
211
|
const prov = provById.get(src.identity);
|
|
189
212
|
|
|
@@ -193,6 +216,10 @@ export function planReconcile(
|
|
|
193
216
|
if (src.kind === 'sql') {
|
|
194
217
|
if (!prov) {
|
|
195
218
|
create.set(src.identity, 'new in source (provenance-tracked kind — invisible to the catalog)');
|
|
219
|
+
} else if (options.rebuild && only) {
|
|
220
|
+
// Forced recovery: rebuild through the recorded drop even when the source hash matches.
|
|
221
|
+
sqlDrops.set(src.identity, { dropSql: prov.dropSql ?? src.drop ?? '', reason: 'forced rebuild from source (--rebuild --only)' });
|
|
222
|
+
create.set(src.identity, 'forced rebuild from source (--rebuild --only, through the recorded drop)');
|
|
196
223
|
} else if (src.hash !== prov.srcHash) {
|
|
197
224
|
sqlDrops.set(src.identity, { dropSql: prov.dropSql ?? src.drop ?? '', reason: 'source changed' });
|
|
198
225
|
create.set(src.identity, 'source changed (rebuilt through the recorded drop)');
|
|
@@ -224,6 +251,27 @@ export function planReconcile(
|
|
|
224
251
|
continue;
|
|
225
252
|
}
|
|
226
253
|
|
|
254
|
+
// Forced recovery (--rebuild --only): rebuild this provenanced object from source even when
|
|
255
|
+
// the hashes show no diff. This is the clean exit from a self-consistent-but-WRONG provenance
|
|
256
|
+
// row — a mistaken --rebaseline re-records the source hash against the stale live def, so both
|
|
257
|
+
// srcChanged and liveChanged read false below and every normal verb no-ops. The dependency
|
|
258
|
+
// cascade recreates any live dependents; the post-batch introspection re-records honest
|
|
259
|
+
// provenance. Gated on `only` so a bare --rebuild never becomes an implicit rebuild-the-world.
|
|
260
|
+
if (options.rebuild && only) {
|
|
261
|
+
// If the live object was genuinely hand-edited (drift), record it before overwriting — the
|
|
262
|
+
// same audit the --overwrite-drift path keeps, so a forced rebuild never silently erases
|
|
263
|
+
// what it clobbered.
|
|
264
|
+
if (liveObj.defHash !== prov.defHash) {
|
|
265
|
+
drift.push({
|
|
266
|
+
identity: src.identity,
|
|
267
|
+
detail: 'live definition differs from what the reconciler applied — rebuilt from source (--rebuild --only overwrote it)',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (isRelation(src.kind)) rebuild.set(src.identity, 'forced rebuild from source (--rebuild --only)');
|
|
271
|
+
else fnReplace.set(src.identity, 'forced replace from source (--rebuild --only)');
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
227
275
|
const srcChanged = src.hash !== prov.srcHash;
|
|
228
276
|
const liveChanged = liveObj.defHash !== prov.defHash;
|
|
229
277
|
|
|
@@ -253,12 +301,14 @@ export function planReconcile(
|
|
|
253
301
|
}
|
|
254
302
|
|
|
255
303
|
for (const liveObj of live.objects) {
|
|
304
|
+
if (only && !only.has(liveObj.identity)) continue;
|
|
256
305
|
if (srcById.has(liveObj.identity)) continue;
|
|
257
306
|
if (provById.has(liveObj.identity)) drop.set(liveObj.identity, 'removed from source');
|
|
258
307
|
else unmanaged.push(liveObj.identity);
|
|
259
308
|
}
|
|
260
309
|
|
|
261
310
|
for (const prov of provRows) {
|
|
311
|
+
if (only && !only.has(prov.identity)) continue;
|
|
262
312
|
if (srcById.has(prov.identity) || liveById.has(prov.identity)) continue;
|
|
263
313
|
// A removed 'sql' object is invisible to the catalog — pruning would silently orphan
|
|
264
314
|
// it in the database. The recorded drop_sql removes it (the C1 closure); only a
|
package/src/cli/index.ts
CHANGED
|
@@ -371,7 +371,7 @@ Usage:
|
|
|
371
371
|
everystack db:pull [--stage <name> | --database-url <url>] [--schema public] [--out <dir | file.ts>] [--derived-out <file.ts>] [--abilities public-read] Introspect the live DB → render field() Models (the brownfield on-ramp). --out <dir> writes one file per model + index.ts (the default shape); --out <file.ts> writes a single module; stdout otherwise. --derived-out <file.ts> extracts the derived layer (descriptors + sequences) as its own self-contained module — alone it leaves the models untouched (the hand-maintained-barrel splice); with --out the models render omits the now-external derived layer. Every model scaffolds its authz decision as comments (db:check fails until authored); --abilities public-read stamps the common stanza (public read, admin write) uncommented — explicit generated code, never a runtime default. --matviews-as-tables renders every matview as defineMaterializedTable with INTROSPECTED fields (the canonical-sync flip: a pipeline-owned table everystack migrates but never refreshes) — names land in an exported materializedTables array to spread into your models; fields come back nullable/unkeyed (matviews carry no PK/NOT NULL) — tighten on review; add --suggest-keys to probe the LIVE rows for functionally-unique columns (one scan per matview) and surface each as a commented .primaryKey() suggestion. docs/derived-objects.md#flipping-a-matview-to-a-materialized-table---matviews-as-tables
|
|
372
372
|
Both introspect via the deployed ops Lambda by default; --database-url (or an inherited DATABASE_URL) connects directly — for a schema that exists only on a local Postgres.
|
|
373
373
|
everystack db:fingerprint [--stage <name> | --database-url <url>] [--models <barrel>] [--json] Content-address the live base schema (tables+constraints+authz) and compare against the models — MATCH/MISMATCH (exit 1), plus the unfingerprinted-objects report
|
|
374
|
-
everystack db:reconcile [--stage <name> | --database-url <url>] [--apply] [--check] [--baseline] [--rebuild] [--overwrite-drift] [--json] Reconcile the derived layer (functions/views/matviews/triggers) against the DECLARED descriptors (defineView/defineMaterializedView/defineFunction/defineSql/trigger() on models, from the barrel) — the single home (db/sql is retired; leftover .sql files fail with the migration path): plan with rebuild-cost estimates by default; --check is the CI gate; --apply executes (atomic — DDL + provenance in one transaction) and records provenance + schema_log; --apply --stage runs credential-free in the ops Lambda (no admin URL on the deployer, the db:apply twin), --apply --database-url runs direct. Hand-edits are drift (never overwritten silently). First contact with existing objects: --baseline TRUSTS live == source (records provenance, verifies nothing), --rebuild GUARANTEES it (drop+create from source). They are mutually exclusive.
|
|
374
|
+
everystack db:reconcile [--stage <name> | --database-url <url>] [--apply] [--check] [--baseline] [--rebuild] [--overwrite-drift] [--only a,b] [--json] Reconcile the derived layer (functions/views/matviews/triggers) against the DECLARED descriptors (defineView/defineMaterializedView/defineFunction/defineSql/trigger() on models, from the barrel) — the single home (db/sql is retired; leftover .sql files fail with the migration path): plan with rebuild-cost estimates by default; --check is the CI gate; --apply executes (atomic — DDL + provenance in one transaction) and records provenance + schema_log; --apply --stage runs credential-free in the ops Lambda (no admin URL on the deployer, the db:apply twin), --apply --database-url runs direct. Hand-edits are drift (never overwritten silently). First contact with existing objects: --baseline TRUSTS live == source (records provenance, verifies nothing), --rebuild GUARANTEES it (drop+create from source). They are mutually exclusive. --only <schema.name,…> restricts the run to the named objects (surgical); with --rebuild it FORCES those to rebuild from source even when the hashes show no diff — the recovery exit when a mistaken --rebaseline left a self-consistent-but-wrong provenance row (the dependency cascade rebuilds their live dependents).
|
|
375
375
|
everystack db:refresh [--stage <name> | --database-url <url> | --direct] [--only a,b] [--list] Refresh the declared materialized views in dependency order, credential-free. --stage runs the whole refresh in the ops Lambda on the operator connection (no URL on the operator's machine — the data-lane twin of the reconcile lane); --database-url/--direct refresh over a direct connection (dev); --only refreshes a named subset (full identity or bare name); --list previews the order without connecting. Plain REFRESH (ACCESS EXCLUSIVE); fail-fast, idempotent to re-run.
|
|
376
376
|
everystack db:sync [--database-url <url>] [--models <barrel>] [--schema-out <file.ts>] [--allow-drops] [--overwrite-drift] [--baseline] [--json] Make the database match your checkout — one verb, both layers: apply the state diff (tables+authz, one transaction, verified by re-diff), reconcile the derived layer against the declared descriptors, report the resulting fingerprint vs the models' declared one. Dev databases only (direct connection required); DROPs held back unless --allow-drops; derived drift refuses unless --overwrite-drift; exit 1 when not converged
|
|
377
377
|
everystack db:diff --from-models <barrel> [--to-models db/models/index.ts] [--allow-drops] [--check] [--json] The state edge between two declared states, NO database: the SQL db:generate would produce, computed purely — CI plan previews (--check exits 1 on a non-empty edge) and computed rollbacks (swap the flags)
|