Repository navigation
chore: version packages - #133
Open
github-actions[bot] wants to merge 1 commit into
Open
github-actions[bot] wants to merge 1 commit into
github-actions[bot] wants to merge 1 commit into
Conversation
|
Important Review skippedBot user detected. To trigger a single review, invoke the
|
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
November 2, 2026 17:58
26eab25 to
2592d99
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 })andDb.findAllBy(query, { key })in/databasebatch lookups by a key with Effect'sRequestResolver: N concurrent calls send one statement, the query withkey IN (...)added to its WHERE, and each row decodes through the query's row schema. A batch never mixesDatabases, transactions or params, so a tenant filter in the query (a value or aparam.*, given with.with(params)) keeps each statement to one tenant, andrequireTenantScopechecks 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'sSqlClient.BeginTransaction, one round trip less than theSET TRANSACTIONit sent before. A dialect gives the BEGIN statement with the new optionalDialectTransactions.beginTransaction;postgresDialecthas it, and a dialect without it still sendsSET TRANSACTION. SQLite keeps its client's own BEGIN.observesees the BEGIN statement, and a failed BEGIN reports it asDatabaseError.sql.0315f9e:
/databaseaddsrunOne,runOptionandrunNonEmpty:runwith a check on how many rows came back.runOnewants exactly one row,runOptionreturns anOptionand wants at most one, andrunNonEmptyreturns aNonEmptyReadonlyArrayand wants at least one. A wrong count fails withResultCardinalityError(expected,rows) in the error channel. The query runs as written, with noLIMITadded, so a duplicate row is found, not hidden.8588e88:
PG.columnandSQLite.columntake client-side defaults as Effects:defaultFnruns for each row an insert leaves the column out of, andonUpdatefor each update that does not set it (and for an insert, when the column has no other default). An Effect can read theClock,Randomor a service, soTestClockand 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(...))requiresIdGenwhenidis{ defaultFn: IdGen.next }.Database.runruns them;fillDefaults(write)runs them for a write you compile yourself, andcompilerefuses a write that still leaves one out, and anINSERT ... SELECTthat does not select such a column. They write no DDL and no migration.Table,PgSchemaTableandSqliteSchemaTablegain two type parameters for the requirements; they default tonever.24aff1d: Column names apart from keys, on every dialect.
CH.column(type, { name: "org_id" })now doeswhat
PG.columnalready did: the column is stored asorg_idand read, written, filtered andindexed as its key. A new
casingtable option ("snake_case"or"camelCase", theCasingtype), on
CH.table,PG.tableandSQLite.table, names every column that gives nonamefrom its key, by drizzle-orm's rule:orgIdisorg_id,HTTPStatusishttp_status. The DDL, migrations, inserts, updates, upsert targets,returningand tenant scope use the database name; rows decode under the key. A ClickHousematerialized 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 aDatabaseErrororTransactionCommitFailedfor a broken constraint intoUniqueViolation,ForeignKeyViolation,NotNullViolationorCheckViolation, so a caller cancatchTaga duplicate key. Each carries theconstraint,tableandcolumnthe server named. Postgres maps SQLSTATE 23505, 23503, 23502 and 23514; ClickHouse maps code 469 (VIOLATED_CONSTRAINT) toCheckViolation. SQLite and D1 map theSQLITE_CONSTRAINT_UNIQUE,_PRIMARYKEY,_FOREIGNKEY,_NOTNULLand_CHECKextended codes, with the table and column SQLite names.constraintViolation(error)reads one without failing.DatabaseErrorandTransactionCommitFailednow keep the server's numeric error code aserrorCode, which ClickHouse reports andsqlStatedropped.ClickhouseHttpClientclassifies code 469 asConstraintErrorinstead ofStatementTimeoutError.d855cd8:
Db.layerTest({ dialect })from/databaseis aDatabasewith no server, for unit tests. It also givesTestDatabase:statementsholds each statement it was sent (SQL and parameters, transaction control included), andreply(...)queues the rows, or the driverSqlError, the next statements get. With no reply queued, a statement getsrespond(statement)from the options, or no rows. The layer isfromSqlClientover a fakeSqlClient, so compiling, row decoding by the query's own schema,requireTenantScopeand transactions run as they do against a server.9cdbb38:
Db.stream(query, params?)andDatabaseApi.streamrun whatruntakes and return aStreamof 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
CompiledQueryDecodeErrorand itsrowIndex. The stream uses theSqlClientstatement's.stream, reads on the transaction's connection insidetransaction, and runs in one clientspan that counts the rows it emitted. On
sqliteDialectandd1Dialect, whose Effect drivershave no streaming read, the whole result is read first and then emitted row by row.
046e378:
CurrentDialect, aContext.Reference, sets the dialect acompilewrites for when nooptions.dialectis given:CH.compile(query, params).pipe(Effect.provideService(CH.CurrentDialect, postgresDialect)). The order isoptions.dialect, thenCurrentDialect, 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:
/databaseaddspaginate(query, { orderBy, after, limit }, params?): keyset (cursor) pagination over a built query. It adds a seek on the sort key, theORDER BYandLIMIT limit + 1, and returns{ rows, nextCursor }, with noOFFSET. With one direction the seek is a row comparison,(a, b) < (?, ?); with mixed directions it is the expandedORform, 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 newInvalidCursorerror. 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/databasebuilds the plain CRUD of a table on theDatabasein context, asSqlModel.makeRepositorydoes for aModel.Class:insert,insertVoid,update,updateVoid,findByIdanddelete.findByIdandupdatereturn anOption. Inserts take thetableSchemas(table).insertrow and updates itsupdaterow. A table with a tenant column gets aTenantRepository: 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 noRETURNING, useinsertVoid,updateVoid(a mutation that waits),findByIdanddelete(a lightweight delete that waits). SQLite and D1 support every method.ba92520: Statement observation and parameter redaction for
Database.observeExitonfromSqlClientis called with every statement once it ends: itsdb.*names, tenant scope, route, duration, row count, orerror.type.spanParameters: trueputs the bound values on the statement span asdb.query.parameter.<index>. It is off by default, so spans carry no values.Redacted(for example in a column typedbrand(text, Schema.RedactedFromValue(Schema.String))) is marked inCompiledQuery.sensitiveParameters. Spans write it as<redacted>, andobserveExitgets it stillRedacted. 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 aPG.jsonb(Schema.Struct({ profile: Schema.Struct({ city: Schema.String }) }))column is anExpr<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.jsonPathTextreads with->>,jsonContainsfilters with@>(bound as$n::jsonb), andjsonSetwrites one path withjsonb_setinside an UPDATE'sset, without reading the row first. Values compared with a path, and the value of ajsonSet, 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, …) andjsonPathSchemaare 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 newQuerynamespace on/clickhouse,/postgresand/sqlitegives a data-last form of everyCHQuerymethod (select,where,groupBy,having,orderBy,limit,offset,format,distinct,distinctOn, the row locks,route,crossTenant, the table and subquery joins,withCTE), next tofrom,fromQueryandfromUnion:CH.Query.from(Events).pipe(CH.Query.select(...), CH.Query.where(...)). Each function is adualthat 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 thatpipecarries.26a90c7: A
wherethat proves a joined column not NULL now narrows that column for a laterselect.$.l.Id.isNotNull()and$.l.Id.eq(…)prove it, alone or under.and(…),and(…)or thewherelist. The row type and the derived row schema both drop| null, soleftJoin(…).where(($) => [$.l.Id.isNotNull()]).select(($) => ({ id: $.l.Id }))readsid: string, and a NULL in that column fails to decode.or,not,whenand aselectmade before thewherekeep| null.Conditionhas 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'ssome/none/everyand_count.findMany'swheregets 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, andr.posts.count(where?), anExpr<number>for aselector awhere. The filters are correlatedEXISTS/NOT EXISTSsubqueries._count: { posts: true }infindManyadds_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_countlevel is split to the dialect's bound-value limit like a level ofwith. See docs/relations.md.1311ae8:
Db.defineRelations(tables, (r) => ...)andDb.findMany(schema.table, { where, orderBy, limit, with })in/databaseread rows with their related rows, nested to any depth, as Drizzle's relational queries and Prisma'sincludedo.r.oneandr.manyare checked against the tables. Each level ofwithis one statement,WHERE <key> IN (...)over the keys of the level above, decoded through its own row codec, so a nestedtimestamptzis aDateTime.Utc. A level whose parent rows carry one tenant is pinned to it, andrequireTenantScopechecks every level. A level whose keys pass the dialect's bound-value limit (D1: 100) is split into severalINstatements. Works on Postgres, ClickHouse, SQLite and D1. See docs/relations.md and design/relations.md.46828b3:
CompiledQueryDecodeErroris safe to log. It carriesdialect,collection,columnandissues(the Schema path, issue kind and expected type of each issue), and itsmessagenames them:Compiled query row 0 did not match its declared output schema: at ["phone"] expected number. No field holds a row value, sotoString(),JSON.stringify,util.inspectandCause.prettyprint none. The Schema error moves fromcauseto the non-enumerableschemaErrorproperty, because it can quote the value (the ClickHouseDateTimecodec did). NewformatDecodeError(error, { verbose? })gives a one-line summary for a log;verbose: trueappends the raw Schema message. Breaking for code that readerror.causeor built the error by hand: readerror.schemaError, and passissues.e3cb32d: A new
/seedentry makes test and demo rows from a table's column Schemas, through Effect'sSchema→Arbitrary, with no dependency beyondeffect.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 safeSQLite.integers,float4,DateTime,timestamptzandSQLite.timestampyears,uuid, no U+0000 intext), and a branded column gets only values of its brand.columnsoverrides a column with a value, anArbitrary(pick(ids)for a foreign key) or a function of the row index.uniquecolumns, and a Postgres or SQLite table's one-column primary key anduniquecolumns, get distinct values. A table with a tenant column needstenant(every row in one tenant, so the insert is"single-tenant") orcrossTenant: true.insert(table, options)writes the rows throughinsertIntoon theDatabasein context,batchSizerows per statement, andarbitrary(table)gives whole rows toArbitrary.checkEffectfor property tests. Defaulted and computed columns are left to the database. Failures are aSeedError.472cdef: A statement's client span is now the only span it gets. It carries
db.query.text(the statement as sent), and on failuredb.response.status_code(the SQLSTATE) besideerror.type, so the SQL driver's own span, which repeated the query text under the database name, runs untraced. PassdriverSpans: truetofromSqlClientto keep it as a child for what only the driver records, such asserver.addressor connection waits.observeruns 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, withdb.operation.nameanddb.collection.nameto match.79084ae:
tableSchemas(table)derives the Effect Schemas of a table's rows from its columns, on/clickhouse,/postgresand/sqlite:select(every column, decoded from the wire),insert(computed columns left out, defaulted and nullable ones optional),update(every writable column optional), andjson,jsonCreateandjsonUpdate, the same rows as plain JSON for an HTTP API. The names followModel.Class. A decodedinsertorupdaterow is a rowinsertInto(...).valuesorupdate(...).settakes, 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 withreplace(). Matching is case-sensitive by default and{ insensitive: true }ignores case, on every dialect: ClickHouse and Postgres writeLIKE/ILIKE; SQLite and D1, whoseLIKEignores ASCII case and which have noILIKE, writeGLOB(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 withClickHouseOnly,PostgresOnlyorSqliteOnly(SQLite and D1 share one set), andcompile,compileUnionandrunrefuse 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 isoptions.dialect, else the entry's dialect;fromSqlClientnow returns aDatabaseApi<"postgres">,DatabaseApi<"clickhouse">orDatabaseApi<"sqlite">whoserunchecks 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)andexcludedAll(except?)build anonConflictDoUpdatesetthat copies columns from the row proposed for insertion:set: excluded(["name", "rent"])writesSET "name" = "excluded"."name", "rent" = "excluded"."rent".excludedAllcopies 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 --schemacompares 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 (--jsonfor machines), so CI fails on a missing column, a type or nullability change, or a missing index. It reads the config'sschemaanddriverand runs the same comparison asverify, 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, andMigrate.verifySnapshot(snapshot)to compare a database with any snapshot.Patch Changes
ClickhouseHttpClientescapes bound parameters the way ClickHouse reads them. A string with anewline, tab or NUL no longer fails with
BAD_QUERY_PARAMETER, and a backslash is no longerread 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).
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.requireTenantScoperefuses the statement before execution. This prevents an insert for one tenant from updating another tenant through a shared conflicting ID.