Skip to content

chore: version packages - #133

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main
Open

github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@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 Databases, 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.<index>. 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 <redacted>, 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 aRedactedvalue as its plain value. It used to bind theRedactedobject, 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<string | null> 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<number> 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 <key> 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.integers, 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 (clickhouse-http: string parameters are sent unescaped, so newlines fail and backslashes change the value #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.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 53de4c53-eeb7-40d1-8fd5-9d2115b07139

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 26eab25 to 2592d99 Compare November 2, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants