@shivaedev/effect-test-sql 0.5.4 → 0.6.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +40 -13
  3. package/dist/server/database.d.ts +3 -0
  4. package/dist/server/database.d.ts.map +1 -1
  5. package/dist/server/database.js.map +1 -1
  6. package/dist/server/internal/databaseCleanup.d.ts +7 -0
  7. package/dist/server/internal/databaseCleanup.d.ts.map +1 -0
  8. package/dist/server/internal/databaseCleanup.js +34 -0
  9. package/dist/server/internal/databaseCleanup.js.map +1 -0
  10. package/dist/server/internal/databaseOwner.d.ts +3 -0
  11. package/dist/server/internal/databaseOwner.d.ts.map +1 -0
  12. package/dist/server/internal/databaseOwner.js +24 -0
  13. package/dist/server/internal/databaseOwner.js.map +1 -0
  14. package/dist/server/internal/databases.d.ts +2 -1
  15. package/dist/server/internal/databases.d.ts.map +1 -1
  16. package/dist/server/internal/databases.js +5 -4
  17. package/dist/server/internal/databases.js.map +1 -1
  18. package/dist/server/postgres.d.ts +4 -3
  19. package/dist/server/postgres.d.ts.map +1 -1
  20. package/dist/server/postgres.js +13 -8
  21. package/dist/server/postgres.js.map +1 -1
  22. package/dist/server/withIsolatedDatabase.d.ts +4 -2
  23. package/dist/server/withIsolatedDatabase.d.ts.map +1 -1
  24. package/dist/server/withIsolatedDatabase.js +2 -2
  25. package/dist/server/withIsolatedDatabase.js.map +1 -1
  26. package/package.json +7 -10
  27. package/src/server/database.ts +5 -0
  28. package/src/server/internal/databaseCleanup.ts +40 -0
  29. package/src/server/internal/databaseOwner.ts +25 -0
  30. package/src/server/internal/databases.ts +10 -5
  31. package/src/server/postgres.ts +21 -11
  32. package/src/server/withIsolatedDatabase.ts +3 -3
  33. package/docs/north-star.md +0 -9
  34. package/docs/roadmap.md +0 -20
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 - 2026-10-18
4
+
5
+ ### Added
6
+
7
+ - `PostgresTestDatabase.awaitCleanup`, a native `Effect<void>` that waits for already-queued drops and preserves cleanup failures as SQL defects.
8
+ - Select an empty or migrated database from one test layer with `PostgresTestDatabase.clone(template)` or `withIsolatedDatabase(effect, { template })`. `PostgresTestTemplate` names the choices; migrated clones remain the default. Story traits can choose a template before acquiring their app services.
9
+ - Accept a scoped `PostgresTestLoader` callback in `fixtures` that receives the migrated template's redacted URL, so app options can open their own pool during once-per-layer preparation. Existing fixture Effects remain supported.
10
+
11
+ ### Changed
12
+
13
+ - Drop isolated databases in the background and await outstanding cleanup when the PostgreSQL test layer closes.
14
+
15
+ ### Fixed
16
+
17
+ - Retry database drops when PostgreSQL's connection budget is exhausted, and sweep abandoned local test databases at startup. Retries use live time even when a test clock is paused. Permanent cleanup failures still surface as SQL defects when the layer closes.
18
+
19
+ ## 0.5.5 - 2026-10-18
20
+
21
+ ### Changed
22
+
23
+ - Release with updated workspace dependencies.
24
+
3
25
  ## 0.5.4 - 2026-10-18
4
26
 
5
27
  ### Fixed
package/README.md CHANGED
@@ -22,9 +22,9 @@ The next test still sees the original name. Tests that must observe a committed
22
22
 
23
23
  ### Prepare one fixture
24
24
 
25
- `makePostgresTestLayer` creates a template database through an administrative connection, runs `migratePostgres` and optional fixtures there, then closes its preparation connections. It creates a shared test database from that template and provides `SqlClient`, `PgClient`, `Reactivity` and `PostgresTestDatabase`. The template remains closed and unchanged while tests run. Each isolated database is another copy of that template; its migrations and fixtures do not run again. Set `prepare: false` for migration tests: both migrations and fixtures are skipped, so the shared database and each clone start empty.
25
+ `makePostgresTestLayer` creates an empty template and a migrated template through an administrative connection, runs `migratePostgres` and optional fixtures in the migrated template, then closes its preparation connections. It creates a shared test database from the migrated template and provides `SqlClient`, `PgClient`, `Reactivity` and `PostgresTestDatabase`. The templates remain closed and unchanged while tests run. Each isolated database copies the template it asks for; migrations and fixtures do not run again when cloning. Set `prepare: false` for a suite that needs only empty databases: both migrations and fixtures are skipped.
26
26
 
27
- The Layer's scope owns the template, shared database and their connections. `makeEffectIt` builds the Layer once for its worker fixture. Each test's wrapper owns the transaction or isolated database. Closing those scopes releases the connections and drops the disposable databases; the administrative database named by `url` remains in place.
27
+ The Layer's scope owns the template, shared database and their connections. `makeEffectIt` builds the Layer once for its worker fixture. Each test's wrapper owns the transaction or isolated database. Closing a test scope releases its connections and queues its database drop in the background. Closing the Layer waits for all queued drops. To wait earlier, run `PostgresTestDatabase.awaitCleanup` after the isolated scope ends; it waits for drops queued before that Effect starts and reports cleanup failures as SQL defects. The shared database and templates stay available; the administrative database named by `url` remains in place.
28
28
 
29
29
  Define the tester in test support:
30
30
 
@@ -61,7 +61,23 @@ export const { effectApp } = makeEffectIt({
61
61
  });
62
62
  ```
63
63
 
64
- Use the application's migration loader in place of the example. `migrations` takes the existing `PostgresMigrationOptions`: `loader`, `lockTimeout`, and an optional ledger `table`. Fixtures are an Effect with access to the native PostgreSQL services. Additional migration or fixture dependencies remain requirements of the Layer and must be supplied before passing it to the runner. Setup failures remain failures; acquired databases are dropped when the acquisition scope closes.
64
+ Use the application's migration loader in place of the example. `migrations` takes effect-sql's `PostgresMigrationOptions`. Fixtures are an Effect with access to the native PostgreSQL services. Additional migration or fixture dependencies remain requirements of the Layer and must be supplied before passing it to the runner. Setup failures remain failures; acquired databases are dropped when the acquisition scope closes.
65
+
66
+ Fixtures can also be a `PostgresTestLoader`: a callback from the migrated template's redacted URL to a scoped Effect. Use it when a loader needs application options with its own pool, such as Better Auth's options. Acquire the pool in that Effect with `Effect.acquireRelease` and close it in the release action. The callback runs once during preparation, after migrations, with the native SQL services available. Its resources close before the first clone. For example, a loader using a separate native PostgreSQL client can be written as:
67
+
68
+ ```ts
69
+ import * as PgClient from "@effect/sql-pg/PgClient";
70
+
71
+ const layer = makePostgresTestLayer({
72
+ url,
73
+ migrations: appMigrations,
74
+ fixtures: (templateUrl) => Effect.gen(function* () {
75
+ const client = yield* PgClient.makeClient({ url: templateUrl });
76
+ yield* client`create table app_fixture (name text not null)`;
77
+ yield* client`insert into app_fixture values ('Original')`;
78
+ }),
79
+ });
80
+ ```
65
81
 
66
82
  ### Budget PostgreSQL connections
67
83
 
@@ -91,7 +107,7 @@ PostgreSQL sequence advances survive rollback. Avoid asserting exact generated s
91
107
 
92
108
  ### Real commits: an isolated database
93
109
 
94
- Use `withIsolatedDatabase` for after-commit effects, LISTEN/NOTIFY, advisory-lock scenarios that need independent connections, migration tests, or work through several connections. It runs without a wrapping transaction on its clone. Ordinary autocommit and `transact` commit for real, and the clone is dropped when the wrapper finishes.
110
+ Use `withIsolatedDatabase` for after-commit effects, LISTEN/NOTIFY, advisory-lock scenarios that need independent connections, migration tests, or work through several connections. It runs without a wrapping transaction on its clone. Ordinary autocommit and `transact` commit for real, and the wrapper queues its clone for background cleanup when it finishes. The Layer waits for cleanup at the end of its scope.
95
111
 
96
112
  An isolated block can live inside the rollback tester above:
97
113
 
@@ -110,6 +126,21 @@ The clone starts from the template, including the original fixtures, even if the
110
126
 
111
127
  Inside an isolated block, `PostgresTestDatabase.url` names that clone. Use it to acquire additional native PostgreSQL connections in the block's scope. Native SQL services are replaced in the block's Context; application services acquired earlier retain the client they captured. Build application service Layers inside the isolated block when they acquire SQL clients; use `Layer.fresh` when reusing a Layer definition already acquired by the worker. Keep servers and background fibers in that scope so they finish before the database is dropped. Separate databases isolate database contents; external services and server capacity need separate planning.
112
128
 
129
+ ### Choose an empty database per story or trait
130
+
131
+ The default clone is migrated. A first-deploy or upgrade block can select the empty template from the same Layer:
132
+
133
+ ```ts
134
+ yield* withIsolatedDatabase(Effect.gen(function* () {
135
+ const sql = yield* SqlClient.SqlClient;
136
+ expect(yield* sql`select table_name from information_schema.tables where table_schema = 'public'`).toEqual([]);
137
+ }), { template: "empty" });
138
+ ```
139
+
140
+ For a `test-story` kit that chooses through traits, keep a `template: PostgresTestTemplate` field on its engine, initially `"migrated"`. Add a database stage before the stages that boot the app. A trait in that stage sets `engine.template = "empty"`; the stage's `after` hook acquires `yield* database.clone(engine.template)` from `PostgresTestDatabase`. Run app acquisition and verbs with `Effect.provideContext(services)` using that returned Context. The story's Scope owns the clone and its connections. Do not also use `around: withIsolatedDatabase` for this kit, since that would acquire a migrated clone before the traits run.
141
+
142
+ Both selections copy a closed PostgreSQL template with `CREATE DATABASE ... TEMPLATE`; neither selection executes migrations or fixture loaders. The amount of preparation work does not add per-clone migration or loader work. PostgreSQL still has to copy the template's stored data.
143
+
113
144
  ### Test a backfill against existing rows
114
145
 
115
146
  A backfill needs rows in the old schema. Prepare an empty template with the application's own `PostgresMigrationOptions`, then use `migrationSteps` inside `withIsolatedDatabase`. `migrateTo` accepts the migration name as written without a file extension (`"001_create_contacts"`), its unpadded form (`"1_create_contacts"`), or its bare numeric id (`1`, `"1"` or `"001"`). It commits migrations through that target; seed rows with the native SQL client, then yield `migrateToLatest` to apply the remaining migrations on the same database.
@@ -173,30 +204,26 @@ Use `prepare: false` when preparing the test Layer, and `withIsolatedDatabase` f
173
204
 
174
205
  The assertion uses the SQL `Model`'s selected database fields, including generated ids and excluding JSON-only fields. Supply an exact PostgreSQL type for every field: schemas such as `Schema.Number` do not determine whether storage is `integer`, `numeric` or `double precision`. It fails with a typed `ModelColumnMismatch` containing all detected missing, extra and wrongly typed column problems. The type map is checked against the model both by TypeScript and at runtime. This check covers names and types; it does not check defaults, nullability, constraints or application data transformations.
175
206
 
176
- The starter's notes migration test uses this path with its repository model, so applications generated with `pnpm create @shivaedev/app` include an upgrade check.
177
-
178
207
  ### API
179
208
 
180
209
  Import each name from the module that defines it. The package has no root entry.
181
210
 
182
211
  | Module | Exports |
183
212
  | --- | --- |
184
- | `@shivaedev/effect-test-sql/postgres.ts` | `makePostgresTestLayer({ url, migrations, fixtures?, maxConnections?, prepare? })`, `PostgresTestOptions` |
213
+ | `@shivaedev/effect-test-sql/postgres.ts` | `makePostgresTestLayer({ url, migrations, fixtures?, maxConnections?, prepare? })`, `PostgresTestOptions`, `PostgresTestLoader` |
185
214
  | `@shivaedev/effect-test-sql/withTestTransaction.ts` | `withTestTransaction(effect)` |
186
- | `@shivaedev/effect-test-sql/withIsolatedDatabase.ts` | `withIsolatedDatabase(effect)` |
187
- | `@shivaedev/effect-test-sql/database.ts` | `PostgresTestDatabase`, `PostgresTestDatabaseService`, `PostgresTestServices` |
215
+ | `@shivaedev/effect-test-sql/withIsolatedDatabase.ts` | `withIsolatedDatabase(effect, { template? }?)` |
216
+ | `@shivaedev/effect-test-sql/database.ts` | `PostgresTestDatabase`, `PostgresTestDatabaseService`, `PostgresTestServices`, `PostgresTestTemplate` |
188
217
  | `@shivaedev/effect-test-sql/migrationSteps.ts` | `migrationSteps(appMigrations)` returning `{ migrateTo(name), migrateToLatest }` |
189
218
  | `@shivaedev/effect-test-sql/migrationUpgrade.ts` | `migrationUpgrade({ migrations, upTo, table })`, `MigrationUpgradeOptions`, `PostgresColumn` |
190
219
  | `@shivaedev/effect-test-sql/assertModelColumns.ts` | `assertModelColumns(columns, model, types)`, `ModelColumnMismatch` |
191
220
 
192
- `url` is a redacted PostgreSQL URL for an existing administrative database. `PostgresTestDatabase` supplies the current database's redacted `url` and a scoped `isolated` Effect that acquires another clone's service Context. Prefer `withIsolatedDatabase` to acquire and release that Context around a test. Wrappers retain the body's result, failures and unmet service requirements, with native `SqlError` added for acquisition or transaction failures. Cleanup failures are defects rather than successful cleanup.
221
+ `url` is a redacted PostgreSQL URL for an existing administrative database. `PostgresTestDatabase` supplies the current database's redacted `url`, an `awaitCleanup: Effect<void>` drain, a scoped `isolated` Effect that acquires a migrated clone's service Context, and `clone(template)` for an explicit `"empty"` or `"migrated"` choice. With `prepare: false`, both choices are empty. Prefer `withIsolatedDatabase` to acquire and release that Context around a test. Wrappers retain the body's result, failures and unmet service requirements, with native `SqlError` added for acquisition or transaction failures. Cleanup failures surface as SQL defects when the Layer closes. Drops that fail with connection exhaustion (SQLSTATE `53300`) retry every 100 ms of live time for up to 300 retries, even while a test clock is paused; if capacity does not return, the SQL defect remains visible. Startup sweeps databases left by dead processes on the same host and databases from the older unowned naming format. Databases owned by live processes or another host are retained, so local parallel workers keep their databases. Use the same host to reclaim its abandoned databases.
193
222
 
194
- ### Install, setup and evidence
223
+ ### Install and setup
195
224
 
196
225
  ```sh
197
226
  pnpm add --save-dev @shivaedev/effect-test-sql @shivaedev/effect-test @effect/sql-pg @effect/vitest effect vitest
198
227
  ```
199
228
 
200
229
  Use versions compatible with the package's peer dependencies. The package requires Node.js 24 or later. The PostgreSQL role must be able to create and drop databases; use a dedicated test server. PostgreSQL 13 or later supports the forced database drops used for cleanup. The migrations and fixtures must complete their work during preparation; their scoped resources are closed before cloning.
201
-
202
- The package tests use `PLATFORM_EFFECT_TEST_SQL_TEST_DATABASE_URL`. Repository setup and CI prepare that database on real PostgreSQL. Local tests skip when that variable is absent; CI rejects its absence. The PostgreSQL tests prove overlapping same-row visibility, shared-key lock contention and release, rollback, nested savepoints, runner/harness scope composition, one-time template preparation, commits visible through another connection, and database cleanup. They also measure twelve concurrent clones through `pg_stat_activity` at both the default and overridden pool limits, verify administrative connection release, and exercise connection-limit diagnostics using restricted PostgreSQL roles. Compiler and packed-consumer tests prove the exercised public type and export contracts. These are library checks, not evidence for an application's servers or notification delivery.
@@ -4,7 +4,10 @@ import type * as Reactivity from "effect/reactivity/Reactivity";
4
4
  import type { SqlClient } from "effect/sql";
5
5
  import type { SqlError } from "effect/sql/SqlError";
6
6
  export type PostgresTestServices = PgClient.PgClient | SqlClient.SqlClient | Reactivity.Reactivity;
7
+ export type PostgresTestTemplate = "empty" | "migrated";
7
8
  export interface PostgresTestDatabaseService {
9
+ readonly awaitCleanup: Effect.Effect<void>;
10
+ readonly clone: (template: PostgresTestTemplate) => Effect.Effect<Context.Context<PostgresTestServices | PostgresTestDatabase>, SqlError, Scope.Scope>;
8
11
  readonly isolated: Effect.Effect<Context.Context<PostgresTestServices | PostgresTestDatabase>, SqlError, Scope.Scope>;
9
12
  readonly url: Redacted.Redacted<string>;
10
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/server/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,KAAK,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC;AAEnG,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,GAAG,oBAAoB,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACtH,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CACxC;;AAED,qBAAa,oBAAqB,SAAQ,yBAEzC;CAAG"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/server/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,KAAK,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC;AACnG,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,UAAU,CAAC;AAExD,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,CACf,QAAQ,EAAE,oBAAoB,KAC1B,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,GAAG,oBAAoB,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACxG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,GAAG,oBAAoB,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACtH,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CACxC;;AAED,qBAAa,oBAAqB,SAAQ,yBAEzC;CAAG"}
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/server/database.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAA0C,MAAM,QAAQ,CAAC;AAYzE,MAAM,OAAO,oBAAqB,SAAQ,OAAO,CAAC,OAAO,EAAqD,CAC7G,iDAAiD,CACjD;CAAG"}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/server/database.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAA0C,MAAM,QAAQ,CAAC;AAiBzE,MAAM,OAAO,oBAAqB,SAAQ,OAAO,CAAC,OAAO,EAAqD,CAC7G,iDAAiD,CACjD;CAAG"}
@@ -0,0 +1,7 @@
1
+ import { Effect } from "effect";
2
+ import type { SqlError } from "effect/sql/SqlError";
3
+ export declare const databaseCleanup: (url: import("effect/Redacted").Redacted<string>) => Effect.Effect<{
4
+ awaitCleanup: Effect.Effect<void, never, never>;
5
+ release: (name: string) => Effect.Effect<void, never, never>;
6
+ }, SqlError, import("effect/Scope").Scope>;
7
+ //# sourceMappingURL=databaseCleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"databaseCleanup.d.ts","sourceRoot":"","sources":["../../../src/server/internal/databaseCleanup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAyC,MAAM,QAAQ,CAAC;AAC9E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,eAAO,MAAM,eAAe;;oBAyBJ,MAAM;0CAS5B,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { Clock, Effect, Fiber, Predicate, Schedule } from "effect";
2
+ import { abandonedDatabase } from "./databaseOwner.js";
3
+ import { administer } from "./databases.js";
4
+ export const databaseCleanup = Effect.fn("PostgresTest.databaseCleanup")(function* (url) {
5
+ const pending = [];
6
+ function drop(name) {
7
+ return administer(url, (admin) => admin `drop database if exists ${admin(name)} with (force)`).pipe(Effect.retry({
8
+ schedule: Schedule.spaced("100 millis"),
9
+ times: 300,
10
+ while: (error) => Predicate.hasProperty(error.reason.cause, "code") && error.reason.cause.code === "53300",
11
+ }), Effect.provideService(Clock.Clock, Clock.Clock.defaultValue()));
12
+ }
13
+ const awaitCleanup = Effect.suspend(() => Effect.gen(function* () {
14
+ const exits = yield* Effect.forEach([...pending], Fiber.join);
15
+ yield* Effect.forEach(exits, (exit) => exit).pipe(Effect.orDie);
16
+ }));
17
+ yield* Effect.addFinalizer(() => awaitCleanup);
18
+ const leftovers = yield* administer(url, (admin) => admin `select datname from pg_database`);
19
+ for (const { datname } of leftovers) {
20
+ if (abandonedDatabase(datname)) {
21
+ yield* drop(datname);
22
+ }
23
+ }
24
+ function release(name) {
25
+ return Effect.gen(function* () {
26
+ const previous = pending.at(-1);
27
+ const ready = previous === undefined ? Effect.void : Fiber.join(previous);
28
+ const fiber = yield* Effect.andThen(ready, Effect.exit(drop(name))).pipe(Effect.uninterruptible, Effect.forkDetach);
29
+ pending.push(fiber);
30
+ });
31
+ }
32
+ return { awaitCleanup, release };
33
+ });
34
+ //# sourceMappingURL=databaseCleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"databaseCleanup.js","sourceRoot":"","sources":["../../../src/server/internal/databaseCleanup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function databaseName(): string;
2
+ export declare function abandonedDatabase(name: string): boolean;
3
+ //# sourceMappingURL=databaseOwner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"databaseOwner.d.ts","sourceRoot":"","sources":["../../../src/server/internal/databaseOwner.ts"],"names":[],"mappings":"AAMA,wBAAgB,YAAY,WAE3B;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,WAc7C"}
@@ -0,0 +1,24 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hostname } from "node:os";
3
+ import process from "node:process";
4
+ const host = createHash("sha256").update(hostname()).digest("hex").slice(0, 8);
5
+ export function databaseName() {
6
+ return `effect_test_${host}_${process.pid}_${crypto.randomUUID().replaceAll("-", "")}`;
7
+ }
8
+ export function abandonedDatabase(name) {
9
+ if (/^effect_test_[a-f0-9]{32}$/u.test(name)) {
10
+ return true;
11
+ }
12
+ const owner = /^effect_test_(?<host>[a-f0-9]{8})_(?<pid>[0-9]+)_[a-f0-9]{32}$/u.exec(name);
13
+ if (owner === null || owner.groups?.host !== host) {
14
+ return false;
15
+ }
16
+ try {
17
+ process.kill(Number(owner.groups.pid), 0);
18
+ return false;
19
+ }
20
+ catch (error) {
21
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
22
+ }
23
+ }
24
+ //# sourceMappingURL=databaseOwner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"databaseOwner.js","sourceRoot":"","sources":["../../../src/server/internal/databaseOwner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAE/E,MAAM,UAAU,YAAY;IAC3B,OAAO,eAAe,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;AACxF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC7C,IAAI,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,OAAO,IAAI,CAAC;IACb,CAAC;IACD,MAAM,KAAK,GAAG,iEAAiE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3F,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,EAAE,CAAC;QACnD,OAAO,KAAK,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1C,OAAO,KAAK,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC;IAC5E,CAAC;AACF,CAAC"}
@@ -4,7 +4,8 @@ import * as Reactivity from "effect/reactivity/Reactivity";
4
4
  import { SqlClient } from "effect/sql";
5
5
  import type { SqlError } from "effect/sql/SqlError";
6
6
  export declare function connection(url: Redacted.Redacted<string>, maxConnections: number): Layer.Layer<PgClient.PgClient | Reactivity.Reactivity | SqlClient.SqlClient, SqlError, never>;
7
- export declare const database: (url: Redacted.Redacted<string>, template?: any) => Effect.Effect<{
7
+ export declare const administer: <A>(url: Redacted.Redacted<string>, operation: (admin: PgClient.PgClient) => Effect.Effect<A, SqlError>) => Effect.Effect<A, SqlError, never>;
8
+ export declare const database: (url: Redacted.Redacted<string>, release: (name: string) => Effect.Effect<unknown>, template?: any) => Effect.Effect<{
8
9
  name: string;
9
10
  url: Redacted.Redacted<string>;
10
11
  }, SqlError, import("effect/Scope").Scope>;
@@ -1 +1 @@
1
- {"version":3,"file":"databases.d.ts","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAW,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,MAAM,iGAMhF;AAWD,eAAO,MAAM,QAAQ;;;0CAUnB,CAAC"}
1
+ {"version":3,"file":"databases.d.ts","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAW,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,wBAAgB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,MAAM,iGAMhF;AAED,eAAO,MAAM,UAAU,GAAmD,CAAC,qDAEvD,QAAQ,CAAC,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,sCAKlE,CAAC;AAEH,eAAO,MAAM,QAAQ,mDAEJ,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;;;0CAYhD,CAAC"}
@@ -3,18 +3,19 @@ import { Context, Effect, Layer, Redacted } from "effect";
3
3
  import * as Reactivity from "effect/reactivity/Reactivity";
4
4
  import { SqlClient } from "effect/sql";
5
5
  import { connectionLimitHint } from "./connectionLimitHint.js";
6
+ import { databaseName } from "./databaseOwner.js";
6
7
  export function connection(url, maxConnections) {
7
8
  return Layer.effectContext(Effect.map(PgClient.make({ maxConnections, url }), (client) => Context.make(SqlClient.SqlClient, client).pipe(Context.add(PgClient.PgClient, client)))).pipe(Layer.provideMerge(Layer.fresh(Reactivity.layer)));
8
9
  }
9
- const administer = Effect.fn("PostgresTest.administer")(function* (url, operation) {
10
+ export const administer = Effect.fn("PostgresTest.administer")(function* (url, operation) {
10
11
  return yield* Effect.scoped(Effect.flatMap(PgClient.makeClient({ url }), operation).pipe(Effect.mapError(connectionLimitHint))).pipe(Effect.provide(Reactivity.layer));
11
12
  });
12
- export const database = Effect.fn("PostgresTest.database")(function* (url, template = "template0") {
13
- const name = `effect_test_${crypto.randomUUID().replaceAll("-", "")}`;
13
+ export const database = Effect.fn("PostgresTest.database")(function* (url, release, template = "template0") {
14
+ const name = databaseName();
14
15
  const target = new URL(Redacted.value(url));
15
16
  target.pathname = `/${name}`;
16
17
  target.searchParams.delete("database");
17
- yield* Effect.acquireRelease(administer(url, (admin) => admin `create database ${admin(name)} template ${admin(template)}`), () => Effect.orDie(administer(url, (admin) => admin `drop database ${admin(name)} with (force)`)));
18
+ yield* Effect.acquireRelease(administer(url, (admin) => admin `create database ${admin(name)} template ${admin(template)}`), () => release(name));
18
19
  return { name, url: Redacted.make(target.toString()) };
19
20
  });
20
21
  //# sourceMappingURL=databases.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"databases.js","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,MAAM,UAAU,UAAU,CAAC,GAA8B,EAAE,cAAsB;IAChF,OAAO,KAAK,CAAC,aAAa,CACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAC7D,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CACtF,CACD,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,yBAAyB,CAAC,CAAC,QAAQ,CAAC,EAChE,GAA8B,EAC9B,SAAyE;IAEzE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,IAAI,CACnI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAChC,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,QAAQ,CAAC,EAAE,GAA8B,EAAE,QAAQ,GAAG,WAAW;IAC3H,MAAM,IAAI,GAAG,eAAe,MAAM,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;IACtE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,MAAM,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC;IAC7B,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACvC,KAAK,CAAC,CAAC,MAAM,CAAC,cAAc,CAC3B,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAA,mBAAmB,KAAK,CAAC,IAAI,CAAC,aAAa,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,EAC7F,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAA,iBAAiB,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAChG,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"databases.js","sourceRoot":"","sources":["../../../src/server/internal/databases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,UAAU,UAAU,CAAC,GAA8B,EAAE,cAAsB;IAChF,OAAO,KAAK,CAAC,aAAa,CACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAC7D,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CACtF,CACD,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,yBAAyB,CAAC,CAAC,QAAQ,CAAC,EACvE,GAA8B,EAC9B,SAAmE;IAEnE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,IAAI,CACnI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAChC,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,QAAQ,CAAC,EACnE,GAA8B,EAC9B,OAAiD,EACjD,QAAQ,GAAG,WAAW;IAEtB,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,MAAM,CAAC,QAAQ,GAAG,IAAI,IAAI,EAAE,CAAC;IAC7B,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACvC,KAAK,CAAC,CAAC,MAAM,CAAC,cAAc,CAC3B,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAA,mBAAmB,KAAK,CAAC,IAAI,CAAC,aAAa,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,EAC7F,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CACnB,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC,CAAC,CAAC"}
@@ -1,13 +1,14 @@
1
- import { Effect, Layer, type Redacted } from "effect";
1
+ import { Effect, Layer, type Redacted, type Scope } from "effect";
2
2
  import * as Reactivity from "effect/reactivity/Reactivity";
3
3
  import { type PostgresMigrationOptions } from "@shivaedev/effect-sql/migrations.ts";
4
4
  import { PostgresTestDatabase, type PostgresTestServices } from "#database.ts";
5
+ export type PostgresTestLoader<E = never, R = never> = (url: Redacted.Redacted<string>) => Effect.Effect<unknown, E, R | PostgresTestServices | Scope.Scope>;
5
6
  export interface PostgresTestOptions<E = never, R = never, TFixtures = never> {
6
- readonly fixtures?: Effect.Effect<unknown, E, TFixtures | PostgresTestServices>;
7
+ readonly fixtures?: Effect.Effect<unknown, E, TFixtures | PostgresTestServices | Scope.Scope> | PostgresTestLoader<E, TFixtures>;
7
8
  readonly maxConnections?: number;
8
9
  readonly migrations: PostgresMigrationOptions<R>;
9
10
  readonly prepare?: boolean;
10
11
  readonly url: Redacted.Redacted<string>;
11
12
  }
12
- export declare function makePostgresTestLayer<E = never, R = never, TFixtures = never>(options: PostgresTestOptions<E, R, TFixtures>): Layer.Layer<import("@effect/sql-pg/PgClient").PgClient | PostgresTestDatabase | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient, E | import("effect/sql/Migrator").MigrationError | import("effect/sql/SqlError").SqlError, Exclude<Exclude<Exclude<Exclude<R, import("@effect/sql-pg/PgClient").PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, import("effect/Scope").Scope>, import("effect/Scope").Scope>, Reactivity.Reactivity> | Exclude<Exclude<Exclude<Exclude<TFixtures, import("@effect/sql-pg/PgClient").PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, import("effect/Scope").Scope>, import("effect/Scope").Scope>, Reactivity.Reactivity>>;
13
+ export declare function makePostgresTestLayer<E = never, R = never, TFixtures = never>(options: PostgresTestOptions<E, R, TFixtures>): Layer.Layer<import("@effect/sql-pg/PgClient").PgClient | PostgresTestDatabase | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient, E | import("effect/sql/Migrator").MigrationError | import("effect/sql/SqlError").SqlError, Exclude<Exclude<Exclude<Exclude<R, import("@effect/sql-pg/PgClient").PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, Scope.Scope>, Scope.Scope>, Reactivity.Reactivity> | Exclude<Exclude<Exclude<Exclude<TFixtures, import("@effect/sql-pg/PgClient").PgClient | Reactivity.Reactivity | import("effect/sql/SqlClient").SqlClient>, Scope.Scope>, Scope.Scope>, Reactivity.Reactivity>>;
13
14
  //# sourceMappingURL=postgres.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,MAAM,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAC/D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAmB,KAAK,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAAoC,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAIjH,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,SAAS,GAAG,oBAAoB,CAAC,CAAC;IAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,wBAAgB,qBAAqB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,2sBA0B3H"}
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,MAAM,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC3E,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAmB,KAAK,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAAoC,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAKjH,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,CACtD,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,GAAG,oBAAoB,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;AAEvE,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,SAAS,GAAG,oBAAoB,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,kBAAkB,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACjI,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,wBAAgB,qBAAqB,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,GAAG,KAAK,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,uoBA+B3H"}
@@ -3,28 +3,33 @@ import * as Reactivity from "effect/reactivity/Reactivity";
3
3
  import { migratePostgres } from "@shivaedev/effect-sql/migrations.ts";
4
4
  import { PostgresTestDatabase } from "#database.ts";
5
5
  import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
6
+ import { databaseCleanup } from "#internal/databaseCleanup.ts";
6
7
  import { connection, database } from "#internal/databases.ts";
7
8
  export function makePostgresTestLayer(options) {
8
9
  return Layer.effectContext(Effect.gen(function* () {
9
10
  const maxConnections = options.maxConnections ?? 2;
10
- const template = yield* database(options.url);
11
+ const { awaitCleanup, release } = yield* databaseCleanup(options.url);
12
+ const empty = yield* database(options.url, release);
13
+ const template = options.prepare === false ? empty : yield* database(options.url, release, empty.name);
11
14
  if (options.prepare !== false) {
12
15
  yield* Effect.scoped(Effect.gen(function* () {
13
16
  const services = yield* Layer.build(connection(template.url, maxConnections));
14
17
  yield* migratePostgres(options.migrations).pipe(Effect.provideContext(services));
15
18
  if (options.fixtures !== undefined) {
16
- yield* options.fixtures.pipe(Effect.provideContext(services));
19
+ const fixtures = typeof options.fixtures === "function" ? options.fixtures(template.url) : options.fixtures;
20
+ yield* fixtures.pipe(Effect.provideContext(services));
17
21
  }
18
22
  }));
19
23
  }
20
- const shared = yield* database(options.url, template.name);
24
+ const shared = yield* database(options.url, release, template.name);
21
25
  const services = yield* Layer.build(connection(shared.url, maxConnections));
22
- const isolated = Effect.gen(function* () {
23
- const clone = yield* database(options.url, template.name);
24
- const context = yield* Layer.build(connection(clone.url, maxConnections));
25
- return Context.add(context, PostgresTestDatabase, { isolated, url: clone.url });
26
+ const clone = (source) => Effect.gen(function* () {
27
+ const target = yield* database(options.url, release, source === "empty" ? empty.name : template.name);
28
+ const context = yield* Layer.build(connection(target.url, maxConnections));
29
+ return Context.add(context, PostgresTestDatabase, { awaitCleanup, clone, isolated, url: target.url });
26
30
  });
27
- return Context.add(services, PostgresTestDatabase, { isolated, url: shared.url });
31
+ const isolated = clone("migrated");
32
+ return Context.add(services, PostgresTestDatabase, { awaitCleanup, clone, isolated, url: shared.url });
28
33
  }).pipe(Effect.mapError(connectionLimitHint))).pipe(Layer.provide(Reactivity.layer));
29
34
  }
30
35
  //# sourceMappingURL=postgres.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAiB,MAAM,QAAQ,CAAC;AAC/D,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAiC,MAAM,qCAAqC,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAA+D,MAAM,cAAc,CAAC;AACjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAU9D,MAAM,UAAU,qBAAqB,CAA0C,OAA6C;IAC3H,OAAO,KAAK,CAAC,aAAa,CACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,OAAO,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;gBAC9E,KAAK,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACjF,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;oBACpC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC/D,CAAC;YACF,CAAC,CAAC,CACF,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;QAC5E,MAAM,QAAQ,GAA4C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAC7E,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC1D,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC;YAC1E,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QACjF,CAAC,CAAC,CAAC;QACH,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAC7C,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;AACzC,CAAC"}
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/server/postgres.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  import { Effect } from "effect";
2
- import { PostgresTestDatabase } from "./database.ts";
3
- export declare function withIsolatedDatabase<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E | import("effect/sql/SqlError").SqlError, PostgresTestDatabase | Exclude<Exclude<Exclude<R, PostgresTestDatabase | import("./database.ts").PostgresTestServices>, import("effect/Scope").Scope>, import("effect/Scope").Scope>>;
2
+ import { PostgresTestDatabase, type PostgresTestTemplate } from "./database.ts";
3
+ export declare function withIsolatedDatabase<A, E, R>(effect: Effect.Effect<A, E, R>, options?: {
4
+ readonly template?: PostgresTestTemplate;
5
+ }): Effect.Effect<A, E | import("effect/sql/SqlError").SqlError, PostgresTestDatabase | Exclude<Exclude<Exclude<R, PostgresTestDatabase | import("./database.ts").PostgresTestServices>, import("effect/Scope").Scope>, import("effect/Scope").Scope>>;
4
6
  //# sourceMappingURL=withIsolatedDatabase.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"withIsolatedDatabase.d.ts","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,sPAQ3E"}
1
+ {"version":3,"file":"withIsolatedDatabase.d.ts","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEhF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAA;CAAE,sPAQnI"}
@@ -1,10 +1,10 @@
1
1
  import { Effect } from "effect";
2
2
  import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
3
3
  import { PostgresTestDatabase } from "./database.js";
4
- export function withIsolatedDatabase(effect) {
4
+ export function withIsolatedDatabase(effect, options) {
5
5
  return Effect.scoped(Effect.gen(function* () {
6
6
  const database = yield* PostgresTestDatabase;
7
- const services = yield* database.isolated;
7
+ const services = yield* options?.template === undefined ? database.isolated : database.clone(options.template);
8
8
  return yield* Effect.scoped(effect.pipe(Effect.provideContext(services)));
9
9
  })).pipe(Effect.mapError(connectionLimitHint));
10
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"withIsolatedDatabase.js","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,UAAU,oBAAoB,CAAU,MAA8B;IAC3E,OAAO,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,oBAAoB,CAAC;QAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC1C,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3E,CAAC,CAAC,CACF,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC;AAC9C,CAAC"}
1
+ {"version":3,"file":"withIsolatedDatabase.js","sourceRoot":"","sources":["../../src/server/withIsolatedDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAA6B,MAAM,eAAe,CAAC;AAEhF,MAAM,UAAU,oBAAoB,CAAU,MAA8B,EAAE,OAAsD;IACnI,OAAO,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,oBAAoB,CAAC;QAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,OAAO,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/G,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3E,CAAC,CAAC,CACF,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC;AAC9C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-test-sql",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "PostgreSQL rollback and real-commit test isolation for Effect",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-test-sql#readme",
6
6
  "bugs": {
@@ -24,9 +24,8 @@
24
24
  },
25
25
  "exports": {
26
26
  "./*.ts": {
27
- "types": "./dist/server/*.d.ts",
28
27
  "source": "./src/server/*.ts",
29
- "import": "./dist/server/*.js",
28
+ "types": "./dist/server/*.d.ts",
30
29
  "default": "./dist/server/*.js"
31
30
  },
32
31
  "./internal/*": null,
@@ -38,23 +37,21 @@
38
37
  "!src/**/*.test.ts",
39
38
  "!src/**/*.spec.ts",
40
39
  "!src/**/test-support",
41
- "docs",
42
40
  "CHANGELOG.md",
43
41
  "README.md"
44
42
  ],
45
43
  "dependencies": {
46
- "@shivaedev/effect-sql": "0.5.1"
44
+ "@shivaedev/effect-sql": "0.6.0"
47
45
  },
48
46
  "devDependencies": {
49
47
  "@effect/sql-pg": "4.0.4",
50
48
  "@effect/vitest": "4.0.4",
51
- "@shivaedev/effect-test": "0.5.0",
52
- "@shivaedev/quality": "0.23.0",
53
- "@shivaedev/test-story": "0.6.0",
49
+ "@shivaedev/effect-test": "0.6.0",
50
+ "@shivaedev/quality": "0.27.1",
51
+ "@shivaedev/test-story": "0.10.0",
54
52
  "@types/node": "24.19.2",
55
- "@typescript/native": "npm:typescript@7.0.2",
56
53
  "effect": "4.0.4",
57
- "typescript": "npm:@typescript/typescript6@6.0.2",
54
+ "typescript": "7.0.2",
58
55
  "vitest": "5.0.3"
59
56
  },
60
57
  "peerDependencies": {
@@ -5,8 +5,13 @@ import type { SqlClient } from "effect/sql";
5
5
  import type { SqlError } from "effect/sql/SqlError";
6
6
 
7
7
  export type PostgresTestServices = PgClient.PgClient | SqlClient.SqlClient | Reactivity.Reactivity;
8
+ export type PostgresTestTemplate = "empty" | "migrated";
8
9
 
9
10
  export interface PostgresTestDatabaseService {
11
+ readonly awaitCleanup: Effect.Effect<void>;
12
+ readonly clone: (
13
+ template: PostgresTestTemplate,
14
+ ) => Effect.Effect<Context.Context<PostgresTestServices | PostgresTestDatabase>, SqlError, Scope.Scope>;
10
15
  readonly isolated: Effect.Effect<Context.Context<PostgresTestServices | PostgresTestDatabase>, SqlError, Scope.Scope>;
11
16
  readonly url: Redacted.Redacted<string>;
12
17
  }
@@ -0,0 +1,40 @@
1
+ import { Clock, Effect, type Exit, Fiber, Predicate, Schedule } from "effect";
2
+ import type { SqlError } from "effect/sql/SqlError";
3
+ import { abandonedDatabase } from "./databaseOwner.ts";
4
+ import { administer } from "./databases.ts";
5
+
6
+ export const databaseCleanup = Effect.fn("PostgresTest.databaseCleanup")(function* (url: Parameters<typeof administer>[0]) {
7
+ const pending: Fiber.Fiber<Exit.Exit<unknown, SqlError>>[] = [];
8
+ function drop(name: string) {
9
+ return administer(url, (admin) => admin`drop database if exists ${admin(name)} with (force)`).pipe(
10
+ Effect.retry({
11
+ schedule: Schedule.spaced("100 millis"),
12
+ times: 300,
13
+ while: (error) => Predicate.hasProperty(error.reason.cause, "code") && error.reason.cause.code === "53300",
14
+ }),
15
+ Effect.provideService(Clock.Clock, Clock.Clock.defaultValue()),
16
+ );
17
+ }
18
+ const awaitCleanup = Effect.suspend(() =>
19
+ Effect.gen(function* () {
20
+ const exits = yield* Effect.forEach([...pending], Fiber.join);
21
+ yield* Effect.forEach(exits, (exit) => exit).pipe(Effect.orDie);
22
+ }),
23
+ );
24
+ yield* Effect.addFinalizer(() => awaitCleanup);
25
+ const leftovers = yield* administer(url, (admin) => admin<{ datname: string }>`select datname from pg_database`);
26
+ for (const { datname } of leftovers) {
27
+ if (abandonedDatabase(datname)) {
28
+ yield* drop(datname);
29
+ }
30
+ }
31
+ function release(name: string) {
32
+ return Effect.gen(function* () {
33
+ const previous = pending.at(-1);
34
+ const ready = previous === undefined ? Effect.void : Fiber.join(previous);
35
+ const fiber = yield* Effect.andThen(ready, Effect.exit(drop(name))).pipe(Effect.uninterruptible, Effect.forkDetach);
36
+ pending.push(fiber);
37
+ });
38
+ }
39
+ return { awaitCleanup, release };
40
+ });
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hostname } from "node:os";
3
+ import process from "node:process";
4
+
5
+ const host = createHash("sha256").update(hostname()).digest("hex").slice(0, 8);
6
+
7
+ export function databaseName() {
8
+ return `effect_test_${host}_${process.pid}_${crypto.randomUUID().replaceAll("-", "")}`;
9
+ }
10
+
11
+ export function abandonedDatabase(name: string) {
12
+ if (/^effect_test_[a-f0-9]{32}$/u.test(name)) {
13
+ return true;
14
+ }
15
+ const owner = /^effect_test_(?<host>[a-f0-9]{8})_(?<pid>[0-9]+)_[a-f0-9]{32}$/u.exec(name);
16
+ if (owner === null || owner.groups?.host !== host) {
17
+ return false;
18
+ }
19
+ try {
20
+ process.kill(Number(owner.groups.pid), 0);
21
+ return false;
22
+ } catch (error) {
23
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
24
+ }
25
+ }
@@ -4,6 +4,7 @@ import * as Reactivity from "effect/reactivity/Reactivity";
4
4
  import { SqlClient } from "effect/sql";
5
5
  import type { SqlError } from "effect/sql/SqlError";
6
6
  import { connectionLimitHint } from "./connectionLimitHint.ts";
7
+ import { databaseName } from "./databaseOwner.ts";
7
8
 
8
9
  export function connection(url: Redacted.Redacted<string>, maxConnections: number) {
9
10
  return Layer.effectContext(
@@ -13,23 +14,27 @@ export function connection(url: Redacted.Redacted<string>, maxConnections: numbe
13
14
  ).pipe(Layer.provideMerge(Layer.fresh(Reactivity.layer)));
14
15
  }
15
16
 
16
- const administer = Effect.fn("PostgresTest.administer")(function* (
17
+ export const administer = Effect.fn("PostgresTest.administer")(function* <A>(
17
18
  url: Redacted.Redacted<string>,
18
- operation: (admin: PgClient.PgClient) => Effect.Effect<unknown, SqlError>,
19
+ operation: (admin: PgClient.PgClient) => Effect.Effect<A, SqlError>,
19
20
  ) {
20
21
  return yield* Effect.scoped(Effect.flatMap(PgClient.makeClient({ url }), operation).pipe(Effect.mapError(connectionLimitHint))).pipe(
21
22
  Effect.provide(Reactivity.layer),
22
23
  );
23
24
  });
24
25
 
25
- export const database = Effect.fn("PostgresTest.database")(function* (url: Redacted.Redacted<string>, template = "template0") {
26
- const name = `effect_test_${crypto.randomUUID().replaceAll("-", "")}`;
26
+ export const database = Effect.fn("PostgresTest.database")(function* (
27
+ url: Redacted.Redacted<string>,
28
+ release: (name: string) => Effect.Effect<unknown>,
29
+ template = "template0",
30
+ ) {
31
+ const name = databaseName();
27
32
  const target = new URL(Redacted.value(url));
28
33
  target.pathname = `/${name}`;
29
34
  target.searchParams.delete("database");
30
35
  yield* Effect.acquireRelease(
31
36
  administer(url, (admin) => admin`create database ${admin(name)} template ${admin(template)}`),
32
- () => Effect.orDie(administer(url, (admin) => admin`drop database ${admin(name)} with (force)`)),
37
+ () => release(name),
33
38
  );
34
39
  return { name, url: Redacted.make(target.toString()) };
35
40
  });
@@ -1,12 +1,17 @@
1
- import { Context, Effect, Layer, type Redacted } from "effect";
1
+ import { Context, Effect, Layer, type Redacted, type Scope } from "effect";
2
2
  import * as Reactivity from "effect/reactivity/Reactivity";
3
3
  import { migratePostgres, type PostgresMigrationOptions } from "@shivaedev/effect-sql/migrations.ts";
4
4
  import { PostgresTestDatabase, type PostgresTestDatabaseService, type PostgresTestServices } from "#database.ts";
5
5
  import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
6
+ import { databaseCleanup } from "#internal/databaseCleanup.ts";
6
7
  import { connection, database } from "#internal/databases.ts";
7
8
 
9
+ export type PostgresTestLoader<E = never, R = never> = (
10
+ url: Redacted.Redacted<string>,
11
+ ) => Effect.Effect<unknown, E, R | PostgresTestServices | Scope.Scope>;
12
+
8
13
  export interface PostgresTestOptions<E = never, R = never, TFixtures = never> {
9
- readonly fixtures?: Effect.Effect<unknown, E, TFixtures | PostgresTestServices>;
14
+ readonly fixtures?: Effect.Effect<unknown, E, TFixtures | PostgresTestServices | Scope.Scope> | PostgresTestLoader<E, TFixtures>;
10
15
  readonly maxConnections?: number;
11
16
  readonly migrations: PostgresMigrationOptions<R>;
12
17
  readonly prepare?: boolean;
@@ -17,26 +22,31 @@ export function makePostgresTestLayer<E = never, R = never, TFixtures = never>(o
17
22
  return Layer.effectContext(
18
23
  Effect.gen(function* () {
19
24
  const maxConnections = options.maxConnections ?? 2;
20
- const template = yield* database(options.url);
25
+ const { awaitCleanup, release } = yield* databaseCleanup(options.url);
26
+ const empty = yield* database(options.url, release);
27
+ const template = options.prepare === false ? empty : yield* database(options.url, release, empty.name);
21
28
  if (options.prepare !== false) {
22
29
  yield* Effect.scoped(
23
30
  Effect.gen(function* () {
24
31
  const services = yield* Layer.build(connection(template.url, maxConnections));
25
32
  yield* migratePostgres(options.migrations).pipe(Effect.provideContext(services));
26
33
  if (options.fixtures !== undefined) {
27
- yield* options.fixtures.pipe(Effect.provideContext(services));
34
+ const fixtures = typeof options.fixtures === "function" ? options.fixtures(template.url) : options.fixtures;
35
+ yield* fixtures.pipe(Effect.provideContext(services));
28
36
  }
29
37
  }),
30
38
  );
31
39
  }
32
- const shared = yield* database(options.url, template.name);
40
+ const shared = yield* database(options.url, release, template.name);
33
41
  const services = yield* Layer.build(connection(shared.url, maxConnections));
34
- const isolated: PostgresTestDatabaseService["isolated"] = Effect.gen(function* () {
35
- const clone = yield* database(options.url, template.name);
36
- const context = yield* Layer.build(connection(clone.url, maxConnections));
37
- return Context.add(context, PostgresTestDatabase, { isolated, url: clone.url });
38
- });
39
- return Context.add(services, PostgresTestDatabase, { isolated, url: shared.url });
42
+ const clone: PostgresTestDatabaseService["clone"] = (source) =>
43
+ Effect.gen(function* () {
44
+ const target = yield* database(options.url, release, source === "empty" ? empty.name : template.name);
45
+ const context = yield* Layer.build(connection(target.url, maxConnections));
46
+ return Context.add(context, PostgresTestDatabase, { awaitCleanup, clone, isolated, url: target.url });
47
+ });
48
+ const isolated = clone("migrated");
49
+ return Context.add(services, PostgresTestDatabase, { awaitCleanup, clone, isolated, url: shared.url });
40
50
  }).pipe(Effect.mapError(connectionLimitHint)),
41
51
  ).pipe(Layer.provide(Reactivity.layer));
42
52
  }
@@ -1,12 +1,12 @@
1
1
  import { Effect } from "effect";
2
2
  import { connectionLimitHint } from "#internal/connectionLimitHint.ts";
3
- import { PostgresTestDatabase } from "./database.ts";
3
+ import { PostgresTestDatabase, type PostgresTestTemplate } from "./database.ts";
4
4
 
5
- export function withIsolatedDatabase<A, E, R>(effect: Effect.Effect<A, E, R>) {
5
+ export function withIsolatedDatabase<A, E, R>(effect: Effect.Effect<A, E, R>, options?: { readonly template?: PostgresTestTemplate }) {
6
6
  return Effect.scoped(
7
7
  Effect.gen(function* () {
8
8
  const database = yield* PostgresTestDatabase;
9
- const services = yield* database.isolated;
9
+ const services = yield* options?.template === undefined ? database.isolated : database.clone(options.template);
10
10
  return yield* Effect.scoped(effect.pipe(Effect.provideContext(services)));
11
11
  }),
12
12
  ).pipe(Effect.mapError(connectionLimitHint));
@@ -1,9 +0,0 @@
1
- # PostgreSQL tests with deliberate lifetimes
2
-
3
- Database tests repeat expensive setup and fragile cleanup when each application invents its own fixture lifecycle. The ideal is one migration and fixture preparation for a worker fixture, ordinary tests whose data disappears through rollback, and a deliberate escape for tests that need PostgreSQL to commit.
4
-
5
- Native Effect owns Context, Scope, failure, interruption and SQL transaction connections. `effect-test` owns the runner; `effect-sql` owns migrations and commit-bound transactions. This package joins those paths with PostgreSQL database resources. It does not implement another migration engine, pool driver or test runner.
6
-
7
- Prepare an immutable template, close its connections, and clone it for the shared database and isolated scenarios. Connection finalizers precede database drops. A rejected or interrupted test must not become a successful test during cleanup. A rolled-back nested transaction uses a savepoint, and a rolled-back test must not publish commit-bound invalidations.
8
-
9
- Isolation does not erase PostgreSQL's concurrency rules. Conflicting keyed writes share locks in the ordinary path. Separate databases permit independent data and real commits, while cluster-wide resources still need scenario-specific choices. Preserve these boundaries over a promise of universal concurrency.
package/docs/roadmap.md DELETED
@@ -1,20 +0,0 @@
1
- # Roadmap
2
-
3
- ## Built
4
-
5
- - Prepare migrations and optional fixtures once in a closed template database, supply native PostgreSQL services on a shared database, and roll back ordinary tests. [PostgreSQL lifecycle tests](../src/server/postgres.test.ts) and [transaction tests](../src/server/transaction.spec.ts) prove preparation and rollback.
6
- - Compose the wrapping transaction with `makeEffectIt`, harness scopes and nested `transact` savepoints. [Runner tests](../src/server/vitest.spec.ts) and [application-layer tests](../src/server/applicationLayers.spec.ts) prove the composition.
7
- - Clone isolated databases for real commits and drop them after success, typed failure, defects or interruption. [Isolation tests](../src/server/isolated.spec.ts) prove these lifetimes and worker cleanup; [cleanup failure tests](../src/server/cleanupFailure.spec.ts) prove a failed drop becomes a defect carrying the SQL failure.
8
- - Start with an empty schema using `prepare: false`, then use `migrationSteps` to stop at named migrations, seed old-schema data and finish the upgrade on the same database. [Migration-step tests](../src/server/migrationSteps.test.ts) prove committed backfills and rejection of unknown names and wrapping transactions; [lifecycle tests](../src/server/migrationStepsLifecycle.spec.ts) prove empty preparation, scoped loader resources, resume and rejection of backwards steps.
9
- - Use `migrationUpgrade` to commit an old migration prefix and apply the remaining migrations, then compare live PostgreSQL columns with an Effect SQL model using `assertModelColumns`. [Upgrade tests](../src/server/migrationUpgrade.test.ts) prove committed old data, exact column types, drift detection and invalid-state rejection; [model-column tests](../src/server/assertModelColumns.test.ts) prove model field and type comparisons.
10
- - Default to two pooled connections per database, allow `maxConnections` overrides and close administrative sessions between operations. [Connection-budget tests](../src/server/connectionBudget.spec.ts) measure twelve concurrent databases with default and explicit pool sizes; [idle-cleanup tests](../src/server/postgresIdleCleanup.spec.ts) prove idle workers release their connections.
11
- - Preserve SQLSTATE `53300` on connection exhaustion and add connection-limit and worker-count guidance. [Connection-limit tests](../src/server/connectionLimit.spec.ts) prove setup and wrapper diagnostics against PostgreSQL and preserve other SQL failures' identity.
12
- - Check public contracts with [compiler tests](../src/server/postgres.typecheck.test.ts), [migration-step compiler tests](../src/server/migrationSteps.typecheck.test.ts), [upgrade compiler tests](../src/server/migrationUpgrade.typecheck.test.ts) and [packed-consumer checks](../../../script/package-check/fixtures/effect-test-sql/types.ts.txt).
13
-
14
- ## Next
15
-
16
- No additional package work is selected. Let application migration and test-isolation needs identify the next change.
17
-
18
- ## Open questions
19
-
20
- No package scope questions are currently selected.