zod-sqlite 1.0.2 → 1.0.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/README.md CHANGED
@@ -1,6 +1,15 @@
1
- # Zod-SQLite
2
-
3
- Generate type-safe SQLite table schemas from Zod validation schemas. Define your database structure once using Zod, and automatically generate both SQL CREATE TABLE statements and runtime validation schemas with full TypeScript type inference.
1
+ <div align="center">
2
+ <h1>Zod-SQLite</h1>
3
+ <p><strong>Generate type-safe SQLite table schemas from Zod validation schemas. Define your database structure once using Zod, and automatically generate both SQL CREATE TABLE statements and runtime validation schemas with full TypeScript type inference.</strong></p>
4
+ <p>
5
+ <a href="https://github.com/favorodera/zod-sqlite/blob/main/LICENSE"><img src="https://img.shields.io/github/license/favorodera/zod-sqlite.svg?style=plastic&label=License&color=blue" alt="License"></a>
6
+ <a href="https://github.com/favorodera/zod-sqlite/stargazers"><img src="https://img.shields.io/github/stars/favorodera/zod-sqlite.svg?style=plastic&label=Stars&color=blue" alt="GitHub Stars"></a>
7
+ <a href="https://npmx.dev/package/zod-sqlite"><img src="https://img.shields.io/npm/dt/zod-sqlite.svg?style=plastic&label=NPM%20Downloads&color=blue" alt="NPM Downloads"></a>
8
+ <a href="https://npmx.dev/package/zod-sqlite"><img src="https://img.shields.io/npm/v/zod-sqlite.svg?style=plastic&label=Version&color=blue" alt="NPM Version"></a>
9
+ </p>
10
+ </div>
11
+
12
+ <br>
4
13
 
5
14
  ## Table of Contents
6
15
 
@@ -44,57 +53,50 @@ This tool bridges the gap between Zod schemas and SQLite database definitions. I
44
53
  npm install zod-sqlite
45
54
  ```
46
55
 
47
- Requires Zod v4 as a peer dependency:
48
-
49
- ```bash
50
- npm install zod
51
- ```
52
-
53
56
  ## Quick Start
54
57
 
55
58
  Here's a simple example creating a users table:
56
59
 
57
60
  ```typescript
58
- import { createTable } from 'zod-sqlite'
59
- import { z } from 'zod'
60
-
61
- const users = createTable({
62
- name: 'users',
63
- columns: [
64
- { name: 'id', schema: z.int() },
65
- { name: 'email', schema: z.email() },
66
- { name: 'username', schema: z.string().min(3).max(20) },
67
- { name: 'created_at', schema: z.date().default(new Date()) }
68
- ],
69
- primaryKeys: ['id'],
70
- indexes: [
71
- { name: 'idx_users_email', columns: ['email'], unique: true }
72
- ]
73
- })
74
-
75
- // Use the generated SQL
76
- console.log(users.table)
77
- // CREATE TABLE users (
78
- // id INTEGER NOT NULL,
79
- // email TEXT NOT NULL,
80
- // username TEXT NOT NULL CHECK(length(username) >= 3 AND length(username) <= 20),
81
- // created_at DATE NOT NULL DEFAULT '2026-01-06T...',
82
- // PRIMARY KEY (id)
83
- // );
84
-
85
- console.log(users.indexes[0])
86
- // CREATE UNIQUE INDEX idx_users_email ON users (email);
87
-
88
- // Validate data at runtime
89
- const result = users.schema.safeParse({
90
- id: 1,
91
- email: 'user@example.com',
92
- username: 'john',
93
- created_at: new Date()
94
- })
95
-
96
- // TypeScript type inference
97
- type User = z.infer<typeof users.schema>
61
+ import { z } from 'zod'
62
+ import { createTable } from 'zod-sqlite'
63
+
64
+ const users = createTable({
65
+ columns: [
66
+ { name: 'id', schema: z.int() },
67
+ { name: 'email', schema: z.email() },
68
+ { name: 'username', schema: z.string().min(3)
69
+ .max(20) },
70
+ { name: 'created_at', schema: z.date().default(new Date()) },
71
+ ],
72
+ indexes: [{ columns: ['email'], name: 'idx_users_email', unique: true }],
73
+ name: 'users',
74
+ primaryKeys: ['id'],
75
+ })
76
+
77
+ // Use the generated SQL
78
+ console.log(users.table)
79
+ // CREATE TABLE users (
80
+ // id INTEGER NOT NULL,
81
+ // email TEXT NOT NULL,
82
+ // username TEXT NOT NULL CHECK(length(username) >= 3 AND length(username) <= 20),
83
+ // created_at DATE NOT NULL DEFAULT '2026-01-06T...',
84
+ // PRIMARY KEY (id)
85
+ // );
86
+
87
+ console.log(users.indexes[0])
88
+ // CREATE UNIQUE INDEX idx_users_email ON users (email);
89
+
90
+ // Validate data at runtime
91
+ const result = users.schema.safeParse({
92
+ created_at: new Date(),
93
+ email: 'user@example.com',
94
+ id: 1,
95
+ username: 'john',
96
+ })
97
+
98
+ // TypeScript type inference
99
+ type User = z.infer<typeof users.schema>
98
100
  // { id: number; email: string; username: string; created_at: Date }
99
101
  ```
100
102
 
@@ -112,7 +114,7 @@ Zod schemas serve as the source of truth for your database structure. Each colum
112
114
  `createTable` is the primary entry point. It accepts a configuration object and returns three things:
113
115
 
114
116
  ```typescript
115
- const result = createTable(config)
117
+ const result = createTable(config)
116
118
  // Returns: { table: string, indexes: string[], schema: ZodObject }
117
119
  ```
118
120
 
@@ -140,7 +142,7 @@ Creates a table definition with SQL statements and validation schema.
140
142
 
141
143
  **Returns:**
142
144
 
143
- ```typescript
145
+ ```plaintext
144
146
  {
145
147
  table: string // CREATE TABLE SQL statement
146
148
  indexes: string[] // Array of CREATE INDEX statements
@@ -151,14 +153,14 @@ Creates a table definition with SQL statements and validation schema.
151
153
  **Example:**
152
154
 
153
155
  ```typescript
154
- const { table, indexes, schema } = createTable({
155
- name: 'products',
156
- columns: [
157
- { name: 'id', schema: z.int() },
158
- { name: 'name', schema: z.string() },
159
- { name: 'price', schema: z.number().min(0) }
160
- ],
161
- primaryKeys: ['id']
156
+ const { indexes, schema, table } = createTable({
157
+ columns: [
158
+ { name: 'id', schema: z.int() },
159
+ { name: 'name', schema: z.string() },
160
+ { name: 'price', schema: z.number().min(0) },
161
+ ],
162
+ name: 'products',
163
+ primaryKeys: ['id'],
162
164
  })
163
165
  ```
164
166
 
@@ -167,11 +169,11 @@ const { table, indexes, schema } = createTable({
167
169
  Configuration object for table creation.
168
170
 
169
171
  ```typescript
170
- type TableConfig = {
171
- name: string // Table name
172
- columns: ColumnConfig[] // Array of column definitions
173
- primaryKeys: string[] // Column names forming primary key
174
- indexes?: IndexConfig[] // Optional index configurations
172
+ interface TableConfig {
173
+ columns: Array<ColumnConfig> // Array of column definitions
174
+ indexes?: Array<IndexConfig> // Optional index configurations
175
+ name: string // Table name
176
+ primaryKeys: Array<string> // Column names forming primary key
175
177
  }
176
178
  ```
177
179
 
@@ -180,11 +182,11 @@ type TableConfig = {
180
182
  Configuration for a single column.
181
183
 
182
184
  ```typescript
183
- type ColumnConfig = {
184
- name: string // Column name
185
- schema: ZodType // Zod schema defining type and validation
186
- unique?: boolean // Whether values must be unique
187
- references?: ForeignKeyReference // Foreign key configuration
185
+ interface ColumnConfig {
186
+ name: string // Column name
187
+ references?: ForeignKeyReference // Foreign key configuration
188
+ schema: ZodType // Zod schema defining type and validation
189
+ unique?: boolean // Whether values must be unique
188
190
  }
189
191
  ```
190
192
 
@@ -193,19 +195,19 @@ type ColumnConfig = {
193
195
  Foreign key constraint configuration.
194
196
 
195
197
  ```typescript
196
- type ForeignKeyReference = {
197
- table: string // Referenced table name
198
- column: string // Referenced column name
199
- onDelete?: ForeignKeyAction // Action on parent deletion
200
- onUpdate?: ForeignKeyAction // Action on parent update
201
- }
202
-
203
- type ForeignKeyAction =
204
- | 'NO ACTION'
205
- | 'RESTRICT'
206
- | 'SET NULL'
207
- | 'SET DEFAULT'
208
- | 'CASCADE'
198
+ interface ForeignKeyReference {
199
+ column: string // Referenced column name
200
+ onDelete?: ForeignKeyAction // Action on parent deletion
201
+ onUpdate?: ForeignKeyAction // Action on parent update
202
+ table: string // Referenced table name
203
+ }
204
+
205
+ type ForeignKeyAction
206
+ = | 'CASCADE'
207
+ | 'NO ACTION'
208
+ | 'RESTRICT'
209
+ | 'SET DEFAULT'
210
+ | 'SET NULL'
209
211
  ```
210
212
 
211
213
  ### IndexConfig
@@ -213,11 +215,11 @@ type ForeignKeyAction =
213
215
  Index configuration for query optimization.
214
216
 
215
217
  ```typescript
216
- type IndexConfig = {
217
- name: string // Index name
218
- columns: string[] // Indexed column names
219
- unique?: boolean // Whether this is a unique index
220
- where?: string // Optional WHERE clause for partial index
218
+ interface IndexConfig {
219
+ columns: Array<string> // Indexed column names
220
+ name: string // Index name
221
+ unique?: boolean // Whether this is a unique index
222
+ where?: string // Optional WHERE clause for partial index
221
223
  }
222
224
  ```
223
225
 
@@ -269,23 +271,24 @@ These Zod wrappers are automatically unwrapped:
269
271
  - `.default(value)` - Adds DEFAULT clause
270
272
 
271
273
  ```typescript
272
- z.string().optional() // TEXT (nullable)
273
- z.number().default(0) // REAL NOT NULL DEFAULT 0
274
- z.string().nullable().default('n/a') // TEXT DEFAULT 'n/a'
274
+ z.string().optional() // TEXT (nullable)
275
+ z.number().default(0) // REAL NOT NULL DEFAULT 0
276
+ z.string().nullable()
277
+ .default('n/a') // TEXT DEFAULT 'n/a'
275
278
  ```
276
279
 
277
280
  ## Column Configuration
278
281
 
279
282
  ### Basic Columns
280
283
 
281
- ```typescript
284
+ ```plaintext
282
285
  { name: 'email', schema: z.email() }
283
286
  // SQL: email TEXT NOT NULL
284
287
  ```
285
288
 
286
289
  ### Optional and Nullable Columns
287
290
 
288
- ```typescript
291
+ ```plaintext
289
292
  { name: 'bio', schema: z.string().optional() }
290
293
  // SQL: bio TEXT
291
294
 
@@ -295,7 +298,7 @@ z.string().nullable().default('n/a') // TEXT DEFAULT 'n/a'
295
298
 
296
299
  ### Columns with Default Values
297
300
 
298
- ```typescript
301
+ ```plaintext
299
302
  { name: 'status', schema: z.enum(['active', 'inactive']).default('active') }
300
303
  // SQL: status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'inactive'))
301
304
 
@@ -305,7 +308,7 @@ z.string().nullable().default('n/a') // TEXT DEFAULT 'n/a'
305
308
 
306
309
  ### Unique Columns
307
310
 
308
- ```typescript
311
+ ```plaintext
309
312
  { name: 'username', schema: z.string(), unique: true }
310
313
  // SQL: username TEXT NOT NULL UNIQUE
311
314
  ```
@@ -316,7 +319,7 @@ SQL CHECK constraints are automatically generated from Zod validation rules.
316
319
 
317
320
  ### Enum Constraints
318
321
 
319
- ```typescript
322
+ ```plaintext
320
323
  {
321
324
  name: 'role',
322
325
  schema: z.enum(['admin', 'user', 'guest'])
@@ -326,7 +329,7 @@ SQL CHECK constraints are automatically generated from Zod validation rules.
326
329
 
327
330
  ### Literal Constraints
328
331
 
329
- ```typescript
332
+ ```plaintext
330
333
  {
331
334
  name: 'type',
332
335
  schema: z.literal('premium')
@@ -346,7 +349,7 @@ SQL CHECK constraints are automatically generated from Zod validation rules.
346
349
 
347
350
  ### Numeric Range Constraints
348
351
 
349
- ```typescript
352
+ ```plaintext
350
353
  {
351
354
  name: 'age',
352
355
  schema: z.int().min(18).max(120)
@@ -362,7 +365,7 @@ SQL CHECK constraints are automatically generated from Zod validation rules.
362
365
 
363
366
  ### String Length Constraints
364
367
 
365
- ```typescript
368
+ ```plaintext
366
369
  {
367
370
  name: 'username',
368
371
  schema: z.string().min(3).max(20)
@@ -383,14 +386,14 @@ SQL CHECK constraints are automatically generated from Zod validation rules.
383
386
  The most common pattern for entity tables:
384
387
 
385
388
  ```typescript
386
- createTable({
387
- name: 'users',
388
- columns: [
389
- { name: 'id', schema: z.int() },
390
- { name: 'email', schema: z.string() }
391
- ],
392
- primaryKeys: ['id']
393
- })
389
+ createTable({
390
+ columns: [
391
+ { name: 'id', schema: z.int() },
392
+ { name: 'email', schema: z.string() },
393
+ ],
394
+ name: 'users',
395
+ primaryKeys: ['id'],
396
+ })
394
397
  // SQL: PRIMARY KEY (id)
395
398
  ```
396
399
 
@@ -399,29 +402,35 @@ createTable({
399
402
  Used for junction tables and multi-tenant data:
400
403
 
401
404
  ```typescript
402
- createTable({
403
- name: 'user_roles',
404
- columns: [
405
- { name: 'user_id', schema: z.int() },
406
- { name: 'role_id', schema: z.int() }
407
- ],
408
- primaryKeys: ['user_id', 'role_id']
409
- })
405
+ createTable({
406
+ columns: [
407
+ { name: 'user_id', schema: z.int() },
408
+ { name: 'role_id', schema: z.int() },
409
+ ],
410
+ name: 'user_roles',
411
+ primaryKeys: [
412
+ 'user_id',
413
+ 'role_id',
414
+ ],
415
+ })
410
416
  // SQL: PRIMARY KEY (user_id, role_id)
411
417
  ```
412
418
 
413
419
  ### Multi-Tenant Example
414
420
 
415
421
  ```typescript
416
- createTable({
417
- name: 'documents',
418
- columns: [
419
- { name: 'tenant_id', schema: z.string() },
420
- { name: 'doc_id', schema: z.int() },
421
- { name: 'title', schema: z.string() }
422
- ],
423
- primaryKeys: ['tenant_id', 'doc_id']
424
- })
422
+ createTable({
423
+ columns: [
424
+ { name: 'tenant_id', schema: z.string() },
425
+ { name: 'doc_id', schema: z.int() },
426
+ { name: 'title', schema: z.string() },
427
+ ],
428
+ name: 'documents',
429
+ primaryKeys: [
430
+ 'tenant_id',
431
+ 'doc_id',
432
+ ],
433
+ })
425
434
  // SQL: PRIMARY KEY (tenant_id, doc_id)
426
435
  ```
427
436
 
@@ -430,21 +439,21 @@ createTable({
430
439
  ### Basic Foreign Key
431
440
 
432
441
  ```typescript
433
- createTable({
434
- name: 'posts',
435
- columns: [
436
- { name: 'id', schema: z.int() },
437
- {
438
- name: 'author_id',
439
- schema: z.int(),
440
- references: {
441
- table: 'users',
442
- column: 'id'
443
- }
444
- }
445
- ],
446
- primaryKeys: ['id']
447
- })
442
+ createTable({
443
+ columns: [
444
+ { name: 'id', schema: z.int() },
445
+ {
446
+ name: 'author_id',
447
+ references: {
448
+ column: 'id',
449
+ table: 'users',
450
+ },
451
+ schema: z.int(),
452
+ },
453
+ ],
454
+ name: 'posts',
455
+ primaryKeys: ['id'],
456
+ })
448
457
  // SQL: author_id INTEGER NOT NULL REFERENCES users(id)
449
458
  ```
450
459
 
@@ -452,7 +461,7 @@ createTable({
452
461
 
453
462
  Automatically delete child records when parent is deleted:
454
463
 
455
- ```typescript
464
+ ```plaintext
456
465
  {
457
466
  name: 'user_id',
458
467
  schema: z.int(),
@@ -469,7 +478,7 @@ Automatically delete child records when parent is deleted:
469
478
 
470
479
  Prevent deletion of parent if children exist:
471
480
 
472
- ```typescript
481
+ ```plaintext
473
482
  {
474
483
  name: 'category_id',
475
484
  schema: z.int(),
@@ -486,7 +495,7 @@ Prevent deletion of parent if children exist:
486
495
 
487
496
  Set foreign key to NULL when parent is deleted:
488
497
 
489
- ```typescript
498
+ ```plaintext
490
499
  {
491
500
  name: 'manager_id',
492
501
  schema: z.int().nullable(),
@@ -503,7 +512,7 @@ Set foreign key to NULL when parent is deleted:
503
512
 
504
513
  Propagate updates to child records:
505
514
 
506
- ```typescript
515
+ ```plaintext
507
516
  {
508
517
  name: 'parent_id',
509
518
  schema: z.int(),
@@ -521,12 +530,12 @@ Propagate updates to child records:
521
530
 
522
531
  ### Simple Index
523
532
 
524
- ```typescript
533
+ ```plaintext
525
534
  indexes: [
526
- {
527
- name: 'idx_users_email',
528
- columns: ['email']
529
- }
535
+ {
536
+ columns: ['email'],
537
+ name: 'idx_users_email',
538
+ },
530
539
  ]
531
540
  // SQL: CREATE INDEX idx_users_email ON users (email);
532
541
  ```
@@ -536,13 +545,13 @@ indexes: [
536
545
  Enforce uniqueness at the database level:
537
546
 
538
547
  ```typescript
539
- indexes: [
540
- {
541
- name: 'idx_users_username',
542
- columns: ['username'],
543
- unique: true
544
- }
545
- ]
548
+ indexes: [
549
+ {
550
+ columns: ['username'],
551
+ name: 'idx_users_username',
552
+ unique: true,
553
+ },
554
+ ]
546
555
  // SQL: CREATE UNIQUE INDEX idx_users_username ON users (username);
547
556
  ```
548
557
 
@@ -551,12 +560,15 @@ indexes: [
551
560
  Index multiple columns together for multi-column queries:
552
561
 
553
562
  ```typescript
554
- indexes: [
555
- {
556
- name: 'idx_posts_author_date',
557
- columns: ['author_id', 'created_at']
558
- }
559
- ]
563
+ indexes: [
564
+ {
565
+ columns: [
566
+ 'author_id',
567
+ 'created_at',
568
+ ],
569
+ name: 'idx_posts_author_date',
570
+ },
571
+ ]
560
572
  // SQL: CREATE INDEX idx_posts_author_date ON posts (author_id, created_at);
561
573
  ```
562
574
 
@@ -571,13 +583,13 @@ SELECT * FROM posts WHERE author_id = 123 AND created_at > '2024-01-01';
571
583
  Index only rows matching a condition:
572
584
 
573
585
  ```typescript
574
- indexes: [
575
- {
576
- name: 'idx_active_users',
577
- columns: ['last_login'],
578
- where: 'deleted_at IS NULL'
579
- }
580
- ]
586
+ indexes: [
587
+ {
588
+ columns: ['last_login'],
589
+ name: 'idx_active_users',
590
+ where: 'deleted_at IS NULL',
591
+ },
592
+ ]
581
593
  // SQL: CREATE INDEX idx_active_users ON posts (last_login) WHERE deleted_at IS NULL;
582
594
  ```
583
595
 
@@ -591,142 +603,160 @@ Benefits:
591
603
  ### Complete Blog Example
592
604
 
593
605
  ```typescript
594
- // Users table
595
- const users = createTable({
596
- name: 'users',
597
- columns: [
598
- { name: 'id', schema: z.int() },
599
- { name: 'email', schema: z.email(), unique: true },
600
- { name: 'username', schema: z.string().min(3).max(20), unique: true },
601
- { name: 'role', schema: z.enum(['admin', 'author', 'reader']).default('reader') },
602
- { name: 'created_at', schema: z.date().default(new Date()) }
603
- ],
604
- primaryKeys: ['id'],
605
- indexes: [
606
- { name: 'idx_users_email', columns: ['email'], unique: true },
607
- { name: 'idx_users_role', columns: ['role'] }
608
- ]
609
- })
610
-
611
- // Posts table with foreign key
612
- const posts = createTable({
613
- name: 'posts',
614
- columns: [
615
- { name: 'id', schema: z.int() },
616
- { name: 'title', schema: z.string().min(1).max(200) },
617
- { name: 'content', schema: z.string() },
618
- { name: 'status', schema: z.enum(['draft', 'published', 'archived']).default('draft') },
619
- {
620
- name: 'author_id',
621
- schema: z.int(),
622
- references: {
623
- table: 'users',
624
- column: 'id',
625
- onDelete: 'CASCADE'
626
- }
627
- },
628
- { name: 'published_at', schema: z.date().nullable() },
629
- { name: 'created_at', schema: z.date().default(new Date()) }
630
- ],
631
- primaryKeys: ['id'],
632
- indexes: [
633
- { name: 'idx_posts_author', columns: ['author_id'] },
634
- {
635
- name: 'idx_posts_published',
636
- columns: ['published_at'],
637
- where: "status = 'published'"
638
- },
639
- { name: 'idx_posts_status_date', columns: ['status', 'created_at'] }
640
- ]
641
- })
642
-
643
- // Junction table for tags (many-to-many)
644
- const postTags = createTable({
645
- name: 'post_tags',
646
- columns: [
647
- {
648
- name: 'post_id',
649
- schema: z.int(),
650
- references: {
651
- table: 'posts',
652
- column: 'id',
653
- onDelete: 'CASCADE'
654
- }
655
- },
656
- {
657
- name: 'tag_id',
658
- schema: z.int(),
659
- references: {
660
- table: 'tags',
661
- column: 'id',
662
- onDelete: 'CASCADE'
663
- }
664
- },
665
- { name: 'created_at', schema: z.date().default(new Date()) }
666
- ],
667
- primaryKeys: ['post_id', 'tag_id']
606
+ // Users table
607
+ const users = createTable({
608
+ columns: [
609
+ { name: 'id', schema: z.int() },
610
+ { name: 'email', schema: z.email(), unique: true },
611
+ { name: 'username', schema: z.string().min(3)
612
+ .max(20), unique: true },
613
+ { name: 'role', schema: z.enum([
614
+ 'admin',
615
+ 'author',
616
+ 'reader',
617
+ ]).default('reader') },
618
+ { name: 'created_at', schema: z.date().default(new Date()) },
619
+ ],
620
+ indexes: [
621
+ { columns: ['email'], name: 'idx_users_email', unique: true },
622
+ { columns: ['role'], name: 'idx_users_role' },
623
+ ],
624
+ name: 'users',
625
+ primaryKeys: ['id'],
626
+ })
627
+
628
+ // Posts table with foreign key
629
+ const posts = createTable({
630
+ columns: [
631
+ { name: 'id', schema: z.int() },
632
+ { name: 'title', schema: z.string().min(1)
633
+ .max(200) },
634
+ { name: 'content', schema: z.string() },
635
+ { name: 'status', schema: z.enum([
636
+ 'draft',
637
+ 'published',
638
+ 'archived',
639
+ ]).default('draft') },
640
+ {
641
+ name: 'author_id',
642
+ references: {
643
+ column: 'id',
644
+ onDelete: 'CASCADE',
645
+ table: 'users',
646
+ },
647
+ schema: z.int(),
648
+ },
649
+ { name: 'published_at', schema: z.date().nullable() },
650
+ { name: 'created_at', schema: z.date().default(new Date()) },
651
+ ],
652
+ indexes: [
653
+ { columns: ['author_id'], name: 'idx_posts_author' },
654
+ {
655
+ columns: ['published_at'],
656
+ name: 'idx_posts_published',
657
+ where: 'status = \'published\'',
658
+ },
659
+ { columns: [
660
+ 'status',
661
+ 'created_at',
662
+ ], name: 'idx_posts_status_date' },
663
+ ],
664
+ name: 'posts',
665
+ primaryKeys: ['id'],
666
+ })
667
+
668
+ // Junction table for tags (many-to-many)
669
+ const postTags = createTable({
670
+ columns: [
671
+ {
672
+ name: 'post_id',
673
+ references: {
674
+ column: 'id',
675
+ onDelete: 'CASCADE',
676
+ table: 'posts',
677
+ },
678
+ schema: z.int(),
679
+ },
680
+ {
681
+ name: 'tag_id',
682
+ references: {
683
+ column: 'id',
684
+ onDelete: 'CASCADE',
685
+ table: 'tags',
686
+ },
687
+ schema: z.int(),
688
+ },
689
+ { name: 'created_at', schema: z.date().default(new Date()) },
690
+ ],
691
+ name: 'post_tags',
692
+ primaryKeys: [
693
+ 'post_id',
694
+ 'tag_id',
695
+ ],
668
696
  })
669
697
  ```
670
698
 
671
699
  ### E-commerce Example
672
700
 
673
701
  ```typescript
674
- const products = createTable({
675
- name: 'products',
676
- columns: [
677
- { name: 'id', schema: z.int() },
678
- { name: 'sku', schema: z.string().length(10), unique: true },
679
- { name: 'name', schema: z.string().min(1) },
680
- { name: 'description', schema: z.string().optional() },
681
- { name: 'price', schema: z.number().min(0) },
682
- { name: 'stock', schema: z.int().min(0).default(0) },
683
- { name: 'active', schema: z.boolean().default(true) },
684
- {
685
- name: 'category_id',
686
- schema: z.int(),
687
- references: {
688
- table: 'categories',
689
- column: 'id',
690
- onDelete: 'RESTRICT'
691
- }
692
- }
693
- ],
694
- primaryKeys: ['id'],
695
- indexes: [
696
- { name: 'idx_products_sku', columns: ['sku'], unique: true },
697
- { name: 'idx_products_category', columns: ['category_id'] },
698
- {
699
- name: 'idx_products_active',
700
- columns: ['price', 'stock'],
701
- where: 'active = 1'
702
- }
703
- ]
702
+ const products = createTable({
703
+ columns: [
704
+ { name: 'id', schema: z.int() },
705
+ { name: 'sku', schema: z.string().length(10), unique: true },
706
+ { name: 'name', schema: z.string().min(1) },
707
+ { name: 'description', schema: z.string().optional() },
708
+ { name: 'price', schema: z.number().min(0) },
709
+ { name: 'stock', schema: z.int().min(0)
710
+ .default(0) },
711
+ { name: 'active', schema: z.boolean().default(true) },
712
+ {
713
+ name: 'category_id',
714
+ references: {
715
+ column: 'id',
716
+ onDelete: 'RESTRICT',
717
+ table: 'categories',
718
+ },
719
+ schema: z.int(),
720
+ },
721
+ ],
722
+ indexes: [
723
+ { columns: ['sku'], name: 'idx_products_sku', unique: true },
724
+ { columns: ['category_id'], name: 'idx_products_category' },
725
+ {
726
+ columns: [
727
+ 'price',
728
+ 'stock',
729
+ ],
730
+ name: 'idx_products_active',
731
+ where: 'active = 1',
732
+ },
733
+ ],
734
+ name: 'products',
735
+ primaryKeys: ['id'],
704
736
  })
705
737
  ```
706
738
 
707
739
  ### Self-Referencing Table
708
740
 
709
741
  ```typescript
710
- const employees = createTable({
711
- name: 'employees',
712
- columns: [
713
- { name: 'id', schema: z.int() },
714
- { name: 'name', schema: z.string() },
715
- { name: 'email', schema: z.email(), unique: true },
716
- {
717
- name: 'manager_id',
718
- schema: z.int().nullable(),
719
- references: {
720
- table: 'employees',
721
- column: 'id',
722
- onDelete: 'SET NULL'
723
- }
724
- }
725
- ],
726
- primaryKeys: ['id'],
727
- indexes: [
728
- { name: 'idx_employees_manager', columns: ['manager_id'] }
729
- ]
742
+ const employees = createTable({
743
+ columns: [
744
+ { name: 'id', schema: z.int() },
745
+ { name: 'name', schema: z.string() },
746
+ { name: 'email', schema: z.email(), unique: true },
747
+ {
748
+ name: 'manager_id',
749
+ references: {
750
+ column: 'id',
751
+ onDelete: 'SET NULL',
752
+ table: 'employees',
753
+ },
754
+ schema: z.int().nullable(),
755
+ },
756
+ ],
757
+ indexes: [{ columns: ['manager_id'], name: 'idx_employees_manager' }],
758
+ name: 'employees',
759
+ primaryKeys: ['id'],
730
760
  })
731
761
  ```
732
762
 
@@ -738,16 +768,21 @@ const employees = createTable({
738
768
 
739
769
  2. **Use appropriate types**: Choose the most specific Zod type that matches your data:
740
770
  ```typescript
741
- z.int() // For IDs and counts
742
- z.number().min(0) // For prices and quantities
743
- z.enum(['a', 'b']) // For status fields
744
- z.email() // For email addresses
771
+ z.int() // For IDs and counts
772
+ z.number().min(0) // For prices and quantities
773
+ z.enum([
774
+ 'a',
775
+ 'b',
776
+ ]) // For status fields
777
+ z.email() // For email addresses
745
778
  ```
746
779
 
747
780
  3. **Add validation at the schema level**: Leverage Zod's validation to prevent invalid data:
748
781
  ```typescript
749
- z.string().min(3).max(50) // Username length
750
- z.number().min(0).max(5) // Rating scale
782
+ z.string().min(3)
783
+ .max(50) // Username length
784
+ z.number().min(0)
785
+ .max(5) // Rating scale
751
786
  ```
752
787
 
753
788
  ### Foreign Keys
@@ -772,20 +807,23 @@ const employees = createTable({
772
807
 
773
808
  3. **Use composite indexes wisely**: Order matters - most selective column first:
774
809
  ```typescript
775
- indexes: [
776
- // Good: Filters by tenant first (high selectivity)
777
- { name: 'idx_tenant_user', columns: ['tenant_id', 'user_id'] }
810
+ indexes: [
811
+ // Good: Filters by tenant first (high selectivity)
812
+ { columns: [
813
+ 'tenant_id',
814
+ 'user_id',
815
+ ], name: 'idx_tenant_user' },
778
816
  ]
779
817
  ```
780
818
 
781
819
  4. **Consider partial indexes**: Reduce index size for filtered queries:
782
820
  ```typescript
783
- indexes: [
784
- {
785
- name: 'idx_active_records',
786
- columns: ['created_at'],
787
- where: 'deleted_at IS NULL' // Only index active records
788
- }
821
+ indexes: [
822
+ {
823
+ columns: ['created_at'],
824
+ name: 'idx_active_records',
825
+ where: 'deleted_at IS NULL', // Only index active records
826
+ },
789
827
  ]
790
828
  ```
791
829
 
@@ -809,15 +847,15 @@ const employees = createTable({
809
847
 
810
848
  1. **Use type inference**: Let TypeScript infer types from your schema:
811
849
  ```typescript
812
- const { schema } = createTable(config)
850
+ const { schema } = createTable(config)
813
851
  type User = z.infer<typeof schema>
814
852
  ```
815
853
 
816
854
  2. **Validate at boundaries**: Use the generated schema to validate external data:
817
855
  ```typescript
818
- const result = users.schema.safeParse(inputData)
819
- if (!result.success) {
820
- console.error(result.error)
856
+ const result = users.schema.safeParse(inputData)
857
+ if (!result.success) {
858
+ console.error(result.error)
821
859
  }
822
860
  ```
823
861
 
@@ -832,40 +870,43 @@ Single-column foreign keys are supported at the column level. Composite foreign
832
870
  **Pattern for composite foreign keys:**
833
871
 
834
872
  ```typescript
835
- // 1. Create parent table with composite primary key
836
- const { table: ordersTable } = createTable({
837
- name: 'orders',
838
- columns: [
839
- { name: 'tenant_id', schema: z.string() },
840
- { name: 'order_id', schema: z.int() },
841
- { name: 'total', schema: z.number() },
842
- ],
843
- primaryKeys: ['tenant_id', 'order_id'],
844
- })
845
-
846
- // 2. Create child table with matching columns
847
- const { table: itemsTable } = createTable({
848
- name: 'order_items',
849
- columns: [
850
- { name: 'id', schema: z.int() },
851
- { name: 'tenant_id', schema: z.string() },
852
- { name: 'order_id', schema: z.int() },
853
- { name: 'product', schema: z.string() },
854
- ],
855
- primaryKeys: ['id'],
856
- })
857
-
858
- // 3. Execute table creation
859
- db.exec(ordersTable)
860
- db.exec(itemsTable)
861
-
862
- // 4. Add composite foreign key constraint
863
- db.exec(`
864
- ALTER TABLE order_items
865
- ADD CONSTRAINT fk_order
866
- FOREIGN KEY (tenant_id, order_id)
867
- REFERENCES orders(tenant_id, order_id)
868
- ON DELETE CASCADE
873
+ // 1. Create parent table with composite primary key
874
+ const { table: ordersTable } = createTable({
875
+ columns: [
876
+ { name: 'tenant_id', schema: z.string() },
877
+ { name: 'order_id', schema: z.int() },
878
+ { name: 'total', schema: z.number() },
879
+ ],
880
+ name: 'orders',
881
+ primaryKeys: [
882
+ 'tenant_id',
883
+ 'order_id',
884
+ ],
885
+ })
886
+
887
+ // 2. Create child table with matching columns
888
+ const { table: itemsTable } = createTable({
889
+ columns: [
890
+ { name: 'id', schema: z.int() },
891
+ { name: 'tenant_id', schema: z.string() },
892
+ { name: 'order_id', schema: z.int() },
893
+ { name: 'product', schema: z.string() },
894
+ ],
895
+ name: 'order_items',
896
+ primaryKeys: ['id'],
897
+ })
898
+
899
+ // 3. Execute table creation
900
+ db.exec(ordersTable)
901
+ db.exec(itemsTable)
902
+
903
+ // 4. Add composite foreign key constraint
904
+ db.exec(`
905
+ ALTER TABLE order_items
906
+ ADD CONSTRAINT fk_order
907
+ FOREIGN KEY (tenant_id, order_id)
908
+ REFERENCES orders(tenant_id, order_id)
909
+ ON DELETE CASCADE
869
910
  `)
870
911
  ```
871
912
 
@@ -878,7 +919,7 @@ Only specific Zod validations generate CHECK constraints:
878
919
 
879
920
  Custom refinements and complex validations work at the application level but don't generate SQL constraints:
880
921
 
881
- ```typescript
922
+ ```plaintext
882
923
  {
883
924
  name: 'email',
884
925
  schema: z.string().refine(val => val.includes('@'), 'Must contain @')
@@ -894,7 +935,7 @@ Custom refinements and complex validations work at the application level but don
894
935
 
895
936
  Arrays and objects are stored as TEXT with JSON serialization. You must handle serialization manually:
896
937
 
897
- ```typescript
938
+ ```plaintext
898
939
  { name: 'tags', schema: z.array(z.string()) }
899
940
  // SQL: tags TEXT NOT NULL
900
941
 
@@ -908,7 +949,7 @@ const tags = JSON.parse(result.tags) // ['tech', 'news']
908
949
 
909
950
  **Recommendation:** For better queryability, consider separate tables for array data:
910
951
  ```typescript
911
- // Instead of storing tags as JSON array
952
+ // Instead of storing tags as JSON array
912
953
  // Use a junction table: post_tags (post_id, tag_id)
913
954
  ```
914
955
 
@@ -916,7 +957,7 @@ const tags = JSON.parse(result.tags) // ['tech', 'news']
916
957
 
917
958
  Dates are stored as TEXT in ISO 8601 format. SQLite doesn't have a native DATE type:
918
959
 
919
- ```typescript
960
+ ```plaintext
920
961
  { name: 'created_at', schema: z.date() }
921
962
  // SQL: created_at DATE NOT NULL
922
963
  // Stored as TEXT: '2026-01-07T12:30:00.000Z'
@@ -961,12 +1002,12 @@ This tool is designed specifically for SQLite. Some features may not translate t
961
1002
  **Critical:** SQLite does not enforce foreign key constraints by default. You must enable them:
962
1003
 
963
1004
  ```typescript
964
- // Example using db0
965
- const db = createDatabase(sqlite({
966
- name: './db.sqlite',
967
- }))
968
-
969
- // Enable for each database connection
1005
+ // Example using db0
1006
+ const db = createDatabase(sqlite({
1007
+ name: './db.sqlite',
1008
+ }))
1009
+
1010
+ // Enable for each database connection
970
1011
  db.exec('PRAGMA foreign_keys = ON')
971
1012
  ```
972
1013