From 2592d99b35932682ffff149e60abfc5484ded7b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:58:10 +0000 Subject: [PATCH] chore: version packages --- .changeset/batched-lookups.md | 5 -- .changeset/begin-transaction-settings.md | 5 -- .changeset/cardinality-helpers.md | 5 -- .changeset/clickhouse-http-param-escaping.md | 8 --- .changeset/client-defaults.md | 5 -- .changeset/column-names-and-casing.md | 12 ---- .changeset/coming-from-drizzle-prisma.md | 5 -- .changeset/constraint-errors.md | 5 -- .changeset/database-test-layer.md | 5 -- .changeset/db-stream.md | 11 ---- .changeset/dialect-in-context.md | 5 -- .changeset/keyset-pagination.md | 5 -- .changeset/make-repository.md | 5 -- .changeset/observe-exit-redaction.md | 10 ---- .changeset/pg-json-paths.md | 5 -- .changeset/pipeable-query-api.md | 5 -- .changeset/predicate-facts-joins.md | 5 -- .changeset/relation-filters.md | 5 -- .changeset/relational-reads.md | 5 -- .changeset/safe-conflict-tenant.md | 5 -- .changeset/safe-decode-errors.md | 5 -- .changeset/safe-sqlite-rebuild.md | 5 -- .changeset/seed-from-schemas.md | 5 -- .changeset/statement-span-only.md | 5 -- .changeset/table-row-schemas.md | 5 -- .changeset/text-search.md | 5 -- .changeset/type-level-dialect.md | 5 -- .changeset/upsert-excluded.md | 5 -- .changeset/verify-schema.md | 5 -- CHANGELOG.md | 58 ++++++++++++++++++++ package.json | 2 +- 31 files changed, 59 insertions(+), 167 deletions(-) delete mode 100644 .changeset/batched-lookups.md delete mode 100644 .changeset/begin-transaction-settings.md delete mode 100644 .changeset/cardinality-helpers.md delete mode 100644 .changeset/clickhouse-http-param-escaping.md delete mode 100644 .changeset/client-defaults.md delete mode 100644 .changeset/column-names-and-casing.md delete mode 100644 .changeset/coming-from-drizzle-prisma.md delete mode 100644 .changeset/constraint-errors.md delete mode 100644 .changeset/database-test-layer.md delete mode 100644 .changeset/db-stream.md delete mode 100644 .changeset/dialect-in-context.md delete mode 100644 .changeset/keyset-pagination.md delete mode 100644 .changeset/make-repository.md delete mode 100644 .changeset/observe-exit-redaction.md delete mode 100644 .changeset/pg-json-paths.md delete mode 100644 .changeset/pipeable-query-api.md delete mode 100644 .changeset/predicate-facts-joins.md delete mode 100644 .changeset/relation-filters.md delete mode 100644 .changeset/relational-reads.md delete mode 100644 .changeset/safe-conflict-tenant.md delete mode 100644 .changeset/safe-decode-errors.md delete mode 100644 .changeset/safe-sqlite-rebuild.md delete mode 100644 .changeset/seed-from-schemas.md delete mode 100644 .changeset/statement-span-only.md delete mode 100644 .changeset/table-row-schemas.md delete mode 100644 .changeset/text-search.md delete mode 100644 .changeset/type-level-dialect.md delete mode 100644 .changeset/upsert-excluded.md delete mode 100644 .changeset/verify-schema.md diff --git a/.changeset/batched-lookups.md b/.changeset/batched-lookups.md deleted file mode 100644 index ff4c586..0000000 --- a/.changeset/batched-lookups.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`Db.findById(query, { key })` and `Db.findAllBy(query, { key })` in `/database` batch lookups by a key with Effect's `RequestResolver`: N concurrent calls send one statement, the query with `key IN (...)` added to its WHERE, and each row decodes through the query's row schema. A batch never mixes `Database`s, transactions or params, so a tenant filter in the query (a value or a `param.*`, given with `.with(params)`) keeps each statement to one tenant, and `requireTenantScope` checks it. On a dialect with a bound-parameter limit (D1: 100) a large batch is split into statements that each fit. diff --git a/.changeset/begin-transaction-settings.md b/.changeset/begin-transaction-settings.md deleted file mode 100644 index 2e41303..0000000 --- a/.changeset/begin-transaction-settings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`Db.transaction({ isolationLevel, accessMode, deferrable })` on Postgres puts the settings in the statement that starts the transaction (`BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY`) through Effect's `SqlClient.BeginTransaction`, one round trip less than the `SET TRANSACTION` it sent before. A dialect gives the BEGIN statement with the new optional `DialectTransactions.beginTransaction`; `postgresDialect` has it, and a dialect without it still sends `SET TRANSACTION`. SQLite keeps its client's own BEGIN. `observe` sees the BEGIN statement, and a failed BEGIN reports it as `DatabaseError.sql`. diff --git a/.changeset/cardinality-helpers.md b/.changeset/cardinality-helpers.md deleted file mode 100644 index eb6bc2c..0000000 --- a/.changeset/cardinality-helpers.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`/database` adds `runOne`, `runOption` and `runNonEmpty`: `run` with a check on how many rows came back. `runOne` wants exactly one row, `runOption` returns an `Option` and wants at most one, and `runNonEmpty` returns a `NonEmptyReadonlyArray` and wants at least one. A wrong count fails with `ResultCardinalityError` (`expected`, `rows`) in the error channel. The query runs as written, with no `LIMIT` added, so a duplicate row is found, not hidden. diff --git a/.changeset/clickhouse-http-param-escaping.md b/.changeset/clickhouse-http-param-escaping.md deleted file mode 100644 index b74908d..0000000 --- a/.changeset/clickhouse-http-param-escaping.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@maple-dev/effect-orm": patch ---- - -`ClickhouseHttpClient` escapes bound parameters the way ClickHouse reads them. A string with a -newline, tab or NUL no longer fails with `BAD_QUERY_PARAMETER`, and a backslash is no longer -read as the start of an escape sequence, which silently changed the value. Arrays are sent as -ClickHouse literals, so arrays of strings now bind at all (#38). diff --git a/.changeset/client-defaults.md b/.changeset/client-defaults.md deleted file mode 100644 index 3a88cb5..0000000 --- a/.changeset/client-defaults.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`PG.column` and `SQLite.column` take client-side defaults as Effects: `defaultFn` runs for each row an insert leaves the column out of, and `onUpdate` for each update that does not set it (and for an insert, when the column has no other default). An Effect can read the `Clock`, `Random` or a service, so `TestClock` and a test layer give exact values in tests. Such a column is optional in the insert row type, and the Effect's requirements become the write's: `Db.run(PG.insertInto(Notes).values(...))` requires `IdGen` when `id` is `{ defaultFn: IdGen.next }`. `Database.run` runs them; `fillDefaults(write)` runs them for a write you compile yourself, and `compile` refuses a write that still leaves one out, and an `INSERT ... SELECT` that does not select such a column. They write no DDL and no migration. `Table`, `PgSchemaTable` and `SqliteSchemaTable` gain two type parameters for the requirements; they default to `never`. diff --git a/.changeset/column-names-and-casing.md b/.changeset/column-names-and-casing.md deleted file mode 100644 index cbc7ee0..0000000 --- a/.changeset/column-names-and-casing.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -Column names apart from keys, on every dialect. `CH.column(type, { name: "org_id" })` now does -what `PG.column` already did: the column is stored as `org_id` and read, written, filtered and -indexed as its key. A new `casing` table option (`"snake_case"` or `"camelCase"`, the `Casing` -type), on `CH.table`, `PG.table` and `SQLite.table`, names every column that gives no `name` from its key, by drizzle-orm's rule: `orgId` is -`org_id`, `HTTPStatus` is `http_status`. The DDL, migrations, inserts, updates, upsert targets, -`returning` and tenant scope use the database name; rows decode under the key. A ClickHouse -materialized view whose target renames a column records a problem, since ClickHouse fills the -target by database name. Two keys that map to one database name are a definition problem. diff --git a/.changeset/coming-from-drizzle-prisma.md b/.changeset/coming-from-drizzle-prisma.md deleted file mode 100644 index 0aa206d..0000000 --- a/.changeset/coming-from-drizzle-prisma.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": patch ---- - -A new guide, `docs/coming-from-drizzle-prisma.md`, maps the common Prisma and Drizzle operations to effect-orm, says what differs by design, and shows a Cloudflare Worker that reaches Postgres through Hyperdrive. Its examples run on PGlite in the doc checks. diff --git a/.changeset/constraint-errors.md b/.changeset/constraint-errors.md deleted file mode 100644 index 2fa3420..0000000 --- a/.changeset/constraint-errors.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -Constraint violations are typed errors. `refineConstraints(effect)` turns a `DatabaseError` or `TransactionCommitFailed` for a broken constraint into `UniqueViolation`, `ForeignKeyViolation`, `NotNullViolation` or `CheckViolation`, so a caller can `catchTag` a duplicate key. Each carries the `constraint`, `table` and `column` the server named. Postgres maps SQLSTATE 23505, 23503, 23502 and 23514; ClickHouse maps code 469 (`VIOLATED_CONSTRAINT`) to `CheckViolation`. SQLite and D1 map the `SQLITE_CONSTRAINT_UNIQUE`, `_PRIMARYKEY`, `_FOREIGNKEY`, `_NOTNULL` and `_CHECK` extended codes, with the table and column SQLite names. `constraintViolation(error)` reads one without failing. `DatabaseError` and `TransactionCommitFailed` now keep the server's numeric error code as `errorCode`, which ClickHouse reports and `sqlState` dropped. `ClickhouseHttpClient` classifies code 469 as `ConstraintError` instead of `StatementTimeoutError`. diff --git a/.changeset/database-test-layer.md b/.changeset/database-test-layer.md deleted file mode 100644 index d1b6653..0000000 --- a/.changeset/database-test-layer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`Db.layerTest({ dialect })` from `/database` is a `Database` with no server, for unit tests. It also gives `TestDatabase`: `statements` holds each statement it was sent (SQL and parameters, transaction control included), and `reply(...)` queues the rows, or the driver `SqlError`, the next statements get. With no reply queued, a statement gets `respond(statement)` from the options, or no rows. The layer is `fromSqlClient` over a fake `SqlClient`, so compiling, row decoding by the query's own schema, `requireTenantScope` and transactions run as they do against a server. diff --git a/.changeset/db-stream.md b/.changeset/db-stream.md deleted file mode 100644 index cf018c3..0000000 --- a/.changeset/db-stream.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`Db.stream(query, params?)` and `DatabaseApi.stream` run what `run` takes and return a `Stream` -of its rows. Each row is decoded by the query's row schema when it arrives, so a large result -does not have to fit in memory. A row that does not match fails the stream with -`CompiledQueryDecodeError` and its `rowIndex`. The stream uses the `SqlClient` statement's -`.stream`, reads on the transaction's connection inside `transaction`, and runs in one client -span that counts the rows it emitted. On `sqliteDialect` and `d1Dialect`, whose Effect drivers -have no streaming read, the whole result is read first and then emitted row by row. diff --git a/.changeset/dialect-in-context.md b/.changeset/dialect-in-context.md deleted file mode 100644 index 50515f8..0000000 --- a/.changeset/dialect-in-context.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`CurrentDialect`, a `Context.Reference`, sets the dialect a `compile` writes for when no `options.dialect` is given: `CH.compile(query, params).pipe(Effect.provideService(CH.CurrentDialect, postgresDialect))`. The order is `options.dialect`, then `CurrentDialect`, then the entry's dialect. A compile's state (its dialect, syntax, bound values and failures) is now kept in the context of the fiber that runs it, not in module state, so compiles for two dialects in concurrent fibers stay independent. diff --git a/.changeset/keyset-pagination.md b/.changeset/keyset-pagination.md deleted file mode 100644 index 90379d8..0000000 --- a/.changeset/keyset-pagination.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`/database` adds `paginate(query, { orderBy, after, limit }, params?)`: keyset (cursor) pagination over a built query. It adds a seek on the sort key, the `ORDER BY` and `LIMIT limit + 1`, and returns `{ rows, nextCursor }`, with no `OFFSET`. With one direction the seek is a row comparison, `(a, b) < (?, ?)`; with mixed directions it is the expanded `OR` form, bounded on the first key. The key is one or more selected, non-null aliases; end it with a unique column. The cursor is base64url text, encoded through the key columns' schemas. A cursor that does not decode, or that was written for another sort key, fails with the new `InvalidCursor` error. Postgres, ClickHouse, SQLite and D1. A nullable key is refused at run time too, since dialects sort NULLs differently. diff --git a/.changeset/make-repository.md b/.changeset/make-repository.md deleted file mode 100644 index 6bc120a..0000000 --- a/.changeset/make-repository.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`makeRepository(table, { idColumn, tenantColumn })` in `/database` builds the plain CRUD of a table on the `Database` in context, as `SqlModel.makeRepository` does for a `Model.Class`: `insert`, `insertVoid`, `update`, `updateVoid`, `findById` and `delete`. `findById` and `update` return an `Option`. Inserts take the `tableSchemas(table).insert` row and updates its `update` row. A table with a tenant column gets a `TenantRepository`: every method takes the tenant first, writes or filters the tenant column from it, and compiles as `"single-tenant"`, so a call cannot read or write another tenant's rows. A patch cannot change the id or the tenant column. On ClickHouse, which has no `RETURNING`, use `insertVoid`, `updateVoid` (a mutation that waits), `findById` and `delete` (a lightweight delete that waits). SQLite and D1 support every method. diff --git a/.changeset/observe-exit-redaction.md b/.changeset/observe-exit-redaction.md deleted file mode 100644 index 4616c18..0000000 --- a/.changeset/observe-exit-redaction.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -Statement observation and parameter redaction for `Database`. - -- `observeExit` on `fromSqlClient` is called with every statement once it ends: its `db.*` names, tenant scope, route, duration, row count, or `error.type`. -- `spanParameters: true` puts the bound values on the statement span as `db.query.parameter.`. It is off by default, so spans carry no values. -- A value given as `Redacted` (for example in a column typed `brand(text, Schema.RedactedFromValue(Schema.String))`) is marked in `CompiledQuery.sensitiveParameters`. Spans write it as ``, and `observeExit` gets it still `Redacted`. The driver gets the plain value. This covers every dialect that binds values: Postgres, SQLite and D1. -- `sql\`...\`` binds a `Redacted` value as its plain value. It used to bind the `Redacted` object, which the driver wrote as the text ``. On ClickHouse, which writes values into the statement text, it refuses one. diff --git a/.changeset/pg-json-paths.md b/.changeset/pg-json-paths.md deleted file mode 100644 index 24256bf..0000000 --- a/.changeset/pg-json-paths.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -Typed jsonb paths on Postgres, read off the column's schema. `PG.jsonPath($.payload, "profile", "city")` on a `PG.jsonb(Schema.Struct({ profile: Schema.Struct({ city: Schema.String }) }))` column is an `Expr` that compiles to `("payload" -> 'profile' -> 'city')` and decodes with the field's own schema; only paths the schema has type-check, and a number key indexes an array. `jsonPathText` reads with `->>`, `jsonContains` filters with `@>` (bound as `$n::jsonb`), and `jsonSet` writes one path with `jsonb_set` inside an UPDATE's `set`, without reading the row first. Values compared with a path, and the value of a `jsonSet`, are encoded by the field's schema and bound as JSON. A path the schema does not have, or one through a field whose stored form differs from its type, is a defect at compile time. The path types (`JsonPath`, `JsonPathValue`, `JsonContained`, …) and `jsonPathSchema` are exported for helpers. Postgres only: compiled for ClickHouse, SQLite or D1 these are a defect; SQLite JSON paths are not built yet. diff --git a/.changeset/pipeable-query-api.md b/.changeset/pipeable-query-api.md deleted file mode 100644 index 2bdc583..0000000 --- a/.changeset/pipeable-query-api.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -A query is now `Pipeable`, and a new `Query` namespace on `/clickhouse`, `/postgres` and `/sqlite` gives a data-last form of every `CHQuery` method (`select`, `where`, `groupBy`, `having`, `orderBy`, `limit`, `offset`, `format`, `distinct`, `distinctOn`, the row locks, `route`, `crossTenant`, the table and subquery joins, `withCTE`), next to `from`, `fromQuery` and `fromUnion`: `CH.Query.from(Events).pipe(CH.Query.select(...), CH.Query.where(...))`. Each function is a `dual` that calls the fluent method, so both styles write the same SQL on every dialect, and each also has a data-first form. `$` in a data-last callback is typed from the query that `pipe` carries. diff --git a/.changeset/predicate-facts-joins.md b/.changeset/predicate-facts-joins.md deleted file mode 100644 index abd162c..0000000 --- a/.changeset/predicate-facts-joins.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -A `where` that proves a joined column not NULL now narrows that column for a later `select`. `$.l.Id.isNotNull()` and `$.l.Id.eq(…)` prove it, alone or under `.and(…)`, `and(…)` or the `where` list. The row type and the derived row schema both drop `| null`, so `leftJoin(…).where(($) => [$.l.Id.isNotNull()]).select(($) => ({ id: $.l.Id }))` reads `id: string`, and a NULL in that column fails to decode. `or`, `not`, `when` and a `select` made before the `where` keep `| null`. `Condition` has a second type parameter for these facts; it defaults to none, so existing code does not change. diff --git a/.changeset/relation-filters.md b/.changeset/relation-filters.md deleted file mode 100644 index f321f81..0000000 --- a/.changeset/relation-filters.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -Relation filters and counts in `/database`, as Prisma's `some` / `none` / `every` and `_count`. `findMany`'s `where` gets the relations of its level as a second argument: `where: ($, r) => [r.posts.some(($) => [$.published.eq(false)])]`. `Db.related(schema.users, $)` gives the same filters on a builder query, and `r.posts.count(where?)`, an `Expr` for a `select` or a `where`. The filters are correlated `EXISTS` / `NOT EXISTS` subqueries. `_count: { posts: true }` in `findMany` adds `_count: { posts: number }` to each row, with one grouped statement per level. When both tables declare a tenant column, the subquery matches the outer row's tenant, and a filter under a scoped query stays `"single-tenant"`. Works on Postgres, ClickHouse 25+, SQLite and D1; a `_count` level is split to the dialect's bound-value limit like a level of `with`. See docs/relations.md. diff --git a/.changeset/relational-reads.md b/.changeset/relational-reads.md deleted file mode 100644 index b6c3d54..0000000 --- a/.changeset/relational-reads.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`Db.defineRelations(tables, (r) => ...)` and `Db.findMany(schema.table, { where, orderBy, limit, with })` in `/database` read rows with their related rows, nested to any depth, as Drizzle's relational queries and Prisma's `include` do. `r.one` and `r.many` are checked against the tables. Each level of `with` is one statement, `WHERE IN (...)` over the keys of the level above, decoded through its own row codec, so a nested `timestamptz` is a `DateTime.Utc`. A level whose parent rows carry one tenant is pinned to it, and `requireTenantScope` checks every level. A level whose keys pass the dialect's bound-value limit (D1: 100) is split into several `IN` statements. Works on Postgres, ClickHouse, SQLite and D1. See docs/relations.md and design/relations.md. diff --git a/.changeset/safe-conflict-tenant.md b/.changeset/safe-conflict-tenant.md deleted file mode 100644 index b4daf32..0000000 --- a/.changeset/safe-conflict-tenant.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": patch ---- - -An upsert is single-tenant only when its conflict-update path also proves the existing row's tenant. A tenant-inclusive column target or an explicit tenant predicate can prove it; otherwise `requireTenantScope` refuses the statement before execution. This prevents an insert for one tenant from updating another tenant through a shared conflicting ID. diff --git a/.changeset/safe-decode-errors.md b/.changeset/safe-decode-errors.md deleted file mode 100644 index 56c285a..0000000 --- a/.changeset/safe-decode-errors.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`CompiledQueryDecodeError` is safe to log. It carries `dialect`, `collection`, `column` and `issues` (the Schema path, issue kind and expected type of each issue), and its `message` names them: `Compiled query row 0 did not match its declared output schema: at ["phone"] expected number`. No field holds a row value, so `toString()`, `JSON.stringify`, `util.inspect` and `Cause.pretty` print none. The Schema error moves from `cause` to the non-enumerable `schemaError` property, because it can quote the value (the ClickHouse `DateTime` codec did). New `formatDecodeError(error, { verbose? })` gives a one-line summary for a log; `verbose: true` appends the raw Schema message. Breaking for code that read `error.cause` or built the error by hand: read `error.schemaError`, and pass `issues`. diff --git a/.changeset/safe-sqlite-rebuild.md b/.changeset/safe-sqlite-rebuild.md deleted file mode 100644 index 7e43caf..0000000 --- a/.changeset/safe-sqlite-rebuild.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": patch ---- - -Generated SQLite rebuilds check live inbound foreign keys before copying or dropping the old table. A referenced table is refused instead of letting a transaction-wrapped DROP fire CASCADE or SET NULL actions. The guard works in a D1 batch and rolls back with the migration; ordinary rebuilds still run. Older generated SQL must be reviewed or regenerated before applying it. diff --git a/.changeset/seed-from-schemas.md b/.changeset/seed-from-schemas.md deleted file mode 100644 index c680fb9..0000000 --- a/.changeset/seed-from-schemas.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -A new `/seed` entry makes test and demo rows from a table's column Schemas, through Effect's `Schema` → `Arbitrary`, with no dependency beyond `effect`. `rows(table, { count, seed })` returns decoded rows: the same seed gives the same rows, and each column samples from its own seed, so a new column does not change the others. The built-in column types add the range their SQL type stores (integer widths and safe `SQLite.integer`s, `float4`, `DateTime`, `timestamptz` and `SQLite.timestamp` years, `uuid`, no U+0000 in `text`), and a branded column gets only values of its brand. `columns` overrides a column with a value, an `Arbitrary` (`pick(ids)` for a foreign key) or a function of the row index. `unique` columns, and a Postgres or SQLite table's one-column primary key and `unique` columns, get distinct values. A table with a tenant column needs `tenant` (every row in one tenant, so the insert is `"single-tenant"`) or `crossTenant: true`. `insert(table, options)` writes the rows through `insertInto` on the `Database` in context, `batchSize` rows per statement, and `arbitrary(table)` gives whole rows to `Arbitrary.checkEffect` for property tests. Defaulted and computed columns are left to the database. Failures are a `SeedError`. diff --git a/.changeset/statement-span-only.md b/.changeset/statement-span-only.md deleted file mode 100644 index e0fd60e..0000000 --- a/.changeset/statement-span-only.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -A statement's client span is now the only span it gets. It carries `db.query.text` (the statement as sent), and on failure `db.response.status_code` (the SQLSTATE) beside `error.type`, so the SQL driver's own span, which repeated the query text under the database name, runs untraced. Pass `driverSpans: true` to `fromSqlClient` to keep it as a child for what only the driver records, such as `server.address` or connection waits. `observe` runs inside the statement span, so attributes it annotates land there. Plain SQL (`Db.sql`) spans are named after the statement's leading operation and first table (`UPDATE items`) instead of the database system, with `db.operation.name` and `db.collection.name` to match. diff --git a/.changeset/table-row-schemas.md b/.changeset/table-row-schemas.md deleted file mode 100644 index 774ebe3..0000000 --- a/.changeset/table-row-schemas.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`tableSchemas(table)` derives the Effect Schemas of a table's rows from its columns, on `/clickhouse`, `/postgres` and `/sqlite`: `select` (every column, decoded from the wire), `insert` (computed columns left out, defaulted and nullable ones optional), `update` (every writable column optional), and `json`, `jsonCreate` and `jsonUpdate`, the same rows as plain JSON for an HTTP API. The names follow `Model.Class`. A decoded `insert` or `update` row is a row `insertInto(...).values` or `update(...).set` takes, so request bodies and repositories no longer need Schemas written by hand next to the table. diff --git a/.changeset/text-search.md b/.changeset/text-search.md deleted file mode 100644 index 29bcae6..0000000 --- a/.changeset/text-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -String expressions add `.contains(value)`, `.startsWith(value)` and `.endsWith(value)`: text search that treats the value as text, not as a LIKE pattern. `%`, `_` and `\` in the value are escaped with a backslash. A string is escaped before it is bound; a param or expression is escaped in SQL with `replace()`. Matching is case-sensitive by default and `{ insensitive: true }` ignores case, on every dialect: ClickHouse and Postgres write `LIKE` / `ILIKE`; SQLite and D1, whose `LIKE` ignores ASCII case and which have no `ILIKE`, write `GLOB` (with `*`, `?` and `[` escaped) / `lower(x) LIKE lower(pattern) ESCAPE '\'`. `escapeLike(text)` is the escape step on its own, for a `.like()` pattern. diff --git a/.changeset/type-level-dialect.md b/.changeset/type-level-dialect.md deleted file mode 100644 index 564c0cb..0000000 --- a/.changeset/type-level-dialect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -A query that cannot compile for its target is now a type error, not only a failure at compile time. A built-in function of one dialect (`CH.countIf`, `PG.countIf`, `SQLite.count`, ...) marks its result with `ClickHouseOnly`, `PostgresOnly` or `SqliteOnly` (SQLite and D1 share one set), and `compile`, `compileUnion` and `run` refuse a query with a function of another dialect (`wrongDialect`), or with functions of two (`mixedDialects`). Row locks (`forUpdate`, `forShare`, ...) are refused the same way for ClickHouse, SQLite and D1 (`unsupportedClause`), as only Postgres writes them. The target is `options.dialect`, else the entry's dialect; `fromSqlClient` now returns a `DatabaseApi<"postgres">`, `DatabaseApi<"clickhouse">` or `DatabaseApi<"sqlite">` whose `run` checks it. A column ref now names its source (`SourceEntry`), and a query refuses a column of a source that is not in its FROM or JOIN (`sourceNotInQuery`); a correlated subquery can use the outer query's accessor. The runtime checks stay. Also, `orderBy(($) => [...])` now carries the params of every sort key, not only the first. diff --git a/.changeset/upsert-excluded.md b/.changeset/upsert-excluded.md deleted file mode 100644 index 889ea5e..0000000 --- a/.changeset/upsert-excluded.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`excluded(columns)` and `excludedAll(except?)` build an `onConflictDoUpdate` `set` that copies columns from the row proposed for insertion: `set: excluded(["name", "rent"])` writes `SET "name" = "excluded"."name", "rent" = "excluded"."rent"`. `excludedAll` copies every column the insert writes, except the named ones and the conflict target's columns; a column the rows leave out keeps its old value. Names are checked against the table's writable columns. Setting the tenant column from the proposed row (here or in the callback form) now keeps the rows' tenant scope instead of making the insert cross-tenant. Postgres, SQLite and D1. diff --git a/.changeset/verify-schema.md b/.changeset/verify-schema.md deleted file mode 100644 index 3b1ff03..0000000 --- a/.changeset/verify-schema.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@maple-dev/effect-orm": minor ---- - -`effect-orm verify --schema` compares the table definitions with a live database, with no migrations folder and no ledger. It exits 3 on drift and prints one line per difference (`--json` for machines), so CI fails on a missing column, a type or nullability change, or a missing index. It reads the config's `schema` and `driver` and runs the same comparison as `verify`, for ClickHouse and Postgres; a SQLite or D1 config is refused, since there is no SQLite catalog reader yet. In code: `verifySchema(config, cwd)` from `/kit`, and `Migrate.verifySnapshot(snapshot)` to compare a database with any snapshot. diff --git a/CHANGELOG.md b/CHANGELOG.md index b127f94..607d28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # @maple-dev/effect-orm +## 0.9.0 + +### Minor Changes + +- 3407bad: `Db.findById(query, { key })` and `Db.findAllBy(query, { key })` in `/database` batch lookups by a key with Effect's `RequestResolver`: N concurrent calls send one statement, the query with `key IN (...)` added to its WHERE, and each row decodes through the query's row schema. A batch never mixes `Database`s, transactions or params, so a tenant filter in the query (a value or a `param.*`, given with `.with(params)`) keeps each statement to one tenant, and `requireTenantScope` checks it. On a dialect with a bound-parameter limit (D1: 100) a large batch is split into statements that each fit. +- 4391d05: `Db.transaction({ isolationLevel, accessMode, deferrable })` on Postgres puts the settings in the statement that starts the transaction (`BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY`) through Effect's `SqlClient.BeginTransaction`, one round trip less than the `SET TRANSACTION` it sent before. A dialect gives the BEGIN statement with the new optional `DialectTransactions.beginTransaction`; `postgresDialect` has it, and a dialect without it still sends `SET TRANSACTION`. SQLite keeps its client's own BEGIN. `observe` sees the BEGIN statement, and a failed BEGIN reports it as `DatabaseError.sql`. +- 0315f9e: `/database` adds `runOne`, `runOption` and `runNonEmpty`: `run` with a check on how many rows came back. `runOne` wants exactly one row, `runOption` returns an `Option` and wants at most one, and `runNonEmpty` returns a `NonEmptyReadonlyArray` and wants at least one. A wrong count fails with `ResultCardinalityError` (`expected`, `rows`) in the error channel. The query runs as written, with no `LIMIT` added, so a duplicate row is found, not hidden. +- 8588e88: `PG.column` and `SQLite.column` take client-side defaults as Effects: `defaultFn` runs for each row an insert leaves the column out of, and `onUpdate` for each update that does not set it (and for an insert, when the column has no other default). An Effect can read the `Clock`, `Random` or a service, so `TestClock` and a test layer give exact values in tests. Such a column is optional in the insert row type, and the Effect's requirements become the write's: `Db.run(PG.insertInto(Notes).values(...))` requires `IdGen` when `id` is `{ defaultFn: IdGen.next }`. `Database.run` runs them; `fillDefaults(write)` runs them for a write you compile yourself, and `compile` refuses a write that still leaves one out, and an `INSERT ... SELECT` that does not select such a column. They write no DDL and no migration. `Table`, `PgSchemaTable` and `SqliteSchemaTable` gain two type parameters for the requirements; they default to `never`. +- 24aff1d: Column names apart from keys, on every dialect. `CH.column(type, { name: "org_id" })` now does + what `PG.column` already did: the column is stored as `org_id` and read, written, filtered and + indexed as its key. A new `casing` table option (`"snake_case"` or `"camelCase"`, the `Casing` + type), on `CH.table`, `PG.table` and `SQLite.table`, names every column that gives no `name` from its key, by drizzle-orm's rule: `orgId` is + `org_id`, `HTTPStatus` is `http_status`. The DDL, migrations, inserts, updates, upsert targets, + `returning` and tenant scope use the database name; rows decode under the key. A ClickHouse + materialized view whose target renames a column records a problem, since ClickHouse fills the + target by database name. Two keys that map to one database name are a definition problem. +- d0efaaf: Constraint violations are typed errors. `refineConstraints(effect)` turns a `DatabaseError` or `TransactionCommitFailed` for a broken constraint into `UniqueViolation`, `ForeignKeyViolation`, `NotNullViolation` or `CheckViolation`, so a caller can `catchTag` a duplicate key. Each carries the `constraint`, `table` and `column` the server named. Postgres maps SQLSTATE 23505, 23503, 23502 and 23514; ClickHouse maps code 469 (`VIOLATED_CONSTRAINT`) to `CheckViolation`. SQLite and D1 map the `SQLITE_CONSTRAINT_UNIQUE`, `_PRIMARYKEY`, `_FOREIGNKEY`, `_NOTNULL` and `_CHECK` extended codes, with the table and column SQLite names. `constraintViolation(error)` reads one without failing. `DatabaseError` and `TransactionCommitFailed` now keep the server's numeric error code as `errorCode`, which ClickHouse reports and `sqlState` dropped. `ClickhouseHttpClient` classifies code 469 as `ConstraintError` instead of `StatementTimeoutError`. +- d855cd8: `Db.layerTest({ dialect })` from `/database` is a `Database` with no server, for unit tests. It also gives `TestDatabase`: `statements` holds each statement it was sent (SQL and parameters, transaction control included), and `reply(...)` queues the rows, or the driver `SqlError`, the next statements get. With no reply queued, a statement gets `respond(statement)` from the options, or no rows. The layer is `fromSqlClient` over a fake `SqlClient`, so compiling, row decoding by the query's own schema, `requireTenantScope` and transactions run as they do against a server. +- 9cdbb38: `Db.stream(query, params?)` and `DatabaseApi.stream` run what `run` takes and return a `Stream` + of its rows. Each row is decoded by the query's row schema when it arrives, so a large result + does not have to fit in memory. A row that does not match fails the stream with + `CompiledQueryDecodeError` and its `rowIndex`. The stream uses the `SqlClient` statement's + `.stream`, reads on the transaction's connection inside `transaction`, and runs in one client + span that counts the rows it emitted. On `sqliteDialect` and `d1Dialect`, whose Effect drivers + have no streaming read, the whole result is read first and then emitted row by row. +- 046e378: `CurrentDialect`, a `Context.Reference`, sets the dialect a `compile` writes for when no `options.dialect` is given: `CH.compile(query, params).pipe(Effect.provideService(CH.CurrentDialect, postgresDialect))`. The order is `options.dialect`, then `CurrentDialect`, then the entry's dialect. A compile's state (its dialect, syntax, bound values and failures) is now kept in the context of the fiber that runs it, not in module state, so compiles for two dialects in concurrent fibers stay independent. +- f525d26: `/database` adds `paginate(query, { orderBy, after, limit }, params?)`: keyset (cursor) pagination over a built query. It adds a seek on the sort key, the `ORDER BY` and `LIMIT limit + 1`, and returns `{ rows, nextCursor }`, with no `OFFSET`. With one direction the seek is a row comparison, `(a, b) < (?, ?)`; with mixed directions it is the expanded `OR` form, bounded on the first key. The key is one or more selected, non-null aliases; end it with a unique column. The cursor is base64url text, encoded through the key columns' schemas. A cursor that does not decode, or that was written for another sort key, fails with the new `InvalidCursor` error. Postgres, ClickHouse, SQLite and D1. A nullable key is refused at run time too, since dialects sort NULLs differently. +- b4f5c51: `makeRepository(table, { idColumn, tenantColumn })` in `/database` builds the plain CRUD of a table on the `Database` in context, as `SqlModel.makeRepository` does for a `Model.Class`: `insert`, `insertVoid`, `update`, `updateVoid`, `findById` and `delete`. `findById` and `update` return an `Option`. Inserts take the `tableSchemas(table).insert` row and updates its `update` row. A table with a tenant column gets a `TenantRepository`: every method takes the tenant first, writes or filters the tenant column from it, and compiles as `"single-tenant"`, so a call cannot read or write another tenant's rows. A patch cannot change the id or the tenant column. On ClickHouse, which has no `RETURNING`, use `insertVoid`, `updateVoid` (a mutation that waits), `findById` and `delete` (a lightweight delete that waits). SQLite and D1 support every method. +- ba92520: Statement observation and parameter redaction for `Database`. + + - `observeExit` on `fromSqlClient` is called with every statement once it ends: its `db.*` names, tenant scope, route, duration, row count, or `error.type`. + - `spanParameters: true` puts the bound values on the statement span as `db.query.parameter.`. It is off by default, so spans carry no values. + - A value given as `Redacted` (for example in a column typed `brand(text, Schema.RedactedFromValue(Schema.String))`) is marked in `CompiledQuery.sensitiveParameters`. Spans write it as ``, and `observeExit` gets it still `Redacted`. The driver gets the plain value. This covers every dialect that binds values: Postgres, SQLite and D1. + - `sql\`...\`` binds a `Redacted` value as its plain value. It used to bind the `Redacted` object, which the driver wrote as the text ``. On ClickHouse, which writes values into the statement text, it refuses one. +- 9e8fa2c: Typed jsonb paths on Postgres, read off the column's schema. `PG.jsonPath($.payload, "profile", "city")` on a `PG.jsonb(Schema.Struct({ profile: Schema.Struct({ city: Schema.String }) }))` column is an `Expr` that compiles to `("payload" -> 'profile' -> 'city')` and decodes with the field's own schema; only paths the schema has type-check, and a number key indexes an array. `jsonPathText` reads with `->>`, `jsonContains` filters with `@>` (bound as `$n::jsonb`), and `jsonSet` writes one path with `jsonb_set` inside an UPDATE's `set`, without reading the row first. Values compared with a path, and the value of a `jsonSet`, are encoded by the field's schema and bound as JSON. A path the schema does not have, or one through a field whose stored form differs from its type, is a defect at compile time. The path types (`JsonPath`, `JsonPathValue`, `JsonContained`, …) and `jsonPathSchema` are exported for helpers. Postgres only: compiled for ClickHouse, SQLite or D1 these are a defect; SQLite JSON paths are not built yet. +- 6aeaecb: A query is now `Pipeable`, and a new `Query` namespace on `/clickhouse`, `/postgres` and `/sqlite` gives a data-last form of every `CHQuery` method (`select`, `where`, `groupBy`, `having`, `orderBy`, `limit`, `offset`, `format`, `distinct`, `distinctOn`, the row locks, `route`, `crossTenant`, the table and subquery joins, `withCTE`), next to `from`, `fromQuery` and `fromUnion`: `CH.Query.from(Events).pipe(CH.Query.select(...), CH.Query.where(...))`. Each function is a `dual` that calls the fluent method, so both styles write the same SQL on every dialect, and each also has a data-first form. `$` in a data-last callback is typed from the query that `pipe` carries. +- 26a90c7: A `where` that proves a joined column not NULL now narrows that column for a later `select`. `$.l.Id.isNotNull()` and `$.l.Id.eq(…)` prove it, alone or under `.and(…)`, `and(…)` or the `where` list. The row type and the derived row schema both drop `| null`, so `leftJoin(…).where(($) => [$.l.Id.isNotNull()]).select(($) => ({ id: $.l.Id }))` reads `id: string`, and a NULL in that column fails to decode. `or`, `not`, `when` and a `select` made before the `where` keep `| null`. `Condition` has a second type parameter for these facts; it defaults to none, so existing code does not change. +- 8be9e66: Relation filters and counts in `/database`, as Prisma's `some` / `none` / `every` and `_count`. `findMany`'s `where` gets the relations of its level as a second argument: `where: ($, r) => [r.posts.some(($) => [$.published.eq(false)])]`. `Db.related(schema.users, $)` gives the same filters on a builder query, and `r.posts.count(where?)`, an `Expr` for a `select` or a `where`. The filters are correlated `EXISTS` / `NOT EXISTS` subqueries. `_count: { posts: true }` in `findMany` adds `_count: { posts: number }` to each row, with one grouped statement per level. When both tables declare a tenant column, the subquery matches the outer row's tenant, and a filter under a scoped query stays `"single-tenant"`. Works on Postgres, ClickHouse 25+, SQLite and D1; a `_count` level is split to the dialect's bound-value limit like a level of `with`. See docs/relations.md. +- 1311ae8: `Db.defineRelations(tables, (r) => ...)` and `Db.findMany(schema.table, { where, orderBy, limit, with })` in `/database` read rows with their related rows, nested to any depth, as Drizzle's relational queries and Prisma's `include` do. `r.one` and `r.many` are checked against the tables. Each level of `with` is one statement, `WHERE IN (...)` over the keys of the level above, decoded through its own row codec, so a nested `timestamptz` is a `DateTime.Utc`. A level whose parent rows carry one tenant is pinned to it, and `requireTenantScope` checks every level. A level whose keys pass the dialect's bound-value limit (D1: 100) is split into several `IN` statements. Works on Postgres, ClickHouse, SQLite and D1. See docs/relations.md and design/relations.md. +- 46828b3: `CompiledQueryDecodeError` is safe to log. It carries `dialect`, `collection`, `column` and `issues` (the Schema path, issue kind and expected type of each issue), and its `message` names them: `Compiled query row 0 did not match its declared output schema: at ["phone"] expected number`. No field holds a row value, so `toString()`, `JSON.stringify`, `util.inspect` and `Cause.pretty` print none. The Schema error moves from `cause` to the non-enumerable `schemaError` property, because it can quote the value (the ClickHouse `DateTime` codec did). New `formatDecodeError(error, { verbose? })` gives a one-line summary for a log; `verbose: true` appends the raw Schema message. Breaking for code that read `error.cause` or built the error by hand: read `error.schemaError`, and pass `issues`. +- e3cb32d: A new `/seed` entry makes test and demo rows from a table's column Schemas, through Effect's `Schema` → `Arbitrary`, with no dependency beyond `effect`. `rows(table, { count, seed })` returns decoded rows: the same seed gives the same rows, and each column samples from its own seed, so a new column does not change the others. The built-in column types add the range their SQL type stores (integer widths and safe `SQLite.integer`s, `float4`, `DateTime`, `timestamptz` and `SQLite.timestamp` years, `uuid`, no U+0000 in `text`), and a branded column gets only values of its brand. `columns` overrides a column with a value, an `Arbitrary` (`pick(ids)` for a foreign key) or a function of the row index. `unique` columns, and a Postgres or SQLite table's one-column primary key and `unique` columns, get distinct values. A table with a tenant column needs `tenant` (every row in one tenant, so the insert is `"single-tenant"`) or `crossTenant: true`. `insert(table, options)` writes the rows through `insertInto` on the `Database` in context, `batchSize` rows per statement, and `arbitrary(table)` gives whole rows to `Arbitrary.checkEffect` for property tests. Defaulted and computed columns are left to the database. Failures are a `SeedError`. +- 472cdef: A statement's client span is now the only span it gets. It carries `db.query.text` (the statement as sent), and on failure `db.response.status_code` (the SQLSTATE) beside `error.type`, so the SQL driver's own span, which repeated the query text under the database name, runs untraced. Pass `driverSpans: true` to `fromSqlClient` to keep it as a child for what only the driver records, such as `server.address` or connection waits. `observe` runs inside the statement span, so attributes it annotates land there. Plain SQL (`Db.sql`) spans are named after the statement's leading operation and first table (`UPDATE items`) instead of the database system, with `db.operation.name` and `db.collection.name` to match. +- 79084ae: `tableSchemas(table)` derives the Effect Schemas of a table's rows from its columns, on `/clickhouse`, `/postgres` and `/sqlite`: `select` (every column, decoded from the wire), `insert` (computed columns left out, defaulted and nullable ones optional), `update` (every writable column optional), and `json`, `jsonCreate` and `jsonUpdate`, the same rows as plain JSON for an HTTP API. The names follow `Model.Class`. A decoded `insert` or `update` row is a row `insertInto(...).values` or `update(...).set` takes, so request bodies and repositories no longer need Schemas written by hand next to the table. +- da3869d: String expressions add `.contains(value)`, `.startsWith(value)` and `.endsWith(value)`: text search that treats the value as text, not as a LIKE pattern. `%`, `_` and `\` in the value are escaped with a backslash. A string is escaped before it is bound; a param or expression is escaped in SQL with `replace()`. Matching is case-sensitive by default and `{ insensitive: true }` ignores case, on every dialect: ClickHouse and Postgres write `LIKE` / `ILIKE`; SQLite and D1, whose `LIKE` ignores ASCII case and which have no `ILIKE`, write `GLOB` (with `*`, `?` and `[` escaped) / `lower(x) LIKE lower(pattern) ESCAPE '\'`. `escapeLike(text)` is the escape step on its own, for a `.like()` pattern. +- de36b89: A query that cannot compile for its target is now a type error, not only a failure at compile time. A built-in function of one dialect (`CH.countIf`, `PG.countIf`, `SQLite.count`, ...) marks its result with `ClickHouseOnly`, `PostgresOnly` or `SqliteOnly` (SQLite and D1 share one set), and `compile`, `compileUnion` and `run` refuse a query with a function of another dialect (`wrongDialect`), or with functions of two (`mixedDialects`). Row locks (`forUpdate`, `forShare`, ...) are refused the same way for ClickHouse, SQLite and D1 (`unsupportedClause`), as only Postgres writes them. The target is `options.dialect`, else the entry's dialect; `fromSqlClient` now returns a `DatabaseApi<"postgres">`, `DatabaseApi<"clickhouse">` or `DatabaseApi<"sqlite">` whose `run` checks it. A column ref now names its source (`SourceEntry`), and a query refuses a column of a source that is not in its FROM or JOIN (`sourceNotInQuery`); a correlated subquery can use the outer query's accessor. The runtime checks stay. Also, `orderBy(($) => [...])` now carries the params of every sort key, not only the first. +- 90e2065: `excluded(columns)` and `excludedAll(except?)` build an `onConflictDoUpdate` `set` that copies columns from the row proposed for insertion: `set: excluded(["name", "rent"])` writes `SET "name" = "excluded"."name", "rent" = "excluded"."rent"`. `excludedAll` copies every column the insert writes, except the named ones and the conflict target's columns; a column the rows leave out keeps its old value. Names are checked against the table's writable columns. Setting the tenant column from the proposed row (here or in the callback form) now keeps the rows' tenant scope instead of making the insert cross-tenant. Postgres, SQLite and D1. +- a40e5e3: `effect-orm verify --schema` compares the table definitions with a live database, with no migrations folder and no ledger. It exits 3 on drift and prints one line per difference (`--json` for machines), so CI fails on a missing column, a type or nullability change, or a missing index. It reads the config's `schema` and `driver` and runs the same comparison as `verify`, for ClickHouse and Postgres; a SQLite or D1 config is refused, since there is no SQLite catalog reader yet. In code: `verifySchema(config, cwd)` from `/kit`, and `Migrate.verifySnapshot(snapshot)` to compare a database with any snapshot. + +### Patch Changes + +- 790635b: `ClickhouseHttpClient` escapes bound parameters the way ClickHouse reads them. A string with a + newline, tab or NUL no longer fails with `BAD_QUERY_PARAMETER`, and a backslash is no longer + read as the start of an escape sequence, which silently changed the value. Arrays are sent as + ClickHouse literals, so arrays of strings now bind at all (#38). +- 26fff93: A new guide, `docs/coming-from-drizzle-prisma.md`, maps the common Prisma and Drizzle operations to effect-orm, says what differs by design, and shows a Cloudflare Worker that reaches Postgres through Hyperdrive. Its examples run on PGlite in the doc checks. +- 3b471ab: An upsert is single-tenant only when its conflict-update path also proves the existing row's tenant. A tenant-inclusive column target or an explicit tenant predicate can prove it; otherwise `requireTenantScope` refuses the statement before execution. This prevents an insert for one tenant from updating another tenant through a shared conflicting ID. +- 0f086e1: Generated SQLite rebuilds check live inbound foreign keys before copying or dropping the old table. A referenced table is refused instead of letting a transaction-wrapped DROP fire CASCADE or SET NULL actions. The guard works in a D1 batch and rolls back with the migration; ordinary rebuilds still run. Older generated SQL must be reviewed or regenerated before applying it. + ## 0.8.0 ### Minor Changes diff --git a/package.json b/package.json index c73f2c3..4520548 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@maple-dev/effect-orm", - "version": "0.8.0", + "version": "0.9.0", "description": "Type-safe ClickHouse, Postgres and SQLite queries, result decoding, and reproducible benchmarks for Effect and TypeScript", "keywords": [ "clickhouse",