litedbmodel 0.22.2 → 1.0.0

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.
@@ -12,7 +12,7 @@ A detailed comparison of litedbmodel with popular TypeScript/JavaScript ORMs.
12
12
  | | Migrations | Manual | Manual | Kit (optional) | Built-in | Built-in | Built-in | Built-in | Knex |
13
13
  | **Query API** | Primary query style | Tuple array | Fluent QB | Fluent QB | QB / Find | Fluent client | QB / EM | Fluent | Fluent |
14
14
  | | Raw SQL escape hatch† | ✅ `query()` | ⚠️ `sql` | ⚠️ `sql` | ⚠️ `query()` | ⚠️ `$queryRaw` | ⚠️ `execute()` | ⚠️ `query()` | ⚠️ `raw()` |
15
- | **Type Safety**†† | Compile-time type safety | ⚠️ partial | ✅ | ✅ | ⚠️ partial | ✅ | ⚠️ partial | ❌ runtime-heavy | ⚠️ partial |
15
+ | **Type Safety**†† | Compile-time type safety | ✅ | ✅ | ✅ | ⚠️ partial | ✅ | ⚠️ partial | ❌ runtime-heavy | ⚠️ partial |
16
16
  | **Column Refs**‡ | How columns are referenced | **Symbols** | String literals | Column objects | Strings/decorators | Object keys | Strings | Strings | Strings |
17
17
  | **IDE Support**§ | Refactoring safety (IDE) | ✅ Full | ❌ | ⚠️ Partial | ❌ | ❌ | ❌ | ❌ | ❌ |
18
18
  | **Updates** | Declarative partial update | ✅ **SKIP** | ❌ manual | ❌ manual | ❌ manual | ⚠️ `undefined` | ❌ manual | ❌ manual | ❌ manual |
@@ -52,7 +52,8 @@ Whether the *tool's standard relation API* fetches nested relations in a single
52
52
 
53
53
  **†† Type safety (litedbmodel):**
54
54
  - `[Column, value]` tuples: **Compile-time** type checking via Column symbols
55
- - Operator expressions (`` [`${Model.col} > ?`, val] ``): **ESLint plugin** checks column references (not compile-time)
55
+ - ``sql`` tagged template conditions (``[sql`${Col} > ?`, val]``): **Compile-time** type checking — the ``sql`` tag preserves the Column's value type
56
+ - ESLint plugin provides additional SQL syntax and cross-model validation
56
57
 
57
58
  **\*\* DB portability:**
58
59
  - **Multi-DB** = databases the tool can talk to
@@ -106,6 +107,7 @@ class UserModel extends DBModel {
106
107
  }
107
108
 
108
109
  // Migrations: Manual SQL files
110
+ // Code generation: litedbmodel-gen generates @column() from schema.sql (optional)
109
111
  ```
110
112
 
111
113
  ### Prisma
@@ -156,7 +158,7 @@ drizzle-kit generate:pg
156
158
  |---------|-------------|--------|---------|---------|--------|
157
159
  | **Schema Format** | TS Decorators | Prisma DSL | TS Decorators | TS Schema | TS Interface |
158
160
  | **Migration Tool** | Manual | Built-in | Built-in | drizzle-kit | Manual |
159
- | **Auto-Generate** | ❌ | ✅ | ✅ | ✅ | ❌ |
161
+ | **Auto-Generate** | ⚠️ [litedbmodel-gen](https://www.npmjs.com/package/litedbmodel-gen) | ✅ | ✅ | ✅ | ❌ |
160
162
  | **Reversible** | Manual | ✅ | ✅ | ⚠️ | Manual |
161
163
 
162
164
  ---
@@ -175,7 +177,7 @@ const users = await User.find([
175
177
  [User.is_active, true],
176
178
  User.or(
177
179
  [[User.role, 'admin']],
178
- [[User.role, 'moderator'], [`${User.level} >= ?`, 5]],
180
+ [[User.role, 'moderator'], [sql`${User.level} >= ?`, 5]],
179
181
  ),
180
182
  ]);
181
183
 
@@ -352,7 +354,7 @@ const users = await User.query(`
352
354
  | Scenario | litedbmodel | Query Builder ORMs |
353
355
  |----------|-------------|-------------------|
354
356
  | **Simple CRUD** | Tuple API: `[[User.name, 'John']]` | Object: `{ name: 'John' }` |
355
- | **Complex WHERE** | SQL operators: `` [`${User.age} > ?`, 18] `` | DSL methods: `.where('age', '>', 18)` |
357
+ | **Complex WHERE** | `sql` tag: `` [sql`${User.age} > ?`, 18] `` | DSL methods: `.where('age', '>', 18)` |
356
358
  | **JOINs, CTEs** | Real SQL via `query()` or Query-Based Models | Limited DSL or raw escape |
357
359
  | **Debugging** | See actual SQL | Translate DSL → SQL mentally |
358
360
  | **Learning** | Know SQL = Ready | Learn DSL + SQL |
@@ -369,9 +371,9 @@ const users = await User.query(`
369
371
 
370
372
  ## 5. Type Safety
371
373
 
372
- ### litedbmodel - Compile-time (Column Symbols)
374
+ ### litedbmodel - Compile-time (Column Symbols + `sql` Tag)
373
375
 
374
- Type derived from decorators, checked via Column symbol references.
376
+ Type derived from decorators, checked via Column symbol references and `sql` tagged template.
375
377
 
376
378
  ```typescript
377
379
  // Compile-time validation via Column symbols
@@ -383,8 +385,9 @@ await User.create([
383
385
 
384
386
  // Conditions also type-checked
385
387
  await User.find([
386
- [User.id, 1], // ✅ Compile-time (Column symbol)
387
- [`${User.age} > ?`, 18], // ✅ ESLint checks column reference
388
+ [User.id, 1], // ✅ Compile-time (Column symbol)
389
+ [sql`${User.age} > ?`, 18], // ✅ Compile-time (sql tag preserves Column value type)
390
+ // [sql`${User.age} > ?`, 'x'], // ❌ Compile error (string not assignable to number)
388
391
  ]);
389
392
  ```
390
393
 
@@ -475,13 +478,14 @@ const users = await db
475
478
  | Feature | litedbmodel | Prisma | TypeORM | Drizzle | Kysely |
476
479
  |---------|-------------|--------|---------|---------|--------|
477
480
  | **Type Source** | Decorators → Symbols | .prisma → Generated | Decorators | TS Schema | TS Interface |
478
- | **Condition Check** | ✅ Full† | ✅ Full | ⚠️ Partial | ✅ Full | ✅ Full |
481
+ | **Condition Check** | ✅ Full | ✅ Full | ⚠️ Partial | ✅ Full | ✅ Full |
479
482
  | **Result Types** | ✅ Model types | ✅ Generated | ✅ Entity types | ✅ Inferred | ✅ Inferred |
480
- | **Compile-Time** | ⚠️ Partial† | ✅ | ⚠️ | ✅ | ✅ |
483
+ | **Compile-Time** | ✅ Full† | ✅ | ⚠️ | ✅ | ✅ |
481
484
 
482
- *† litedbmodel's type safety is split between compile-time and ESLint:*
485
+ *† litedbmodel's compile-time type safety covers both patterns:*
483
486
  - *`[Column, value]` tuples: Compile-time type checking via Column symbols*
484
- - *Operator expressions (`` [`${Model.col} > ?`, val] ``): ESLint plugin checks column references (not compile-time)*
487
+ - *`[sql\`${Col} > ?\`, val]` tuples: Compile-time type checking — the `sql` tag preserves the Column's value type*
488
+ - *ESLint plugin provides additional SQL syntax validation and cross-model column checking*
485
489
 
486
490
  ---
487
491
 
@@ -1152,13 +1156,14 @@ For detailed benchmark results comparing litedbmodel with Prisma, Kysely, Drizzl
1152
1156
  ### litedbmodel's Characteristics
1153
1157
 
1154
1158
  1. **Symbol-Based Columns**: IDE "Find References" and "Rename Symbol" work
1155
- 2. **SKIP Sentinel**: Declarative conditional fields without if-statements
1156
- 3. **Minimal Footprint**: 60KB minified, no binary dependencies
1157
- 4. **Middleware**: Cross-cutting concern support (logging, auth, metrics)
1158
- 5. **Auto N+1 Prevention**: Batch loading enabled automatically for multiple records
1159
- 6. **Composite Key Support**: Full support for composite keys in relations
1160
- 7. **Fixed SQL Parameters**: `ANY()` and `unnest()` keep parameter count constant
1161
- 8. **Readable SQL**: Simple, debuggable queries (no hash aliases or deep nesting)
1159
+ 2. **`sql` Tagged Template**: Type-safe operator conditions and parameterized queries
1160
+ 3. **SKIP Sentinel**: Declarative conditional fields without if-statements
1161
+ 4. **Minimal Footprint**: 60KB minified, no binary dependencies
1162
+ 5. **Middleware**: Cross-cutting concern support (logging, auth, metrics)
1163
+ 6. **Auto N+1 Prevention**: Batch loading enabled automatically for multiple records
1164
+ 7. **Composite Key Support**: Full support for composite keys in relations
1165
+ 8. **Fixed SQL Parameters**: `ANY()` and `unnest()` keep parameter count constant
1166
+ 9. **Readable SQL**: Simple, debuggable queries (no hash aliases or deep nesting)
1162
1167
 
1163
1168
  ### Trade-offs
1164
1169
 
@@ -0,0 +1,259 @@
1
+ # `sql` Tagged Template Literal
2
+
3
+ The `sql` tagged template provides type-safe, parameterized SQL fragments for conditions, `withQuery`, `QUERY`, and `execute`.
4
+
5
+ ```typescript
6
+ import { sql } from 'litedbmodel';
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Conditions
12
+
13
+ ### Column Tuples with `sql` Tag (Pattern A)
14
+
15
+ Use `[sql\`...\`, value]` tuples for operator conditions. The `sql` tag extracts the Column reference and preserves its value type, so the tuple's second element is type-checked:
16
+
17
+ ```typescript
18
+ await User.find([
19
+ [sql`${User.age} > ?`, 18], // ✅ number
20
+ [sql`${User.age} BETWEEN ? AND ?`, [18, 65]], // ✅ number[]
21
+ [sql`${User.name} LIKE ?`, '%test%'], // ✅ string
22
+ [sql`${User.status} IN (?)`, ['active', 'pending']], // ✅ string[]
23
+ sql`${User.deleted_at} IS NULL`, // ✅ no value needed
24
+ ]);
25
+ ```
26
+
27
+ Type errors are caught at compile time:
28
+
29
+ ```typescript
30
+ [sql`${User.age} > ?`, 'hello'] // ❌ Type error: string is not assignable to number
31
+ [sql`${User.name} LIKE ?`, 42] // ❌ Type error: number is not assignable to string
32
+ ```
33
+
34
+ ### Embedded Values (Pattern B)
35
+
36
+ Values can be interpolated directly in the template. The `sql` tag automatically converts them to `?` placeholders and extracts them as parameters:
37
+
38
+ ```typescript
39
+ await User.find([
40
+ sql`${User.age} > ${18}`,
41
+ sql`${User.age} BETWEEN ${18} AND ${65}`,
42
+ sql`${User.name} LIKE ${'%test%'}`,
43
+ sql`${User.status} IN ${['active', 'pending']}`,
44
+ sql`${User.deleted_at} IS NULL`,
45
+ ]);
46
+ ```
47
+
48
+ Both patterns produce the same internal representation and can be mixed freely.
49
+
50
+ ### SKIP Pattern
51
+
52
+ Both patterns work with `SKIP` for conditional conditions:
53
+
54
+ ```typescript
55
+ import { SKIP } from 'litedbmodel';
56
+
57
+ await User.find([
58
+ [User.deleted, false],
59
+ query.name ? [sql`${User.name} LIKE ?`, `%${query.name}%`] : SKIP,
60
+ [User.status, query.status ?? SKIP],
61
+ ]);
62
+ ```
63
+
64
+ ### OR Conditions
65
+
66
+ ```typescript
67
+ await User.find([
68
+ [sql`${User.age} >= ?`, 18],
69
+ User.or(
70
+ [[sql`${User.role} = ?`, 'admin']],
71
+ [[sql`${User.role} = ?`, 'moderator']],
72
+ ),
73
+ ]);
74
+ ```
75
+
76
+ ### Conditions Builder
77
+
78
+ The `Conditions` class provides an `addSql()` method for dynamic condition building:
79
+
80
+ ```typescript
81
+ const where = new Conditions<User>();
82
+ where.add(User.is_active, true);
83
+ where.addSql(sql`${User.age} > ?`, 18);
84
+ where.addSql(sql`${User.deleted_at} IS NULL`); // value-free
85
+ const users = await User.find(where.build());
86
+ ```
87
+
88
+ ---
89
+
90
+ ## `withQuery` — Parameterized Query-Based Models
91
+
92
+ The `sql` tag eliminates manual `$1`/`$2` placeholder numbering and DB dialect differences. Parameters are co-located with their SQL usage:
93
+
94
+ ```typescript
95
+ @model('sales_report')
96
+ class SalesReportModel extends DBModel {
97
+ @column() product_id?: number;
98
+ @column() total_revenue?: number;
99
+
100
+ static forPeriod(startDate: string, endDate: string) {
101
+ return this.withQuery(sql`
102
+ SELECT p.id AS product_id, SUM(oi.price) AS total_revenue
103
+ FROM products p
104
+ INNER JOIN order_items oi ON p.id = oi.product_id
105
+ INNER JOIN orders o ON oi.order_id = o.id
106
+ WHERE o.status = 'completed'
107
+ AND o.created_at >= ${startDate}
108
+ AND o.created_at < ${endDate}
109
+ GROUP BY p.id
110
+ `);
111
+ }
112
+ }
113
+
114
+ const Q1Report = SalesReport.forPeriod('2024-01-01', '2024-04-01');
115
+ const results = await Q1Report.find([
116
+ [sql`${SalesReport.total_revenue} > ?`, 10000],
117
+ ]);
118
+ ```
119
+
120
+ The `sql` tag always uses `?` placeholders internally. The dialect conversion (`?` → `$1`/`$2` for PostgreSQL) is handled transparently at execution time.
121
+
122
+ The `{ sql, params }` object form is still supported for backward compatibility.
123
+
124
+ ---
125
+
126
+ ## `QUERY` — Static Query-Based Models
127
+
128
+ Use the `sql` tag for `QUERY` to get Column and table name references that are refactoring-safe:
129
+
130
+ ```typescript
131
+ @model('user_activity')
132
+ class UserActivityModel extends DBModel {
133
+ @column() user_id?: number;
134
+ @column() user_name?: string;
135
+ @column() total_posts?: number;
136
+
137
+ static QUERY = sql`
138
+ SELECT
139
+ ${User.id} AS user_id,
140
+ ${User.name} AS user_name,
141
+ COUNT(${Post.id}) AS total_posts
142
+ FROM ${User.TABLE_NAME}
143
+ LEFT JOIN ${Post.TABLE_NAME} ON ${User.id} = ${Post.user_id}
144
+ GROUP BY ${User.id}, ${User.name}
145
+ `;
146
+ }
147
+ ```
148
+
149
+ When only Column references and `TABLE_NAME` objects are interpolated (no runtime values), the `sql` tag embeds them directly in the SQL string with no parameters.
150
+
151
+ String `QUERY` is still supported for backward compatibility.
152
+
153
+ ---
154
+
155
+ ## `execute` — Raw SQL Execution
156
+
157
+ `DBModel.execute()` accepts `sql` tagged templates:
158
+
159
+ ```typescript
160
+ await DBModel.execute(sql`SELECT process_daily_aggregates(${targetDate})`);
161
+ ```
162
+
163
+ String + params form is still supported:
164
+
165
+ ```typescript
166
+ await DBModel.execute('SELECT process_daily_aggregates($1)', [targetDate]);
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Interpolation Rules
172
+
173
+ The `sql` tag processes interpolated values according to their type:
174
+
175
+ | Interpolated Value | Processing | Example Output |
176
+ |---|---|---|
177
+ | `Column` | Embedded as column name | `age`, `name` |
178
+ | `{ TABLE_NAME }` | Embedded as table name | `users`, `posts` |
179
+ | `sql.raw(str)` | Embedded as raw SQL (no parameterization) | `ASC`, `DISTINCT` |
180
+ | `sql.ref(col)` | Embedded as `table.column` | `users.id` |
181
+ | `parentRef(col)` | Embedded as `table.column` | `users.id` |
182
+ | Nested `sql` fragment | SQL expanded, params merged | See below |
183
+ | Array | Expanded to `?, ?, ?` + params extracted | `?, ?, ?` |
184
+ | Primitive / Date | Replaced with `?` + param extracted | `?` |
185
+ | `null` / `undefined` | Embedded as `NULL` literal | `NULL` |
186
+
187
+ ### `sql.raw()` — Raw SQL Embedding
188
+
189
+ Use for dynamic SQL keywords that should not be parameterized:
190
+
191
+ ```typescript
192
+ const direction = sql.raw(isAsc ? 'ASC' : 'DESC');
193
+ const results = await Ranking.query(
194
+ sql`SELECT * FROM rankings ORDER BY score ${direction} LIMIT ${limit}`
195
+ );
196
+ ```
197
+
198
+ ### `sql.ref()` — Table-Qualified Column References
199
+
200
+ Use for JOINs where table disambiguation is needed:
201
+
202
+ ```typescript
203
+ sql`SELECT ${sql.ref(User.id)}, ${sql.ref(Post.title)}
204
+ FROM users
205
+ JOIN posts ON ${sql.ref(User.id)} = ${sql.ref(Post.user_id)}`
206
+ ```
207
+
208
+ | Interpolation | Output |
209
+ |---|---|
210
+ | `${User.id}` | `id` |
211
+ | `${sql.ref(User.id)}` | `users.id` |
212
+
213
+ ### Nested `sql` Fragments
214
+
215
+ A `sql` fragment can be interpolated inside another. The inner SQL is expanded in place and parameters are merged:
216
+
217
+ ```typescript
218
+ const sub = sql`SELECT user_id FROM orders WHERE status = ${'paid'}`;
219
+ // → { sql: 'SELECT user_id FROM orders WHERE status = ?', params: ['paid'] }
220
+
221
+ sql`SELECT * FROM users WHERE id IN (${sub}) AND age > ${18}`;
222
+ // → {
223
+ // sql: 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = ?) AND age > ?',
224
+ // params: ['paid', 18]
225
+ // }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Return Types
231
+
232
+ The `sql` tag returns different types based on the interpolated values, enabling type-safe usage in different contexts:
233
+
234
+ | Interpolation Pattern | Return Type | Usage |
235
+ |---|---|---|
236
+ | Single `Column` only | `SqlTypedFragment<V, M>` | Condition tuple first element, or value-free condition (IS NULL) |
237
+ | Single `Column` + value(s) | `SqlCondition<M>` | Direct condition element (Pattern B) |
238
+ | Multiple Columns / TABLE_NAMEs | `SqlFragment` | `QUERY`, `withQuery`, `execute` |
239
+ | Mixed (Columns + values) | `SqlFragment` | `withQuery`, `execute` |
240
+
241
+ - `SqlTypedFragment<V, M>` carries the Column's value type `V` and model type `M`, enabling type checking on the tuple's second element.
242
+ - `SqlCondition<M>` carries the model type `M` for model validation.
243
+ - `SqlFragment` is the general type for arbitrary SQL with parameters.
244
+
245
+ All three types have `sql: string` and `params: readonly unknown[]` properties.
246
+
247
+ ---
248
+
249
+ ## Backward Compatibility
250
+
251
+ The `sql` tag is a purely additive feature. All existing patterns continue to work:
252
+
253
+ | Existing Pattern | Status |
254
+ |---|---|
255
+ | `[Column, value]` tuples | Recommended for equality conditions |
256
+ | `[string, value]` tuples (template literals) | Supported (backward compatible) |
257
+ | `{ sql, params }` for `withQuery` | Supported (backward compatible) |
258
+ | `string` for `QUERY` | Supported (backward compatible) |
259
+ | `execute(sql, params)` | Supported (backward compatible) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litedbmodel",
3
- "version": "0.22.2",
3
+ "version": "1.0.0",
4
4
  "description": "A lightweight TypeScript ORM for PostgreSQL, MySQL, and SQLite with Active Record pattern",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",