Skip to content

Releases: productdevbook/sumak

v0.0.16

20 May 01:39

Choose a tag to compare

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:

  • ViewsCREATE VIEW, CREATE OR REPLACE VIEW (PG / MySQL / SQLite), CREATE OR ALTER VIEW (MSSQL), materialized views with REFRESH MATERIALIZED VIEW CONCURRENTLY.
  • SequencesCREATE SEQUENCE with the full grammar (OWNED BY, CACHE, MINVALUE, CYCLE …), ALTER SEQUENCE, runtime nextval / currval / setval.
  • Custom typesCREATE TYPE … AS ENUM, CREATE DOMAIN with CHECK / DEFAULT / NOT NULL, ALTER TYPE … ADD VALUE, ALTER TYPE … RENAME [VALUE].
  • Row Level SecurityCREATE POLICY, ALTER POLICY (both rename and modify forms), DROP POLICY, plus the four AlterTableBuilder toggles: ENABLE / DISABLE / FORCE / NO FORCE row-level security. The backbone of any multi-tenant SaaS on Postgres.
  • Constraints and indexesUNIQUE NULLS NOT DISTINCT (PG 15+), partial indexes (CREATE INDEX … WHERE deleted_at IS NULL — the soft-delete classic), and PG EXCLUDE constraints for the booking-system overlap pattern that's nearly impossible to enforce correctly in application code:
    EXCLUDE USING gist (room WITH =, during WITH &&)
  • Schema documentationCOMMENT ON TABLE, COMMENT ON COLUMN (standalone on PG, inline on MySQL).
  • MaintenanceVACUUM / ANALYZE / REINDEX (PG, with options), LOCK TABLE with shortcut methods for all eight PG lock modes, TRUNCATE TABLE with PG-flavored grammar, CREATE / DROP EXTENSION.
  • PG-specificLISTEN / UNLISTEN / NOTIFY for pubsub, COPY FROM STDIN / COPY TO STDOUT for 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, throwing ValidationError (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 hasTransformNode at 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

  • #155 — EXTRACT, DATE_TRUNC, AGE typed builders
  • #159 — dateAdd / dateSub with dialect-aware emission

SQL functions — strings, regex, math, JSON, arrays, sequences

Read more

v0.0.15

18 May 14:02

Choose a tag to compare

   🚀 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

   🏎 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

29 Apr 16:19

Choose a tag to compare

   🚀 Features

   🐞 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

24 Apr 13:11

Choose a tag to compare

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 testsexpectTypeOf suite pins the public API shape.
  • Dockerized roundtrips — optional INTEGRATION_DB=1 MySQL 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

23 Apr 21:06

Choose a tag to compare

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-subclassed Error. ~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 row

Transactions. 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 with ON 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)introspectPggenerateSchemaCode — 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 cast

The 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:
    View changes on GitHub

v0.0.11

21 Apr 13:32

Choose a tag to compare

   🚀 Features

   🐞 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 &nbsp;-&nbsp; 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...
Read more

v0.0.10

04 Apr 18:00

Choose a tag to compare

   🚀 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

04 Apr 17:18

Choose a tag to compare

   🚀 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

04 Apr 15:49

Choose a tag to compare

   🐞 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

04 Apr 14:55

Choose a tag to compare

   🚀 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)
    View changes on GitHub