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/LICENSE +21 -0
- package/README.md +395 -354
- package/dist/index.d.mts +460 -0
- package/dist/index.mjs +283 -0
- package/package.json +38 -55
- package/dist/index.cjs +0 -255
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -478
- package/dist/index.d.ts +0 -478
- package/dist/index.js +0 -253
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
|
|
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 {
|
|
59
|
-
import {
|
|
60
|
-
|
|
61
|
-
const users = createTable({
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{ name: '
|
|
65
|
-
{ name: '
|
|
66
|
-
|
|
67
|
-
{ name: 'created_at', schema: z.date().default(new Date()) }
|
|
68
|
-
],
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
username: 'john',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
```
|
|
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 {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
{ name: '
|
|
158
|
-
{ name: '
|
|
159
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
184
|
-
name: string
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
type ForeignKeyAction
|
|
204
|
-
| '
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
unique?: boolean
|
|
220
|
-
where?: string
|
|
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()
|
|
273
|
-
z.number().default(0)
|
|
274
|
-
z.string().nullable()
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
{ name: '
|
|
390
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
{ name: '
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
primaryKeys: [
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
{ name: '
|
|
420
|
-
{ name: '
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
primaryKeys: [
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
{
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
533
|
+
```plaintext
|
|
525
534
|
indexes: [
|
|
526
|
-
{
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
542
|
-
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
{ name: '
|
|
599
|
-
{ name: '
|
|
600
|
-
|
|
601
|
-
{ name: 'role', schema: z.enum([
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
{ name: '
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
{
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
},
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
{
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
{ name: '
|
|
678
|
-
{ name: '
|
|
679
|
-
{ name: '
|
|
680
|
-
{ name: '
|
|
681
|
-
{ name: '
|
|
682
|
-
|
|
683
|
-
{ name: 'active', schema: z.boolean().default(true) },
|
|
684
|
-
{
|
|
685
|
-
name: 'category_id',
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
],
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
{
|
|
697
|
-
{
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
{ name: '
|
|
714
|
-
{ name: '
|
|
715
|
-
{
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
table: 'employees',
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
],
|
|
726
|
-
|
|
727
|
-
|
|
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()
|
|
742
|
-
z.number().min(0)
|
|
743
|
-
z.enum([
|
|
744
|
-
|
|
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)
|
|
750
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
where: 'deleted_at IS NULL'
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
{ name: '
|
|
840
|
-
{ name: '
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
primaryKeys: [
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
{ name: '
|
|
853
|
-
{ name: '
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
db.exec(
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|