From 9a1d7ba0a0a88fca225db0cb2ca15df1d4d7e298 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Tue, 21 Jul 2026 20:21:48 -0700 Subject: [PATCH 01/27] fix(database): run effectdb CLI on Node.js Use Effect Node services and Node-compatible filesystem operations so published commands no longer require Bun. Keep Bun limited to workspace tooling and remove it from consumer engine metadata. --- .beads/issues.jsonl | 1 + bun.lock | 32 +++++++++++++++-- packages/database/package.json | 7 ++-- packages/database/src/cli.ts | 8 ++--- .../database/src/internal/postgres-config.ts | 11 ++++-- .../src/internal/postgres-source-discovery.ts | 16 ++++++--- packages/database/src/postgres/migrate.ts | 35 +++++++++---------- packages/database/src/postgres/pull.ts | 13 ++++--- packages/querybuilder/package.json | 3 +- 9 files changed, 83 insertions(+), 43 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 7e05d5b9..b701b01e 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -26,6 +26,7 @@ {"id":"effect-qb-dn3.4","title":"P1 validate effect-db rows with SqlSchema","description":"Use v4 effect/unstable/sql/SqlSchema for high-risk effect-db result rows, starting with Postgres introspection and migration ledger reads.","acceptance_criteria":"Introspection and migration ledger row shapes are represented by internal schemas; selected unsafe typed row reads are replaced or wrapped with SqlSchema helpers; checksum/migration behavior is unchanged except for earlier shape failures; behavior/integration tests cover happy path and malformed-row failure where feasible.","notes":"Status: open. Validation plan: postgres-introspector behavior tests, postgres-migrate behavior tests, CLI integration tests if Postgres is available. Keep source-code discovery independent of SqlSchema.\nImplemented in second migration slice. Added v4 SqlSchema row decoders around Postgres catalog introspection reads and applied-migration ledger reads. Malformed catalog and ledger row tests now assert schema decode failure before metadata/migration state is built. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/postgres-introspector.behavior.test.ts test/internal/behavior/postgres-migrate.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:38Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:45:33Z","started_at":"2026-05-13T02:42:58Z","closed_at":"2026-05-13T02:45:33Z","close_reason":"effect-db catalog and migration ledger row reads now decode through v4 SqlSchema with malformed-row tests and repo gates passing.","labels":["effect-db","effect-v4","migration","sqlschema"],"dependencies":[{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:38Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3.3","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:13:13Z","started_at":"2026-07-22T03:13:13Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-1ib","title":"Merge effect v4 branch into main","description":"Bring refactor/effect-v4 up to date with current main changes, ensure published package dependency metadata treats effect as a peer dependency, run quality gates, then merge and push main.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:39:26Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T03:00:19Z","started_at":"2026-06-29T02:39:47Z","closed_at":"2026-06-29T03:00:19Z","close_reason":"Merged refactor/effect-v4 into main; local type, build, test, and pack gates pass. Docker integration was attempted but local Docker hung before tests could complete.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-9cp","title":"Document scoped executor transactions","description":"README transactions section should lead with the preferred Executor.withTransaction pipe-based scope and clarify that nested withTransaction calls use savepoints, while Query.transaction/savepoint remain low-level statement builders.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:32:14Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T02:33:38Z","started_at":"2026-06-29T02:32:39Z","closed_at":"2026-06-29T02:33:38Z","close_reason":"Added pipe-based Executor.withTransaction documentation and regenerated README type fixtures.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-px9","title":"Remove executor withSavepoint helper","description":"Public executor transaction composition should use withTransaction only. Nested withTransaction calls should represent savepoint scopes through the underlying SqlClient transaction implementation, so the explicit withSavepoint helper should be removed from exports, tests, and docs.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:00:45Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T02:02:41Z","started_at":"2026-06-29T02:00:53Z","closed_at":"2026-06-29T02:02:41Z","close_reason":"Removed the public withSavepoint helper and updated tests/types to use nested withTransaction for savepoint scopes.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/bun.lock b/bun.lock index c7b5723d..54a4c400 100644 --- a/bun.lock +++ b/bun.lock @@ -25,10 +25,10 @@ "effectdb": "./dist/cli.js", }, "dependencies": { - "@effect/platform-bun": "4.0.0-beta.98", - "@effect/platform-node-shared": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.98", "@effect/sql-pg": "4.0.0-beta.98", "effect-qb": "workspace:*", + "tinyglobby": "0.2.17", "typescript": "^6.0.2", }, "peerDependencies": { @@ -56,6 +56,8 @@ "packages": { "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-QorRKhvwHDPwvE8lH5lE0r0dI2z+zj12VUN4h7hfoIunKUiXSoSOMZqF3w13W30RpV6ivsvJywUycaa/18yMPA=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="], "@effect/sql-mysql2": ["@effect/sql-mysql2@4.0.0-beta.98", "", { "dependencies": { "mysql2": "^3.22.6" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-BIBkQNJTNPjv+80H0mpm5GMTHRukLuAXZ1UCYA2O8Cwl4gysua1li3BxAlK7BEHMr8byqoIyjBhPbHrWTWi09Q=="], @@ -64,6 +66,8 @@ "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.98", "", { "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-cc41uLhYBqexdbTNu4dlui+31E8hcVLEapLySa0C8d60FmBY8IEAV/RD3oF+6pqPslKEZ9p1+XVLdDm0iflw5Q=="], + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], @@ -104,8 +108,12 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -120,6 +128,8 @@ "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], @@ -128,6 +138,8 @@ "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], + "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], @@ -136,8 +148,12 @@ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], + "moo": ["moo@0.5.3", "", {}, "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="], "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], @@ -176,6 +192,8 @@ "pgsql-ast-parser": ["pgsql-ast-parser@12.0.2", "", { "dependencies": { "moo": "^0.5.1", "nearley": "^2.19.5" } }, "sha512-1WWa96Sw6h4uv9GLw98EzH/+xoBTC8j2TwV/AMW3E+Ir/fHOu/jLLbj6kPiz3y2bGISTKNYvKWwHoqvQ5FLuAw=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], "postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="], @@ -192,6 +210,10 @@ "randexp": ["randexp@0.4.6", "", { "dependencies": { "discontinuous-range": "1.0.0", "ret": "~0.1.10" } }, "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], @@ -200,10 +222,16 @@ "sql-escaper": ["sql-escaper@1.5.1", "", {}, "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "toml": ["toml@4.1.2", "", {}, "sha512-m0vXfHODcw3gk+KONAOlVQ5yNHc3yS3B1ybM3HS1vqDoS0RWTDDVBVVTYi8hH0k+2OM1vmo9fb1WX9EVqjqfHA=="], "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "undici": ["undici@8.8.0", "", {}, "sha512-ubshXMXwF3MQIMF1y/WxZdNBnjEKeSg2wF5mcGUtU55YTw34tnVVpKRlLf7ruDXZ5344KokPVX4RBx1wJm64Bw=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], diff --git a/packages/database/package.json b/packages/database/package.json index c9832eb4..c6995800 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -12,8 +12,7 @@ }, "homepage": "https://github.com/relsunkaev/effect-qb#readme", "engines": { - "node": ">=22", - "bun": ">=1.3.5" + "node": ">=22" }, "bin": { "effectdb": "./dist/cli.js" @@ -56,10 +55,10 @@ "prepack": "bun run build" }, "dependencies": { - "@effect/platform-bun": "4.0.0-beta.98", - "@effect/platform-node-shared": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.98", "@effect/sql-pg": "4.0.0-beta.98", "effect-qb": "workspace:*", + "tinyglobby": "0.2.17", "typescript": "^6.0.2" }, "peerDependencies": { diff --git a/packages/database/src/cli.ts b/packages/database/src/cli.ts index 8f369ee6..15b11814 100644 --- a/packages/database/src/cli.ts +++ b/packages/database/src/cli.ts @@ -1,5 +1,5 @@ -#!/usr/bin/env bun -import { BunRuntime, BunServices } from "@effect/platform-bun" +#!/usr/bin/env node +import { NodeRuntime, NodeServices } from "@effect/platform-node" import { Command, Flag } from "effect/unstable/cli" import * as Effect from "effect/Effect" import * as Option from "effect/Option" @@ -402,6 +402,6 @@ const cli = Command.run(root, { }) cli.pipe( - Effect.provide(BunServices.layer), - BunRuntime.runMain + Effect.provide(NodeServices.layer), + NodeRuntime.runMain ) diff --git a/packages/database/src/internal/postgres-config.ts b/packages/database/src/internal/postgres-config.ts index 4502a747..6fb7c07d 100644 --- a/packages/database/src/internal/postgres-config.ts +++ b/packages/database/src/internal/postgres-config.ts @@ -1,3 +1,4 @@ +import { access } from "node:fs/promises" import { dirname, resolve } from "node:path" import { pathToFileURL } from "node:url" @@ -244,8 +245,14 @@ const defaultConfig = (): EffectDbConfig => ({ } }) -const fileExists = async (path: string): Promise => - await Bun.file(path).exists() +const fileExists = async (path: string): Promise => { + try { + await access(path) + return true + } catch { + return false + } +} const loadModuleConfig = async (path: string): Promise => { const imported = await import(pathToFileURL(path).href) diff --git a/packages/database/src/internal/postgres-source-discovery.ts b/packages/database/src/internal/postgres-source-discovery.ts index 2dc8e516..7edee1a4 100644 --- a/packages/database/src/internal/postgres-source-discovery.ts +++ b/packages/database/src/internal/postgres-source-discovery.ts @@ -1,8 +1,9 @@ import * as Std from "effect-qb" import { randomUUID } from "node:crypto" -import { rm } from "node:fs/promises" +import { readFile, rm, writeFile } from "node:fs/promises" import { basename, dirname, extname, join, relative, resolve } from "node:path" import { pathToFileURL } from "node:url" +import { glob } from "tinyglobby" import ts from "typescript" import { @@ -548,8 +549,8 @@ const createTemporaryExportModule = async ( ): Promise => { const extension = extname(filePath) || ".ts" const tempPath = join(dirname(filePath), `.__effect_qb_discovery_${basename(filePath, extension)}_${randomUUID()}${extension}`) - const contents = await Bun.file(filePath).text() - await Bun.write( + const contents = await readFile(filePath, "utf8") + await writeFile( tempPath, `${contents}\nconst __effect_qb_discovery_exports = { ${names.join(", ")} }\nexport default __effect_qb_discovery_exports\n` ) @@ -586,7 +587,12 @@ const scanPattern = async ( pattern: string ): Promise> => { const matches: string[] = [] - for await (const match of new Bun.Glob(pattern).scan({ cwd, absolute: true, dot: true, followSymlinks: true })) { + for (const match of await glob(pattern, { + cwd, + absolute: true, + dot: true, + followSymbolicLinks: true + })) { if (DEFAULT_SOURCE_EXTENSIONS.has(extname(match))) { matches.push(resolve(match)) } @@ -615,7 +621,7 @@ export const discoverSourceSchema = async ( } const declarations: SourceDeclaration[] = [] for (const filePath of [...included].filter((file) => !excluded.has(file)).sort()) { - const contents = await Bun.file(filePath).text() + const contents = await readFile(filePath, "utf8") declarations.push(...discoverInFile(filePath, contents)) } const duplicateKeys = new Map() diff --git a/packages/database/src/postgres/migrate.ts b/packages/database/src/postgres/migrate.ts index 94581017..35fe5c3a 100644 --- a/packages/database/src/postgres/migrate.ts +++ b/packages/database/src/postgres/migrate.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto" -import { mkdir } from "node:fs/promises" -import { join, resolve } from "node:path" +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises" +import { basename, join, resolve } from "node:path" import * as SqlClient from "effect/unstable/sql/SqlClient" import * as SqlSchema from "effect/unstable/sql/SqlSchema" @@ -202,10 +202,7 @@ export const writeMigrationFile = async ( ): Promise => { const directory = resolve(migrationsDir) await mkdir(directory, { recursive: true }) - const files = await Array.fromAsync(new Bun.Glob("*.sql").scan({ - cwd: directory, - absolute: false - })) + const files = (await readdir(directory)).filter((file) => file.endsWith(".sql")) const nextNumber = files .map((file) => /^(\d+)_/.exec(file)?.[1]) .filter((value): value is string => value !== undefined) @@ -213,7 +210,7 @@ export const writeMigrationFile = async ( .reduce((max, current) => Math.max(max, current), 0) + 1 const fileName = `${String(nextNumber).padStart(4, "0")}_${sanitizeName(name)}.sql` const filePath = join(directory, fileName) - await Bun.write(filePath, `${renderMigrationFile(changes)}\n`) + await writeFile(filePath, `${renderMigrationFile(changes)}\n`) return filePath } @@ -239,17 +236,17 @@ export const readPendingMigrationFiles = async ( ): Promise> => { await ensureDirectory(migrationsDir) const directory = resolve(migrationsDir) - const files = (await Array.fromAsync(new Bun.Glob("*.sql").scan({ - cwd: directory, - absolute: true - }))).sort() + const files = (await readdir(directory)) + .filter((file) => file.endsWith(".sql")) + .map((file) => join(directory, file)) + .sort() const pending: MigrationFile[] = [] for (const path of files) { - const name = path.slice(path.lastIndexOf("/") + 1) + const name = basename(path) if (appliedNames.has(name)) { continue } - const contents = await Bun.file(path).text() + const contents = await readFile(path, "utf8") const parsed = parseMigrationSections(contents) pending.push({ name, @@ -267,14 +264,14 @@ export const readMigrationFiles = async ( ): Promise> => { await ensureDirectory(migrationsDir) const directory = resolve(migrationsDir) - const files = (await Array.fromAsync(new Bun.Glob("*.sql").scan({ - cwd: directory, - absolute: true - }))).sort() + const files = (await readdir(directory)) + .filter((file) => file.endsWith(".sql")) + .map((file) => join(directory, file)) + .sort() const parsed: MigrationFile[] = [] for (const path of files) { - const name = path.slice(path.lastIndexOf("/") + 1) - const contents = await Bun.file(path).text() + const name = basename(path) + const contents = await readFile(path, "utf8") const sections = parseMigrationSections(contents) parsed.push({ name, diff --git a/packages/database/src/postgres/pull.ts b/packages/database/src/postgres/pull.ts index 40879f82..d806165b 100644 --- a/packages/database/src/postgres/pull.ts +++ b/packages/database/src/postgres/pull.ts @@ -1,4 +1,4 @@ -import { mkdir } from "node:fs/promises" +import { mkdir, readFile, writeFile } from "node:fs/promises" import { dirname, extname, relative, resolve } from "node:path" import { Datatypes } from "effect-qb/postgres" @@ -2865,9 +2865,12 @@ export const planPostgresPull = async ( if (existing !== undefined) { return existing } - const original = await Bun.file(filePath).exists() - ? await Bun.file(filePath).text() - : "" + const original = await readFile(filePath, "utf8").catch((cause: unknown) => { + if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === "ENOENT") { + return "" + } + throw cause + }) const created = { original, replacements: [] as SourceBinding[], @@ -3208,7 +3211,7 @@ export const planPostgresPull = async ( export const applyPullPlan = async (plan: PullPlan): Promise => { for (const update of plan.updates) { await mkdir(dirname(update.filePath), { recursive: true }) - await Bun.write(update.filePath, update.after) + await writeFile(update.filePath, update.after) } } diff --git a/packages/querybuilder/package.json b/packages/querybuilder/package.json index ab4b28e0..3696e422 100644 --- a/packages/querybuilder/package.json +++ b/packages/querybuilder/package.json @@ -12,8 +12,7 @@ }, "homepage": "https://github.com/relsunkaev/effect-qb#readme", "engines": { - "node": ">=22", - "bun": ">=1.3.5" + "node": ">=22" }, "files": [ "dist", From cc721120044e9f884a88ad068ceed248bc154781 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Tue, 21 Jul 2026 20:22:02 -0700 Subject: [PATCH 02/27] test(pack): verify effect-db runs without Bun Exercise the packed config, migration, pull, and CLI paths under Node.js with Bun excluded from the CLI PATH. --- scripts/check-packed-querybuilder.ts | 63 +++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/scripts/check-packed-querybuilder.ts b/scripts/check-packed-querybuilder.ts index 68299b32..be5a1346 100644 --- a/scripts/check-packed-querybuilder.ts +++ b/scripts/check-packed-querybuilder.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from "node:fs/promises" +import { mkdtemp, rm, symlink } from "node:fs/promises" import { tmpdir } from "node:os" import { dirname, join, resolve } from "node:path" @@ -73,6 +73,7 @@ const main = async () => { const packedTarball = await querybuilderTarballPath() const packedDatabaseTarball = await databaseTarballPath() const consumerDir = await mkdtemp(join(tmpdir(), "effect-qb-pack-smoke-")) + const nodeOnlyBinDir = await mkdtemp(join(tmpdir(), "effect-db-node-bin-")) try { await Bun.write(join(consumerDir, "package.json"), `${JSON.stringify({ @@ -241,8 +242,68 @@ const main = async () => { await run(["bun", "install", "--no-save"], consumerDir) await run([join(cwd, "node_modules", ".bin", "tsgo"), "-p", "tsconfig.json"], consumerDir) + + const nodePath = Bun.which("node") + if (nodePath === null) { + throw new Error("Node.js is required for the packed effect-db smoke test") + } + await symlink(nodePath, join(nodeOnlyBinDir, "node")) + await Bun.write(join(consumerDir, "node-smoke.mjs"), ` +import { mkdir, readFile, writeFile } from "node:fs/promises" +import { join } from "node:path" +import { loadPostgresConfig } from "effect-db" +import { readMigrationFiles, writeMigrationFile } from "effect-db/postgres/migrate" +import { applyPullPlan } from "effect-db/postgres/pull" + +const workspace = join(process.cwd(), "node-runtime-workspace") +await mkdir(workspace) +await writeFile(join(workspace, "effectdb.config.ts"), \`import { defineConfig } from "effect-db" + +const config = { + dialect: "postgres", + db: { url: "postgres://localhost/effect_db" }, + source: { include: ["src/**/*.ts"] }, + migrations: { dir: "migrations", table: "effect_qb_migrations" }, + safety: { nonDestructiveDefault: true } +} satisfies Parameters[0] + +export default defineConfig(config)\n\`) +const loaded = await loadPostgresConfig(workspace) +if (loaded.config.dialect !== "postgres") throw new Error("failed to load config under Node.js") + +const migrationsDir = join(workspace, "migrations") +await writeMigrationFile(migrationsDir, "node runtime", []) +const migrations = await readMigrationFiles(migrationsDir) +if (migrations.length !== 1) throw new Error("failed to read migrations under Node.js") + +const pulledPath = join(workspace, "src", "schema.ts") +await applyPullPlan({ updates: [{ filePath: pulledPath, before: "", after: "export {}\\n" }] }) +if (await readFile(pulledPath, "utf8") !== "export {}\\n") throw new Error("failed to apply pull plan under Node.js") +`) + await run([nodePath, "node-smoke.mjs"], consumerDir) + const cli = Bun.spawn([ + join(consumerDir, "node_modules", ".bin", "effectdb"), + "--help" + ], { + cwd: consumerDir, + env: { + ...process.env, + PATH: nodeOnlyBinDir + }, + stdout: "pipe", + stderr: "pipe" + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(cli.stdout).text(), + new Response(cli.stderr).text(), + cli.exited + ]) + if (exitCode !== 0 || !stdout.includes("effectdb")) { + throw new Error(`Packed effect-db CLI failed under Node.js:\n${stdout}${stderr}`) + } } finally { await rm(consumerDir, { recursive: true, force: true }) + await rm(nodeOnlyBinDir, { recursive: true, force: true }) await rm(packedTarball, { force: true }) await rm(packedDatabaseTarball, { force: true }) await rm(dirname(packedDatabaseTarball), { recursive: true, force: true }) From 4ae4880e3d4d2c54ebd8d0254e403a850e14dc21 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Tue, 21 Jul 2026 20:22:47 -0700 Subject: [PATCH 03/27] chore(beads): close Node CLI runtime fix --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index b701b01e..34da57e5 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -26,7 +26,7 @@ {"id":"effect-qb-dn3.4","title":"P1 validate effect-db rows with SqlSchema","description":"Use v4 effect/unstable/sql/SqlSchema for high-risk effect-db result rows, starting with Postgres introspection and migration ledger reads.","acceptance_criteria":"Introspection and migration ledger row shapes are represented by internal schemas; selected unsafe typed row reads are replaced or wrapped with SqlSchema helpers; checksum/migration behavior is unchanged except for earlier shape failures; behavior/integration tests cover happy path and malformed-row failure where feasible.","notes":"Status: open. Validation plan: postgres-introspector behavior tests, postgres-migrate behavior tests, CLI integration tests if Postgres is available. Keep source-code discovery independent of SqlSchema.\nImplemented in second migration slice. Added v4 SqlSchema row decoders around Postgres catalog introspection reads and applied-migration ledger reads. Malformed catalog and ledger row tests now assert schema decode failure before metadata/migration state is built. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/postgres-introspector.behavior.test.ts test/internal/behavior/postgres-migrate.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:38Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:45:33Z","started_at":"2026-05-13T02:42:58Z","closed_at":"2026-05-13T02:45:33Z","close_reason":"effect-db catalog and migration ledger row reads now decode through v4 SqlSchema with malformed-row tests and repo gates passing.","labels":["effect-db","effect-v4","migration","sqlschema"],"dependencies":[{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:38Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3.3","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:13:13Z","started_at":"2026-07-22T03:13:13Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"closed","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:22:13Z","started_at":"2026-07-22T03:13:13Z","closed_at":"2026-07-22T03:22:13Z","close_reason":"effect-db now uses the Effect Node runtime and Node-compatible file APIs; packed Node 22 smoke and behavior/type tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-1ib","title":"Merge effect v4 branch into main","description":"Bring refactor/effect-v4 up to date with current main changes, ensure published package dependency metadata treats effect as a peer dependency, run quality gates, then merge and push main.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:39:26Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T03:00:19Z","started_at":"2026-06-29T02:39:47Z","closed_at":"2026-06-29T03:00:19Z","close_reason":"Merged refactor/effect-v4 into main; local type, build, test, and pack gates pass. Docker integration was attempted but local Docker hung before tests could complete.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-9cp","title":"Document scoped executor transactions","description":"README transactions section should lead with the preferred Executor.withTransaction pipe-based scope and clarify that nested withTransaction calls use savepoints, while Query.transaction/savepoint remain low-level statement builders.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:32:14Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T02:33:38Z","started_at":"2026-06-29T02:32:39Z","closed_at":"2026-06-29T02:33:38Z","close_reason":"Added pipe-based Executor.withTransaction documentation and regenerated README type fixtures.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-px9","title":"Remove executor withSavepoint helper","description":"Public executor transaction composition should use withTransaction only. Nested withTransaction calls should represent savepoint scopes through the underlying SqlClient transaction implementation, so the explicit withSavepoint helper should be removed from exports, tests, and docs.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:00:45Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T02:02:41Z","started_at":"2026-06-29T02:00:53Z","closed_at":"2026-06-29T02:02:41Z","close_reason":"Removed the public withSavepoint helper and updated tests/types to use nested withTransaction for savepoint scopes.","dependency_count":0,"dependent_count":0,"comment_count":0} From 2c5045df9ad311b431585fa18a9d9778cfdb71f7 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 12:08:27 -0700 Subject: [PATCH 04/27] chore(repo): document branch naming convention --- .beads/issues.jsonl | 1 + AGENTS.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 34da57e5..d7c0fbde 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -52,6 +52,7 @@ {"id":"effect-qb-dn3.7","title":"P2 harden JSON path schema inference for v4","description":"Revisit packages/querybuilder/src/internal/runtime/schema.ts manual Schema AST inspection under v4, adding focused coverage for JSON path narrowing before changing the inference logic.","acceptance_criteria":"Focused tests cover object, tuple, union, optional/missing, transformed/refined, wildcard/non-exact fallback, and JSON-compatible fallback cases; v4 AST assumptions are documented in code or bead notes; public helpers are preferred where available without weakening SQL semantics.","notes":"Implemented. Exact JSON path schema inference now walks the original v4 Schema AST rather than SchemaAST.toType so encoded-side field transformations such as NumberFromString are preserved for JSON subvalues. Added focused runtime-schema coverage for object paths, tuple indexes, unions, optional properties, missing-path fallback, wildcard fallback, refined leaves, transformations, and json/toJson compatible-vs-non-compatible fallback. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/runtime-schema.behavior.test.ts test/public/behavior/json.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:19:08Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:56:33Z","started_at":"2026-05-13T02:52:39Z","labels":["effect-v4","json","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.7","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:19:08Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.7","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.6","title":"P2 preserve v4 Schema issues in RowDecodeError","description":"Improve row decoding to use v4 Result/issue-shaped Schema APIs instead of exception-only decode flow, preserving structured SchemaIssue detail for users.","acceptance_criteria":"RowDecodeError exposes or formats structured schema issue detail; existing normalization/schema phase behavior remains compatible; JSON nested schema failures produce stable path/message output; executor behavior tests cover decode failure details.","notes":"Implemented. RowDecodeError now preserves v4 SchemaError.message and SchemaError.issue on schema decode failures while keeping the existing cause field. decodeProjectionValue uses Schema.decodeUnknownExit and extracts SchemaError from Exit instead of relying on decodeUnknownSync exceptions. Added nested JSON decode failure coverage asserting stable projection metadata plus schemaError detail. Checks passed: bun --config ./bunfig.test.toml test test/public/behavior/executor.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:59Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:52:24Z","started_at":"2026-05-13T02:49:58Z","closed_at":"2026-05-13T02:52:24Z","close_reason":"RowDecodeError now carries structured v4 Schema issue details and nested JSON decode failure coverage passes with repo gates.","labels":["effect-v4","errors","executor","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.6","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:59Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.6","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.5","title":"P2 add opt-in v4 Schema codecs","description":"Expose or document opt-in column/runtime helpers for v4 Schema codecs such as DateFromString, BigIntFromString, BigDecimalFromString, DurationFromString, and encoded string codecs without silently changing default column decoded types.","acceptance_criteria":"Default temporal/numeric/binary decoded types remain stable; opt-in recipes or helpers are available for supported codecs; invalid schema overrides still fail at type level; README/generated type snippets are updated and checked.","notes":"Status: open. Validation plan: column schema compatibility type tests, README generated type tests, runtime decode tests for at least one temporal/numeric codec.\nImplemented in third migration slice. Documented opt-in v4 codec pairings through C.schema for BigIntFromString, BigDecimalFromString, DurationFromString, and flipped Uint8ArrayFromBase64. Added type tests proving opt-in decoded types and default column types remain unchanged, plus executor behavior coverage for codec transforms after canonical normalization. Checks passed: bun --config ./bunfig.test.toml test test/public/behavior/executor.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:48:39Z","started_at":"2026-05-13T02:45:55Z","closed_at":"2026-05-13T02:48:39Z","close_reason":"v4 codec usage is documented and covered by type/runtime tests while default column decoded types remain unchanged.","labels":["codecs","effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.5","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:50Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.5","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-zk7","title":"Document branch naming convention","description":"Record the repository preference that automation branches must not use the codex/ prefix.","acceptance_criteria":"AGENTS.md explicitly says not to prefix branch names with codex/, and the active branch is renamed without that prefix.","status":"closed","priority":3,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:08:08Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:08:24Z","started_at":"2026-07-22T19:08:12Z","closed_at":"2026-07-22T19:08:24Z","close_reason":"Recorded the no codex/ branch-prefix convention and renamed the active branch.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-yll.5","title":"Phase 5 — Consolidate portable datatypes, expose ./standard in package.json, docs","description":"# Phase 5 — Tighten\n\nDepends on Phase 4 (`src/standard/` must exist).\n\nPolish phase: move portable datatypes into `standard/`, expose the public entry point in `package.json`, and add documentation.\n\n## Substeps\n\n1. **Datatypes audit.** Walk through `src/{mysql,postgres,sqlite}/datatypes/`. Move every type that's universal SQL into `src/standard/datatypes/`. Per-dialect folders should retain only true natives: postgres ranges/`citext`/`jsonb`/array types; mysql `set`/`enum`/`mediumtext`; sqlite-specific affinity quirks. Update per-dialect `datatypes/index.ts` to re-export from `standard/datatypes/` and add the extras.\n2. **`package.json` exports.** Add `\"./standard\"` to the `exports` map in `packages/querybuilder/package.json`, mirroring the existing `\"./mysql\"`, `\"./postgres\"`, `\"./sqlite\"` shape. Verify `dist/standard/` is produced by the build.\n3. **Documentation.**\n - Add a `docs/` page explaining the new `standard` namespace, the dialect tag rule (`\"standard\"` is portable; mixing concrete dialects is a compile error), and migration tips for users who want portable queries.\n - Update `README.md` with a short \"Writing portable queries\" section using the standard namespace.\n - Add an example file under `docs/examples/` demonstrating a portable query and a postgres-narrowed query.\n4. **Build + smoke test.** Run `bun run build` per package. Verify `dist/standard/` shape mirrors `dist/postgres/` for the equivalent files. Run the README examples unchanged.\n\n## Files touched\n\n**Modified:** `packages/querybuilder/package.json`; `src/{mysql,postgres,sqlite}/datatypes/index.ts`; `README.md`; `docs/*`.\n\n**Created:** docs page for standard namespace; example file(s).\n\n**Possibly moved:** specific datatype definitions from per-dialect to `src/standard/datatypes/`.\n\n## Acceptance\n\n- `package.json` exposes `./standard` as a public entry point; `dist/standard/` builds successfully.\n- Per-dialect `datatypes/` folders contain only genuinely dialect-specific types.\n- README and docs reflect the new namespace.\n- All tests pass.\n- `import * as Std from \"@effect-qb/querybuilder/standard\"` works in a fresh project.\n","acceptance_criteria":"package.json exposes ./standard; dist/standard/ builds; per-dialect datatypes/ folders contain only genuinely dialect-specific types; README and docs include a 'Writing portable queries' section using the standard namespace; an example file demonstrates a portable query and a postgres-narrowed query; all tests pass.","notes":"Partial docs/build pieces are present: package exports include ./standard, packages/querybuilder/scripts/build.ts emits standard.js, docs/standard-namespace.md and docs/examples/standard-query.ts were added, and README/package README mention portable queries. Not acceptance-complete: portable datatypes were not audited/moved out of per-dialect folders, and docs are minimal.\n2026-05-23 update: standard column coverage now includes varchar, char, bigint, real, and blob with runtime type coverage; package build and pack smoke pass for effect-qb/standard. Remaining phase-5 work is any deeper per-dialect datatype folder audit/documentation expansion.","status":"open","priority":3,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-19T20:53:23Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-24T01:48:19Z","labels":["architecture","dialect","docs","phase-5","querybuilder","refactor"],"dependencies":[{"issue_id":"effect-qb-yll.5","depends_on_id":"effect-qb-yll","type":"parent-child","created_at":"2026-05-19T13:53:22Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-yll.5","depends_on_id":"effect-qb-yll.4","type":"blocks","created_at":"2026-05-19T13:53:30Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.10","title":"P3 decide schema optics follow-up","description":"Track the schema optics/lenses follow-up from docs/effect-v4-schema-optics.md so related v4 work remains under the epic.","acceptance_criteria":"Decision remains linked to the epic; Effect Optic is documented only for decoded value helpers unless a separate JsonPath.focus implementation bead is opened; no mechanical migration work depends on optics.","notes":"Status: deferred by current evaluation. docs/effect-v4-schema-optics.md verdict: useful narrowly for decoded values and API inspiration, not a replacement for schema derivation/runtime inference/SQL JSON paths.","status":"closed","priority":3,"issue_type":"decision","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:19:44Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:42:11Z","closed_at":"2026-05-13T02:42:11Z","close_reason":"Optics follow-up decision recorded in docs/effect-v4-schema-optics.md; no mechanical v4 migration work depends on optics.","labels":["effect-v4","migration","optics","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.10","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:19:44Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.8","title":"P3 migrate effect-db CLI/platform imports","description":"Move effect-db CLI/platform usage to v4-aligned packages/imports and evaluate v4 CLI improvements for destructive confirmation flows without breaking non-interactive CI usage.","acceptance_criteria":"CLI imports are v4-aligned; existing commands still parse and run; boolean flag behavior is verified, especially destructive flags; any interactive confirmation is additive and bypassable for CI.","notes":"Status: open. Validation plan: postgres CLI behavior/integration tests and direct --help/parse smoke tests with Bun.\nImplemented in first migration slice. effect-db CLI now imports Command/Flag from effect/unstable/cli and BunRuntime/BunServices from @effect/platform-bun v4. Verified root and subcommand help paths: bun packages/database/src/cli.ts --help; bun packages/database/src/cli.ts push --help; bun packages/database/src/cli.ts migrate generate --help; bun packages/database/src/cli.ts migrate down --help. Boolean flags remain visible for non-interactive CI flows.","status":"closed","priority":3,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:19:21Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:42:05Z","closed_at":"2026-05-13T02:42:05Z","close_reason":"effect-db CLI/platform imports migrated to v4 and help/flag parsing smoke tests passed.","labels":["cli","effect-db","effect-v4","migration"],"dependencies":[{"issue_id":"effect-qb-dn3.8","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:19:20Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.8","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} diff --git a/AGENTS.md b/AGENTS.md index 96a37536..3b86e8cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,6 +119,10 @@ bun --hot ./index.ts For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. +## Git + +- Do not prefix branch names with `codex/`. + ## Beads Issue Tracker From 90b4d4f9a761fffef79d043a384c66fdd7f1f147 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 12:42:05 -0700 Subject: [PATCH 05/27] chore(repo): record esbuild runtime boundary --- .beads/issues.jsonl | 1 + AGENTS.md | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index d7c0fbde..665a4d4f 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -52,6 +52,7 @@ {"id":"effect-qb-dn3.7","title":"P2 harden JSON path schema inference for v4","description":"Revisit packages/querybuilder/src/internal/runtime/schema.ts manual Schema AST inspection under v4, adding focused coverage for JSON path narrowing before changing the inference logic.","acceptance_criteria":"Focused tests cover object, tuple, union, optional/missing, transformed/refined, wildcard/non-exact fallback, and JSON-compatible fallback cases; v4 AST assumptions are documented in code or bead notes; public helpers are preferred where available without weakening SQL semantics.","notes":"Implemented. Exact JSON path schema inference now walks the original v4 Schema AST rather than SchemaAST.toType so encoded-side field transformations such as NumberFromString are preserved for JSON subvalues. Added focused runtime-schema coverage for object paths, tuple indexes, unions, optional properties, missing-path fallback, wildcard fallback, refined leaves, transformations, and json/toJson compatible-vs-non-compatible fallback. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/runtime-schema.behavior.test.ts test/public/behavior/json.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:19:08Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:56:33Z","started_at":"2026-05-13T02:52:39Z","labels":["effect-v4","json","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.7","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:19:08Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.7","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.6","title":"P2 preserve v4 Schema issues in RowDecodeError","description":"Improve row decoding to use v4 Result/issue-shaped Schema APIs instead of exception-only decode flow, preserving structured SchemaIssue detail for users.","acceptance_criteria":"RowDecodeError exposes or formats structured schema issue detail; existing normalization/schema phase behavior remains compatible; JSON nested schema failures produce stable path/message output; executor behavior tests cover decode failure details.","notes":"Implemented. RowDecodeError now preserves v4 SchemaError.message and SchemaError.issue on schema decode failures while keeping the existing cause field. decodeProjectionValue uses Schema.decodeUnknownExit and extracts SchemaError from Exit instead of relying on decodeUnknownSync exceptions. Added nested JSON decode failure coverage asserting stable projection metadata plus schemaError detail. Checks passed: bun --config ./bunfig.test.toml test test/public/behavior/executor.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:59Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:52:24Z","started_at":"2026-05-13T02:49:58Z","closed_at":"2026-05-13T02:52:24Z","close_reason":"RowDecodeError now carries structured v4 Schema issue details and nested JSON decode failure coverage passes with repo gates.","labels":["effect-v4","errors","executor","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.6","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:59Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.6","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.5","title":"P2 add opt-in v4 Schema codecs","description":"Expose or document opt-in column/runtime helpers for v4 Schema codecs such as DateFromString, BigIntFromString, BigDecimalFromString, DurationFromString, and encoded string codecs without silently changing default column decoded types.","acceptance_criteria":"Default temporal/numeric/binary decoded types remain stable; opt-in recipes or helpers are available for supported codecs; invalid schema overrides still fail at type level; README/generated type snippets are updated and checked.","notes":"Status: open. Validation plan: column schema compatibility type tests, README generated type tests, runtime decode tests for at least one temporal/numeric codec.\nImplemented in third migration slice. Documented opt-in v4 codec pairings through C.schema for BigIntFromString, BigDecimalFromString, DurationFromString, and flipped Uint8ArrayFromBase64. Added type tests proving opt-in decoded types and default column types remain unchanged, plus executor behavior coverage for codec transforms after canonical normalization. Checks passed: bun --config ./bunfig.test.toml test test/public/behavior/executor.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:48:39Z","started_at":"2026-05-13T02:45:55Z","closed_at":"2026-05-13T02:48:39Z","close_reason":"v4 codec usage is documented and covered by type/runtime tests while default column decoded types remain unchanged.","labels":["codecs","effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.5","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:50Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.5","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-7h9","title":"Record esbuild runtime assumption","description":"Document that effect-qb and effect-db consumer code is expected to be bundled or built with esbuild, so runtime TypeScript loader compatibility is not a repository responsibility.","acceptance_criteria":"AGENTS.md records esbuild as the owner of TypeScript and module transformation while Bun remains the workspace package manager.","status":"closed","priority":3,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:40:55Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:41:42Z","started_at":"2026-07-22T19:41:00Z","closed_at":"2026-07-22T19:41:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-zk7","title":"Document branch naming convention","description":"Record the repository preference that automation branches must not use the codex/ prefix.","acceptance_criteria":"AGENTS.md explicitly says not to prefix branch names with codex/, and the active branch is renamed without that prefix.","status":"closed","priority":3,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:08:08Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:08:24Z","started_at":"2026-07-22T19:08:12Z","closed_at":"2026-07-22T19:08:24Z","close_reason":"Recorded the no codex/ branch-prefix convention and renamed the active branch.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-yll.5","title":"Phase 5 — Consolidate portable datatypes, expose ./standard in package.json, docs","description":"# Phase 5 — Tighten\n\nDepends on Phase 4 (`src/standard/` must exist).\n\nPolish phase: move portable datatypes into `standard/`, expose the public entry point in `package.json`, and add documentation.\n\n## Substeps\n\n1. **Datatypes audit.** Walk through `src/{mysql,postgres,sqlite}/datatypes/`. Move every type that's universal SQL into `src/standard/datatypes/`. Per-dialect folders should retain only true natives: postgres ranges/`citext`/`jsonb`/array types; mysql `set`/`enum`/`mediumtext`; sqlite-specific affinity quirks. Update per-dialect `datatypes/index.ts` to re-export from `standard/datatypes/` and add the extras.\n2. **`package.json` exports.** Add `\"./standard\"` to the `exports` map in `packages/querybuilder/package.json`, mirroring the existing `\"./mysql\"`, `\"./postgres\"`, `\"./sqlite\"` shape. Verify `dist/standard/` is produced by the build.\n3. **Documentation.**\n - Add a `docs/` page explaining the new `standard` namespace, the dialect tag rule (`\"standard\"` is portable; mixing concrete dialects is a compile error), and migration tips for users who want portable queries.\n - Update `README.md` with a short \"Writing portable queries\" section using the standard namespace.\n - Add an example file under `docs/examples/` demonstrating a portable query and a postgres-narrowed query.\n4. **Build + smoke test.** Run `bun run build` per package. Verify `dist/standard/` shape mirrors `dist/postgres/` for the equivalent files. Run the README examples unchanged.\n\n## Files touched\n\n**Modified:** `packages/querybuilder/package.json`; `src/{mysql,postgres,sqlite}/datatypes/index.ts`; `README.md`; `docs/*`.\n\n**Created:** docs page for standard namespace; example file(s).\n\n**Possibly moved:** specific datatype definitions from per-dialect to `src/standard/datatypes/`.\n\n## Acceptance\n\n- `package.json` exposes `./standard` as a public entry point; `dist/standard/` builds successfully.\n- Per-dialect `datatypes/` folders contain only genuinely dialect-specific types.\n- README and docs reflect the new namespace.\n- All tests pass.\n- `import * as Std from \"@effect-qb/querybuilder/standard\"` works in a fresh project.\n","acceptance_criteria":"package.json exposes ./standard; dist/standard/ builds; per-dialect datatypes/ folders contain only genuinely dialect-specific types; README and docs include a 'Writing portable queries' section using the standard namespace; an example file demonstrates a portable query and a postgres-narrowed query; all tests pass.","notes":"Partial docs/build pieces are present: package exports include ./standard, packages/querybuilder/scripts/build.ts emits standard.js, docs/standard-namespace.md and docs/examples/standard-query.ts were added, and README/package README mention portable queries. Not acceptance-complete: portable datatypes were not audited/moved out of per-dialect folders, and docs are minimal.\n2026-05-23 update: standard column coverage now includes varchar, char, bigint, real, and blob with runtime type coverage; package build and pack smoke pass for effect-qb/standard. Remaining phase-5 work is any deeper per-dialect datatype folder audit/documentation expansion.","status":"open","priority":3,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-19T20:53:23Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-24T01:48:19Z","labels":["architecture","dialect","docs","phase-5","querybuilder","refactor"],"dependencies":[{"issue_id":"effect-qb-yll.5","depends_on_id":"effect-qb-yll","type":"parent-child","created_at":"2026-05-19T13:53:22Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-yll.5","depends_on_id":"effect-qb-yll.4","type":"blocks","created_at":"2026-05-19T13:53:30Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.10","title":"P3 decide schema optics follow-up","description":"Track the schema optics/lenses follow-up from docs/effect-v4-schema-optics.md so related v4 work remains under the epic.","acceptance_criteria":"Decision remains linked to the epic; Effect Optic is documented only for decoded value helpers unless a separate JsonPath.focus implementation bead is opened; no mechanical migration work depends on optics.","notes":"Status: deferred by current evaluation. docs/effect-v4-schema-optics.md verdict: useful narrowly for decoded values and API inspiration, not a replacement for schema derivation/runtime inference/SQL JSON paths.","status":"closed","priority":3,"issue_type":"decision","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:19:44Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:42:11Z","closed_at":"2026-05-13T02:42:11Z","close_reason":"Optics follow-up decision recorded in docs/effect-v4-schema-optics.md; no mechanical v4 migration work depends on optics.","labels":["effect-v4","migration","optics","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.10","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:19:44Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/AGENTS.md b/AGENTS.md index 3b86e8cd..f2896c14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,6 +123,13 @@ For more information, read the Bun API docs in `node_modules/bun-types/docs/**.m - Do not prefix branch names with `codex/`. +## Runtime and build boundaries + +- Keep Bun as the workspace package manager and tooling runtime. +- Keep published `effect-qb` and `effect-db` runtime behavior Node.js-native. +- Assume consumer code is bundled or built with esbuild. Do not add a runtime + TypeScript loader solely to support unbuilt TypeScript syntax. + ## Beads Issue Tracker From e3b9f8db1aff7f2b845bcd46f686e8f5386494ea Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:06:45 -0700 Subject: [PATCH 06/27] fix(database): generate valid extension-only pull sources --- .beads/issues.jsonl | 2 + packages/database/src/postgres/pull.ts | 120 +++++++++--------- .../integration/postgres-cli.integration.ts | 74 ++++++----- 3 files changed, 98 insertions(+), 98 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 665a4d4f..d98e4cc1 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -26,6 +26,8 @@ {"id":"effect-qb-dn3.4","title":"P1 validate effect-db rows with SqlSchema","description":"Use v4 effect/unstable/sql/SqlSchema for high-risk effect-db result rows, starting with Postgres introspection and migration ledger reads.","acceptance_criteria":"Introspection and migration ledger row shapes are represented by internal schemas; selected unsafe typed row reads are replaced or wrapped with SqlSchema helpers; checksum/migration behavior is unchanged except for earlier shape failures; behavior/integration tests cover happy path and malformed-row failure where feasible.","notes":"Status: open. Validation plan: postgres-introspector behavior tests, postgres-migrate behavior tests, CLI integration tests if Postgres is available. Keep source-code discovery independent of SqlSchema.\nImplemented in second migration slice. Added v4 SqlSchema row decoders around Postgres catalog introspection reads and applied-migration ledger reads. Malformed catalog and ledger row tests now assert schema decode failure before metadata/migration state is built. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/postgres-introspector.behavior.test.ts test/internal/behavior/postgres-migrate.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:38Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:45:33Z","started_at":"2026-05-13T02:42:58Z","closed_at":"2026-05-13T02:45:33Z","close_reason":"effect-db catalog and migration ledger row reads now decode through v4 SqlSchema with malformed-row tests and repo gates passing.","labels":["effect-db","effect-v4","migration","sqlschema"],"dependencies":[{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:38Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3.3","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","status":"open","priority":2,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:50:07Z","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-02d","title":"Complete Node-native package verification","description":"Finish the Node-native boundaries for effect-db and effect-qb while retaining Bun for workspace package management and tooling.","acceptance_criteria":"Postgres CLI integration executes packages/database/dist/cli.js with Node; CLI host output uses Effect Platform services with Node implementations confined to runtime assembly; production package source typechecks with explicit Node types and without Bun ambient types; packed artifacts are smoked on Node 22 and Node 24 in CI; the root workspace is private and declares its Bun package manager/engine; focused and existing quality gates pass.","notes":"User clarified that portable host capabilities should use Effect Platform wherever practical; Node remains the executable adapter through NodeServices/NodeRuntime.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:43:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:45:50Z","started_at":"2026-07-22T19:43:55Z","labels":["ci","node","packaging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"closed","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:22:13Z","started_at":"2026-07-22T03:13:13Z","closed_at":"2026-07-22T03:22:13Z","close_reason":"effect-db now uses the Effect Node runtime and Node-compatible file APIs; packed Node 22 smoke and behavior/type tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-1ib","title":"Merge effect v4 branch into main","description":"Bring refactor/effect-v4 up to date with current main changes, ensure published package dependency metadata treats effect as a peer dependency, run quality gates, then merge and push main.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:39:26Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T03:00:19Z","started_at":"2026-06-29T02:39:47Z","closed_at":"2026-06-29T03:00:19Z","close_reason":"Merged refactor/effect-v4 into main; local type, build, test, and pack gates pass. Docker integration was attempted but local Docker hung before tests could complete.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-9cp","title":"Document scoped executor transactions","description":"README transactions section should lead with the preferred Executor.withTransaction pipe-based scope and clarify that nested withTransaction calls use savepoints, while Query.transaction/savepoint remain low-level statement builders.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:32:14Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T02:33:38Z","started_at":"2026-06-29T02:32:39Z","closed_at":"2026-06-29T02:33:38Z","close_reason":"Added pipe-based Executor.withTransaction documentation and regenerated README type fixtures.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/packages/database/src/postgres/pull.ts b/packages/database/src/postgres/pull.ts index d806165b..71f5b1da 100644 --- a/packages/database/src/postgres/pull.ts +++ b/packages/database/src/postgres/pull.ts @@ -471,7 +471,7 @@ const renderQueryColumnReference = ( if (context.tableColumnsAlias !== undefined) { return renderColumnAccess(context.tableColumnsAlias, name) } - return `${PG_ALIAS}.Query.column(${renderStringLiteral(name)}, ${renderQueryTypeExpression(column, context)}${column.nullable ? ", true" : ""})` + return `${STD_ROOT_ALIAS}.Query.column(${renderStringLiteral(name)}, ${renderQueryTypeExpression(column, context)}${column.nullable ? ", true" : ""})` } type PipeRender = { @@ -691,7 +691,7 @@ const renderSqlExpressionCode = ( ): string => { const operands = collectBooleanOperands(value, operator.toUpperCase() as "AND" | "OR") .map((item) => renderSqlExpressionCode(item, context)) - const base = `${PG_ALIAS}.Query.${operator}(${operands.slice(0, 2).join(", ")})` + const base = `${STD_ROOT_ALIAS}.Query.${operator}(${operands.slice(0, 2).join(", ")})` return operands.length > 2 ? `${base}.pipe(${operands.slice(2).join(", ")})` : base @@ -746,39 +746,39 @@ const renderSqlExpressionCode = ( case "ref": return renderQueryColumnReference(expression.name, context) case "string": - return `${PG_ALIAS}.Query.literal(${renderStringLiteral(expression.value)})` + return `${STD_ROOT_ALIAS}.Query.literal(${renderStringLiteral(expression.value)})` case "integer": - return `${PG_ALIAS}.Query.literal(${String(expression.value)})` + return `${STD_ROOT_ALIAS}.Query.literal(${String(expression.value)})` case "numeric": - return `${PG_ALIAS}.Query.literal(${String(expression.value)})` + return `${STD_ROOT_ALIAS}.Query.literal(${String(expression.value)})` case "boolean": - return `${PG_ALIAS}.Query.literal(${String(expression.value)})` + return `${STD_ROOT_ALIAS}.Query.literal(${String(expression.value)})` case "null": - return `${PG_ALIAS}.Query.literal(null)` + return `${STD_ROOT_ALIAS}.Query.literal(null)` case "keyword": { const keyword = (expression.keyword as string).toLowerCase() switch (keyword) { case "current_date": - return `${PG_ALIAS}.Function.currentDate()` + return `${STD_ROOT_ALIAS}.Function.currentDate()` case "current_time": - return `${PG_ALIAS}.Function.currentTime()` + return `${STD_ROOT_ALIAS}.Function.currentTime()` case "current_timestamp": - return `${PG_ALIAS}.Function.currentTimestamp()` + return `${STD_ROOT_ALIAS}.Function.currentTimestamp()` case "localtime": - return `${PG_ALIAS}.Function.localTime()` + return `${STD_ROOT_ALIAS}.Function.localTime()` case "localtimestamp": - return `${PG_ALIAS}.Function.localTimestamp()` + return `${STD_ROOT_ALIAS}.Function.localTimestamp()` case "current_schema": case "current_catalog": case "current_role": case "current_user": case "session_user": case "user": - return `${PG_ALIAS}.Function.call(${renderStringLiteral(keyword)})` + return `${STD_ROOT_ALIAS}.Function.call(${renderStringLiteral(keyword)})` case "distinct": throw new Error("Unsupported PostgreSQL keyword in pulled schema: distinct") } - return `${PG_ALIAS}.Function.call(${renderStringLiteral(keyword)})` + return `${STD_ROOT_ALIAS}.Function.call(${renderStringLiteral(keyword)})` } case "cast": return `${CAST_ALIAS}.to(${renderSqlExpressionCode(expression.operand, context, false)}, ${renderCastTarget(expression.to, context)})` @@ -799,23 +799,23 @@ const renderSqlExpressionCode = ( const args = Array.isArray(expression.args) ? expression.args : [] switch (name) { case "lower": - return `${PG_ALIAS}.Function.lower(${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")})` + return `${STD_ROOT_ALIAS}.Function.lower(${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")})` case "upper": - return `${PG_ALIAS}.Function.upper(${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")})` + return `${STD_ROOT_ALIAS}.Function.upper(${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")})` case "coalesce": - return `${PG_ALIAS}.Function.coalesce(${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")})` + return `${STD_ROOT_ALIAS}.Function.coalesce(${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")})` case "now": - return `${PG_ALIAS}.Function.now()` + return `${STD_ROOT_ALIAS}.Function.now()` case "current_timestamp": - return `${PG_ALIAS}.Function.currentTimestamp()` + return `${STD_ROOT_ALIAS}.Function.currentTimestamp()` case "current_date": - return `${PG_ALIAS}.Function.currentDate()` + return `${STD_ROOT_ALIAS}.Function.currentDate()` case "current_time": - return `${PG_ALIAS}.Function.currentTime()` + return `${STD_ROOT_ALIAS}.Function.currentTime()` case "localtime": - return `${PG_ALIAS}.Function.localTime()` + return `${STD_ROOT_ALIAS}.Function.localTime()` case "localtimestamp": - return `${PG_ALIAS}.Function.localTimestamp()` + return `${STD_ROOT_ALIAS}.Function.localTimestamp()` case "uuid_generate_v4": case "gen_random_uuid": return `${PG_ALIAS}.Function.uuidGenerateV4()` @@ -857,7 +857,7 @@ const renderSqlExpressionCode = ( case "jsonb_typeof": return `${PG_ALIAS}.Jsonb.typeOf(${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")})` } - return `${PG_ALIAS}.Function.call(${renderStringLiteral(name)}${args.length === 0 ? "" : `, ${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")}`})` + return `${STD_ROOT_ALIAS}.Function.call(${renderStringLiteral(name)}${args.length === 0 ? "" : `, ${args.map((arg) => renderSqlExpressionCode(arg, context)).join(", ")}`})` } case "binary": { const op = expression.op as string @@ -865,7 +865,7 @@ const renderSqlExpressionCode = ( const anyArgs = Array.isArray(expression.right.args) ? expression.right.args : [] if (anyArgs.length === 1 && anyArgs[0]?.type === "array") { const arrayValues = anyArgs[0].expressions.map((item: PgSqlExpr) => renderSqlExpressionCode(item, context)) - return `${PG_ALIAS}.Query.in(${renderSqlExpressionCode(expression.left, context)}, ${arrayValues.join(", ")})` + return `${STD_ROOT_ALIAS}.Query.in(${renderSqlExpressionCode(expression.left, context)}, ${arrayValues.join(", ")})` } } if (op === "#>" || op === "#>>") { @@ -884,18 +884,18 @@ const renderSqlExpressionCode = ( const right = renderSqlExpressionCode(expression.right, context) switch (op) { case "=": - return `${PG_ALIAS}.Query.eq(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.eq(${left}, ${right})` case "!=": case "<>": - return `${PG_ALIAS}.Query.neq(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.neq(${left}, ${right})` case "<": - return `${PG_ALIAS}.Query.lt(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.lt(${left}, ${right})` case "<=": - return `${PG_ALIAS}.Query.lte(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.lte(${left}, ${right})` case ">": - return `${PG_ALIAS}.Query.gt(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.gt(${left}, ${right})` case ">=": - return `${PG_ALIAS}.Query.gte(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.gte(${left}, ${right})` case "AND": case "and": return renderBooleanChain("and", expression) @@ -904,18 +904,18 @@ const renderSqlExpressionCode = ( return renderBooleanChain("or", expression) case "LIKE": case "like": - return `${PG_ALIAS}.Query.like(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.like(${left}, ${right})` case "ILIKE": case "ilike": - return `${PG_ALIAS}.Query.ilike(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.ilike(${left}, ${right})` case "~": - return `${PG_ALIAS}.Query.regexMatch(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.regexMatch(${left}, ${right})` case "~*": - return `${PG_ALIAS}.Query.regexIMatch(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.regexIMatch(${left}, ${right})` case "!~": - return `${PG_ALIAS}.Query.regexNotMatch(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.regexNotMatch(${left}, ${right})` case "!~*": - return `${PG_ALIAS}.Query.regexNotIMatch(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.regexNotIMatch(${left}, ${right})` case "?": { const key = extractStringLiteral(expression.right) if (key === undefined) { @@ -942,15 +942,15 @@ const renderSqlExpressionCode = ( case "@@": return `${PG_ALIAS}.Jsonb.pathMatch(${left}, ${right})` case "IS DISTINCT FROM": - return `${PG_ALIAS}.Query.isDistinctFrom(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.isDistinctFrom(${left}, ${right})` case "IS NOT DISTINCT FROM": - return `${PG_ALIAS}.Query.isNotDistinctFrom(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.isNotDistinctFrom(${left}, ${right})` case "@>": - return `${PG_ALIAS}.Query.contains(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.contains(${left}, ${right})` case "<@": - return `${PG_ALIAS}.Query.containedBy(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.containedBy(${left}, ${right})` case "&&": - return `${PG_ALIAS}.Query.overlaps(${left}, ${right})` + return `${STD_ROOT_ALIAS}.Query.overlaps(${left}, ${right})` } throw new Error(`Unsupported PostgreSQL binary operator in pulled schema: ${expression.op}`) } @@ -958,23 +958,23 @@ const renderSqlExpressionCode = ( const operand = renderSqlExpressionCode(expression.operand, context) switch (expression.op.toUpperCase()) { case "IS NULL": - return `${PG_ALIAS}.Query.isNull(${operand})` + return `${STD_ROOT_ALIAS}.Query.isNull(${operand})` case "IS NOT NULL": - return `${PG_ALIAS}.Query.isNotNull(${operand})` + return `${STD_ROOT_ALIAS}.Query.isNotNull(${operand})` case "IS TRUE": - return `${PG_ALIAS}.Query.and(${PG_ALIAS}.Query.isNotNull(${operand}), ${PG_ALIAS}.Query.eq(${operand}, ${PG_ALIAS}.Query.literal(true)))` + return `${STD_ROOT_ALIAS}.Query.and(${STD_ROOT_ALIAS}.Query.isNotNull(${operand}), ${STD_ROOT_ALIAS}.Query.eq(${operand}, ${STD_ROOT_ALIAS}.Query.literal(true)))` case "IS FALSE": - return `${PG_ALIAS}.Query.and(${PG_ALIAS}.Query.isNotNull(${operand}), ${PG_ALIAS}.Query.eq(${operand}, ${PG_ALIAS}.Query.literal(false)))` + return `${STD_ROOT_ALIAS}.Query.and(${STD_ROOT_ALIAS}.Query.isNotNull(${operand}), ${STD_ROOT_ALIAS}.Query.eq(${operand}, ${STD_ROOT_ALIAS}.Query.literal(false)))` case "IS NOT TRUE": - return `${PG_ALIAS}.Query.or(${PG_ALIAS}.Query.isNull(${operand}), ${PG_ALIAS}.Query.eq(${operand}, ${PG_ALIAS}.Query.literal(false)))` + return `${STD_ROOT_ALIAS}.Query.or(${STD_ROOT_ALIAS}.Query.isNull(${operand}), ${STD_ROOT_ALIAS}.Query.eq(${operand}, ${STD_ROOT_ALIAS}.Query.literal(false)))` case "IS NOT FALSE": - return `${PG_ALIAS}.Query.or(${PG_ALIAS}.Query.isNull(${operand}), ${PG_ALIAS}.Query.eq(${operand}, ${PG_ALIAS}.Query.literal(true)))` + return `${STD_ROOT_ALIAS}.Query.or(${STD_ROOT_ALIAS}.Query.isNull(${operand}), ${STD_ROOT_ALIAS}.Query.eq(${operand}, ${STD_ROOT_ALIAS}.Query.literal(true)))` case "IS UNKNOWN": - return `${PG_ALIAS}.Query.isNull(${operand})` + return `${STD_ROOT_ALIAS}.Query.isNull(${operand})` case "IS NOT UNKNOWN": - return `${PG_ALIAS}.Query.isNotNull(${operand})` + return `${STD_ROOT_ALIAS}.Query.isNotNull(${operand})` case "NOT": - return `${PG_ALIAS}.Query.not(${operand})` + return `${STD_ROOT_ALIAS}.Query.not(${operand})` } throw new Error(`Unsupported PostgreSQL unary operator in pulled schema: ${expression.op}`) } @@ -982,8 +982,8 @@ const renderSqlExpressionCode = ( { const values = Array.isArray(expression.expressions) ? expression.expressions : [] return values.length === 0 - ? `${PG_ALIAS}.Function.call("array")` - : `${PG_ALIAS}.Function.call("array", ${values.map((item: PgSqlExpr) => renderSqlExpressionCode(item, context)).join(", ")})` + ? `${STD_ROOT_ALIAS}.Function.call("array")` + : `${STD_ROOT_ALIAS}.Function.call("array", ${values.map((item: PgSqlExpr) => renderSqlExpressionCode(item, context)).join(", ")})` } case "case": { const whens = Array.isArray(expression.whens) ? expression.whens : [] @@ -991,20 +991,20 @@ const renderSqlExpressionCode = ( throw new Error("Unsupported PostgreSQL case expression in pulled schema") } const base = expression.value === null - ? `${PG_ALIAS}.Query.case()` + ? `${STD_ROOT_ALIAS}.Query.case()` : expression.value === undefined - ? `${PG_ALIAS}.Query.case()` - : `${PG_ALIAS}.Query.match(${renderSqlExpressionCode(expression.value, context)})` + ? `${STD_ROOT_ALIAS}.Query.case()` + : `${STD_ROOT_ALIAS}.Query.match(${renderSqlExpressionCode(expression.value, context)})` const chained = whens.reduce( (acc, branch) => `${acc}.when(${renderSqlExpressionCode(branch.when, context)}, ${renderSqlExpressionCode(branch.value, context)})`, base ) return expression.else == null - ? `${chained}.else(${PG_ALIAS}.Query.literal(null))` + ? `${chained}.else(${STD_ROOT_ALIAS}.Query.literal(null))` : `${chained}.else(${renderSqlExpressionCode(expression.else, context)})` } case "extract": - return `${PG_ALIAS}.Function.call("extract", ${renderStringLiteral((expression.field as { readonly name: string }).name)}, ${renderSqlExpressionCode(expression.from, context)})` + return `${STD_ROOT_ALIAS}.Function.call("extract", ${renderStringLiteral((expression.field as { readonly name: string }).name)}, ${renderSqlExpressionCode(expression.from, context)})` default: throw new Error(`Unsupported PostgreSQL expression in pulled schema: ${expression.type}`) } @@ -1202,7 +1202,7 @@ const tryRenderCollateExpressionCode = ( : operator === ">" ? "gt" : "gte" - return `${PG_ALIAS}.Query.${method}(${renderDdlExpressionCode(left, context)}, ${renderDdlExpressionCode(right, context)})` + return `${STD_ROOT_ALIAS}.Query.${method}(${renderDdlExpressionCode(left, context)}, ${renderDdlExpressionCode(right, context)})` }) if (binary !== undefined) { return binary @@ -1225,7 +1225,7 @@ const tryRenderCollateExpressionCode = ( const renderedCollation = collationParts.length === 1 ? renderStringLiteral(collationParts[0]!) : `[${collationParts.map((part) => renderStringLiteral(part)).join(", ")}]` - return `${PG_ALIAS}.Query.collate(${renderDdlExpressionCode(expressionSql, context)}, ${renderedCollation})` + return `${STD_ROOT_ALIAS}.Query.collate(${renderDdlExpressionCode(expressionSql, context)}, ${renderedCollation})` }) if (collate !== undefined) { return collate diff --git a/test/public/integration/postgres-cli.integration.ts b/test/public/integration/postgres-cli.integration.ts index 8c6ab9d2..ba4c21d0 100644 --- a/test/public/integration/postgres-cli.integration.ts +++ b/test/public/integration/postgres-cli.integration.ts @@ -59,7 +59,7 @@ const renderTableSource = ( ) => ` import * as Pg from "effect-qb/postgres" import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make(${JSON.stringify(schemaName)}) @@ -384,12 +384,12 @@ test("postgres cli safe mode applies additive changes and skips destructive drif const { workspace, schemaName } = await makeSourceWorkspace(` import * as Pg from "effect-qb/postgres" import { Cast, Check, Function as F, Index, PrimaryKey, Query as Q, Table, Unique } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") export const users = db.table("users", { - id: C.int().pipe(C.identityByDefault), + id: C.int().pipe(Pg.Column.identityByDefault), email: C.text(), nickname: C.text().pipe(C.nullable), displayName: C.text().pipe(C.default(Cast.to(Q.literal("guest"), Q.type.text()))), @@ -412,13 +412,13 @@ export const users = db.table("users", { await writeFile(schemaFile(workspace), ` import * as Pg from "effect-qb/postgres" import { Cast, Function as F, PrimaryKey, Query as Q, Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make(${JSON.stringify(schemaName)}) export const users = db.table("users", { - id: C.int().pipe(C.identityByDefault), - email: C.text().pipe(C.ddlType("character varying(255)")), + id: C.int().pipe(Pg.Column.identityByDefault), + email: C.text().pipe(Pg.Column.ddlType("character varying(255)")), nickname: C.text(), displayName: C.text().pipe(C.default(Cast.to(Q.literal("member"), Q.type.text()))), emailLower: C.text().pipe(C.generated(F.upper(Q.column("email", Q.type.text())))), @@ -777,7 +777,7 @@ test("postgres cli surfaces manual enum changes during push and migrate generate import * as Schema from "effect/Schema" import * as Pg from "effect-qb/postgres" import { Query as Q, Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") const types = Pg.Schema.make("__SCHEMA__") @@ -786,7 +786,7 @@ const status = types.enum("status", ["pending", "active"]) export const users = db.table("users", { id: C.text(), - status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(C.ddlType("\\"__SCHEMA__\\".\\"status\\"")) + status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(Pg.Column.ddlType("\\"__SCHEMA__\\".\\"status\\"")) }).pipe( Table.primaryKey((table) => table.id) ) @@ -805,7 +805,7 @@ export { status } import * as Schema from "effect/Schema" import * as Pg from "effect-qb/postgres" import { Query as Q, Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make(${JSON.stringify(schemaName)}) const types = Pg.Schema.make(${JSON.stringify(schemaName)}) @@ -814,7 +814,7 @@ const status = types.enum("status", ["pending"]) export const users = db.table("users", { id: C.text(), - status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(C.ddlType(${JSON.stringify(`"${schemaName}"."status"`)})) + status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(Pg.Column.ddlType(${JSON.stringify(`"${schemaName}"."status"`)})) }).pipe( Table.primaryKey((table) => table.id) ) @@ -837,7 +837,7 @@ export { status } import * as Schema from "effect/Schema" import * as Pg from "effect-qb/postgres" import { Query as Q, Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make(${JSON.stringify(schemaName)}) const types = Pg.Schema.make(${JSON.stringify(schemaName)}) @@ -846,7 +846,7 @@ const status = types.enum("status", ["active", "pending"]) export const users = db.table("users", { id: C.text(), - status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(C.ddlType(${JSON.stringify(`"${schemaName}"."status"`)})) + status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(Pg.Column.ddlType(${JSON.stringify(`"${schemaName}"."status"`)})) }).pipe( Table.primaryKey((table) => table.id) ) @@ -904,7 +904,7 @@ test("postgres cli pull fails when filtered tables reference missing source targ "schema.ts": ` import * as Pg from "effect-qb/postgres" import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") @@ -1017,7 +1017,7 @@ test("postgres cli honors source include exclude and table filters across multip "tables/users.ts": ` import * as Pg from "effect-qb/postgres" import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") @@ -1031,7 +1031,7 @@ export const users = db.table("users", { "tables/orgs.ts": ` import * as Pg from "effect-qb/postgres" import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") @@ -1045,7 +1045,7 @@ export const orgs = db.table("orgs", { "tables/ignored.ts": ` import * as Pg from "effect-qb/postgres" import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") @@ -1118,7 +1118,7 @@ test("postgres cli round-trips enum, foreign-key, generated, identity, and rich import * as Schema from "effect/Schema" import * as Pg from "effect-qb/postgres" import { Cast, ForeignKey, Function as F, Index, PrimaryKey, Query as Q, Table, Unique } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const tables = Pg.Schema.make("__SCHEMA__") const types = Pg.Schema.make("__SCHEMA__") @@ -1134,9 +1134,9 @@ const orgs = tables.table("orgs", { ) const users = tables.table("users", { - id: C.int().pipe(C.identityByDefault), + id: C.int().pipe(Pg.Column.identityByDefault), orgId: C.uuid(), - status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(C.ddlType("\\"__SCHEMA__\\".\\"status\\"")), + status: C.custom(Schema.String, Pg.Type.enum("status")).pipe(Pg.Column.ddlType("\\"__SCHEMA__\\".\\"status\\"")), email: C.text(), alias: C.text().pipe(C.nullable), displayName: C.text().pipe(C.default(Cast.to(Q.literal("guest"), Q.type.text()))), @@ -1221,7 +1221,7 @@ export { status, orgs, users } expect(pulledSchema).toContain(`deferrable: true`) expect(pulledSchema).toContain(`initiallyDeferred: true`) expect(pulledSchema).toContain(`users_email_lookup_idx`) - expect(pulledSchema).toContain(`include: ["displayName"]`) + expect(pulledSchema).toContain(`Pg.Index.include((table) => table.displayName)`) expect(pulledSchema).toContain(`order: "desc"`) expect(pulledSchema).toContain(`nulls: "last"`) expect(pulledSchema).toContain(`users_note_idx`) @@ -1230,7 +1230,7 @@ export { status, orgs, users } expect(pulledSchema).toContain(`Column.generated(`) const secondPullDryRun = await runCli("pull", "--config", config, "--dry-run") - expect(secondPullDryRun.exitCode).toBe(0) + expect(secondPullDryRun).toMatchObject({ exitCode: 0 }) expect(secondPullDryRun.stdout).toContain("schema definitions are already up to date") } finally { await dropSchema(schemaName).catch(() => undefined) @@ -1242,7 +1242,7 @@ test("postgres cli pulls supported checks and deferrable constraints into canoni const { workspace, schemaName } = await makeSourceWorkspace(` import * as Pg from "effect-qb/postgres" import { PrimaryKey, Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") @@ -1280,7 +1280,7 @@ export const users = db.table("users", { expect(pulledSchema).toContain(`initiallyDeferred: true`) expect(pulledSchema).toContain(`users_email_key`) expect(pulledSchema).toContain(`users_email_check`) - expect(pulledSchema).toContain(`noInherit: true`) + expect(pulledSchema).toContain(`Pg.Check.noInherit`) } finally { await dropSchema(schemaName).catch(() => undefined) @@ -1387,8 +1387,8 @@ test("postgres cli pull preserves non-default index collations", async () => { test("postgres cli pulls composite foreign keys into canonical source definitions", async () => { const { workspace, schemaName } = await makeSourceWorkspace(` import * as Pg from "effect-qb/postgres" -import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { PrimaryKey, Table } from "effect-qb" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") @@ -1436,13 +1436,11 @@ export { orgs, memberships } const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`memberships_org_fkey`) - expect(pulledSchema).toContain(`columns: ["tenantId", "orgSlug"]`) - expect(pulledSchema).toContain(`target: () => orgs`) - expect(pulledSchema).toContain(`referencedColumns: ["tenantId", "slug"]`) - expect(pulledSchema).toContain(`onDelete: "cascade"`) - expect(pulledSchema).toContain(`onUpdate: "noAction"`) - expect(pulledSchema).toContain(`deferrable: true`) - expect(pulledSchema).toContain(`initiallyDeferred: true`) + expect(pulledSchema).toContain(`ForeignKey.make((table) => [table.tenantId, table.orgSlug], () => [orgs.tenantId, orgs.slug])`) + expect(pulledSchema).toContain(`ForeignKey.onDelete("cascade")`) + expect(pulledSchema).toContain(`ForeignKey.onUpdate("noAction")`) + expect(pulledSchema).toContain(`Pg.ForeignKey.deferrable`) + expect(pulledSchema).toContain(`Pg.ForeignKey.initiallyDeferred`) } finally { await dropSchema(schemaName).catch(() => undefined) @@ -1454,7 +1452,7 @@ test("postgres cli pulls schema-builder table declarations into canonical source const { workspace, schemaName } = await makeSourceWorkspace(` import * as Pg from "effect-qb/postgres" import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" const db = Pg.Schema.make("__SCHEMA__") @@ -1484,7 +1482,7 @@ export const audits = db.table("audits", { const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`const audits = db.table(`) - expect(pulledSchema).toContain(`actorName: Column.text().pipe(Column.nullable, Column.index({ name: "audits_actor_name_idx"`) + expect(pulledSchema).toContain(`actorName: Column.text().pipe(Column.nullable, Pg.Column.index({ name: "audits_actor_name_idx"`) expect(pulledSchema).toContain(`audits_actor_name_idx`) await assertIdempotentPullPush(config) @@ -1525,10 +1523,10 @@ test("postgres cli pulls builtin postgres columns with dedicated constructors", expect(pull.stdout).toContain("updated 1 file(s)") const pulledSchema = await readSchema(workspace) - expect(pulledSchema).toContain(`payload: Column.jsonb(Schema.Unknown).pipe(Column.nullable)`) + expect(pulledSchema).toContain(`payload: Pg.Column.jsonb(Schema.Unknown).pipe(Column.nullable)`) expect(pulledSchema).toContain(`Column.varchar(32)`) expect(pulledSchema).toContain(`Column.char(1)`) - expect(pulledSchema).toContain(`Column.text().pipe(Column.array(), Column.nullable)`) + expect(pulledSchema).toContain(`Column.text().pipe(Pg.Column.array(), Column.nullable)`) expect(pulledSchema).toContain(`Column.number({ precision: 10, scale: 4 }).pipe(Column.nullable)`) expect(pulledSchema).toContain(`Column.int8()`) expect(pulledSchema).toContain(`Column.timestamptz()`) @@ -1551,7 +1549,7 @@ test("postgres cli pulls class table declarations into canonical source definiti const { workspace, schemaName } = await makeSourceWorkspace(` import * as Pg from "effect-qb/postgres" import { Table } from "effect-qb" -import { Column as C } from "effect-qb/postgres" +import { Column as C } from "effect-qb" export class Sessions extends Table.Class("sessions", "__SCHEMA__")({ id: C.uuid().pipe(C.primaryKey), @@ -1581,7 +1579,7 @@ export class Sessions extends Table.Class("sessions", "__SCHEMA__")({ expect(pulledSchema).toContain(`class Sessions extends Table.Class("sessions", "${schemaName}")({`) expect(pulledSchema).toContain(`lastSeenAt: Column.timestamp().pipe(`) expect(pulledSchema).toContain(`Column.timestamp().pipe(Column.nullable)`) - expect(pulledSchema).toContain(`email: Column.text().pipe(Column.index({ name: "sessions_email_idx"`) + expect(pulledSchema).toContain(`email: Column.text().pipe(Pg.Column.index({ name: "sessions_email_idx"`) expect(pulledSchema).toContain(`sessions_email_idx`) } finally { From aa246e49bfe6e29824a54ed663c66d75eca892d3 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:07:05 -0700 Subject: [PATCH 07/27] fix(database): decode Postgres migration identifiers --- packages/database/src/postgres/migrate.ts | 5 ++++- test/internal/behavior/postgres-migrate.behavior.test.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/database/src/postgres/migrate.ts b/packages/database/src/postgres/migrate.ts index 35fe5c3a..da18e3f5 100644 --- a/packages/database/src/postgres/migrate.ts +++ b/packages/database/src/postgres/migrate.ts @@ -118,7 +118,10 @@ export interface AppliedMigrationRow { const EmptyRequest = Schema.Struct({}) const AppliedMigrationRowSchema = Schema.Struct({ - id: Schema.Number, + id: Schema.Union([ + Schema.Number.check(Schema.isFinite()), + Schema.NumberFromString.check(Schema.isFinite()) + ]), name: Schema.String, checksum: Schema.NullOr(Schema.String) }) diff --git a/test/internal/behavior/postgres-migrate.behavior.test.ts b/test/internal/behavior/postgres-migrate.behavior.test.ts index 10854b73..2393aed9 100644 --- a/test/internal/behavior/postgres-migrate.behavior.test.ts +++ b/test/internal/behavior/postgres-migrate.behavior.test.ts @@ -101,7 +101,7 @@ describe("postgres migrations", () => { if (statement.includes("select id, name, checksum")) { return Effect.succeed([ { - id: "1", + id: "not-a-number", name: "0001_init.sql", checksum: null } From 1aa447b8181fbe2b4a178aec286e1be056b7ec14 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:07:20 -0700 Subject: [PATCH 08/27] refactor(database): route CLI output through Effect Terminal --- packages/database/src/cli.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/database/src/cli.ts b/packages/database/src/cli.ts index 15b11814..5c4e1d57 100644 --- a/packages/database/src/cli.ts +++ b/packages/database/src/cli.ts @@ -3,6 +3,7 @@ import { NodeRuntime, NodeServices } from "@effect/platform-node" import { Command, Flag } from "effect/unstable/cli" import * as Effect from "effect/Effect" import * as Option from "effect/Option" +import * as Terminal from "effect/Terminal" import { loadPostgresConfig, resolveDatabaseUrl } from "./internal/postgres-config.js" import { @@ -42,17 +43,13 @@ const effectFromPromise = (evaluate: () => Promise): Effect.Effect => - Effect.sync(() => { - console.log(line) - }) +const log = (line: string) => + Effect.flatMap(Terminal.Terminal, (terminal) => terminal.display(`${line}\n`)) -const logLines = (lines: readonly string[]): Effect.Effect => - Effect.sync(() => { - if (lines.length > 0) { - console.log(lines.join("\n")) - } - }) +const logLines = (lines: readonly string[]) => + lines.length === 0 + ? Effect.void + : Effect.flatMap(Terminal.Terminal, (terminal) => terminal.display(`${lines.join("\n")}\n`)) const configOption = Flag.string("config").pipe( Flag.optional, From 70565f661b831b672fb49429428b4bda315cb352 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:07:33 -0700 Subject: [PATCH 09/27] test(database): exercise built CLI with Node --- package.json | 4 ++-- scripts/test-integration.ts | 2 +- test/public/integration/postgres-cli.integration.ts | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 37cf9900..d0bf05cb 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "generate:readme-types": "bun scripts/generate-readme-type-tests.ts", "release": "bun scripts/release.ts", "test:pack": "bun scripts/check-packed-querybuilder.ts", - "test": "bun --config ./bunfig.test.toml test", + "test": "bun test", "test:types": "bun run generate:readme-types && bunx tsgo -p tsconfig.type-tests.json", - "test:integration": "bun scripts/test-integration.ts", + "test:integration": "bun run build && bun scripts/test-integration.ts", "test:integration:datatypes": "bun scripts/test-datatype-matrix-integration.ts" }, "devDependencies": { diff --git a/scripts/test-integration.ts b/scripts/test-integration.ts index 3f921bcc..21a3b0c7 100644 --- a/scripts/test-integration.ts +++ b/scripts/test-integration.ts @@ -6,7 +6,7 @@ const bunfig = "./bunfig.integration.toml" const main = async () => { try { await $`docker compose -f ${composeFile} up -d --wait` - await $`bun --config ${bunfig} test --timeout 60000 ./test/public/integration/*.integration.ts` + await $`bun --config=${bunfig} test --timeout 60000 ./test/public/integration/*.integration.ts` } finally { try { await $`docker compose -f ${composeFile} down -v` diff --git a/test/public/integration/postgres-cli.integration.ts b/test/public/integration/postgres-cli.integration.ts index ba4c21d0..1c95488b 100644 --- a/test/public/integration/postgres-cli.integration.ts +++ b/test/public/integration/postgres-cli.integration.ts @@ -6,7 +6,11 @@ import { dirname, join } from "node:path" import { execPostgres, withPostgresLock } from "./helpers.ts" const repoRoot = process.cwd() -const cliEntry = join(repoRoot, "packages", "database", "src", "cli.ts") +const cliEntry = join(repoRoot, "packages", "database", "dist", "cli.js") +const nodePath = Bun.which("node") +if (nodePath === null) { + throw new Error("Node.js is required for effect-db CLI integration tests") +} const postgresUrl = "postgres://effect_qb:effect_qb@127.0.0.1:55432/effect_qb_test" const randomId = () => Math.random().toString(36).slice(2, 10) @@ -145,7 +149,7 @@ const runCli = async (...args: readonly string[]): Promise<{ }> => { return withPostgresLock(async () => { const proc = Bun.spawn([ - process.execPath, + nodePath, cliEntry, ...args ], { @@ -173,7 +177,7 @@ const runCliUnlocked = async (...args: readonly string[]): Promise<{ readonly stderr: string }> => { const proc = Bun.spawn([ - process.execPath, + nodePath, cliEntry, ...args ], { From c32ca0d6713b19fb5e6f1b0724f60366e0763f69 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:07:46 -0700 Subject: [PATCH 10/27] build(repo): add Node-native package verification --- .github/workflows/ci.yml | 50 ++++++++++-------------------- bun.lock | 13 ++++++-- package.json | 9 +++++- packages/database/scripts/build.ts | 2 +- tsconfig.node.json | 10 ++++++ 5 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 tsconfig.node.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c6860c4..b17582c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.3.5 - name: Install dependencies run: bun install --frozen-lockfile @@ -46,12 +46,13 @@ jobs: bunx npm pack --dry-run ./packages/querybuilder bunx npm pack --dry-run ./packages/database - - name: Smoke test packed querybuilder package - run: bun run test:pack - verify-node: - name: Verify Node 22 + name: Verify packed artifacts (Node ${{ matrix.node }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: [22, 24] steps: - name: Checkout uses: actions/checkout@v4 @@ -59,38 +60,24 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Build package - run: bun run build + bun-version: 1.3.5 - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 22.0.0 + node-version: ${{ matrix.node }} - - name: Smoke test published entrypoints - run: | - node --input-type=module <<'EOF' - import * as Root from "./packages/querybuilder/dist/index.js" - import * as Postgres from "./packages/querybuilder/dist/postgres.js" - import * as Database from "./packages/database/dist/index.js" + - name: Install dependencies + run: bun install --frozen-lockfile - if (typeof Root.Query?.select !== "function") { - throw new Error("querybuilder root entrypoint did not load") - } + - name: Build package + run: bun run build - if (typeof Postgres.Renderer?.make !== "function" || typeof Postgres.Executor?.make !== "function") { - throw new Error("querybuilder postgres entrypoint did not load") - } + - name: Typecheck production sources without Bun globals + run: bun run test:types:node - if (typeof Database.defineConfig !== "function") { - throw new Error("database entrypoint did not load") - } - EOF + - name: Smoke test packed packages + run: bun run test:pack integration: name: Integration @@ -103,13 +90,10 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.3.5 - name: Install dependencies run: bun install --frozen-lockfile - - name: Build package - run: bun run build - - name: Run integration tests run: bun run test:integration diff --git a/bun.lock b/bun.lock index 54a4c400..0e6d1ed9 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@effect/sql-pg": "4.0.0-beta.98", "@effect/sql-sqlite-bun": "4.0.0-beta.98", "@types/bun": "latest", + "@types/node": "^22.0.0", "@typescript/native-preview": "beta", "effect": "4.0.0-beta.98", "effect-db": "workspace:*", @@ -84,7 +85,7 @@ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -232,7 +233,7 @@ "undici": ["undici@8.8.0", "", {}, "sha512-ubshXMXwF3MQIMF1y/WxZdNBnjEKeSg2wF5mcGUtU55YTw34tnVVpKRlLf7ruDXZ5344KokPVX4RBx1wJm64Bw=="], - "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], @@ -242,8 +243,16 @@ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "@types/ws/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + + "bun-types/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + "pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "@types/ws/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "bun-types/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], diff --git a/package.json b/package.json index d0bf05cb..2ca20850 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,12 @@ { "name": "effect-qb-workspace", "version": "0.20.0", + "private": true, "type": "module", + "packageManager": "bun@1.3.5", + "engines": { + "bun": ">=1.3.5" + }, "workspaces": [ "packages/*" ], @@ -21,7 +26,8 @@ "release": "bun scripts/release.ts", "test:pack": "bun scripts/check-packed-querybuilder.ts", "test": "bun test", - "test:types": "bun run generate:readme-types && bunx tsgo -p tsconfig.type-tests.json", + "test:types": "bun run generate:readme-types && bunx tsgo -p tsconfig.type-tests.json && bun run test:types:node", + "test:types:node": "bunx tsgo -p tsconfig.node.json", "test:integration": "bun run build && bun scripts/test-integration.ts", "test:integration:datatypes": "bun scripts/test-datatype-matrix-integration.ts" }, @@ -31,6 +37,7 @@ "@effect/sql-pg": "4.0.0-beta.98", "@effect/sql-sqlite-bun": "4.0.0-beta.98", "@types/bun": "latest", + "@types/node": "^22.0.0", "@typescript/native-preview": "beta", "effect": "4.0.0-beta.98", "effect-db": "workspace:*", diff --git a/packages/database/scripts/build.ts b/packages/database/scripts/build.ts index aa0a8a28..59beb751 100644 --- a/packages/database/scripts/build.ts +++ b/packages/database/scripts/build.ts @@ -56,7 +56,7 @@ const main = async () => { "--skipLibCheck", "--allowImportingTsExtensions", "--types", - "bun", + "node", "src/index.ts", "src/postgres/pull.ts", "src/postgres/push.ts", diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 00000000..5af02114 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"] + }, + "include": [ + "packages/*/src/**/*.ts" + ] +} From 5e458f4859d9deb4265dd8f3d1fb0e9f8c248022 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:07:55 -0700 Subject: [PATCH 11/27] docs(repo): prefer Effect Platform runtime services --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index f2896c14..f4ccc3ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,6 +127,9 @@ For more information, read the Bun API docs in `node_modules/bun-types/docs/**.m - Keep Bun as the workspace package manager and tooling runtime. - Keep published `effect-qb` and `effect-db` runtime behavior Node.js-native. +- Prefer Effect Platform services for filesystem, path, terminal, process, and + other host capabilities. Keep Node-specific implementations at the runtime + assembly boundary with `NodeServices` and `NodeRuntime`. - Assume consumer code is bundled or built with esbuild. Do not add a runtime TypeScript loader solely to support unbuilt TypeScript syntax. From a7d8167fc42bbf7d0ec533ba154d9645fd9623d0 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:09:05 -0700 Subject: [PATCH 12/27] chore(beads): close Node-native verification --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index d98e4cc1..2c9ba262 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -27,7 +27,7 @@ {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","status":"open","priority":2,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:50:07Z","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"effect-qb-02d","title":"Complete Node-native package verification","description":"Finish the Node-native boundaries for effect-db and effect-qb while retaining Bun for workspace package management and tooling.","acceptance_criteria":"Postgres CLI integration executes packages/database/dist/cli.js with Node; CLI host output uses Effect Platform services with Node implementations confined to runtime assembly; production package source typechecks with explicit Node types and without Bun ambient types; packed artifacts are smoked on Node 22 and Node 24 in CI; the root workspace is private and declares its Bun package manager/engine; focused and existing quality gates pass.","notes":"User clarified that portable host capabilities should use Effect Platform wherever practical; Node remains the executable adapter through NodeServices/NodeRuntime.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:43:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:45:50Z","started_at":"2026-07-22T19:43:55Z","labels":["ci","node","packaging"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-02d","title":"Complete Node-native package verification","description":"Finish the Node-native boundaries for effect-db and effect-qb while retaining Bun for workspace package management and tooling.","acceptance_criteria":"Postgres CLI integration executes packages/database/dist/cli.js with Node; CLI host output uses Effect Platform services with Node implementations confined to runtime assembly; production package source typechecks with explicit Node types and without Bun ambient types; packed artifacts are smoked on Node 22 and Node 24 in CI; the root workspace is private and declares its Bun package manager/engine; focused and existing quality gates pass.","notes":"User clarified that portable host capabilities should use Effect Platform wherever practical; Node remains the executable adapter through NodeServices/NodeRuntime.\nImplemented: built CLI integration now runs dist/cli.js with Node; CLI output uses Effect Terminal; production sources typecheck with @types/node and no Bun globals; CI smokes packed packages on Node 22/24; root declares Bun package metadata. Production-path verification also fixed stale extension-only pull generation, Postgres bigint migration ID decoding, and the silent no-op Bun test config argument. Verified 534 behavior tests, 55 live integration tests, full type tests, build, frozen install, and packed Node 22/24 smoke.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:43:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:08:18Z","started_at":"2026-07-22T19:43:55Z","closed_at":"2026-07-22T20:08:18Z","close_reason":"Node-native package verification and production CLI coverage are implemented and passing; broader Effect Platform I/O migration remains tracked by effect-qb-8l5.","labels":["ci","node","packaging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"closed","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:22:13Z","started_at":"2026-07-22T03:13:13Z","closed_at":"2026-07-22T03:22:13Z","close_reason":"effect-db now uses the Effect Node runtime and Node-compatible file APIs; packed Node 22 smoke and behavior/type tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-1ib","title":"Merge effect v4 branch into main","description":"Bring refactor/effect-v4 up to date with current main changes, ensure published package dependency metadata treats effect as a peer dependency, run quality gates, then merge and push main.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:39:26Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T03:00:19Z","started_at":"2026-06-29T02:39:47Z","closed_at":"2026-06-29T03:00:19Z","close_reason":"Merged refactor/effect-v4 into main; local type, build, test, and pack gates pass. Docker integration was attempted but local Docker hung before tests could complete.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-9cp","title":"Document scoped executor transactions","description":"README transactions section should lead with the preferred Executor.withTransaction pipe-based scope and clarify that nested withTransaction calls use savepoints, while Query.transaction/savepoint remain low-level statement builders.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:32:14Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T02:33:38Z","started_at":"2026-06-29T02:32:39Z","closed_at":"2026-06-29T02:33:38Z","close_reason":"Added pipe-based Executor.withTransaction documentation and regenerated README type fixtures.","dependency_count":0,"dependent_count":0,"comment_count":0} From 0051f2df0d92ba3ea99037c2da612f50f2ca6205 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:26:38 -0700 Subject: [PATCH 13/27] chore(beads): claim Effect Platform migration --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2c9ba262..cbebfe56 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -26,7 +26,7 @@ {"id":"effect-qb-dn3.4","title":"P1 validate effect-db rows with SqlSchema","description":"Use v4 effect/unstable/sql/SqlSchema for high-risk effect-db result rows, starting with Postgres introspection and migration ledger reads.","acceptance_criteria":"Introspection and migration ledger row shapes are represented by internal schemas; selected unsafe typed row reads are replaced or wrapped with SqlSchema helpers; checksum/migration behavior is unchanged except for earlier shape failures; behavior/integration tests cover happy path and malformed-row failure where feasible.","notes":"Status: open. Validation plan: postgres-introspector behavior tests, postgres-migrate behavior tests, CLI integration tests if Postgres is available. Keep source-code discovery independent of SqlSchema.\nImplemented in second migration slice. Added v4 SqlSchema row decoders around Postgres catalog introspection reads and applied-migration ledger reads. Malformed catalog and ledger row tests now assert schema decode failure before metadata/migration state is built. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/postgres-introspector.behavior.test.ts test/internal/behavior/postgres-migrate.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:38Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:45:33Z","started_at":"2026-05-13T02:42:58Z","closed_at":"2026-05-13T02:45:33Z","close_reason":"effect-db catalog and migration ledger row reads now decode through v4 SqlSchema with malformed-row tests and repo gates passing.","labels":["effect-db","effect-v4","migration","sqlschema"],"dependencies":[{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:38Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3.3","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","status":"open","priority":2,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T19:50:07Z","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:12:55Z","started_at":"2026-07-22T20:12:55Z","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-02d","title":"Complete Node-native package verification","description":"Finish the Node-native boundaries for effect-db and effect-qb while retaining Bun for workspace package management and tooling.","acceptance_criteria":"Postgres CLI integration executes packages/database/dist/cli.js with Node; CLI host output uses Effect Platform services with Node implementations confined to runtime assembly; production package source typechecks with explicit Node types and without Bun ambient types; packed artifacts are smoked on Node 22 and Node 24 in CI; the root workspace is private and declares its Bun package manager/engine; focused and existing quality gates pass.","notes":"User clarified that portable host capabilities should use Effect Platform wherever practical; Node remains the executable adapter through NodeServices/NodeRuntime.\nImplemented: built CLI integration now runs dist/cli.js with Node; CLI output uses Effect Terminal; production sources typecheck with @types/node and no Bun globals; CI smokes packed packages on Node 22/24; root declares Bun package metadata. Production-path verification also fixed stale extension-only pull generation, Postgres bigint migration ID decoding, and the silent no-op Bun test config argument. Verified 534 behavior tests, 55 live integration tests, full type tests, build, frozen install, and packed Node 22/24 smoke.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:43:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:08:18Z","started_at":"2026-07-22T19:43:55Z","closed_at":"2026-07-22T20:08:18Z","close_reason":"Node-native package verification and production CLI coverage are implemented and passing; broader Effect Platform I/O migration remains tracked by effect-qb-8l5.","labels":["ci","node","packaging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"closed","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:22:13Z","started_at":"2026-07-22T03:13:13Z","closed_at":"2026-07-22T03:22:13Z","close_reason":"effect-db now uses the Effect Node runtime and Node-compatible file APIs; packed Node 22 smoke and behavior/type tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-1ib","title":"Merge effect v4 branch into main","description":"Bring refactor/effect-v4 up to date with current main changes, ensure published package dependency metadata treats effect as a peer dependency, run quality gates, then merge and push main.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:39:26Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T03:00:19Z","started_at":"2026-06-29T02:39:47Z","closed_at":"2026-06-29T03:00:19Z","close_reason":"Merged refactor/effect-v4 into main; local type, build, test, and pack gates pass. Docker integration was attempted but local Docker hung before tests could complete.","dependency_count":0,"dependent_count":0,"comment_count":0} From ded9444ccd1ff27fc84127003c7c6d4363825cb4 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:26:44 -0700 Subject: [PATCH 14/27] refactor(database): route I/O through Effect Platform services --- packages/database/src/cli.ts | 158 +++++------ packages/database/src/index.ts | 1 + .../database/src/internal/node-platform.ts | 20 ++ .../database/src/internal/postgres-config.ts | 160 ++++++----- .../database/src/internal/postgres-runtime.ts | 6 +- .../src/internal/postgres-source-discovery.ts | 267 ++++++++++-------- packages/database/src/postgres/migrate.ts | 229 ++++++++------- packages/database/src/postgres/pull.ts | 115 +++++--- packages/database/src/postgres/push.ts | 54 ++-- 9 files changed, 564 insertions(+), 446 deletions(-) create mode 100644 packages/database/src/internal/node-platform.ts diff --git a/packages/database/src/cli.ts b/packages/database/src/cli.ts index 5c4e1d57..ba56e885 100644 --- a/packages/database/src/cli.ts +++ b/packages/database/src/cli.ts @@ -5,9 +5,9 @@ import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as Terminal from "effect/Terminal" -import { loadPostgresConfig, resolveDatabaseUrl } from "./internal/postgres-config.js" +import { loadPostgresConfigEffect, resolveDatabaseUrl, type EffectDbConfig } from "./internal/postgres-config.js" import { - loadPostgresSchemaPlan, + loadPostgresSchemaPlanEffect, selectedPostgresSchemaChanges, skippedPostgresSchemaChanges, summarizeSelectedPostgresPlan, @@ -18,30 +18,19 @@ import { applyMigrationFiles, applyStatements, loadAppliedMigrationRows, - loadPostgresMigrationState, - migrationDirFromConfig, + loadPostgresMigrationStateEffect, + migrationDirFromConfigEffect, migrationFileLabel, - readMigrationFiles, + readMigrationFilesEffect, rollbackMigrationFiles, withMigrationLock, - writeMigrationFile + writeMigrationFileEffect } from "./postgres/migrate.js" -import { planPostgresPull, applyPullPlan, summarizePullPlan } from "./postgres/pull.js" -import { runPostgresUrl } from "./internal/postgres-runtime.js" +import { planPostgresPullEffect, applyPullPlanEffect, summarizePullPlanEffect } from "./postgres/pull.js" +import { providePostgresUrl } from "./internal/postgres-runtime.js" import { introspectPostgresSchema } from "./internal/postgres-introspector.js" import { filterDiscoveredSourceSchema } from "./internal/postgres-source-filter.js" -import { discoverSourceSchema } from "./internal/postgres-source-discovery.js" - -const toError = (cause: unknown): Error => - cause instanceof Error - ? cause - : new Error(String(cause)) - -const effectFromPromise = (evaluate: () => Promise): Effect.Effect => - Effect.tryPromise({ - try: evaluate, - catch: toError - }) +import { discoverSourceSchemaEffect } from "./internal/postgres-source-discovery.js" const log = (line: string) => Effect.flatMap(Terminal.Terminal, (terminal) => terminal.display(`${line}\n`)) @@ -80,19 +69,19 @@ const stepsOption = Flag.integer("steps").pipe( Flag.withDescription("Number of applied migrations to roll back") ) -const withLoadedConfig = ( +const withLoadedConfig = ( explicitConfigPath: Option.Option, explicitUrl: Option.Option, f: (args: { readonly cwd: string readonly configPath?: string readonly databaseUrl: string - readonly config: Awaited>["config"] - }) => Promise -): Effect.Effect => - effectFromPromise(async () => { - const loaded = await loadPostgresConfig(process.cwd(), Option.getOrUndefined(explicitConfigPath)) - return await f({ + readonly config: EffectDbConfig + }) => Effect.Effect +) => + Effect.gen(function*() { + const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(explicitConfigPath)) + return yield* f({ cwd: loaded.cwd, configPath: loaded.path, databaseUrl: resolveDatabaseUrl(loaded.config, Option.getOrUndefined(explicitUrl)), @@ -110,8 +99,8 @@ const push = Command.make( }, ({ config, url, dryRun, allowDestructive }) => Effect.gen(function*() { - const { plan, discovered } = yield* withLoadedConfig(config, url, async ({ cwd, config, databaseUrl }) => - loadPostgresSchemaPlan(cwd, config, databaseUrl) + const { plan, discovered } = yield* withLoadedConfig(config, url, ({ cwd, config, databaseUrl }) => + loadPostgresSchemaPlanEffect(cwd, config, databaseUrl) ) const selected = selectedPostgresSchemaChanges(plan, allowDestructive) const skipped = skippedPostgresSchemaChanges(plan, allowDestructive) @@ -126,7 +115,7 @@ const push = Command.make( } if (selected.length > 0) { yield* withLoadedConfig(config, url, ({ databaseUrl }) => - runPostgresUrl( + providePostgresUrl( databaseUrl, applyStatements(selected.map((change) => change.sql!).filter((sql): sql is string => sql !== undefined)) ) @@ -150,18 +139,18 @@ const pull = Command.make( }, ({ config, url, dryRun }) => Effect.gen(function*() { - const { loaded, database, discovered, plan } = yield* effectFromPromise(async () => { - const loaded = await loadPostgresConfig(process.cwd(), Option.getOrUndefined(config)) + const { loaded, database, discovered, plan } = yield* Effect.gen(function*() { + const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) const discovered = filterDiscoveredSourceSchema( - await discoverSourceSchema(loaded.cwd, loaded.config.source), + yield* discoverSourceSchemaEffect(loaded.cwd, loaded.config.source), loaded.config.filter ) const database = withoutManagedMigrationTable( - await runPostgresUrl(databaseUrl, introspectPostgresSchema(loaded.config.filter)), + yield* providePostgresUrl(databaseUrl, introspectPostgresSchema(loaded.config.filter)), loaded.config.migrations.table ) - const plan = await planPostgresPull(loaded.cwd, loaded.config.source, discovered, database) + const plan = yield* planPostgresPullEffect(loaded.cwd, loaded.config.source, discovered, database) return { loaded, database, discovered, plan } }) void database @@ -169,9 +158,9 @@ const pull = Command.make( if (plan.updates.length === 0) { return yield* log("schema definitions are already up to date") } - yield* logLines(summarizePullPlan(loaded.cwd, plan)) + yield* logLines(yield* summarizePullPlanEffect(loaded.cwd, plan)) if (!dryRun) { - yield* effectFromPromise(() => applyPullPlan(plan)) + yield* applyPullPlanEffect(plan) yield* log(`updated ${plan.updates.length} file(s)`) } }) @@ -187,10 +176,10 @@ const migrateGenerate = Command.make( }, ({ config, url, allowDestructive, name }) => Effect.gen(function*() { - const { loaded, plan } = yield* effectFromPromise(async () => { - const loaded = await loadPostgresConfig(process.cwd(), Option.getOrUndefined(config)) + const { loaded, plan } = yield* Effect.gen(function*() { + const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) - const { plan } = await loadPostgresSchemaPlan(loaded.cwd, loaded.config, databaseUrl) + const { plan } = yield* loadPostgresSchemaPlanEffect(loaded.cwd, loaded.config, databaseUrl) return { loaded, plan } }) const selected = selectedPostgresSchemaChanges(plan, allowDestructive) @@ -198,12 +187,11 @@ const migrateGenerate = Command.make( if (selected.length === 0) { yield* log("no executable migration changes selected") } else { - const filePath = yield* effectFromPromise(() => - writeMigrationFile( - migrationDirFromConfig(loaded.cwd, loaded.config.migrations.dir), - Option.getOrElse(name, () => allowDestructive ? "schema_destructive" : "schema_safe"), - selected - ) + const migrationsDir = yield* migrationDirFromConfigEffect(loaded.cwd, loaded.config.migrations.dir) + const filePath = yield* writeMigrationFileEffect( + migrationsDir, + Option.getOrElse(name, () => allowDestructive ? "schema_destructive" : "schema_safe"), + selected ) yield* log(`wrote ${migrationFileLabel(filePath)}`) } @@ -221,30 +209,24 @@ const migrateUp = Command.make( }, ({ config, url }) => Effect.gen(function*() { - const { loaded, databaseUrl } = yield* effectFromPromise(async (): Promise<{ - readonly loaded: Awaited> - readonly databaseUrl: string - }> => { - const loaded = await loadPostgresConfig(process.cwd(), Option.getOrUndefined(config)) + const { loaded, databaseUrl } = yield* Effect.gen(function*() { + const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) return { loaded, databaseUrl } }) - const applied = yield* effectFromPromise(() => - runPostgresUrl( - databaseUrl, - withMigrationLock(loaded.config.migrations.table, Effect.gen(function*() { - const files = yield* Effect.promise(() => - readMigrationFiles(migrationDirFromConfig(loaded.cwd, loaded.config.migrations.dir)) - ) - const appliedRows = yield* loadAppliedMigrationRows(loaded.config.migrations.table, files) - const applied = new Set(appliedRows.map((row) => row.name)) - const currentPending = files.filter((file) => !applied.has(file.name)) - if (currentPending.length > 0) { - yield* applyMigrationFiles(loaded.config.migrations.table, currentPending) - } - return currentPending - })) - ) + const applied = yield* providePostgresUrl( + databaseUrl, + withMigrationLock(loaded.config.migrations.table, Effect.gen(function*() { + const migrationsDir = yield* migrationDirFromConfigEffect(loaded.cwd, loaded.config.migrations.dir) + const files = yield* readMigrationFilesEffect(migrationsDir) + const appliedRows = yield* loadAppliedMigrationRows(loaded.config.migrations.table, files) + const applied = new Set(appliedRows.map((row) => row.name)) + const currentPending = files.filter((file) => !applied.has(file.name)) + if (currentPending.length > 0) { + yield* applyMigrationFiles(loaded.config.migrations.table, currentPending) + } + return currentPending + })) ) if (applied.length === 0) { return yield* log("no pending migrations") @@ -264,10 +246,10 @@ const migrateStatus = Command.make( }, ({ config, url }) => Effect.gen(function*() { - const { loaded, databaseUrl, appliedRows, pending } = yield* effectFromPromise(async () => { - const loaded = await loadPostgresConfig(process.cwd(), Option.getOrUndefined(config)) + const { loaded, databaseUrl, appliedRows, pending } = yield* Effect.gen(function*() { + const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) - const state = await loadPostgresMigrationState(loaded, databaseUrl) + const state = yield* loadPostgresMigrationStateEffect(loaded, databaseUrl) return { loaded, databaseUrl, @@ -296,10 +278,10 @@ const migrateDown = Command.make( }, ({ config, url, dryRun, steps }) => Effect.gen(function*() { - const { loaded, databaseUrl, selected } = yield* effectFromPromise(async () => { - const loaded = await loadPostgresConfig(process.cwd(), Option.getOrUndefined(config)) + const { loaded, databaseUrl, selected } = yield* Effect.gen(function*() { + const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) - const state = await loadPostgresMigrationState(loaded, databaseUrl) + const state = yield* loadPostgresMigrationStateEffect(loaded, databaseUrl) const stepCount = Math.max(1, Option.getOrElse(steps, () => 1)) const applied = [...state.appliedRows].slice(Math.max(0, state.appliedRows.length - stepCount)).reverse() const fileByName = new Map(state.files.map((file) => [file.name, file])) @@ -327,13 +309,11 @@ const migrateDown = Command.make( ...selected.map((file) => ` - ${file.name}`) ]) if (!dryRun) { - yield* effectFromPromise(() => - runPostgresUrl( - databaseUrl, - withMigrationLock( - loaded.config.migrations.table, - rollbackMigrationFiles(loaded.config.migrations.table, selected) - ) + yield* providePostgresUrl( + databaseUrl, + withMigrationLock( + loaded.config.migrations.table, + rollbackMigrationFiles(loaded.config.migrations.table, selected) ) ) yield* log(`rolled back ${selected.length} migration(s)`) @@ -350,10 +330,10 @@ const migrateRepair = Command.make( }, ({ config, url, dryRun }) => Effect.gen(function*() { - const { loaded, databaseUrl, orphanNames } = yield* effectFromPromise(async () => { - const loaded = await loadPostgresConfig(process.cwd(), Option.getOrUndefined(config)) + const { loaded, databaseUrl, orphanNames } = yield* Effect.gen(function*() { + const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) - const state = await loadPostgresMigrationState(loaded, databaseUrl) + const state = yield* loadPostgresMigrationStateEffect(loaded, databaseUrl) const fileNames = new Set(state.files.map((file) => file.name)) const orphanNames = state.appliedRows .map((row) => row.name) @@ -372,13 +352,11 @@ const migrateRepair = Command.make( ...orphanNames.map((name) => ` - ${name}`) ]) if (!dryRun) { - yield* effectFromPromise(() => - runPostgresUrl( - databaseUrl, - withMigrationLock( - loaded.config.migrations.table, - deleteAppliedMigrationNames(loaded.config.migrations.table, orphanNames) - ) + yield* providePostgresUrl( + databaseUrl, + withMigrationLock( + loaded.config.migrations.table, + deleteAppliedMigrationNames(loaded.config.migrations.table, orphanNames) ) ) yield* log(`repaired ${orphanNames.length} migration record(s)`) diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 566beed2..758fdf63 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -1,6 +1,7 @@ export { defineConfig, loadPostgresConfig, + loadPostgresConfigEffect, resolveDatabaseUrl, type EffectDbConfig, type FilterConfig, diff --git a/packages/database/src/internal/node-platform.ts b/packages/database/src/internal/node-platform.ts new file mode 100644 index 00000000..a9067c82 --- /dev/null +++ b/packages/database/src/internal/node-platform.ts @@ -0,0 +1,20 @@ +import { NodePath, NodeServices } from "@effect/platform-node" +import * as Crypto from "effect/Crypto" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" + +export type PlatformServices = + | Crypto.Crypto + | FileSystem.FileSystem + | Path.Path + +export const runNodePlatform = ( + effect: Effect.Effect +): Promise => + Effect.runPromise(Effect.provide(effect, NodeServices.layer)) + +export const runNodePath = ( + effect: Effect.Effect +): A => + Effect.runSync(Effect.provide(effect, NodePath.layer)) diff --git a/packages/database/src/internal/postgres-config.ts b/packages/database/src/internal/postgres-config.ts index 6fb7c07d..25c1027a 100644 --- a/packages/database/src/internal/postgres-config.ts +++ b/packages/database/src/internal/postgres-config.ts @@ -1,6 +1,8 @@ -import { access } from "node:fs/promises" -import { dirname, resolve } from "node:path" -import { pathToFileURL } from "node:url" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" + +import { runNodePlatform } from "./node-platform.js" export type FilterConfig = { readonly schemas?: readonly string[] @@ -245,85 +247,97 @@ const defaultConfig = (): EffectDbConfig => ({ } }) -const fileExists = async (path: string): Promise => { - try { - await access(path) - return true - } catch { - return false - } -} - -const loadModuleConfig = async (path: string): Promise => { - const imported = await import(pathToFileURL(path).href) - return imported.default ?? imported.config ?? imported -} - -export const loadPostgresConfig = async ( +const loadModuleConfig = (path: string): Effect.Effect => + Effect.gen(function*() { + const paths = yield* Path.Path + const url = yield* paths.toFileUrl(path) + const imported = yield* Effect.tryPromise({ + try: () => import(url.href), + catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)) + }) + return imported.default ?? imported.config ?? imported + }) + +export const loadPostgresConfigEffect = ( cwd: string, explicitPath?: string -): Promise => { - const configPath = explicitPath === undefined - ? await (async () => { - for (const name of DEFAULT_CONFIG_NAMES) { - const candidate = resolve(cwd, name) - if (await fileExists(candidate)) { - return candidate +): Effect.Effect => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path + const configPath = explicitPath === undefined + ? yield* Effect.gen(function*() { + for (const name of DEFAULT_CONFIG_NAMES) { + const candidate = paths.resolve(cwd, name) + if (yield* fs.exists(candidate)) { + return candidate + } } - } - return undefined - })() - : resolve(cwd, explicitPath) + return undefined + }) + : paths.resolve(cwd, explicitPath) - if (configPath === undefined) { - return { - config: defaultConfig(), - cwd + if (configPath === undefined) { + return { + config: defaultConfig(), + cwd + } } - } - const loaded = await loadModuleConfig(configPath) - if (typeof loaded !== "object" || loaded === null) { - throw new Error(`Config file '${configPath}' did not export an object`) - } - validatePartialPostgresConfig(loaded, "config") - - const partial = loaded as Partial - - const merged = { - ...defaultConfig(), - ...partial, - db: { - ...defaultConfig().db, - ...(partial.db ?? {}) - }, - source: { - ...defaultConfig().source, - ...(partial.source ?? {}) - }, - filter: partial.filter === undefined - ? undefined - : { - ...(partial.filter ?? {}) - }, - migrations: { - ...defaultConfig().migrations, - ...(partial.migrations ?? {}) - }, - safety: { - ...defaultConfig().safety, - ...(partial.safety ?? {}) + const loaded = yield* loadModuleConfig(configPath) + if (typeof loaded !== "object" || loaded === null) { + return yield* Effect.fail(new Error(`Config file '${configPath}' did not export an object`)) } - } satisfies EffectDbConfig + yield* Effect.try({ + try: () => validatePartialPostgresConfig(loaded, "config"), + catch: (cause) => cause + }) + + const partial = loaded as Partial + + const merged = { + ...defaultConfig(), + ...partial, + db: { + ...defaultConfig().db, + ...(partial.db ?? {}) + }, + source: { + ...defaultConfig().source, + ...(partial.source ?? {}) + }, + filter: partial.filter === undefined + ? undefined + : { + ...(partial.filter ?? {}) + }, + migrations: { + ...defaultConfig().migrations, + ...(partial.migrations ?? {}) + }, + safety: { + ...defaultConfig().safety, + ...(partial.safety ?? {}) + } + } satisfies EffectDbConfig - validateResolvedPostgresConfig(merged) + yield* Effect.try({ + try: () => validateResolvedPostgresConfig(merged), + catch: (cause) => cause + }) - return { - config: merged, - cwd: dirname(configPath), - path: configPath - } -} + return { + config: merged, + cwd: paths.dirname(configPath), + path: configPath + } + }) + +export const loadPostgresConfig = ( + cwd: string, + explicitPath?: string +): Promise => + runNodePlatform(loadPostgresConfigEffect(cwd, explicitPath)) export const resolveDatabaseUrl = ( config: EffectDbConfig, diff --git a/packages/database/src/internal/postgres-runtime.ts b/packages/database/src/internal/postgres-runtime.ts index f859d002..aeb7dc1a 100644 --- a/packages/database/src/internal/postgres-runtime.ts +++ b/packages/database/src/internal/postgres-runtime.ts @@ -3,10 +3,10 @@ import { PgClient } from "@effect/sql-pg" import * as Effect from "effect/Effect" import * as Redacted from "effect/Redacted" -export const providePostgresUrl = ( +export const providePostgresUrl = ( url: string, - effect: Effect.Effect -): Effect.Effect => + effect: Effect.Effect +) => Effect.provide(effect, PgClient.layer({ url: Redacted.make(url) })) diff --git a/packages/database/src/internal/postgres-source-discovery.ts b/packages/database/src/internal/postgres-source-discovery.ts index 7edee1a4..183768e5 100644 --- a/packages/database/src/internal/postgres-source-discovery.ts +++ b/packages/database/src/internal/postgres-source-discovery.ts @@ -1,8 +1,8 @@ import * as Std from "effect-qb" -import { randomUUID } from "node:crypto" -import { readFile, rm, writeFile } from "node:fs/promises" -import { basename, dirname, extname, join, relative, resolve } from "node:path" -import { pathToFileURL } from "node:url" +import * as Crypto from "effect/Crypto" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" import { glob } from "tinyglobby" import ts from "typescript" @@ -14,6 +14,7 @@ import { tableKey, type SchemaModel } from "effect-qb/postgres/metadata" +import { runNodePlatform } from "./node-platform.js" type DiscoveryImportInfo = { readonly postgresModules: Set @@ -543,144 +544,176 @@ const discoverInFile = ( return declarations } -const createTemporaryExportModule = async ( +const createTemporaryExportModuleEffect = ( filePath: string, names: readonly string[] -): Promise => { - const extension = extname(filePath) || ".ts" - const tempPath = join(dirname(filePath), `.__effect_qb_discovery_${basename(filePath, extension)}_${randomUUID()}${extension}`) - const contents = await readFile(filePath, "utf8") - await writeFile( - tempPath, - `${contents}\nconst __effect_qb_discovery_exports = { ${names.join(", ")} }\nexport default __effect_qb_discovery_exports\n` - ) - return tempPath -} +): Effect.Effect => + Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path + const extension = paths.extname(filePath) || ".ts" + const uuid = yield* crypto.randomUUIDv4 + const tempPath = paths.join( + paths.dirname(filePath), + `.__effect_qb_discovery_${paths.basename(filePath, extension)}_${uuid}${extension}` + ) + const contents = yield* fs.readFileString(filePath) + yield* fs.writeFileString( + tempPath, + `${contents}\nconst __effect_qb_discovery_exports = { ${names.join(", ")} }\nexport default __effect_qb_discovery_exports\n` + ) + return tempPath + }) -const importDiscoveredValues = async ( +const importDiscoveredValuesEffect = ( declarations: readonly SourceDeclaration[] -): Promise> => { - const byFile = new Map() - for (const declaration of declarations) { - const names = byFile.get(declaration.filePath) ?? [] - names.push(declaration.identifier) - byFile.set(declaration.filePath, names) - } - const values: unknown[] = [] - for (const [filePath, names] of byFile) { - const tempPath = await createTemporaryExportModule(filePath, [...new Set(names)]) - try { - const imported = await import(pathToFileURL(tempPath).href) +): Effect.Effect, unknown, Crypto.Crypto | FileSystem.FileSystem | Path.Path> => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path + const byFile = new Map() + for (const declaration of declarations) { + const names = byFile.get(declaration.filePath) ?? [] + names.push(declaration.identifier) + byFile.set(declaration.filePath, names) + } + const values: unknown[] = [] + for (const [filePath, names] of byFile) { + const tempPath = yield* createTemporaryExportModuleEffect(filePath, [...new Set(names)]) + const imported = yield* Effect.flatMap(paths.toFileUrl(tempPath), (url) => + Effect.tryPromise({ + try: () => import(url.href), + catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)) + })).pipe( + Effect.ensuring(Effect.ignore(fs.remove(tempPath, { force: true }))) + ) const exportedValues = imported.default as Record | undefined for (const name of names) { values.push(exportedValues?.[name]) } - } finally { - await rm(tempPath, { force: true }).catch(() => undefined) } - } - return values -} + return values + }) -const scanPattern = async ( +const scanPatternEffect = ( cwd: string, pattern: string -): Promise> => { - const matches: string[] = [] - for (const match of await glob(pattern, { - cwd, - absolute: true, - dot: true, - followSymbolicLinks: true - })) { - if (DEFAULT_SOURCE_EXTENSIONS.has(extname(match))) { - matches.push(resolve(match)) +): Effect.Effect, unknown, Path.Path> => + Effect.gen(function*() { + const paths = yield* Path.Path + const matches: string[] = [] + for (const match of yield* Effect.tryPromise({ + try: () => glob(pattern, { + cwd, + absolute: true, + dot: true, + followSymbolicLinks: true + }), + catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)) + })) { + if (DEFAULT_SOURCE_EXTENSIONS.has(paths.extname(match))) { + matches.push(paths.resolve(match)) + } } - } - return matches -} + return matches + }) -export const discoverSourceSchema = async ( +export const discoverSourceSchemaEffect = ( cwd: string, source: { readonly include: readonly string[] readonly exclude?: readonly string[] } -): Promise => { - const included = new Set() - for (const pattern of source.include) { - for (const match of await scanPattern(cwd, pattern)) { - included.add(match) +): Effect.Effect => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path + const included = new Set() + for (const pattern of source.include) { + for (const match of yield* scanPatternEffect(cwd, pattern)) { + included.add(match) + } } - } - const excluded = new Set() - for (const pattern of source.exclude ?? []) { - for (const match of await scanPattern(cwd, pattern)) { - excluded.add(match) + const excluded = new Set() + for (const pattern of source.exclude ?? []) { + for (const match of yield* scanPatternEffect(cwd, pattern)) { + excluded.add(match) + } } - } - const declarations: SourceDeclaration[] = [] - for (const filePath of [...included].filter((file) => !excluded.has(file)).sort()) { - const contents = await readFile(filePath, "utf8") - declarations.push(...discoverInFile(filePath, contents)) - } - const duplicateKeys = new Map() - for (const declaration of declarations) { - const key = `${declaration.filePath}:${declaration.identifier}` - if (duplicateKeys.has(key)) { - throw new Error(`Duplicate discovered declaration '${declaration.identifier}' in '${relative(cwd, declaration.filePath)}'`) + const declarations: SourceDeclaration[] = [] + for (const filePath of [...included].filter((file) => !excluded.has(file)).sort()) { + const contents = yield* fs.readFileString(filePath) + declarations.push(...discoverInFile(filePath, contents)) } - duplicateKeys.set(key, key) - } - const values = await importDiscoveredValues(declarations) - const bindings: SourceBinding[] = [] - const seenKeys = new Map() - for (const [index, value] of values.entries()) { - const declaration = declarations[index] - if (declaration === undefined) { - continue + const duplicateKeys = new Map() + for (const declaration of declarations) { + const key = `${declaration.filePath}:${declaration.identifier}` + if (duplicateKeys.has(key)) { + return yield* Effect.fail(new Error( + `Duplicate discovered declaration '${declaration.identifier}' in '${paths.relative(cwd, declaration.filePath)}'` + )) + } + duplicateKeys.set(key, key) } - if (isTableDefinition(value)) { - const state = (value as any)[Std.Table.TypeId] as { - readonly schemaName?: string - readonly baseName: string + const values = yield* importDiscoveredValuesEffect(declarations) + const bindings: SourceBinding[] = [] + const seenKeys = new Map() + for (const [index, value] of values.entries()) { + const declaration = declarations[index] + if (declaration === undefined) { + continue } - const key = sourceIdentityKey(state.schemaName, state.baseName) - const existing = seenKeys.get(key) - if (existing) { - throw new Error( - `Duplicate discovered table identity '${tableKey(state.schemaName, state.baseName)}' in '${relative(cwd, existing.filePath)}' and '${relative(cwd, declaration.filePath)}'` - ) + if (isTableDefinition(value)) { + const state = (value as any)[Std.Table.TypeId] as { + readonly schemaName?: string + readonly baseName: string + } + const key = sourceIdentityKey(state.schemaName, state.baseName) + const existing = seenKeys.get(key) + if (existing) { + return yield* Effect.fail(new Error( + `Duplicate discovered table identity '${tableKey(state.schemaName, state.baseName)}' in '${paths.relative(cwd, existing.filePath)}' and '${paths.relative(cwd, declaration.filePath)}'` + )) + } + seenKeys.set(key, declaration) + bindings.push({ + declaration, + value, + key, + kind: "table" + }) + continue } - seenKeys.set(key, declaration) - bindings.push({ - declaration, - value, - key, - kind: "table" - }) - continue - } - if (isEnumDefinition(value)) { - const key = sourceIdentityKey(value.schemaName, value.name) - const existing = seenKeys.get(key) - if (existing) { - throw new Error( - `Duplicate discovered enum identity '${enumKey(value.schemaName, value.name)}' in '${relative(cwd, existing.filePath)}' and '${relative(cwd, declaration.filePath)}'` - ) + if (isEnumDefinition(value)) { + const key = sourceIdentityKey(value.schemaName, value.name) + const existing = seenKeys.get(key) + if (existing) { + return yield* Effect.fail(new Error( + `Duplicate discovered enum identity '${enumKey(value.schemaName, value.name)}' in '${paths.relative(cwd, existing.filePath)}' and '${paths.relative(cwd, declaration.filePath)}'` + )) + } + seenKeys.set(key, declaration) + bindings.push({ + declaration, + value, + key, + kind: "enum" + }) } - seenKeys.set(key, declaration) - bindings.push({ - declaration, - value, - key, - kind: "enum" - }) } + return { + declarations, + bindings, + model: fromDiscoveredValues(values) + } + }) + +export const discoverSourceSchema = ( + cwd: string, + source: { + readonly include: readonly string[] + readonly exclude?: readonly string[] } - return { - declarations, - bindings, - model: fromDiscoveredValues(values) - } -} +): Promise => + runNodePlatform(discoverSourceSchemaEffect(cwd, source)) diff --git a/packages/database/src/postgres/migrate.ts b/packages/database/src/postgres/migrate.ts index da18e3f5..51fd3441 100644 --- a/packages/database/src/postgres/migrate.ts +++ b/packages/database/src/postgres/migrate.ts @@ -1,15 +1,16 @@ -import { createHash } from "node:crypto" -import { mkdir, readFile, readdir, writeFile } from "node:fs/promises" -import { basename, join, resolve } from "node:path" - +import * as Crypto from "effect/Crypto" +import * as Encoding from "effect/Encoding" import * as SqlClient from "effect/unstable/sql/SqlClient" import * as SqlSchema from "effect/unstable/sql/SqlSchema" import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" import * as Schema from "effect/Schema" -import { runPostgresUrl } from "../internal/postgres-runtime.js" +import { runNodePath, runNodePlatform, type PlatformServices } from "../internal/node-platform.js" +import type { LoadedPostgresConfig } from "../internal/postgres-config.js" +import { providePostgresUrl } from "../internal/postgres-runtime.js" import type { SchemaChange } from "../internal/postgres-schema-diff.js" -import type { loadPostgresConfig } from "../internal/postgres-config.js" const MIGRATION_UP_MARKER = "-- effect-db:up" const MIGRATION_DOWN_MARKER = "-- effect-db:down" @@ -98,8 +99,14 @@ const migrationTableSql = (tableName: string): string => const normalizeMigrationContents = (contents: string): string => contents.replaceAll("\r\n", "\n") -const migrationChecksumOf = (contents: string): string => - `${MIGRATION_CHECKSUM_PREFIX}:${createHash("sha256").update(normalizeMigrationContents(contents)).digest("hex")}` +const migrationChecksumOfEffect = ( + contents: string +): Effect.Effect => + Effect.flatMap(Crypto.Crypto, (crypto) => + Effect.map( + crypto.digest("SHA-256", new TextEncoder().encode(normalizeMigrationContents(contents))), + (digest) => `${MIGRATION_CHECKSUM_PREFIX}:${Encoding.encodeHex(digest)}` + )) export interface MigrationFile { readonly name: string @@ -126,8 +133,6 @@ const AppliedMigrationRowSchema = Schema.Struct({ checksum: Schema.NullOr(Schema.String) }) -type LoadedConfig = Awaited> - const renderStatements = (statements: readonly string[]): string => statements .map((statement) => statement.endsWith(";") ? statement : `${statement};`) @@ -198,28 +203,34 @@ export const renderMigrationFile = ( return sections.join("\n") } -export const writeMigrationFile = async ( +export const writeMigrationFileEffect = ( migrationsDir: string, name: string, changes: readonly SchemaChange[] -): Promise => { - const directory = resolve(migrationsDir) - await mkdir(directory, { recursive: true }) - const files = (await readdir(directory)).filter((file) => file.endsWith(".sql")) - const nextNumber = files - .map((file) => /^(\d+)_/.exec(file)?.[1]) - .filter((value): value is string => value !== undefined) - .map((value) => Number(value)) - .reduce((max, current) => Math.max(max, current), 0) + 1 - const fileName = `${String(nextNumber).padStart(4, "0")}_${sanitizeName(name)}.sql` - const filePath = join(directory, fileName) - await writeFile(filePath, `${renderMigrationFile(changes)}\n`) - return filePath -} - -const ensureDirectory = async (dir: string): Promise => { - await mkdir(resolve(dir), { recursive: true }) -} +): Effect.Effect => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path + const directory = paths.resolve(migrationsDir) + yield* fs.makeDirectory(directory, { recursive: true }) + const files = (yield* fs.readDirectory(directory)).filter((file) => file.endsWith(".sql")) + const nextNumber = files + .map((file) => /^(\d+)_/.exec(file)?.[1]) + .filter((value): value is string => value !== undefined) + .map((value) => Number(value)) + .reduce((max, current) => Math.max(max, current), 0) + 1 + const fileName = `${String(nextNumber).padStart(4, "0")}_${sanitizeName(name)}.sql` + const filePath = paths.join(directory, fileName) + yield* fs.writeFileString(filePath, `${renderMigrationFile(changes)}\n`) + return filePath + }) + +export const writeMigrationFile = ( + migrationsDir: string, + name: string, + changes: readonly SchemaChange[] +): Promise => + runNodePlatform(writeMigrationFileEffect(migrationsDir, name, changes)) export const applyStatements = ( statements: readonly string[] @@ -233,59 +244,53 @@ export const applyStatements = ( ) )) -export const readPendingMigrationFiles = async ( +export const readPendingMigrationFilesEffect = ( migrationsDir: string, appliedNames: ReadonlySet -): Promise> => { - await ensureDirectory(migrationsDir) - const directory = resolve(migrationsDir) - const files = (await readdir(directory)) - .filter((file) => file.endsWith(".sql")) - .map((file) => join(directory, file)) - .sort() - const pending: MigrationFile[] = [] - for (const path of files) { - const name = basename(path) - if (appliedNames.has(name)) { - continue +): Effect.Effect, unknown, Crypto.Crypto | FileSystem.FileSystem | Path.Path> => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path + const directory = paths.resolve(migrationsDir) + yield* fs.makeDirectory(directory, { recursive: true }) + const files = (yield* fs.readDirectory(directory)) + .filter((file) => file.endsWith(".sql")) + .map((file) => paths.join(directory, file)) + .sort() + const pending: MigrationFile[] = [] + for (const filePath of files) { + const name = paths.basename(filePath) + if (appliedNames.has(name)) { + continue + } + const contents = yield* fs.readFileString(filePath) + const parsed = parseMigrationSections(contents) + pending.push({ + name, + path: filePath, + sql: parsed.sql, + downSql: parsed.downSql, + checksum: yield* migrationChecksumOfEffect(contents) + }) } - const contents = await readFile(path, "utf8") - const parsed = parseMigrationSections(contents) - pending.push({ - name, - path, - sql: parsed.sql, - downSql: parsed.downSql, - checksum: migrationChecksumOf(contents) - }) - } - return pending -} + return pending + }) -export const readMigrationFiles = async ( +export const readPendingMigrationFiles = ( + migrationsDir: string, + appliedNames: ReadonlySet +): Promise> => + runNodePlatform(readPendingMigrationFilesEffect(migrationsDir, appliedNames)) + +export const readMigrationFilesEffect = ( migrationsDir: string -): Promise> => { - await ensureDirectory(migrationsDir) - const directory = resolve(migrationsDir) - const files = (await readdir(directory)) - .filter((file) => file.endsWith(".sql")) - .map((file) => join(directory, file)) - .sort() - const parsed: MigrationFile[] = [] - for (const path of files) { - const name = basename(path) - const contents = await readFile(path, "utf8") - const sections = parseMigrationSections(contents) - parsed.push({ - name, - path, - sql: sections.sql, - downSql: sections.downSql, - checksum: migrationChecksumOf(contents) - }) - } - return parsed -} +): Effect.Effect, unknown, Crypto.Crypto | FileSystem.FileSystem | Path.Path> => + readPendingMigrationFilesEffect(migrationsDir, new Set()) + +export const readMigrationFiles = ( + migrationsDir: string +): Promise> => + runNodePlatform(readMigrationFilesEffect(migrationsDir)) export const ensureMigrationTable = ( tableName: string @@ -297,10 +302,10 @@ export const ensureMigrationTable = ( yield* Effect.asVoid(sql.unsafe(`alter table ${qualifyIdentifier(tableName)} alter column checksum drop not null`)) })) -export const withMigrationLock = ( +export const withMigrationLock = ( tableName: string, - effect: Effect.Effect -): Effect.Effect => + effect: Effect.Effect +): Effect.Effect => Effect.flatMap(SqlClient.SqlClient, (sql) => sql.withTransaction(Effect.gen(function*() { yield* Effect.asVoid(sql.unsafe( @@ -471,27 +476,51 @@ export const deleteAppliedMigrationNames = ( export const migrationFileLabel = (path: string): string => path.slice(path.lastIndexOf("/") + 1) +export const migrationDirFromConfigEffect = ( + cwd: string, + dir: string +): Effect.Effect => + Effect.map(Path.Path, (paths) => paths.resolve(cwd, dir)) + export const migrationDirFromConfig = (cwd: string, dir: string): string => - resolve(cwd, dir) + runNodePath(migrationDirFromConfigEffect(cwd, dir)) -export const loadPostgresMigrationState = async ( - loaded: LoadedConfig, +export const loadPostgresMigrationStateEffect = ( + loaded: LoadedPostgresConfig, databaseUrl: string -) => { - const files = await readMigrationFiles(migrationDirFromConfig(loaded.cwd, loaded.config.migrations.dir)) - const appliedRows = await runPostgresUrl( - databaseUrl, - withMigrationLock( - loaded.config.migrations.table, - loadAppliedMigrationRows(loaded.config.migrations.table, files) +): Effect.Effect<{ + readonly files: ReadonlyArray + readonly appliedRows: ReadonlyArray + readonly appliedNames: ReadonlySet + readonly pending: ReadonlyArray +}, unknown, PlatformServices> => + Effect.gen(function*() { + const migrationsDir = yield* migrationDirFromConfigEffect(loaded.cwd, loaded.config.migrations.dir) + const files = yield* readMigrationFilesEffect(migrationsDir) + const appliedRows = yield* providePostgresUrl( + databaseUrl, + withMigrationLock( + loaded.config.migrations.table, + loadAppliedMigrationRows(loaded.config.migrations.table, files) + ) ) - ) - const appliedNames = new Set(appliedRows.map((row) => row.name)) - const pending = files.filter((file) => !appliedNames.has(file.name)) - return { - files, - appliedRows, - appliedNames, - pending - } -} + const appliedNames = new Set(appliedRows.map((row) => row.name)) + const pending = files.filter((file) => !appliedNames.has(file.name)) + return { + files, + appliedRows, + appliedNames, + pending + } + }) + +export const loadPostgresMigrationState = ( + loaded: LoadedPostgresConfig, + databaseUrl: string +): Promise<{ + readonly files: ReadonlyArray + readonly appliedRows: ReadonlyArray + readonly appliedNames: ReadonlySet + readonly pending: ReadonlyArray +}> => + runNodePlatform(loadPostgresMigrationStateEffect(loaded, databaseUrl)) diff --git a/packages/database/src/postgres/pull.ts b/packages/database/src/postgres/pull.ts index 71f5b1da..cee41c59 100644 --- a/packages/database/src/postgres/pull.ts +++ b/packages/database/src/postgres/pull.ts @@ -1,12 +1,13 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises" -import { dirname, extname, relative, resolve } from "node:path" - +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" import { Datatypes } from "effect-qb/postgres" import type { ColumnModel, EnumModel, SchemaModel, TableModel, DdlExpressionLike, IndexKeySpec, TableOptionSpec } from "effect-qb/postgres/metadata" import { defaultConstraintName } from "../internal/postgres-schema-sql.js" import { enumKey, tableKey, renderDdlExpressionSql, normalizeDdlExpressionSql, toEnumModel, toTableModel } from "effect-qb/postgres/metadata" import type { DiscoveredSourceSchema, SourceBinding, SourceDeclaration } from "../internal/postgres-source-discovery.js" import { canonicalizePostgresTypeName, inferPostgresTypeKind } from "../internal/postgres-type-utils.js" +import { runNodePath, runNodePlatform } from "../internal/node-platform.js" import { parse, type Expr as PgSqlExpr } from "pgsql-ast-parser" const TABLE_ALIAS = "Table" @@ -2761,7 +2762,8 @@ const renderCanonicalNewModule = ( const inferSourceRoot = ( cwd: string, - includes: readonly string[] + includes: readonly string[], + paths: Path.Path ): string => { const first = includes[0] ?? "src/**/*.ts" const wildcard = first.search(/[*?{\[]/) @@ -2770,12 +2772,12 @@ const inferSourceRoot = ( return cwd } if (prefix.endsWith("/")) { - return resolve(cwd, prefix) + return paths.resolve(cwd, prefix) } - if (extname(prefix).length > 0) { - return resolve(cwd, dirname(prefix)) + if (paths.extname(prefix).length > 0) { + return paths.resolve(cwd, paths.dirname(prefix)) } - return resolve(cwd, prefix) + return paths.resolve(cwd, prefix) } const renderDeclaredModule = ( @@ -2813,7 +2815,7 @@ const renderDeclaredModule = ( return ensureImports(body) } -export const planPostgresPull = async ( +export const planPostgresPullEffect = ( cwd: string, source: { readonly include: readonly string[] @@ -2821,7 +2823,10 @@ export const planPostgresPull = async ( }, discovered: DiscoveredSourceSchema, database: SchemaModel -): Promise => { +): Effect.Effect => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path const bindingByKey = new Map(discovered.bindings.map((binding) => [binding.key, binding])) const databaseTablesByKey = new Map(database.tables.map((table) => [schemaObjectKey(table.schemaName, table.name), table])) const databaseEnumsByKey = new Map(database.enums.map((enumType) => [schemaObjectKey(enumType.schemaName, enumType.name), enumType])) @@ -2829,7 +2834,7 @@ export const planPostgresPull = async ( bindingByKey, enumKeys: new Set(databaseEnumsByKey.keys()) } - const sourceRoot = inferSourceRoot(cwd, source.include) + const sourceRoot = inferSourceRoot(cwd, source.include, paths) const schemaFilePathByName = new Map() for (const binding of discovered.bindings) { @@ -2853,24 +2858,24 @@ export const planPostgresPull = async ( }> }>() - const ensureFilePlan = async (filePath: string): Promise<{ + const ensureFilePlan = (filePath: string): Effect.Effect<{ readonly original: string readonly replacements: SourceBinding[] readonly additions: Array<{ readonly binding: SourceBinding readonly model: TableModel | EnumModel }> - }> => { + }, unknown> => Effect.gen(function*() { const existing = filePlans.get(filePath) if (existing !== undefined) { return existing } - const original = await readFile(filePath, "utf8").catch((cause: unknown) => { - if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === "ENOENT") { - return "" - } - throw cause - }) + const original = yield* fs.readFileString(filePath).pipe( + Effect.catchIf( + (cause) => cause.reason._tag === "NotFound", + () => Effect.succeed("") + ) + ) const created = { original, replacements: [] as SourceBinding[], @@ -2881,24 +2886,24 @@ export const planPostgresPull = async ( } filePlans.set(filePath, created) return created - } + }) - const scheduleReplacement = async ( + const scheduleReplacement = ( binding: SourceBinding, model: TableModel | EnumModel - ): Promise => { - const plan = await ensureFilePlan(binding.declaration.filePath) + ): Effect.Effect => Effect.gen(function*() { + const plan = yield* ensureFilePlan(binding.declaration.filePath) plan.replacements.push(binding) bindingByKey.set(binding.key, binding) void model - } + }) for (const [key, table] of databaseTablesByKey) { const binding = bindingByKey.get(key) if (binding !== undefined && binding.kind === "table") { matchedSourceBindings.add(binding) matchedDbTableKeys.add(key) - await scheduleReplacement(binding, table) + yield* scheduleReplacement(binding, table) } } @@ -2907,7 +2912,7 @@ export const planPostgresPull = async ( if (binding !== undefined && binding.kind === "enum") { matchedSourceBindings.add(binding) matchedDbEnumKeys.add(key) - await scheduleReplacement(binding, enumType) + yield* scheduleReplacement(binding, enumType) } } @@ -2920,7 +2925,7 @@ export const planPostgresPull = async ( for (const { source: binding, db: table } of renameTablePairs) { matchedSourceBindings.add(binding) matchedDbTableKeys.add(schemaObjectKey(table.schemaName, table.name)) - await scheduleReplacement(binding, table) + yield* scheduleReplacement(binding, table) } const renameEnumPairs = pairUniqueBySignature( @@ -2932,7 +2937,7 @@ export const planPostgresPull = async ( for (const { source: binding, db: enumType } of renameEnumPairs) { matchedSourceBindings.add(binding) matchedDbEnumKeys.add(schemaObjectKey(enumType.schemaName, enumType.name)) - await scheduleReplacement(binding, enumType) + yield* scheduleReplacement(binding, enumType) } const newBindingsByFile = new Map => { - for (const update of plan.updates) { - await mkdir(dirname(update.filePath), { recursive: true }) - await writeFile(update.filePath, update.after) - } -} +export const planPostgresPull = ( + cwd: string, + source: { + readonly include: readonly string[] + readonly exclude?: readonly string[] + }, + discovered: DiscoveredSourceSchema, + database: SchemaModel +): Promise => + runNodePlatform(planPostgresPullEffect(cwd, source, discovered, database)) + +export const applyPullPlanEffect = ( + plan: PullPlan +): Effect.Effect => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const paths = yield* Path.Path + for (const update of plan.updates) { + yield* fs.makeDirectory(paths.dirname(update.filePath), { recursive: true }) + yield* fs.writeFileString(update.filePath, update.after) + } + }) + +export const applyPullPlan = (plan: PullPlan): Promise => + runNodePlatform(applyPullPlanEffect(plan)) + +export const summarizePullPlanEffect = ( + cwd: string, + plan: PullPlan +): Effect.Effect => + Effect.map(Path.Path, (paths) => + plan.updates.map((update) => + `${update.before.length === 0 ? "create" : "update"} ${paths.relative(cwd, update.filePath)}` + )) export const summarizePullPlan = (cwd: string, plan: PullPlan): readonly string[] => - plan.updates.map((update) => `${update.before.length === 0 ? "create" : "update"} ${relative(cwd, update.filePath)}`) + runNodePath(summarizePullPlanEffect(cwd, plan)) diff --git a/packages/database/src/postgres/push.ts b/packages/database/src/postgres/push.ts index 7d3146b8..93a82deb 100644 --- a/packages/database/src/postgres/push.ts +++ b/packages/database/src/postgres/push.ts @@ -1,14 +1,13 @@ import { tableKey, type SchemaModel } from "effect-qb/postgres/metadata" +import * as Effect from "effect/Effect" import { introspectPostgresSchema } from "../internal/postgres-introspector.js" -import { runPostgresUrl } from "../internal/postgres-runtime.js" +import { runNodePlatform, type PlatformServices } from "../internal/node-platform.js" +import type { EffectDbConfig } from "../internal/postgres-config.js" +import { providePostgresUrl } from "../internal/postgres-runtime.js" import { planPostgresSchemaDiff, type SchemaChange, type SchemaPlan } from "../internal/postgres-schema-diff.js" -import { discoverSourceSchema } from "../internal/postgres-source-discovery.js" +import { discoverSourceSchemaEffect, type DiscoveredSourceSchema } from "../internal/postgres-source-discovery.js" import { filterDiscoveredSourceSchema } from "../internal/postgres-source-filter.js" -import type { loadPostgresConfig } from "../internal/postgres-config.js" - -type LoadedConfig = Awaited> -type EffectDbConfig = LoadedConfig["config"] export type { SchemaChange, SchemaPlan } @@ -114,24 +113,35 @@ export const withoutManagedMigrationTable = ( } } -export const loadPostgresSchemaPlan = async ( +export const loadPostgresSchemaPlanEffect = ( + cwd: string, + config: EffectDbConfig, + databaseUrl: string +): Effect.Effect<{ + readonly plan: SchemaPlan + readonly discovered: DiscoveredSourceSchema +}, unknown, PlatformServices> => + Effect.gen(function*() { + const discovered = filterDiscoveredSourceSchema( + yield* discoverSourceSchemaEffect(cwd, config.source), + config.filter + ) + const database = withoutManagedMigrationTable( + yield* providePostgresUrl(databaseUrl, introspectPostgresSchema(config.filter)), + config.migrations.table + ) + return { + plan: planPostgresSchemaDiff(discovered.model, database), + discovered + } + }) + +export const loadPostgresSchemaPlan = ( cwd: string, config: EffectDbConfig, databaseUrl: string ): Promise<{ readonly plan: SchemaPlan - readonly discovered: Awaited> -}> => { - const discovered = filterDiscoveredSourceSchema( - await discoverSourceSchema(cwd, config.source), - config.filter - ) - const database = withoutManagedMigrationTable( - await runPostgresUrl(databaseUrl, introspectPostgresSchema(config.filter)), - config.migrations.table - ) - return { - plan: planPostgresSchemaDiff(discovered.model, database), - discovered - } -} + readonly discovered: DiscoveredSourceSchema +}> => + runNodePlatform(loadPostgresSchemaPlanEffect(cwd, config, databaseUrl)) From 0a3650d9f7679b15d533796f6ac47c0919389fd9 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:26:48 -0700 Subject: [PATCH 15/27] test(database): cover injected platform services --- .../behavior/postgres-config.behavior.test.ts | 28 ++++++++++++++++++ .../postgres-migrate.behavior.test.ts | 29 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/test/internal/behavior/postgres-config.behavior.test.ts b/test/internal/behavior/postgres-config.behavior.test.ts index b3f3c2a0..72e26996 100644 --- a/test/internal/behavior/postgres-config.behavior.test.ts +++ b/test/internal/behavior/postgres-config.behavior.test.ts @@ -2,12 +2,40 @@ import { mkdtemp, rm } from "node:fs/promises" import { join } from "node:path" import { describe, expect, test } from "bun:test" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" import { loadPostgresConfig, resolveDatabaseUrl } from "effect-db" +import { loadPostgresConfigEffect } from "../../../packages/database/src/internal/postgres-config.js" const repoRoot = process.cwd() describe("postgres config", () => { + test("loads defaults through provided platform services", async () => { + const checkedPaths: string[] = [] + const loaded = await Effect.runPromise( + loadPostgresConfigEffect("/workspace").pipe( + Effect.provide(FileSystem.layerNoop({ + exists: (path) => { + checkedPaths.push(path) + return Effect.succeed(false) + } + })), + Effect.provide(Path.layer) + ) + ) + + expect(loaded.cwd).toBe("/workspace") + expect(loaded.path).toBeUndefined() + expect(checkedPaths).toEqual([ + "/workspace/effectdb.config.ts", + "/workspace/effectdb.config.mts", + "/workspace/effectdb.config.js", + "/workspace/effectdb.config.mjs" + ]) + }) + test("rejects invalid dialects", async () => { const tempDir = await mkdtemp(join(repoRoot, "test/.tmp-postgres-config-")) try { diff --git a/test/internal/behavior/postgres-migrate.behavior.test.ts b/test/internal/behavior/postgres-migrate.behavior.test.ts index 2393aed9..d408922b 100644 --- a/test/internal/behavior/postgres-migrate.behavior.test.ts +++ b/test/internal/behavior/postgres-migrate.behavior.test.ts @@ -3,15 +3,42 @@ import { join } from "node:path" import * as SqlClient from "effect/unstable/sql/SqlClient" import { describe, expect, test } from "bun:test" +import * as Crypto from "effect/Crypto" import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" -import { readMigrationFiles, ensureMigrationTable, readAppliedMigrationRows } from "../../../packages/database/src/postgres/migrate.js" +import { readMigrationFiles, readMigrationFilesEffect, ensureMigrationTable, readAppliedMigrationRows } from "../../../packages/database/src/postgres/migrate.js" import { withoutManagedMigrationTable } from "../../../packages/database/src/postgres/push.js" import type { SchemaModel } from "effect-qb/postgres/metadata" const repoRoot = process.cwd() describe("postgres migrations", () => { + test("reads migrations through provided platform services", async () => { + const files = await Effect.runPromise( + readMigrationFilesEffect("/migrations").pipe( + Effect.provide(FileSystem.layerNoop({ + makeDirectory: () => Effect.void, + readDirectory: () => Effect.succeed(["0001_init.sql"]), + readFileString: () => Effect.succeed("create table users (id integer);\n") + })), + Effect.provide(Path.layer), + Effect.provideService(Crypto.Crypto, Crypto.make({ + randomBytes: (size) => new Uint8Array(size), + digest: () => Effect.succeed(Uint8Array.from([1, 2, 255])) + })) + ) + ) + + expect(files).toEqual([{ + name: "0001_init.sql", + path: "/migrations/0001_init.sql", + sql: "create table users (id integer);", + checksum: "sha256:0102ff" + }]) + }) + test("parses up and down sections and normalizes checksum line endings", async () => { const tempDir = await mkdtemp(join(repoRoot, "test/.tmp-postgres-migrate-")) try { From 9380db9579736a228f22d8880cdde731ec2bb2d1 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:26:53 -0700 Subject: [PATCH 16/27] docs(database): explain portable runtime services --- packages/database/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/database/README.md b/packages/database/README.md index 27b4cdf4..7811aef5 100644 --- a/packages/database/README.md +++ b/packages/database/README.md @@ -44,6 +44,14 @@ effectdb migrate generate effectdb migrate up ``` +## Runtime services + +The CLI obtains file-system, path, crypto, and terminal capabilities from +`@effect/platform-node`. The core workflows also export `*Effect` variants that +accept the corresponding Effect Platform services, so another runtime can +provide its own implementations. Existing Promise APIs remain Node-backed for +compatibility. + ## Operational Notes `effectdb` manages canonical table and enum declarations, not the entire Postgres catalog. From 09abe5f740887ae0a00b6e98223ae41cbb9d792c Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:27:23 -0700 Subject: [PATCH 17/27] chore(beads): close Effect Platform migration --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index cbebfe56..d2d1e699 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -26,7 +26,7 @@ {"id":"effect-qb-dn3.4","title":"P1 validate effect-db rows with SqlSchema","description":"Use v4 effect/unstable/sql/SqlSchema for high-risk effect-db result rows, starting with Postgres introspection and migration ledger reads.","acceptance_criteria":"Introspection and migration ledger row shapes are represented by internal schemas; selected unsafe typed row reads are replaced or wrapped with SqlSchema helpers; checksum/migration behavior is unchanged except for earlier shape failures; behavior/integration tests cover happy path and malformed-row failure where feasible.","notes":"Status: open. Validation plan: postgres-introspector behavior tests, postgres-migrate behavior tests, CLI integration tests if Postgres is available. Keep source-code discovery independent of SqlSchema.\nImplemented in second migration slice. Added v4 SqlSchema row decoders around Postgres catalog introspection reads and applied-migration ledger reads. Malformed catalog and ledger row tests now assert schema decode failure before metadata/migration state is built. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/postgres-introspector.behavior.test.ts test/internal/behavior/postgres-migrate.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:38Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:45:33Z","started_at":"2026-05-13T02:42:58Z","closed_at":"2026-05-13T02:45:33Z","close_reason":"effect-db catalog and migration ledger row reads now decode through v4 SqlSchema with malformed-row tests and repo gates passing.","labels":["effect-db","effect-v4","migration","sqlschema"],"dependencies":[{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:38Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3.3","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:12:55Z","started_at":"2026-07-22T20:12:55Z","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","notes":"Implemented Effect variants for config loading, source discovery, pull planning/application, schema planning, and migration filesystem workflows. Promise exports remain compatible through one NodeServices adapter; the CLI composes Effect workflows and provides NodeServices once at the root. Source runtime code has no direct node:fs, node:path, or node:crypto imports. tinyglobby remains for discovery because Effect FileSystem.glob does not expose the existing dot-file and symlink-following semantics. Added injected FileSystem, Path, and Crypto tests. Verified 536 behavior tests, 55 live integration tests, full tsgo types, build, and packed smoke on Node 22 and 24.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:27:07Z","started_at":"2026-07-22T20:12:55Z","closed_at":"2026-07-22T20:27:07Z","close_reason":"effect-db filesystem, path, and crypto workflows now use Effect Platform services with compatible Node Promise adapters and NodeServices CLI assembly; all acceptance gates pass.","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-02d","title":"Complete Node-native package verification","description":"Finish the Node-native boundaries for effect-db and effect-qb while retaining Bun for workspace package management and tooling.","acceptance_criteria":"Postgres CLI integration executes packages/database/dist/cli.js with Node; CLI host output uses Effect Platform services with Node implementations confined to runtime assembly; production package source typechecks with explicit Node types and without Bun ambient types; packed artifacts are smoked on Node 22 and Node 24 in CI; the root workspace is private and declares its Bun package manager/engine; focused and existing quality gates pass.","notes":"User clarified that portable host capabilities should use Effect Platform wherever practical; Node remains the executable adapter through NodeServices/NodeRuntime.\nImplemented: built CLI integration now runs dist/cli.js with Node; CLI output uses Effect Terminal; production sources typecheck with @types/node and no Bun globals; CI smokes packed packages on Node 22/24; root declares Bun package metadata. Production-path verification also fixed stale extension-only pull generation, Postgres bigint migration ID decoding, and the silent no-op Bun test config argument. Verified 534 behavior tests, 55 live integration tests, full type tests, build, frozen install, and packed Node 22/24 smoke.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:43:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:08:18Z","started_at":"2026-07-22T19:43:55Z","closed_at":"2026-07-22T20:08:18Z","close_reason":"Node-native package verification and production CLI coverage are implemented and passing; broader Effect Platform I/O migration remains tracked by effect-qb-8l5.","labels":["ci","node","packaging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"closed","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:22:13Z","started_at":"2026-07-22T03:13:13Z","closed_at":"2026-07-22T03:22:13Z","close_reason":"effect-db now uses the Effect Node runtime and Node-compatible file APIs; packed Node 22 smoke and behavior/type tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-1ib","title":"Merge effect v4 branch into main","description":"Bring refactor/effect-v4 up to date with current main changes, ensure published package dependency metadata treats effect as a peer dependency, run quality gates, then merge and push main.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-06-29T02:39:26Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-06-29T03:00:19Z","started_at":"2026-06-29T02:39:47Z","closed_at":"2026-06-29T03:00:19Z","close_reason":"Merged refactor/effect-v4 into main; local type, build, test, and pack gates pass. Docker integration was attempted but local Docker hung before tests could complete.","dependency_count":0,"dependent_count":0,"comment_count":0} From 3079f1cd211daeeada26ecd8f365d4c96d236830 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:47:24 -0700 Subject: [PATCH 18/27] chore(beads): claim CLI logging migration --- .beads/issues.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index d2d1e699..e4dc331f 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -26,6 +26,7 @@ {"id":"effect-qb-dn3.4","title":"P1 validate effect-db rows with SqlSchema","description":"Use v4 effect/unstable/sql/SqlSchema for high-risk effect-db result rows, starting with Postgres introspection and migration ledger reads.","acceptance_criteria":"Introspection and migration ledger row shapes are represented by internal schemas; selected unsafe typed row reads are replaced or wrapped with SqlSchema helpers; checksum/migration behavior is unchanged except for earlier shape failures; behavior/integration tests cover happy path and malformed-row failure where feasible.","notes":"Status: open. Validation plan: postgres-introspector behavior tests, postgres-migrate behavior tests, CLI integration tests if Postgres is available. Keep source-code discovery independent of SqlSchema.\nImplemented in second migration slice. Added v4 SqlSchema row decoders around Postgres catalog introspection reads and applied-migration ledger reads. Malformed catalog and ledger row tests now assert schema decode failure before metadata/migration state is built. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/postgres-introspector.behavior.test.ts test/internal/behavior/postgres-migrate.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:38Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:45:33Z","started_at":"2026-05-13T02:42:58Z","closed_at":"2026-05-13T02:45:33Z","close_reason":"effect-db catalog and migration ledger row reads now decode through v4 SqlSchema with malformed-row tests and repo gates passing.","labels":["effect-db","effect-v4","migration","sqlschema"],"dependencies":[{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:38Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3.3","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"effect-qb-9bn","title":"Route effect-db diagnostics through Effect Logger","description":"Use Effect CLI's built-in log-level setting for effect-db operational logs while reserving Terminal output for stable command results.","acceptance_criteria":"CLI operational summaries use Effect log levels and stderr; stable plans/status remain Terminal stdout; --log-level controls emitted diagnostics without custom filtering; tests cover stdout/stderr and suppression; Node package smoke remains green.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T20:40:41Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:40:49Z","started_at":"2026-07-22T20:40:49Z","labels":["database","effect-cli","logging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","notes":"Implemented Effect variants for config loading, source discovery, pull planning/application, schema planning, and migration filesystem workflows. Promise exports remain compatible through one NodeServices adapter; the CLI composes Effect workflows and provides NodeServices once at the root. Source runtime code has no direct node:fs, node:path, or node:crypto imports. tinyglobby remains for discovery because Effect FileSystem.glob does not expose the existing dot-file and symlink-following semantics. Added injected FileSystem, Path, and Crypto tests. Verified 536 behavior tests, 55 live integration tests, full tsgo types, build, and packed smoke on Node 22 and 24.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:27:07Z","started_at":"2026-07-22T20:12:55Z","closed_at":"2026-07-22T20:27:07Z","close_reason":"effect-db filesystem, path, and crypto workflows now use Effect Platform services with compatible Node Promise adapters and NodeServices CLI assembly; all acceptance gates pass.","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-02d","title":"Complete Node-native package verification","description":"Finish the Node-native boundaries for effect-db and effect-qb while retaining Bun for workspace package management and tooling.","acceptance_criteria":"Postgres CLI integration executes packages/database/dist/cli.js with Node; CLI host output uses Effect Platform services with Node implementations confined to runtime assembly; production package source typechecks with explicit Node types and without Bun ambient types; packed artifacts are smoked on Node 22 and Node 24 in CI; the root workspace is private and declares its Bun package manager/engine; focused and existing quality gates pass.","notes":"User clarified that portable host capabilities should use Effect Platform wherever practical; Node remains the executable adapter through NodeServices/NodeRuntime.\nImplemented: built CLI integration now runs dist/cli.js with Node; CLI output uses Effect Terminal; production sources typecheck with @types/node and no Bun globals; CI smokes packed packages on Node 22/24; root declares Bun package metadata. Production-path verification also fixed stale extension-only pull generation, Postgres bigint migration ID decoding, and the silent no-op Bun test config argument. Verified 534 behavior tests, 55 live integration tests, full type tests, build, frozen install, and packed Node 22/24 smoke.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:43:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:08:18Z","started_at":"2026-07-22T19:43:55Z","closed_at":"2026-07-22T20:08:18Z","close_reason":"Node-native package verification and production CLI coverage are implemented and passing; broader Effect Platform I/O migration remains tracked by effect-qb-8l5.","labels":["ci","node","packaging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"closed","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:22:13Z","started_at":"2026-07-22T03:13:13Z","closed_at":"2026-07-22T03:22:13Z","close_reason":"effect-db now uses the Effect Node runtime and Node-compatible file APIs; packed Node 22 smoke and behavior/type tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} From 683ff4775b8eb59318cd531fc46c5aa437b69bce Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:47:30 -0700 Subject: [PATCH 19/27] feat(database): add level-aware CLI logging --- packages/database/src/cli.ts | 75 +++++++----- .../integration/postgres-cli.integration.ts | 109 ++++++++++-------- 2 files changed, 102 insertions(+), 82 deletions(-) diff --git a/packages/database/src/cli.ts b/packages/database/src/cli.ts index ba56e885..329d23f6 100644 --- a/packages/database/src/cli.ts +++ b/packages/database/src/cli.ts @@ -2,6 +2,7 @@ import { NodeRuntime, NodeServices } from "@effect/platform-node" import { Command, Flag } from "effect/unstable/cli" import * as Effect from "effect/Effect" +import * as Logger from "effect/Logger" import * as Option from "effect/Option" import * as Terminal from "effect/Terminal" @@ -32,14 +33,17 @@ import { introspectPostgresSchema } from "./internal/postgres-introspector.js" import { filterDiscoveredSourceSchema } from "./internal/postgres-source-filter.js" import { discoverSourceSchemaEffect } from "./internal/postgres-source-discovery.js" -const log = (line: string) => - Effect.flatMap(Terminal.Terminal, (terminal) => terminal.display(`${line}\n`)) - -const logLines = (lines: readonly string[]) => +const outputLines = (lines: readonly string[]) => lines.length === 0 ? Effect.void : Effect.flatMap(Terminal.Terminal, (terminal) => terminal.display(`${lines.join("\n")}\n`)) +const logInfoLines = (lines: readonly string[]) => + Effect.forEach(lines, (line) => Effect.logInfo(line), { discard: true }) + +const logWarningLines = (lines: readonly string[]) => + Effect.forEach(lines, (line) => Effect.logWarning(line), { discard: true }) + const configOption = Flag.string("config").pipe( Flag.optional, Flag.withAlias("c"), @@ -69,6 +73,14 @@ const stepsOption = Flag.integer("steps").pipe( Flag.withDescription("Number of applied migrations to roll back") ) +const loadConfig = (explicitConfigPath: Option.Option) => + loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(explicitConfigPath)).pipe( + Effect.tap((loaded) => Effect.logDebug("loaded database config", { + cwd: loaded.cwd, + path: loaded.path + })) + ) + const withLoadedConfig = ( explicitConfigPath: Option.Option, explicitUrl: Option.Option, @@ -80,7 +92,7 @@ const withLoadedConfig = ( }) => Effect.Effect ) => Effect.gen(function*() { - const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(explicitConfigPath)) + const loaded = yield* loadConfig(explicitConfigPath) return yield* f({ cwd: loaded.cwd, configPath: loaded.path, @@ -104,12 +116,12 @@ const push = Command.make( ) const selected = selectedPostgresSchemaChanges(plan, allowDestructive) const skipped = skippedPostgresSchemaChanges(plan, allowDestructive) - yield* logLines([ + yield* outputLines([ `discovered ${discovered.model.tables.length} table(s) and ${discovered.model.enums.length} enum(s)`, ...summarizeSelectedPostgresPlan("planned changes", plan.changes) ]) if (dryRun) { - return yield* logLines(skipped.length === 0 + return yield* outputLines(skipped.length === 0 ? [] : summarizeSelectedPostgresPlan("skipped changes", skipped)) } @@ -120,12 +132,12 @@ const push = Command.make( applyStatements(selected.map((change) => change.sql!).filter((sql): sql is string => sql !== undefined)) ) ) - yield* log(`applied ${selected.length} statement(s)`) + yield* Effect.logInfo(`applied ${selected.length} statement(s)`) } else { - yield* log("no executable statements selected") + yield* Effect.logInfo("no executable statements selected") } if (skipped.length > 0) { - yield* logLines(summarizeSelectedPostgresPlan("skipped changes", skipped)) + yield* logWarningLines(summarizeSelectedPostgresPlan("skipped changes", skipped)) } }) ) @@ -140,7 +152,7 @@ const pull = Command.make( ({ config, url, dryRun }) => Effect.gen(function*() { const { loaded, database, discovered, plan } = yield* Effect.gen(function*() { - const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) + const loaded = yield* loadConfig(config) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) const discovered = filterDiscoveredSourceSchema( yield* discoverSourceSchemaEffect(loaded.cwd, loaded.config.source), @@ -156,12 +168,12 @@ const pull = Command.make( void database void discovered if (plan.updates.length === 0) { - return yield* log("schema definitions are already up to date") + return yield* Effect.logInfo("schema definitions are already up to date") } - yield* logLines(yield* summarizePullPlanEffect(loaded.cwd, plan)) + yield* outputLines(yield* summarizePullPlanEffect(loaded.cwd, plan)) if (!dryRun) { yield* applyPullPlanEffect(plan) - yield* log(`updated ${plan.updates.length} file(s)`) + yield* Effect.logInfo(`updated ${plan.updates.length} file(s)`) } }) ) @@ -177,7 +189,7 @@ const migrateGenerate = Command.make( ({ config, url, allowDestructive, name }) => Effect.gen(function*() { const { loaded, plan } = yield* Effect.gen(function*() { - const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) + const loaded = yield* loadConfig(config) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) const { plan } = yield* loadPostgresSchemaPlanEffect(loaded.cwd, loaded.config, databaseUrl) return { loaded, plan } @@ -185,7 +197,7 @@ const migrateGenerate = Command.make( const selected = selectedPostgresSchemaChanges(plan, allowDestructive) const skipped = skippedPostgresSchemaChanges(plan, allowDestructive) if (selected.length === 0) { - yield* log("no executable migration changes selected") + yield* Effect.logInfo("no executable migration changes selected") } else { const migrationsDir = yield* migrationDirFromConfigEffect(loaded.cwd, loaded.config.migrations.dir) const filePath = yield* writeMigrationFileEffect( @@ -193,10 +205,10 @@ const migrateGenerate = Command.make( Option.getOrElse(name, () => allowDestructive ? "schema_destructive" : "schema_safe"), selected ) - yield* log(`wrote ${migrationFileLabel(filePath)}`) + yield* Effect.logInfo(`wrote ${migrationFileLabel(filePath)}`) } if (skipped.length > 0) { - yield* logLines(summarizeSelectedPostgresPlan("skipped changes", skipped)) + yield* logWarningLines(summarizeSelectedPostgresPlan("skipped changes", skipped)) } }) ) @@ -210,7 +222,7 @@ const migrateUp = Command.make( ({ config, url }) => Effect.gen(function*() { const { loaded, databaseUrl } = yield* Effect.gen(function*() { - const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) + const loaded = yield* loadConfig(config) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) return { loaded, databaseUrl } }) @@ -229,9 +241,9 @@ const migrateUp = Command.make( })) ) if (applied.length === 0) { - return yield* log("no pending migrations") + return yield* Effect.logInfo("no pending migrations") } - yield* logLines([ + yield* logInfoLines([ `applied ${applied.length} migration(s)`, ...applied.map((file) => ` - ${file.name}`) ]) @@ -247,7 +259,7 @@ const migrateStatus = Command.make( ({ config, url }) => Effect.gen(function*() { const { loaded, databaseUrl, appliedRows, pending } = yield* Effect.gen(function*() { - const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) + const loaded = yield* loadConfig(config) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) const state = yield* loadPostgresMigrationStateEffect(loaded, databaseUrl) return { @@ -259,7 +271,7 @@ const migrateStatus = Command.make( }) void loaded void databaseUrl - yield* logLines([ + yield* outputLines([ `applied migrations (${appliedRows.length}):`, ...appliedRows.map((row) => ` - ${row.name}`), `pending migrations (${pending.length}):`, @@ -279,7 +291,7 @@ const migrateDown = Command.make( ({ config, url, dryRun, steps }) => Effect.gen(function*() { const { loaded, databaseUrl, selected } = yield* Effect.gen(function*() { - const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) + const loaded = yield* loadConfig(config) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) const state = yield* loadPostgresMigrationStateEffect(loaded, databaseUrl) const stepCount = Math.max(1, Option.getOrElse(steps, () => 1)) @@ -302,9 +314,9 @@ const migrateDown = Command.make( } }) if (selected.length === 0) { - return yield* log("no applied migrations") + return yield* Effect.logInfo("no applied migrations") } - yield* logLines([ + yield* outputLines([ `rollback migrations (${selected.length}):`, ...selected.map((file) => ` - ${file.name}`) ]) @@ -316,7 +328,7 @@ const migrateDown = Command.make( rollbackMigrationFiles(loaded.config.migrations.table, selected) ) ) - yield* log(`rolled back ${selected.length} migration(s)`) + yield* Effect.logInfo(`rolled back ${selected.length} migration(s)`) } }) ) @@ -331,7 +343,7 @@ const migrateRepair = Command.make( ({ config, url, dryRun }) => Effect.gen(function*() { const { loaded, databaseUrl, orphanNames } = yield* Effect.gen(function*() { - const loaded = yield* loadPostgresConfigEffect(process.cwd(), Option.getOrUndefined(config)) + const loaded = yield* loadConfig(config) const databaseUrl = resolveDatabaseUrl(loaded.config, Option.getOrUndefined(url)) const state = yield* loadPostgresMigrationStateEffect(loaded, databaseUrl) const fileNames = new Set(state.files.map((file) => file.name)) @@ -345,9 +357,9 @@ const migrateRepair = Command.make( } }) if (orphanNames.length === 0) { - return yield* log("migration ledger is already aligned") + return yield* Effect.logInfo("migration ledger is already aligned") } - yield* logLines([ + yield* outputLines([ `repairing ${orphanNames.length} orphaned migration record(s):`, ...orphanNames.map((name) => ` - ${name}`) ]) @@ -359,7 +371,7 @@ const migrateRepair = Command.make( deleteAppliedMigrationNames(loaded.config.migrations.table, orphanNames) ) ) - yield* log(`repaired ${orphanNames.length} migration record(s)`) + yield* Effect.logInfo(`repaired ${orphanNames.length} migration record(s)`) } }) ) @@ -377,6 +389,7 @@ const cli = Command.run(root, { }) cli.pipe( + Effect.provideService(Logger.LogToStderr, true), Effect.provide(NodeServices.layer), NodeRuntime.runMain ) diff --git a/test/public/integration/postgres-cli.integration.ts b/test/public/integration/postgres-cli.integration.ts index 1c95488b..0f58da61 100644 --- a/test/public/integration/postgres-cli.integration.ts +++ b/test/public/integration/postgres-cli.integration.ts @@ -241,11 +241,11 @@ const listConstraints = (schemaName: string, tableName: string) => const assertIdempotentPullPush = async (config: string) => { const secondPullDryRun = await runCli("pull", "--config", config, "--dry-run") expect(secondPullDryRun.exitCode).toBe(0) - expect(secondPullDryRun.stdout).toContain("schema definitions are already up to date") + expect(secondPullDryRun.stderr).toContain("schema definitions are already up to date") const secondPull = await runCli("pull", "--config", config) expect(secondPull.exitCode).toBe(0) - expect(secondPull.stdout).toContain("schema definitions are already up to date") + expect(secondPull.stderr).toContain("schema definitions are already up to date") const pushDryRun = await runCli("push", "--config", config, "--dry-run") expect(pushDryRun.exitCode).toBe(0) @@ -266,7 +266,7 @@ test("postgres cli supports push pull and migrations against a live database", a const push = await runCli("push", "--config", config) expect(push.exitCode).toBe(0) - expect(push.stdout).toContain("applied 2 statement(s)") + expect(push.stderr).toContain("applied 2 statement(s)") const createdTables = await execPostgres( `select tablename from pg_tables where schemaname = $1 order by tablename`, @@ -285,16 +285,23 @@ test("postgres cli supports push pull and migrations against a live database", a const pull = await runCli("pull", "--config", config) expect(pull.exitCode).toBe(0) - expect(pull.stdout).toContain("updated 1 file(s)") + expect(pull.stderr).toContain("updated 1 file(s)") const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`name: Column.text().pipe(Column.nullable)`) expect(pulledSchema).toContain(`users_email_idx`) expect(pulledSchema).toContain(`export { users }`) - const secondPullDryRun = await runCli("pull", "--config", config, "--dry-run") + const secondPullDryRun = await runCli("--log-level", "debug", "pull", "--config", config, "--dry-run") expect(secondPullDryRun.exitCode).toBe(0) - expect(secondPullDryRun.stdout).toContain("schema definitions are already up to date") + expect(secondPullDryRun.stderr).toContain("schema definitions are already up to date") + expect(secondPullDryRun.stderr).toContain("loaded database config") + + const quietPullDryRun = await runCli("--log-level", "none", "pull", "--config", config, "--dry-run") + expect(quietPullDryRun.exitCode).toBe(0) + expect(`${quietPullDryRun.stdout}\n${quietPullDryRun.stderr}`).not.toContain( + "schema definitions are already up to date" + ) await writeFile( schemaFile(workspace), @@ -306,18 +313,18 @@ test("postgres cli supports push pull and migrations against a live database", a const migrateGenerate = await runCli("migrate", "generate", "--config", config, "--name", "add_nickname") expect(migrateGenerate.exitCode).toBe(0) - expect(migrateGenerate.stdout).toContain("wrote 0001_add_nickname.sql") + expect(migrateGenerate.stderr).toContain("wrote 0001_add_nickname.sql") const migrationSql = await readFile(join(workspace, "migrations", "0001_add_nickname.sql"), "utf8") expect(migrationSql).toContain(`alter table "${schemaName}"."users" add column "nickname" text;`) const migrateUp = await runCli("migrate", "up", "--config", config) expect(migrateUp.exitCode).toBe(0) - expect(migrateUp.stdout).toContain("applied 1 migration(s)") + expect(migrateUp.stderr).toContain("applied 1 migration(s)") const secondMigrateUp = await runCli("migrate", "up", "--config", config) expect(secondMigrateUp.exitCode).toBe(0) - expect(secondMigrateUp.stdout).toContain("no pending migrations") + expect(secondMigrateUp.stderr).toContain("no pending migrations") const userColumns = await listColumns(schemaName, "users") expect(userColumns).toEqual([ @@ -338,7 +345,7 @@ test("postgres cli supports push pull and migrations against a live database", a const noOpGenerate = await runCli("migrate", "generate", "--config", config) expect(noOpGenerate.exitCode).toBe(0) - expect(noOpGenerate.stdout).toContain("no executable migration changes selected") + expect(noOpGenerate.stderr).toContain("no executable migration changes selected") } finally { await dropSchema(schemaName).catch(() => undefined) await rm(workspace, { recursive: true, force: true }) @@ -363,8 +370,8 @@ test("postgres cli blocks destructive push changes unless explicitly allowed", a const safePush = await runCli("push", "--config", config) expect(safePush.exitCode).toBe(0) expect(safePush.stdout).toContain(`drop column ${schemaName}.users.email`) - expect(safePush.stdout).toContain("no executable statements selected") - expect(safePush.stdout).toContain("skipped changes:") + expect(safePush.stderr).toContain("no executable statements selected") + expect(safePush.stderr).toContain("skipped changes:") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -373,7 +380,7 @@ test("postgres cli blocks destructive push changes unless explicitly allowed", a const destructivePush = await runCli("push", "--config", config, "--allow-destructive") expect(destructivePush.exitCode).toBe(0) - expect(destructivePush.stdout).toContain("applied 1 statement(s)") + expect(destructivePush.stderr).toContain("applied 1 statement(s)") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" } @@ -435,7 +442,7 @@ export const users = db.table("users", { const safePush = await runCli("push", "--config", config) expect(safePush.exitCode).toBe(0) expect(safePush.stdout).toContain(`add column ${schemaName}.users.notes`) - expect(safePush.stdout).toContain("applied 1 statement(s)") + expect(safePush.stderr).toContain("applied 1 statement(s)") expect(safePush.stdout).toContain(`drop constraint ${schemaName}.users.users_email_check`) expect(safePush.stdout).toContain(`drop constraint ${schemaName}.users.users_email_key`) expect(safePush.stdout).toContain(`drop index ${schemaName}.users.users_email_idx`) @@ -443,7 +450,7 @@ export const users = db.table("users", { expect(safePush.stdout).toContain(`replace column ${schemaName}.users.email (drop)`) expect(safePush.stdout).toContain(`replace column ${schemaName}.users.emailLower (drop)`) expect(safePush.stdout).toContain(`replace column ${schemaName}.users.nickname (drop)`) - expect(safePush.stdout).toContain("skipped changes:") + expect(safePush.stderr).toContain("skipped changes:") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -475,8 +482,8 @@ export const users = db.table("users", { const secondSafePush = await runCli("push", "--config", config) expect(secondSafePush.exitCode).toBe(0) - expect(secondSafePush.stdout).toContain("no executable statements selected") - expect(secondSafePush.stdout).toContain("skipped changes:") + expect(secondSafePush.stderr).toContain("no executable statements selected") + expect(secondSafePush.stderr).toContain("skipped changes:") expect(secondSafePush.stdout).toContain(`drop constraint ${schemaName}.users.users_email_check`) expect(secondSafePush.stdout).toContain(`drop index ${schemaName}.users.users_email_idx`) expect(secondSafePush.stdout).toContain(`replace column ${schemaName}.users.email (drop)`) @@ -503,9 +510,9 @@ test("postgres cli migrate generate can split safe and destructive changes", asy const safeGenerate = await runCli("migrate", "generate", "--config", config, "--name", "safe_phase") expect(safeGenerate.exitCode).toBe(0) - expect(safeGenerate.stdout).toContain("wrote 0001_safe_phase.sql") - expect(safeGenerate.stdout).toContain(`drop column ${schemaName}.users.email`) - expect(safeGenerate.stdout).toContain("skipped changes:") + expect(safeGenerate.stderr).toContain("wrote 0001_safe_phase.sql") + expect(safeGenerate.stderr).toContain(`drop column ${schemaName}.users.email`) + expect(safeGenerate.stderr).toContain("skipped changes:") const safeSql = await readFile(join(workspace, "migrations", "0001_safe_phase.sql"), "utf8") expect(safeSql).toContain(`alter table "${schemaName}"."users" add column "nickname" text;`) @@ -513,7 +520,7 @@ test("postgres cli migrate generate can split safe and destructive changes", asy const safeUp = await runCli("migrate", "up", "--config", config) expect(safeUp.exitCode).toBe(0) - expect(safeUp.stdout).toContain("applied 1 migration(s)") + expect(safeUp.stderr).toContain("applied 1 migration(s)") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -531,7 +538,7 @@ test("postgres cli migrate generate can split safe and destructive changes", asy "destructive_phase" ) expect(destructiveGenerate.exitCode).toBe(0) - expect(destructiveGenerate.stdout).toContain("wrote 0002_destructive_phase.sql") + expect(destructiveGenerate.stderr).toContain("wrote 0002_destructive_phase.sql") const destructiveSql = await readFile(join(workspace, "migrations", "0002_destructive_phase.sql"), "utf8") expect(destructiveSql).toContain(`alter table "${schemaName}"."users" drop column "email";`) @@ -539,7 +546,7 @@ test("postgres cli migrate generate can split safe and destructive changes", asy const destructiveUp = await runCli("migrate", "up", "--config", config) expect(destructiveUp.exitCode).toBe(0) - expect(destructiveUp.stdout).toContain("applied 1 migration(s)") + expect(destructiveUp.stderr).toContain("applied 1 migration(s)") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -578,10 +585,10 @@ test("postgres cli applies pending migrations from alternate dirs and tables in const migrateUp = await runCli("migrate", "up", "--config", config) expect(migrateUp.exitCode).toBe(0) - expect(migrateUp.stdout).toContain("applied 3 migration(s)") - expect(migrateUp.stdout).toContain("0001_add_slug.sql") - expect(migrateUp.stdout).toContain("0002_add_nickname.sql") - expect(migrateUp.stdout).toContain("0010_add_title.sql") + expect(migrateUp.stderr).toContain("applied 3 migration(s)") + expect(migrateUp.stderr).toContain("0001_add_slug.sql") + expect(migrateUp.stderr).toContain("0002_add_nickname.sql") + expect(migrateUp.stderr).toContain("0010_add_title.sql") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -604,7 +611,7 @@ test("postgres cli applies pending migrations from alternate dirs and tables in const secondUp = await runCli("migrate", "up", "--config", config) expect(secondUp.exitCode).toBe(0) - expect(secondUp.stdout).toContain("no pending migrations") + expect(secondUp.stderr).toContain("no pending migrations") } finally { await dropSchema(schemaName).catch(() => undefined) await rm(workspace, { recursive: true, force: true }) @@ -642,7 +649,7 @@ alter table "${schemaName}"."users" drop column "nickname"; const migrateUp = await runCli("migrate", "up", "--config", config) expect(migrateUp.exitCode).toBe(0) - expect(migrateUp.stdout).toContain("applied 2 migration(s)") + expect(migrateUp.stderr).toContain("applied 2 migration(s)") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -658,7 +665,7 @@ alter table "${schemaName}"."users" drop column "nickname"; const migrateDown = await runCli("migrate", "down", "--config", config, "--steps", "1") expect(migrateDown.exitCode).toBe(0) - expect(migrateDown.stdout).toContain("rolled back 1 migration(s)") + expect(migrateDown.stderr).toContain("rolled back 1 migration(s)") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -673,7 +680,7 @@ alter table "${schemaName}"."users" drop column "nickname"; const repair = await runCli("migrate", "repair", "--config", config) expect(repair.exitCode).toBe(0) - expect(repair.stdout).toContain("repaired 1 migration record(s)") + expect(repair.stderr).toContain("repaired 1 migration record(s)") const statusAfterRepair = await runCli("migrate", "status", "--config", config) expect(statusAfterRepair.exitCode).toBe(0) @@ -754,8 +761,8 @@ alter table "${schemaName}"."users" add column "slug" text; expect(first.exitCode).toBe(0) expect(second.exitCode).toBe(0) - expect(`${first.stdout}\n${second.stdout}`).toContain("applied 1 migration(s)") - expect(`${first.stdout}\n${second.stdout}`).toContain("no pending migrations") + expect(`${first.stdout}\n${first.stderr}\n${second.stdout}\n${second.stderr}`).toContain("applied 1 migration(s)") + expect(`${first.stdout}\n${first.stderr}\n${second.stdout}\n${second.stderr}`).toContain("no pending migrations") const ledgerRows = await execPostgres<{ readonly count: number @@ -829,13 +836,13 @@ export { status } const shrinkPush = await runCli("push", "--config", config) expect(shrinkPush.exitCode).toBe(0) expect(shrinkPush.stdout).toContain(`manual enum migration required for ${schemaName}.status`) - expect(shrinkPush.stdout).toContain("no executable statements selected") - expect(shrinkPush.stdout).toContain("skipped changes:") + expect(shrinkPush.stderr).toContain("no executable statements selected") + expect(shrinkPush.stderr).toContain("skipped changes:") const shrinkGenerate = await runCli("migrate", "generate", "--config", config, "--name", "enum_shrink") expect(shrinkGenerate.exitCode).toBe(0) - expect(shrinkGenerate.stdout).toContain("no executable migration changes selected") - expect(shrinkGenerate.stdout).toContain(`manual enum migration required for ${schemaName}.status`) + expect(shrinkGenerate.stderr).toContain("no executable migration changes selected") + expect(shrinkGenerate.stderr).toContain(`manual enum migration required for ${schemaName}.status`) await writeFile(schemaFile(workspace), ` import * as Schema from "effect/Schema" @@ -861,12 +868,12 @@ export { status } const reorderPush = await runCli("push", "--config", config) expect(reorderPush.exitCode).toBe(0) expect(reorderPush.stdout).toContain(`manual enum migration required for ${schemaName}.status`) - expect(reorderPush.stdout).toContain("no executable statements selected") + expect(reorderPush.stderr).toContain("no executable statements selected") const reorderGenerate = await runCli("migrate", "generate", "--config", config, "--name", "enum_reorder") expect(reorderGenerate.exitCode).toBe(0) - expect(reorderGenerate.stdout).toContain("no executable migration changes selected") - expect(reorderGenerate.stdout).toContain(`manual enum migration required for ${schemaName}.status`) + expect(reorderGenerate.stderr).toContain("no executable migration changes selected") + expect(reorderGenerate.stderr).toContain(`manual enum migration required for ${schemaName}.status`) } finally { await dropSchema(schemaName).catch(() => undefined) await rm(workspace, { recursive: true, force: true }) @@ -969,7 +976,7 @@ test("postgres cli accepts --url overrides over the configured database url", as const push = await runCli("push", "--config", config, "--url", postgresUrl) expect(push.exitCode).toBe(0) - expect(push.stdout).toContain("applied 2 statement(s)") + expect(push.stderr).toContain("applied 2 statement(s)") const createdTables = await execPostgres( `select tablename from pg_tables where schemaname = $1 order by tablename`, @@ -983,7 +990,7 @@ test("postgres cli accepts --url overrides over the configured database url", as const pull = await runCli("pull", "--config", config, "--url", postgresUrl) expect(pull.exitCode).toBe(0) - expect(pull.stdout).toContain("updated 1 file(s)") + expect(pull.stderr).toContain("updated 1 file(s)") const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`name: Column.text().pipe(Column.nullable)`) @@ -998,11 +1005,11 @@ test("postgres cli accepts --url overrides over the configured database url", as const migrateGenerate = await runCli("migrate", "generate", "--config", config, "--url", postgresUrl, "--name", "override_path") expect(migrateGenerate.exitCode).toBe(0) - expect(migrateGenerate.stdout).toContain("wrote 0001_override_path.sql") + expect(migrateGenerate.stderr).toContain("wrote 0001_override_path.sql") const migrateUp = await runCli("migrate", "up", "--config", config, "--url", postgresUrl) expect(migrateUp.exitCode).toBe(0) - expect(migrateUp.stdout).toContain("applied 1 migration(s)") + expect(migrateUp.stderr).toContain("applied 1 migration(s)") expect(await listColumns(schemaName, "users")).toEqual([ { column_name: "id" }, @@ -1102,7 +1109,7 @@ export const audits = db.table("audits", { const pull = await runCli("pull", "--config", config) expect(pull.exitCode).toBe(0) - expect(pull.stdout).toContain("updated 2 file(s)") + expect(pull.stderr).toContain("updated 2 file(s)") expect(await readFile(join(workspace, "tables", "users.ts"), "utf8")).toContain("nickname") expect(await readFile(join(workspace, "tables", "orgs.ts"), "utf8")).toContain("slug") @@ -1110,7 +1117,7 @@ export const audits = db.table("audits", { const secondPullDryRun = await runCli("pull", "--config", config, "--dry-run") expect(secondPullDryRun.exitCode).toBe(0) - expect(secondPullDryRun.stdout).toContain("schema definitions are already up to date") + expect(secondPullDryRun.stderr).toContain("schema definitions are already up to date") } finally { await dropSchema(schemaName).catch(() => undefined) await rm(workspace, { recursive: true, force: true }) @@ -1212,7 +1219,7 @@ export { status, orgs, users } const pull = await runCli("pull", "--config", config) expect(pull.exitCode).toBe(0) - expect(pull.stdout).toContain("updated 1 file(s)") + expect(pull.stderr).toContain("updated 1 file(s)") const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`const status = types.enum("status", ["pending", "active"])`) @@ -1235,7 +1242,7 @@ export { status, orgs, users } const secondPullDryRun = await runCli("pull", "--config", config, "--dry-run") expect(secondPullDryRun).toMatchObject({ exitCode: 0 }) - expect(secondPullDryRun.stdout).toContain("schema definitions are already up to date") + expect(secondPullDryRun.stderr).toContain("schema definitions are already up to date") } finally { await dropSchema(schemaName).catch(() => undefined) await rm(workspace, { recursive: true, force: true }) @@ -1276,7 +1283,7 @@ export const users = db.table("users", { const pull = await runCli("pull", "--config", config) expect(pull.exitCode).toBe(0) - expect(pull.stdout).toContain("updated 1 file(s)") + expect(pull.stderr).toContain("updated 1 file(s)") const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`users_pkey`) @@ -1436,7 +1443,7 @@ export { orgs, memberships } const pull = await runCli("pull", "--config", config) expect(pull.exitCode).toBe(0) - expect(pull.stdout).toContain("updated 1 file(s)") + expect(pull.stderr).toContain("updated 1 file(s)") const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`memberships_org_fkey`) @@ -1524,7 +1531,7 @@ test("postgres cli pulls builtin postgres columns with dedicated constructors", const pull = await runCli("pull", "--config", config) expect(pull.exitCode).toBe(0) - expect(pull.stdout).toContain("updated 1 file(s)") + expect(pull.stderr).toContain("updated 1 file(s)") const pulledSchema = await readSchema(workspace) expect(pulledSchema).toContain(`payload: Pg.Column.jsonb(Schema.Unknown).pipe(Column.nullable)`) From c4c4da31667541a07ceab23a38cd6fdc81f679ca Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:47:31 -0700 Subject: [PATCH 20/27] docs(database): document CLI log levels --- packages/database/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/database/README.md b/packages/database/README.md index 7811aef5..677ec8f3 100644 --- a/packages/database/README.md +++ b/packages/database/README.md @@ -44,6 +44,11 @@ effectdb migrate generate effectdb migrate up ``` +`effectdb` uses Effect CLI's global `--log-level` flag. Operational logs are +written to stderr and default to `info`; use `--log-level debug` for config +diagnostics or `--log-level none` to suppress logs. Plans and status +output remain on stdout. + ## Runtime services The CLI obtains file-system, path, crypto, and terminal capabilities from From 5491998bfdb58ee5570bf80e9884d9bc51489aee Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 13:47:48 -0700 Subject: [PATCH 21/27] chore(beads): close CLI logging migration --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e4dc331f..5fbfb04c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -26,7 +26,7 @@ {"id":"effect-qb-dn3.4","title":"P1 validate effect-db rows with SqlSchema","description":"Use v4 effect/unstable/sql/SqlSchema for high-risk effect-db result rows, starting with Postgres introspection and migration ledger reads.","acceptance_criteria":"Introspection and migration ledger row shapes are represented by internal schemas; selected unsafe typed row reads are replaced or wrapped with SqlSchema helpers; checksum/migration behavior is unchanged except for earlier shape failures; behavior/integration tests cover happy path and malformed-row failure where feasible.","notes":"Status: open. Validation plan: postgres-introspector behavior tests, postgres-migrate behavior tests, CLI integration tests if Postgres is available. Keep source-code discovery independent of SqlSchema.\nImplemented in second migration slice. Added v4 SqlSchema row decoders around Postgres catalog introspection reads and applied-migration ledger reads. Malformed catalog and ledger row tests now assert schema decode failure before metadata/migration state is built. Checks passed: bun --config ./bunfig.test.toml test test/internal/behavior/postgres-introspector.behavior.test.ts test/internal/behavior/postgres-migrate.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json; bun run test:types; bun run build; bun run test; bun run test:pack.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:38Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:45:33Z","started_at":"2026-05-13T02:42:58Z","closed_at":"2026-05-13T02:45:33Z","close_reason":"effect-db catalog and migration ledger row reads now decode through v4 SqlSchema with malformed-row tests and repo gates passing.","labels":["effect-db","effect-v4","migration","sqlschema"],"dependencies":[{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:38Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.4","depends_on_id":"effect-qb-dn3.3","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"effect-qb-dn3.3","title":"P1 adopt v4 SQL core","description":"Replace standalone @effect/sql imports with effect/unstable/sql imports and update built-in executor transaction/stream handling for v4 SqlClient semantics.","acceptance_criteria":"Executor imports use v4 SQL core; streamFromSqlClient no longer relies on stale global TransactionConnection behavior; transaction consumption semantics remain covered; UniqueViolation reason usage is documented or tested without deleting existing dialect-specific SQLSTATE catalogs.","notes":"Status: open. Validation plan: executor behavior tests, executor stream type tests, SQL error behavior tests, and integration tests if driver setup is available.\nImplemented in first migration slice. SQL imports now use effect/unstable/sql/*; v4 driver packages are version-aligned; streamFromSqlClient uses the client transactionService and v4 Stream APIs instead of stale TransactionConnection/mapChunks paths. Existing dialect-specific SQLSTATE catalogs were preserved; UniqueViolation v4 reason behavior remains documented in docs/effect-v4-opportunities.md. Verified by executor behavior tests, SQL error behavior tests, full tsgo type-test project, build, pack, and full Bun test script.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:33Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:55Z","closed_at":"2026-05-13T02:41:55Z","close_reason":"v4 SQL core imports and transaction/stream handling implemented; executor and SQL error behavior tests passed.","labels":["effect-v4","migration","sql"],"dependencies":[{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:33Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.3","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"id":"effect-qb-dn3.2","title":"P1 replace experimental VariantSchema","description":"Move table select/insert/update schema derivation from @effect/experimental/VariantSchema to effect/unstable/schema/VariantSchema while preserving deriveSchemas public behavior and generated declaration intent.","acceptance_criteria":"@effect/experimental is no longer needed by querybuilder; deriveSchemas still derives select, insert, and update schemas with generated/default/nullable/primary-key/brand semantics; table behavior and type tests pass; generated declarations are intentionally updated if v4 types require it.","notes":"Status: open. Depends on the mechanical Schema API migration compiling. Validation plan: table behavior tests, table type tests, README schema examples, and tsgo type-test project.\nImplemented in first migration slice by moving table schema derivation to effect/unstable/schema/VariantSchema and removing @effect/experimental from package manifests. deriveSchemas now returns v4 Schema.Decoder values for select/insert/update. Verified by table behavior, generated README type tests, packed consumer test, and full tsgo type-test project.","status":"closed","priority":1,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:25Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:43Z","closed_at":"2026-05-13T02:41:43Z","close_reason":"VariantSchema migration implemented with @effect/experimental removed and deriveSchemas preserved under v4 tests.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:25Z","created_by":"Ramazan Elsunkaev","metadata":"{}"},{"issue_id":"effect-qb-dn3.2","depends_on_id":"effect-qb-dn3.1","type":"blocks","created_at":"2026-05-12T19:20:09Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"effect-qb-9bn","title":"Route effect-db diagnostics through Effect Logger","description":"Use Effect CLI's built-in log-level setting for effect-db operational logs while reserving Terminal output for stable command results.","acceptance_criteria":"CLI operational summaries use Effect log levels and stderr; stable plans/status remain Terminal stdout; --log-level controls emitted diagnostics without custom filtering; tests cover stdout/stderr and suppression; Node package smoke remains green.","status":"in_progress","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T20:40:41Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:40:49Z","started_at":"2026-07-22T20:40:49Z","labels":["database","effect-cli","logging"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-9bn","title":"Route effect-db diagnostics through Effect Logger","description":"Use Effect CLI's built-in log-level setting for effect-db operational logs while reserving Terminal output for stable command results.","acceptance_criteria":"CLI operational summaries use Effect log levels and stderr; stable plans/status remain Terminal stdout; --log-level controls emitted diagnostics without custom filtering; tests cover stdout/stderr and suppression; Node package smoke remains green.","notes":"Implemented Effect Logger-based operational summaries with info/warning/debug levels and stderr routing; Terminal remains the stable stdout boundary for plans and status. Effect CLI's built-in --log-level controls diagnostics, including debug config context and none suppression. Verified 536 behavior tests, 55 live integration tests, tsgo types, build, and packed Node 22/24 smoke.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T20:40:41Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:47:45Z","started_at":"2026-07-22T20:40:49Z","closed_at":"2026-07-22T20:47:45Z","close_reason":"effect-db operational diagnostics now use Effect Logger with built-in log-level control and stable Terminal stdout; all acceptance gates pass.","labels":["database","effect-cli","logging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-8l5","title":"Move effect-db I/O behind Effect Platform services","description":"The published effect-db Promise APIs still access node:fs, node:path, and node:crypto directly in config loading, source discovery, pull, and migration code. Introduce one authoritative Effect Platform boundary using FileSystem, Path, and Crypto, then migrate the CLI as the representative production caller without duplicating behavior.","acceptance_criteria":"The CLI obtains FileSystem, Path, and Crypto from NodeServices; portable core workflows contain no direct node:fs, node:path, or node:crypto access; existing Promise-facing exports remain compatible or follow an explicitly approved breaking transition; focused tests use test platform services; Node 22/24 package smoke remains green.","notes":"Implemented Effect variants for config loading, source discovery, pull planning/application, schema planning, and migration filesystem workflows. Promise exports remain compatible through one NodeServices adapter; the CLI composes Effect workflows and provides NodeServices once at the root. Source runtime code has no direct node:fs, node:path, or node:crypto imports. tinyglobby remains for discovery because Effect FileSystem.glob does not expose the existing dot-file and symlink-following semantics. Added injected FileSystem, Path, and Crypto tests. Verified 536 behavior tests, 55 live integration tests, full tsgo types, build, and packed smoke on Node 22 and 24.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:50:07Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:27:07Z","started_at":"2026-07-22T20:12:55Z","closed_at":"2026-07-22T20:27:07Z","close_reason":"effect-db filesystem, path, and crypto workflows now use Effect Platform services with compatible Node Promise adapters and NodeServices CLI assembly; all acceptance gates pass.","labels":["database","effect-platform","portability"],"dependencies":[{"issue_id":"effect-qb-8l5","depends_on_id":"effect-qb-02d","type":"discovered-from","created_at":"2026-07-22T12:50:07Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-02d","title":"Complete Node-native package verification","description":"Finish the Node-native boundaries for effect-db and effect-qb while retaining Bun for workspace package management and tooling.","acceptance_criteria":"Postgres CLI integration executes packages/database/dist/cli.js with Node; CLI host output uses Effect Platform services with Node implementations confined to runtime assembly; production package source typechecks with explicit Node types and without Bun ambient types; packed artifacts are smoked on Node 22 and Node 24 in CI; the root workspace is private and declares its Bun package manager/engine; focused and existing quality gates pass.","notes":"User clarified that portable host capabilities should use Effect Platform wherever practical; Node remains the executable adapter through NodeServices/NodeRuntime.\nImplemented: built CLI integration now runs dist/cli.js with Node; CLI output uses Effect Terminal; production sources typecheck with @types/node and no Bun globals; CI smokes packed packages on Node 22/24; root declares Bun package metadata. Production-path verification also fixed stale extension-only pull generation, Postgres bigint migration ID decoding, and the silent no-op Bun test config argument. Verified 534 behavior tests, 55 live integration tests, full type tests, build, frozen install, and packed Node 22/24 smoke.","status":"closed","priority":2,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T19:43:50Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T20:08:18Z","started_at":"2026-07-22T19:43:55Z","closed_at":"2026-07-22T20:08:18Z","close_reason":"Node-native package verification and production CLI coverage are implemented and passing; broader Effect Platform I/O migration remains tracked by effect-qb-8l5.","labels":["ci","node","packaging"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-tmy","title":"Run effect-db CLI on Node.js","description":"The published effectdb binary currently requires Bun through its shebang and platform runtime even though the package already supports Node.js. Replace the CLI runtime with Effect Node services while keeping Bun for repository build and test tooling.","acceptance_criteria":"A packed effect-db CLI starts under Node.js without Bun on PATH, retains CLI help behavior, and package metadata no longer requires Bun for consumers.","status":"closed","priority":2,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-22T03:13:09Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-22T03:22:13Z","started_at":"2026-07-22T03:13:13Z","closed_at":"2026-07-22T03:22:13Z","close_reason":"effect-db now uses the Effect Node runtime and Node-compatible file APIs; packed Node 22 smoke and behavior/type tests pass.","dependency_count":0,"dependent_count":0,"comment_count":0} From ec2be3f5f9f73be21bf3c795c357eba7fbccf001 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 17:40:27 -0700 Subject: [PATCH 22/27] chore(beads): track Node release hardening --- .beads/issues.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5fbfb04c..80e2e4dc 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,5 +1,6 @@ {"id":"effect-qb-dn3.1","title":"P0 migrate Effect dependencies and schema API surface","description":"Smallest coherent implementation slice. Align workspace/package dependencies to effect@4.0.0-beta.66 and matching v4 packages, remove @effect/experimental where replaced, and perform the mechanical Schema API migration called out in docs/effect-v4-opportunities.md without intentional behavior changes.","acceptance_criteria":"Effect package versions are aligned to the observed v4 beta line; v3 Schema call shapes listed in the opportunity doc are migrated; README/type-test examples are updated as needed; behavior tests covering risky runtime schema/json paths still pass; bun run test:types, bun test, and relevant tsgo checks are run or blockers documented.","notes":"Status: in_progress. Starting with repo inspection and focused regression/type coverage around current Schema API and JSON path inference before the mechanical v4 dependency/API migration.\nBaseline before migration: focused Bun behavior tests passed for runtime-value/json/table/executor; tsgo type-test project passed. Commands: bun --config ./bunfig.test.toml test test/internal/behavior/runtime-value.behavior.test.ts test/public/behavior/json.behavior.test.ts test/internal/behavior/table.test.ts test/public/behavior/executor.behavior.test.ts; bunx tsgo -p tsconfig.type-tests.json.\nImplemented and verified in the first migration slice. Dependencies aligned to effect@4.0.0-beta.66 and matching v4 driver/platform packages; removed @effect/experimental, @effect/sql, @effect/cli, and @effect/platform where v4 unstable exports replaced them; migrated mechanical Schema API changes and README/type examples. Checks passed: bunx tsgo -p tsconfig.type-tests.json; bun --config ./bunfig.test.toml test test/internal/behavior/runtime-value.behavior.test.ts test/public/behavior/json.behavior.test.ts test/internal/behavior/table.test.ts test/public/behavior/executor.behavior.test.ts test/public/behavior/mysql-errors.behavior.test.ts test/public/behavior/postgres-errors.behavior.test.ts; bun run test:types; bun run build; bun run test:pack; bun run test.","status":"closed","priority":0,"issue_type":"task","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:18Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:41:33Z","started_at":"2026-05-13T02:20:14Z","closed_at":"2026-05-13T02:41:33Z","close_reason":"P0 dependency and mechanical Schema API migration implemented; full type, build, pack, focused behavior, and full Bun tests passed.","labels":["effect-v4","migration","schema"],"dependencies":[{"issue_id":"effect-qb-dn3.1","depends_on_id":"effect-qb-dn3","type":"parent-child","created_at":"2026-05-12T19:18:18Z","created_by":"Ramazan Elsunkaev","metadata":"{}"}],"dependency_count":0,"dependent_count":6,"comment_count":0} {"id":"effect-qb-dn3","title":"Effect v4 migration epic","description":"Parent epic for the Effect v4 migration work described in docs/effect-v4-opportunities.md. Scope includes dependency alignment, mechanical Schema API migration, VariantSchema replacement, v4 SQL core adoption, SqlSchema validation, optional v4 codecs, row decode error improvements, JSON schema inference review, and CLI/platform packaging.","acceptance_criteria":"All child beads map to docs/effect-v4-opportunities.md opportunities; every opportunity is implemented, explicitly deferred, or blocked with a concrete reason; relevant tests/type checks/lint checks pass; coherent increments are committed.","notes":"Source of truth: docs/effect-v4-opportunities.md checked against effect@4.0.0-beta.66. Assumption: this branch is the v4 line, so no Effect 3 compatibility shim is required unless a child bead discovers a concrete compatibility requirement.","status":"open","priority":0,"issue_type":"epic","owner":"relsunkaev@outlook.com","created_at":"2026-05-13T02:18:02Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-05-13T02:18:02Z","labels":["effect-v4","migration"],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"effect-qb-2ur","title":"Harden Node-native verification and release v0.21.0","description":"Close the remaining verification gaps for the Node-native effect-db CLI, merge the completed branch into main, and publish the next stable release through the repository workflows.","acceptance_criteria":"Live PostgreSQL CLI integration runs on Node 22 and 24 in CI; packed installed effect-db exercises a real database command without Bun available to the CLI; fatal CLI failures have an explicit stderr contract; portable Effect Platform workflows have focused injected-service coverage for remaining write/discovery paths; all local and remote gates pass; changes are merged to main; v0.21.0 is published to npm and GitHub with verified artifacts.","status":"in_progress","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-23T00:38:49Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-23T00:38:58Z","started_at":"2026-07-23T00:38:58Z","labels":["ci","database","node","release"],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-xzx","title":"Release v0.20.0","description":"Publish the verified Effect v4 beta.98 code as the next stable zerover release for effect-qb and effect-db, keeping Effect dependencies pinned to 4.0.0-beta.98 and promoting npm latest to 0.20.0.","acceptance_criteria":"Release notes cover the complete stable delta since v0.19.0; package and CLI versions are 0.20.0 while Effect dependencies remain 4.0.0-beta.98; build, unit, type, pack, and integration gates pass; v0.20.0 publishes both packages under latest with signed provenance; GitHub marks v0.20.0 as Latest; release evidence is recorded and the issue is closed.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-13T19:53:35Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-13T20:00:09Z","started_at":"2026-07-13T19:53:40Z","closed_at":"2026-07-13T20:00:09Z","close_reason":"Published effect-qb and effect-db 0.20.0 via v0.20.0 after PR #9; publish run 29280566140 succeeded; npm latest tags, matching tarball shasums, signed provenance, GitHub Latest metadata, and a clean registry consumer smoke were verified.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-vt7","title":"Classify beta GitHub releases as prereleases","description":"The publish workflow maps prerelease versions to the npm beta dist-tag but unconditionally creates a normal GitHub release, causing beta versions to become Latest. Derive GitHub release classification from the same npm-tag decision and correct v4.0.0-beta.98 metadata.","acceptance_criteria":"Publish workflow passes prerelease=true and latest=false for hyphenated versions, stable versions remain latest releases, actionlint passes, and v4.0.0-beta.98 is shown as a GitHub prerelease rather than Latest.","status":"closed","priority":1,"issue_type":"bug","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-13T19:47:12Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-13T19:50:37Z","started_at":"2026-07-13T19:47:17Z","closed_at":"2026-07-13T19:50:37Z","close_reason":"Publish workflow now classifies npm beta releases as GitHub prereleases and explicitly clears the make_latest pointer; actionlint passes; beta.92 and beta.98 metadata verified as prerelease with v0.17.0 restored as latest.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"effect-qb-lbx","title":"Release v4.0.0-beta.98","description":"Upgrade the Effect v4 dependency stack from beta.92 to beta.98, cut matching effect-qb and effect-db beta.98 packages, publish through the repository release workflows, and verify npm plus GitHub release state.","acceptance_criteria":"All Effect packages and peer dependencies resolve to 4.0.0-beta.98; build, unit, type, pack, and integration gates pass; v4.0.0-beta.98 is tagged and published for effect-qb/effect-db under the beta dist-tag; the GitHub release and publish workflow succeed.","status":"closed","priority":1,"issue_type":"task","assignee":"Ramazan Elsunkaev","owner":"relsunkaev@outlook.com","created_at":"2026-07-13T19:35:31Z","created_by":"Ramazan Elsunkaev","updated_at":"2026-07-13T19:45:11Z","started_at":"2026-07-13T19:36:09Z","closed_at":"2026-07-13T19:45:11Z","close_reason":"Published effect-qb and effect-db 4.0.0-beta.98 via tag v4.0.0-beta.98; PR #6 and publish run 29279588367 succeeded; npm beta tags and signed provenance verified.","dependency_count":0,"dependent_count":0,"comment_count":0} From dd38ebfac8dbeb23fd70f121b8a8bfc7188bc852 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 17:47:23 -0700 Subject: [PATCH 23/27] fix(database): report CLI failures on stderr --- packages/database/src/cli.ts | 3 ++- test/public/integration/postgres-cli.integration.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/database/src/cli.ts b/packages/database/src/cli.ts index 329d23f6..22aea616 100644 --- a/packages/database/src/cli.ts +++ b/packages/database/src/cli.ts @@ -389,7 +389,8 @@ const cli = Command.run(root, { }) cli.pipe( + Effect.tapCause((cause) => Effect.logError(cause)), Effect.provideService(Logger.LogToStderr, true), Effect.provide(NodeServices.layer), - NodeRuntime.runMain + NodeRuntime.runMain({ disableErrorReporting: true }) ) diff --git a/test/public/integration/postgres-cli.integration.ts b/test/public/integration/postgres-cli.integration.ts index 0f58da61..367336ce 100644 --- a/test/public/integration/postgres-cli.integration.ts +++ b/test/public/integration/postgres-cli.integration.ts @@ -252,6 +252,16 @@ const assertIdempotentPullPush = async (config: string) => { expect(pushDryRun.stdout).toContain("planned changes: none") } +test("postgres cli reports fatal failures on stderr", async () => { + const missingConfig = join(repoRoot, "test", `missing-effectdb-${randomId()}.config.mjs`) + const result = await runCliUnlocked("pull", "--config", missingConfig, "--dry-run") + + expect(result.exitCode).toBe(1) + expect(result.stdout).toBe("") + expect(result.stderr).toContain("ERROR") + expect(result.stderr).toContain("missing-effectdb-") +}) + test("postgres cli supports push pull and migrations against a live database", async () => { const { workspace, schemaName } = await makeWorkspace() try { From 15a54e5079ac494ff5811c2c8875f29765b15df2 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 17:47:24 -0700 Subject: [PATCH 24/27] test(database): cover portable write services --- .../postgres-migrate.behavior.test.ts | 30 ++++++++++++++- ...ostgres-schema-management.behavior.test.ts | 37 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/test/internal/behavior/postgres-migrate.behavior.test.ts b/test/internal/behavior/postgres-migrate.behavior.test.ts index d408922b..77dcc246 100644 --- a/test/internal/behavior/postgres-migrate.behavior.test.ts +++ b/test/internal/behavior/postgres-migrate.behavior.test.ts @@ -8,7 +8,7 @@ import * as Effect from "effect/Effect" import * as FileSystem from "effect/FileSystem" import * as Path from "effect/Path" -import { readMigrationFiles, readMigrationFilesEffect, ensureMigrationTable, readAppliedMigrationRows } from "../../../packages/database/src/postgres/migrate.js" +import { readMigrationFiles, readMigrationFilesEffect, writeMigrationFileEffect, ensureMigrationTable, readAppliedMigrationRows } from "../../../packages/database/src/postgres/migrate.js" import { withoutManagedMigrationTable } from "../../../packages/database/src/postgres/push.js" import type { SchemaModel } from "effect-qb/postgres/metadata" @@ -39,6 +39,34 @@ describe("postgres migrations", () => { }]) }) + test("writes migrations through provided platform services", async () => { + const directories: string[] = [] + const writes: Array<{ readonly path: string; readonly contents: string }> = [] + const filePath = await Effect.runPromise( + writeMigrationFileEffect("/migrations", "add users", []).pipe( + Effect.provide(FileSystem.layerNoop({ + makeDirectory: (path) => { + directories.push(path) + return Effect.void + }, + readDirectory: () => Effect.succeed(["0002_previous.sql", "README.md"]), + writeFileString: (path, contents) => { + writes.push({ path, contents }) + return Effect.void + } + })), + Effect.provide(Path.layer) + ) + ) + + expect(filePath).toBe("/migrations/0003_add_users.sql") + expect(directories).toEqual(["/migrations"]) + expect(writes).toEqual([{ + path: "/migrations/0003_add_users.sql", + contents: "-- effect-db:up\n\n" + }]) + }) + test("parses up and down sections and normalizes checksum line endings", async () => { const tempDir = await mkdtemp(join(repoRoot, "test/.tmp-postgres-migrate-")) try { diff --git a/test/internal/behavior/postgres-schema-management.behavior.test.ts b/test/internal/behavior/postgres-schema-management.behavior.test.ts index 37b71cbc..04cb2f84 100644 --- a/test/internal/behavior/postgres-schema-management.behavior.test.ts +++ b/test/internal/behavior/postgres-schema-management.behavior.test.ts @@ -3,6 +3,9 @@ import { join, relative } from "node:path" // @ts-nocheck import { describe, expect, test } from "bun:test" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" import * as Schema from "effect/Schema" import * as Pg from "#postgres" @@ -16,12 +19,44 @@ import { discoverSourceSchema, type DiscoveredSourceSchema } from "../../../packages/database/src/internal/postgres-source-discovery.js" -import { planPostgresPull } from "../../../packages/database/src/postgres/pull.js" +import { applyPullPlanEffect, planPostgresPull } from "../../../packages/database/src/postgres/pull.js" import * as StdRoot from "#standard" const repoRoot = process.cwd() describe("postgres schema management", () => { + test("applies pull plans through provided platform services", async () => { + const directories: string[] = [] + const writes: Array<{ readonly path: string; readonly contents: string }> = [] + await Effect.runPromise( + applyPullPlanEffect({ + updates: [{ + filePath: "/workspace/src/schema.ts", + before: "", + after: "export {}\n" + }] + }).pipe( + Effect.provide(FileSystem.layerNoop({ + makeDirectory: (path) => { + directories.push(path) + return Effect.void + }, + writeFileString: (path, contents) => { + writes.push({ path, contents }) + return Effect.void + } + })), + Effect.provide(Path.layer) + ) + ) + + expect(directories).toEqual(["/workspace/src"]) + expect(writes).toEqual([{ + path: "/workspace/src/schema.ts", + contents: "export {}\n" + }]) + }) + test("source table models use casing metadata as physical identifiers", () => { const organizations = StdRoot.Table.make("OrganizationAccounts", { id: StdRoot.Column.uuid().pipe(StdRoot.Column.primaryKey), From 7a266622aaea2722efee3fe1b741854e4074b5c5 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 17:47:25 -0700 Subject: [PATCH 25/27] test(pack): exercise installed CLI against Postgres --- scripts/check-packed-querybuilder.ts | 88 +++++++++++++++++++++------- scripts/test-integration.ts | 17 ++++++ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/scripts/check-packed-querybuilder.ts b/scripts/check-packed-querybuilder.ts index be5a1346..24ae49cb 100644 --- a/scripts/check-packed-querybuilder.ts +++ b/scripts/check-packed-querybuilder.ts @@ -1,10 +1,11 @@ -import { mkdtemp, rm, symlink } from "node:fs/promises" +import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises" import { tmpdir } from "node:os" import { dirname, join, resolve } from "node:path" const cwd = process.cwd() const querybuilderPackageDir = join(cwd, "packages", "querybuilder") const databasePackageDir = join(cwd, "packages", "database") +const postgresUrl = process.env.EFFECT_DB_SMOKE_POSTGRES_URL const querybuilderTarballPath = async () => { const proc = Bun.spawn([ @@ -281,25 +282,72 @@ await applyPullPlan({ updates: [{ filePath: pulledPath, before: "", after: "expo if (await readFile(pulledPath, "utf8") !== "export {}\\n") throw new Error("failed to apply pull plan under Node.js") `) await run([nodePath, "node-smoke.mjs"], consumerDir) - const cli = Bun.spawn([ - join(consumerDir, "node_modules", ".bin", "effectdb"), - "--help" - ], { - cwd: consumerDir, - env: { - ...process.env, - PATH: nodeOnlyBinDir - }, - stdout: "pipe", - stderr: "pipe" - }) - const [stdout, stderr, exitCode] = await Promise.all([ - new Response(cli.stdout).text(), - new Response(cli.stderr).text(), - cli.exited - ]) - if (exitCode !== 0 || !stdout.includes("effectdb")) { - throw new Error(`Packed effect-db CLI failed under Node.js:\n${stdout}${stderr}`) + const runCli = async (args: readonly string[]) => { + const cli = Bun.spawn([ + join(consumerDir, "node_modules", ".bin", "effectdb"), + ...args + ], { + cwd: consumerDir, + env: { + ...process.env, + PATH: nodeOnlyBinDir + }, + stdout: "pipe", + stderr: "pipe" + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(cli.stdout).text(), + new Response(cli.stderr).text(), + cli.exited + ]) + return { stdout, stderr, exitCode } + } + + const help = await runCli(["--help"]) + if (help.exitCode !== 0 || !help.stdout.includes("effectdb")) { + throw new Error(`Packed effect-db CLI failed under Node.js:\n${help.stdout}${help.stderr}`) + } + + if (postgresUrl !== undefined) { + const liveWorkspace = join(consumerDir, "packed-cli-live") + const schemaName = `pack_smoke_${crypto.randomUUID().replaceAll("-", "")}` + await mkdir(liveWorkspace) + await Bun.write(join(liveWorkspace, "effectdb.config.mjs"), ` +import { defineConfig } from "effect-db" + +export default defineConfig({ + dialect: "postgres", + db: { url: ${JSON.stringify(postgresUrl)} }, + source: { include: ["schema.mjs"] }, + filter: { schemas: [${JSON.stringify(schemaName)}] }, + migrations: { dir: "migrations", table: ${JSON.stringify(`${schemaName}.effect_qb_migrations`)} }, + safety: { nonDestructiveDefault: true } +}) +`) + await Bun.write(join(liveWorkspace, "schema.mjs"), ` +import { Column, Table } from "effect-qb" +import * as Pg from "effect-qb/postgres" + +const db = Pg.Schema.make(${JSON.stringify(schemaName)}) +const users = db.table("users", { + id: Column.uuid() +}).pipe(Table.primaryKey((table) => table.id)) + +export { users } +`) + const push = await runCli([ + "push", + "--config", + join(liveWorkspace, "effectdb.config.mjs"), + "--dry-run" + ]) + if ( + push.exitCode !== 0 || + !push.stdout.includes(`create schema ${schemaName}`) || + !push.stdout.includes(`create table ${schemaName}.users`) + ) { + throw new Error(`Packed effect-db CLI live smoke failed under Node.js:\n${push.stdout}${push.stderr}`) + } } } finally { await rm(consumerDir, { recursive: true, force: true }) diff --git a/scripts/test-integration.ts b/scripts/test-integration.ts index 21a3b0c7..a5e0c773 100644 --- a/scripts/test-integration.ts +++ b/scripts/test-integration.ts @@ -2,10 +2,27 @@ import { $ } from "bun" const composeFile = "docker-compose.integration.yml" const bunfig = "./bunfig.integration.toml" +const postgresUrl = "postgres://effect_qb:effect_qb@127.0.0.1:55432/effect_qb_test" + +const runPackedSmoke = async () => { + const proc = Bun.spawn(["bun", "run", "test:pack"], { + env: { + ...process.env, + EFFECT_DB_SMOKE_POSTGRES_URL: postgresUrl + }, + stdout: "inherit", + stderr: "inherit" + }) + const exitCode = await proc.exited + if (exitCode !== 0) { + throw new Error(`Packed package smoke failed with exit code ${exitCode}`) + } +} const main = async () => { try { await $`docker compose -f ${composeFile} up -d --wait` + await runPackedSmoke() await $`bun --config=${bunfig} test --timeout 60000 ./test/public/integration/*.integration.ts` } finally { try { From 42e9c00c3dd66bd7cc34cbf520f197a0adc30d04 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 17:47:26 -0700 Subject: [PATCH 26/27] ci: run Node integration across supported versions --- .github/workflows/ci.yml | 11 ++++++++++- .github/workflows/release.yml | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b17582c3..b5386855 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,9 +80,13 @@ jobs: run: bun run test:pack integration: - name: Integration + name: Integration (Node ${{ matrix.node }}) runs-on: ubuntu-latest needs: verify + strategy: + fail-fast: false + matrix: + node: [22, 24] steps: - name: Checkout uses: actions/checkout@v4 @@ -92,6 +96,11 @@ jobs: with: bun-version: 1.3.5 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - name: Install dependencies run: bun install --frozen-lockfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be4a1b25..753aa38f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,11 @@ jobs: with: bun-version: 1.3.5 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + - name: Install dependencies run: bun install --frozen-lockfile From c79315072f7672d4c3e35db1a613bcc230072203 Mon Sep 17 00:00:00 2001 From: Ramazan Elsunkaev Date: Wed, 22 Jul 2026 17:52:54 -0700 Subject: [PATCH 27/27] fix(ci): preserve required check names --- .github/workflows/ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5386855..e3d1b2f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,12 +47,16 @@ jobs: bunx npm pack --dry-run ./packages/database verify-node: - name: Verify packed artifacts (Node ${{ matrix.node }}) + name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - node: [22, 24] + include: + - node: 22 + name: Verify Node 22 + - node: 24 + name: Verify Node 24 steps: - name: Checkout uses: actions/checkout@v4 @@ -80,13 +84,17 @@ jobs: run: bun run test:pack integration: - name: Integration (Node ${{ matrix.node }}) + name: ${{ matrix.name }} runs-on: ubuntu-latest needs: verify strategy: fail-fast: false matrix: - node: [22, 24] + include: + - node: 22 + name: Integration + - node: 24 + name: Integration Node 24 steps: - name: Checkout uses: actions/checkout@v4