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 +895 -0
- package/dist/index.cjs +255 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +478 -0
- package/dist/index.d.ts +478 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
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.
|