@livestore/common 0.1.0-dev.9 → 0.2.0-dev.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/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/fixture.d.ts +46 -34
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/adapter-types.d.ts +1 -1
- package/dist/adapter-types.js +1 -1
- package/dist/derived-mutations.d.ts +4 -4
- package/dist/derived-mutations.d.ts.map +1 -1
- package/dist/derived-mutations.js.map +1 -1
- package/dist/devtools/devtools-messages.d.ts +53 -53
- package/dist/devtools/devtools-messages.js +1 -1
- package/dist/devtools/devtools-messages.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mutation.d.ts +1 -1
- package/dist/mutation.d.ts.map +1 -1
- package/dist/mutation.js +6 -1
- package/dist/mutation.js.map +1 -1
- package/dist/query-builder/api.d.ts +190 -0
- package/dist/query-builder/api.d.ts.map +1 -0
- package/dist/query-builder/api.js +8 -0
- package/dist/query-builder/api.js.map +1 -0
- package/dist/query-builder/impl.d.ts +12 -0
- package/dist/query-builder/impl.d.ts.map +1 -0
- package/dist/query-builder/impl.js +226 -0
- package/dist/query-builder/impl.js.map +1 -0
- package/dist/query-builder/impl.test.d.ts +2 -0
- package/dist/query-builder/impl.test.d.ts.map +1 -0
- package/dist/query-builder/impl.test.js +183 -0
- package/dist/query-builder/impl.test.js.map +1 -0
- package/dist/query-builder/mod.d.ts +10 -0
- package/dist/query-builder/mod.d.ts.map +1 -0
- package/dist/query-builder/mod.js +10 -0
- package/dist/query-builder/mod.js.map +1 -0
- package/dist/query-info.d.ts +29 -39
- package/dist/query-info.d.ts.map +1 -1
- package/dist/query-info.js +4 -35
- package/dist/query-info.js.map +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/mutations.d.ts +8 -8
- package/dist/schema/schema-helpers.d.ts +2 -2
- package/dist/schema/schema-helpers.d.ts.map +1 -1
- package/dist/schema/system-tables.d.ts +246 -204
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/table-def.d.ts +45 -24
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +10 -1
- package/dist/schema/table-def.js.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts +1 -0
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +8 -5
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sync/next/test/mutation-fixtures.d.ts +7 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter-types.ts +1 -1
- package/src/derived-mutations.ts +4 -8
- package/src/devtools/devtools-messages.ts +1 -1
- package/src/index.ts +1 -0
- package/src/mutation.ts +9 -2
- package/src/query-builder/api.ts +288 -0
- package/src/query-builder/impl.test.ts +205 -0
- package/src/query-builder/impl.ts +268 -0
- package/src/query-builder/mod.ts +10 -0
- package/src/query-info.ts +66 -93
- package/src/schema/index.ts +4 -2
- package/src/schema/schema-helpers.ts +2 -2
- package/src/schema/table-def.ts +99 -68
- package/src/sql-queries/sql-queries.ts +9 -6
- package/src/version.ts +1 -1
@@ -0,0 +1,288 @@
|
|
1
|
+
import type { GetValForKey } from '@livestore/utils'
|
2
|
+
import { type Option, Predicate, type Schema } from '@livestore/utils/effect'
|
3
|
+
|
4
|
+
import type { SessionIdSymbol } from '../adapter-types.js'
|
5
|
+
import type { QueryInfo } from '../query-info.js'
|
6
|
+
import type { DbSchema } from '../schema/index.js'
|
7
|
+
import type { SqliteDsl } from '../schema/table-def.js'
|
8
|
+
import type { SqlValue } from '../util.js'
|
9
|
+
|
10
|
+
export type QueryBuilderAst = QueryBuilderAst.SelectQuery | QueryBuilderAst.CountQuery | QueryBuilderAst.RowQuery
|
11
|
+
|
12
|
+
export namespace QueryBuilderAst {
|
13
|
+
export type SelectQuery = {
|
14
|
+
readonly _tag: 'SelectQuery'
|
15
|
+
readonly columns: string[]
|
16
|
+
readonly pickFirst: false | { fallback: () => any }
|
17
|
+
readonly select: {
|
18
|
+
columns: ReadonlyArray<string>
|
19
|
+
}
|
20
|
+
readonly orderBy: ReadonlyArray<OrderBy>
|
21
|
+
readonly offset: Option.Option<number>
|
22
|
+
readonly limit: Option.Option<number>
|
23
|
+
readonly tableDef: DbSchema.TableDefBase
|
24
|
+
readonly where: ReadonlyArray<QueryBuilderAst.Where>
|
25
|
+
readonly resultSchemaSingle: Schema.Schema<any>
|
26
|
+
}
|
27
|
+
|
28
|
+
export type CountQuery = {
|
29
|
+
readonly _tag: 'CountQuery'
|
30
|
+
readonly tableDef: DbSchema.TableDefBase
|
31
|
+
readonly where: ReadonlyArray<QueryBuilderAst.Where>
|
32
|
+
readonly resultSchema: Schema.Schema<number, ReadonlyArray<{ count: number }>>
|
33
|
+
}
|
34
|
+
|
35
|
+
export type RowQuery = {
|
36
|
+
readonly _tag: 'RowQuery'
|
37
|
+
readonly tableDef: DbSchema.TableDefBase
|
38
|
+
readonly id: string | SessionIdSymbol
|
39
|
+
readonly insertValues: Record<string, unknown>
|
40
|
+
}
|
41
|
+
|
42
|
+
export type Where = {
|
43
|
+
readonly col: string
|
44
|
+
readonly op: QueryBuilder.WhereOps
|
45
|
+
readonly value: unknown
|
46
|
+
}
|
47
|
+
|
48
|
+
export type OrderBy = {
|
49
|
+
readonly col: string
|
50
|
+
readonly direction: 'asc' | 'desc'
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
export const QueryBuilderAstSymbol = Symbol.for('QueryBuilderAst')
|
55
|
+
export type QueryBuilderAstSymbol = typeof QueryBuilderAstSymbol
|
56
|
+
export const TypeId = Symbol.for('QueryBuilder')
|
57
|
+
export type TypeId = typeof TypeId
|
58
|
+
|
59
|
+
export const isQueryBuilder = (value: unknown): value is QueryBuilder<any, any, any> =>
|
60
|
+
Predicate.hasProperty(value, TypeId)
|
61
|
+
|
62
|
+
export type QueryBuilder<
|
63
|
+
TResult,
|
64
|
+
TTableDef extends DbSchema.TableDefBase,
|
65
|
+
/** Used to gradually remove features from the API based on the query context */
|
66
|
+
TWithout extends QueryBuilder.ApiFeature = never,
|
67
|
+
TQueryInfo extends QueryInfo = QueryInfo.None,
|
68
|
+
> = {
|
69
|
+
readonly [TypeId]: TypeId
|
70
|
+
readonly [QueryBuilderAstSymbol]: QueryBuilderAst
|
71
|
+
readonly asSql: () => { query: string; bindValues: SqlValue[] }
|
72
|
+
readonly toString: () => string
|
73
|
+
} & Omit<QueryBuilder.ApiFull<TResult, TTableDef, TWithout, TQueryInfo>, TWithout>
|
74
|
+
|
75
|
+
export namespace QueryBuilder {
|
76
|
+
export type Any = QueryBuilder<any, any, any, any>
|
77
|
+
export type WhereOps = WhereOps.Equality | WhereOps.Order | WhereOps.Like | WhereOps.In
|
78
|
+
|
79
|
+
export namespace WhereOps {
|
80
|
+
export type Equality = '=' | '!='
|
81
|
+
export type Order = '<' | '>' | '<=' | '>='
|
82
|
+
export type Like = 'LIKE' | 'NOT LIKE' | 'ILIKE' | 'NOT ILIKE'
|
83
|
+
export type In = 'IN' | 'NOT IN'
|
84
|
+
|
85
|
+
export type SingleValue = Equality | Order | Like
|
86
|
+
export type MultiValue = In
|
87
|
+
}
|
88
|
+
|
89
|
+
export type ApiFeature = 'select' | 'where' | 'count' | 'orderBy' | 'offset' | 'limit' | 'first' | 'row'
|
90
|
+
|
91
|
+
export type WhereParams<TTableDef extends DbSchema.TableDefBase> = Partial<{
|
92
|
+
[K in keyof TTableDef['sqliteDef']['columns']]:
|
93
|
+
| TTableDef['sqliteDef']['columns'][K]['schema']['Type']
|
94
|
+
| { op: QueryBuilder.WhereOps.SingleValue; value: TTableDef['sqliteDef']['columns'][K]['schema']['Type'] }
|
95
|
+
| {
|
96
|
+
op: QueryBuilder.WhereOps.MultiValue
|
97
|
+
value: ReadonlyArray<TTableDef['sqliteDef']['columns'][K]['schema']['Type']>
|
98
|
+
}
|
99
|
+
| undefined
|
100
|
+
}>
|
101
|
+
|
102
|
+
export type OrderByParams<TTableDef extends DbSchema.TableDefBase> = ReadonlyArray<{
|
103
|
+
col: keyof TTableDef['sqliteDef']['columns'] & string
|
104
|
+
direction: 'asc' | 'desc'
|
105
|
+
}>
|
106
|
+
|
107
|
+
export type ApiFull<
|
108
|
+
TResult,
|
109
|
+
TTableDef extends DbSchema.TableDefBase,
|
110
|
+
TWithout extends ApiFeature,
|
111
|
+
TQueryInfo extends QueryInfo,
|
112
|
+
> = {
|
113
|
+
/**
|
114
|
+
* `SELECT *` is the default
|
115
|
+
*
|
116
|
+
* Example:
|
117
|
+
* ```ts
|
118
|
+
* db.todos.select('id', 'text', 'completed')
|
119
|
+
* db.todos.select('id', { pluck: true })
|
120
|
+
* ```
|
121
|
+
*/
|
122
|
+
readonly select: {
|
123
|
+
<TColumn extends keyof TTableDef['sqliteDef']['columns'] & string, TPluck extends boolean = false>(
|
124
|
+
column: TColumn,
|
125
|
+
options?: { pluck: TPluck },
|
126
|
+
): QueryBuilder<
|
127
|
+
TPluck extends true
|
128
|
+
? ReadonlyArray<TTableDef['sqliteDef']['columns'][TColumn]['schema']['Type']>
|
129
|
+
: ReadonlyArray<{
|
130
|
+
readonly [K in TColumn]: TTableDef['sqliteDef']['columns'][K]['schema']['Type']
|
131
|
+
}>,
|
132
|
+
TTableDef,
|
133
|
+
TWithout | 'row' | 'select',
|
134
|
+
TQueryInfo
|
135
|
+
>
|
136
|
+
<TColumns extends keyof TTableDef['sqliteDef']['columns'] & string>(
|
137
|
+
...columns: TColumns[]
|
138
|
+
// TODO also support arbitrary SQL selects
|
139
|
+
// params: QueryBuilderSelectParams,
|
140
|
+
): QueryBuilder<
|
141
|
+
ReadonlyArray<{
|
142
|
+
readonly [K in TColumns]: TTableDef['sqliteDef']['columns'][K]['schema']['Type']
|
143
|
+
}>,
|
144
|
+
TTableDef,
|
145
|
+
TWithout | 'row' | 'select' | 'count',
|
146
|
+
TQueryInfo
|
147
|
+
>
|
148
|
+
}
|
149
|
+
|
150
|
+
/**
|
151
|
+
* Notes:
|
152
|
+
* - All where clauses are `AND`ed together by default.
|
153
|
+
* - `null` values only support `=` and `!=` which is translated to `IS NULL` and `IS NOT NULL`.
|
154
|
+
*
|
155
|
+
* Example:
|
156
|
+
* ```ts
|
157
|
+
* db.todos.where('completed', true)
|
158
|
+
* db.todos.where('completed', '!=', true)
|
159
|
+
* db.todos.where({ completed: true })
|
160
|
+
* db.todos.where({ completed: { op: '!=', value: true } })
|
161
|
+
* ```
|
162
|
+
*
|
163
|
+
* TODO: Also support `OR`
|
164
|
+
*/
|
165
|
+
readonly where: {
|
166
|
+
<TParams extends QueryBuilder.WhereParams<TTableDef>>(
|
167
|
+
params: TParams,
|
168
|
+
): QueryBuilder<TResult, TTableDef, TWithout | 'row' | 'select', TQueryInfo>
|
169
|
+
<TColName extends keyof TTableDef['sqliteDef']['columns']>(
|
170
|
+
col: TColName,
|
171
|
+
value: TTableDef['sqliteDef']['columns'][TColName]['schema']['Type'],
|
172
|
+
): QueryBuilder<TResult, TTableDef, TWithout | 'row' | 'select', TQueryInfo>
|
173
|
+
<TColName extends keyof TTableDef['sqliteDef']['columns']>(
|
174
|
+
col: TColName,
|
175
|
+
op: QueryBuilder.WhereOps,
|
176
|
+
value: TTableDef['sqliteDef']['columns'][TColName]['schema']['Type'],
|
177
|
+
): QueryBuilder<TResult, TTableDef, TWithout | 'row' | 'select', TQueryInfo>
|
178
|
+
}
|
179
|
+
|
180
|
+
/**
|
181
|
+
* Example:
|
182
|
+
* ```ts
|
183
|
+
* db.todos.count()
|
184
|
+
* db.todos.count().where('completed', true)
|
185
|
+
* ```
|
186
|
+
*/
|
187
|
+
readonly count: () => QueryBuilder<
|
188
|
+
number,
|
189
|
+
TTableDef,
|
190
|
+
TWithout | 'row' | 'count' | 'select' | 'orderBy' | 'first' | 'offset' | 'limit',
|
191
|
+
TQueryInfo
|
192
|
+
>
|
193
|
+
|
194
|
+
/**
|
195
|
+
* Example:
|
196
|
+
* ```ts
|
197
|
+
* db.todos.orderBy('createdAt', 'desc')
|
198
|
+
* ```
|
199
|
+
*/
|
200
|
+
readonly orderBy: {
|
201
|
+
<TColName extends keyof TTableDef['sqliteDef']['columns'] & string>(
|
202
|
+
col: TColName,
|
203
|
+
direction: 'asc' | 'desc',
|
204
|
+
): QueryBuilder<TResult, TTableDef, TWithout, TQueryInfo>
|
205
|
+
<TParams extends QueryBuilder.OrderByParams<TTableDef>>(
|
206
|
+
params: TParams,
|
207
|
+
): QueryBuilder<TResult, TTableDef, TWithout, TQueryInfo>
|
208
|
+
}
|
209
|
+
|
210
|
+
/**
|
211
|
+
* Example:
|
212
|
+
* ```ts
|
213
|
+
* db.todos.offset(10)
|
214
|
+
* ```
|
215
|
+
*/
|
216
|
+
readonly offset: (
|
217
|
+
offset: number,
|
218
|
+
) => QueryBuilder<TResult, TTableDef, TWithout | 'row' | 'offset' | 'orderBy', TQueryInfo>
|
219
|
+
|
220
|
+
/**
|
221
|
+
* Example:
|
222
|
+
* ```ts
|
223
|
+
* db.todos.limit(10)
|
224
|
+
* ```
|
225
|
+
*/
|
226
|
+
readonly limit: (
|
227
|
+
limit: number,
|
228
|
+
) => QueryBuilder<TResult, TTableDef, TWithout | 'row' | 'limit' | 'offset' | 'first' | 'orderBy', TQueryInfo>
|
229
|
+
|
230
|
+
/**
|
231
|
+
* Example:
|
232
|
+
* ```ts
|
233
|
+
* db.todos.first()
|
234
|
+
* ```
|
235
|
+
*/
|
236
|
+
readonly first: <TFallback extends GetSingle<TResult> = never>(options?: {
|
237
|
+
fallback?: () => TFallback
|
238
|
+
}) => QueryBuilder<
|
239
|
+
TFallback | GetSingle<TResult>,
|
240
|
+
TTableDef,
|
241
|
+
TWithout | 'row' | 'first' | 'orderBy' | 'select' | 'limit' | 'offset' | 'where',
|
242
|
+
TQueryInfo
|
243
|
+
>
|
244
|
+
|
245
|
+
/**
|
246
|
+
*
|
247
|
+
*/
|
248
|
+
readonly row: TTableDef['options']['isSingleton'] extends true
|
249
|
+
? () => QueryBuilder<RowQuery.Result<TTableDef>, TTableDef, QueryBuilder.ApiFeature, QueryInfo.Row>
|
250
|
+
: TTableDef['options']['deriveMutations']['enabled'] extends false
|
251
|
+
? (_: 'Error: Need to enable deriveMutations to use row()') => any
|
252
|
+
: TTableDef['options']['requiredInsertColumnNames'] extends never
|
253
|
+
? (
|
254
|
+
id: string | SessionIdSymbol,
|
255
|
+
) => QueryBuilder<RowQuery.Result<TTableDef>, TTableDef, QueryBuilder.ApiFeature, QueryInfo.Row>
|
256
|
+
: <TOptions extends RowQuery.RequiredColumnsOptions<TTableDef>>(
|
257
|
+
id: string | SessionIdSymbol,
|
258
|
+
opts: TOptions,
|
259
|
+
) => QueryBuilder<RowQuery.Result<TTableDef>, TTableDef, QueryBuilder.ApiFeature, QueryInfo.Row>
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
export namespace RowQuery {
|
264
|
+
export type RequiredColumnsOptions<TTableDef extends DbSchema.TableDefBase> = {
|
265
|
+
/**
|
266
|
+
* Values to be inserted into the row if it doesn't exist yet
|
267
|
+
*/
|
268
|
+
insertValues: Pick<
|
269
|
+
SqliteDsl.FromColumns.RowDecodedAll<TTableDef['sqliteDef']['columns']>,
|
270
|
+
SqliteDsl.FromColumns.RequiredInsertColumnNames<Omit<TTableDef['sqliteDef']['columns'], 'id'>>
|
271
|
+
>
|
272
|
+
}
|
273
|
+
|
274
|
+
export type Result<TTableDef extends DbSchema.TableDefBase> = TTableDef['options']['isSingleColumn'] extends true
|
275
|
+
? GetValForKey<SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>, 'value'>
|
276
|
+
: SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
|
277
|
+
|
278
|
+
export type ResultEncoded<TTableDef extends DbSchema.TableDefBase> =
|
279
|
+
TTableDef['options']['isSingleColumn'] extends true
|
280
|
+
? GetValForKey<SqliteDsl.FromColumns.RowEncoded<TTableDef['sqliteDef']['columns']>, 'value'>
|
281
|
+
: SqliteDsl.FromColumns.RowEncoded<TTableDef['sqliteDef']['columns']>
|
282
|
+
}
|
283
|
+
|
284
|
+
type GetSingle<T> = T extends ReadonlyArray<infer U> ? U : never
|
285
|
+
|
286
|
+
// export type QueryBuilderParamRef = { _tag: 'QueryBuilderParamRef' }
|
287
|
+
// export type QueryBuilderSelectParams = { [key: string]: QueryBuilderSelectParam }
|
288
|
+
// export type QueryBuilderSelectParam = boolean | ((ref: QueryBuilderParamRef) => QueryBuilder<any, any>)
|
@@ -0,0 +1,205 @@
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
2
|
+
import { describe, expect, it } from 'vitest'
|
3
|
+
|
4
|
+
import { DbSchema } from '../schema/index.js'
|
5
|
+
|
6
|
+
const todos = DbSchema.table(
|
7
|
+
'todos',
|
8
|
+
{
|
9
|
+
id: DbSchema.text({ primaryKey: true }),
|
10
|
+
text: DbSchema.text({ default: '', nullable: false }),
|
11
|
+
completed: DbSchema.boolean({ default: false, nullable: false }),
|
12
|
+
status: DbSchema.text({ schema: Schema.Literal('active', 'completed') }),
|
13
|
+
deletedAt: DbSchema.datetime({ nullable: true }),
|
14
|
+
// TODO consider leaning more into Effect schema
|
15
|
+
// other: Schema.Number.pipe(DbSchema.asInteger),
|
16
|
+
},
|
17
|
+
{ deriveMutations: true },
|
18
|
+
)
|
19
|
+
|
20
|
+
const comments = DbSchema.table('comments', {
|
21
|
+
id: DbSchema.text({ primaryKey: true }),
|
22
|
+
text: DbSchema.text({ default: '', nullable: false }),
|
23
|
+
todoId: DbSchema.text({}),
|
24
|
+
})
|
25
|
+
|
26
|
+
const db = { todos: todos.query, comments: comments.query }
|
27
|
+
|
28
|
+
describe('query builder', () => {
|
29
|
+
describe('basic queries', () => {
|
30
|
+
it('should handle simple SELECT queries', () => {
|
31
|
+
expect(db.todos.asSql()).toMatchInlineSnapshot(`
|
32
|
+
{
|
33
|
+
"bindValues": [],
|
34
|
+
"query": "SELECT * FROM 'todos'",
|
35
|
+
}
|
36
|
+
`)
|
37
|
+
|
38
|
+
expect(db.todos.select('id', 'text').asSql()).toMatchInlineSnapshot(`
|
39
|
+
{
|
40
|
+
"bindValues": [],
|
41
|
+
"query": "SELECT id, text FROM 'todos'",
|
42
|
+
}
|
43
|
+
`)
|
44
|
+
})
|
45
|
+
|
46
|
+
it('should handle WHERE clauses', () => {
|
47
|
+
expect(db.todos.select('id', 'text').where('completed', true).asSql()).toMatchInlineSnapshot(`
|
48
|
+
{
|
49
|
+
"bindValues": [
|
50
|
+
1,
|
51
|
+
],
|
52
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ?",
|
53
|
+
}
|
54
|
+
`)
|
55
|
+
expect(db.todos.select('id', 'text').where('completed', '!=', true).asSql()).toMatchInlineSnapshot(`
|
56
|
+
{
|
57
|
+
"bindValues": [
|
58
|
+
1,
|
59
|
+
],
|
60
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed != ?",
|
61
|
+
}
|
62
|
+
`)
|
63
|
+
expect(db.todos.select('id', 'text').where({ completed: true }).asSql()).toMatchInlineSnapshot(`
|
64
|
+
{
|
65
|
+
"bindValues": [
|
66
|
+
1,
|
67
|
+
],
|
68
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ?",
|
69
|
+
}
|
70
|
+
`)
|
71
|
+
expect(db.todos.select('id', 'text').where({ completed: undefined }).asSql()).toMatchInlineSnapshot(`
|
72
|
+
{
|
73
|
+
"bindValues": [],
|
74
|
+
"query": "SELECT id, text FROM 'todos'",
|
75
|
+
}
|
76
|
+
`)
|
77
|
+
expect(
|
78
|
+
db.todos
|
79
|
+
.select('id', 'text')
|
80
|
+
.where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } })
|
81
|
+
.asSql(),
|
82
|
+
).toMatchInlineSnapshot(`
|
83
|
+
{
|
84
|
+
"bindValues": [
|
85
|
+
"2024-01-01T00:00:00.000Z",
|
86
|
+
],
|
87
|
+
"query": "SELECT id, text FROM 'todos' WHERE deletedAt <= ?",
|
88
|
+
}
|
89
|
+
`)
|
90
|
+
})
|
91
|
+
|
92
|
+
it('should handle OFFSET and LIMIT clauses', () => {
|
93
|
+
expect(db.todos.select('id', 'text').where('completed', true).offset(10).limit(10).asSql())
|
94
|
+
.toMatchInlineSnapshot(`
|
95
|
+
{
|
96
|
+
"bindValues": [
|
97
|
+
1,
|
98
|
+
10,
|
99
|
+
10,
|
100
|
+
],
|
101
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ? OFFSET ? LIMIT ?",
|
102
|
+
}
|
103
|
+
`)
|
104
|
+
})
|
105
|
+
|
106
|
+
it('should handle COUNT queries', () => {
|
107
|
+
expect(db.todos.count().asSql()).toMatchInlineSnapshot(`
|
108
|
+
{
|
109
|
+
"bindValues": [],
|
110
|
+
"query": "SELECT COUNT(*) as count FROM 'todos'",
|
111
|
+
}
|
112
|
+
`)
|
113
|
+
expect(db.todos.count().where('completed', true).asSql()).toMatchInlineSnapshot(`
|
114
|
+
{
|
115
|
+
"bindValues": [
|
116
|
+
1,
|
117
|
+
],
|
118
|
+
"query": "SELECT COUNT(*) as count FROM 'todos' WHERE completed = ?",
|
119
|
+
}
|
120
|
+
`)
|
121
|
+
})
|
122
|
+
|
123
|
+
it('should handle NULL comparisons', () => {
|
124
|
+
expect(db.todos.select('id', 'text').where('deletedAt', '=', null).asSql()).toMatchInlineSnapshot(`
|
125
|
+
{
|
126
|
+
"bindValues": [],
|
127
|
+
"query": "SELECT id, text FROM 'todos' WHERE deletedAt IS NULL",
|
128
|
+
}
|
129
|
+
`)
|
130
|
+
expect(db.todos.select('id', 'text').where('deletedAt', '!=', null).asSql()).toMatchInlineSnapshot(`
|
131
|
+
{
|
132
|
+
"bindValues": [],
|
133
|
+
"query": "SELECT id, text FROM 'todos' WHERE deletedAt IS NOT NULL",
|
134
|
+
}
|
135
|
+
`)
|
136
|
+
})
|
137
|
+
|
138
|
+
it('should handle orderBy', () => {
|
139
|
+
expect(db.todos.orderBy('completed', 'desc').asSql()).toMatchInlineSnapshot(`
|
140
|
+
{
|
141
|
+
"bindValues": [],
|
142
|
+
"query": "SELECT * FROM 'todos' ORDER BY completed desc",
|
143
|
+
}
|
144
|
+
`)
|
145
|
+
|
146
|
+
expect(db.todos.orderBy([{ col: 'completed', direction: 'desc' }]).asSql()).toMatchInlineSnapshot(`
|
147
|
+
{
|
148
|
+
"bindValues": [],
|
149
|
+
"query": "SELECT * FROM 'todos' ORDER BY completed desc",
|
150
|
+
}
|
151
|
+
`)
|
152
|
+
|
153
|
+
expect(db.todos.orderBy([]).asSql()).toMatchInlineSnapshot(`
|
154
|
+
{
|
155
|
+
"bindValues": [],
|
156
|
+
"query": "SELECT * FROM 'todos'",
|
157
|
+
}
|
158
|
+
`)
|
159
|
+
})
|
160
|
+
})
|
161
|
+
|
162
|
+
describe('row queries', () => {
|
163
|
+
it('should handle row queries', () => {
|
164
|
+
expect(db.todos.row('123', { insertValues: { status: 'completed' } }).asSql()).toMatchInlineSnapshot(`
|
165
|
+
{
|
166
|
+
"bindValues": [
|
167
|
+
"123",
|
168
|
+
],
|
169
|
+
"query": "SELECT * FROM 'todos' WHERE id = ?",
|
170
|
+
}
|
171
|
+
`)
|
172
|
+
})
|
173
|
+
})
|
174
|
+
})
|
175
|
+
|
176
|
+
// TODO nested queries
|
177
|
+
// const rawSql = <A, I>(sql: string, params: { [key: string]: any }, schema: Schema.Schema<A, I>) =>
|
178
|
+
// ({
|
179
|
+
// sql,
|
180
|
+
// params,
|
181
|
+
// schema,
|
182
|
+
// }) as any as QueryBuilder<A, any>
|
183
|
+
|
184
|
+
// Translates to
|
185
|
+
// SELECT todos.*, (SELECT COUNT(*) FROM comments WHERE comments.todoId = todos.id) AS commentsCount
|
186
|
+
// FROM todos WHERE todos.completed = true
|
187
|
+
// const q4CommentsCountSchema = Schema.Struct({ count: Schema.Number }).pipe(
|
188
|
+
// Schema.pluck('count'),
|
189
|
+
// Schema.Array,
|
190
|
+
// Schema.headOrElse(),
|
191
|
+
// )
|
192
|
+
// const _q4$ = db.todos
|
193
|
+
// .select({
|
194
|
+
// commentsCount: (ref) =>
|
195
|
+
// rawSql(
|
196
|
+
// sql`SELECT COUNT(*) as count FROM comments WHERE comments.todoId = $todoId`,
|
197
|
+
// { todoId: ref },
|
198
|
+
// q4CommentsCountSchema,
|
199
|
+
// ),
|
200
|
+
// })
|
201
|
+
// .where({ completed: true })
|
202
|
+
|
203
|
+
// const _q5$ = db.todos
|
204
|
+
// .select({ commentsCount: (todoId: TODO) => comments.query.where({ todoId }).count() })
|
205
|
+
// .where({ completed: true })
|