@b9g/zen 0.1.2 → 0.1.4
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 +39 -0
- package/README.md +155 -139
- package/package.json +3 -3
- package/{chunk-NBXBBEMA.js → src/_chunks/chunk-2C6KOX4F.js} +1 -1
- package/{chunk-ARUUB3H4.js → src/_chunks/chunk-2R6FDKLS.js} +1 -1
- package/{chunk-BEX6VPES.js → src/_chunks/chunk-DKLSJISE.js} +24 -0
- package/{ddl-OT6HPLQY.js → src/_chunks/ddl-32B7E53E.js} +2 -2
- package/src/bun.d.ts +1 -0
- package/src/bun.js +16 -5
- package/src/impl/builtins.d.ts +52 -0
- package/src/impl/database.d.ts +511 -0
- package/src/impl/ddl.d.ts +34 -0
- package/src/impl/errors.d.ts +195 -0
- package/src/impl/query.d.ts +249 -0
- package/src/impl/sql.d.ts +47 -0
- package/src/impl/table.d.ts +961 -0
- package/src/impl/template.d.ts +75 -0
- package/src/mysql.d.ts +6 -0
- package/src/mysql.js +13 -6
- package/src/postgres.d.ts +6 -0
- package/src/postgres.js +15 -6
- package/src/sqlite.d.ts +6 -0
- package/src/sqlite.js +9 -5
- package/src/zen.js +24 -30
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.4] - 2025-12-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `db.explain()` - Get query execution plan (EXPLAIN QUERY PLAN for SQLite, EXPLAIN for PostgreSQL/MySQL)
|
|
13
|
+
- `explain()` method on Driver interface - each driver owns its dialect-specific EXPLAIN syntax
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- README documented fake APIs that never existed (`Users.ddl()`, `Posts.ensureColumn()`, `Posts.ensureIndex()`, `Users.copyColumn()`)
|
|
18
|
+
- README now documents the real APIs: `db.ensureTable()`, `db.ensureView()`, `db.ensureConstraints()`, `db.copyColumn()`
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `getColumns()` is now required on Driver interface (was optional with fallback)
|
|
23
|
+
- Removed dialect-switching logic from database.ts - drivers own all dialect-specific behavior
|
|
24
|
+
|
|
25
|
+
## [0.1.3] - 2025-12-22
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Validation that compound constraints (`indexes`, `unique`, `references` options) have 2+ fields
|
|
30
|
+
- Single-field constraints now throw `TableDefinitionError` with helpful message pointing to field-level API
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **TypeScript types now work in published package** - Updated libuild to fix module augmentation and `.d.ts` path resolution
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- New tagline: "Define Zod tables. Write raw SQL. Get typed objects."
|
|
39
|
+
|
|
40
|
+
### Documentation
|
|
41
|
+
|
|
42
|
+
- Added `.db.inserted()`, `.db.updated()`, `.db.upserted()` documentation with examples
|
|
43
|
+
- Added compound unique constraints example
|
|
44
|
+
- Fixed table naming consistency (all plural)
|
|
45
|
+
- Various README cleanups and improvements
|
|
46
|
+
|
|
8
47
|
## [0.1.2] - 2025-12-22
|
|
9
48
|
|
|
10
49
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ZenDB
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Define Zod tables. Write raw SQL. Get typed objects.
|
|
4
4
|
|
|
5
5
|
[Website](https://zendb.org) · [GitHub](https://github.com/bikeshaving/zen) · [npm](https://www.npmjs.com/package/@b9g/zen)
|
|
6
6
|
|
|
@@ -47,7 +47,7 @@ import SQLiteDriver from "@b9g/zen/sqlite";
|
|
|
47
47
|
const driver = new SQLiteDriver("file:app.db");
|
|
48
48
|
|
|
49
49
|
// 1. Define tables
|
|
50
|
-
|
|
50
|
+
let Users = table("users", {
|
|
51
51
|
id: z.string().uuid().db.primary().db.auto(),
|
|
52
52
|
email: z.string().email().db.unique(),
|
|
53
53
|
name: z.string(),
|
|
@@ -73,14 +73,14 @@ db.addEventListener("upgradeneeded", (e) => {
|
|
|
73
73
|
}
|
|
74
74
|
if (e.oldVersion < 2) {
|
|
75
75
|
// Evolve schema: add avatar column (safe, additive)
|
|
76
|
-
|
|
76
|
+
Users = table("users", {
|
|
77
77
|
id: z.string().uuid().db.primary().db.auto(),
|
|
78
78
|
email: z.string().email().db.unique(),
|
|
79
79
|
name: z.string(),
|
|
80
|
-
avatar: z.string().optional(),
|
|
80
|
+
avatar: z.string().optional(), // new field
|
|
81
81
|
});
|
|
82
|
-
await db.ensureTable(
|
|
83
|
-
await db.ensureConstraints(
|
|
82
|
+
await db.ensureTable(Users); // adds missing columns/indexes
|
|
83
|
+
await db.ensureConstraints(Users); // applies new constraints on existing data
|
|
84
84
|
}
|
|
85
85
|
})());
|
|
86
86
|
});
|
|
@@ -115,14 +115,8 @@ await db.update(Users, {name: "Alice Smith"}, user.id);
|
|
|
115
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
116
|
|
|
117
117
|
### What Zen is not:
|
|
118
|
-
- **Zen is not a query builder** — Rather than
|
|
119
|
-
|
|
120
|
-
db.get(Posts)`
|
|
121
|
-
WHERE ${Posts.cols.published} = ${true}
|
|
122
|
-
ORDER BY ${Posts.cols.publishDate}
|
|
123
|
-
DESC LIMIT 20
|
|
124
|
-
```.
|
|
125
|
-
- **Zen is not an ORM** — Tables are not classes, they are Zod-powered singletons which provide schema-aware SQL-fragment helpers. These tables can be passed to CRUD helpers to validate writes, generate DDL, and normalize joined data into an object graph.
|
|
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.
|
|
126
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.
|
|
127
121
|
|
|
128
122
|
### Safety
|
|
@@ -133,9 +127,6 @@ Zen is the missing link between SQL and typed data. By writing tables with Zod s
|
|
|
133
127
|
- **No destructive helpers** — No `dropColumn()`, `dropTable()`, `renameColumn()`
|
|
134
128
|
- **No automatic migrations** — Schema changes are explicit in upgrade events
|
|
135
129
|
|
|
136
|
-
Migrations are **additive and idempotent** by design. Use `ensureColumn()`, `ensureIndex()`, `copyColumn()` for safe schema evolution. Breaking changes require multi-step migrations. Rollbacks are new forward migrations.
|
|
137
|
-
|
|
138
|
-
|
|
139
130
|
## Table Definitions
|
|
140
131
|
|
|
141
132
|
```typescript
|
|
@@ -214,10 +205,37 @@ const Users = table("users", {
|
|
|
214
205
|
|
|
215
206
|
// id and createdAt are optional - auto-generated if not provided
|
|
216
207
|
const user = await db.insert(Users, {name: "Alice"});
|
|
217
|
-
user.id;
|
|
208
|
+
user.id; // "550e8400-e29b-41d4-a716-446655440000"
|
|
218
209
|
user.createdAt; // 2024-01-15T10:30:00.000Z
|
|
219
210
|
```
|
|
220
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
|
+
|
|
221
239
|
**Automatic JSON encoding/decoding:**
|
|
222
240
|
|
|
223
241
|
Objects (`z.object()`) and arrays (`z.array()`) are automatically serialized to JSON when stored and parsed back when read:
|
|
@@ -238,7 +256,7 @@ const settings = await db.insert(Settings, {
|
|
|
238
256
|
|
|
239
257
|
// On read: JSON strings are parsed back to objects/arrays
|
|
240
258
|
settings.config.theme; // "dark" (object, not string)
|
|
241
|
-
settings.tags[0];
|
|
259
|
+
settings.tags[0]; // "admin" (array, not string)
|
|
242
260
|
```
|
|
243
261
|
|
|
244
262
|
**Custom encoding/decoding:**
|
|
@@ -246,7 +264,7 @@ settings.tags[0]; // "admin" (array, not string)
|
|
|
246
264
|
Override automatic JSON encoding with custom transformations:
|
|
247
265
|
|
|
248
266
|
```typescript
|
|
249
|
-
const
|
|
267
|
+
const Articles = table("articles", {
|
|
250
268
|
id: z.string().db.primary(),
|
|
251
269
|
// Store array as CSV instead of JSON
|
|
252
270
|
tags: z.array(z.string())
|
|
@@ -255,8 +273,8 @@ const Custom = table("custom", {
|
|
|
255
273
|
.db.type("TEXT"), // Required: explicit column type for DDL
|
|
256
274
|
});
|
|
257
275
|
|
|
258
|
-
await db.insert(
|
|
259
|
-
// Stored as: tags='
|
|
276
|
+
await db.insert(Articles, {id: "a1", tags: ["news", "tech", "featured"]});
|
|
277
|
+
// Stored as: tags='news,tech,featured' (not '["news","tech","featured"]')
|
|
260
278
|
```
|
|
261
279
|
|
|
262
280
|
**Explicit column types:**
|
|
@@ -301,14 +319,16 @@ const posts = await db.all([Posts, Users.active])`
|
|
|
301
319
|
`;
|
|
302
320
|
```
|
|
303
321
|
|
|
304
|
-
**Compound indexes** via table options:
|
|
322
|
+
**Compound indexes and unique constraints** via table options:
|
|
305
323
|
```typescript
|
|
306
324
|
const Posts = table("posts", {
|
|
307
325
|
id: z.string().db.primary(),
|
|
308
326
|
authorId: z.string(),
|
|
327
|
+
slug: z.string(),
|
|
309
328
|
createdAt: z.date(),
|
|
310
329
|
}, {
|
|
311
330
|
indexes: [["authorId", "createdAt"]],
|
|
331
|
+
unique: [["authorId", "slug"]], // unique together
|
|
312
332
|
});
|
|
313
333
|
```
|
|
314
334
|
|
|
@@ -355,16 +375,16 @@ type Post = Row<typeof Posts>;
|
|
|
355
375
|
// Post includes: id, title, authorId, titleUpper, tags
|
|
356
376
|
|
|
357
377
|
const posts = await db.all([Posts, Users, PostTags, Tags])`
|
|
358
|
-
JOIN
|
|
359
|
-
LEFT JOIN
|
|
360
|
-
LEFT JOIN
|
|
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)}
|
|
361
381
|
`;
|
|
362
382
|
|
|
363
383
|
const post = posts[0];
|
|
364
384
|
post.titleUpper; // "HELLO WORLD" — typed as string
|
|
365
|
-
post.tags;
|
|
366
|
-
Object.keys(post);
|
|
367
|
-
JSON.stringify(post);
|
|
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)
|
|
368
388
|
```
|
|
369
389
|
|
|
370
390
|
Derived properties:
|
|
@@ -376,9 +396,9 @@ Derived properties:
|
|
|
376
396
|
|
|
377
397
|
**Partial selects** with `pick()`:
|
|
378
398
|
```typescript
|
|
379
|
-
const
|
|
380
|
-
const posts = await db.all([Posts,
|
|
381
|
-
JOIN
|
|
399
|
+
const UserSummaries = Users.pick("id", "name");
|
|
400
|
+
const posts = await db.all([Posts, UserSummaries])`
|
|
401
|
+
JOIN ${UserSummaries} ON ${UserSummaries.on(Posts)}
|
|
382
402
|
`;
|
|
383
403
|
// posts[0].author has only id and name
|
|
384
404
|
```
|
|
@@ -470,6 +490,29 @@ await db.exec`CREATE INDEX idx_posts_author ON ${Posts}(${Posts.cols.authorId})`
|
|
|
470
490
|
const count = await db.val<number>`SELECT COUNT(*) FROM ${Posts}`;
|
|
471
491
|
```
|
|
472
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
|
+
|
|
473
516
|
## Fragment Helpers
|
|
474
517
|
|
|
475
518
|
Type-safe SQL fragments as methods on Table objects:
|
|
@@ -519,29 +562,6 @@ const posts = await db.all(Posts)`WHERE ${Posts.in("id", [])}`;
|
|
|
519
562
|
// → WHERE 1 = 0
|
|
520
563
|
```
|
|
521
564
|
|
|
522
|
-
## CRUD Helpers
|
|
523
|
-
|
|
524
|
-
```typescript
|
|
525
|
-
// Insert with Zod validation (uses RETURNING to get actual row)
|
|
526
|
-
const user = await db.insert(Users, {
|
|
527
|
-
email: "alice@example.com",
|
|
528
|
-
name: "Alice",
|
|
529
|
-
});
|
|
530
|
-
// Returns actual row from DB, including auto-generated id and DB-computed defaults
|
|
531
|
-
const userId = user.id;
|
|
532
|
-
|
|
533
|
-
// Update by primary key (uses RETURNING)
|
|
534
|
-
const updated = await db.update(Users, {name: "Bob"}, userId);
|
|
535
|
-
|
|
536
|
-
// Delete by primary key
|
|
537
|
-
await db.delete(Users, userId);
|
|
538
|
-
|
|
539
|
-
// Soft delete (sets deletedAt timestamp, requires softDelete() field)
|
|
540
|
-
await db.softDelete(Users, userId);
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
**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.
|
|
544
|
-
|
|
545
565
|
## Transactions
|
|
546
566
|
|
|
547
567
|
```typescript
|
|
@@ -575,14 +595,16 @@ IndexedDB-style event-based migrations:
|
|
|
575
595
|
db.addEventListener("upgradeneeded", (e) => {
|
|
576
596
|
e.waitUntil((async () => {
|
|
577
597
|
if (e.oldVersion < 1) {
|
|
578
|
-
await db.
|
|
579
|
-
await db.
|
|
598
|
+
await db.ensureTable(Users);
|
|
599
|
+
await db.ensureTable(Posts);
|
|
580
600
|
}
|
|
581
601
|
if (e.oldVersion < 2) {
|
|
582
|
-
|
|
602
|
+
// Add a new column - just update the schema and call ensureTable again
|
|
603
|
+
await db.ensureTable(Posts); // Adds missing "views" column
|
|
583
604
|
}
|
|
584
605
|
if (e.oldVersion < 3) {
|
|
585
|
-
|
|
606
|
+
// Add constraints after data cleanup
|
|
607
|
+
await db.ensureConstraints(Posts);
|
|
586
608
|
}
|
|
587
609
|
})());
|
|
588
610
|
});
|
|
@@ -603,7 +625,7 @@ await db.open(3); // Opens at version 3, fires upgradeneeded if needed
|
|
|
603
625
|
zen provides idempotent helpers that encourage safe, additive-only migrations:
|
|
604
626
|
|
|
605
627
|
```typescript
|
|
606
|
-
// Add a new column
|
|
628
|
+
// Add a new column - update schema and call ensureTable
|
|
607
629
|
const Posts = table("posts", {
|
|
608
630
|
id: z.string().db.primary(),
|
|
609
631
|
title: z.string(),
|
|
@@ -611,24 +633,32 @@ const Posts = table("posts", {
|
|
|
611
633
|
});
|
|
612
634
|
|
|
613
635
|
if (e.oldVersion < 2) {
|
|
614
|
-
await db.
|
|
636
|
+
await db.ensureTable(Posts); // Adds missing columns from schema
|
|
615
637
|
}
|
|
616
|
-
// → ALTER TABLE "posts" ADD COLUMN
|
|
638
|
+
// → ALTER TABLE "posts" ADD COLUMN "views" REAL DEFAULT 0
|
|
639
|
+
|
|
640
|
+
// Add indexes - defined in schema, applied by ensureTable
|
|
641
|
+
const Posts = table("posts", {
|
|
642
|
+
id: z.string().db.primary(),
|
|
643
|
+
title: z.string().db.index(), // NEW - add index
|
|
644
|
+
views: z.number().db.inserted(() => 0),
|
|
645
|
+
});
|
|
617
646
|
|
|
618
|
-
// Add an index
|
|
619
647
|
if (e.oldVersion < 3) {
|
|
620
|
-
await db.
|
|
648
|
+
await db.ensureTable(Posts); // Adds missing indexes
|
|
621
649
|
}
|
|
622
|
-
// → CREATE INDEX IF NOT EXISTS "
|
|
650
|
+
// → CREATE INDEX IF NOT EXISTS "idx_posts_title" ON "posts"("title")
|
|
623
651
|
|
|
624
652
|
// Safe column rename (additive, non-destructive)
|
|
625
653
|
const Users = table("users", {
|
|
626
|
-
|
|
654
|
+
id: z.string().db.primary(),
|
|
655
|
+
email: z.string().email(), // Keep old column
|
|
656
|
+
emailAddress: z.string().email(), // NEW - add new column
|
|
627
657
|
});
|
|
628
658
|
|
|
629
659
|
if (e.oldVersion < 4) {
|
|
630
|
-
await db.
|
|
631
|
-
await db.
|
|
660
|
+
await db.ensureTable(Users); // Adds emailAddress column
|
|
661
|
+
await db.copyColumn(Users, "email", "emailAddress"); // Copy data
|
|
632
662
|
// Keep old "email" column for backwards compat
|
|
633
663
|
// Drop it in a later migration if needed (manual SQL)
|
|
634
664
|
}
|
|
@@ -636,9 +666,10 @@ if (e.oldVersion < 4) {
|
|
|
636
666
|
```
|
|
637
667
|
|
|
638
668
|
**Helper methods:**
|
|
639
|
-
- `
|
|
640
|
-
- `
|
|
641
|
-
- `
|
|
669
|
+
- `db.ensureTable(table)` - Idempotent CREATE TABLE / ADD COLUMN / CREATE INDEX
|
|
670
|
+
- `db.ensureView(view)` - Idempotent DROP + CREATE VIEW
|
|
671
|
+
- `db.ensureConstraints(table)` - Add unique/FK constraints (with preflight checks)
|
|
672
|
+
- `db.copyColumn(table, from, to)` - Copy data between columns (for safe renames)
|
|
642
673
|
|
|
643
674
|
All helpers read from your table schema (single source of truth) and are safe to run multiple times (idempotent).
|
|
644
675
|
|
|
@@ -818,6 +849,7 @@ const refs = Posts.references(); // [{fieldName: "authorId", table: Users,
|
|
|
818
849
|
- Tagged template queries are cached by template object identity (compiled once per call site)
|
|
819
850
|
- Normalization cost is O(rows) with hash maps per table
|
|
820
851
|
- Reference resolution is zero-cost after deduplication
|
|
852
|
+
- Zod validation happens on writes, never on reads.
|
|
821
853
|
|
|
822
854
|
## Driver Interface
|
|
823
855
|
|
|
@@ -916,25 +948,9 @@ const query = db.print`SELECT * FROM ${Posts} WHERE ${Posts.cols.published} = ${
|
|
|
916
948
|
console.log(query.sql); // SELECT * FROM "posts" WHERE "posts"."published" = ?
|
|
917
949
|
console.log(query.params); // [true]
|
|
918
950
|
|
|
919
|
-
// Inspect DDL generation
|
|
920
|
-
const ddl = db.print`${Posts.ddl()}`;
|
|
921
|
-
console.log(ddl.sql); // CREATE TABLE IF NOT EXISTS "posts" (...)
|
|
922
|
-
|
|
923
|
-
// Analyze query execution plan
|
|
924
|
-
const plan = await db.explain`
|
|
925
|
-
SELECT * FROM ${Posts}
|
|
926
|
-
WHERE ${Posts.cols.authorId} = ${userId}
|
|
927
|
-
`;
|
|
928
|
-
console.log(plan);
|
|
929
|
-
// SQLite: [{ detail: "SEARCH posts USING INDEX idx_posts_authorId (authorId=?)" }]
|
|
930
|
-
// PostgreSQL: [{ "QUERY PLAN": "Index Scan using idx_posts_authorId on posts" }]
|
|
931
|
-
|
|
932
951
|
// Debug fragments
|
|
933
952
|
console.log(Posts.set({ title: "Updated" }).toString());
|
|
934
953
|
// SQLFragment { sql: "\"title\" = ?", params: ["Updated"] }
|
|
935
|
-
|
|
936
|
-
console.log(Posts.ddl().toString());
|
|
937
|
-
// DDLFragment { type: "create-table", table: "posts" }
|
|
938
954
|
```
|
|
939
955
|
|
|
940
956
|
## Dialect Support
|
|
@@ -970,41 +986,41 @@ Override with `.db.type("CUSTOM")` when using custom encode/decode.
|
|
|
970
986
|
```typescript
|
|
971
987
|
import {
|
|
972
988
|
// Zod (extended with .db namespace)
|
|
973
|
-
z,
|
|
989
|
+
z, // Re-exported Zod with .db already available
|
|
990
|
+
extendZod, // Extend a separate Zod instance (advanced)
|
|
974
991
|
|
|
975
992
|
// Table and view definition
|
|
976
|
-
table,
|
|
977
|
-
view,
|
|
978
|
-
isTable,
|
|
979
|
-
isView,
|
|
980
|
-
extendZod, // Extend a separate Zod instance (advanced)
|
|
993
|
+
table, // Create a table definition from Zod schema
|
|
994
|
+
view, // Create a read-only view from a table
|
|
995
|
+
isTable, // Type guard for Table objects
|
|
996
|
+
isView, // Type guard for View objects
|
|
981
997
|
|
|
982
998
|
// Database
|
|
983
|
-
Database,
|
|
984
|
-
Transaction,
|
|
985
|
-
DatabaseUpgradeEvent,
|
|
999
|
+
Database, // Main database class
|
|
1000
|
+
Transaction, // Transaction context (passed to transaction callbacks)
|
|
1001
|
+
DatabaseUpgradeEvent, // Event object for "upgradeneeded" handler
|
|
986
1002
|
|
|
987
1003
|
// SQL builtins (for .db.inserted() / .db.updated())
|
|
988
|
-
NOW,
|
|
989
|
-
TODAY,
|
|
990
|
-
CURRENT_TIMESTAMP,
|
|
991
|
-
CURRENT_DATE,
|
|
992
|
-
CURRENT_TIME,
|
|
1004
|
+
NOW, // CURRENT_TIMESTAMP alias
|
|
1005
|
+
TODAY, // CURRENT_DATE alias
|
|
1006
|
+
CURRENT_TIMESTAMP, // SQL CURRENT_TIMESTAMP
|
|
1007
|
+
CURRENT_DATE, // SQL CURRENT_DATE
|
|
1008
|
+
CURRENT_TIME, // SQL CURRENT_TIME
|
|
993
1009
|
|
|
994
1010
|
// Errors
|
|
995
|
-
DatabaseError,
|
|
996
|
-
ValidationError,
|
|
997
|
-
TableDefinitionError,
|
|
998
|
-
MigrationError,
|
|
999
|
-
MigrationLockError,
|
|
1000
|
-
QueryError,
|
|
1001
|
-
NotFoundError,
|
|
1002
|
-
AlreadyExistsError,
|
|
1011
|
+
DatabaseError, // Base error class
|
|
1012
|
+
ValidationError, // Schema validation failed
|
|
1013
|
+
TableDefinitionError, // Invalid table definition
|
|
1014
|
+
MigrationError, // Migration failed
|
|
1015
|
+
MigrationLockError, // Failed to acquire migration lock
|
|
1016
|
+
QueryError, // SQL execution failed
|
|
1017
|
+
NotFoundError, // Entity not found
|
|
1018
|
+
AlreadyExistsError, // Unique constraint violated
|
|
1003
1019
|
ConstraintViolationError, // Database constraint violated
|
|
1004
|
-
ConnectionError,
|
|
1005
|
-
TransactionError,
|
|
1006
|
-
isDatabaseError,
|
|
1007
|
-
hasErrorCode,
|
|
1020
|
+
ConnectionError, // Connection failed
|
|
1021
|
+
TransactionError, // Transaction failed
|
|
1022
|
+
isDatabaseError, // Type guard for DatabaseError
|
|
1023
|
+
hasErrorCode, // Check error code
|
|
1008
1024
|
} from "@b9g/zen";
|
|
1009
1025
|
```
|
|
1010
1026
|
|
|
@@ -1013,34 +1029,34 @@ import {
|
|
|
1013
1029
|
```typescript
|
|
1014
1030
|
import type {
|
|
1015
1031
|
// Table types
|
|
1016
|
-
Table,
|
|
1017
|
-
PartialTable,
|
|
1018
|
-
DerivedTable,
|
|
1019
|
-
TableOptions,
|
|
1020
|
-
ReferenceInfo,
|
|
1021
|
-
CompoundReference,
|
|
1032
|
+
Table, // Table definition object
|
|
1033
|
+
PartialTable, // Table created via .pick()
|
|
1034
|
+
DerivedTable, // Table with derived fields via .derive()
|
|
1035
|
+
TableOptions, // Options for table()
|
|
1036
|
+
ReferenceInfo, // Foreign key reference metadata
|
|
1037
|
+
CompoundReference, // Compound foreign key reference
|
|
1022
1038
|
|
|
1023
1039
|
// Field types
|
|
1024
|
-
FieldMeta,
|
|
1025
|
-
FieldType,
|
|
1026
|
-
FieldDBMeta,
|
|
1040
|
+
FieldMeta, // Field metadata for form generation
|
|
1041
|
+
FieldType, // Field type enum
|
|
1042
|
+
FieldDBMeta, // Database-specific field metadata
|
|
1027
1043
|
|
|
1028
1044
|
// Type inference
|
|
1029
|
-
Row,
|
|
1030
|
-
Insert,
|
|
1031
|
-
Update,
|
|
1045
|
+
Row, // Infer row type from Table (after read)
|
|
1046
|
+
Insert, // Infer insert type from Table (respects defaults/.db.auto())
|
|
1047
|
+
Update, // Infer update type from Table (all fields optional)
|
|
1032
1048
|
|
|
1033
1049
|
// Fragment types
|
|
1034
|
-
SetValues,
|
|
1035
|
-
SQLTemplate,
|
|
1036
|
-
SQLDialect,
|
|
1050
|
+
SetValues, // Values accepted by Table.set()
|
|
1051
|
+
SQLTemplate, // SQL template object (return type of set(), on(), etc.)
|
|
1052
|
+
SQLDialect, // "sqlite" | "postgresql" | "mysql"
|
|
1037
1053
|
|
|
1038
1054
|
// Driver types
|
|
1039
|
-
Driver,
|
|
1040
|
-
TaggedQuery,
|
|
1055
|
+
Driver, // Driver interface for adapters
|
|
1056
|
+
TaggedQuery, // Tagged template query function
|
|
1041
1057
|
|
|
1042
1058
|
// Error types
|
|
1043
|
-
DatabaseErrorCode,
|
|
1059
|
+
DatabaseErrorCode, // Error code string literals
|
|
1044
1060
|
} from "@b9g/zen";
|
|
1045
1061
|
```
|
|
1046
1062
|
|
|
@@ -1064,12 +1080,6 @@ const Posts = table("posts", {
|
|
|
1064
1080
|
|
|
1065
1081
|
const rows = [{id: "u1", email: "alice@example.com", deletedAt: null}];
|
|
1066
1082
|
|
|
1067
|
-
// DDL Generation
|
|
1068
|
-
Users.ddl(); // DDLFragment for CREATE TABLE
|
|
1069
|
-
Users.ensureColumn("emailAddress"); // DDLFragment for ALTER TABLE ADD COLUMN
|
|
1070
|
-
Users.ensureIndex(["email"]); // DDLFragment for CREATE INDEX
|
|
1071
|
-
Users.copyColumn("email", "emailAddress"); // SQLFragment for UPDATE (copy data)
|
|
1072
|
-
|
|
1073
1083
|
// Query Fragments
|
|
1074
1084
|
Users.set({email: "alice@example.com"}); // SQLFragment for SET clause
|
|
1075
1085
|
Users.values(rows); // SQLFragment for INSERT VALUES
|
|
@@ -1136,9 +1146,15 @@ await db.transaction(async (tx) => {
|
|
|
1136
1146
|
await tx.exec`SELECT 1`;
|
|
1137
1147
|
});
|
|
1138
1148
|
|
|
1149
|
+
// Schema Management
|
|
1150
|
+
await db.ensureTable(Users); // CREATE TABLE / ADD COLUMN / CREATE INDEX
|
|
1151
|
+
await db.ensureView(AdminUsers); // DROP + CREATE VIEW
|
|
1152
|
+
await db.ensureConstraints(Users); // Add unique/FK constraints
|
|
1153
|
+
await db.copyColumn(Users, "old", "new"); // Copy data between columns
|
|
1154
|
+
|
|
1139
1155
|
// Debugging
|
|
1140
|
-
db.print`SELECT 1`;
|
|
1141
|
-
await db.explain`SELECT * FROM ${Users}`;
|
|
1156
|
+
db.print`SELECT 1`; // Returns { sql, params } without executing
|
|
1157
|
+
await db.explain`SELECT * FROM ${Users}`; // Returns query execution plan
|
|
1142
1158
|
```
|
|
1143
1159
|
|
|
1144
1160
|
### Driver Exports
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/zen",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Define Zod tables. Write raw SQL. Get typed objects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"database",
|
|
7
7
|
"sql",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"zod": "^4.0.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@b9g/libuild": "^0.1.
|
|
29
|
+
"@b9g/libuild": "^0.1.21",
|
|
30
30
|
"@eslint/js": "^9.39.2",
|
|
31
31
|
"@types/better-sqlite3": "^7.6.0",
|
|
32
32
|
"@types/bun": "^1.3.4",
|
|
@@ -793,6 +793,30 @@ function table(name, shape, options = {}) {
|
|
|
793
793
|
name
|
|
794
794
|
);
|
|
795
795
|
}
|
|
796
|
+
for (const idx of options.indexes ?? []) {
|
|
797
|
+
if (idx.length < 2) {
|
|
798
|
+
throw new TableDefinitionError(
|
|
799
|
+
`Compound index in table "${name}" must have at least 2 fields. Use .db.index() for single-field indexes.`,
|
|
800
|
+
name
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
for (const u of options.unique ?? []) {
|
|
805
|
+
if (u.length < 2) {
|
|
806
|
+
throw new TableDefinitionError(
|
|
807
|
+
`Compound unique constraint in table "${name}" must have at least 2 fields. Use .db.unique() for single-field constraints.`,
|
|
808
|
+
name
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
for (const ref of options.references ?? []) {
|
|
813
|
+
if (ref.fields.length < 2) {
|
|
814
|
+
throw new TableDefinitionError(
|
|
815
|
+
`Compound foreign key in table "${name}" must have at least 2 fields. Use .db.references() for single-field foreign keys.`,
|
|
816
|
+
name
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
796
820
|
const zodShape = {};
|
|
797
821
|
const meta = {
|
|
798
822
|
primary: null,
|