{"id":48110235,"url":"https://github.com/productdevbook/sumak","last_synced_at":"2026-04-05T17:01:09.693Z","repository":{"id":349090633,"uuid":"1200791416","full_name":"productdevbook/sumak","owner":"productdevbook","description":"Type-safe SQL query builder. Zero dependencies, AST-first, hookable, tree-shakeable. Pure TypeScript.","archived":false,"fork":false,"pushed_at":"2026-04-04T16:39:55.000Z","size":1082,"stargazers_count":57,"open_issues_count":2,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-04T16:43:44.553Z","etag":null,"topics":["ast","database","esm","mysql","orm","postgresql","query-builder","sql","sqlite","tree-shaking","type-safe","typescript","zero-dependency"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/productdevbook.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["productdevbook"]}},"created_at":"2026-04-03T20:42:16.000Z","updated_at":"2026-04-04T16:40:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/productdevbook/sumak","commit_stats":null,"previous_names":["productdevbook/sumak"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/productdevbook/sumak","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fsumak","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fsumak/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fsumak/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fsumak/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/productdevbook","download_url":"https://codeload.github.com/productdevbook/sumak/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/productdevbook%2Fsumak/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31442924,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T15:22:31.103Z","status":"ssl_error","status_checked_at":"2026-04-05T15:22:00.205Z","response_time":75,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast","database","esm","mysql","orm","postgresql","query-builder","sql","sqlite","tree-shaking","type-safe","typescript","zero-dependency"],"created_at":"2026-04-04T16:03:17.512Z","updated_at":"2026-04-05T17:01:09.683Z","avatar_url":"https://github.com/productdevbook.png","language":"TypeScript","readme":"\u003cp align=\"center\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\".github/assets/cover.jpg\" alt=\"sumak — Type-safe SQL query builder\" width=\"100%\"\u003e\n  \u003cbr\u003e\u003cbr\u003e\n  \u003cb style=\"font-size: 2em;\"\u003esumak\u003c/b\u003e\n  \u003cbr\u003e\u003cbr\u003e\n  Type-safe SQL query builder with powerful SQL printers.\n  \u003cbr\u003e\n  Zero dependencies, AST-first, hookable, tree-shakeable. Pure TypeScript, works everywhere.\n  \u003cbr\u003e\u003cbr\u003e\n  \u003ca href=\"https://npmjs.com/package/sumak\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/sumak?style=flat\u0026colorA=18181B\u0026colorB=e11d48\" alt=\"npm version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://npmjs.com/package/sumak\"\u003e\u003cimg src=\"https://img.shields.io/npm/dm/sumak?style=flat\u0026colorA=18181B\u0026colorB=e11d48\" alt=\"npm downloads\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://bundlephobia.com/result?p=sumak\"\u003e\u003cimg src=\"https://img.shields.io/bundlephobia/minzip/sumak?style=flat\u0026colorA=18181B\u0026colorB=e11d48\" alt=\"bundle size\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/productdevbook/sumak/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/productdevbook/sumak?style=flat\u0026colorA=18181B\u0026colorB=e11d48\" alt=\"license\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## Table of Contents\n\n- [Install](#install)\n- [Quick Start](#quick-start)\n- [SELECT](#select)\n- [INSERT](#insert)\n- [UPDATE](#update)\n- [DELETE](#delete)\n- [WHERE Conditions](#where-conditions)\n- [Joins](#joins)\n- [Expressions](#expressions)\n- [Aggregates](#aggregates)\n- [Window Functions](#window-functions)\n- [SQL Functions](#sql-functions)\n- [Subqueries](#subqueries)\n- [Set Operations](#set-operations)\n- [CTEs (WITH)](#ctes-with)\n- [Conditional / Dynamic Queries](#conditional--dynamic-queries)\n- [Raw SQL](#raw-sql)\n- [ON CONFLICT / Upsert](#on-conflict--upsert)\n- [MERGE](#merge-sql2003)\n- [Row Locking](#row-locking)\n- [Schema Builder (DDL)](#schema-builder-ddl)\n- [Full-Text Search](#full-text-search)\n- [Temporal Tables](#temporal-tables-sql2011)\n- [Plugins](#plugins)\n- [Hooks](#hooks)\n- [Dialects](#dialects)\n- [Architecture](#architecture)\n\n---\n\n## Install\n\n```sh\nnpm install sumak\n```\n\n## Quick Start\n\nDefine your tables and create a typed instance:\n\n```ts\nimport { sumak, pgDialect, serial, text, boolean, integer, jsonb } from \"sumak\"\n\nconst db = sumak({\n  dialect: pgDialect(),\n  tables: {\n    users: {\n      id: serial().primaryKey(),\n      name: text().notNull(),\n      email: text().notNull(),\n      age: integer(),\n      active: boolean().defaultTo(true),\n      meta: jsonb(),\n    },\n    posts: {\n      id: serial().primaryKey(),\n      title: text().notNull(),\n      userId: integer().references(\"users\", \"id\"),\n    },\n  },\n})\n```\n\nThat's it. `db` now knows every table, column, and type. All queries are fully type-checked.\n\n---\n\n## SELECT\n\n```ts\n// Basic select\ndb.selectFrom(\"users\").select(\"id\", \"name\").toSQL()\n// SELECT \"id\", \"name\" FROM \"users\"\n\n// Select all columns\ndb.selectFrom(\"users\").selectAll().toSQL()\n\n// With WHERE, ORDER BY, LIMIT, OFFSET\ndb.selectFrom(\"users\")\n  .select(\"id\", \"name\")\n  .where(({ age }) =\u003e age.gte(18))\n  .orderBy(\"name\")\n  .limit(10)\n  .offset(20)\n  .toSQL()\n\n// DISTINCT\ndb.selectFrom(\"users\").select(\"name\").distinct().toSQL()\n\n// DISTINCT ON (PostgreSQL)\ndb.selectFrom(\"users\")\n  .selectAll()\n  .distinctOn(\"dept\")\n  .orderBy(\"dept\")\n  .orderBy(\"salary\", \"DESC\")\n  .toSQL()\n```\n\n---\n\n## INSERT\n\n```ts\n// Single row\ndb.insertInto(\"users\").values({ name: \"Alice\", email: \"alice@example.com\" }).toSQL()\n\n// Multiple rows\ndb.insertInto(\"users\")\n  .valuesMany([\n    { name: \"Alice\", email: \"a@b.com\" },\n    { name: \"Bob\", email: \"b@b.com\" },\n  ])\n  .toSQL()\n\n// RETURNING\ndb.insertInto(\"users\").values({ name: \"Alice\", email: \"a@b.com\" }).returningAll().toSQL()\n\n// INSERT ... SELECT\nconst source = db.selectFrom(\"users\").select(\"name\", \"email\").build()\ndb.insertInto(\"archive\").fromSelect(source).toSQL()\n\n// DEFAULT VALUES\ndb.insertInto(\"users\").defaultValues().toSQL()\n\n// SQLite: INSERT OR IGNORE / INSERT OR REPLACE\ndb.insertInto(\"users\").values({ name: \"Alice\" }).orIgnore().toSQL()\n```\n\n---\n\n## UPDATE\n\n```ts\n// Basic update\ndb.update(\"users\")\n  .set({ active: false })\n  .where(({ id }) =\u003e id.eq(1))\n  .toSQL()\n\n// SET with expression\ndb.update(\"users\")\n  .setExpr(\"name\", val(\"Anonymous\"))\n  .where(({ active }) =\u003e active.eq(false))\n  .toSQL()\n\n// UPDATE ... FROM (PostgreSQL)\ndb.update(\"users\")\n  .set({ name: \"Bob\" })\n  .from(\"posts\")\n  .where(({ id }) =\u003e id.eq(1))\n  .toSQL()\n\n// UPDATE with JOIN (MySQL)\ndb.update(\"orders\").set({ total: 0 }).innerJoin(\"users\", onExpr).toSQL()\n\n// RETURNING\ndb.update(\"users\")\n  .set({ active: false })\n  .where(({ id }) =\u003e id.eq(1))\n  .returningAll()\n  .toSQL()\n\n// ORDER BY + LIMIT (MySQL)\ndb.update(\"users\").set({ active: false }).orderBy(\"id\").limit(lit(10)).toSQL()\n```\n\n---\n\n## DELETE\n\n```ts\ndb.deleteFrom(\"users\")\n  .where(({ id }) =\u003e id.eq(1))\n  .toSQL()\n\n// RETURNING\ndb.deleteFrom(\"users\")\n  .where(({ id }) =\u003e id.eq(1))\n  .returning(\"id\")\n  .toSQL()\n\n// DELETE ... USING (PostgreSQL)\ndb.deleteFrom(\"orders\").using(\"users\").where(onExpr).toSQL()\n\n// DELETE with JOIN (MySQL)\ndb.deleteFrom(\"orders\")\n  .innerJoin(\"users\", onExpr)\n  .where(({ id }) =\u003e id.eq(1))\n  .toSQL()\n```\n\n---\n\n## WHERE Conditions\n\nEvery `.where()` takes a callback with typed column proxies.\n\n### Comparisons\n\n```ts\n.where(({ age }) =\u003e age.eq(25))        // = 25\n.where(({ age }) =\u003e age.neq(0))        // != 0\n.where(({ age }) =\u003e age.gt(18))        // \u003e 18\n.where(({ age }) =\u003e age.gte(18))       // \u003e= 18\n.where(({ age }) =\u003e age.lt(65))        // \u003c 65\n.where(({ age }) =\u003e age.lte(65))       // \u003c= 65\n```\n\n### Pattern Matching\n\n```ts\n.where(({ name }) =\u003e name.like(\"%ali%\"))         // LIKE\n.where(({ name }) =\u003e name.notLike(\"%bob%\"))      // NOT LIKE\n.where(({ name }) =\u003e name.ilike(\"%alice%\"))      // ILIKE (PG)\n.where(({ email }) =\u003e email.notIlike(\"%spam%\"))  // NOT ILIKE\n```\n\n### Range \u0026 Lists\n\n```ts\n.where(({ age }) =\u003e age.between(18, 65))           // BETWEEN\n.where(({ age }) =\u003e age.notBetween(0, 17))         // NOT BETWEEN\n.where(({ age }) =\u003e age.betweenSymmetric(65, 18))  // BETWEEN SYMMETRIC (PG)\n.where(({ id }) =\u003e id.in([1, 2, 3]))               // IN\n.where(({ id }) =\u003e id.notIn([99, 100]))             // NOT IN\n```\n\n### Null Checks\n\n```ts\n.where(({ bio }) =\u003e bio.isNull())       // IS NULL\n.where(({ email }) =\u003e email.isNotNull()) // IS NOT NULL\n```\n\n### Null-Safe Comparisons\n\n```ts\n.where(({ age }) =\u003e age.isDistinctFrom(null))      // IS DISTINCT FROM\n.where(({ age }) =\u003e age.isNotDistinctFrom(25))     // IS NOT DISTINCT FROM\n```\n\n### IN Subquery\n\n```ts\nconst deptIds = db\n  .selectFrom(\"departments\")\n  .select(\"id\")\n  .build()\n\n  .where(({ dept_id }) =\u003e dept_id.inSubquery(deptIds)) // IN (SELECT ...)\n  .where(({ dept_id }) =\u003e dept_id.notInSubquery(deptIds)) // NOT IN (SELECT ...)\n```\n\n### Logical Combinators\n\n```ts\n// AND (variadic — 2 or more args)\n.where(({ age, active }) =\u003e\n  and(age.gt(18), active.eq(true)),\n)\n\n// AND with 3+ conditions\n.where(({ id, age, active }) =\u003e\n  and(id.gt(0), age.gt(18), active.eq(true)),\n)\n\n// OR (variadic)\n.where(({ name, email }) =\u003e\n  or(name.like(\"%alice%\"), email.like(\"%alice%\")),\n)\n\n// NOT\n.where(({ active }) =\u003e not(active.eq(true)))\n```\n\n### Multiple WHERE (implicit AND)\n\n```ts\n// Calling .where() multiple times ANDs conditions together\ndb.selectFrom(\"users\")\n  .select(\"id\")\n  .where(({ age }) =\u003e age.gt(18))\n  .where(({ active }) =\u003e active.eq(true))\n  .toSQL()\n// WHERE (\"age\" \u003e $1) AND (\"active\" = $2)\n```\n\n### Column-to-Column Comparisons\n\n```ts\n.where(({ price, cost }) =\u003e price.gtCol(cost))    // \"price\" \u003e \"cost\"\n.where(({ a, b }) =\u003e a.eqCol(b))                  // \"a\" = \"b\"\n.where(({ a, b }) =\u003e a.neqCol(b))                 // \"a\" != \"b\"\n.where(({ a, b }) =\u003e a.gteCol(b))                 // \"a\" \u003e= \"b\"\n.where(({ a, b }) =\u003e a.ltCol(b))                  // \"a\" \u003c \"b\"\n.where(({ a, b }) =\u003e a.lteCol(b))                 // \"a\" \u003c= \"b\"\n```\n\n---\n\n## Joins\n\n```ts\n// INNER JOIN\ndb.selectFrom(\"users\")\n  .innerJoin(\"posts\", ({ users, posts }) =\u003e users.id.eqCol(posts.userId))\n  .select(\"id\", \"title\")\n  .toSQL()\n\n// LEFT JOIN — joined columns become nullable\ndb.selectFrom(\"users\")\n  .leftJoin(\"posts\", ({ users, posts }) =\u003e users.id.eqCol(posts.userId))\n  .toSQL()\n\n// RIGHT JOIN\ndb.selectFrom(\"users\")\n  .rightJoin(\"posts\", ({ users, posts }) =\u003e users.id.eqCol(posts.userId))\n  .toSQL()\n\n// FULL JOIN — both sides nullable\ndb.selectFrom(\"users\")\n  .fullJoin(\"posts\", ({ users, posts }) =\u003e users.id.eqCol(posts.userId))\n  .toSQL()\n\n// CROSS JOIN\ndb.selectFrom(\"users\").crossJoin(\"posts\").toSQL()\n\n// LATERAL JOINs (correlated subqueries)\ndb.selectFrom(\"users\").innerJoinLateral(subquery, \"recent_posts\", onExpr).toSQL()\n\ndb.selectFrom(\"users\").leftJoinLateral(subquery, \"recent_posts\", onExpr).toSQL()\n\ndb.selectFrom(\"users\").crossJoinLateral(subquery, \"latest\").toSQL()\n```\n\n---\n\n## Expressions\n\n### Computed Columns\n\n```ts\nimport { val, cast, rawExpr } from \"sumak\"\n\n// Add a computed column with alias\ndb.selectFrom(\"users\").selectExpr(val(\"hello\"), \"greeting\").toSQL()\n\n// Multiple expressions at once\ndb.selectFrom(\"users\")\n  .selectExprs({\n    total: count(),\n    greeting: val(\"hello\"),\n  })\n  .toSQL()\n\n// CAST\ndb.selectFrom(\"users\")\n  .selectExpr(cast(val(42), \"text\"), \"idAsText\")\n  .toSQL()\n```\n\n### Arithmetic\n\n```ts\nimport { add, sub, mul, div, mod, neg } from \"sumak\"\n\ndb.selectFrom(\"orders\").selectExpr(mul(col.price, col.qty), \"total\").toSQL()\n// (\"price\" * \"qty\") AS \"total\"\n\ndb.selectFrom(\"orders\")\n  .selectExpr(add(col.price, val(10)), \"adjusted\")\n  .toSQL()\n```\n\n### CASE / WHEN\n\n```ts\nimport { case_, val } from \"sumak\"\n\ndb.selectFrom(\"users\")\n  .selectExpr(\n    case_()\n      .when(col.active.eq(true), val(\"active\"))\n      .when(col.active.eq(false), val(\"inactive\"))\n      .else_(val(\"unknown\"))\n      .end(),\n    \"status\",\n  )\n  .toSQL()\n```\n\n### JSON Operations\n\n```ts\nimport { jsonRef, jsonAgg, toJson, jsonBuildObject } from \"sumak\"\n\n// Access: -\u003e  (JSON object), -\u003e\u003e (text value)\ndb.selectFrom(\"users\")\n  .selectExpr(jsonRef(col.meta, \"name\", \"-\u003e\u003e\"), \"metaName\")\n  .toSQL()\n\n// JSON_AGG / TO_JSON\ndb.selectFrom(\"users\").selectExpr(jsonAgg(col.name), \"namesJson\").toSQL()\n\n// JSON_BUILD_OBJECT\ndb.selectFrom(\"users\")\n  .selectExpr(jsonBuildObject([\"name\", col.name], [\"age\", col.age]), \"obj\")\n  .toSQL()\n```\n\n### PostgreSQL Array Operators\n\n```ts\nimport { arrayContains, arrayContainedBy, arrayOverlaps, rawExpr } from \"sumak\"\n\n.where(() =\u003e arrayContains(rawExpr(\"tags\"), rawExpr(\"ARRAY['sql']\")))    // @\u003e\n.where(() =\u003e arrayContainedBy(rawExpr(\"tags\"), rawExpr(\"ARRAY[...]\")))   // \u003c@\n.where(() =\u003e arrayOverlaps(rawExpr(\"tags\"), rawExpr(\"ARRAY['sql']\")))    // \u0026\u0026\n```\n\n---\n\n## Aggregates\n\n```ts\nimport { count, countDistinct, sum, sumDistinct, avg, avgDistinct, min, max, coalesce } from \"sumak\"\n\ndb.selectFrom(\"users\").selectExpr(count(), \"total\").toSQL()\ndb.selectFrom(\"users\").selectExpr(countDistinct(col.dept), \"uniqueDepts\").toSQL()\ndb.selectFrom(\"orders\").selectExpr(sumDistinct(col.amount), \"uniqueSum\").toSQL()\ndb.selectFrom(\"orders\").selectExpr(avg(col.amount), \"avgAmount\").toSQL()\n\n// COALESCE (variadic)\ndb.selectFrom(\"users\")\n  .selectExpr(coalesce(col.nick, col.name, val(\"Anonymous\")), \"displayName\")\n  .toSQL()\n```\n\n### Aggregate with FILTER (PostgreSQL)\n\n```ts\nimport { filter, count } from \"sumak\"\n\ndb.selectFrom(\"users\").selectExpr(filter(count(), activeExpr), \"activeCount\").toSQL()\n// COUNT(*) FILTER (WHERE ...)\n```\n\n### Aggregate with ORDER BY\n\n```ts\nimport { stringAgg, arrayAgg } from \"sumak\"\n\n// STRING_AGG with ORDER BY\ndb.selectFrom(\"users\")\n  .selectExpr(stringAgg(col.name, \", \", [{ expr: col.name, direction: \"ASC\" }]), \"names\")\n  .toSQL()\n// STRING_AGG(\"name\", ', ' ORDER BY \"name\" ASC)\n\n// ARRAY_AGG\ndb.selectFrom(\"users\").selectExpr(arrayAgg(col.id), \"ids\").toSQL()\n```\n\n---\n\n## Window Functions\n\n```ts\nimport { over, rowNumber, rank, denseRank, lag, lead, ntile, count, sum } from \"sumak\"\n\n// ROW_NUMBER\ndb.selectFrom(\"employees\")\n  .selectExpr(\n    over(rowNumber(), (w) =\u003e w.partitionBy(\"dept\").orderBy(\"salary\", \"DESC\")),\n    \"rn\",\n  )\n  .toSQL()\n\n// RANK / DENSE_RANK\nover(rank(), (w) =\u003e w.orderBy(\"score\", \"DESC\"))\nover(denseRank(), (w) =\u003e w.orderBy(\"score\", \"DESC\"))\n\n// Running total with frame\nover(sum(col.amount), (w) =\u003e\n  w\n    .partitionBy(\"userId\")\n    .orderBy(\"createdAt\")\n    .rows({ type: \"unbounded_preceding\" }, { type: \"current_row\" }),\n)\n\n// RANGE / GROUPS frames\nover(count(), (w) =\u003e\n  w.orderBy(\"salary\").range({ type: \"preceding\", value: 100 }, { type: \"following\", value: 100 }),\n)\n\n// LAG / LEAD / NTILE\nover(lag(col.price, 1), (w) =\u003e w.orderBy(\"date\"))\nover(lead(col.price, 1), (w) =\u003e w.orderBy(\"date\"))\nover(ntile(4), (w) =\u003e w.orderBy(\"salary\", \"DESC\"))\n```\n\n---\n\n## SQL Functions\n\n### String\n\n```ts\nimport { upper, lower, concat, substring, trim, length } from \"sumak\"\n\nupper(col.name) // UPPER(\"name\")\nlower(col.email) // LOWER(\"email\")\nconcat(col.first, val(\" \"), col.last) // CONCAT(...)\nsubstring(col.name, 1, 3) // SUBSTRING(\"name\", 1, 3)\ntrim(col.name) // TRIM(\"name\")\nlength(col.name) // LENGTH(\"name\")\n```\n\n### Numeric\n\n```ts\nimport { abs, round, ceil, floor, greatest, least } from \"sumak\"\n\nabs(col.balance) // ABS(\"balance\")\nround(col.price, 2) // ROUND(\"price\", 2)\nceil(col.amount) // CEIL(\"amount\")\nfloor(col.amount) // FLOOR(\"amount\")\ngreatest(col.a, col.b) // GREATEST(\"a\", \"b\")\nleast(col.a, col.b) // LEAST(\"a\", \"b\")\n```\n\n### Conditional\n\n```ts\nimport { nullif, coalesce } from \"sumak\"\n\nnullif(col.age, val(0)) // NULLIF(\"age\", 0)\ncoalesce(col.nick, col.name, val(\"Anonymous\")) // COALESCE(...)\n```\n\n### Date/Time\n\n```ts\nimport { now, currentTimestamp } from \"sumak\"\n\nnow() // NOW()\ncurrentTimestamp() // CURRENT_TIMESTAMP()\n```\n\n---\n\n## Subqueries\n\n### EXISTS / NOT EXISTS\n\n```ts\nimport { exists, notExists } from \"sumak\"\n\ndb.selectFrom(\"users\")\n  .where(() =\u003e\n    exists(\n      db\n        .selectFrom(\"posts\")\n        .where(({ userId }) =\u003e userId.eq(1))\n        .build(),\n    ),\n  )\n  .toSQL()\n```\n\n### Derived Tables (Subquery in FROM)\n\n```ts\nconst sub = db\n  .selectFrom(\"users\")\n  .select(\"id\", \"name\")\n  .where(({ age }) =\u003e age.gt(18))\n\ndb.selectFromSubquery(sub, \"adults\").selectAll().toSQL()\n// SELECT * FROM (SELECT ...) AS \"adults\"\n```\n\n### IN Subquery\n\n```ts\nconst deptIds = db.selectFrom(\"departments\").select(\"id\").build()\n\ndb.selectFrom(\"users\")\n  .where(({ dept_id }) =\u003e dept_id.inSubquery(deptIds))\n  .toSQL()\n```\n\n---\n\n## Set Operations\n\n```ts\nconst active = db\n  .selectFrom(\"users\")\n  .select(\"id\")\n  .where(({ active }) =\u003e active.eq(true))\nconst premium = db\n  .selectFrom(\"users\")\n  .select(\"id\")\n  .where(({ tier }) =\u003e tier.eq(\"premium\"))\n\nactive.union(premium).toSQL() // UNION\nactive.unionAll(premium).toSQL() // UNION ALL\nactive.intersect(premium).toSQL() // INTERSECT\nactive.intersectAll(premium).toSQL() // INTERSECT ALL\nactive.except(premium).toSQL() // EXCEPT\nactive.exceptAll(premium).toSQL() // EXCEPT ALL\n```\n\n---\n\n## CTEs (WITH)\n\n```ts\nconst activeCte = db\n  .selectFrom(\"users\")\n  .where(({ active }) =\u003e active.eq(true))\n  .build()\n\ndb.selectFrom(\"users\").with(\"active_users\", activeCte).toSQL()\n\n// Recursive CTE\ndb.selectFrom(\"categories\").with(\"tree\", recursiveQuery, true).toSQL()\n```\n\n---\n\n## Conditional / Dynamic Queries\n\n### `$if()` — conditional clause\n\n```ts\nconst withFilter = true\nconst withOrder = false\n\ndb.selectFrom(\"users\")\n  .select(\"id\", \"name\")\n  .$if(withFilter, (qb) =\u003e qb.where(({ age }) =\u003e age.gt(18)))\n  .$if(withOrder, (qb) =\u003e qb.orderBy(\"name\"))\n  .toSQL()\n// WHERE applied, ORDER BY skipped\n```\n\n### `$call()` — reusable query fragments\n\n```ts\nconst withPagination = (qb) =\u003e qb.limit(10).offset(20)\nconst onlyActive = (qb) =\u003e qb.where(({ active }) =\u003e active.eq(true))\n\ndb.selectFrom(\"users\").select(\"id\", \"name\").$call(onlyActive).$call(withPagination).toSQL()\n```\n\n### `clear*()` — reset clauses\n\n```ts\ndb.selectFrom(\"users\")\n  .select(\"id\")\n  .orderBy(\"name\")\n  .clearOrderBy() // removes ORDER BY\n  .orderBy(\"id\", \"DESC\") // re-add different order\n  .toSQL()\n```\n\nAvailable: `clearWhere()`, `clearOrderBy()`, `clearLimit()`, `clearOffset()`, `clearGroupBy()`, `clearHaving()`, `clearSelect()`.\n\n---\n\n## Raw SQL\n\n### `sql` tagged template\n\n```ts\nimport { sql } from \"sumak\"\n\n// Primitives are parameterized\nsql`SELECT * FROM users WHERE name = ${\"Alice\"}`\n// params: [\"Alice\"]\n\n// Expressions are inlined\nsql`SELECT * FROM users WHERE active = ${val(true)}`\n// → ... WHERE active = TRUE\n\n// Helpers\nsql`SELECT ${sql.ref(\"id\")} FROM ${sql.table(\"users\", \"public\")}`\n// → SELECT \"id\" FROM \"public\".\"users\"\n\n// In queries\ndb.selectFrom(\"users\")\n  .selectExpr(sql`CURRENT_DATE`, \"today\")\n  .toSQL()\n```\n\n### `rawExpr()` escape hatch\n\n```ts\nimport { rawExpr } from \"sumak\"\n\n// In WHERE\ndb.selectFrom(\"users\")\n  .where(() =\u003e rawExpr\u003cboolean\u003e(\"age \u003e 18\"))\n  .toSQL()\n\n// In SELECT\ndb.selectFrom(\"users\").selectExpr(rawExpr\u003cnumber\u003e(\"EXTRACT(YEAR FROM created_at)\"), \"year\").toSQL()\n```\n\n---\n\n## ON CONFLICT / Upsert\n\n```ts\n// PostgreSQL: ON CONFLICT DO NOTHING\ndb.insertInto(\"users\")\n  .values({ name: \"Alice\", email: \"a@b.com\" })\n  .onConflictDoNothing(\"email\")\n  .toSQL()\n\n// ON CONFLICT DO UPDATE (with Expression)\ndb.insertInto(\"users\")\n  .values({ name: \"Alice\", email: \"a@b.com\" })\n  .onConflictDoUpdate([\"email\"], [{ column: \"name\", value: val(\"Updated\") }])\n  .toSQL()\n\n// ON CONFLICT DO UPDATE (with plain object — auto-parameterized)\ndb.insertInto(\"users\")\n  .values({ name: \"Alice\", email: \"a@b.com\" })\n  .onConflictDoUpdateSet([\"email\"], { name: \"Alice Updated\" })\n  .toSQL()\n\n// ON CONFLICT ON CONSTRAINT\ndb.insertInto(\"users\")\n  .values({ name: \"Alice\", email: \"a@b.com\" })\n  .onConflictConstraintDoNothing(\"users_email_key\")\n  .toSQL()\n\n// MySQL: ON DUPLICATE KEY UPDATE\ndb.insertInto(\"users\")\n  .values({ name: \"Alice\" })\n  .onDuplicateKeyUpdate([{ column: \"name\", value: val(\"Alice\") }])\n  .toSQL()\n```\n\n---\n\n## MERGE (SQL:2003)\n\n```ts\ndb.mergeInto(\"users\", \"staging\", \"s\", ({ target, source }) =\u003e target.id.eqCol(source.id))\n  .whenMatchedThenUpdate({ name: \"updated\" })\n  .whenNotMatchedThenInsert({ name: \"Alice\", email: \"a@b.com\" })\n  .toSQL()\n\n// Conditional delete\ndb.mergeInto(\"users\", \"staging\", \"s\", ({ target, source }) =\u003e target.id.eqCol(source.id))\n  .whenMatchedThenDelete()\n  .toSQL()\n```\n\n---\n\n## Row Locking\n\n```ts\ndb.selectFrom(\"users\").select(\"id\").forUpdate().toSQL() // FOR UPDATE\ndb.selectFrom(\"users\").select(\"id\").forShare().toSQL() // FOR SHARE\ndb.selectFrom(\"users\").select(\"id\").forNoKeyUpdate().toSQL() // FOR NO KEY UPDATE (PG)\ndb.selectFrom(\"users\").select(\"id\").forKeyShare().toSQL() // FOR KEY SHARE (PG)\n\n// Modifiers\ndb.selectFrom(\"users\").select(\"id\").forUpdate().skipLocked().toSQL() // SKIP LOCKED\ndb.selectFrom(\"users\").select(\"id\").forUpdate().noWait().toSQL() // NOWAIT\n```\n\n---\n\n## EXPLAIN\n\n```ts\ndb.selectFrom(\"users\").select(\"id\").explain().toSQL()\n// EXPLAIN SELECT \"id\" FROM \"users\"\n\ndb.selectFrom(\"users\").select(\"id\").explain({ analyze: true }).toSQL()\n// EXPLAIN ANALYZE SELECT ...\n\ndb.selectFrom(\"users\").select(\"id\").explain({ format: \"JSON\" }).toSQL()\n// EXPLAIN (FORMAT JSON) SELECT ...\n```\n\n---\n\n## Schema Builder (DDL)\n\nThe schema builder generates DDL SQL (CREATE, ALTER, DROP). It is separate from the query builder — you use `db.compileDDL(node)` to compile DDL nodes.\n\n### CREATE TABLE\n\n```ts\ndb.schema\n  .createTable(\"users\")\n  .ifNotExists()\n  .addColumn(\"id\", \"serial\", (c) =\u003e c.primaryKey())\n  .addColumn(\"name\", \"varchar(255)\", (c) =\u003e c.notNull())\n  .addColumn(\"email\", \"varchar\", (c) =\u003e c.unique().notNull())\n  .addColumn(\"active\", \"boolean\", (c) =\u003e c.defaultTo(lit(true)))\n  .build()\n\n// Foreign key with ON DELETE CASCADE\ndb.schema\n  .createTable(\"posts\")\n  .addColumn(\"id\", \"serial\", (c) =\u003e c.primaryKey())\n  .addColumn(\"user_id\", \"integer\", (c) =\u003e c.notNull().references(\"users\", \"id\").onDelete(\"CASCADE\"))\n  .build()\n\n// Composite primary key\ndb.schema\n  .createTable(\"order_items\")\n  .addColumn(\"order_id\", \"integer\")\n  .addColumn(\"product_id\", \"integer\")\n  .addPrimaryKeyConstraint(\"pk_order_items\", [\"order_id\", \"product_id\"])\n  .build()\n```\n\n### ALTER TABLE\n\n```ts\ndb.schema\n  .alterTable(\"users\")\n  .addColumn(\"age\", \"integer\", (c) =\u003e c.notNull())\n  .build()\n\ndb.schema.alterTable(\"users\").dropColumn(\"age\").build()\ndb.schema.alterTable(\"users\").renameColumn(\"name\", \"full_name\").build()\ndb.schema.alterTable(\"users\").renameTo(\"people\").build()\n\ndb.schema\n  .alterTable(\"users\")\n  .alterColumn(\"age\", { type: \"set_data_type\", dataType: \"bigint\" })\n  .build()\ndb.schema.alterTable(\"users\").alterColumn(\"name\", { type: \"set_not_null\" }).build()\n```\n\n### CREATE INDEX\n\n```ts\ndb.schema.createIndex(\"idx_users_name\").on(\"users\").column(\"name\").build()\ndb.schema.createIndex(\"uq_email\").unique().on(\"users\").column(\"email\").build()\n\n// Multi-column with direction\ndb.schema\n  .createIndex(\"idx_multi\")\n  .on(\"users\")\n  .column(\"last_name\", \"ASC\")\n  .column(\"age\", \"DESC\")\n  .build()\n\n// GIN index (PG)\ndb.schema.createIndex(\"idx_tags\").on(\"posts\").column(\"tags\").using(\"gin\").build()\n\n// Partial index\ndb.schema\n  .createIndex(\"idx_active\")\n  .on(\"users\")\n  .column(\"email\")\n  .where(rawExpr(\"active = true\"))\n  .build()\n```\n\n### CREATE VIEW\n\n```ts\ndb.schema.createView(\"active_users\").asSelect(selectQuery).build()\ndb.schema.createView(\"stats\").materialized().asSelect(selectQuery).build()\ndb.schema.createView(\"my_view\").orReplace().columns(\"id\", \"name\").asSelect(selectQuery).build()\n```\n\n### DROP\n\n```ts\ndb.schema.dropTable(\"users\").ifExists().cascade().build()\ndb.schema.dropIndex(\"idx_name\").ifExists().build()\ndb.schema.dropView(\"my_view\").materialized().ifExists().build()\n```\n\n### Auto-Generate from Schema\n\nThe schema you pass to `sumak({ tables })` can auto-generate CREATE TABLE SQL:\n\n```ts\nconst db = sumak({\n  dialect: pgDialect(),\n  tables: {\n    users: {\n      id: serial().primaryKey(),\n      name: text().notNull(),\n      email: text().notNull(),\n    },\n    posts: {\n      id: serial().primaryKey(),\n      title: text().notNull(),\n      userId: integer().references(\"users\", \"id\"),\n    },\n  },\n})\n\nconst ddl = db.generateDDL()\n// [\n//   { sql: 'CREATE TABLE \"users\" (\"id\" serial PRIMARY KEY NOT NULL, \"name\" text NOT NULL, \"email\" text NOT NULL)', params: [] },\n//   { sql: 'CREATE TABLE \"posts\" (\"id\" serial PRIMARY KEY NOT NULL, \"title\" text NOT NULL, \"userId\" integer REFERENCES \"users\"(\"id\"))', params: [] },\n// ]\n\n// With IF NOT EXISTS\nconst safeDDL = db.generateDDL({ ifNotExists: true })\n```\n\n\u003e Compile any DDL node: `db.compileDDL(node)` returns `{ sql, params }`.\n\n---\n\n## Full-Text Search\n\nDialect-aware — same API, different SQL per dialect:\n\n```ts\nimport { textSearch, val } from \"sumak\"\n\n// PostgreSQL: to_tsvector(\"name\") @@ to_tsquery('alice')\ndb.selectFrom(\"users\")\n  .where(({ name }) =\u003e textSearch([name.toExpr()], val(\"alice\")))\n  .toSQL()\n\n// MySQL: MATCH(`name`) AGAINST(? IN BOOLEAN MODE)\n// SQLite: (\"name\" MATCH ?)\n// MSSQL: CONTAINS(([name]), @p0)\n```\n\n---\n\n## Temporal Tables (SQL:2011)\n\n```ts\n// Point-in-time query\ndb.selectFrom(\"users\")\n  .forSystemTime({ kind: \"as_of\", timestamp: lit(\"2024-01-01\") })\n  .toSQL()\n\n// Time range\ndb.selectFrom(\"users\")\n  .forSystemTime({ kind: \"between\", start: lit(\"2024-01-01\"), end: lit(\"2024-12-31\") })\n  .toSQL()\n\n// Full history\ndb.selectFrom(\"users\").forSystemTime({ kind: \"all\" }).toSQL()\n```\n\nModes: `as_of`, `from_to`, `between`, `contained_in`, `all`.\n\n---\n\n## Plugins\n\n```ts\nimport { WithSchemaPlugin, SoftDeletePlugin, CamelCasePlugin } from \"sumak\"\n\nconst db = sumak({\n  dialect: pgDialect(),\n  plugins: [\n    new WithSchemaPlugin(\"public\"),      // auto \"public\".\"users\"\n    new SoftDeletePlugin({ tables: [\"users\"] }), // auto WHERE deleted_at IS NULL\n  ],\n  tables: { ... },\n})\n```\n\n---\n\n## Hooks\n\n```ts\n// Query logging\ndb.hook(\"query:after\", (ctx) =\u003e {\n  console.log(`[SQL] ${ctx.query.sql}`)\n})\n\n// Modify AST before compilation\ndb.hook(\"select:before\", (ctx) =\u003e {\n  // Add tenant isolation, audit filters, etc.\n})\n\n// Transform results\ndb.hook(\"result:transform\", (rows) =\u003e {\n  return rows.map(toCamelCase)\n})\n\n// Unregister\nconst off = db.hook(\"query:before\", handler)\noff()\n```\n\n---\n\n## Dialects\n\n4 dialects supported. Same query, different SQL:\n\n```ts\n// PostgreSQL  → SELECT \"id\" FROM \"users\" WHERE (\"id\" = $1)\n// MySQL       → SELECT `id` FROM `users` WHERE (`id` = ?)\n// SQLite      → SELECT \"id\" FROM \"users\" WHERE (\"id\" = ?)\n// MSSQL       → SELECT [id] FROM [users] WHERE ([id] = @p0)\n```\n\n```ts\nimport { pgDialect } from \"sumak/pg\"\nimport { mysqlDialect } from \"sumak/mysql\"\nimport { sqliteDialect } from \"sumak/sqlite\"\nimport { mssqlDialect } from \"sumak/mssql\"\n```\n\n### Tree Shaking\n\nImport only the dialect you need — unused dialects are eliminated:\n\n```ts\nimport { sumak } from \"sumak\"\nimport { pgDialect } from \"sumak/pg\"\nimport { serial, text } from \"sumak/schema\"\n```\n\n---\n\n## Architecture\n\nsumak uses a 5-layer pipeline. Your code never touches SQL strings — everything flows through an AST.\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│  1. SCHEMA                                                      │\n│     sumak({ dialect, tables: { users: { id: serial(), ... } } })│\n│     → DB type auto-inferred, zero codegen                       │\n├─────────────────────────────────────────────────────────────────┤\n│  2. BUILDER                                                     │\n│     db.selectFrom(\"users\").select(\"id\").where(...)              │\n│     → Immutable, chainable, fully type-checked                  │\n├─────────────────────────────────────────────────────────────────┤\n│  3. AST                                                         │\n│     .build() → SelectNode (frozen, discriminated union)         │\n│     → ~40 node types, Object.freeze on all outputs              │\n├─────────────────────────────────────────────────────────────────┤\n│  4. PLUGIN / HOOK                                               │\n│     Plugin.transformNode() → Hook \"query:before\"                │\n│     → AST rewriting, tenant isolation, soft delete, logging     │\n├─────────────────────────────────────────────────────────────────┤\n│  5. PRINTER                                                     │\n│     .toSQL() → { sql: \"SELECT ...\", params: [...] }             │\n│     → Dialect-specific: PG ($1), MySQL (?), MSSQL (@p0)        │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n### Why AST-first?\n\nThe query is never a string until the very last step. This means:\n\n- **Plugins can rewrite queries** — add WHERE clauses, prefix schemas, transform joins\n- **Hooks can inspect/modify** — logging, tracing, tenant isolation\n- **Printers are swappable** — same AST, different SQL per dialect\n- **No SQL injection** — values are always parameterized\n\n### Key design decisions\n\n- **Params at print time** — no global state, no index tracking during build\n- **Immutable builders** — every method returns a new instance\n- **Proxy-based column access** — `({ age }) =\u003e age.gt(18)` with full type safety\n- **Phantom types** — `Expression\u003cT\u003e` carries type info with zero runtime cost\n\n---\n\n## Acknowledgments\n\nsumak wouldn't exist without the incredible work of these projects:\n\n- **[Kysely](https://github.com/kysely-org/kysely)** — Pioneered the AST-first approach for TypeScript query builders. The `DB/TB/O` generic threading pattern, immutable builder design, and visitor-based printer architecture are directly inspired by Kysely.\n- **[Drizzle ORM](https://github.com/drizzle-team/drizzle-orm)** — Proved that schema-as-code (no codegen) is the right developer experience. The `defineTable()` + column builder pattern in sumak follows Drizzle's lead.\n- **[JOOQ](https://github.com/jOOQ/jOOQ)** — The original AST-first SQL builder (Java). Showed that a clean AST layer makes multi-dialect support elegant.\n- **[SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)** — Demonstrated that separating the expression layer from the ORM layer gives maximum flexibility.\n\n---\n\n## License\n\n[MIT](./LICENSE)\n","funding_links":["https://github.com/sponsors/productdevbook"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fproductdevbook%2Fsumak","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fproductdevbook%2Fsumak","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fproductdevbook%2Fsumak/lists"}