litedbmodel 0.22.3 → 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.
- package/README.md +89 -79
- package/dist/Column.d.ts +17 -9
- package/dist/Column.d.ts.map +1 -1
- package/dist/Column.js +39 -4
- package/dist/Column.js.map +1 -1
- package/dist/DBModel.d.ts +39 -16
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +69 -41
- package/dist/DBModel.js.map +1 -1
- package/dist/SqlFragment.d.ts +118 -0
- package/dist/SqlFragment.d.ts.map +1 -0
- package/dist/SqlFragment.js +237 -0
- package/dist/SqlFragment.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/docs/COMPARISON.md +25 -20
- package/docs/sql-tagged-template.md +259 -0
- package/package.json +1 -1
|
@@ -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) |
|