A TypeScript generator for sqlc, written directly in WebAssembly text (WAT). The WASI plugin turns sqlc's analyzed schema, queries, and options into TypeScript for Node, Bun, and Deno.
WABT assembles the source and Binaryen optimizes it. Deno tools pack constant strings and TypeScript support templates into the module. Those templates become generated files; the plugin runs WAT code. The module uses fixed 64 MiB memory, explicit bounds checks, and no allocator or garbage collector.
The plugin replaces this repository's Go implementation. The release filename and sqlc options stay the same; the Go library and process plugin have been removed. See migration.
Each pushed v* tag publishes a WASI plugin and its SHA-256 checksum to
GitHub Releases.
Copy the sqlc configuration from the release notes: it includes the exact
download URL and checksum. No local plugin build is needed.
For an existing config, replace its wasm section with:
wasm:
url: https://github.com/bonakodo/sqlc-gen-typescript-native/releases/download/vX.Y.Z/sqlc-gen-typescript-native.wasm
sha256: REPLACE_WITH_THE_RELEASE_SHA256Replace vX.Y.Z with the release tag and copy the checksum from its notes or
the first field in sqlc-gen-typescript-native.wasm.sha256. Run
sqlc generate. Keep the version URL and matching checksum pinned in your
config.
Install Deno 2.9 or later, then run:
deno task tools
deno task builddeno task tools downloads WABT 1.0.41 and Binaryen 132, verifies their SHA-256
hashes, and extracts them into the ignored bin/.tools directory. It supports
Linux on arm64 and amd64 and macOS on arm64. On macOS amd64, supply your own
WABT 1.0.41 executables. It changes no system settings. Ordinary builds do not
install native tools. Deno may fetch standard-library packages on first use;
deno.lock pins those dependencies. Set WAT2WASM, WASM2WAT, WASMOPT, or
DENO to select explicit executable paths.
The build writes these files under bin/:
sqlc-gen-typescript-native.wasm: optimized WASI plugin.sqlc-gen-typescript-native.wasm.sha256: release checksum.sqlc-gen-typescript-native.raw.wasm: direct WABT assembly used by tests.sqlc-gen-typescript-native.wat: combined source with unpacked static data.sqlc-gen-typescript-native.packed.wat: combined source with packed data.
The default packs static data losslessly and restores it at startup.
PACK_DATA=0 deno task build leaves it unpacked for inspection and comparison.
The build needs no Go, TinyGo, protoc, Node, Bun, database, or TypeScript compiler.
Use the absolute path to the local WASM file:
version: "2"
plugins:
- name: typescript
wasm:
url: file:///absolute/path/sqlc-gen-typescript-native/bin/sqlc-gen-typescript-native.wasm
sql:
- engine: postgresql
schema: schema.sql
queries: query.sql
codegen:
- plugin: typescript
out: db
options:
runtime: node
driver: pgRun sqlc generate. A local file:// plugin does not need a checksum on recent
sqlc versions; pin a published artifact with its SHA-256 checksum when sharing a
config.
The plugin reads one protobuf request from stdin and writes one response to stdout, then exits. Diagnostics go to stderr. Validation and capacity errors produce a diagnostic and no partial response; a host I/O error can interrupt transmission. The protocol schema tracks sqlc v1.31.1.
| Engine | Driver | Node | Bun | Deno |
|---|---|---|---|---|
| PostgreSQL | pg |
yes | yes | yes |
| PostgreSQL | postgres |
yes | yes | yes |
| MySQL | mysql2 |
yes | yes | yes |
| SQLite | better-sqlite3 |
yes | yes | yes |
| SQLite | @bonakodo/sqlite |
— | — | yes |
runtime defaults to node; driver is required. Engine/driver mismatches
fail before sqlc receives output files. Generated imports use bare package
names, including @bonakodo/sqlite and mysql2/promise. Put full Deno
specifiers and version pins in your application's deno.json imports, for
example:
{
"imports": {
"postgres": "npm:postgres@3.4.9"
}
}Registry-prefixed and versioned Deno driver options remain accepted, but only
select the driver; the import map controls its version. Pin Node and Bun
dependencies in package.json.
For pg and better-sqlite3, install their @types packages. Queries that use
PostgreSQL intervals with pg also import the postgres-interval type package;
add it as a direct dependency when your package manager restricts transitive
imports. Deno output includes bare @deno-types hints; map @types/pg and
@types/better-sqlite3 to their full specifiers in imports when using those
drivers. Map postgres-interval there when using PostgreSQL intervals with
pg. Generated Node and Bun modules use .ts relative imports; use a runtime
that supports them, or TypeScript's rewriteRelativeImportExtensions when
emitting JavaScript. See the checked configuration in
tests/integration/tsconfig.json. The test and example import maps resolve
@bonakodo/sqlite to the published jsr:@bonakodo/sqlite@0.1.0 package.
Generated code uses the bare package name so applications can supply their own
import map. The SQLite runtime configures safeIntegers() and raw() per
statement, then calls get(), all(), or run(). For @bonakodo/sqlite, it
uses Symbol.dispose on eviction, errors, clearing, and uncached completion.
Connection settings stay unchanged; JSON stays text or bytes without driver
parsing.
Set DENO_SQLITE_PATH to an absolute SQLite shared library path before running
the tests or example. On macOS with Homebrew SQLite:
export DENO_SQLITE_PATH="$(brew --prefix sqlite)/lib/libsqlite3.dylib"On other platforms, use the installed .so or .dll path. Runtime calls need
--allow-env=DENO_SQLITE_PATH --allow-ffi; the example also needs read access
to its schema. The package does not download a native library.
For -- name: GetAuthor :one, a query module exports getAuthorQuery,
GetAuthorArgs, GetAuthorRow, and getAuthor(database, args). Queries
without arguments omit args. Pass a driver connection, pool, or transaction
handle; the generated code does not open or close connections.
- Each SQL source produces a matching
*_sql.tsmodule, retaining subdirectories. models.tscontains table interfaces.- Catalog enums produce
enums.tswith value arrays and their derived types. index.tsexports models, enums, and query namespaces.query_helpers.tsshares argument and row types, parameter encoders, row decoders, and field metadata across SQL modules. Query modules keep their public names and interfaces. Internal names use a short role and a hash of the field contract, so unrelated queries usually leave them unchanged. Full contract checks govern sharing; rare hash collisions and conflicting imported type names can still rename their collision group.runtime_common.tscontains helpers used across engines. A second file,runtime_sqlite.ts,runtime_postgresql.ts, orruntime_mysql.ts, contains the selected engine's conversions, query execution helpers, and driver connection types. Generation includes only the helpers and value kinds the queries need. Query functions share statement handling and pass encoded parameter arrays directly when their order matches the SQL bindings.codec_error.tscontains structured conversion errors without bound or stored values. Conversions assemble diagnostic context only when they fail.- Server-driver output includes
json.tswith reusable JSON value types. - Snake_case field names become camelCase; existing camelCase names retain their spelling in every driver. Unsafe names use quoted properties.
- Output order and names are stable. Generation does not change the request.
| Command | Result |
|---|---|
:one |
Row or null; server drivers require exactly one row, SQLite returns the first |
:many |
Row array; no matches returns [] |
:exec |
void |
:execlastid |
MySQL: number, number | string, or string according to big-number options; Deno SQLite: number in driver mode, bigint in native mode; better-sqlite3: bigint |
:execrows |
bigint count |
:execresult |
{ rowsAffected: bigint; lastInsertId: bigint | null }; PostgreSQL insert ID is null |
Functions return promises except @bonakodo/sqlite functions, which run
synchronously. Native SQLite mode also makes better-sqlite3 functions
synchronous, so both can run inside synchronous transaction callbacks.
PostgreSQL rejects :execlastid; use INSERT ... RETURNING with :one. The
older better-sqlite3 generator rejected :execlastid; this plugin adds it with
a bigint result. Unsupported annotations produce errors.
The SQLite drivers use positional rows and checked conversions. Deno statements
use a bounded statement cache; better-sqlite3 owns statement disposal. Integer
reads use per-statement settings without changing the connection. MySQL uses its
promise execute() API to bind values safely, including SQL text containing
?. PostgreSQL keeps repeated parameters in one bind slot so server type
inference stays consistent. Compiler-marked SQLite/MySQL slices expand at
runtime; empty slices emit NULL. PostgreSQL arrays bind as one parameter; use
ANY($1) for array membership. PostgreSQL sqlc.slice produces a generation
error with this guidance instead of emitting a query that fails at runtime.
@bonakodo/sqlite keeps up to 16 statements per connection, shared by query
files through their generated runtime_sqlite.ts. Set the limit on each actual
Database instance; use zero for workloads with little SQL reuse:
import { configureStatementCache, clearStatementCache } from "./db/runtime_sqlite.ts";
configureStatementCache(database, 16); // 0 disables generator-owned caching
try {
// Use generated queries, including synchronous transaction callbacks.
} finally {
try {
clearStatementCache(database);
} finally {
database.close();
}
}The cache uses weak connection keys, exact SQL and row mode, and LRU eviction. It configures integer and row settings once per prepared statement. Values and results never enter cache keys or retained entries; the driver resets statements and clears transient bindings before returning. Slices share this same limit, so each distinct expanded SQL consumes an entry. The limit counts statements, not bytes: long SQL and many placeholders can use more native memory.
Arguments validate before acquisition. Each call owns its statement through
execution, conversion, and cleanup. Nested calls use separate temporary
statements when the matching entry is active or every slot is active; they
finalize at call exit. The cache never waits, grows its retained entry count
under pressure, or retries failed SQL. Execution, reset, and conversion errors
discard the statement. Cleanup errors propagate with the existing finally
precedence. Clearing tries all idle statements and throws the first cleanup
error. A driver-rejected disposal stays tracked for a later clear; active
statements finalize when their calls finish. Lowering a limit
keeps active handles until their calls finish, then trims to the new limit.
Clear before connection close or application-controlled changes that invalidate
statements. Clearing after close is also safe: the driver's close() already
finalizes its native handles. A later query uses the driver's closed-connection
error; a new Database receives its own cache. SQLite's prepare-v2 schema rebuild
handles ordinary schema changes. There is no automatic replay after a stale
handle or schema error. Commit and rollback do not clear the cache. The cache
owns only statements it prepares and never touches caller-owned iterators.
Separate generated outputs each have their own runtime; reuse one output when
query modules must share the limit. Workers and processes have separate handles.
Other drivers keep their supported ownership rules: better-sqlite3 exposes no
explicit statement finalization API, so this generator adds no retained cache.
MySQL2's execute() owns its per-connection LRU; configure maxPreparedStatements
on its connections or pool (zero is not a disable setting). pg uses unnamed
queries and postgres.js uses its existing unsafe() preparation policy. The
generator does not name, deallocate, or replay PostgreSQL statements. Pool
transactions still require a checked-out connection or transaction handle.
See driver preparation and
measurements for tested limits, versions, and tradeoffs.
Set emit_query_factory: true to also bind queries to a connection once:
import { createQueries } from "./db/index.ts";
const queries = createQueries(database);
const author = queries.query.getAuthor({ id: 1n });
database.transaction(() => {
const transactionQueries = createQueries(database);
transactionQueries.query.renameAuthor({ id: 1n, name: "Ada" });
})();Each root factory key comes from a SQL source path: query.sql becomes query,
and billing/payments.sql becomes billingPayments. A query module also
exports its own createQueries(database) factory, with a suffix if a query
already owns that name. Standalone functions keep their names and signatures.
Factories keep synchronous SQLite calls and asynchronous server calls unchanged;
they do not open, close, cache, or start transactions. Bind the transaction
handle supplied by your driver when it differs from the main connection.
types_only omits factories.
| Option | Default | Effect |
|---|---|---|
runtime |
node |
node, bun, or deno |
driver |
required | Driver from the table above |
sqlite_type_mode |
driver |
Older plugin types, or native value conversions |
types_only |
false |
Models, query types, and enum value arrays; no SQL, functions, codecs, or runtime module |
emit_null_as_undefined |
false |
Absent rows and nullable results use undefined; nullable args may be omitted |
optional_nullable_args |
unset | Controls nullable argument optionality independently; unset preserves emit_null_as_undefined behavior |
emit_query_factory |
false |
Add connection-bound factories to query modules and the index |
emitLemmaScript |
false |
Emit optional LemmaScript external query contracts and query metadata; does not prove SQL authorization |
emit_sql_as_const |
true |
Export SQL constants; false keeps SQL private |
mysql2.support_big_numbers |
false |
BIGINT values can be number | string |
mysql2.big_number_strings |
false |
With support_big_numbers, BIGINT values are always string |
mysql2.insert_id_unsigned |
unset | true interprets insert IDs as unsigned; false as signed; unset rejects ambiguous negative packet values |
overrides |
[] |
Column/database type overrides with optional imports and codecs |
type_mappings |
{} |
Reusable named types/codecs or SQLite codec presets |
query_overrides |
[] |
Types, codecs, and nullability for a named query's argument or result alias |
Generated output omits boilerplate documentation and retains SQL, user comments, types, and runtime checks.
MySQL big-number flags are set on each generated query. Its DECIMAL values
remain strings. PostgreSQL uses default driver parsers: int8/numeric are
strings, small integers and floats are numbers, timestamps are Date, binary
values are Buffer, and parsed JSON values use JsonValue. PostgreSQL array
element types follow the selected driver; pg's numeric array parser returns
numbers and can lose decimal precision. Enum labels become named string unions.
Raw enum/circle array text from pg is decoded when it has no registered array
parser. Box arrays retain their semicolon delimiter. For postgres.js queries
with array results, the generator reads raw cells so SQL NULL stays distinct
from the text "NULL"; scalar columns still use the driver's own parsers. Custom
driver parsers may need matching overrides and codecs.
MySQL insert IDs follow the same big-number type policy as BIGINT row fields.
With support enabled, large IDs remain exact strings. With support disabled, an
unsafe insert ID throws instead of returning a rounded number. This error occurs
after the INSERT executes; do not retry the insert just because the ID
conversion failed. Enable support_big_numbers before inserting large IDs.
mysql2 reports negative signed keys and unsigned insert IDs above 2^63 - 1
using the same packet value. The wrappers reject that ambiguous value unless the
caller specifies mysql2.insert_id_unsigned: true for unsigned IDs or false
for signed IDs. This applies to :execlastid and :execresult; it does not
change database column types or driver connection settings. Use separate codegen
blocks when tables need different insert-ID policies.
sqlite_type_mode: driver keeps the older SQLite value rules with corrected
text types:
| Declared SQLite type | @bonakodo/sqlite | better-sqlite3 |
|---|---|---|
| Integer | number | bigint |
number |
| NUMERIC/DECIMAL | number | bigint |
any |
| REAL/float | number |
number |
| Boolean | number |
boolean |
| Date/time | string |
Date |
| BLOB | Uint8Array |
Buffer |
| Text | string |
string |
For the Deno driver, generated statements read integers as bigint, then driver
mode returns safe integers as number and larger integers as bigint. Native
mode keeps every integer as bigint.
The better-sqlite3 wrappers now actually convert booleans/dates and map
snake_case columns to the emitted camelCase properties. Other unknown SQLite
types remain any in driver mode. Missing rows and nullable values keep their
configured null policy. number integer and insert-ID APIs retain their usual
JavaScript precision limits; use native SQLite mode for exact signed 64-bit
data.
sqlite_type_mode: native uses the migrated native value contract for either
SQLite driver: integers are bigint, booleans are boolean, dates are Date,
REAL/NUMERIC values are finite number, blobs are Uint8Array, and JSON
remains raw UTF-8/SQLite bytes. Dates use UTC when stored text has no zone and
keep millisecond precision. Numeric timestamps need a codec. Codecs control
their own storage representation. Unknown SQLite types use unknown in native
mode.
Catalog enums produce a readonly value array and a type derived from that array
in enums.ts:
export const StatusValues = ["active", "inactive"] as const;
export type Status = (typeof StatusValues)[number];Import either export from enums.ts or index.ts. Models, query arguments, and
rows use the shared type; plain strings such as "active" remain valid.
Nullable fields and PostgreSQL arrays keep their existing null and array rules.
The array retains the database's label order. as const makes it readonly to
TypeScript; it does not freeze the array at runtime.
Enums outside the default schema include the schema in their names, such as
ArchiveStatus. Name clashes with models, helper types, or other enum exports
receive stable numeric suffixes. Existing model names stay unchanged.
All catalog enums appear, including enums unused by queries. types_only keeps
these static value arrays and derived types without database dependencies. Types
without catalog enum labels retain the driver's fallback type; this does not
infer enum labels from strings, CHECK constraints, or bare MySQL enum
metadata. Overrides still control field types and optional value conversions.
The pg, postgres.js, and mysql2 drivers parse JSON into JavaScript values. Their
JSON/JSONB fields use exported recursive JsonValue, JsonObject, and
JsonArray types. JsonValue covers strings, numbers, booleans, objects,
arrays, and JSON null. It excludes functions, bigint, and other non-JSON values.
These types remain available in types_only output.
Use an override to give a field a domain type without a codec:
options:
driver: pg
overrides:
- column: users.preferences
ts_type: UserPreferences
import: { path: "../types.ts", name: UserPreferences }The override supplies a static type contract; it does not validate a JSON schema at runtime. Add a codec when you need validation or a different representation. JSON serialization still follows the selected driver's binding rules. SQLite drivers return stored text or bytes, so converting their JSON to an object still requires a codec.
Built-in JSON bindings reject values that JSON cannot represent, including nested undefined, bigint, functions, non-finite numbers, class instances, and cycles. Built-in PostgreSQL JSON array bindings preserve the distinction between an SQL array dimension and an array inside a JSON document. Custom codecs instead return driver-ready values and remain responsible for their driver's JSON binding rules; the runtime does not serialize their output a second time.
JSON null remains a valid value even in a NOT NULL JSON column. On nullable
columns, the server drivers return JavaScript null for both JSON null and SQL
NULL. With emit_null_as_undefined, the existing null policy maps that
ambiguous top-level value to undefined; nulls inside JSON arrays and objects
remain null.
options:
runtime: deno
driver: "@bonakodo/sqlite"
sqlite_type_mode: native
overrides:
- column: documents.id
ts_type: DocumentID
import: { path: "../types.ts", name: DocumentID }
- db_type: JSON
nullable: true
ts_type: Payload
import: { path: "../codecs.ts", name: Payload }
codec: { path: "../codecs.ts", name: payloadCodec }Column selectors accept two to four parts and * wildcards. They take
precedence over database type selectors, which also match declared nullability.
An imported branded type can refine a default type. Changes of value
representation require a codec with encode and decode methods; parsed server
JSON also permits domain type overrides without a codec. The runtime handles
nullable values outside the codec; PostgreSQL array codecs apply to each
non-null element. Relative import paths resolve from the output directory and
rebase for nested query modules. The emitter validates type expressions and
escapes SQL, comments, property names, and module specifiers.
Use named mappings and columns to share conversions. Built-in presets require
SQLite native mode and do not infer storage formats from column names:
options:
runtime: deno
driver: "@bonakodo/sqlite"
sqlite_type_mode: native
optional_nullable_args: true
emit_query_factory: true
type_mappings:
timestamp: { preset: epoch_milliseconds }
settings: { preset: json_text }
overrides:
- db_type: integer
nullable_all: true
preset: safe_integer
- columns: ["authors.created_at", "records.expires_at"]
mapping: timestamp
- column: authors.active
preset: sqlite_boolean
- column: authors.settings
mapping: settingssafe_integer maps SQLite integers to JavaScript numbers and rejects unsafe
values. epoch_milliseconds maps integer milliseconds to Date.
sqlite_boolean binds booleans and reads integer 0/1. json_text stores JSON
as text and returns parsed values. Its default static type is unknown; an
explicit ts_type describes an application contract, not runtime validation.
For validation, import createJsonTextCodec from the generated
runtime_sqlite.ts, export a codec made with createJsonTextCodec(parse) from
your own module, and reference that codec in your mapping. Its parser runs
on reads and writes and determines its TypeScript result. The runtime also
includes the preset codecs used by your mappings as safeInteger,
epochMilliseconds, sqliteBoolean, and jsonText. A json_text mapping also
retains createJsonTextCodec. External SQLite codec modules retain the public
preset helpers and factory, since the generator cannot inspect their imports.
Direct codec mappings to ./runtime_sqlite.ts select the named helper.
The old ./runtime.ts mapping path still resolves to the selected runtime.
Column overrides retain first-match precedence over database-type overrides.
nullable_all: true applies a database-type rule to both required and nullable
columns. It avoids duplicating an otherwise identical override. With
optional_nullable_args: true, omitted or undefined nullable inputs bind SQL
NULL while results still use null unless emit_null_as_undefined is enabled.
Omission binds NULL; it does not omit an INSERT column or invoke its SQL
default. Required JSON-text codecs encode JavaScript null as JSON text
"null"; nullable JSON-text arguments use null for SQL NULL.
sqlc analyzes SQL before the plugin runs. For a scalar subquery or aggregate whose metadata needs correction, target the exact SQL query name and result alias or parameter name:
query_overrides:
- query: GetLatestExpiry
column: latestExpiry
mapping: timestamp
nullable: true
- query: HasStarted
parameter: nowMillis
mapping: timestamp
nullable: false
- query: GetDisplayName
column: displayName
ts_type: string
nullable: falseThese settings override the query boundary without changing table model types or
mutating sqlc's request. An explicit primitive override for an unknown
expression selects its checked runtime conversion. Setting nullable: false
still rejects an actual SQL NULL for non-JSON values; it does not supply a
default. Server drivers cannot distinguish SQL NULL from JSON null in parsed
JSON results. A runtime type change otherwise needs a codec, as with schema
overrides. SQL parsing, missing table functions, and query analysis errors still
require support in sqlc itself.
Set emitLemmaScript: true in a codegen block to emit
LemmaScript annotations on named query
functions. The option accepts a boolean and defaults to false. Omitting it
or setting it to false preserves the existing output byte for byte. Enabling
it adds comments; it does not change runtime behavior or add a runtime
dependency on LemmaScript.
sql:
- engine: sqlite
schema: schema.sql
queries: query.sql
codegen:
- plugin: typescript
out: db
options:
runtime: deno
driver: "@bonakodo/sqlite"
sqlite_type_mode: native
emitLemmaScript: trueEach query function receives //@ extern and //@ impure. These declarations
make the database call a trusted external interface and model each call's
result independently. Two calls with the same arguments need not return the
same rows. The generator does not mark a database query pure or verify.
See LemmaScript's cross-file calls
and external declarations.
Inside the function, generated //@ contract comments describe its engine,
driver, command, return shape, cardinality, null handling, argument bindings,
and field conversions. These are plain-language facts for review. LemmaScript
ignores contract text when proving a program; it is not a formal
postcondition. The comments distinguish query metadata from properties that
depend on the database, driver, or application codecs.
Add formal preconditions and postconditions to SQL query comments with
@lemma. Write expressions against the generated TypeScript names: arguments
use args, and \result denotes the returned value. Each directive occupies
one SQL comment line:
| SQL comment | Generated annotation | Meaning |
|---|---|---|
-- @lemma requires EXPR |
//@ requires EXPR |
A condition callers must establish before the call |
-- @lemma ensures EXPR |
//@ ensures EXPR |
A guarantee callers may assume on normal return |
-- @lemma contract TEXT |
//@ contract TEXT |
Additional intent for review; ignored by the prover |
For example, with text columns documents.id and documents.owner_id:
-- name: GetOwnedDocument :one
-- Return a document only to its owner.
-- @lemma requires args.actorId.length > 0
-- @lemma ensures \result === null || \result.ownerId === args.actorId
-- @lemma contract Ownership must hold for the authenticated actor at query execution.
SELECT id, owner_id
FROM documents
WHERE id = sqlc.arg(id)
AND owner_id = sqlc.arg(actor_id);The query function includes the following annotations in addition to generated query metadata and its usual execution code:
//@ extern
//@ impure
export function getOwnedDocument(
database: Database,
args: GetOwnedDocumentArgs,
): GetOwnedDocumentRow | null {
//@ requires args.actorId.length > 0
//@ ensures \result === null || \result.ownerId === args.actorId
//@ contract Ownership must hold for the authenticated actor at query execution.
// Generated query execution follows.
}An external postcondition is an assumption, not a proof of the SQL. Before trusting the ownership guarantee above, independently establish that the query enforces it and that the caller supplies the authenticated actor's ID. A nonempty string does not establish the actor's identity. A caller's proof can then use the guarantee, conditional on that external contract being true. The plugin does not infer ownership or workspace rules from column names, parse SQL into a proof model, prove sqlc correct, or add authorization checks at runtime.
Use null or undefined in contracts according to
emit_null_as_undefined, and guard optional rows before referring to their
fields. Match expressions to the selected command: :one can return no row,
:many returns an array, and execution commands return their documented
counts or IDs. Postconditions describe normal returns only. They do not prove
that a query succeeds, that a failed write leaves no changes, or that access
still holds after another operation or an await. Review write effects,
transactions, and access revocation separately. Custom codecs and type
overrides also remain part of the trusted interface.
With the option enabled, the generator rejects unknown @lemma directives,
empty payloads, and embedded control characters or line separators. Ordinary SQL comments remain
documentation. The plugin does not check whether an expression is a valid or
true LemmaScript formula; run a compatible LemmaScript verifier as a separate
build check. The annotation integration targets LemmaScript 0.6.1 with its
Dafny backend; other versions or backends may differ in their support for
external impure calls and generated TypeScript types. Extraction confirms that
this version lifts the emitted preconditions, postconditions, and impure
marker. This is an annotation check, not a completed proof of generated code.
For asynchronous drivers, \result retains the external function's
Promise<T> type in LemmaScript 0.6.1; it does not become the resolved row.
The generator adds an explicit note to those functions and does not invent
row postconditions. User formulas still pass through unchanged. Proving a
resolved-row property needs an async-capable model or a separate synchronous
model. Imported connection types, codecs, and generated type aliases may also
need explicit models before a backend can check a caller.
emit_query_factory factories still forward to the named query functions.
For proofs, call the named exports directly so the verifier can resolve their
contracts. types_only emits no query functions, so it emits no callable
LemmaScript contracts.
Generated conversions throw QueryCodecError, a TypeError with query,
file, field, expectedType, and phase (encode or decode). Import it
from the generated index or codec_error.ts. Its message and public cause use
only static metadata. They exclude input and result values, including values
inside an error thrown by an application codec. originalCause() provides the
original exception for explicit local debugging; it may contain application
data. SQL constraint and driver errors propagate unchanged.
expectedType describes the generated type. Imported type aliases in that
string can change after regeneration; use the other fields to identify a
specific query or field.
After regenerating, handle conversion failures through QueryCodecError fields
instead of matching old error messages or checking for RangeError. Direct
runtime calls without query context throw the underlying error types. Generated
bindings now reject missing required inputs; use sqlc.narg or a query override
when an argument should accept SQL NULL. The new factory, mapping, and
input-null options are opt-in. Output always omits boilerplate documentation.
Remove the compact key from existing configurations; the parser rejects it.
Generated runtime imports now use runtime_common.ts and the selected engine's
file. Update any application imports from runtime.ts, then remove that stale
file after regeneration. Query modules share private types and converters in
query_helpers.ts; keep that file with the rest of the generated output.
From the older TypeScript plugin, change the plugin WASM URL and keep your
runtime, driver, and mysql2 options. The default API follows that plugin. Output
text is not byte-compatible: this plugin adds shared models/runtime/index files,
corrects collisions and escaping, and rejects unsupported inputs instead of
silently omitting wrappers. Pg time/timetz use the strings its parser
returns; nullable PostgreSQL array elements are reflected in their types.
Existing camelCase properties now retain their spelling, better-sqlite3 text
fields use string, and parsed server JSON uses JsonValue in place of any.
MySQL callers that enable big-number support must handle string insert IDs.
From the native sqlc branch, move gen.typescript.out to codegen.out and the
remaining settings to codegen.options. Set runtime: deno,
driver: @bonakodo/sqlite, and sqlite_type_mode: native explicitly. Configure
the output path in codegen.out. No modified sqlc binary is needed.
The plugin consumes the analysis sqlc sends. It cannot add compiler features
that run before plugin dispatch. With stock sqlc, schema-only configuration,
sparse SQLite ?NNN parameters, untyped parameters, and JSON table-function
analysis depend on the host's support. Stock sqlc may rewrite SQLite JSONB
projections to JSON text; the plugin cannot recover the original stored bytes
after that rewrite. A host that supplies richer protobuf metadata can include
table identities and correct nullability for embeds. For stock-sqlc embeds, the
plugin recovers nullability from ordinary base-table SELECT/join trees,
including aliases and outer joins. CTEs, derived tables, set operations, table
functions, and other complex forms retain conservative nullable fields when the
supplied metadata cannot prove a relation required.
From this project's earlier Go releases, keep the WASM URL shape and generator
options. Replace any process configuration with wasm. Applications that
called the Go library must invoke the WASI plugin through sqlc or a WASI host.
The fixed-memory plugin accepts requests up to 16 MiB, 65,536 protobuf records,
16,384 JSON tokens, 1,024 output files, and 16 MiB of combined file contents.
Work records must also fit the unused input region.
deno task tools # install pinned assembler and optimizer
deno task build # assemble and optimize the plugin
deno task check # formatting, lint, types, build, and tests
deno task test # Deno.test component, full-plugin, and adversarial suites
deno task integration # stock sqlc plus generated-code checks and runtime tests
deno task runtime # build and test WASM-generated TypeScript support
deno task live # PostgreSQL/MySQL tests after integrationTests use Deno.test and checked-in reference fixtures. The WASM suites check
both direct assembly and optimized modules, parser boundaries, naming, types,
SQL bindings, driver output, and exact protobuf responses. The
adversarial suite covers NUL bytes, malformed
Unicode, oversized requests, mutation cases, and WASI I/O faults. Workers let
the runner stop a module that loops forever.
The integration suite needs stock sqlc, Node, npm, Deno, and the WASM build tools. Install the pinned sqlc binary without a compiler:
tools/setup-sqlc.sh
export PATH="$PWD/bin/.tools/sqlc-1.31.1:$PATH"
deno task integrationThe installer supports macOS and Linux on arm64 and amd64, verifies the official
release archive checksum, and writes only to bin/.tools. The scripts also
accept SQLC=/absolute/path/to/sqlc.
Integration checks real sqlc output for supported drivers and options, checks
generated TypeScript against pinned package declarations, and runs queries under
Deno, Node, and Bun. Test dependencies, including Bun and TypeScript, live in
tests/integration/package.json and its lockfile. They take no part in the
plugin build. See integration tests.
deno task live uses dedicated PostgreSQL and MySQL test databases and creates
and drops fixture tables. Set SQLC_LIVE_PG_URL and SQLC_LIVE_MYSQL_URL, or
use the local sqlc_test defaults in tests/integration/server_live_test.ts.
The script starts no database services. CI supplies PostgreSQL 17 and MySQL 8.4
and checks JSON, arrays, geometry, binding, CRUD, transactions, and BIGINT. The
Deno SQLite example also regenerates and runs
in CI.
| Directory | Contents |
|---|---|
src/ |
WAT modules, constant strings, Unicode and inflection tables |
src/templates/ |
Common and engine runtimes, JSON types, and codec errors |
tools/ |
Pinned tool setup, assembly, data packing, and test commands |
tests/wasm/ |
Deno component and full-plugin tests, fixtures, and WASI host |
tests/tools/ |
Tests for Deno data encoders and packers |
tests/runtime/ |
Tests for generated TypeScript support code |
tests/integration/ |
Stock-sqlc and Node/Bun/Deno checks, test dependencies |
tests/fixtures/ |
SQL fixtures and stock-sqlc metadata |
examples/ |
Applications using the released or locally built WASI plugin |
bin/ |
Ignored build output and local tools |
To publish a release, push a new v* tag, such as v1.2.3. The workflow
checks, builds, and tests the WASM, then checks generated example files with
stock sqlc. It publishes sqlc-gen-typescript-native.wasm, its .wasm.sha256
file, and release notes with a pinned config. Tags containing a hyphen, such as
v1.2.3-rc.1, produce prereleases. Use a new tag for each release so published
URLs and checksums stay valid.
The generator uses the MIT license. Adapted emitter logic and third-party data retain their original notices and license texts in NOTICE.