@thomasfosterau/effect-drizzle-ext-pg 0.0.0-stage → 0.1.0

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/src/testing.ts ADDED
@@ -0,0 +1,447 @@
1
+ /**
2
+ * @copyright Thomas Foster
3
+ *
4
+ * The integration-test transaction runner: every spec runs inside a real
5
+ * database transaction that is ALWAYS rolled back, so a suite needs no
6
+ * truncation, no fixture reset and no ordering discipline.
7
+ *
8
+ * The mechanics are the ones the consuming applications carry today, with
9
+ * three things removed because they are the caller's, not the library's:
10
+ * the viewer services a spec layers on top, the proxy-client variants an
11
+ * application's own deployment shape needs, and the resolution of the test
12
+ * database URL (the caller passes it — see {@link testDatabaseLayer}).
13
+ *
14
+ * The shape is:
15
+ *
16
+ * 1. `db.transaction(...)` opens an OUTER transaction whose only job is the
17
+ * always-rollback, and whose handle is discarded.
18
+ * 2. The spec's effect is wrapped in the application's own `transact`, so the
19
+ * spec opens its own transaction exactly as a request handler does in
20
+ * production (website #1883) — a SAVEPOINT on the outer one. Binding
21
+ * `CurrentTransaction` from the harness instead would work and touch far
22
+ * fewer lines, but it would make the specs the one place in the system
23
+ * where a write runs without anything having called `transact` — precisely
24
+ * the code path they exist to exercise.
25
+ * 3. The inner `Exit` is captured, then a sentinel failure forces the outer
26
+ * transaction to roll back.
27
+ * 4. The captured `Exit` is projected back to a Promise (success resolves,
28
+ * failure throws the underlying cause) so ordinary
29
+ * `await expect(...).rejects` assertions keep working.
30
+ *
31
+ * The library owns no schema, so a spec creates the `TEMPORARY` tables it
32
+ * needs inside that transaction: DDL is transactional in PostgreSQL, so the
33
+ * always-rollback disposes of them with everything else.
34
+ *
35
+ * The database the runner connects to is provisioned by
36
+ * {@link provisionTestDatabase} (defined in `./provisioning.ts`, re-exported
37
+ * here): an explicit URL, else a fresh migrated database on a reachable
38
+ * local server, else a Testcontainers container.
39
+ *
40
+ * @module testing
41
+ */
42
+
43
+ import type { AnyRelations } from "drizzle-orm/relations";
44
+ import * as Arr from "effect/Array";
45
+ import * as Cause from "effect/Cause";
46
+ import * as Duration from "effect/Duration";
47
+ import * as Effect from "effect/Effect";
48
+ import * as Exit from "effect/Exit";
49
+ import * as HashSet from "effect/HashSet";
50
+ import * as Layer from "effect/Layer";
51
+ import type * as ManagedRuntime from "effect/ManagedRuntime";
52
+ import * as Predicate from "effect/Predicate";
53
+ import type * as Redacted from "effect/Redacted";
54
+ import * as Record from "effect/Record";
55
+ import * as Schedule from "effect/Schedule";
56
+ import * as Schema from "effect/Schema";
57
+ import { isSqlError, type SqlError } from "effect/sql/SqlError";
58
+ import { type PgDatabase, pgClientFromUrl } from "./index.ts";
59
+
60
+ export {
61
+ connectionUrl,
62
+ ContainerStartFailed,
63
+ type DatabaseSource,
64
+ type DatabaseSourceOptions,
65
+ DatabaseUnreachable,
66
+ InvalidDatabasePrefix,
67
+ type MigrationRunner,
68
+ probeServer,
69
+ type ProvisionedDatabase,
70
+ type ProvisionOptions,
71
+ provisionTestDatabase,
72
+ type ResolutionEnvironment,
73
+ resolveDatabaseSource,
74
+ type ServerProbe,
75
+ ServerUnreachable,
76
+ testDatabaseName,
77
+ } from "./provisioning.ts";
78
+
79
+ // ── Connect-seam retry classification ─────────────────────────────────
80
+ //
81
+ // {@link runInTestTransaction} retries the *open* of a per-test transaction
82
+ // when it fails at the connect/acquire seam under fork-pool startup
83
+ // contention, but never re-runs test logic. Which `SqlError`s count as a
84
+ // retryable connect failure is decided here.
85
+ //
86
+ // ## Why `isRetryable` alone is not enough (website #1055)
87
+ //
88
+ // The observed CI flake surfaces as
89
+ // `SqlError: Failed to acquire connection for transaction (cause: … timeout
90
+ // exceeded when trying to connect)` and, on immediate re-runs, bursts of
91
+ // `read ECONNRESET`. Both are raised by `@effect/sql-pg`'s pool acquirer
92
+ // (`reserveRaw`) and passed through its `classifyError`, which only maps a
93
+ // cause to the **retryable** `ConnectionError` when the cause carries a
94
+ // PostgreSQL SQLSTATE in the `08` class. node-pg's pool-acquisition timeout
95
+ // is a plain `Error` with **no** `.code`, and a socket `ECONNRESET` carries a
96
+ // Node `code` (`"ECONNRESET"`) that is not an `08xxx` SQLSTATE — so both are
97
+ // bucketed as a **non-retryable** `UnknownError` (`isRetryable === false`). A
98
+ // guard that only trusted `isRetryable` therefore never fired for the exact
99
+ // failure it was built to absorb.
100
+ //
101
+ // These failures still originate at the connect/acquire seam — before the
102
+ // transaction body opens — so they are safe to retry when composed (at the
103
+ // call site) with the `captured === null` guard that proves no test work has
104
+ // run. We identify the seam by the `operation` label `@effect/sql-pg` stamps
105
+ // on the reason (`"acquireConnection"` for the pool `reserveRaw` path,
106
+ // `"connect"` for client/pool creation), which is present regardless of how
107
+ // the underlying cause classified.
108
+
109
+ /**
110
+ * The `operation` labels `@effect/sql-pg` stamps on a connect/acquire-seam
111
+ * `SqlError` reason. A failure carrying one of these happened while opening a
112
+ * connection, i.e. before any transaction body ran.
113
+ */
114
+ const CONNECT_SEAM_OPERATIONS = HashSet.make("acquireConnection", "connect");
115
+
116
+ /**
117
+ * Whether `error` is a `SqlError` that failed at the connect/acquire seam and
118
+ * so is safe to retry (when the caller has also confirmed no test body ran).
119
+ *
120
+ * Returns `true` for:
121
+ * - any reason the driver already flags `isRetryable` (`ConnectionError` from
122
+ * an `08xxx` SQLSTATE, `DeadlockError`, `SerializationError`, the timeout
123
+ * reasons), and
124
+ * - a reason whose `operation` is a connect/acquire seam label, which catches
125
+ * the misclassified `UnknownError` the website #1055 flake surfaces as
126
+ * (pool "timeout exceeded when trying to connect" / `ECONNRESET`, neither
127
+ * of which carries an `08` SQLSTATE).
128
+ *
129
+ * Returns `false` for a `UniqueViolation` / `ConstraintError` /
130
+ * `SqlSyntaxError` raised by a test's own query (`operation === "execute"`),
131
+ * so genuine test failures are never retried.
132
+ */
133
+ // oxlint-disable-next-line effect/no-unknown-parameters -- published signature: the predicate classifies whatever failure value a retry's `while` hands it, and `isSqlError` is the parse; narrowing the parameter would break callers
134
+ export const isRetryableConnectFailure = (error: unknown): boolean => {
135
+ if (!isSqlError(error)) return false;
136
+ if (error.isRetryable) return true;
137
+ const operation = error.reason.operation;
138
+ return operation !== undefined && HashSet.has(CONNECT_SEAM_OPERATIONS, operation);
139
+ };
140
+
141
+ /**
142
+ * `maxConnections` is pinned to 1 because a fork runs its tests strictly
143
+ * sequentially and the runner holds exactly one transaction-bound connection
144
+ * at a time (the whole test body runs inside a single `db.transaction`, with
145
+ * the spec's own `transact` nesting a SAVEPOINT on that same connection) — so
146
+ * 1 is the real per-fork demand, not a compromise. It is also what forces an
147
+ * operation-opened transaction to nest as a SAVEPOINT on the same connection
148
+ * (and so see the outer transaction's uncommitted writes) rather than
149
+ * silently acquiring a second connection that cannot.
150
+ */
151
+ const TEST_POOL_MAX_CONNECTIONS = 1;
152
+
153
+ /**
154
+ * How long a single connection attempt may take before the driver gives up.
155
+ *
156
+ * Under fork parallelism all forks race to open their connection at once
157
+ * while each imports the module graph, so at startup the connect handshakes
158
+ * contend. A generous per-attempt timeout costs nothing on the happy path (a
159
+ * healthy connect resolves in milliseconds and never approaches this
160
+ * ceiling), so it stays as cheap insurance for slow CI service-container
161
+ * startup.
162
+ */
163
+ const TEST_CONNECT_TIMEOUT = Duration.seconds(30);
164
+
165
+ /** Sentinel cause used to force the outer test transaction to roll back. */
166
+ const ROLLBACK_TAG = "__effect_test_rollback__";
167
+
168
+ interface RollbackSignal {
169
+ readonly _tag: typeof ROLLBACK_TAG;
170
+ }
171
+
172
+ const rollback: RollbackSignal = { _tag: ROLLBACK_TAG };
173
+
174
+ const isRollback = (u: unknown): u is RollbackSignal =>
175
+ Predicate.isObjectOrArray(u) && Predicate.isTagged(u, ROLLBACK_TAG);
176
+
177
+ /**
178
+ * Whether a captured failure cause carries the rollback sentinel.
179
+ *
180
+ * Effect causes are nested objects; this is a structural walk that finds the
181
+ * rollback tag without depending on `Cause` internals.
182
+ */
183
+ const causeContainsRollback = (cause: unknown): boolean => {
184
+ // oxlint-disable-next-line effect/avoid-native-object-helpers -- the visited set must compare by REFERENCE (cycle detection over an opaque graph); `HashSet` compares by `Equal`, under which distinct but structurally equal `Cause` nodes collide
185
+ const seen = new Set<unknown>();
186
+ const stack: Array<unknown> = [cause];
187
+ // oxlint-disable-next-line effect/imperative-loops -- an explicit stack walk over an opaque object graph; the ported mechanism, unchanged.
188
+ while (Arr.isReadonlyArrayNonEmpty(stack)) {
189
+ const node = stack.pop();
190
+ if (!Predicate.isObjectOrArray(node) || seen.has(node)) continue;
191
+ seen.add(node);
192
+ if (isRollback(node)) return true;
193
+ Arr.forEach(Array.isArray(node) ? node : Record.values(node), (value) => {
194
+ stack.push(value);
195
+ });
196
+ }
197
+ return false;
198
+ };
199
+
200
+ // ── Connect-time retry policy ─────────────────────────────────────────
201
+ //
202
+ // See the connect-seam classification above for the flake this guards
203
+ // against and why `isRetryable` alone doesn't catch it (website #1055). The
204
+ // attempt count and backoff floor are tunable here. Five retries over an
205
+ // exponential, jittered backoff (≈0.25s, 0.5s, 1s, 2s, 4s, with jitter to
206
+ // de-correlate the parallel forks) comfortably outlasts a transient
207
+ // saturation spike without meaningfully slowing a healthy run (which never
208
+ // retries — the schedule only fires for a connect-seam `SqlError` that failed
209
+ // before any test body ran; see the `while` guard at the call site).
210
+ const CONNECT_RETRY_ATTEMPTS = 5;
211
+ const CONNECT_RETRY_BASE_DELAY = Duration.millis(250);
212
+
213
+ const connectRetrySchedule = Schedule.max([
214
+ Schedule.exponential(CONNECT_RETRY_BASE_DELAY),
215
+ Schedule.recurs(CONNECT_RETRY_ATTEMPTS),
216
+ ]).pipe(Schedule.jittered);
217
+
218
+ /**
219
+ * A failure of the runner itself rather than of the spec it ran: the outer
220
+ * transaction failed before the spec's `Exit` was captured, or failed with
221
+ * something other than the rollback sentinel. Thrown (rejecting the runner's
222
+ * Promise), never put in an Effect error channel — the runner is the test
223
+ * harness's boundary.
224
+ */
225
+ class TestTransactionError extends Schema.TaggedError<TestTransactionError>()(
226
+ "TestTransactionError",
227
+ { message: Schema.String },
228
+ ) {}
229
+
230
+ /**
231
+ * The always-rollback test transaction runner for one application's
232
+ * {@link PgDatabase}, built by {@link makeTestTransaction}.
233
+ *
234
+ * Only the service KEY matters to these signatures — the handle types are
235
+ * closed over by the factory — so the interface is parameterised by the key
236
+ * string alone.
237
+ */
238
+ export interface TestTransaction<Key extends string> {
239
+ /**
240
+ * Runs `effect` inside a transaction that is always rolled back once the
241
+ * test completes, against a caller-supplied runtime.
242
+ *
243
+ * `R` is the runtime's own context. It **must include the `Database`
244
+ * key**, which is what lets the program below yield the client; that
245
+ * relationship is not expressible as a generic constraint once `R` can be a
246
+ * union that also carries unrelated services (a harness's platform binding
247
+ * stubs, a spec's viewer services), so it is documented here rather than
248
+ * enforced by the type checker — the same trade-off the ported wrapper
249
+ * makes.
250
+ *
251
+ * `${Key}/CurrentTransaction` is in the ACCEPTED requirement, not in the
252
+ * runtime's catalogue: nothing provides it as a layer. The runner wraps the
253
+ * effect in `transact`, so the spec stands in for the handler that opens
254
+ * the boundary in production, and the requirement is discharged there.
255
+ */
256
+ readonly runInTestTransaction: <R, A, E>(
257
+ runtime: ManagedRuntime.ManagedRuntime<R, SqlError>,
258
+ effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>,
259
+ ) => Promise<A>;
260
+
261
+ /**
262
+ * {@link runInTestTransaction} with the runtime bound once — the form a
263
+ * suite exports as its own `withTestTransaction(effect)` helper.
264
+ */
265
+ readonly withTestTransaction: <R>(
266
+ runtime: ManagedRuntime.ManagedRuntime<R, SqlError>,
267
+ ) => <A, E>(effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>) => Promise<A>;
268
+
269
+ /**
270
+ * Run `effect` inside a **SAVEPOINT** and hand back its `Exit`, leaving the
271
+ * enclosing transaction usable whether it succeeded or failed.
272
+ *
273
+ * This is what a spec asserting "the constraint rejects this write" needs.
274
+ * A statement that raises in PostgreSQL aborts the whole transaction, so a
275
+ * bare `Effect.exit(tx.insert(…))` captures the expected failure and then
276
+ * poisons every later statement in the test with `current transaction is
277
+ * aborted, commands ignored until end of transaction block` — which
278
+ * surfaces as the *next* query failing, several assertions away from the
279
+ * cause. Nesting the attempt (`tx.transaction`, a SAVEPOINT on the same
280
+ * pinned connection) confines the abort to the savepoint: the driver rolls
281
+ * back to it and the outer transaction, with every fixture the test has
282
+ * already written, carries on.
283
+ *
284
+ * `effect` keeps using the ambient `CurrentTransaction` handle — the
285
+ * savepoint is a property of the CONNECTION, which `maxConnections: 1`
286
+ * pins, so nothing has to be re-threaded through a nested handle.
287
+ *
288
+ * The `Exit` is returned rather than asserted on, so the caller keeps the
289
+ * choice: `expect(exit._tag).toBe("Failure")` for a rejection, or a look at
290
+ * the cause when the message matters.
291
+ */
292
+ readonly attemptInSavepoint: <A, E, R>(
293
+ effect: Effect.Effect<A, E, R>,
294
+ ) => Effect.Effect<Exit.Exit<A, E | SqlError>, never, R | `${Key}/CurrentTransaction`>;
295
+ }
296
+
297
+ /**
298
+ * Builds the always-rollback test transaction runner for one application's
299
+ * database services.
300
+ *
301
+ * The services carry the key strings and the `transact` boundary, so the
302
+ * runner needs nothing else: a suite calls this once next to its
303
+ * {@link makePgDatabase} call and exports the three helpers.
304
+ */
305
+ export const makeTestTransaction = <Relations extends AnyRelations, Key extends string>(
306
+ database: PgDatabase<Relations, Key>,
307
+ ): TestTransaction<Key> => {
308
+ const runInTestTransaction = async <R, A, E>(
309
+ runtime: ManagedRuntime.ManagedRuntime<R, SqlError>,
310
+ effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>,
311
+ ): Promise<A> => {
312
+ // oxlint-disable-next-line effect/prefer-option-over-null -- write-once slot assigned inside the nested `Effect.gen` closure; the `null` sentinel is what the "inner Exit was never captured" check below tests, and TS's control-flow analysis (see the note at the read site) already treats this declaration specially
313
+ let captured: Exit.Exit<A, E | SqlError> | null = null;
314
+
315
+ const program = Effect.gen(function* () {
316
+ const db = yield* database.Database;
317
+ return yield* db.transaction(() =>
318
+ Effect.gen(function* () {
319
+ captured = yield* Effect.exit(database.transact(effect));
320
+ return yield* Effect.fail(rollback);
321
+ }),
322
+ );
323
+ }).pipe(
324
+ // Retry ONLY a connect-time transient failure — never test logic.
325
+ //
326
+ // Two guards keep this honest:
327
+ // 1. `isRetryableConnectFailure(e)` — fires for a `SqlError` that
328
+ // failed at the connect/acquire seam (the retryable
329
+ // `ConnectionError`, plus the misclassified `UnknownError` the
330
+ // website #1055 acquisition-timeout / `ECONNRESET` flake surfaces
331
+ // as) but NOT for a `UniqueViolation` / `ConstraintError` /
332
+ // `SqlSyntaxError` raised by a test's own query. The rollback
333
+ // sentinel that the body always forces is a plain object, not a
334
+ // `SqlError`, so a *successful* test and a *genuinely failing* test
335
+ // (whose Exit is captured before the rollback) both fail the
336
+ // program with `rollback` and never retry.
337
+ // 2. `captured === null` — the inner test body never ran. A
338
+ // connect-seam `SqlError` can in principle surface *after* the body
339
+ // has executed and been captured (e.g. the connection drops on the
340
+ // rollback raise); re-running then would double-execute the test's
341
+ // side effects, so we refuse to retry once any test work has
342
+ // happened. The connect flake by construction fails before the
343
+ // transaction body opens, so `captured` is still null then —
344
+ // exactly the case we retry.
345
+ Effect.retry({
346
+ schedule: connectRetrySchedule,
347
+ while: (e) => captured === null && isRetryableConnectFailure(e),
348
+ }),
349
+ );
350
+
351
+ const outer = await runtime.runPromiseExit(
352
+ // SAFETY: the only requirement `program` adds beyond `R` is the
353
+ // `Database` key it yields, and `R` includes that key — the documented
354
+ // contract of `runInTestTransaction` (see `TestTransaction`), which a
355
+ // generic constraint cannot express.
356
+ program as Effect.Effect<Effect.Success<typeof program>, Effect.Error<typeof program>, R>,
357
+ );
358
+
359
+ // Snapshot to a local const: `captured` is only ever reassigned inside the
360
+ // nested `Effect.gen` closure above, so TS's control-flow analysis doesn't
361
+ // track that write here — it narrows `captured` as if still `null` (its
362
+ // only textually-direct assignment), collapsing every read below to
363
+ // `never`. The annotation restates the variable's real declared type,
364
+ // which the runtime assignment inside the closure does in fact produce.
365
+ // SAFETY: restates `captured`'s declared type verbatim, undoing only the
366
+ // control-flow narrowing described above.
367
+ // oxlint-disable-next-line effect/prefer-option-over-null -- restates `captured`'s declared type verbatim; see the comment above
368
+ const innerExit = captured as Exit.Exit<A, E | SqlError> | null;
369
+ if (!innerExit) {
370
+ if (Exit.isFailure(outer)) {
371
+ throw new TestTransactionError({
372
+ message: `runInTestTransaction: outer transaction failed: ${String(outer.cause)}`,
373
+ });
374
+ }
375
+ throw new TestTransactionError({
376
+ message: "runInTestTransaction: inner Exit was never captured",
377
+ });
378
+ }
379
+
380
+ // The outer Exit should be the rollback failure we forced. Anything
381
+ // else is a transaction-level failure we didn't expect (e.g. the
382
+ // connection dropped between the inner result and the rollback raise).
383
+ if (Exit.isFailure(outer) && !causeContainsRollback(outer.cause)) {
384
+ throw new TestTransactionError({
385
+ message: `runInTestTransaction: unexpected outer failure: ${String(outer.cause)}`,
386
+ });
387
+ }
388
+
389
+ if (Exit.isSuccess(innerExit)) return innerExit.value;
390
+ // Project the captured `Cause` to something a test can assert on.
391
+ // Throwing the raw `Cause` makes vitest serialise it as `{ _tag: 'Die',
392
+ // defect: [Object] }` with an empty `.message`, so `.rejects.toThrow(/…/)`
393
+ // never matches and a defect's real error is invisible. `Cause.squash`
394
+ // collapses the cause to its underlying value — a typed `Fail`'s error
395
+ // (keeps `_tag` + message) or a `Die`'s thrown defect (a real `Error` with
396
+ // a message + stack) — so the actual failure surfaces in vitest output
397
+ // and `.rejects.toThrow(/…/)` matches.
398
+ throw Cause.squash(innerExit.cause);
399
+ };
400
+
401
+ return {
402
+ runInTestTransaction,
403
+ withTestTransaction:
404
+ <R>(runtime: ManagedRuntime.ManagedRuntime<R, SqlError>) =>
405
+ <A, E>(effect: Effect.Effect<A, E, R | `${Key}/CurrentTransaction`>): Promise<A> =>
406
+ runInTestTransaction(runtime, effect),
407
+ attemptInSavepoint: <A, E, R>(
408
+ effect: Effect.Effect<A, E, R>,
409
+ ): Effect.Effect<Exit.Exit<A, E | SqlError>, never, R | `${Key}/CurrentTransaction`> =>
410
+ Effect.gen(function* () {
411
+ const tx = yield* database.CurrentTransaction;
412
+ return yield* Effect.exit(tx.transaction(() => effect));
413
+ }),
414
+ };
415
+ };
416
+
417
+ /**
418
+ * The `Database` layer an integration suite runs against: the application's
419
+ * own {@link PgDatabase.layer} over a `PgClient` built from `url`, with the
420
+ * pool pinned to one connection (see {@link TEST_POOL_MAX_CONNECTIONS}).
421
+ *
422
+ * Built once per process and reused by every spec in it. `url` is the
423
+ * caller's: this library does not read `DATABASE_URL_TEST` or decide how a
424
+ * test database is provisioned.
425
+ *
426
+ * Unlike production, this uses the pinging {@link pgClientFromUrl} on
427
+ * purpose — a suite that cannot reach its database should fail at layer build
428
+ * with one legible `SqlError`, not once per spec on the first query.
429
+ */
430
+ /* oxlint-disable effect/prefer-option-over-null -- driver-facing config: `connectTimeout` is forwarded verbatim to `pgClientFromUrl` and from there to `@effect/sql-pg`'s `PgPoolConfig`, which reads `undefined` as "not supplied". See `pgClientFromUrl`'s block in ./index.ts. */
431
+ export const testDatabaseLayer = <Relations extends AnyRelations, Key extends string>(
432
+ database: PgDatabase<Relations, Key>,
433
+ options: {
434
+ readonly url: Redacted.Redacted<string>;
435
+ readonly connectTimeout?: Duration.Input | undefined;
436
+ },
437
+ ): Layer.Layer<Key, SqlError> =>
438
+ database.layer.pipe(
439
+ Layer.provide(
440
+ pgClientFromUrl({
441
+ url: options.url,
442
+ maxConnections: TEST_POOL_MAX_CONNECTIONS,
443
+ connectTimeout: options.connectTimeout ?? TEST_CONNECT_TIMEOUT,
444
+ }),
445
+ ),
446
+ );
447
+ /* oxlint-enable effect/prefer-option-over-null */
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @copyright Thomas Foster
3
+ *
4
+ * **Schema and migration guards for a drizzle-kit `migrations/` directory**,
5
+ * as bin-less Effects an application's own `db:*` tasks run with its schema
6
+ * module: {@link checkMigrationDrift} (DAG integrity, zero drift against the
7
+ * latest snapshot, base ancestry) and {@link checkViewDependencies} (no view
8
+ * selects from another view). Neither needs a database.
9
+ *
10
+ * `drizzle-kit` is an optional peer that only this subpath imports
11
+ * (`drizzle-kit/api-postgres`). The file-system and `git` access go through
12
+ * the `FileSystem` / `Path` / `ChildProcessSpawner` services — a Node script
13
+ * provides `@effect/platform-node`'s `NodeServices.layer`.
14
+ *
15
+ * @module tooling
16
+ */
17
+
18
+ export {
19
+ type BaseMigrations,
20
+ type BaseRefEnvironment,
21
+ checkBaseAncestry,
22
+ checkDagIntegrity,
23
+ checkMigrationDrift,
24
+ findHeads,
25
+ type GitBase,
26
+ MalformedSnapshot,
27
+ MigrationDriftError,
28
+ type MigrationDriftOptions,
29
+ type MigrationDriftReport,
30
+ readBaseMigrations,
31
+ resolveBaseRef,
32
+ ROOT_SENTINEL_ID,
33
+ SchemaDiffError,
34
+ type Snapshot,
35
+ type SnapshotEntry,
36
+ } from "./migration-drift.ts";
37
+ export {
38
+ checkViewDependencies,
39
+ findViewEdges,
40
+ parseViewDefinitions,
41
+ referencesView,
42
+ type ViewDefinition,
43
+ ViewDependencyError,
44
+ type ViewEdge,
45
+ viewKey,
46
+ } from "./view-dependencies.ts";