@databricks-solutions/lakebase-scm-utils 0.3.9 → 0.3.10

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 (56) hide show
  1. package/dist/scripts/index.cjs +59 -24
  2. package/dist/scripts/index.js +59 -24
  3. package/dist/scripts/lakebase/branch.cli.cjs +55 -10
  4. package/dist/scripts/lakebase/branch.cli.js +55 -10
  5. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +59 -24
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +59 -24
  7. package/dist/scripts/lakebase/collapse-heads.cli.cjs +16 -14
  8. package/dist/scripts/lakebase/collapse-heads.cli.js +16 -14
  9. package/dist/scripts/lakebase/cut-backup.cli.cjs +43 -10
  10. package/dist/scripts/lakebase/cut-backup.cli.js +43 -10
  11. package/dist/scripts/lakebase/cut-tier.cli.cjs +43 -10
  12. package/dist/scripts/lakebase/cut-tier.cli.js +43 -10
  13. package/dist/scripts/lakebase/detect-language.cli.cjs +16 -14
  14. package/dist/scripts/lakebase/detect-language.cli.js +16 -14
  15. package/dist/scripts/lakebase/doctor.cli.cjs +16 -14
  16. package/dist/scripts/lakebase/doctor.cli.js +16 -14
  17. package/dist/scripts/lakebase/get-connection.cli.cjs +16 -14
  18. package/dist/scripts/lakebase/get-connection.cli.js +16 -14
  19. package/dist/scripts/lakebase/index.cjs +59 -24
  20. package/dist/scripts/lakebase/index.d.cts +23 -0
  21. package/dist/scripts/lakebase/index.d.ts +23 -0
  22. package/dist/scripts/lakebase/index.js +59 -24
  23. package/dist/scripts/lakebase/infra-runner.cli.cjs +16 -14
  24. package/dist/scripts/lakebase/infra-runner.cli.js +16 -14
  25. package/dist/scripts/lakebase/new-migration.cli.cjs +16 -14
  26. package/dist/scripts/lakebase/new-migration.cli.js +16 -14
  27. package/dist/scripts/lakebase/schema-diff.cli.cjs +16 -14
  28. package/dist/scripts/lakebase/schema-diff.cli.js +16 -14
  29. package/dist/scripts/lakebase/schema-migrate.cli.cjs +16 -14
  30. package/dist/scripts/lakebase/schema-migrate.cli.js +16 -14
  31. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +59 -24
  32. package/dist/scripts/lakebase/scm-claim-feature.cli.js +59 -24
  33. package/dist/scripts/lakebase/scm-doctor.cli.cjs +59 -24
  34. package/dist/scripts/lakebase/scm-doctor.cli.js +59 -24
  35. package/dist/scripts/lakebase/scm-merge.cli.cjs +16 -14
  36. package/dist/scripts/lakebase/scm-merge.cli.js +16 -14
  37. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +16 -14
  38. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +16 -14
  39. package/dist/scripts/lakebase/scm-recover-orphans.cli.cjs +43 -10
  40. package/dist/scripts/lakebase/scm-recover-orphans.cli.js +43 -10
  41. package/dist/scripts/lakebase/snapshot.cli.cjs +418 -0
  42. package/dist/scripts/lakebase/snapshot.cli.d.cts +1 -0
  43. package/dist/scripts/lakebase/snapshot.cli.d.ts +1 -0
  44. package/dist/scripts/lakebase/snapshot.cli.js +395 -0
  45. package/dist/scripts/util/index.cjs +2 -2
  46. package/dist/scripts/util/index.js +2 -2
  47. package/package.json +2 -1
  48. package/scripts/lakebase/branch-create.ts +93 -14
  49. package/scripts/lakebase/branch.cli.ts +15 -0
  50. package/scripts/lakebase/get-connection.ts +29 -12
  51. package/scripts/lakebase/snapshot.cli.ts +117 -0
  52. package/scripts/lakebase/snapshot.ts +85 -0
  53. package/templates/project/common/scripts/run-tests.sh +43 -2
  54. package/templates/project/python/tests/conftest.py +22 -0
  55. package/templates/project/common/tests/e2e/__pycache__/conftest.cpython-314.pyc +0 -0
  56. package/templates/project/python/tests/__pycache__/conftest.cpython-314.pyc +0 -0
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // CLI for Lakebase SNAPSHOTS: `lakebase-snapshot <create|delete|list> ...`.
3
+ //
4
+ // A snapshot is an immutable point-in-time copy of a branch. Capture the
5
+ // committed schema once, then branch copy-on-write from it per verify cycle
6
+ // (`lakebase-branch create --from-snapshot <id>`) instead of re-running
7
+ // `alembic upgrade head`. Prints JSON on stdout, errors on stderr.
8
+ // 0 = success 1 = runtime error 2 = usage error
9
+ import { createSnapshot, deleteSnapshot, listSnapshots } from "./snapshot.js";
10
+
11
+ interface Args {
12
+ subcommand?: string;
13
+ instance?: string;
14
+ snapshot?: string;
15
+ branch?: string;
16
+ atTime?: string;
17
+ atLsn?: string;
18
+ ttl?: string;
19
+ noExpiry?: boolean;
20
+ host?: string;
21
+ pretty?: boolean;
22
+ help?: boolean;
23
+ }
24
+
25
+ function parseArgs(argv: string[]): Args {
26
+ const out: Args = {};
27
+ let i = 0;
28
+ if (argv[0] && !argv[0].startsWith("-")) {
29
+ out.subcommand = argv[0];
30
+ i = 1;
31
+ }
32
+ for (; i < argv.length; i++) {
33
+ switch (argv[i]) {
34
+ case "--instance": out.instance = argv[++i]; break;
35
+ case "--snapshot": out.snapshot = argv[++i]; break;
36
+ case "--branch": out.branch = argv[++i]; break;
37
+ case "--at-time": out.atTime = argv[++i]; break;
38
+ case "--at-lsn": out.atLsn = argv[++i]; break;
39
+ case "--ttl": out.ttl = argv[++i]; break;
40
+ case "--no-expiry": out.noExpiry = true; break;
41
+ case "--host": out.host = argv[++i]; break;
42
+ case "--pretty": out.pretty = true; break;
43
+ case "--help": case "-h": out.help = true; break;
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ const USAGE = `usage: lakebase-snapshot <create|delete|list> --instance <project> [flags]
50
+ create --snapshot <id> --branch <src> [--at-time <iso> | --at-lsn <lsn>] [--ttl <s> | --no-expiry]
51
+ delete --snapshot <id>
52
+ list
53
+ Then branch from it: lakebase-branch create --branch <v> --from-snapshot <id> --instance <project>`;
54
+
55
+ function out(result: unknown, pretty: boolean): void {
56
+ process.stdout.write(JSON.stringify(result, null, pretty ? 2 : undefined) + "\n");
57
+ }
58
+ function need(a: Args, keys: (keyof Args)[]): boolean {
59
+ const missing = keys.filter((k) => a[k] === undefined || a[k] === "");
60
+ if (missing.length) {
61
+ process.stderr.write(`missing ${missing.map((k) => "--" + String(k)).join(", ")}\n${USAGE}\n`);
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ async function main(): Promise<number> {
68
+ const a = parseArgs(process.argv.slice(2));
69
+ if (a.help || !a.subcommand) {
70
+ process.stderr.write(USAGE + "\n");
71
+ return a.help ? 0 : 2;
72
+ }
73
+ switch (a.subcommand) {
74
+ case "create": {
75
+ if (!need(a, ["instance", "snapshot", "branch"])) return 2;
76
+ const raw = await createSnapshot({
77
+ instance: a.instance!,
78
+ snapshot: a.snapshot!,
79
+ sourceBranch: a.branch!,
80
+ sourceBranchTime: a.atTime,
81
+ sourceBranchLsn: a.atLsn,
82
+ ttl: a.ttl,
83
+ noExpiry: a.noExpiry,
84
+ host: a.host,
85
+ });
86
+ out({ created: a.snapshot, raw: raw.trim() || undefined }, !!a.pretty);
87
+ return 0;
88
+ }
89
+ case "delete": {
90
+ if (!need(a, ["instance", "snapshot"])) return 2;
91
+ await deleteSnapshot({ instance: a.instance!, snapshot: a.snapshot!, host: a.host });
92
+ out({ deleted: a.snapshot }, !!a.pretty);
93
+ return 0;
94
+ }
95
+ case "list": {
96
+ if (!need(a, ["instance"])) return 2;
97
+ const raw = await listSnapshots({ instance: a.instance!, host: a.host });
98
+ try {
99
+ out(JSON.parse(raw), !!a.pretty);
100
+ } catch {
101
+ process.stdout.write(raw);
102
+ }
103
+ return 0;
104
+ }
105
+ default:
106
+ process.stderr.write(`unknown subcommand '${a.subcommand}'\n${USAGE}\n`);
107
+ return 2;
108
+ }
109
+ }
110
+
111
+ main().then(
112
+ (code) => process.exit(code),
113
+ (err) => {
114
+ process.stderr.write(String(err instanceof Error ? err.message : err) + "\n");
115
+ process.exit(1);
116
+ },
117
+ );
@@ -0,0 +1,85 @@
1
+ // Lakebase SNAPSHOT primitives — the fast verify-branch substrate.
2
+ //
3
+ // A snapshot is an immutable point-in-time copy of a branch's data. Capture the
4
+ // committed schema ONCE (create), then branch copy-on-write from it per verify
5
+ // cycle (createBranch({ fromSnapshot })) instead of forking + `alembic upgrade
6
+ // head` every time. Because the snapshot is a fixed point, the per-cycle branch
7
+ // never goes stale. "Restore" is not an in-place op in Lakebase — you restore by
8
+ // branching from the snapshot (see createBranch's fromSnapshot path).
9
+ //
10
+ // API (verified): POST .../snapshots?snapshot_id=<id> with spec.source_branch
11
+ // (+ optional source_branch_time | source_branch_lsn for PITR, + one of
12
+ // no_expiry | ttl | expire_time). Mirrors the one-databricks-CLI-wrapper rule.
13
+ import { runDatabricks } from "./databricks-cli.js";
14
+ import { projectPath } from "./branch-utils.js";
15
+ import { KIT_TIMEOUTS } from "./kit-config.js";
16
+
17
+ /** Full branch resource path from a bare leaf or an already-full path. */
18
+ function branchResource(instance: string, branch: string): string {
19
+ return branch.includes("/") ? branch : `${projectPath(instance)}/branches/${branch}`;
20
+ }
21
+
22
+ /** Full snapshot resource path from a bare id or an already-full path. */
23
+ function snapshotResource(instance: string, snapshot: string): string {
24
+ return snapshot.includes("/") ? snapshot : `${projectPath(instance)}/snapshots/${snapshot}`;
25
+ }
26
+
27
+ export interface SnapshotSpec {
28
+ source_branch: string;
29
+ source_branch_time?: string;
30
+ source_branch_lsn?: string;
31
+ no_expiry?: boolean;
32
+ ttl?: string;
33
+ }
34
+
35
+ export interface CreateSnapshotArgs {
36
+ instance: string;
37
+ /** Client-chosen snapshot id (becomes the resource leaf). */
38
+ snapshot: string;
39
+ /** Branch to snapshot — bare leaf (e.g. "staging") or full path. */
40
+ sourceBranch: string;
41
+ /** PITR: snapshot the branch AS OF this ISO timestamp (source_branch_time). */
42
+ sourceBranchTime?: string;
43
+ /** PITR: snapshot AS OF this LSN (source_branch_lsn). Exclusive with sourceBranchTime. */
44
+ sourceBranchLsn?: string;
45
+ /** TTL string ("604800s"). Mutually exclusive with noExpiry. */
46
+ ttl?: string;
47
+ /** Never auto-expire. Default true when ttl is unset (the API requires one). */
48
+ noExpiry?: boolean;
49
+ host?: string;
50
+ }
51
+
52
+ /** Build the create-snapshot `spec` (pure — unit-testable without the CLI). */
53
+ export function buildSnapshotSpec(args: CreateSnapshotArgs): SnapshotSpec {
54
+ if (args.sourceBranchTime && args.sourceBranchLsn) {
55
+ throw new Error("sourceBranchTime and sourceBranchLsn are mutually exclusive — pass at most one point-in-time.");
56
+ }
57
+ if (args.ttl && args.noExpiry === true) {
58
+ throw new Error(`Cannot set both ttl ("${args.ttl}") and noExpiry: true — they are mutually exclusive.`);
59
+ }
60
+ const spec: SnapshotSpec = { source_branch: branchResource(args.instance, args.sourceBranch) };
61
+ if (args.sourceBranchTime) spec.source_branch_time = args.sourceBranchTime;
62
+ else if (args.sourceBranchLsn) spec.source_branch_lsn = args.sourceBranchLsn;
63
+ if (args.ttl) spec.ttl = args.ttl;
64
+ else if (args.noExpiry ?? true) spec.no_expiry = true;
65
+ return spec;
66
+ }
67
+
68
+ /** Create an immutable snapshot of a branch (waits for the operation to complete). */
69
+ export async function createSnapshot(args: CreateSnapshotArgs): Promise<string> {
70
+ const spec = buildSnapshotSpec(args);
71
+ return runDatabricks(
72
+ ["postgres", "create-snapshot", projectPath(args.instance), args.snapshot, "--json", JSON.stringify({ spec })],
73
+ { host: args.host, timeout: KIT_TIMEOUTS.cliCreateBranch },
74
+ );
75
+ }
76
+
77
+ export async function deleteSnapshot(args: { instance: string; snapshot: string; host?: string }): Promise<string> {
78
+ return runDatabricks(["postgres", "delete-snapshot", snapshotResource(args.instance, args.snapshot)], {
79
+ host: args.host,
80
+ });
81
+ }
82
+
83
+ export async function listSnapshots(args: { instance: string; host?: string }): Promise<string> {
84
+ return runDatabricks(["postgres", "list-snapshots", projectPath(args.instance), "-o", "json"], { host: args.host });
85
+ }
@@ -155,6 +155,27 @@ else
155
155
  exit 1
156
156
  fi
157
157
 
158
+ # NOTE: a per-cycle client AC-scope (vitest -t / playwright --grep from SFTDD_TEST_IDS) was tried and
159
+ # REVERTED (live-proven harmful, 2026-10-24): scoping the per-cycle client pass to the current AC's
160
+ # @T specs stops the FULL client suite from running per cycle, which is what detects SUPERSESSION — a
161
+ # prior test asserting an element the current story removed (e.g. S2 split the combined
162
+ # `tracking-not-tracked` cell into batch/serial). Without that in-cycle RED the driver never refactors
163
+ # the superseded test, so it surfaces LATER as a deploy-verify HALT instead of a smooth in-cycle
164
+ # refactor. The client Vitest is cheap (jsdom) and the expensive Playwright E2E is already cost-gated
165
+ # by cycle layer below, so the full per-cycle client run stays — it is load-bearing, not waste.
166
+ #
167
+ # EXCEPTION — #2 parallel-lane scope (gated): in a SCOPED-LANE drive (CONSORT_SCOPE_AC set, i.e. a
168
+ # parallel per-AC lane / the contract story's serial-first AC), the cross-story supersession is owned by
169
+ # the DETERMINISTIC hoist sweep, NOT by this reactive full-client-run. So there the client pass is scoped
170
+ # to THIS AC's @T specs (it must not re-run the cross-story suite + re-trigger supersession wave-by-wave
171
+ # on the lanes' critical path). FLEET drives (no CONSORT_SCOPE_AC) are UNCHANGED — full client suite,
172
+ # supersession detection intact. The gate is what makes the reverted scope safe to re-introduce here.
173
+ SFTDD_CLIENT_GREP=""
174
+ if [ -n "${CONSORT_SCOPE_AC:-}" ] && [ -n "${SFTDD_TEST_IDS:-}" ]; then
175
+ SFTDD_CLIENT_GREP="@($(echo "$SFTDD_TEST_IDS" | tr -s ' ' '|' | sed 's/^|//;s/|$//'))"
176
+ echo "Scoped-lane client pass (CONSORT_SCOPE_AC=$CONSORT_SCOPE_AC): client tests scoped to $SFTDD_CLIENT_GREP"
177
+ fi
178
+
158
179
  # React SPA client unit tests (Vitest + Testing Library). Only on a full run
159
180
  # (no positional path arg, so a per-cycle backend-layer invocation does not drag
160
181
  # in the client suite), and only when a client/ workspace is present. This is the
@@ -192,7 +213,11 @@ if [ "$#" -eq 0 ] && [ -f "$REPO_ROOT/client/package.json" ]; then
192
213
  echo "Running client strict typecheck (tsc --noEmit, matching CI's build)..."
193
214
  ( cd "$REPO_ROOT/client" && npm run typecheck --if-present )
194
215
  echo "Running client unit tests (Vitest)..."
195
- ( cd "$REPO_ROOT/client" && npm test )
216
+ if [ -n "$SFTDD_CLIENT_GREP" ]; then
217
+ ( cd "$REPO_ROOT/client" && npm test -- -t "$SFTDD_CLIENT_GREP" )
218
+ else
219
+ ( cd "$REPO_ROOT/client" && npm test )
220
+ fi
196
221
  fi
197
222
 
198
223
  # React SPA client E2E (Playwright). The client's end-to-end suite (client/tests/e2e/*.spec.ts) MUST run
@@ -273,7 +298,23 @@ if [ "$#" -eq 0 ] && [ -d "$REPO_ROOT/client/tests/e2e" ] \
273
298
  # (old `!CI` and new `!!BASE_URL` alike) so the verify never reuses a stale
274
299
  # server bound to an unmigrated branch (issue #197's false-500 misdiagnosis).
275
300
  ensure_local_e2e_ports
276
- ( cd "$REPO_ROOT/client" && ./node_modules/.bin/playwright install chromium && CI=1 npm run test:e2e )
301
+ e2e_rc=0
302
+ # #2 scoped-lane: scope the E2E to THIS AC's @T specs (gated on CONSORT_SCOPE_AC; fleet runs full).
303
+ if [ -n "$SFTDD_CLIENT_GREP" ]; then
304
+ ( cd "$REPO_ROOT/client" && ./node_modules/.bin/playwright install chromium && CI=1 npm run test:e2e -- --grep "$SFTDD_CLIENT_GREP" ) || e2e_rc=$?
305
+ else
306
+ ( cd "$REPO_ROOT/client" && ./node_modules/.bin/playwright install chromium && CI=1 npm run test:e2e ) || e2e_rc=$?
307
+ fi
308
+ # #7 within-cycle reap: Playwright's webServer SHOULD self-tear-down, but a killed/detached run
309
+ # leaves a uvicorn (+ vite) on the E2E ports that otherwise lingers until the NEXT cycle's
310
+ # scan-reaper — so a heavy-E2E cycle's repeated runs pile up. Reap them here, right after the run.
311
+ # NEVER when BASE_URL is set (the deploy gate owns that server, same guard as ensure_local_e2e_ports).
312
+ if [ -z "${BASE_URL:-}" ] && command -v lsof >/dev/null 2>&1; then
313
+ for p in "${E2E_BACKEND_PORT:-}" "${E2E_CLIENT_PORT:-}"; do
314
+ [ -n "$p" ] && lsof -ti tcp:"$p" 2>/dev/null | xargs kill 2>/dev/null || true
315
+ done
316
+ fi
317
+ [ "$e2e_rc" -eq 0 ] || exit "$e2e_rc"
277
318
  fi
278
319
  fi
279
320
 
@@ -270,6 +270,28 @@ def pytest_collection_modifyitems(config, items):
270
270
  import os
271
271
  import re
272
272
 
273
+ # DEFER superseded cross-story tests off the serial-first AC's verify (SFTDD_DEFER_FILES, set
274
+ # by the deferred serial-first AC of a parallel-lane fan-out). These prior-story tests fail only
275
+ # because THIS AC's drop migration superseded them; the coordinator's supersession sweep retargets
276
+ # them ONCE right after this AC, and the final feature-deploy verify (no defer) re-runs them
277
+ # retargeted. Deselected by FILE because the migration pass runs them regardless of the testid
278
+ # scope below, and they are typically cross-story / untagged (so always-KEPT by that scope).
279
+ defer_raw = (os.environ.get("SFTDD_DEFER_FILES") or "").strip()
280
+ if defer_raw:
281
+ defer_files = {f for f in re.split(r"[,\s]+", defer_raw) if f.endswith(".py")}
282
+ if defer_files:
283
+ kept, dropped = [], []
284
+ for item in items:
285
+ nodefile = item.nodeid.split("::", 1)[0].replace(os.sep, "/")
286
+ fspath = str(getattr(item, "fspath", "") or "").replace(os.sep, "/")
287
+ if any(nodefile == df or nodefile.endswith("/" + df) or fspath.endswith("/" + df) for df in defer_files):
288
+ dropped.append(item)
289
+ else:
290
+ kept.append(item)
291
+ if dropped:
292
+ config.hook.pytest_deselected(items=dropped)
293
+ items[:] = kept
294
+
273
295
  raw = (os.environ.get("SFTDD_TEST_IDS") or "").strip()
274
296
  if not raw:
275
297
  return