@shivaedev/effect-test-sql 0.5.5 → 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 (32) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +38 -7
  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 +4 -4
  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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
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
+
3
19
  ## 0.5.5 - 2026-10-18
4
20
 
5
21
  ### Changed
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
 
@@ -63,6 +63,22 @@ export const { effectApp } = makeEffectIt({
63
63
 
64
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
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
+ ```
81
+
66
82
  ### Budget PostgreSQL connections
67
83
 
68
84
  Each database pool opens connections on demand, with a default maximum of **two**: the shared database can use two, and each active per-test or per-story clone can use another two. Preparation uses the same limit and closes before tests start. An administrative connection exists only during a create or drop operation and closes as soon as that operation finishes; idle workers keep no administrative connection. Set `maxConnections` on `makePostgresTestLayer` to override the limit for the suite's preparation, shared and isolated pools:
@@ -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.
@@ -179,15 +210,15 @@ Import each name from the module that defines it. The package has no root entry.
179
210
 
180
211
  | Module | Exports |
181
212
  | --- | --- |
182
- | `@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` |
183
214
  | `@shivaedev/effect-test-sql/withTestTransaction.ts` | `withTestTransaction(effect)` |
184
- | `@shivaedev/effect-test-sql/withIsolatedDatabase.ts` | `withIsolatedDatabase(effect)` |
185
- | `@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` |
186
217
  | `@shivaedev/effect-test-sql/migrationSteps.ts` | `migrationSteps(appMigrations)` returning `{ migrateTo(name), migrateToLatest }` |
187
218
  | `@shivaedev/effect-test-sql/migrationUpgrade.ts` | `migrationUpgrade({ migrations, upTo, table })`, `MigrationUpgradeOptions`, `PostgresColumn` |
188
219
  | `@shivaedev/effect-test-sql/assertModelColumns.ts` | `assertModelColumns(columns, model, types)`, `ModelColumnMismatch` |
189
220
 
190
- `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.
191
222
 
192
223
  ### Install and setup
193
224
 
@@ -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.5",
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": {
@@ -41,14 +41,14 @@
41
41
  "README.md"
42
42
  ],
43
43
  "dependencies": {
44
- "@shivaedev/effect-sql": "0.5.2"
44
+ "@shivaedev/effect-sql": "0.6.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@effect/sql-pg": "4.0.4",
48
48
  "@effect/vitest": "4.0.4",
49
49
  "@shivaedev/effect-test": "0.6.0",
50
- "@shivaedev/quality": "0.26.0",
51
- "@shivaedev/test-story": "0.8.0",
50
+ "@shivaedev/quality": "0.27.1",
51
+ "@shivaedev/test-story": "0.10.0",
52
52
  "@types/node": "24.19.2",
53
53
  "effect": "4.0.4",
54
54
  "typescript": "7.0.2",
@@ -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));