Releases: productdevbook/sumak
v0.0.16
v0.0.16 — Sumak grows up
This release marks sumak's transition from a "solid query builder" to "the SQL layer you can deploy to production and stop second-guessing." 47 PRs, ~50 new features, and a long stretch of focused work. But the PR count isn't the point. The point is this: nearly everything you used to drop down to unsafeRawExpr for now has a typed, dialect-aware, plan-cache-friendly first-class API.
What this release fixed
Up to v0.0.15, sumak had a solid core — SELECT / INSERT / UPDATE / DELETE / MERGE, CTEs, basic window functions, joins. But every real application eventually needed one of: a percentile calculation, a regex match, a Postgres EXCLUDE constraint, an RLS policy, a LISTEN/NOTIFY channel. And sumak forced you back to hand-written unsafeRawExpr("...") for each of them.
This release closes that gap. As of v0.0.16, every part of SQL:2003 / 2011 / 2016 / 2023 that modern Postgres / MySQL / SQLite / MSSQL actually ship has a typed builder behind it. Dialect mismatches surface at compile time as UnsupportedDialectFeatureError — you don't make it to the driver and get a parse error.
Analytics and statistics, natively
You used to abandon the builder and write raw SQL for any dashboard query. Now:
db.selectFrom("requests")
.select("region")
.select({
p50: withinGroup(percentileCont(0.5), [{ expr: typedCol("latency_ms") }]),
p99: withinGroup(percentileCont(0.99), [{ expr: typedCol("latency_ms") }]),
jitter: stddev(typedCol("latency_ms")),
spread: variance(typedCol("latency_ms")),
cohort: anyValue(typedCol("user_cohort")),
})
.groupBy("region")The full statistical aggregate family is typed. PERCENTILE_CONT / PERCENTILE_DISC ship with a generic withinGroup helper — not just for these two, but for any future ordered-set aggregate. The complete STDDEV / VARIANCE family, the linear-regression aggregates (CORR, COVAR, REGR_SLOPE, REGR_INTERCEPT, REGR_R2). BIT_AND / OR / XOR and BOOL_AND / OR for permission-flag folds. And the window-position helpers — FIRST_VALUE / LAST_VALUE / NTH_VALUE — with the classic frame-default footgun documented inline.
MSSQL uses different names for most of these (STDEV vs STDDEV, LOG vs LN). The MSSQL printer translates them transparently — you write the standard name and the right keyword goes over the wire.
Date / time and regex — the workhorses
EXTRACT(YEAR FROM ts), DATE_TRUNC('month', ts), AGE(end, start) — Postgres's everyday analytics primitives. Now typed:
.select({
yr: extract("year", typedCol("created_at")),
bucket: dateTrunc("month", typedCol("created_at")),
tenure: age(typedCol("hired_at")),
})dateAdd / dateSub unifies four different dialect grammars behind one API. PG emits expr + INTERVAL '7 days', MySQL emits DATE_ADD(expr, INTERVAL 7 DAY), MSSQL emits DATEADD(day, 7, expr), SQLite emits datetime(expr, '+7 days'). You write dateAdd(col, 7, "day") and the right grammar goes out.
On the regex side, regexpReplace / regexpLike / regexpMatches / regexpSubstr — each gated by a dialect feature flag, with clean refusals on dialects like MSSQL that don't have regex at all.
JSON — SQL:2016 standard functions
The standard JSON functions that Postgres 17 and MySQL 8 both implement — JSON_VALUE, JSON_QUERY, JSON_EXISTS — are now typed. They sit next to PG's operator-based ->, ->>, #>, #>>, but they ship with a RETURNING type cast clause for inline type coercion. IS [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] predicate from SQL:2016 is also wired up, for asking "does this text column hold valid JSON?"
The full Postgres array function set ships under the arr.* namespace: append, prepend, cat, length, position(s), remove, replace, to_string, unnest. If you're building tag systems or multi-valued attributes, life gets quieter the moment you stop reaching for unsafeRawExpr.
MERGE statement reaches feature parity
MERGE was in sumak's core but half-finished. This release closes it out:
WHEN NOT MATCHED BY SOURCE (PG 17, MSSQL) — fires for target rows the source doesn't match. Critical for full-sync patterns:
db.mergeInto("target", { ... })
.whenMatchedThenUpdate({ ... })
.whenNotMatchedThenInsert({ ... })
.whenNotMatchedBySourceThenDelete() // "delete rows that no longer exist in source"RETURNING on MERGE (PG 17) — the mergeAction() helper tells you which branch fired per row ('INSERT' | 'UPDATE' | 'DELETE'). On MSSQL the same builder slot emits an OUTPUT clause instead, with automatic INSERTED. / DELETED. prefixing and a separate mergeActionMssql() helper for $action. Two different grammars, one builder.
Window functions — final gaps closed
Named WINDOW clause (SQL:2003) — stop repeating the same window spec across multiple OVER calls:
db.selectFrom("sales")
.window("w", b => b.partitionBy("region").orderBy("date"))
.select({
rn: over(rowNumber(), "w"),
total: over(sum(col.amount), "w"),
})
// WINDOW "w" AS (PARTITION BY "region" ORDER BY "date")Frame EXCLUDE (SQL:2011) — EXCLUDE CURRENT ROW | GROUP | TIES | NO OTHERS. Lets a running total skip the current row, or skip its peers, in one keyword instead of two CASE statements.
DDL surface — schema-as-code, fully
Sumak was good for migrations but you still had to drop to raw SQL for CREATE VIEW or CREATE SEQUENCE. This release brings sumak up to the level a real production codebase actually needs:
- Views —
CREATE VIEW,CREATE OR REPLACE VIEW(PG / MySQL / SQLite),CREATE OR ALTER VIEW(MSSQL), materialized views withREFRESH MATERIALIZED VIEW CONCURRENTLY. - Sequences —
CREATE SEQUENCEwith the full grammar (OWNED BY, CACHE, MINVALUE, CYCLE …),ALTER SEQUENCE, runtimenextval/currval/setval. - Custom types —
CREATE TYPE … AS ENUM,CREATE DOMAINwith CHECK / DEFAULT / NOT NULL,ALTER TYPE … ADD VALUE,ALTER TYPE … RENAME [VALUE]. - Row Level Security —
CREATE POLICY,ALTER POLICY(both rename and modify forms),DROP POLICY, plus the fourAlterTableBuildertoggles:ENABLE/DISABLE/FORCE/NO FORCErow-level security. The backbone of any multi-tenant SaaS on Postgres. - Constraints and indexes —
UNIQUE NULLS NOT DISTINCT(PG 15+), partial indexes (CREATE INDEX … WHERE deleted_at IS NULL— the soft-delete classic), and PGEXCLUDEconstraints for the booking-system overlap pattern that's nearly impossible to enforce correctly in application code:EXCLUDE USING gist (room WITH =, during WITH &&)
- Schema documentation —
COMMENT ON TABLE,COMMENT ON COLUMN(standalone on PG, inline on MySQL). - Maintenance —
VACUUM/ANALYZE/REINDEX(PG, with options),LOCK TABLEwith shortcut methods for all eight PG lock modes,TRUNCATE TABLEwith PG-flavored grammar,CREATE / DROP EXTENSION. - PG-specific —
LISTEN/UNLISTEN/NOTIFYfor pubsub,COPY FROM STDIN/COPY TO STDOUTfor bulk transfer.
Plugin ecosystem grew
Sumak's hook layer already shipped audit-timestamp, soft-delete, multi-tenant, CASL, etc. This release adds four more:
normalizeStrings— auto-transform column values on INSERT / UPDATE / MERGE (lowercase emails, trim whitespace, empty-to-null, custom function). Lowercase-email consistency stops being a thing you remember to write everywhere.defaults— fill missing INSERT columns from runtime context. Pass thunks like() => currentUserId()or() => currentTenantId()and every insert picks them up automatically.validators— per-column predicate checks before the write, throwingValidationError(with table + column + value attached) for clean error handling upstream.debugLogger— observability for development: log every compiled and executed SQL with filter, slow-query threshold, and custom sink support.
By the numbers
- 47 PRs merged (#142 → #189)
- Tests: 1689 → 3138 (+1449 new tests, all green in parallel)
- Bench scenarios: 7 → 41
src/LOC added: ~12,000- All four dialect printers (
pg/mysql/sqlite/mssql) updated; refusals route through the central feature-flag matrix
Migration
If you're coming from kysely or drizzle, docs/migration-from-kysely-and-drizzle.md ships in this release — side-by-side pattern table, critical differences, and an incremental-port strategy.
Credits
This release was produced over a multi-agent Claude Code session. Every PR carries a Co-Authored-By: Claude Opus 4.7 (1M context) trailer alongside @productdevbook.
Bug fixes and performance
- normalize: identity-preserving
recurse+ restored fixpoint loop — #104 - plugin-manager: cache
hasTransformNodeat construction — #125
All PRs in this release
SQL functions — aggregates
- #142 — ANY_VALUE aggregate
- #146 — PERCENTILE_CONT / PERCENTILE_DISC +
withinGroup - #156 — STDDEV / VARIANCE / CORR / COVAR / REGR_* statistical aggregates
- #167 — BIT/BOOL aggregates + FIRST_VALUE / LAST_VALUE / NTH_VALUE window helpers
SQL functions — date / time
SQL functions — strings, regex, math, JSON, arrays, sequences
v0.0.15
🚀 Features
- builder: Kysely-style .where(col, op, val) overload (B1) - by @productdevbook and Claude Opus 4.7 (1M context) in #99 (8c64b)
- casl: Accept an ability factory function for per-request authz - by @productdevbook and Claude Opus 4.7 (1M context) in #90 and #97 (72e95)
🐞 Bug Fixes
- builder:
- Runtime guard for .where() silent-bug + expanded perf bench - by @productdevbook and Claude Opus 4.7 (1M context) in #95 (07917)
- normalize:
- Run sub-passes to fixed point per call - by @productdevbook and Claude Opus 4.7 (1M context) in #102 (fb821)
- Revert PR #102 fixpoint loop — it was 5× slower - by @productdevbook and Claude Opus 4.7 (1M context) in #103 and #102 (a4689)
🏎 Performance
- builder, normalize: Cut allocations on AND/OR chains - by @productdevbook and Claude Opus 4.7 (1M context) in #98 (8dd24)
- in-list: Close the 100-value IN-list compile gap vs kysely - by @productdevbook and Claude Opus 4.7 (1M context) in #96 (27e59)
View changes on GitHub
v0.0.14
🚀 Features
- plugin: CaslAuthz — CASL rules → AST WHERE injection (#90 deep path) - by @productdevbook and Claude Opus 4.6 in #90 (e530a)
🐞 Bug Fixes
- build: Emit dist files dropped by obuild's isolatedDeclarations bug - by @productdevbook in #93 (1e161)
- builder: Add explicit return type to _setClause() for isolatedDeclarations - by @Markxuxiao, Mark and Claude Opus 4.6 in #92 (06162)
View changes on GitHub
v0.0.13
Big feature drop: CLI, first-party drivers, observability, streaming/abort, benchmark comparisons, and six ready-to-run examples. All while keeping the zero-dep stance.
🚀 CLI — sumak migrate / introspect / generate
Drop a config at your repo root, drive everything from the terminal:
// sumak.config.ts
import { Pool } from "pg"
import { defineConfig } from "sumak/cli"
import { pgDriver } from "sumak/drivers/pg"
import { tables } from "./src/schema.ts"
export default defineConfig({
dialect: "pg",
driver: () => pgDriver(new Pool({ connectionString: process.env.DATABASE_URL })),
schema: () => ({ tables }),
})sumak migrate plan # preview DDL
sumak migrate up # apply pending DDL
sumak migrate up --allow-destructive # permit DROP COLUMN
sumak introspect --out src/schema.generated.ts
sumak generate --out ./migrations/001.sql~40-line argv parser — no commander / yargs / tsx runtime deps.
🔌 First-party driver adapters
import { sumak } from "sumak"
import { pgDialect } from "sumak/pg"
import { pgDriver } from "sumak/drivers/pg" // or /mysql2, /better-sqlite3, /mssql
import { Pool } from "pg"
const db = sumak({
dialect: pgDialect(),
driver: pgDriver(new Pool({ connectionString: process.env.DATABASE_URL })),
tables,
})Drivers are peer dependencies — only what you use ends up in your bundle.
📊 Observability — onQuery + OpenTelemetry
const db = sumak({
dialect: pgDialect(),
driver,
tables,
onQuery: (ev) => {
if (ev.phase === "end") {
console.log(`${ev.sql} — ${ev.durationMs}ms, ${ev.rowCount} rows`)
} else if (ev.phase === "error") {
console.error(`${ev.sql} failed:`, ev.error)
}
},
})OpenTelemetry bridge in one line:
import { onQueryToOtel } from "sumak/otel"
import { trace } from "@opentelemetry/api"
const db = sumak({
dialect: pgDialect(),
driver,
tables,
onQuery: onQueryToOtel(trace.getTracer("sumak")),
})🌊 Streaming + 🛑 AbortSignal
Stream millions of rows without exhausting memory, and cancel queries when the client walks away:
// Stream via pg cursor / better-sqlite3 iterate()
for await (const row of db.selectFrom("events").selectAll().stream()) {
process(row)
}
// AbortSignal on any execute path
const ac = new AbortController()
req.on("close", () => ac.abort())
const users = await db
.selectFrom("users")
.where(({ id }) => id.eq(42))
.many({ signal: ac.signal })🏗️ Migration engine v2
Explicit renames, typed ALTER COLUMN … USING, and a dialect-aware advisory lock so two deploys can't interleave DDL:
import { applyMigration } from "sumak/migrate"
import { sql } from "sumak"
await applyMigration(db, before, after, {
renames: {
tables: { posts: "articles" },
columns: { "users.fullName": "displayName" },
},
typeMigrations: {
"users.age": { using: sql`age::int` }, // text → integer safely
},
lock: true, // pg_advisory_lock / GET_LOCK / sp_getapplock
transaction: true,
})🔎 Introspection v2
MySQL / SQLite / MSSQL now recover composite PKs, CHECK bodies, composite FKs, and named indexes — parity with PG.
import { introspect, generateSchemaCode } from "sumak"
const schema = await introspect(driver, "mysql")
writeFileSync("src/schema.ts", generateSchemaCode(schema))
// Composite constraints and indexes round-trip cleanly.🧱 Query builder gaps filled
ANY / ALL / SOME + array literals:
import { any, all, arrayLiteral, col } from "sumak"
db.selectFrom("products")
.where(({ category }) => category.eq(any(arrayLiteral(["books", "music"]))))
.where(({ price }) => price.lt(all(cheaperSubquery)))
.toSQL()
// WHERE "category" = ANY(ARRAY[$1, $2]) AND "price" < ALL (SELECT ...)VALUES derived tables:
db.selectFromValues({
alias: "seed",
columns: ["id", "label"],
rows: [[1, "foo"], [2, "bar"], [3, "baz"]],
})
.selectAll()
.toSQL()
// SELECT * FROM (VALUES ($1,$2),($3,$4),($5,$6)) AS "seed"("id","label")GROUPING SETS / CUBE / ROLLUP:
import { groupingSets, cube, rollup } from "sumak"
db.selectFrom("sales")
.select("region", "category", ({ amount }) => amount.sum().as("total"))
.groupBy(groupingSets([[col("region"), col("category")], [col("region")], []]))
.toSQL()
// GROUP BY GROUPING SETS (("region","category"), ("region"), ())📋 Schema DSL richer
import { defineTable, integer, serial, text } from "sumak/schema"
const products = defineTable(
"products",
{
id: serial().primaryKey(),
sku: text().notNull(),
priceCents: integer().notNull().check("price_cents > 0"), // column-level CHECK
createdAt: timestamp().notNull().defaultTo(sql`now()`), // expression default
searchVec: text().generatedAlwaysAs(sql`to_tsvector('english', name)`), // GENERATED
},
{
constraints: {
primaryKey: ["id"],
uniques: [{ name: "uq_sku", columns: ["sku"] }],
checks: [{ name: "ck_price", expression: "price_cents > 0" }],
},
indexes: [
{ name: "ix_sku_active", columns: ["sku"], unique: true, where: "deletedAt IS NULL" },
],
},
)🔒 Multi-tenant strict mode + audit userId injection
Refuse cross-tenant inserts at compile time; stamp created_by / updated_by automatically:
import { multiTenant, audit } from "sumak/plugin"
const db = sumak({
dialect: pgDialect(),
driver,
tables,
plugins: [
multiTenant({ tenantId: () => currentRequest.tenantId, strict: true }),
audit({ userId: () => currentRequest.userId }),
],
})
// Compiles: WHERE "tenant_id" = $1 auto-injected
// Refuses: .insertInto("users").values({ tenantId: 99, ... }) ← TypeError🛡️ Security audit
Catch unsafeRawExpr usage in review or at runtime:
import { setUnsafeWarnHandler, findRawNodes } from "sumak/security"
// Runtime hook — log or throw in dev
setUnsafeWarnHandler((ev) => {
console.warn(`unsafe usage in ${ev.stack}: ${ev.argument}`)
})
// Static scan — CI-friendly AST grep
const query = db.selectFrom("users").where(() => unsafeRawExpr("1=1")).build()
if (findRawNodes(query).length > 0) {
throw new Error("raw SQL leaked into production path")
}⚡ Benchmark harness — sumak vs kysely vs drizzle
Compile-time microbench over 7 canonical shapes. sumak is fastest on 6 of 7 — typically 1.1×–2.5× vs kysely and 9×–39× vs drizzle. Full tables: bench/README.
pnpm vitest bench --run bench/compile.bench.ts
PERF_GUARD=1 pnpm vitest run bench/regression.test.ts # CI floor| scenario | sumak (hz) | kysely (hz) | drizzle (hz) |
|---|---|---|---|
| select-where-and | 1,505,721 | 605,936 | 38,868 |
| join-2-tables | 478,841 | 284,523 | 52,851 |
| insert-values | 632,812 | 489,578 | 67,126 |
📚 Six runnable examples — Node 24+
examples/
├── express/ # Express 5 + pg — streaming NDJSON, AbortSignal on disconnect
├── fastify/ # Fastify 5 + pg — layered validation (JSON schema + sumak types + CHECK)
├── aws-lambda/ # Lambda + pg — module-level pool, deadline → AbortSignal
├── nextjs/ # Next.js 16 + pg — Server Actions, dbFor(tid) + multiTenant strict
├── nuxt/ # Nuxt 4 + Nitro + pg — Nitro close hook releases Pool on HMR
└── nitro/ # Nitro 3 standalone — deploys to Node / Cloudflare / Lambda
Each directory has a sumak.config.ts wired to the CLI; pnpm migrate creates the schema.
🧪 Test infrastructure
- Regression corpus — hand-curated AST shapes that once crashed the printer, locked in forever.
- Type-level tests —
expectTypeOfsuite pins the public API shape. - Dockerized roundtrips — optional
INTEGRATION_DB=1MySQL 8.4 + MSSQL 2022 tests. - 1639 tests ✓ across 193 files.
📝 Architecture decisions
docs/adr/001-no-statement-cache.md— rejected an AST-signature cache after bench data showed compile is already 0.57µs–2µs and the hash itself costs an AST walk; net loss unless hit rate > 50%, and I/O dominates by 3–5 orders of magnitude anyway.
💥 Breaking changes
None — all public API remains backwards-compatible with v0.0.12.
Full diff: v0.0.12...v0.0.13
v0.0.12
What changed in v0.0.12
This release has two parts: structural work that shores up the library's foundations, and three big new capabilities built on top of that — an execute layer, a migration engine, and database introspection. Here's what changed and why.
Foundations
Exhaustive never-checks on every AST switch. The AST is a discriminated union with 21 expression variants and 5 top-level DML kinds. Six different places (plugin-manager, two normalizer passes, the optimizer's table-ref collector, the printer, the legacy transformer) dispatched on node.type and every one of them ended with default: return node. When a variant was missed, the node was silently swallowed — no error, no warning. Every switch now ends with default: return assertNever(node, ...): TypeScript errors at compile time when a variant is missing, and UnreachableNodeError is the runtime backstop. Applying this pattern surfaced 6 pre-existing bugs — the plugin chain had been silently skipping json_access, tuple, array_expr, full_text_search, window_function, and nested EXPLAIN.statement.
Shared ASTWalker base class. The six duplicated switches now live in one file. PluginManager extends it, the legacy ASTTransformer extends it. Identity-preserving: if no child changed, the parent returns the same reference. ~170 lines of duplication deleted.
Central dialect feature matrix. 40+ scattered if (dialect === "mysql") throw ... lines collapsed into one FEATURES map with supportsFeature, assertFeature, dialectsForFeature helpers. Adding a feature is one line.
Builder-time arity validation. coalesce(), concat(), greatest(), least(), jsonBuildObject(), tuple() now throw InvalidExpressionError at build time when called with too few arguments — the stack trace points at the caller, not at a driver parse failure three layers deep.
Cross-dialect parity + fast-check fuzzing. Two new testing primitives:
assertParity(qb, tables, { pg, mysql, sqlite, mssql })— runs the same builder chain through all four printers in a single assertion, with first-class{ throws: UnsupportedDialectFeatureError }support.- A fast-check fuzzer generates bounded-depth random ASTs, hands each to every printer, and fails on any
UnreachableNodeError,TypeError, or un-subclassedError. ~1200 iterations per run.
Execute layer
Until now sumak only produced SQL strings. There's now an opt-in Driver interface — four methods (query, execute, optional transaction and close). No peer dependencies; users write their own adapter.
const db = sumak({ dialect: pgDialect(), driver, tables })
await db.selectFrom("users").where(({ id }) => id.eq(42)).one() // Row | throws
await db.selectFrom("users").where(...).many() // Row[]
await db.selectFrom("users").first() // Row | null
await db.insertInto("users").values(...).exec() // { affected }
await db.insertInto("users").values(...).returningAll().one() // typed rowTransactions. db.transaction(async (tx) => { ... }) hands a scoped Sumak instance to the callback, commits on resolve, rolls back on throw. If the Driver implements transaction(), sumak delegates; otherwise it emits BEGIN/COMMIT/ROLLBACK through the TCL printer.
Error model. MissingDriverError when execute methods are called without a driver; UnexpectedRowCountError when .one() sees anything other than one row.
Migration engine
diffSchemas(before, after) takes two tables shapes and produces the DDL steps to go from one to the other. Ordering: DROPs → CREATEs (topologically sorted by FK dependencies) → ALTERs. Destructive changes are gated behind allowDestructive: true.
// Pure — no driver needed, just preview the plan
const plan = planMigration(db, before, after)
// Apply — wraps the plan in a transaction, rolls back on throw
const result = await applyMigration(db, before, after)
// { applied: 3, statements: [...] }No migrations folder, no hash-based ordering. The schema you pass to sumak({ tables }) is the migration target.
Introspection
introspect(driver, "pg" | "mysql" | "sqlite" | "mssql") reads a live database's schema; generateSchemaCode(schema) emits equivalent TypeScript source.
What's recovered:
- Tables, columns, data types (mapped to sumak column factories)
notNull/nullable, primary keys, uniques, foreign keys withON DELETE/UPDATE- SERIAL / IDENTITY / AUTO_INCREMENT via each dialect's tell
PG has the most complete mapping (JSONB, timestamptz, arrays); SQLite is the least precise because it stores declared types as text affinities.
A roundtrip integration test ties the trio together: applyMigration(empty → schema) → introspectPg → generateSchemaCode — every step agrees.
Issue #90 — CASL / __typename
@notwouf asked on #90: authz libraries like CASL want to match rules against a row but need to know what "type" the row is. Drizzle requires a manual as(row, "Message") cast.
The subjectType plugin injects __typename on every row returned from a mapped table:
const msg = await db.selectFrom("messages").where(({ id }) => id.eq(1)).one()
// msg.__typename === "Message"
ability.can("update", msg) // CASL match — no manual castThe underlying mechanism is more general: a new ResultContext (source table + column→table map) is threaded through every transformResult plugin and the result:transform hook. Data masking, audit logging, analytics tagging — anything that needs to know which query produced the rows can use the same pipe.
The numbers
- Tests: 1247 → 1356 (+109)
- pglite integration tests now cover every plugin (softDelete, multiTenant, audit, subjectType) plus CTEs and window functions against a real Postgres
- Lint + format + typecheck pass on every commit
- No new peer dependencies, no breaking changes — the
.toSQL()API is unchanged; the execute layer is strictly additive
New public API
Exported from sumak:
- Driver:
Driver,Row,ExecuteResult,TransactionOptions,MissingDriverError,UnexpectedRowCountError - Migrations:
diffSchemas,planMigration,applyMigration,runPlan,SchemaDef,DiffOptions,MigrationPlan,MigrationOptions,ApplyResult,DestructiveMigrationError,MigrationRequiresDriverError - Introspection:
introspect,introspectPg/Mysql/Sqlite/Mssql,generateSchemaCode,IntrospectedSchema/Table/Column,GenerateOptions - Plugins:
subjectType,SubjectTypeConfig,ResultContext - Dialect:
FEATURES,FeatureKey,supportsFeature,assertFeature,dialectsForFeature - AST:
ast.Walker,ast.mapPreserve,assertNever,UnreachableNodeError
🚀 Features
- ast:
- Exhaustive never-checks across all AST switches - by @productdevbook and Claude Opus 4.7 (1M context) (b0175)
- Shared ASTWalker base — single source of AST traversal - by @productdevbook and Claude Opus 4.7 (1M context) (cf022)
- builder:
- AssertMinArity guard for variadic functions - by @productdevbook and Claude Opus 4.7 (1M context) (7fc9f)
- dialect:
- Central feature matrix + assertFeature guard - by @productdevbook and Claude Opus 4.7 (1M context) (1c39b)
- driver:
- Optional execute layer — .many / .one / .first / .exec - by @productdevbook and Claude Opus 4.7 (1M context) (a25c9)
- Db.transaction() with rollback-on-throw - by @productdevbook and Claude Opus 4.7 (1M context) (38a78)
- introspect:
- Read live DB schema + generate sumak TS code - by @productdevbook and Claude Opus 4.7 (1M context) (2b876)
- migrate:
- Schema diff engine — DDL from before/after - by @productdevbook and Claude Opus 4.7 (1M context) (d5b14)
- Plan + apply runner on top of diffSchemas - by @productdevbook and Claude Opus 4.7 (1M context) (6bd25)
- plugin:
- ResultContext + subjectType plugin - by @productdevbook and Claude Opus 4.7 (1M context) in #90 (a640e)
View changes on GitHub
v0.0.11
🚀 Features
- PostgreSQL schema support — dotted keys, withSchema, CREATE/DROP SCHEMA - by @productdevbook and Claude Opus 4.7 (1M context) in #37 and #47 (092e4)
- plugins: MERGE node support — closes #57 - by @productdevbook and Claude Opus 4.7 (1M context) in #59 and #57 (4feaf)
- spike: SQL:2023 Part 16 (SQL/PGQ) property graph queries - by @productdevbook and Claude Opus 4.7 (1M context) in #30 and #49 (a5b99)
🐞 Bug Fixes
- Make sql.ts and compiled.ts compatible with isolatedDeclarations - by @productdevbook and Claude Opus 4.7 (1M context) (36a75)
- Resolve 5 HIGH-priority audit findings - by @productdevbook and Claude Opus 4.7 (1M context) in #51 (b3a53)
- Resolve MEDIUM audit findings — inline imports, exports, with-builder, FOR UPDATE OF - by @productdevbook and Claude Opus 4.7 (1M context) in #52 (62d23)
- Resolve LOW-priority audit findings — API cleanup round 3 - by @productdevbook and Claude Opus 4.7 (1M context) in #54 (a479b)
- 3 CRITICAL correctness bugs from audit round 2 - by @productdevbook and Claude Opus 4.7 (1M context) in #56 (bca1b)
- Audit round 2 — IMPORTANT + MEDIUM findings - by @productdevbook and Claude Opus 4.7 (1M context) in #58 (42bc8)
- Col.eq(null) / Col.neq(null) auto-lower to IS [NOT] NULL - by @productdevbook and Claude Opus 4.7 (1M context) in #61 (13e22)
- Audit round 3 — CRITICAL + HIGH + MEDIUM findings - by @productdevbook and Claude Opus 4.7 (1M context) in #62 (eed35)
- Audit round 4 — sql template + MERGE schema + optimistic-lock - by @productdevbook and Claude Opus 4.7 (1M context) in #65 (e9e3f)
- Count() overload —
count(col)no longer silently drops the arg - by @productdevbook and Claude Opus 4.7 (1M context) in #66 (de5da) - Audit round 5 — UNION/ORDER BY, MSSQL OUTPUT, GROUPS, USING injection - by @productdevbook and Claude Opus 4.7 (1M context) in #67 (54f41)
- Audit round 6 — BigInt, MSSQL UNION, stack overflow, LATERAL - by @productdevbook and Claude Opus 4.7 (1M context) in #68 (ea3c4)
- Audit round 7 — sentinel collision, DELETE no-WHERE, NULL-in-IN, dialect gaps - by @productdevbook and Claude Opus 4.7 (1M context) in #69 (d4bff)
- FILTER (WHERE …) aggregate guard on MySQL + MSSQL - by @productdevbook and Claude Opus 4.7 (1M context) in #70 (78018)
- Audit round 8 — INSERT OR IGNORE dialect guards + excluded() helper - by @productdevbook and Claude Opus 4.7 (1M context) in #71 (01de0)
- DDL dialect guards — DROP/TRUNCATE/INDEX partial - by @productdevbook and Claude Opus 4.7 (1M context) in #72 (528f1)
- Audit round 9 — validateDataType + TCL isolation + CASCADE guards - by @productdevbook and Claude Opus 4.7 (1M context) in #73 (ab310)
- Audit round 10 — VALUES shape, window-only OVER, CREATE VIEW guards - by @productdevbook and Claude Opus 4.7 (1M context) in #74 (9e337)
- Audit round 11 — MySQL UPDATE RETURNING + distinctFrom + ILIKE - by @productdevbook and Claude Opus 4.7 (1M context) in #75 (ac816)
- JSON
->/->>numeric path renders as integer, not string - by @productdevbook and Claude Opus 4.7 (1M context) in #76 (a6b0f) - Audit #12 — dialect guards, orderBy whitespace, JOIN ON requirement - by @productdevbook and Claude Opus 4.7 (1M context) in #77 and #12 (2731b)
- Audit #13 — nested sql sentinels, plugin CTE traversal, pushdown, enumType - by @productdevbook and Claude Opus 4.7 (1M context) in #78 and #13 (efe45)
- Audit #14 — MSSQL UPDATE/DELETE bounds, MERGE walk + alias, CASE guard - by @productdevbook and Claude Opus 4.7 (1M context) in #79 and #14 (dcc73)
- Audit #15 — MSSQL boolean literals, portable NOW, parseTableRef, int fold - by @productdevbook and Claude Opus 4.7 (1M context) in #80 and #15 (8a573)
- Audit #16 — DDL printExpr, UNION paren, MSSQL IF NOT EXISTS - by @productdevbook and Claude Opus 4.7 (1M context) in #81 and #16 (fbce5)
- Audit #17 — WHERE subquery plugin walk + MERGE guards + autoIncrement dialect - by @productdevbook and Claude Opus 4.7 (1M context) in #82 and #17 (2e1d0)
- Audit #18 — JSON dialect guards, BETWEEN SYMMETRIC, GREATEST/LEAST on SQLite - by @productdevbook and Claude Opus 4.7 (1M context) in #83 and #18 (d36f9)
- Audit #19 — MySQL DELETE JOIN, OptimisticLock INSERT, INSERT dim check - by @productdevbook and Claude Opus 4.7 (1M context) in #84 and #19 (b7e3d)
- Audit #20 — MERGE expression walk, drop
- by **fold, DDL validateDataType (#85)** and Claude Opus 4.7 (1M context) in #20 () - Audit #21 — MSSQL DELETE JOIN, MySQL lock modes, EXPLAIN dialect overrides - by @productdevbook and Claude Opus 4.7 (1M context) in #86 and #21 (cab60)
- Audit #22 — comma-joined ALTER, MSSQL sp_rename, PG DELETE+JOIN reject - by @productdevbook and Claude Opus 4.7 (1M context) in #87 and #22 (f1b15)
- Audit #23 — DDL asSelect routes through compile() (plugin-walk) - by @productdevbook and Claude Opus 4.7 (1M context) in https://github.com/productdevbook/sumak/issue...
v0.0.10
🚀 Features
- 7-layer pipeline — normalize (NbE), optimize (rewrite rules), compiled queries, JSON optics - by @productdevbook and Claude Opus 4.6 (1M context) (46e4b)
View changes on GitHub
v0.0.9
🚀 Features
- 4 enterprise plugins — SoftDelete v2, AuditTimestamp, MultiTenant, QueryLimit - by @productdevbook and Claude Opus 4.6 (1M context) (0e81b)
- OptimisticLockPlugin + DataMaskingPlugin - by @productdevbook and Claude Opus 4.6 (1M context) (f1aa1)
- CursorPaginationPlugin — keyset pagination - by @productdevbook and Claude Opus 4.6 (1M context) (0f729)
- SelectCount() convenience + HAVING with aggregates - by @productdevbook and Claude Opus 4.6 (1M context) (22dca)
🐞 Bug Fixes
- Replace CursorPaginationPlugin with .cursorPaginate() builder method - by @productdevbook and Claude Opus 4.6 (1M context) (3c0b9)
- MultiTenantPlugin and OptimisticLockPlugin use callback pattern - by @productdevbook and Claude Opus 4.6 (1M context) (6a941)
View changes on GitHub
v0.0.8
🐞 Bug Fixes
- Remove global param state + add .toSQL() to all typed builders - by @productdevbook and Claude Opus 4.6 (1M context) (a94c0)
View changes on GitHub
v0.0.7
🚀 Features
- DDL Schema Builder — CREATE TABLE, ALTER TABLE, CREATE INDEX, CREATE VIEW, DROP - by @productdevbook and Claude Opus 4.6 (1M context) (ededf)
- GenerateDDL() auto CREATE TABLE from schema + multi-dialect DDL tests - by @productdevbook and Claude Opus 4.6 (1M context) (1fc49)
- Enhanced ColumnBuilder + EXPLAIN on all query types + interval() - by @productdevbook and Claude Opus 4.6 (1M context) (0fea0)
- Self-join aliases, subqueryExpr, returningExpr, Col.asc/desc - by @productdevbook and Claude Opus 4.6 (1M context) (f56c6)
- OrWhere, Col.cast, TRUNCATE TABLE - by @productdevbook and Claude Opus 4.6 (1M context) (5e7a2)