Changelog
Unreleased
in progressAdded
-
coredebuggable errors —SCHEMIC_DEBUG=1(or--stack) prints the full stack + the.causechain on any CLI failure (default output unchanged, now with a hint line), and a crashing schema module always reports the FAILING FILE path (original error ascause). -
coretyped cross-connection resolution — (A) a resolver'sctx.connections.<name>handle is now THENABLE to that sibling's FULL ORM client (const main = await ctx.connections.main; main.select(...)) while keeping direct.query; (B) the CHAINED config builder —defineConfig().connection(name, driverFactory, staticConfig | (ctx, args) => config | config[])— where the driver FACTORY itself is the marker and each resolver'sctx.connectionsis contextually typed with the ACCUMULATED prior connections (order = visibility = structural cycle prevention). The literaldefineConfig({ connections })form is unchanged. - table COMPOSITION + derived schemas (cross-driver convention, from real usage):
defineTable(name, s.object()), a public natives.object().fieldsmap (inverse of.shape),TableDef.extend(shape | s.object())typed cast-free column mixins, and derived Standard-Schema input schemas —TableDef.create(defaults/id optional, internal dropped) andTableDef.update(partial, id/readonly excluded) — composable via.partial/.extend/.refine/.or. -
coreORM client P1 foundation —OrmClientBase(disposable bound-client contract:close+[Symbol.asyncDispose], soawait using db = await connect()auto-closes), theasyncDisposablemixin, andresolveConnection(name?)(managed path over the project config). - the bound ORM client (P1 reads) at
@schemic/<driver>/client—connect(name?)MANAGED from the config /connect(client)BYO (close = no-op),db.select(table)pre-bound + awaitable (thenable builder; standalone.run(db)still works), AsyncDisposable; surreal adds a disposableforkSession(). -
coreconfig-as-factory with PARAMETERIZED connections —defineConfigis generic and returns the config with a typedconnect(name, args?): connection names autocomplete (a typo is a compile error),argsis the resolver's own declared 2nd param ((ctx, args) => config | config[], typed per connection; absent for static connections), and each entry's own client type is inferred (heterogeneous-driver projects type per-connection). An ARRAY resolution is bulk-only (migrations enumerate it) —connectthrows a teaching error; pass args selecting one.keyis a display label (not an address); entries may add a dialectlabelhook for bulk reporting. Resolvers can query sibling connections viactx.connectionsat runtime (lazy open through the entries' embedded client openers, cycle-detected, auto-closed). CLI:--args <json>+--arg k=vsugar feed resolver args. -
corethe config loader accepts a NAMEDschemicexport as well as a default — scaffolded form becomesexport const schemic = defineConfig(...)inschemic.config.ts(deterministicimport { schemic }->schemic.connect()auto-import, no file rename); a bareschemic.tsis also discovered (shape-guarded). - ORM P2 WRITES on the bound client — split builders (
db.create(T).content(data),db.update(T, id).merge(...)/.content(...)/.set(...),db.delete(T, id)— pg exportsremove), decoding through the codec channel fail-fast and returning typed rows that CARRY their id. - query Phase 1 READS — richer WHERE operators under the shared cross-driver op contract, pagination, and
one()/get()/count()terminals on the select builder. - table PRESETS —
defineTable.preset(...)reusable table fragments applied via the chained single-argTableDef.use(a).use(b)(ratified cross-driver form; columns + indexes). - TYPED FRAGMENTS (phases 0–3) — the query builder and raw
surqlcompose BOTH ways: eager marker resolution in the tag (TableDef/FunctionDef/surql.$paths splice as text, output is always a plainBoundQuery); builders interpolate as subquery fragments with namespaced binds; raw predicates drop into.where(...); `surql….as<T>()retypes a fragment (the[T]rule); contextual TYPED callbacks on authoring slots (events(e) =>withe.after/e.beforetyped to the table shape, field clauses(f) =>, permissions(p) =>withp.row/p.auth, function bodies with args typed by name); typedOperand<T>—$paramrefs and fragments are legal builder operands (type mismatch = compile error); thesurql.fnbuiltin catalog (live-verified vs 3.1.4) + kind-mapped ref stdlib (u.name.length().gt(3));block()typed statement builder with OBJECT bindings (.let({ n: v }),.for({ item: iter }, body)— the var name is a real property, so rename/find-refs work);$parentcorrelated subqueries;Def.call(args)typed named-arg function calls that also accept refs + builders;ParamRef.as<T>()types an untypedsurql.$param chain for typed operand/call/fn positions (type-only cast). FULL-TS function bodies (zero raw surql):surql.fnreturns retypeableSurql(.as<T>everywhere) withhttp.*<R>response generics, plain object/array args SPLICE embedded refs ({ to: [$email] }) while pure data binds whole, ref PROPERTY PATHS (sv.res.id->$res.id,$parent-aware), andblock().returntakes predicate Exprs. Plus lazy record refss.recordId(() => User)` (kills mutual-link import cycles) and auto-blocking of multi-statement event bodies. - the authoring index re-exports the SDK VALUE surface (
Surreal,RecordId,Table,DateTime,Duration, geometry types, …) so apps never importsurrealdbdirectly — single-instance by construction (the SDK's#privateclasses are nominal; dual copies breakinstanceofand assignability). -
defineSingleton(name, shape, { id? })— one-record tables: emits the LITERAL id type (DEFINE FIELD id … TYPE 'default', DB-enforced), id-optional client sugar (db.get(Config); create/update/delete target THE record), and the literal id survives lower/normalize so it emits, diffs, andpullregeneratesdefineSingleton. -
DEFINE PARAMwith the access-style secret split — an INLINE LITERAL value is fully managed (emit/diff/migrations/pull round-trip); anenv()/secret()value is SECRET and excluded from snapshots/migrations (SurrealDB stores param values readably), deployed out-of-band via newsc param push/check/list(placeholder + binding — the value never appears in DDL text, andpulldrops out-of-band params so values never reach generated source); a bare schema declares presence only. TypedDef.$deep param ref; the def splices$namein templates — and aParamDefin ANY value position (operands, block values, fn/call args, spliced object values) splices$nametyped by itsT(other def kinds in value positions throw guidance instead of serializing to[object Object]). Expression values are rejected by design (the DB stores them EVALUATED — they can't round-trip). -
formatSurql— pretty-prints generated SurrealQL (INFO collapses bodies to one line;pullnow writes statement-per-line with indented nested blocks and wrapped wide objects; idempotent, strings untouched). Wired into pull's function/event/access renders and exported from/driverfor external display panes. Drift-safe by construction (normalize canonicalizes formatting — which now also strips trailing commas; hand-authored ones phantom-diffed before). Every display/output boundary pretty-prints (sc diff, gen, live diff, migration files with line-aware indent) while every COMPARISON stays canonical single-line — snapshots unchanged, no phantom churn on upgrade.
Fixed
-
corethe DEFAULT migrations dir now follows the documented contract — RELATIVE TO THE SCHEMA (its siblingmigrationsdir) instead of a root-fixed./database/migrations. A nested schema (schema: "./src/database/schema") previously split state:initscaffolded the snapshot schema-relative whilegenwrote migrations + a second snapshot at the root default. Standard scaffold layouts are unchanged; an explicitmigrationsoverride still resolves from the root. -
inline()bind rewriting is boundary-aware —$b1no longer corrupts$b10with 10+ binds (latent). -
normalizecanonicalizes FORMATTING of function blocks / event exprs / field clauses / permissions (quote-aware whitespace collapse + INFO-style punctuation spacing + strip;-before-}) — any multi-line-authored surql body previously phantom-diffed forever against INFO's single-line printing; also foldss"..."->'...'on function blocks/events (inlined strings phantom-diffed). - an empty
block()no longer emits invalid{ ; }— it renders the valid no-op{ }(live-verified). -
coremulti-line DDL renders with PER-LINE diff indicators — now that drivers pretty-print display statements, every line of a statement gets its+/-insc diff(a bare continuation line read as context), unified-patch hunk counts count LINES not statements, the rollback block dims/indents per line, and the inline word-diff view collapses whitespace onto one line.
Changed (BREAKING — alpha) Breaking
- dropped the deprecated
$-less field aliases.unique()/.index()— use.$unique()/.$index()(aligns with postgres, already$-only; table-level composite.index(name, fields)unchanged). -
db.queryis SDK-FAITHFUL — awaiting resolves the PER-STATEMENT result array (the old first-statement unwrap silently dropped every result after statement #1);surql<[T1, T2]>typing flows end-to-end, plain strings takedb.query<[User[]]>(...). Correspondingly.as(...)takes a decoder TUPLE mirroring the statements —.as([z.number(), User.object.array()])resolves[number, App[]]positionally; decoders are plain schemas (TableDef.objectis the bridge into Zod land — no bespoke rows decoder), and a decoder-count mismatch is a teaching error. - the
surrealdbSDK moved from a regular dependency to a PEER dependency (app-vs-driver version drift created dual SDK copies whose nominal#privateclasses are incompatible), and `surql….as<T>()replaces the separatesurql.expr` tag (a second tag name broke editor syntax highlighting).
0.1.0-alpha.24
July 1, 2026Added
-
coreKindEngine.excludeFromMigrations— a kind can opt OUT of the migration pipeline entirely. Objects of an excluded kind are skipped by snapshot, diff, gen, and the introspect-compare, so they never enter a migration file nor phantom-diff; the kind is managed out-of-band by the driver's ownsc <kind> …commands. For secret-bearing kinds whose lifecycle doesn't fit committed migrations (SurrealDBDEFINE ACCESS: the DB redacts keys on introspection, and keys rotate independently). -
coreDriverCommandcontract — drivers can contribute dialect-specific CLI commands invoked assc <kind> <verb> [args](e.g. surrealaccess rotate <name>, postgresmatview refresh <name>). Core owns only the general mechanism: it discoversdriver.commands, parses argv (variadic positionals + value/boolean flags), resolves the connection, and dispatches torunwith aCommandContext({conn, config, io with prompt(), secrets}); the driver owns each kind/verb's meaning. -
clithe dispatch forDriverCommands — theschemic/scbin discovers the active driver'scommands(from the project config) and registers each assc <kind> <verb> [args], grouped by kind, with--help. It parses the invocation (variadic positionals + value/boolean flags), opens the connection, and runs the command with itsCommandContext. No project / no driver commands -> no-op (built-in commands unaffected). -
coresecret-bearing DDL foundations (Phase-2a of the DEFINE ACCESS secret contract) —SecretRef+env()/secret()author-time helpers + a pluggableSecretProvider(default readsprocess.env), and a write-onlybindingscarrier ($param->SecretRef) onStatement+Diff. The secret value never lives in the schema, snapshot, or migration: it is resolved at apply through the provider and passed as a bound parameter. Drivers re-exportenv/secret; apply-time resolution + migration persistence land next. -
cores.*fields now expose the Standard Schema~standardinterface (forwarded from the wrapped Zod schema onSFieldBase), so a Schemic field drops straight into any Standard Schema consumer (tRPC, TanStack Form/Router, …) without unwrapping to.schema.validateruns the decode direction (wire -> app). Postgres inherits it via core'sSFieldBase; surrealdb mirrors it on its own base (it does not yet share core'sSFieldBase). -
coreSFieldBase(thes.*base) gains the remaining Zod 4 shared-base methods for closer drop-in parity —nonoptional,exactOptional,isOptional,isNullable,toJSONSchema, adescriptiongetter,register, andspa.@schemic/postgresinherits them immediately (it composes core'sSFieldBase);@schemic/surrealdbmirrors them on its own base.
Fixed
-
clibareschemic/sc(no args) now lists the active driver's contributed commands (sc <kind> <verb>) in its help, likesc --helpalready did. The no-arg help printed before driver commands had registered; registration now runs first.
0.1.0-alpha.22
June 26, 2026Added
- DEFINE ACCESS (Phase 1, non-secret) —
.comment()+.withRefresh()with a full round-trip (emit / introspect / canonical / lower / pull) and a RECORD-is-database-only guard (throws onON NAMESPACE/ON ROOT+ RECORD). Secret-key forms (WITH JWT/ISSUER) deferred to Phase 2. - DEFINE FIELD + INDEX + EVENT + FUNCTION + ACCESS syntax-coverage groups — pin every authorable clause author -> emit (FIELD: TYPE, OVERWRITE / IF NOT EXISTS, FLEXIBLE, REFERENCE ON DELETE, DEFAULT / DEFAULT ALWAYS, READONLY, VALUE, ASSERT, COMPUTED, PERMISSIONS, COMMENT; INDEX: FIELDS, composite, UNIQUE, FULLTEXT analyzer + BM25 + HIGHLIGHTS, HNSW, DISKANN, COUNT, COMMENT; EVENT: WHEN + THEN, OVERWRITE, IF NOT EXISTS, WHEN-omitted, ordered THEN; FUNCTION: args, return type, no-args, OVERWRITE / IF NOT EXISTS, PERMISSIONS FULL/NONE/WHERE, COMMENT). Plus
docs/SYNTAX-COVERAGE.md, the engine-validated grammar + coverage tracker. - DEFINE EVENT
async+commentauthoring —.event()/defineEventnow takeasync?: boolean | { retry?, maxDepth? }andcomment?, emittingASYNC [RETRY] [MAXDEPTH]+COMMENTin grammar order, with a full round-trip (emit strips the materializedRETRY 1/MAXDEPTH 3defaults; introspect +pullregenerate clean authoring). -
defineAnalyzer().function(cb, name?)— optional custom name for the auto-defined function (mirrors.$unique(name)/.$index(name)); the collision check still applies. - author-time DEFINE FIELD validation —
emit()now rejects the combos the SurrealDB parser rejects, with a clear gen-time error instead of a cryptic apply failure ($computedis mutually exclusive with$value/$default/$readonly/$reference/$assertand top-level only;$referencerequires a record-link type and top-level;FLEXIBLEis schemafull-only). Invalid combos that previously emitted bad DDL now throw (they failed at apply anyway).
Changed (BREAKING — alpha) Breaking
-
.$fulltext({ bm25 })narrows fromboolean | [k1, b]to[k1, b]only —bm25: trueis dropped (it was a no-op: BM25 is always-on and the materialized default is stripped on emit, sotrueemitted nothing).bm25now means purely "tune the parameters." -
defineAccess(name)now requires an explicit scope —.onDatabase()/.onNamespace()— enforced at compile time (defineAccessreturns anUnscopedAccessDefexposing only the two scope methods, sodefineAccess("x").bearer(...)is a type error), withemit()still throwing as a runtime backstop. The silentON DATABASEdefault is gone — access scope is a security boundary, so it must be chosen, not defaulted. - renamed
.reference()->.$reference()on field builders — field DDL clauses are$-prefixed (consistent with.$unique()/.$index()/.$default()). Update callers + the pull renderer migrated.
Changed
-
clithe driver loader now requires the@schemic/<driver>/driverentry (dropped the index fallback) — completes the M0.3 package split. Drivers must be >= 0.1.0-alpha.21.
Fixed
-
defineAnalyzer().function()now emits its auto-definedDEFINE FUNCTION—genpreviously produced an analyzer referencing a non-existent<analyzer>_fn. The inlined function is emitted before the analyzer and deduped; a name collision with a differently-bodied function throws (no silent clobber). -
clischemic gennow shows the rendered migration before the title prompt (you review the actual DDL while naming it), instead of after writing.
0.1.0-alpha.21
June 23, 2026Added
-
defineFunction(args).returns(R).call(db, args)— the (B) DB-functions-as-code call site over core'scallFunction: args encoded via the param schemas, result decoded through.returns(R)(so.returns(s.datetime()).call(db)yields a realDate). First driver impl of thecallablecapability.
Changed (BREAKING — alpha) Breaking
- the authoring index (
@schemic/<driver>) is now side-effect-free —s.*/define*/surqlonly. Moved out: - the connection factory + connection types →@schemic/<driver>/connection(surrealConnection,postgresConnection/PgConn/pgSql). Updateschemic.config.tsimports. - theDriverimpl +emit*/lower/introspect+ theregisterDriverside-effect →@schemic/<driver>/driver(engine/CLI-only). So importings.*no longer drags the diff/emit engine or registers the driver. The query builder stays at@schemic/<driver>/query. (surrealdb also made its field registriesglobalThissingletons so the index and/drivermodule instances share state.)
Changed
-
clithe driver loader resolves a driver via its@schemic/<driver>/driversubpath first (falling back to the package index for not-yet-split drivers).
0.1.0-alpha.20
June 23, 2026Added
-
corecallFunctionin@schemic/core/query— invoke a defined DB function via thecallablecapability and decode the result through.returns(R)(the neutral half of the query layer's (B).call()).CallableFunctions.invokenow returns the raw function result forRto decode (no driver implementedcallableyet, so no break). -
clischemic pull --watch— poll the live DB (--interval, default 2s) and re-pull as it changes (preview, or apply with--write); a DB-poll loop, not fsWatch (which would self-trigger on pull's own file writes).
Changed
-
pullrenders analyzer filters via the typed.filters(f => [...])builder callback instead of string literals (round-trips identically).
Fixed
-
defineAnalyzer().filters()no longer dedupes — duplicate filters pass through verbatim (follow-up to the alpha.19 tokenizers fix; drops the now-unuseduniqueClausehelper).
0.1.0-alpha.19
June 23, 2026Fixed
-
defineAnalyzer().tokenizers()no longer dedupes — duplicate tokenizers pass through verbatim (TOKENIZERS blank, blank).
0.1.0-alpha.18
June 23, 2026Added
-
core@schemic/core/query— the neutral query toolkit driver builders compose:FieldRefBase(+brandRef),Project<P>projection inference,projectionSchema/decodeProjection. Plus thecallablecapability on theDrivercontract. - typed single-table
select()query builder at@schemic/<driver>/query(where/orderBy/limit/.returnprojection; decode-by-default;.raw()opts out) — the driver-owned builder composing the core toolkit. - full
DEFINE ANALYZERcoverage + a fluentdefineAnalyzerbuilder (tokenizers, filters, function, comment).
Changed (BREAKING — alpha, no stable consumers) Breaking
-
.flexible()/.loose()/.strict()are now object-only — a compile error on non-object fields (was a silent no-op).defineAnalyzer's config-object form is dropped in favor of the fluent builder.
Older versions (pre-alpha.18) → repo CHANGELOG