@thomasfosterau/effect-drizzle-ext-pg 0.0.0 → 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.
@@ -0,0 +1,408 @@
1
+ import { pgClientFromUrl } from "./index.js";
2
+ import { applyMigrations } from "./migrations.js";
3
+ import * as PgClient from "@effect/sql-pg/PgClient";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Arr from "effect/Array";
7
+ import * as Schema from "effect/Schema";
8
+ import * as Cause from "effect/Cause";
9
+ import * as Duration from "effect/Duration";
10
+ import * as Exit from "effect/Exit";
11
+ import * as HashSet from "effect/HashSet";
12
+ import * as Predicate from "effect/Predicate";
13
+ import * as Record from "effect/Record";
14
+ import * as Schedule from "effect/Schedule";
15
+ import { isSqlError } from "effect/sql/SqlError";
16
+ import * as Redacted from "effect/Redacted";
17
+ // src/provisioning.ts
18
+ /**
19
+ * @copyright Thomas Foster
20
+ *
21
+ * **Ephemeral test-database provisioning** — the half of an integration
22
+ * suite's global setup every consuming application carried as
23
+ * `packages/integration-harness/local-database.ts`. Re-exported from
24
+ * `./testing`; the reference is the preferenceflow application's copy.
25
+ *
26
+ * A suite's database is resolved in this order ({@link resolveDatabaseSource}):
27
+ *
28
+ * 0. **An explicit `DATABASE_URL_TEST` / `DATABASE_URL`** wins and is used
29
+ * as-is: it must already be migrated — the escape hatch for pointing the
30
+ * suite at a deployed stage's database or any other prepared server.
31
+ * Nothing is provisioned and nothing is torn down.
32
+ * 1. **A reachable local server** — a CI service container, a devcontainer's
33
+ * PostgreSQL, or anything the standard `PG*` variables (`PGHOST`,
34
+ * `PGPORT`, `PGUSER`, `PGPASSWORD`) point at; the default is a trust-auth
35
+ * `postgres@127.0.0.1:5432`. Each run gets a **freshly created database**
36
+ * on it: drop, create, apply every migration.
37
+ * 2. **A Testcontainers-launched container** when nothing is listening and
38
+ * no `PG*` variable was set explicitly. Needs a Docker daemon and the
39
+ * optional peer `@testcontainers/postgresql`, which is imported only on
40
+ * this path. The container is brand new, so migrations apply directly, and
41
+ * it stops when the provisioning scope closes.
42
+ *
43
+ * Explicit `PG*` config that is unreachable is a hard error
44
+ * ({@link DatabaseUnreachable}), not a silent fallback: if you pointed the
45
+ * suite somewhere, it should not quietly run elsewhere.
46
+ *
47
+ * Either way run N's verdict is independent of run N−1: no state survives
48
+ * between runs. The per-checkout database name
49
+ * `<prefix>_test_<sha256(migrationsDir)[:8]>` keeps parallel worktrees
50
+ * sharing one server from dropping each other's database mid-run — pass the
51
+ * migrations directory as an absolute path, which is per-checkout.
52
+ *
53
+ * What stays with the application: fixture users and seed data (website
54
+ * #1847 keeps the harness → database edge one-way), and the container image
55
+ * and database-name prefix, which are parameters here.
56
+ *
57
+ * @module provisioning
58
+ */
59
+ /** The variables that count as "explicitly pointed at a server". */
60
+ const SERVER_VARIABLES = [
61
+ "PGHOST",
62
+ "PGPORT",
63
+ "PGUSER",
64
+ "PGPASSWORD"
65
+ ];
66
+ /** How long one connection attempt against the admin database may take. */
67
+ const CONNECT_TIMEOUT = Duration.seconds(10);
68
+ /** Where a local server is expected, honouring the standard `PG*` variables. */
69
+ const serverConfig = (env) => ({
70
+ host: env.PGHOST ?? "127.0.0.1",
71
+ port: Number(env.PGPORT ?? "5432"),
72
+ user: env.PGUSER ?? "postgres",
73
+ password: env.PGPASSWORD ?? ""
74
+ });
75
+ /** Was a server location explicitly configured (vs. the loopback default)? */
76
+ const serverExplicitlyConfigured = (env) => SERVER_VARIABLES.some((name) => (env[name] ?? "") !== "");
77
+ /** The connection URL for `database` on the configured local server. */
78
+ const connectionUrl = (env, database) => {
79
+ const { host, port, user, password } = serverConfig(env);
80
+ const auth = password === "" ? user : `${user}:${encodeURIComponent(password)}`;
81
+ return Redacted.make(`postgresql://${auth}@${host}:${port}/${database}`);
82
+ };
83
+ /** `user@host:port` — the configured server, printable (no password). */
84
+ const serverLabel = (env) => {
85
+ const { host, port, user } = serverConfig(env);
86
+ return `${user}@${host}:${port}`;
87
+ };
88
+ /** A database-name prefix is spliced into DDL unquoted-safe, so it is restricted to an identifier. */
89
+ const DATABASE_PREFIX = /^[a-z_][a-z0-9_]*$/;
90
+ /** The prefix is not a lower-case SQL identifier. */
91
+ var InvalidDatabasePrefix = class extends Schema.TaggedError()("InvalidDatabasePrefix", { prefix: Schema.String }) {
92
+ get message() {
93
+ return `database-name prefix '${this.prefix}' must match ${DATABASE_PREFIX.source}`;
94
+ }
95
+ };
96
+ /**
97
+ * The deterministic per-checkout database name
98
+ * `<prefix>_test_<sha256(migrationsDir)[:8]>`: concurrent runs from different
99
+ * worktrees never collide, and a stale database self-heals on the next run.
100
+ */
101
+ const testDatabaseName = (options) => Effect.gen(function* () {
102
+ if (!DATABASE_PREFIX.test(options.prefix)) return yield* new InvalidDatabasePrefix({ prefix: options.prefix });
103
+ const digest = yield* Effect.promise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(options.migrationsDir)));
104
+ const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 8);
105
+ return `${options.prefix}_test_${hex}`;
106
+ });
107
+ /** One connection attempt against a server's admin database failed. */
108
+ var ServerUnreachable = class extends Schema.TaggedError()("ServerUnreachable", { cause: Schema.Defect() }) {};
109
+ /** A `SELECT 1` over one short-lived connection. */
110
+ const probeServer = (adminUrl) => Effect.gen(function* () {
111
+ yield* (yield* PgClient.PgClient)`SELECT 1`;
112
+ }).pipe(Effect.provide(PgClient.layer({
113
+ url: adminUrl,
114
+ maxConnections: 1,
115
+ connectTimeout: CONNECT_TIMEOUT
116
+ })), Effect.mapError((cause) => new ServerUnreachable({ cause })));
117
+ /** An explicit but unreachable `PG*` target: this run will not fall back to a container. */
118
+ var DatabaseUnreachable = class extends Schema.TaggedError()("DatabaseUnreachable", {
119
+ server: Schema.String,
120
+ cause: Schema.Defect()
121
+ }) {
122
+ get message() {
123
+ return [
124
+ `no PostgreSQL server at ${this.server}.`,
125
+ "PG* variables are set, so this run will not fall back to Testcontainers —",
126
+ "fix the PG* target, unset it to let the suite start its own server, or set",
127
+ "DATABASE_URL_TEST to an already-migrated database."
128
+ ].join("\n");
129
+ }
130
+ };
131
+ /**
132
+ * Resolve where a run's database comes from, in the order the module doc
133
+ * describes: an explicit URL, then a reachable local server (an explicit but
134
+ * unreachable `PG*` target is a {@link DatabaseUnreachable}), then the
135
+ * Testcontainers fallback. Pure apart from the one probe, which only runs
136
+ * when no explicit URL is set.
137
+ */
138
+ const resolveDatabaseSource = (options) => Effect.gen(function* () {
139
+ const { env } = options;
140
+ const explicit = env.DATABASE_URL_TEST ?? env.DATABASE_URL ?? "";
141
+ if (explicit !== "") return {
142
+ _tag: "Explicit",
143
+ url: Redacted.make(explicit)
144
+ };
145
+ const database = yield* testDatabaseName({
146
+ prefix: options.dbNamePrefix,
147
+ migrationsDir: options.migrationsDir
148
+ });
149
+ const adminUrl = connectionUrl(env, "postgres");
150
+ if (yield* (options.probe ?? probeServer)(adminUrl).pipe(Effect.as(true), Effect.catchTag("ServerUnreachable", (unreachable) => serverExplicitlyConfigured(env) ? Effect.fail(new DatabaseUnreachable({
151
+ server: serverLabel(env),
152
+ cause: unreachable.cause
153
+ })) : Effect.succeed(false)))) return {
154
+ _tag: "LocalServer",
155
+ adminUrl,
156
+ database,
157
+ url: connectionUrl(env, database)
158
+ };
159
+ return {
160
+ _tag: "Container",
161
+ image: options.image
162
+ };
163
+ });
164
+ /** Starting the Testcontainers fallback failed — most often, no Docker daemon. */
165
+ var ContainerStartFailed = class extends Schema.TaggedError()("ContainerStartFailed", {
166
+ image: Schema.String,
167
+ server: Schema.String,
168
+ cause: Schema.Defect()
169
+ }) {
170
+ get message() {
171
+ return [
172
+ `no PostgreSQL server at ${this.server}, and starting a ${this.image} container via`,
173
+ "Testcontainers failed — is a Docker daemon available (and `@testcontainers/postgresql`",
174
+ "installed)? The suite needs one of:",
175
+ " - a Docker daemon, or",
176
+ " - a local PostgreSQL server, with PGHOST/PGPORT/PGUSER/PGPASSWORD pointing at it, or",
177
+ " - DATABASE_URL_TEST set to an already-migrated database to skip provisioning."
178
+ ].join("\n");
179
+ }
180
+ };
181
+ /** The default {@link MigrationRunner}: `./migrations` over a one-connection client. */
182
+ const defaultRunner = (migrationsDir) => (url) => applyMigrations(migrationsDir).pipe(Effect.provide(PgClient.layer({
183
+ url,
184
+ maxConnections: 1
185
+ })));
186
+ /** Drop and recreate this checkout's database on the reachable server. */
187
+ const recreateDatabase = (source) => Effect.gen(function* () {
188
+ const admin = yield* PgClient.PgClient;
189
+ yield* admin.unsafe(`DROP DATABASE IF EXISTS "${source.database}" WITH (FORCE)`);
190
+ yield* admin.unsafe(`CREATE DATABASE "${source.database}"`);
191
+ }).pipe(Effect.provide(PgClient.layer({
192
+ url: source.adminUrl,
193
+ maxConnections: 1,
194
+ connectTimeout: CONNECT_TIMEOUT
195
+ })));
196
+ /**
197
+ * Start a fresh server via Testcontainers, stopped when the scope closes. The
198
+ * import is dynamic so a run that found a reachable server never loads the
199
+ * library or talks to Docker at all.
200
+ */
201
+ const startContainer = (image, server) => Effect.acquireRelease(Effect.tryPromise({
202
+ try: async () => {
203
+ const { PostgreSqlContainer } = await import("@testcontainers/postgresql");
204
+ return new PostgreSqlContainer(image).start();
205
+ },
206
+ catch: (cause) => new ContainerStartFailed({
207
+ image,
208
+ server,
209
+ cause
210
+ })
211
+ }), (container) => Effect.promise(() => container.stop())).pipe(Effect.map((container) => Redacted.make(container.getConnectionUri())));
212
+ /**
213
+ * Provision a run's fresh, fully-migrated database — resolve the source,
214
+ * then drop / create / migrate on a reachable server, or start and migrate a
215
+ * container — and return its URL. An explicit source is returned as-is.
216
+ *
217
+ * Scoped: a started container stops when the scope closes. From a vitest
218
+ * global setup, hold a `Scope` across the run and close it in the returned
219
+ * teardown:
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * // vitest global setup
224
+ * import * as NodeServices from "@effect/platform-node/NodeServices";
225
+ * import { provisionTestDatabase } from "@thomasfosterau/effect-drizzle-ext-pg/testing";
226
+ * import * as Effect from "effect/Effect";
227
+ * import * as Exit from "effect/Exit";
228
+ * import * as Redacted from "effect/Redacted";
229
+ * import * as Scope from "effect/Scope";
230
+ *
231
+ * const MIGRATIONS_DIR = "packages/database/migrations";
232
+ *
233
+ * export default async function setup() {
234
+ * const scope = Effect.runSync(Scope.make());
235
+ * const database = await Effect.runPromise(
236
+ * provisionTestDatabase({
237
+ * env: process.env,
238
+ * migrationsDir: MIGRATIONS_DIR,
239
+ * dbNamePrefix: "website",
240
+ * image: "pgvector/pgvector:pg17",
241
+ * }).pipe(Scope.provide(scope), Effect.provide(NodeServices.layer)),
242
+ * );
243
+ * process.env.DATABASE_URL_TEST = Redacted.value(database.url);
244
+ * return () => Effect.runPromise(Scope.close(scope, Exit.void));
245
+ * }
246
+ * ```
247
+ */
248
+ const provisionTestDatabase = (options) => Effect.gen(function* () {
249
+ const source = yield* resolveDatabaseSource(options);
250
+ if (Predicate.isTagged(source, "Explicit")) return {
251
+ url: source.url,
252
+ source
253
+ };
254
+ const url = Predicate.isTagged(source, "LocalServer") ? yield* recreateDatabase(source).pipe(Effect.as(source.url)) : yield* startContainer(source.image, serverLabel(options.env));
255
+ const applied = yield* (options.applyMigrations ?? defaultRunner(options.migrationsDir))(url);
256
+ yield* Effect.logInfo(`provisioned ${Predicate.isTagged(source, "LocalServer") ? `'${source.database}'` : `a ${source.image} container`} (${applied} migrations)`);
257
+ return {
258
+ url,
259
+ source
260
+ };
261
+ }).pipe(Effect.withLogSpan("provisionTestDatabase"));
262
+ // src/testing.ts
263
+ /**
264
+ * The `operation` labels `@effect/sql-pg` stamps on a connect/acquire-seam
265
+ * `SqlError` reason. A failure carrying one of these happened while opening a
266
+ * connection, i.e. before any transaction body ran.
267
+ */
268
+ const CONNECT_SEAM_OPERATIONS = HashSet.make("acquireConnection", "connect");
269
+ /**
270
+ * Whether `error` is a `SqlError` that failed at the connect/acquire seam and
271
+ * so is safe to retry (when the caller has also confirmed no test body ran).
272
+ *
273
+ * Returns `true` for:
274
+ * - any reason the driver already flags `isRetryable` (`ConnectionError` from
275
+ * an `08xxx` SQLSTATE, `DeadlockError`, `SerializationError`, the timeout
276
+ * reasons), and
277
+ * - a reason whose `operation` is a connect/acquire seam label, which catches
278
+ * the misclassified `UnknownError` the website #1055 flake surfaces as
279
+ * (pool "timeout exceeded when trying to connect" / `ECONNRESET`, neither
280
+ * of which carries an `08` SQLSTATE).
281
+ *
282
+ * Returns `false` for a `UniqueViolation` / `ConstraintError` /
283
+ * `SqlSyntaxError` raised by a test's own query (`operation === "execute"`),
284
+ * so genuine test failures are never retried.
285
+ */
286
+ const isRetryableConnectFailure = (error) => {
287
+ if (!isSqlError(error)) return false;
288
+ if (error.isRetryable) return true;
289
+ const operation = error.reason.operation;
290
+ return operation !== void 0 && HashSet.has(CONNECT_SEAM_OPERATIONS, operation);
291
+ };
292
+ /**
293
+ * `maxConnections` is pinned to 1 because a fork runs its tests strictly
294
+ * sequentially and the runner holds exactly one transaction-bound connection
295
+ * at a time (the whole test body runs inside a single `db.transaction`, with
296
+ * the spec's own `transact` nesting a SAVEPOINT on that same connection) — so
297
+ * 1 is the real per-fork demand, not a compromise. It is also what forces an
298
+ * operation-opened transaction to nest as a SAVEPOINT on the same connection
299
+ * (and so see the outer transaction's uncommitted writes) rather than
300
+ * silently acquiring a second connection that cannot.
301
+ */
302
+ const TEST_POOL_MAX_CONNECTIONS = 1;
303
+ /**
304
+ * How long a single connection attempt may take before the driver gives up.
305
+ *
306
+ * Under fork parallelism all forks race to open their connection at once
307
+ * while each imports the module graph, so at startup the connect handshakes
308
+ * contend. A generous per-attempt timeout costs nothing on the happy path (a
309
+ * healthy connect resolves in milliseconds and never approaches this
310
+ * ceiling), so it stays as cheap insurance for slow CI service-container
311
+ * startup.
312
+ */
313
+ const TEST_CONNECT_TIMEOUT = Duration.seconds(30);
314
+ /** Sentinel cause used to force the outer test transaction to roll back. */
315
+ const ROLLBACK_TAG = "__effect_test_rollback__";
316
+ const rollback = { _tag: ROLLBACK_TAG };
317
+ const isRollback = (u) => Predicate.isObjectOrArray(u) && Predicate.isTagged(u, ROLLBACK_TAG);
318
+ /**
319
+ * Whether a captured failure cause carries the rollback sentinel.
320
+ *
321
+ * Effect causes are nested objects; this is a structural walk that finds the
322
+ * rollback tag without depending on `Cause` internals.
323
+ */
324
+ const causeContainsRollback = (cause) => {
325
+ const seen = /* @__PURE__ */ new Set();
326
+ const stack = [cause];
327
+ while (Arr.isReadonlyArrayNonEmpty(stack)) {
328
+ const node = stack.pop();
329
+ if (!Predicate.isObjectOrArray(node) || seen.has(node)) continue;
330
+ seen.add(node);
331
+ if (isRollback(node)) return true;
332
+ Arr.forEach(Array.isArray(node) ? node : Record.values(node), (value) => {
333
+ stack.push(value);
334
+ });
335
+ }
336
+ return false;
337
+ };
338
+ const CONNECT_RETRY_ATTEMPTS = 5;
339
+ const CONNECT_RETRY_BASE_DELAY = Duration.millis(250);
340
+ const connectRetrySchedule = Schedule.max([Schedule.exponential(CONNECT_RETRY_BASE_DELAY), Schedule.recurs(CONNECT_RETRY_ATTEMPTS)]).pipe(Schedule.jittered);
341
+ /**
342
+ * A failure of the runner itself rather than of the spec it ran: the outer
343
+ * transaction failed before the spec's `Exit` was captured, or failed with
344
+ * something other than the rollback sentinel. Thrown (rejecting the runner's
345
+ * Promise), never put in an Effect error channel — the runner is the test
346
+ * harness's boundary.
347
+ */
348
+ var TestTransactionError = class extends Schema.TaggedError()("TestTransactionError", { message: Schema.String }) {};
349
+ /**
350
+ * Builds the always-rollback test transaction runner for one application's
351
+ * database services.
352
+ *
353
+ * The services carry the key strings and the `transact` boundary, so the
354
+ * runner needs nothing else: a suite calls this once next to its
355
+ * {@link makePgDatabase} call and exports the three helpers.
356
+ */
357
+ const makeTestTransaction = (database) => {
358
+ const runInTestTransaction = async (runtime, effect) => {
359
+ let captured = null;
360
+ const program = Effect.gen(function* () {
361
+ return yield* (yield* database.Database).transaction(() => Effect.gen(function* () {
362
+ captured = yield* Effect.exit(database.transact(effect));
363
+ return yield* Effect.fail(rollback);
364
+ }));
365
+ }).pipe(Effect.retry({
366
+ schedule: connectRetrySchedule,
367
+ while: (e) => captured === null && isRetryableConnectFailure(e)
368
+ }));
369
+ const outer = await runtime.runPromiseExit(program);
370
+ const innerExit = captured;
371
+ if (!innerExit) {
372
+ if (Exit.isFailure(outer)) throw new TestTransactionError({ message: `runInTestTransaction: outer transaction failed: ${String(outer.cause)}` });
373
+ throw new TestTransactionError({ message: "runInTestTransaction: inner Exit was never captured" });
374
+ }
375
+ if (Exit.isFailure(outer) && !causeContainsRollback(outer.cause)) throw new TestTransactionError({ message: `runInTestTransaction: unexpected outer failure: ${String(outer.cause)}` });
376
+ if (Exit.isSuccess(innerExit)) return innerExit.value;
377
+ throw Cause.squash(innerExit.cause);
378
+ };
379
+ return {
380
+ runInTestTransaction,
381
+ withTestTransaction: (runtime) => (effect) => runInTestTransaction(runtime, effect),
382
+ attemptInSavepoint: (effect) => Effect.gen(function* () {
383
+ const tx = yield* database.CurrentTransaction;
384
+ return yield* Effect.exit(tx.transaction(() => effect));
385
+ })
386
+ };
387
+ };
388
+ /**
389
+ * The `Database` layer an integration suite runs against: the application's
390
+ * own {@link PgDatabase.layer} over a `PgClient` built from `url`, with the
391
+ * pool pinned to one connection (see {@link TEST_POOL_MAX_CONNECTIONS}).
392
+ *
393
+ * Built once per process and reused by every spec in it. `url` is the
394
+ * caller's: this library does not read `DATABASE_URL_TEST` or decide how a
395
+ * test database is provisioned.
396
+ *
397
+ * Unlike production, this uses the pinging {@link pgClientFromUrl} on
398
+ * purpose — a suite that cannot reach its database should fail at layer build
399
+ * with one legible `SqlError`, not once per spec on the first query.
400
+ */
401
+ const testDatabaseLayer = (database, options) => database.layer.pipe(Layer.provide(pgClientFromUrl({
402
+ url: options.url,
403
+ maxConnections: TEST_POOL_MAX_CONNECTIONS,
404
+ connectTimeout: options.connectTimeout ?? TEST_CONNECT_TIMEOUT
405
+ })));
406
+ export { ContainerStartFailed, DatabaseUnreachable, InvalidDatabasePrefix, ServerUnreachable, connectionUrl, isRetryableConnectFailure, makeTestTransaction, probeServer, provisionTestDatabase, resolveDatabaseSource, testDatabaseLayer, testDatabaseName };
407
+
408
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","names":[],"sources":["../src/provisioning.ts","../src/testing.ts"],"mappings":"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"}
@@ -0,0 +1,217 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import * as Schema from "effect/Schema";
5
+ import * as Option from "effect/Option";
6
+ import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";
7
+ import { PlatformError } from "effect/PlatformError";
8
+ /**
9
+ * drizzle-kit's sentinel for "no real parent" (its `originUUID`): the genesis
10
+ * migration's `prevIds` names this instead of being empty.
11
+ */
12
+ export declare const ROOT_SENTINEL_ID = "00000000-0000-0000-0000-000000000000";
13
+ /** The identity every snapshot must carry for the DAG checks. */
14
+ interface Snapshot {
15
+ readonly id: string;
16
+ readonly prevIds: ReadonlyArray<string>;
17
+ }
18
+ /** One migration directory and its snapshot's identity. */
19
+ interface SnapshotEntry {
20
+ readonly dir: string;
21
+ readonly snapshot: Snapshot;
22
+ }
23
+ declare const MalformedSnapshot_base: Schema.Class<MalformedSnapshot, Schema.TaggedStruct<"MalformedSnapshot", {
24
+ readonly file: Schema.String;
25
+ readonly cause: Schema.Defect;
26
+ }>, import("effect/Cause").YieldableError>;
27
+ /** A snapshot.json could not be read as a drizzle-kit snapshot. */
28
+ export declare class MalformedSnapshot extends MalformedSnapshot_base {
29
+ get message(): string;
30
+ }
31
+ declare const SchemaDiffError_base: Schema.Class<SchemaDiffError, Schema.TaggedStruct<"SchemaDiffError", {
32
+ readonly cause: Schema.Defect;
33
+ }>, import("effect/Cause").YieldableError>;
34
+ /**
35
+ * drizzle-kit itself failed while generating or diffing a snapshot — broken
36
+ * input (a schema module drizzle-kit cannot read) rather than drift.
37
+ */
38
+ export declare class SchemaDiffError extends SchemaDiffError_base {
39
+ get message(): string;
40
+ }
41
+ declare const MigrationDriftError_base: Schema.Class<MigrationDriftError, Schema.TaggedStruct<"MigrationDriftError", {
42
+ readonly issues: Schema.$Array<Schema.String>;
43
+ }>, import("effect/Cause").YieldableError>;
44
+ /** The gate found problems; `issues` holds one human-readable report per violation. */
45
+ export declare class MigrationDriftError extends MigrationDriftError_base {
46
+ get message(): string;
47
+ }
48
+ /** Snapshots not named in any other snapshot's `prevIds` — the DAG's tips. */
49
+ export declare const findHeads: (entries: ReadonlyArray<SnapshotEntry>) => ReadonlyArray<SnapshotEntry>;
50
+ /**
51
+ * Every snapshot id is unique, every `prevIds` reference resolves (or is
52
+ * {@link ROOT_SENTINEL_ID}), the lineage has exactly one head and no cycle,
53
+ * and every snapshot is reachable from that head. One human-readable report
54
+ * per violation; empty when clean.
55
+ */
56
+ export declare const checkDagIntegrity: (entries: ReadonlyArray<SnapshotEntry>) => ReadonlyArray<string>;
57
+ /** The base branch's migration state: its directory names and its latest-by-name snapshot. */
58
+ interface BaseMigrations {
59
+ readonly ref: string;
60
+ readonly dirs: ReadonlyArray<string>;
61
+ readonly head: Option.Option<SnapshotEntry>;
62
+ }
63
+ /**
64
+ * Every migration this branch ADDS must descend from the base's current
65
+ * head. Presence of the base head's snapshot id in this checkout's entries is
66
+ * sufficient: {@link checkDagIntegrity} separately proves the local DAG has a
67
+ * single head from which everything is reachable, so containing the base head
68
+ * means the new tail grew on top of it. A branch that adds no migration
69
+ * directory passes.
70
+ */
71
+ export declare const checkBaseAncestry: (entries: ReadonlyArray<SnapshotEntry>, base: BaseMigrations) => ReadonlyArray<string>;
72
+ /** The environment {@link resolveBaseRef} reads — the shape of `process.env`. */
73
+ interface BaseRefEnvironment {
74
+ readonly [name: string]: string | undefined;
75
+ }
76
+ /**
77
+ * The base ref for the ancestry check, when one is knowable: an explicit
78
+ * `MIGRATION_BASE_REF` wins (local use: `MIGRATION_BASE_REF=origin/trunk`),
79
+ * else `origin/$GITHUB_BASE_REF` — which GitHub Actions sets only on
80
+ * `pull_request` events, so pushes and local runs skip the check.
81
+ */
82
+ export declare const resolveBaseRef: (env: BaseRefEnvironment) => Option.Option<string>;
83
+ /** Where the base branch's migrations are read from, through `git`. */
84
+ interface GitBase {
85
+ /** The base ref, e.g. `origin/trunk` — from {@link resolveBaseRef}. */
86
+ readonly ref: string;
87
+ /** The migrations directory relative to the repository root, e.g. `packages/database/migrations`. */
88
+ readonly migrationsPath: string;
89
+ /** The repository checkout to run `git` in. */
90
+ readonly cwd: string;
91
+ }
92
+ /**
93
+ * The base branch's migration state, read with `git ls-tree` / `git show` so
94
+ * it reflects the base's CURRENT tip. Fetches the base first when the
95
+ * checkout lacks it (a shallow pull-request checkout does).
96
+ */
97
+ export declare const readBaseMigrations: (base: GitBase) => Effect.Effect<BaseMigrations, PlatformError, ChildProcessSpawner>;
98
+ /** The inputs {@link checkMigrationDrift} takes. */
99
+ interface MigrationDriftOptions {
100
+ /** The schema module (`import * as schema from "./schema.ts"`), as drizzle-kit's `generateDrizzleJson` takes it. */
101
+ readonly schema: Record<string, unknown>;
102
+ /** The committed drizzle-kit migrations directory. */
103
+ readonly migrationsDir: string;
104
+ /**
105
+ * The base branch to check ancestry against — `Option.none()` (or absent,
106
+ * or `undefined`) on pushes and local runs, which is what mapping
107
+ * {@link resolveBaseRef} gives outside a pull request.
108
+ */
109
+ readonly base?: Option.Option<GitBase> | undefined;
110
+ }
111
+ /** What a passing run checked. */
112
+ interface MigrationDriftReport {
113
+ readonly snapshots: number;
114
+ readonly baseRef: Option.Option<string>;
115
+ }
116
+ /**
117
+ * Run the three checks. Succeeds with a {@link MigrationDriftReport};
118
+ * fails with a {@link MigrationDriftError} carrying every issue found.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * // An application's `db:migration-drift` task, run with `NodeRuntime.runMain`.
123
+ * import * as NodeServices from "@effect/platform-node/NodeServices";
124
+ * import { checkMigrationDrift, resolveBaseRef } from "@thomasfosterau/effect-drizzle-ext-pg/tooling";
125
+ * import { integer, pgTable } from "drizzle-orm/pg-core";
126
+ * import * as Effect from "effect/Effect";
127
+ * import * as Option from "effect/Option";
128
+ * import * as path from "node:path";
129
+ *
130
+ * const schema = { widget: pgTable("widget", { id: integer().primaryKey() }) };
131
+ * const repoRoot = process.cwd();
132
+ * const base = Option.map(resolveBaseRef(process.env), (ref) => ({
133
+ * ref,
134
+ * migrationsPath: "packages/database/migrations",
135
+ * cwd: repoRoot,
136
+ * }));
137
+ * export const program = checkMigrationDrift({
138
+ * schema,
139
+ * migrationsDir: path.join(repoRoot, "packages/database/migrations"),
140
+ * base,
141
+ * }).pipe(Effect.provide(NodeServices.layer));
142
+ * ```
143
+ */
144
+ export declare const checkMigrationDrift: (options: MigrationDriftOptions) => Effect.Effect<MigrationDriftReport, MigrationDriftError | MalformedSnapshot | SchemaDiffError | PlatformError, FileSystem.FileSystem | Path.Path | ChildProcessSpawner>;
145
+ /** One view as drizzle-kit would create it. */
146
+ interface ViewDefinition {
147
+ readonly schema: string;
148
+ readonly name: string;
149
+ readonly sql: string;
150
+ }
151
+ /** A view that selects from another view. */
152
+ interface ViewEdge {
153
+ readonly from: ViewDefinition;
154
+ readonly to: ViewDefinition;
155
+ }
156
+ /** `schema.name`, the identity used in messages and in reference matching. */
157
+ export declare const viewKey: (view: {
158
+ readonly schema: string;
159
+ readonly name: string;
160
+ }) => string;
161
+ /** Every `CREATE VIEW` in a statement list, as {@link ViewDefinition}s. */
162
+ export declare const parseViewDefinitions: (statements: ReadonlyArray<string>) => ReadonlyArray<ViewDefinition>;
163
+ /**
164
+ * Whether `definition` selects from `target`.
165
+ *
166
+ * Matches both spellings a definition can carry: the quoted,
167
+ * schema-qualified form Drizzle's query builder emits (`"app"."work_view"`),
168
+ * and the bare form a hand-written `sql.raw` FROM clause may use
169
+ * (`app.work_view`), guarded so it cannot fire on a longer identifier ending
170
+ * the same way. A derived table aliased with a view's *unqualified* name is
171
+ * deliberately not a match — it names no schema, so references no view.
172
+ * The one blind spot that leaves: a hand-written FROM naming a view
173
+ * unqualified and leaning on `search_path`.
174
+ */
175
+ export declare const referencesView: (definition: ViewDefinition, target: ViewDefinition) => boolean;
176
+ /** Every view-on-view edge among `views`. Empty is the passing state. */
177
+ export declare const findViewEdges: (views: ReadonlyArray<ViewDefinition>) => ReadonlyArray<ViewEdge>;
178
+ declare const ViewDependencyError_base: Schema.Class<ViewDependencyError, Schema.TaggedStruct<"ViewDependencyError", {
179
+ readonly edges: Schema.$Array<Schema.Struct<{
180
+ readonly from: Schema.String;
181
+ readonly to: Schema.String;
182
+ }>>;
183
+ }>, import("effect/Cause").YieldableError>;
184
+ /** The schema has at least one view-on-view edge. */
185
+ export declare class ViewDependencyError extends ViewDependencyError_base {
186
+ get message(): string;
187
+ }
188
+ /**
189
+ * Generate the whole schema from an empty snapshot — every view exactly as a
190
+ * migration would create it — and assert no definition references another
191
+ * view. Succeeds with the number of views checked.
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * // An application's `db:view-dependencies` task passes its own schema module.
196
+ * import { checkViewDependencies } from "@thomasfosterau/effect-drizzle-ext-pg/tooling";
197
+ * import { integer, pgTable, pgView, text } from "drizzle-orm/pg-core";
198
+ * import * as Effect from "effect/Effect";
199
+ *
200
+ * const article = pgTable("article", { id: integer().primaryKey(), title: text().notNull() });
201
+ * const articleTitles = pgView("article_titles").as((qb) =>
202
+ * qb.select({ title: article.title }).from(article),
203
+ * );
204
+ *
205
+ * const { viewCount } = await Effect.runPromise(
206
+ * checkViewDependencies({ schema: { article, articleTitles } }),
207
+ * );
208
+ * console.log(viewCount); // 1
209
+ * ```
210
+ */
211
+ export declare const checkViewDependencies: (options: {
212
+ readonly schema: Record<string, unknown>;
213
+ }) => Effect.Effect<{
214
+ readonly viewCount: number;
215
+ }, ViewDependencyError | SchemaDiffError>;
216
+ export type { BaseMigrations, BaseRefEnvironment, GitBase, MigrationDriftOptions, MigrationDriftReport, Snapshot, SnapshotEntry, ViewDefinition, ViewEdge };
217
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/tooling/migration-drift.ts","../../src/tooling/view-dependencies.ts"],"mappings":";;;;;;;;;;;qBA+Da;;UAMI;WACN;WACA,SAAS;;;UAIH;WACN;WACA,UAAU;;;;;;;qBAsCR,0BAA0B;MAIxB;;;;;;;;;qBASF,wBAAwB;MAGtB;;;;;;qBAUF,4BAA4B;MAI1B;;;qBAQF,YAAS,SAAa,cAAc,mBAAiB,cAAc;;;;;;;qBA0EnE,oBAAiB,SAAa,cAAc,mBAAiB;;UAiEzD;WACN;WACA,MAAM;WACN,MAAM,OAAO,OAAO;;;;;;;;;;qBAWlB,oBAAiB,SACnB,cAAc,gBAAc,MAC/B,mBACL;;UAiBc;YAEL;;;;;;;;qBASC,iBAAc,KAAS,uBAAqB,OAAO;;UA6E/C;;WAEN;;WAEA;;WAEA;;;;;;;qBAqCE,qBAAkB,MACvB,YACL,OAAO,OAAO,gBAAgB,eAAe;;UA6G/B;;WAGN,QAAQ;;WAER;;;;;;WAOA,OAAO,OAAO,OAAO;;;UAIf;WACN;WACA,SAAS,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA+Bd,sBAAmB,SACrB,0BACR,OAAO,OACR,sBACA,sBAAsB,oBAAoB,kBAAkB,eAC5D,WAAW,aAAa,KAAK,OAAO;;UCxkBrB;WACN;WACA;WACA;;;UAIM;WACN,MAAM;WACN,IAAI;;;qBAUF,UAAO;WAAqB;WAAyB;;;qBAIrD,uBAAoB,YACnB,0BACX,cAAc;;;;;;;;;;;;;qBAyBJ,iBAAc,YAAgB,gBAAc,QAAU;;qBAQtD,gBAAa,OAAW,cAAc,oBAAkB,cAAc;;;;;;;;qBAMtE,4BAA4B;MAM1B;;;;;;;;;;;;;;;;;;;;;;;;;qBA6CF,wBAAqB;WACvB,QAAQ;MACf,OAAO;WAAkB;GAAqB,sBAAsB"}