Extending Egon
Where a new type, operator or database goes, and what has to come with it. Read Architecture first.
A type
In an application, a new type does not need a change to the library: defineType and parameterized are public. See Custom types and the money recipe.
A built-in type (one reached through t):
- Implement it in
packages/core/src/types/(scalars.ts,temporal.ts,composite.ts) as aFieldType:castnever throws and returns{ success, data }or{ success: false, message };fromDbandtoDbare the shared identity function when no conversion is needed, so the decoder can skip them. - Add the builder to
tinschema/field.ts. - Give its logical column type (
DbType) a SQL type in each dialect table (compiler/dialect.ts) and in the migration column builders (migration/builder.ts), and a driver codec in each dialect package (values.ts) where the driver's value differs. - Map it in
@egon-orm/zod(packages/zod/src/to-zod.ts). - Tests: property tests for
cast/toDb/fromDbround trips, type tests for the TypeScript type, and a row in the write and read conformance suites so every database stores and reads it.
An operator or function
- The method on expressions goes in
query/expr.ts(or a free function inquery/functions.ts), typed so that it exists only on expressions of a fitting type and rejectsnullwhere SQL would. - Its node: an existing kind with a new operator name (
BinaryOpincontracts/query.ts), or a new node kind. A new name in the contracts is an addition; a change to an existing node is a contract change and needs an ADR. - Its SQL in each dialect table (
operators,functions, …). A database without it leaves it out of its table, so that the compiler refuses it withQueryError, and gets a capability if the refusal should be explained (Dialect support). - Tests: a case per dialect in
compiler/sql-cases*.ts(run against Postgres bysql.itest.ts), precedence where it is written between operands, and a type test with positive and negative cases.
A dialect
A dialect is a package like packages/sqlite, plus tables in core:
- Contracts.
Dialectincontracts/adapter.tslists the databases; a new value is a contract change and needs an ADR, as do new capabilities (ADR 0006 records those of the tier 2 dialects). - Compiler table in
compiler/dialect.ts: quoting, placeholder style, operators, functions, types, lock clauses. The emitter has no branch on a dialect's name; a difference is a table entry or a capability (scripts/check-dialect-conditionals.mjsenforces this). - Migration table in
migration/dialect.ts: DDL features, column types, literals, the runner's lock. - The package: options, a driver wrapper (
driver.ts), value codecs (values.ts), error normalisation intoDatabaseErrorandConstraintError(errors.ts; the function is internal to the package and passed tocreateAdapterasnormalizeError), the capabilities (dialect.ts), andindex.ts, which callscreateAdapter.createAdapteris internal to the Egon packages: its driver contract is not exported, so a dialect is written inside this repository, not as a third-party package. - Conformance. Each suite in
packages/core/test/conformance/(adapter, read, write, preload, nested, transaction, compiler, migration) runs against the package through a profile file in itstest/directory. A case that needs a missing capability is skipped by name. - Docs. Add the dialect to
scripts/dialect-support.tsand runpnpm docs:dialects;pnpm lintfails when the page is stale.

