@b9g/zen 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.3] - 2025-12-22
9
+
10
+ ### Added
11
+
12
+ - Validation that compound constraints (`indexes`, `unique`, `references` options) have 2+ fields
13
+ - Single-field constraints now throw `TableDefinitionError` with helpful message pointing to field-level API
14
+
15
+ ### Fixed
16
+
17
+ - **TypeScript types now work in published package** - Updated libuild to fix module augmentation and `.d.ts` path resolution
18
+
19
+ ### Changed
20
+
21
+ - New tagline: "Define Zod tables. Write raw SQL. Get typed objects."
22
+
23
+ ### Documentation
24
+
25
+ - Added `.db.inserted()`, `.db.updated()`, `.db.upserted()` documentation with examples
26
+ - Added compound unique constraints example
27
+ - Fixed table naming consistency (all plural)
28
+ - Various README cleanups and improvements
29
+
30
+ ## [0.1.2] - 2025-12-22
31
+
32
+ ### Added
33
+
34
+ - New type exports: `PartialTable`, `DerivedTable`, `SetValues`, `FieldDBMeta`, `ReferenceInfo`, `CompoundReference`, `TaggedQuery`, `SQLDialect`, `isTable`
35
+ - Views documentation section in README
36
+ - `EnsureError` and `SchemaDriftError` documented in error types
37
+
38
+ ### Changed
39
+
40
+ - Reorganized `zen.ts` exports into logical groups
41
+ - README Types section now accurately reflects actual exports (removed non-existent `SQLFragment`, `DDLFragment`, `DBExpression`)
42
+
43
+ ### Fixed
44
+
45
+ - `isTable` type guard now exported (was missing)
46
+
8
47
  ## [0.1.1] - 2025-12-21
9
48
 
10
49
  ### Added
package/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # ZenDB
2
- The simple database client.
3
2
 
4
- Define tables. Write SQL. Get objects.
3
+ Define Zod tables. Write raw SQL. Get typed objects.
5
4
 
6
- Cultivate your data.
5
+ [Website](https://zendb.org) · [GitHub](https://github.com/bikeshaving/zen) · [npm](https://www.npmjs.com/package/@b9g/zen)
7
6
 
8
7
  ## Installation
9
8
 
@@ -48,7 +47,7 @@ import SQLiteDriver from "@b9g/zen/sqlite";
48
47
  const driver = new SQLiteDriver("file:app.db");
49
48
 
50
49
  // 1. Define tables
51
- const Users = table("users", {
50
+ let Users = table("users", {
52
51
  id: z.string().uuid().db.primary().db.auto(),
53
52
  email: z.string().email().db.unique(),
54
53
  name: z.string(),
@@ -58,7 +57,7 @@ const Posts = table("posts", {
58
57
  id: z.string().uuid().db.primary().db.auto(),
59
58
  authorId: z.string().uuid().db.references(Users, "author"),
60
59
  title: z.string(),
61
- published: z.boolean().default(false),
60
+ published: z.boolean().db.inserted(() => false),
62
61
  });
63
62
 
64
63
  // 2. Create database with migrations
@@ -74,14 +73,14 @@ db.addEventListener("upgradeneeded", (e) => {
74
73
  }
75
74
  if (e.oldVersion < 2) {
76
75
  // Evolve schema: add avatar column (safe, additive)
77
- const UsersV2 = table("users", {
76
+ Users = table("users", {
78
77
  id: z.string().uuid().db.primary().db.auto(),
79
78
  email: z.string().email().db.unique(),
80
79
  name: z.string(),
81
- avatar: z.string().optional(),
80
+ avatar: z.string().optional(), // new field
82
81
  });
83
- await db.ensureTable(UsersV2); // adds missing columns/indexes
84
- await db.ensureConstraints(UsersV2); // applies new constraints on existing data
82
+ await db.ensureTable(Users); // adds missing columns/indexes
83
+ await db.ensureConstraints(Users); // applies new constraints on existing data
85
84
  }
86
85
  })());
87
86
  });
@@ -111,6 +110,23 @@ const post = await db.get(Posts, posts[0].id);
111
110
  await db.update(Users, {name: "Alice Smith"}, user.id);
112
111
  ```
113
112
 
113
+ ## Why Zen?
114
+
115
+ Zen is the missing link between SQL and typed data. By writing tables with Zod schema, you get idempotent migration helpers, typed CRUD, normalized object references, and many features other database clients cannot provide.
116
+
117
+ ### What Zen is not:
118
+ - **Zen is not a query builder** — Rather than building SQL with fluent chains `.where().orderBy().limit()`, you write it directly with templates: `` db.all(Posts)`WHERE published = ${true} ORDER BY created_at DESC LIMIT 20` `` Helper functions help you write the tedious parts of SQL without hiding it or limiting your queries.
119
+ - **Zen is not an ORM** — Tables are not classes. They are Zod-powered singletons which provide schema-aware utilities. These tables can be used to validate writes, generate DDL, and deduplicate joined data.
120
+ - **Zen is not a startup** — Zen is an open-source library, not a venture-backed SaaS. There will never be a managed “ZenDB” instance or a “Zen Studio.” The library is a thin wrapper around Zod and JavaScript SQL drivers, with a focus on runtime abstractions rather than complicated tooling.
121
+
122
+ ### Safety
123
+
124
+ - **No lazy loading** — Related data comes from your JOINs
125
+ - **No ORM identity map** — Normalization is per-query, not session-wide
126
+ - **No down migrations** — Forward-only versioning (1 → 2 → 3)
127
+ - **No destructive helpers** — No `dropColumn()`, `dropTable()`, `renameColumn()`
128
+ - **No automatic migrations** — Schema changes are explicit in upgrade events
129
+
114
130
  ## Table Definitions
115
131
 
116
132
  ```typescript
@@ -121,7 +137,7 @@ const Users = table("users", {
121
137
  id: z.string().uuid().db.primary().db.auto(),
122
138
  email: z.string().email().db.unique(),
123
139
  name: z.string().max(100),
124
- role: z.enum(["user", "admin"]).default("user"),
140
+ role: z.enum(["user", "admin"]).db.inserted(() => "user"),
125
141
  createdAt: z.date().db.auto(),
126
142
  });
127
143
 
@@ -130,10 +146,23 @@ const Posts = table("posts", {
130
146
  title: z.string(),
131
147
  content: z.string().optional(),
132
148
  authorId: z.string().uuid().db.references(Users, "author", {onDelete: "cascade"}),
133
- published: z.boolean().default(false),
149
+ published: z.boolean().db.inserted(() => false),
134
150
  });
135
151
  ```
136
152
 
153
+ **Zod to Database Behavior:**
154
+
155
+ | Zod Method | Effect |
156
+ |------------|--------|
157
+ | `.optional()` | Column allows `NULL`; field omittable on insert |
158
+ | `.nullable()` | Column allows `NULL`; must explicitly pass `null` or value |
159
+ | `.string().max(n)` | `VARCHAR(n)` in DDL (if n ≤ 255) |
160
+ | `.string().uuid()` | Used by `.db.auto()` to generate UUIDs |
161
+ | `.number().int()` | `INTEGER` column type |
162
+ | `.date()` | `TIMESTAMPTZ` / `DATETIME` / `TEXT` depending on dialect |
163
+ | `.object()` / `.array()` | Stored as JSON, auto-encoded/decoded |
164
+ | `.default()` | **Throws error** — use `.db.inserted()` instead |
165
+
137
166
  **The `.db` namespace:**
138
167
 
139
168
  The `.db` property is available on all Zod types imported from `@b9g/zen`. It provides database-specific modifiers:
@@ -176,10 +205,37 @@ const Users = table("users", {
176
205
 
177
206
  // id and createdAt are optional - auto-generated if not provided
178
207
  const user = await db.insert(Users, {name: "Alice"});
179
- user.id; // "550e8400-e29b-41d4-a716-446655440000"
208
+ user.id; // "550e8400-e29b-41d4-a716-446655440000"
180
209
  user.createdAt; // 2024-01-15T10:30:00.000Z
181
210
  ```
182
211
 
212
+ **Default values with `.db.inserted()`, `.db.updated()`, `.db.upserted()`:**
213
+
214
+ These methods set default values for write operations. They accept JS functions or SQL builtins (`NOW`, `TODAY`, `CURRENT_TIMESTAMP`, `CURRENT_DATE`, `CURRENT_TIME`):
215
+
216
+ ```typescript
217
+ import {z, table, NOW} from "@b9g/zen";
218
+
219
+ const Posts = table("posts", {
220
+ id: z.string().uuid().db.primary().db.auto(),
221
+ title: z.string(),
222
+ // JS function — runs client-side
223
+ slug: z.string().db.inserted(() => generateSlug()),
224
+ // SQL builtin — runs database-side
225
+ createdAt: z.date().db.inserted(NOW),
226
+ updatedAt: z.date().db.upserted(NOW), // set on insert AND update
227
+ viewCount: z.number().db.inserted(() => 0).db.updated(() => 0), // reset on update
228
+ });
229
+ ```
230
+
231
+ | Method | When applied | Field becomes optional for |
232
+ |--------|--------------|---------------------------|
233
+ | `.db.inserted(value)` | INSERT only | insert |
234
+ | `.db.updated(value)` | UPDATE only | update |
235
+ | `.db.upserted(value)` | INSERT and UPDATE | insert and update |
236
+
237
+ **Why not Zod's `.default()`?** Zod's `.default()` applies at *parse time*, not *write time*. This means defaults would be applied when reading data, not when inserting. Zen throws an error if you use `.default()` — use `.db.inserted()` instead.
238
+
183
239
  **Automatic JSON encoding/decoding:**
184
240
 
185
241
  Objects (`z.object()`) and arrays (`z.array()`) are automatically serialized to JSON when stored and parsed back when read:
@@ -200,7 +256,7 @@ const settings = await db.insert(Settings, {
200
256
 
201
257
  // On read: JSON strings are parsed back to objects/arrays
202
258
  settings.config.theme; // "dark" (object, not string)
203
- settings.tags[0]; // "admin" (array, not string)
259
+ settings.tags[0]; // "admin" (array, not string)
204
260
  ```
205
261
 
206
262
  **Custom encoding/decoding:**
@@ -208,7 +264,7 @@ settings.tags[0]; // "admin" (array, not string)
208
264
  Override automatic JSON encoding with custom transformations:
209
265
 
210
266
  ```typescript
211
- const Custom = table("custom", {
267
+ const Articles = table("articles", {
212
268
  id: z.string().db.primary(),
213
269
  // Store array as CSV instead of JSON
214
270
  tags: z.array(z.string())
@@ -217,8 +273,8 @@ const Custom = table("custom", {
217
273
  .db.type("TEXT"), // Required: explicit column type for DDL
218
274
  });
219
275
 
220
- await db.insert(Custom, {id: "c1", tags: ["a", "b", "c"]});
221
- // Stored as: tags='a,b,c' (not '["a","b","c"]')
276
+ await db.insert(Articles, {id: "a1", tags: ["news", "tech", "featured"]});
277
+ // Stored as: tags='news,tech,featured' (not '["news","tech","featured"]')
222
278
  ```
223
279
 
224
280
  **Explicit column types:**
@@ -263,14 +319,38 @@ const posts = await db.all([Posts, Users.active])`
263
319
  `;
264
320
  ```
265
321
 
266
- **Compound indexes** via table options:
322
+ **Compound indexes and unique constraints** via table options:
267
323
  ```typescript
268
324
  const Posts = table("posts", {
269
325
  id: z.string().db.primary(),
270
326
  authorId: z.string(),
327
+ slug: z.string(),
271
328
  createdAt: z.date(),
272
329
  }, {
273
330
  indexes: [["authorId", "createdAt"]],
331
+ unique: [["authorId", "slug"]], // unique together
332
+ });
333
+ ```
334
+
335
+ **Compound foreign keys** for composite primary keys:
336
+ ```typescript
337
+ const OrderProducts = table("order_products", {
338
+ orderId: z.string().uuid(),
339
+ productId: z.string().uuid(),
340
+ // ... compound primary key
341
+ });
342
+
343
+ const OrderItems = table("order_items", {
344
+ id: z.string().uuid().db.primary(),
345
+ orderId: z.string().uuid(),
346
+ productId: z.string().uuid(),
347
+ quantity: z.number(),
348
+ }, {
349
+ references: [{
350
+ fields: ["orderId", "productId"],
351
+ table: OrderProducts,
352
+ as: "orderProduct",
353
+ }],
274
354
  });
275
355
  ```
276
356
 
@@ -286,39 +366,94 @@ const Posts = table("posts", {
286
366
  derive: {
287
367
  // Pure functions only (no I/O, no side effects)
288
368
  titleUpper: (post) => post.title.toUpperCase(),
289
- tags: (post) => post.postTags?.map(pt => pt.tag) ?? [],
369
+ // Traverse relationships (requires JOIN in query)
370
+ tags: (post) => post.postTags?.map(pt => pt.tag?.name) ?? [],
290
371
  }
291
372
  });
292
373
 
293
- const posts = await db.all([Posts, Users])`
294
- JOIN "users" ON ${Users.on(Posts)}
374
+ type Post = Row<typeof Posts>;
375
+ // Post includes: id, title, authorId, titleUpper, tags
376
+
377
+ const posts = await db.all([Posts, Users, PostTags, Tags])`
378
+ JOIN ${Users} ON ${Users.on(Posts)}
379
+ LEFT JOIN ${PostTags} ON ${PostTags.cols.postId} = ${Posts.cols.id}
380
+ LEFT JOIN ${Tags} ON ${Tags.on(PostTags)}
295
381
  `;
296
382
 
297
- type PostWithDerived = Row<typeof Posts> & {titleUpper: string; tags: string[]};
298
- const post = posts[0] as PostWithDerived;
299
- post.titleUpper; // "HELLO WORLD"
300
- Object.keys(post); // ["id", "title", "authorId", "author"] (no "titleUpper")
301
- JSON.stringify(post); // Excludes derived properties (non-enumerable)
383
+ const post = posts[0];
384
+ post.titleUpper; // "HELLO WORLD" — typed as string
385
+ post.tags; // ["javascript", "typescript"] — traverses relationships
386
+ Object.keys(post); // ["id", "title", "authorId", "author"] (no derived props)
387
+ JSON.stringify(post); // Excludes derived properties (non-enumerable)
302
388
  ```
303
389
 
304
390
  Derived properties:
305
391
  - Are lazy getters (computed on access, not stored)
306
392
  - Are non-enumerable (hidden from `Object.keys()` and `JSON.stringify()`)
307
393
  - Must be pure functions (no I/O, no database queries)
308
- - Only transform data already in the entity
309
- - Are NOT part of TypeScript type inference
394
+ - Can traverse resolved relationships from the same query
395
+ - Are fully typed via `Row<T>` inference
310
396
 
311
397
  **Partial selects** with `pick()`:
312
398
  ```typescript
313
- const UserSummary = Users.pick("id", "name");
314
- const posts = await db.all([Posts, UserSummary])`
315
- JOIN "users" ON ${UserSummary.on(Posts)}
399
+ const UserSummaries = Users.pick("id", "name");
400
+ const posts = await db.all([Posts, UserSummaries])`
401
+ JOIN ${UserSummaries} ON ${UserSummaries.on(Posts)}
316
402
  `;
317
403
  // posts[0].author has only id and name
318
404
  ```
319
405
 
320
406
  **Table identity**: A table definition is a singleton value which is passed to database methods for validation, normalization, schema management, and convenient CRUD operations. It is not a class.
321
407
 
408
+ ## Views
409
+
410
+ Views are read-only projections of tables with predefined WHERE clauses:
411
+
412
+ ```typescript
413
+ import {z, table, view} from "@b9g/zen";
414
+
415
+ const Users = table("users", {
416
+ id: z.string().db.primary(),
417
+ name: z.string(),
418
+ role: z.enum(["user", "admin"]),
419
+ deletedAt: z.date().nullable().db.softDelete(),
420
+ });
421
+
422
+ // Define views with explicit names
423
+ const ActiveUsers = view("active_users", Users)`
424
+ WHERE ${Users.cols.deletedAt} IS NULL
425
+ `;
426
+
427
+ const AdminUsers = view("admin_users", Users)`
428
+ WHERE ${Users.cols.role} = ${"admin"}
429
+ `;
430
+
431
+ // Query from views (same API as tables)
432
+ const admins = await db.all(AdminUsers)``;
433
+ const admin = await db.get(AdminUsers, "u1");
434
+
435
+ // Views are read-only — mutations throw errors
436
+ await db.insert(AdminUsers, {...}); // ✗ Error
437
+ await db.update(AdminUsers, {...}); // ✗ Error
438
+ await db.delete(AdminUsers, "u1"); // ✗ Error
439
+ ```
440
+
441
+ **Auto-generated `.active` view:** Tables with a `.db.softDelete()` field automatically get an `.active` view:
442
+
443
+ ```typescript
444
+ // Equivalent to: view("users_active", Users)`WHERE deletedAt IS NULL`
445
+ const activeUsers = await db.all(Users.active)``;
446
+ ```
447
+
448
+ **Views preserve table relationships:** Views inherit references from their base table, so JOINs work identically:
449
+
450
+ ```typescript
451
+ const posts = await db.all([Posts, AdminUsers])`
452
+ JOIN "admin_users" ON ${AdminUsers.on(Posts)}
453
+ `;
454
+ posts[0].author?.role; // "admin"
455
+ ```
456
+
322
457
  ## Queries
323
458
 
324
459
  Tagged templates with automatic parameterization:
@@ -355,6 +490,29 @@ await db.exec`CREATE INDEX idx_posts_author ON ${Posts}(${Posts.cols.authorId})`
355
490
  const count = await db.val<number>`SELECT COUNT(*) FROM ${Posts}`;
356
491
  ```
357
492
 
493
+ ## CRUD Helpers
494
+ ```typescript
495
+ // Insert with Zod validation (uses RETURNING to get actual row)
496
+ const user = await db.insert(Users, {
497
+ email: "alice@example.com",
498
+ name: "Alice",
499
+ });
500
+ // Returns actual row from DB, including auto-generated id and DB-computed defaults
501
+ const userId = user.id;
502
+
503
+ // Update by primary key (uses RETURNING)
504
+ const updated = await db.update(Users, {name: "Bob"}, userId);
505
+
506
+ // Delete by primary key
507
+ await db.delete(Users, userId);
508
+
509
+ // Soft delete (sets deletedAt timestamp, requires softDelete() field)
510
+ await db.softDelete(Users, userId);
511
+ ```
512
+
513
+ **RETURNING support:** `insert()` and `update()` use `RETURNING *` on SQLite and PostgreSQL to return the actual row from the database, including DB-computed defaults and triggers. MySQL falls back to a separate SELECT.
514
+
515
+
358
516
  ## Fragment Helpers
359
517
 
360
518
  Type-safe SQL fragments as methods on Table objects:
@@ -404,29 +562,6 @@ const posts = await db.all(Posts)`WHERE ${Posts.in("id", [])}`;
404
562
  // → WHERE 1 = 0
405
563
  ```
406
564
 
407
- ## CRUD Helpers
408
-
409
- ```typescript
410
- // Insert with Zod validation (uses RETURNING to get actual row)
411
- const user = await db.insert(Users, {
412
- email: "alice@example.com",
413
- name: "Alice",
414
- });
415
- // Returns actual row from DB, including auto-generated id and DB-computed defaults
416
- const userId = user.id;
417
-
418
- // Update by primary key (uses RETURNING)
419
- const updated = await db.update(Users, {name: "Bob"}, userId);
420
-
421
- // Delete by primary key
422
- await db.delete(Users, userId);
423
-
424
- // Soft delete (sets deletedAt timestamp, requires softDelete() field)
425
- await db.softDelete(Users, userId);
426
- ```
427
-
428
- **RETURNING support:** `insert()` and `update()` use `RETURNING *` on SQLite and PostgreSQL to return the actual row from the database, including DB-computed defaults and triggers. MySQL falls back to a separate SELECT.
429
-
430
565
  ## Transactions
431
566
 
432
567
  ```typescript
@@ -492,7 +627,7 @@ zen provides idempotent helpers that encourage safe, additive-only migrations:
492
627
  const Posts = table("posts", {
493
628
  id: z.string().db.primary(),
494
629
  title: z.string(),
495
- views: z.number().default(0), // NEW - add to schema
630
+ views: z.number().db.inserted(() => 0), // NEW - add to schema
496
631
  });
497
632
 
498
633
  if (e.oldVersion < 2) {
@@ -703,6 +838,7 @@ const refs = Posts.references(); // [{fieldName: "authorId", table: Users,
703
838
  - Tagged template queries are cached by template object identity (compiled once per call site)
704
839
  - Normalization cost is O(rows) with hash maps per table
705
840
  - Reference resolution is zero-cost after deduplication
841
+ - Zod validation happens on writes, never on reads.
706
842
 
707
843
  ## Driver Interface
708
844
 
@@ -734,6 +870,8 @@ interface Driver {
734
870
 
735
871
  **Migration locking**: If the driver provides `withMigrationLock()`, migrations run atomically (PostgreSQL uses advisory locks, MySQL uses `GET_LOCK`, SQLite uses exclusive transactions).
736
872
 
873
+ **Connection pooling**: Handled by the underlying driver. `postgres.js` and `mysql2` pool automatically; `better-sqlite3` uses a single connection (SQLite is single-writer anyway).
874
+
737
875
  ## Error Handling
738
876
 
739
877
  All errors extend `DatabaseError` with typed error codes:
@@ -783,6 +921,8 @@ await db.transaction(async (tx) => {
783
921
  - `AlreadyExistsError` — Unique constraint violated (tableName, field, value)
784
922
  - `QueryError` — SQL execution failed (sql)
785
923
  - `MigrationError` / `MigrationLockError` — Migration failures (fromVersion, toVersion)
924
+ - `EnsureError` — Schema ensure operation failed (operation, table, step)
925
+ - `SchemaDriftError` — Existing schema doesn't match definition (table, drift)
786
926
  - `ConnectionError` / `TransactionError` — Connection/transaction issues
787
927
 
788
928
  ## Debugging
@@ -851,40 +991,41 @@ Override with `.db.type("CUSTOM")` when using custom encode/decode.
851
991
  ```typescript
852
992
  import {
853
993
  // Zod (extended with .db namespace)
854
- z, // Re-exported Zod with .db already available
994
+ z, // Re-exported Zod with .db already available
995
+ extendZod, // Extend a separate Zod instance (advanced)
855
996
 
856
- // Table definition
857
- table, // Create a table definition from Zod schema
858
- isTable, // Type guard for Table objects
859
- extendZod, // Extend a separate Zod instance (advanced)
997
+ // Table and view definition
998
+ table, // Create a table definition from Zod schema
999
+ view, // Create a read-only view from a table
1000
+ isTable, // Type guard for Table objects
1001
+ isView, // Type guard for View objects
860
1002
 
861
1003
  // Database
862
- Database, // Main database class
863
- Transaction, // Transaction context (passed to transaction callbacks)
864
- DatabaseUpgradeEvent, // Event object for "upgradeneeded" handler
1004
+ Database, // Main database class
1005
+ Transaction, // Transaction context (passed to transaction callbacks)
1006
+ DatabaseUpgradeEvent, // Event object for "upgradeneeded" handler
865
1007
 
866
- // DB expressions
867
- db, // Runtime DB expressions (db.now(), db.json(), etc.)
868
- isDBExpression, // Type guard for DBExpression objects
869
-
870
- // Custom field helpers
871
- setDBMeta, // Set database metadata on a Zod schema
872
- getDBMeta, // Get database metadata from a Zod schema
1008
+ // SQL builtins (for .db.inserted() / .db.updated())
1009
+ NOW, // CURRENT_TIMESTAMP alias
1010
+ TODAY, // CURRENT_DATE alias
1011
+ CURRENT_TIMESTAMP, // SQL CURRENT_TIMESTAMP
1012
+ CURRENT_DATE, // SQL CURRENT_DATE
1013
+ CURRENT_TIME, // SQL CURRENT_TIME
873
1014
 
874
1015
  // Errors
875
- DatabaseError, // Base error class
876
- ValidationError, // Schema validation failed
877
- TableDefinitionError, // Invalid table definition
878
- MigrationError, // Migration failed
879
- MigrationLockError, // Failed to acquire migration lock
880
- QueryError, // SQL execution failed
881
- NotFoundError, // Entity not found
882
- AlreadyExistsError, // Unique constraint violated
1016
+ DatabaseError, // Base error class
1017
+ ValidationError, // Schema validation failed
1018
+ TableDefinitionError, // Invalid table definition
1019
+ MigrationError, // Migration failed
1020
+ MigrationLockError, // Failed to acquire migration lock
1021
+ QueryError, // SQL execution failed
1022
+ NotFoundError, // Entity not found
1023
+ AlreadyExistsError, // Unique constraint violated
883
1024
  ConstraintViolationError, // Database constraint violated
884
- ConnectionError, // Connection failed
885
- TransactionError, // Transaction failed
886
- isDatabaseError, // Type guard for DatabaseError
887
- hasErrorCode, // Check error code
1025
+ ConnectionError, // Connection failed
1026
+ TransactionError, // Transaction failed
1027
+ isDatabaseError, // Type guard for DatabaseError
1028
+ hasErrorCode, // Check error code
888
1029
  } from "@b9g/zen";
889
1030
  ```
890
1031
 
@@ -893,38 +1034,34 @@ import {
893
1034
  ```typescript
894
1035
  import type {
895
1036
  // Table types
896
- Table, // Table definition object
897
- PartialTable, // Table created via .pick()
898
- DerivedTable, // Table with derived fields via .derive()
899
- TableOptions, // Options for table()
900
- ReferenceInfo, // Foreign key reference metadata
901
- CompoundReference, // Compound foreign key reference
1037
+ Table, // Table definition object
1038
+ PartialTable, // Table created via .pick()
1039
+ DerivedTable, // Table with derived fields via .derive()
1040
+ TableOptions, // Options for table()
1041
+ ReferenceInfo, // Foreign key reference metadata
1042
+ CompoundReference, // Compound foreign key reference
902
1043
 
903
1044
  // Field types
904
- FieldMeta, // Field metadata for form generation
905
- FieldType, // Field type enum
906
- FieldDBMeta, // Database-specific field metadata
1045
+ FieldMeta, // Field metadata for form generation
1046
+ FieldType, // Field type enum
1047
+ FieldDBMeta, // Database-specific field metadata
907
1048
 
908
1049
  // Type inference
909
- Row, // Infer row type from Table (after read)
910
- Insert, // Infer insert type from Table (respects defaults/.db.auto())
911
- Update, // Infer update type from Table (all fields optional)
1050
+ Row, // Infer row type from Table (after read)
1051
+ Insert, // Infer insert type from Table (respects defaults/.db.auto())
1052
+ Update, // Infer update type from Table (all fields optional)
912
1053
 
913
1054
  // Fragment types
914
- SetValues, // Values accepted by Table.set()
915
- SQLFragment, // SQL fragment object
916
- DDLFragment, // DDL fragment object
917
- SQLDialect, // "sqlite" | "postgresql" | "mysql"
1055
+ SetValues, // Values accepted by Table.set()
1056
+ SQLTemplate, // SQL template object (return type of set(), on(), etc.)
1057
+ SQLDialect, // "sqlite" | "postgresql" | "mysql"
918
1058
 
919
1059
  // Driver types
920
- Driver, // Driver interface for adapters
921
- TaggedQuery, // Tagged template query function
922
-
923
- // Expression types
924
- DBExpression, // Runtime database expression
1060
+ Driver, // Driver interface for adapters
1061
+ TaggedQuery, // Tagged template query function
925
1062
 
926
1063
  // Error types
927
- DatabaseErrorCode, // Error code string literals
1064
+ DatabaseErrorCode, // Error code string literals
928
1065
  } from "@b9g/zen";
929
1066
  ```
930
1067
 
@@ -1040,25 +1177,3 @@ import PostgresDriver from "@b9g/zen/postgres";
1040
1177
  // MySQL (mysql2)
1041
1178
  import MySQLDriver from "@b9g/zen/mysql";
1042
1179
  ```
1043
-
1044
- ## What This Library Does Not Do
1045
-
1046
- **Query Generation:**
1047
- - **No model classes** — Tables are plain definitions, not class instances
1048
- - **No hidden JOINs** — You write all SQL explicitly
1049
- - **No implicit query building** — No `.where().orderBy().limit()` chains
1050
- - **No lazy loading** — Related data comes from your JOINs
1051
- - **No ORM identity map** — Normalization is per-query, not session-wide
1052
-
1053
- **Migrations:**
1054
- - **No down migrations** — Forward-only, monotonic versioning (1 → 2 → 3)
1055
- - **No destructive helpers** — No `dropColumn()`, `dropTable()`, `renameColumn()` methods
1056
- - **No automatic migrations** — DDL must be written explicitly in upgrade events
1057
- - **No migration files** — Event handlers replace traditional migration folders
1058
- - **No branching versions** — Linear version history only
1059
-
1060
- **Safety Philosophy:**
1061
- - Migrations are **additive and idempotent** by design
1062
- - Use `ensureColumn()`, `ensureIndex()`, `copyColumn()` for safe schema changes
1063
- - Breaking changes require multi-step migrations (add, migrate data, deprecate)
1064
- - Version numbers never decrease — rollbacks are new forward migrations
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@b9g/zen",
3
- "version": "0.1.1",
4
- "description": "The simple database client. Define tables. Write SQL. Get objects.",
3
+ "version": "0.1.3",
4
+ "description": "Define Zod tables. Write raw SQL. Get typed objects.",
5
5
  "keywords": [
6
6
  "database",
7
7
  "sql",
@@ -12,12 +12,21 @@
12
12
  "postgres",
13
13
  "mysql"
14
14
  ],
15
+ "author": "Brian Kim <briankimdev@gmail.com>",
15
16
  "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/bikeshaving/zen.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/bikeshaving/zen/issues"
23
+ },
24
+ "homepage": "https://zendb.org",
16
25
  "dependencies": {
17
26
  "zod": "^4.0.0"
18
27
  },
19
28
  "devDependencies": {
20
- "@b9g/libuild": "^0.1.20",
29
+ "@b9g/libuild": "^0.1.21",
21
30
  "@eslint/js": "^9.39.2",
22
31
  "@types/better-sqlite3": "^7.6.0",
23
32
  "@types/bun": "^1.3.4",
@@ -2,7 +2,7 @@ import {
2
2
  isSQLBuiltin,
3
3
  isSQLIdentifier,
4
4
  resolveSQLBuiltin
5
- } from "./chunk-XHXMCOSW.js";
5
+ } from "./chunk-DKLSJISE.js";
6
6
 
7
7
  // src/impl/sql.ts
8
8
  function quoteIdent(name, dialect) {
@@ -4,7 +4,7 @@ import {
4
4
  getViewMeta,
5
5
  ident,
6
6
  makeTemplate
7
- } from "./chunk-XHXMCOSW.js";
7
+ } from "./chunk-DKLSJISE.js";
8
8
 
9
9
  // src/impl/ddl.ts
10
10
  import { z } from "zod";