zod-sqlite 0.1.0-alpha.1

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 ADDED
@@ -0,0 +1,895 @@
1
+ # Zod to 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.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [Core Concepts](#core-concepts)
11
+ - [API Reference](#api-reference)
12
+ - [Type Mappings](#type-mappings)
13
+ - [Column Configuration](#column-configuration)
14
+ - [Constraints and Validation](#constraints-and-validation)
15
+ - [Primary Keys](#primary-keys)
16
+ - [Foreign Keys and Relationships](#foreign-keys-and-relationships)
17
+ - [Indexes](#indexes)
18
+ - [Advanced Usage](#advanced-usage)
19
+ - [Best Practices](#best-practices)
20
+ - [Limitations](#limitations)
21
+
22
+ ## Overview
23
+
24
+ This tool bridges the gap between Zod schemas and SQLite database definitions. Instead of maintaining separate validation logic and database schemas, define your table structure once using Zod, and get:
25
+
26
+ - Syntactically correct SQL CREATE TABLE statements
27
+ - Appropriate indexes for query optimization
28
+ - Zod schemas for runtime validation
29
+ - Full TypeScript type inference
30
+ - Automatic CHECK constraints from Zod validation rules
31
+
32
+ ### Key Features
33
+
34
+ - **Type Safety**: Full TypeScript support with automatic type inference from Zod schemas
35
+ - **Single Source of Truth**: Define your schema once, use it everywhere
36
+ - **Comprehensive Validation**: Automatic CHECK constraints for enums, literals, and numeric ranges
37
+ - **Relationship Support**: Foreign key constraints with cascade actions
38
+ - **Index Management**: Support for standard, unique, and partial indexes
39
+ - **SQLite Compliance**: Generates valid SQLite 3 SQL statements
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install zod-to-sqlite
45
+ ```
46
+
47
+ Requires Zod v4 as a peer dependency:
48
+
49
+ ```bash
50
+ npm install zod@^4.0.0
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ Here's a simple example creating a users table:
56
+
57
+ ```typescript
58
+ import { createTable } from 'zod-to-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>
98
+ // { id: number; email: string; username: string; created_at: Date }
99
+ ```
100
+
101
+ ## Core Concepts
102
+
103
+ ### Schema-Driven Design
104
+
105
+ Zod schemas serve as the source of truth for your database structure. Each column is defined with a Zod schema that serves dual purposes:
106
+
107
+ 1. **Database Level**: Determines the SQLite column type (TEXT, INTEGER, REAL, BLOB, NULL)
108
+ 2. **Application Level**: Provides runtime validation and TypeScript type inference
109
+
110
+ ### The createTable Function
111
+
112
+ `createTable` is the primary entry point. It accepts a configuration object and returns three things:
113
+
114
+ ```typescript
115
+ const result = createTable(config)
116
+ // Returns: { table: string, indexes: string[], schema: ZodObject }
117
+ ```
118
+
119
+ - `table`: A SQL CREATE TABLE statement ready to execute
120
+ - `indexes`: An array of SQL CREATE INDEX statements
121
+ - `schema`: A Zod object schema for data validation
122
+
123
+ ### Column Definition Flow
124
+
125
+ 1. Define columns with Zod schemas
126
+ 2. Each schema is analyzed to determine SQLite type
127
+ 3. Metadata is extracted (nullable, default values, constraints)
128
+ 4. Appropriate SQL column definitions are generated
129
+ 5. CHECK constraints are created from Zod validation rules
130
+
131
+ ## API Reference
132
+
133
+ ### createTable(config)
134
+
135
+ Creates a table definition with SQL statements and validation schema.
136
+
137
+ **Parameters:**
138
+
139
+ - `config`: `TableConfig` - Table configuration object
140
+
141
+ **Returns:**
142
+
143
+ ```typescript
144
+ {
145
+ table: string // CREATE TABLE SQL statement
146
+ indexes: string[] // Array of CREATE INDEX statements
147
+ schema: ZodObject // Zod validation schema
148
+ }
149
+ ```
150
+
151
+ **Example:**
152
+
153
+ ```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']
162
+ })
163
+ ```
164
+
165
+ ### TableConfig
166
+
167
+ Configuration object for table creation.
168
+
169
+ ```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
175
+ }
176
+ ```
177
+
178
+ ### ColumnConfig
179
+
180
+ Configuration for a single column.
181
+
182
+ ```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
188
+ }
189
+ ```
190
+
191
+ ### ForeignKeyReference
192
+
193
+ Foreign key constraint configuration.
194
+
195
+ ```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'
209
+ ```
210
+
211
+ ### IndexConfig
212
+
213
+ Index configuration for query optimization.
214
+
215
+ ```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
221
+ }
222
+ ```
223
+
224
+ ## Type Mappings
225
+
226
+ Zod types map to SQLite column types as follows:
227
+
228
+ ### Text Types
229
+
230
+ | Zod Schema | SQLite Type |
231
+ |------------|-------------|
232
+ | `z.string()` | TEXT |
233
+ | `z.enum(['a', 'b'])` | TEXT |
234
+ | `z.literal('value')` | TEXT |
235
+ | `z.date()` | DATE |
236
+ | `z.iso.datetime()` | DATETIME |
237
+ | `z.array()` | TEXT |
238
+ | `z.object()` | TEXT |
239
+
240
+ ### Numeric Types
241
+
242
+ | Zod Schema | SQLite Type |
243
+ |------------|-------------|
244
+ | `z.number()` | REAL |
245
+ | `z.int()` | INTEGER |
246
+ | `z.int32()` | INTEGER |
247
+ | `z.uint32()` | INTEGER |
248
+ | `z.safeint()` | INTEGER |
249
+ | `z.float32()` | FLOAT |
250
+ | `z.float64()` | FLOAT |
251
+
252
+ ### Other Types
253
+
254
+ | Zod Schema | SQLite Type |
255
+ |------------|-------------|
256
+ | `z.boolean()` | BOOLEAN (stored as 0/1) |
257
+ | `z.bigint()` | BIGINT |
258
+ | `z.date()` | DATE |
259
+ | `z.file()` | BLOB |
260
+ | `z.null()` | NULL |
261
+ | `z.undefined()` | NULL |
262
+
263
+ ### Type Wrappers
264
+
265
+ These Zod wrappers are automatically unwrapped:
266
+
267
+ - `.optional()` - Makes column nullable
268
+ - `.nullable()` - Makes column nullable
269
+ - `.default(value)` - Adds DEFAULT clause
270
+
271
+ ```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'
275
+ ```
276
+
277
+ ## Column Configuration
278
+
279
+ ### Basic Columns
280
+
281
+ ```typescript
282
+ { name: 'email', schema: z.email() }
283
+ // SQL: email TEXT NOT NULL
284
+ ```
285
+
286
+ ### Optional and Nullable Columns
287
+
288
+ ```typescript
289
+ { name: 'bio', schema: z.string().optional() }
290
+ // SQL: bio TEXT
291
+
292
+ { name: 'middle_name', schema: z.string().nullable() }
293
+ // SQL: middle_name TEXT
294
+ ```
295
+
296
+ ### Columns with Default Values
297
+
298
+ ```typescript
299
+ { name: 'status', schema: z.enum(['active', 'inactive']).default('active') }
300
+ // SQL: status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'inactive'))
301
+
302
+ { name: 'count', schema: z.int().default(0) }
303
+ // SQL: count INTEGER NOT NULL DEFAULT 0
304
+ ```
305
+
306
+ ### Unique Columns
307
+
308
+ ```typescript
309
+ { name: 'username', schema: z.string(), unique: true }
310
+ // SQL: username TEXT NOT NULL UNIQUE
311
+ ```
312
+
313
+ ## Constraints and Validation
314
+
315
+ SQL CHECK constraints are automatically generated from Zod validation rules.
316
+
317
+ ### Enum Constraints
318
+
319
+ ```typescript
320
+ {
321
+ name: 'role',
322
+ schema: z.enum(['admin', 'user', 'guest'])
323
+ }
324
+ // SQL: role TEXT NOT NULL CHECK(role IN ('admin', 'user', 'guest'))
325
+ ```
326
+
327
+ ### Literal Constraints
328
+
329
+ ```typescript
330
+ {
331
+ name: 'type',
332
+ schema: z.literal('premium')
333
+ }
334
+ // SQL: type TEXT NOT NULL CHECK(type = 'premium')
335
+
336
+ {
337
+ name: 'category',
338
+ schema: z.union([
339
+ z.literal('electronics'),
340
+ z.literal('clothing'),
341
+ z.literal('food')
342
+ ])
343
+ }
344
+ // SQL: category TEXT NOT NULL CHECK(category IN ('electronics', 'clothing', 'food'))
345
+ ```
346
+
347
+ ### Numeric Range Constraints
348
+
349
+ ```typescript
350
+ {
351
+ name: 'age',
352
+ schema: z.int().min(18).max(120)
353
+ }
354
+ // SQL: age INTEGER NOT NULL CHECK(age >= 18 AND age <= 120)
355
+
356
+ {
357
+ name: 'price',
358
+ schema: z.number().min(0)
359
+ }
360
+ // SQL: price REAL NOT NULL CHECK(price >= 0)
361
+ ```
362
+
363
+ ### String Length Constraints
364
+
365
+ ```typescript
366
+ {
367
+ name: 'username',
368
+ schema: z.string().min(3).max(20)
369
+ }
370
+ // SQL: username TEXT NOT NULL CHECK(length(username) >= 3 AND length(username) <= 20)
371
+
372
+ {
373
+ name: 'code',
374
+ schema: z.string().length(6)
375
+ }
376
+ // SQL: code TEXT NOT NULL CHECK(length(code) = 6)
377
+ ```
378
+
379
+ ## Primary Keys
380
+
381
+ ### Single Column Primary Key
382
+
383
+ The most common pattern for entity tables:
384
+
385
+ ```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
+ })
394
+ // SQL: PRIMARY KEY (id)
395
+ ```
396
+
397
+ ### Composite Primary Key
398
+
399
+ Used for junction tables and multi-tenant data:
400
+
401
+ ```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
+ })
410
+ // SQL: PRIMARY KEY (user_id, role_id)
411
+ ```
412
+
413
+ ### Multi-Tenant Example
414
+
415
+ ```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
+ })
425
+ // SQL: PRIMARY KEY (tenant_id, doc_id)
426
+ ```
427
+
428
+ ## Foreign Keys and Relationships
429
+
430
+ ### Basic Foreign Key
431
+
432
+ ```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
+ })
448
+ // SQL: author_id INTEGER NOT NULL REFERENCES users(id)
449
+ ```
450
+
451
+ ### Cascade Delete
452
+
453
+ Automatically delete child records when parent is deleted:
454
+
455
+ ```typescript
456
+ {
457
+ name: 'user_id',
458
+ schema: z.int(),
459
+ references: {
460
+ table: 'users',
461
+ column: 'id',
462
+ onDelete: 'CASCADE'
463
+ }
464
+ }
465
+ // SQL: user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
466
+ ```
467
+
468
+ ### Restrict Delete
469
+
470
+ Prevent deletion of parent if children exist:
471
+
472
+ ```typescript
473
+ {
474
+ name: 'category_id',
475
+ schema: z.int(),
476
+ references: {
477
+ table: 'categories',
478
+ column: 'id',
479
+ onDelete: 'RESTRICT'
480
+ }
481
+ }
482
+ // SQL: category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE RESTRICT
483
+ ```
484
+
485
+ ### Set Null on Delete
486
+
487
+ Set foreign key to NULL when parent is deleted:
488
+
489
+ ```typescript
490
+ {
491
+ name: 'manager_id',
492
+ schema: z.int().nullable(),
493
+ references: {
494
+ table: 'employees',
495
+ column: 'id',
496
+ onDelete: 'SET NULL'
497
+ }
498
+ }
499
+ // SQL: manager_id INTEGER REFERENCES employees(id) ON DELETE SET NULL
500
+ ```
501
+
502
+ ### Update Cascade
503
+
504
+ Propagate updates to child records:
505
+
506
+ ```typescript
507
+ {
508
+ name: 'parent_id',
509
+ schema: z.int(),
510
+ references: {
511
+ table: 'categories',
512
+ column: 'id',
513
+ onDelete: 'CASCADE',
514
+ onUpdate: 'CASCADE'
515
+ }
516
+ }
517
+ // SQL: parent_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE ON UPDATE CASCADE
518
+ ```
519
+
520
+ ## Indexes
521
+
522
+ ### Simple Index
523
+
524
+ ```typescript
525
+ indexes: [
526
+ {
527
+ name: 'idx_users_email',
528
+ columns: ['email']
529
+ }
530
+ ]
531
+ // SQL: CREATE INDEX idx_users_email ON users (email);
532
+ ```
533
+
534
+ ### Unique Index
535
+
536
+ Enforce uniqueness at the database level:
537
+
538
+ ```typescript
539
+ indexes: [
540
+ {
541
+ name: 'idx_users_username',
542
+ columns: ['username'],
543
+ unique: true
544
+ }
545
+ ]
546
+ // SQL: CREATE UNIQUE INDEX idx_users_username ON users (username);
547
+ ```
548
+
549
+ ### Composite Index
550
+
551
+ Index multiple columns together for multi-column queries:
552
+
553
+ ```typescript
554
+ indexes: [
555
+ {
556
+ name: 'idx_posts_author_date',
557
+ columns: ['author_id', 'created_at']
558
+ }
559
+ ]
560
+ // SQL: CREATE INDEX idx_posts_author_date ON posts (author_id, created_at);
561
+ ```
562
+
563
+ Benefits queries like:
564
+ ```sql
565
+ SELECT * FROM posts WHERE author_id = 123 ORDER BY created_at DESC;
566
+ SELECT * FROM posts WHERE author_id = 123 AND created_at > '2024-01-01';
567
+ ```
568
+
569
+ ### Partial Index
570
+
571
+ Index only rows matching a condition:
572
+
573
+ ```typescript
574
+ indexes: [
575
+ {
576
+ name: 'idx_active_users',
577
+ columns: ['last_login'],
578
+ where: 'deleted_at IS NULL'
579
+ }
580
+ ]
581
+ // SQL: CREATE INDEX idx_active_users ON posts (last_login) WHERE deleted_at IS NULL;
582
+ ```
583
+
584
+ Benefits:
585
+ - Smaller index size
586
+ - Faster updates to non-matching rows
587
+ - Optimized for filtered queries
588
+
589
+ ## Advanced Usage
590
+
591
+ ### Complete Blog Example
592
+
593
+ ```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']
668
+ })
669
+ ```
670
+
671
+ ### E-commerce Example
672
+
673
+ ```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
+ ]
704
+ })
705
+ ```
706
+
707
+ ### Self-Referencing Table
708
+
709
+ ```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
+ ]
730
+ })
731
+ ```
732
+
733
+ ## Best Practices
734
+
735
+ ### Schema Design
736
+
737
+ 1. **Always define primary keys**: Every table should have a primary key for reliable row identification.
738
+
739
+ 2. **Use appropriate types**: Choose the most specific Zod type that matches your data:
740
+ ```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
745
+ ```
746
+
747
+ 3. **Add validation at the schema level**: Leverage Zod's validation to prevent invalid data:
748
+ ```typescript
749
+ z.string().min(3).max(50) // Username length
750
+ z.number().min(0).max(5) // Rating scale
751
+ ```
752
+
753
+ ### Foreign Keys
754
+
755
+ 1. **Always reference primary keys**: Foreign keys should point to primary key or unique columns.
756
+
757
+ 2. **Choose appropriate cascade actions**:
758
+ - Use `CASCADE` for child records that don't make sense without parent
759
+ - Use `RESTRICT` to prevent accidental deletion of referenced data
760
+ - Use `SET NULL` when relationship is optional
761
+
762
+ 3. **Enable foreign key enforcement** in SQLite:
763
+ ```sql
764
+ PRAGMA foreign_keys = ON;
765
+ ```
766
+
767
+ ### Indexes
768
+
769
+ 1. **Index foreign keys**: Always create indexes on foreign key columns for JOIN performance.
770
+
771
+ 2. **Index frequently queried columns**: Add indexes to columns used in WHERE clauses.
772
+
773
+ 3. **Use composite indexes wisely**: Order matters - most selective column first:
774
+ ```typescript
775
+ indexes: [
776
+ // Good: Filters by tenant first (high selectivity)
777
+ { name: 'idx_tenant_user', columns: ['tenant_id', 'user_id'] }
778
+ ]
779
+ ```
780
+
781
+ 4. **Consider partial indexes**: Reduce index size for filtered queries:
782
+ ```typescript
783
+ indexes: [
784
+ {
785
+ name: 'idx_active_records',
786
+ columns: ['created_at'],
787
+ where: 'deleted_at IS NULL' // Only index active records
788
+ }
789
+ ]
790
+ ```
791
+
792
+ 5. **Don't over-index**: Each index slows down writes. Only add indexes that improve query performance.
793
+
794
+ ### Naming Conventions
795
+
796
+ 1. **Tables**: Use plural nouns in snake_case
797
+ - `users`, `blog_posts`, `order_items`
798
+
799
+ 2. **Columns**: Use snake_case
800
+ - `user_id`, `created_at`, `first_name`
801
+
802
+ 3. **Indexes**: Use descriptive names with `idx_` prefix
803
+ - `idx_users_email`, `idx_posts_author_date`
804
+
805
+ 4. **Foreign keys**: Name with `_id` suffix
806
+ - `author_id`, `category_id`, `parent_id`
807
+
808
+ ### Type Safety
809
+
810
+ 1. **Use type inference**: Let TypeScript infer types from your schema:
811
+ ```typescript
812
+ const { schema } = createTable(config)
813
+ type User = z.infer<typeof schema>
814
+ ```
815
+
816
+ 2. **Validate at boundaries**: Use the generated schema to validate external data:
817
+ ```typescript
818
+ const result = users.schema.safeParse(inputData)
819
+ if (!result.success) {
820
+ console.error(result.error)
821
+ }
822
+ ```
823
+
824
+ ## Limitations
825
+
826
+ ### Composite Foreign Keys
827
+
828
+ Currently only single-column foreign keys are supported. Composite foreign keys must be added manually:
829
+
830
+ ```typescript
831
+ // Not supported in column config
832
+ // Workaround: Add after table creation
833
+ ALTER TABLE order_items
834
+ ADD CONSTRAINT fk_order
835
+ FOREIGN KEY (tenant_id, order_id)
836
+ REFERENCES orders(tenant_id, id);
837
+ ```
838
+
839
+ ### Complex CHECK Constraints
840
+
841
+ Only specific Zod validations are converted to CHECK constraints:
842
+ - Enums and literals
843
+ - Numeric min/max
844
+ - String length min/max/equals
845
+
846
+ Custom refinements and complex validations are not converted:
847
+
848
+ ```typescript
849
+ z.string().refine(val => val.includes('@'), 'Must contain @')
850
+ // This validation works in TypeScript but won't create a CHECK constraint
851
+ ```
852
+
853
+ ### Array and Object Storage
854
+
855
+ Arrays and objects are stored as TEXT (JSON). You must handle serialization:
856
+
857
+ ```typescript
858
+ { name: 'tags', schema: z.array(z.string()) }
859
+ // Stored as TEXT, you need to JSON.stringify/parse manually
860
+ ```
861
+
862
+ ### Date Handling
863
+
864
+ Dates are stored as TEXT in ISO 8601 format. SQLite has limited native date support:
865
+
866
+ ```typescript
867
+ { name: 'created_at', schema: z.date() }
868
+ // Stored as TEXT: '2026-01-06T12:30:00.000Z'
869
+ // Use SQLite date functions for queries: date(created_at)
870
+ ```
871
+
872
+ ### No Migration Support
873
+
874
+ This generates CREATE TABLE statements but doesn't handle schema migrations. For production use, consider a migration tool.
875
+
876
+ ### SQLite-Specific
877
+
878
+ This is designed specifically for SQLite. Features may not translate to other databases:
879
+ - Type affinity rules are SQLite-specific
880
+ - Some constraint syntax is SQLite-specific
881
+ - Boolean storage as 0/1 is SQLite convention
882
+
883
+ ### Foreign Key Enforcement
884
+
885
+ SQLite doesn't enforce foreign keys by default. You must enable them:
886
+
887
+ ```sql
888
+ PRAGMA foreign_keys = ON;
889
+ ```
890
+
891
+ This must be set for each database connection.
892
+
893
+ ---
894
+
895
+ For issues, feature requests, or contributions, please visit the project repository.