@bakery-framework/orm 2.0.0-alpha.8 → 2.0.0-rc.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 +9 -9
- package/package.json +3 -3
- package/src/adapters/base.ts +181 -62
- package/src/adapters/mysql.ts +29 -17
- package/src/adapters/observe.ts +11 -11
- package/src/adapters/pgsql.ts +54 -39
- package/src/adapters/registry.ts +13 -13
- package/src/adapters/sqlite.ts +175 -44
- package/src/adapters.ts +4 -4
- package/src/backup.ts +1 -1
- package/src/connection.ts +1 -1
- package/src/define.ts +22 -22
- package/src/field.ts +35 -35
- package/src/globals.d.ts +1 -1
- package/src/index.ts +4 -4
- package/src/orm/index.ts +2 -2
- package/src/orm/mutation.ts +36 -28
- package/src/orm/query.ts +92 -89
- package/src/pool.ts +5 -5
- package/src/schema-registry.ts +3 -3
- package/src/schema-util.ts +48 -17
- package/src/sync/builder.ts +32 -32
- package/src/sync/diff.ts +12 -12
- package/src/sync/engine.ts +19 -19
- package/src/sync/execute.ts +16 -16
- package/src/sync/history.ts +5 -5
- package/src/sync/index.ts +12 -12
- package/src/sync/ledger.ts +23 -23
- package/src/sync/load.ts +16 -16
- package/src/sync/plan.ts +1 -2
- package/src/sync/rename.ts +1 -1
- package/src/sync/rollback.ts +3 -3
- package/src/sync/types.ts +20 -4
- package/src/sync/view-sql.ts +9 -9
- package/templates/schema.example.ts +4 -4
package/src/orm/query.ts
CHANGED
|
@@ -2,18 +2,7 @@ import { Case } from '@bakery-framework/core/utils'
|
|
|
2
2
|
import { throws } from '@bakery-framework/core/utils/common'
|
|
3
3
|
import { getActiveDb, txStorage } from '../connection'
|
|
4
4
|
import type { AppDBSchema as DBSchema } from '../schema-registry'
|
|
5
|
-
import {
|
|
6
|
-
evalOperands,
|
|
7
|
-
isSafeIdentifier,
|
|
8
|
-
qId,
|
|
9
|
-
qRaw,
|
|
10
|
-
SQL_FUNCTIONS,
|
|
11
|
-
col as schemaCol,
|
|
12
|
-
ColumnRef as schemaColumnRef,
|
|
13
|
-
OperatorRef as schemaOperatorRef,
|
|
14
|
-
SQLFunctionRef as schemaSQLFunctionRef,
|
|
15
|
-
WindowRef as schemaWindowRef,
|
|
16
|
-
} from '../schema-util'
|
|
5
|
+
import { ColumnRef as schemaColumnRef, OperatorRef as schemaOperatorRef, SQLFunctionRef as schemaSQLFunctionRef, SQL_FUNCTIONS, WindowRef as schemaWindowRef, col as schemaCol, evalOperands, isSafeIdentifier, nullComparison, qId, qRaw } from '../schema-util'
|
|
17
6
|
import { Mutation } from './mutation'
|
|
18
7
|
|
|
19
8
|
export namespace DB {
|
|
@@ -22,7 +11,7 @@ export namespace DB {
|
|
|
22
11
|
/**
|
|
23
12
|
* Bound a query that only needs its first row. `.get()` otherwise runs the
|
|
24
13
|
* full query and materializes every row into JS objects before discarding all
|
|
25
|
-
* but one
|
|
14
|
+
* but one: `first()` on an unfiltered table scanned the whole table.
|
|
26
15
|
*/
|
|
27
16
|
function singleRow(sql: string): string {
|
|
28
17
|
return /\bLIMIT\s+\d+/i.test(sql) ? sql : `${sql} LIMIT 1`
|
|
@@ -135,7 +124,7 @@ export namespace DB {
|
|
|
135
124
|
* `S`. That read as "columns of the schema in scope" and is not what it
|
|
136
125
|
* means: the right-hand side of a join names a table you are *adding*, so
|
|
137
126
|
* it cannot already be in scope. The parameter was ignored in the body,
|
|
138
|
-
* so every call site got this answer anyway
|
|
127
|
+
* so every call site got this answer anyway: it only misled the reader.
|
|
139
128
|
*/
|
|
140
129
|
export type AllTableColumns = {
|
|
141
130
|
[T in keyof DBSchema]: `${T & string}.${Extract<keyof DBSchema[T] & string, string>}`
|
|
@@ -174,12 +163,12 @@ export namespace DB {
|
|
|
174
163
|
schemaSQLFunctionRef<C>
|
|
175
164
|
|
|
176
165
|
/**
|
|
177
|
-
* `COUNT`, `SUM` and `AVG` also come in a `.distinct` form
|
|
166
|
+
* `COUNT`, `SUM` and `AVG` also come in a `.distinct` form:
|
|
178
167
|
* `DB.count.distinct('users.city')` emits `COUNT(DISTINCT "users"."city")`.
|
|
179
168
|
*
|
|
180
169
|
* Only these three. `DISTINCT` is legal inside `MIN`/`MAX` on every dialect
|
|
181
170
|
* and cannot change their result, so offering it there would imply an effect
|
|
182
|
-
* that does not exist
|
|
171
|
+
* that does not exist: the same reason the builder has no per-column
|
|
183
172
|
* `distinct('col')`.
|
|
184
173
|
*/
|
|
185
174
|
const aggregate = <N extends string>(fnName: N) =>
|
|
@@ -228,7 +217,7 @@ export namespace DB {
|
|
|
228
217
|
/**
|
|
229
218
|
* `'users.score'` or `'users.score DESC'`. The direction is optional and
|
|
230
219
|
* per column, which is why it is spelled inside the string rather than as
|
|
231
|
-
* a separate field
|
|
220
|
+
* a separate field: a window frequently orders by two columns in opposite
|
|
232
221
|
* directions, and one shared flag could not express that.
|
|
233
222
|
*/
|
|
234
223
|
orderBy?: string | string[]
|
|
@@ -245,7 +234,7 @@ export namespace DB {
|
|
|
245
234
|
|
|
246
235
|
const order = list(spec.orderBy).map(entry => {
|
|
247
236
|
// Split the direction off the tail rather than taking a separate
|
|
248
|
-
// argument
|
|
237
|
+
// argument: `safeColumn` would reject 'score DESC' as an identifier, so
|
|
249
238
|
// the two halves have to be validated apart from each other anyway.
|
|
250
239
|
const m = /^(.*?)\s+(ASC|DESC)$/i.exec(String(entry).trim())
|
|
251
240
|
if (!m) return `${safeColumn(String(entry).trim())} ASC`
|
|
@@ -259,7 +248,7 @@ export namespace DB {
|
|
|
259
248
|
/**
|
|
260
249
|
* An aggregate over a window: `SUM("total") OVER (PARTITION BY "user_id")`.
|
|
261
250
|
*
|
|
262
|
-
* ```ts no-check
|
|
251
|
+
* ```ts no-check, illustrative
|
|
263
252
|
* DB.from('orders').select({
|
|
264
253
|
* runningTotal: DB.over(DB.sum('orders.total'), {
|
|
265
254
|
* partitionBy: 'orders.userId',
|
|
@@ -269,7 +258,7 @@ export namespace DB {
|
|
|
269
258
|
* ```
|
|
270
259
|
*
|
|
271
260
|
* Takes an existing function ref rather than a column, so every aggregate the
|
|
272
|
-
* builder already has
|
|
261
|
+
* builder already has, including `DB.count.distinct(…)`, composes with a
|
|
273
262
|
* window without a second set of wrappers.
|
|
274
263
|
*/
|
|
275
264
|
export function over(
|
|
@@ -280,7 +269,7 @@ export namespace DB {
|
|
|
280
269
|
}
|
|
281
270
|
|
|
282
271
|
/**
|
|
283
|
-
* The ranking functions, which take no column
|
|
272
|
+
* The ranking functions, which take no column: the window *is* the argument.
|
|
284
273
|
*
|
|
285
274
|
* Only these three are wrapped by name. The rest of `WINDOW_FUNCTIONS`
|
|
286
275
|
* (`LAG`, `NTILE`, `FIRST_VALUE`, …) take arguments whose meaning differs per
|
|
@@ -299,12 +288,12 @@ export namespace DB {
|
|
|
299
288
|
/**
|
|
300
289
|
* Any window function by name, with its own arguments.
|
|
301
290
|
*
|
|
302
|
-
* ```ts no-check
|
|
291
|
+
* ```ts no-check, illustrative
|
|
303
292
|
* DB.window('LAG', ['orders.total', 1], { orderBy: 'orders.createdAt' })
|
|
304
293
|
* ```
|
|
305
294
|
*
|
|
306
295
|
* The name is checked against `WINDOW_FUNCTIONS` (plus the aggregates) at
|
|
307
|
-
* parse time
|
|
296
|
+
* parse time. It is interpolated, not bound. Arguments go through
|
|
308
297
|
* `evalOperands`, so a bare string binds as a parameter and `DB.col('x')`
|
|
309
298
|
* references a column, exactly as everywhere else in the builder.
|
|
310
299
|
*/
|
|
@@ -384,10 +373,10 @@ export namespace DB {
|
|
|
384
373
|
export type SelectValue<S extends TableSchemas, J extends string> =
|
|
385
374
|
| ColumnString<S, J>
|
|
386
375
|
| SQLFunctionRef<ColumnString<S, J> | '*'>
|
|
387
|
-
//
|
|
376
|
+
// Unparameterized: a window's columns are validated at construction by
|
|
388
377
|
// `safeColumn`, not by the select's column union. Threading `S`/`J` through
|
|
389
378
|
// would mean typing the spec against the same table set, which reads well
|
|
390
|
-
// until a window orders by a *select alias
|
|
379
|
+
// until a window orders by a *select alias*: legal SQL, and not a column
|
|
391
380
|
// of any table in scope.
|
|
392
381
|
| WindowRef
|
|
393
382
|
| QBRaw
|
|
@@ -416,7 +405,7 @@ export namespace DB {
|
|
|
416
405
|
/**
|
|
417
406
|
* Memo for `Case.camel` over result-row keys. Every row of every result set
|
|
418
407
|
* is re-cased key by key, so a 1000-row × 8-column query ran `Case.camel`
|
|
419
|
-
* 8000 times over the same eight strings
|
|
408
|
+
* 8000 times over the same eight strings, and `Case.camel` is two regex
|
|
420
409
|
* passes, one with a replacer callback.
|
|
421
410
|
*
|
|
422
411
|
* Capped `Map`, not `LRUCache`, for the reason given on `snakeCache` in
|
|
@@ -461,13 +450,13 @@ export namespace DB {
|
|
|
461
450
|
abstract parse(): { sql: string; params: any[] }
|
|
462
451
|
|
|
463
452
|
/**
|
|
464
|
-
* `UNION
|
|
453
|
+
* `UNION`: every distinct row from this query and the next.
|
|
465
454
|
*
|
|
466
455
|
* Returns a {@link QBSet}, not `this`. That is the whole shape of the
|
|
467
456
|
* feature: a compound select is not a `SELECT` with an extra clause, it is
|
|
468
457
|
* a different kind of statement whose operands happen to be selects. So
|
|
469
|
-
* `.where()` and `.select()` are gone from the result
|
|
470
|
-
* mean "on which branch?"
|
|
458
|
+
* `.where()` and `.select()` are gone from the result: they would have to
|
|
459
|
+
* mean "on which branch?", and what remains is what SQL allows after the
|
|
471
460
|
* last operand: `orderBy`, `limit`, `offset`.
|
|
472
461
|
*/
|
|
473
462
|
union<Q>(next: QBExecutable<Q>): QBSet<P> {
|
|
@@ -477,7 +466,7 @@ export namespace DB {
|
|
|
477
466
|
])
|
|
478
467
|
}
|
|
479
468
|
|
|
480
|
-
/** `UNION ALL
|
|
469
|
+
/** `UNION ALL`: as `union`, keeping duplicates. Cheaper: no dedupe pass. */
|
|
481
470
|
unionAll<Q>(next: QBExecutable<Q>): QBSet<P> {
|
|
482
471
|
return new QBSet<P>([
|
|
483
472
|
{ op: null, query: this },
|
|
@@ -485,7 +474,7 @@ export namespace DB {
|
|
|
485
474
|
])
|
|
486
475
|
}
|
|
487
476
|
|
|
488
|
-
/** `INTERSECT
|
|
477
|
+
/** `INTERSECT`: rows present in both. */
|
|
489
478
|
intersect<Q>(next: QBExecutable<Q>, all = false): QBSet<P> {
|
|
490
479
|
return new QBSet<P>([
|
|
491
480
|
{ op: null, query: this },
|
|
@@ -493,7 +482,7 @@ export namespace DB {
|
|
|
493
482
|
])
|
|
494
483
|
}
|
|
495
484
|
|
|
496
|
-
/** `EXCEPT
|
|
485
|
+
/** `EXCEPT`: rows in this query that are not in the next. */
|
|
497
486
|
except<Q>(next: QBExecutable<Q>, all = false): QBSet<P> {
|
|
498
487
|
return new QBSet<P>([
|
|
499
488
|
{ op: null, query: this },
|
|
@@ -501,6 +490,27 @@ export namespace DB {
|
|
|
501
490
|
])
|
|
502
491
|
}
|
|
503
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Walk the result a chunk at a time, holding only the chunk.
|
|
495
|
+
*
|
|
496
|
+
* **The name promises a cursor and the implementation is paging**, because
|
|
497
|
+
* Bun cannot stream: the statement becomes a derived table and is
|
|
498
|
+
* re-executed once per 500 rows. Two consequences, and the second is the
|
|
499
|
+
* one that surprises people.
|
|
500
|
+
*
|
|
501
|
+
* Chunk boundaries are only stable under a total order, so a row inserted
|
|
502
|
+
* or deleted mid-walk can be seen twice or missed. `seek()` is keyset
|
|
503
|
+
* pagination and has neither problem.
|
|
504
|
+
*
|
|
505
|
+
* And it is slow in proportion to how badly the ordering indexes.
|
|
506
|
+
* Measured on 100,000 rows: 3x `all()` on SQLite ordered by a primary key,
|
|
507
|
+
* **130x** ordered by a column with no index, 35.7 seconds against
|
|
508
|
+
* 275 ms. Postgres pays a round trip per chunk instead and lands at 19x
|
|
509
|
+
* and 101x. The table is on `pagedIterate` in `adapters/base.ts`.
|
|
510
|
+
*
|
|
511
|
+
* Worth it when the result does not fit in memory. Not worth it merely
|
|
512
|
+
* because the result is large.
|
|
513
|
+
*/
|
|
504
514
|
async *iterable(): AsyncIterable<P> {
|
|
505
515
|
const { sql, params } = this.parse()
|
|
506
516
|
for await (const row of getActiveDb()
|
|
@@ -626,7 +636,7 @@ export namespace DB {
|
|
|
626
636
|
>
|
|
627
637
|
|
|
628
638
|
/**
|
|
629
|
-
* `FULL OUTER JOIN`. Typed like the others, but **MySQL has none
|
|
639
|
+
* `FULL OUTER JOIN`. Typed like the others, but **MySQL has none**: the
|
|
630
640
|
* runtime refuses there, because a capability the compiler cannot see
|
|
631
641
|
* cannot be expressed in this signature.
|
|
632
642
|
*/
|
|
@@ -663,19 +673,19 @@ export namespace DB {
|
|
|
663
673
|
>
|
|
664
674
|
|
|
665
675
|
/**
|
|
666
|
-
* Ordering and paging are legal straight off the table
|
|
667
|
-
* .limit(10)` needs no where or select
|
|
676
|
+
* Ordering and paging are legal straight off the table: `DB.table('t')
|
|
677
|
+
* .limit(10)` needs no where or select, and always worked at runtime.
|
|
668
678
|
* They were simply missing from this stage of the interface chain.
|
|
669
679
|
*/
|
|
670
680
|
orderBy(
|
|
671
681
|
colStr: keyof P | ColumnString<S, J>,
|
|
672
682
|
direction?: 'ASC' | 'DESC',
|
|
673
683
|
): IQBOrderBy<S, J, P>
|
|
674
|
-
/** `SELECT DISTINCT
|
|
684
|
+
/** `SELECT DISTINCT`. See the runtime method for the semantics. */
|
|
675
685
|
distinct(): this
|
|
676
686
|
limit(count: number, offset?: number): IQBLimit<S, J, P>
|
|
677
687
|
paginate(page: number, pageSize: number): IQBLimit<S, J, P>
|
|
678
|
-
/** Cursor pagination
|
|
688
|
+
/** Cursor pagination. See the implementation for why it is not paginate(). */
|
|
679
689
|
seek(
|
|
680
690
|
column: ColumnString<S, J>,
|
|
681
691
|
cursor: unknown,
|
|
@@ -711,11 +721,11 @@ export namespace DB {
|
|
|
711
721
|
colStr: keyof P | ColumnString<S, J>,
|
|
712
722
|
direction?: 'ASC' | 'DESC',
|
|
713
723
|
): IQBOrderBy<S, J, P>
|
|
714
|
-
/** `SELECT DISTINCT
|
|
724
|
+
/** `SELECT DISTINCT`. See the runtime method for the semantics. */
|
|
715
725
|
distinct(): this
|
|
716
726
|
limit(count: number, offset?: number): IQBLimit<S, J, P>
|
|
717
727
|
paginate(page: number, pageSize: number): IQBLimit<S, J, P>
|
|
718
|
-
/** Cursor pagination
|
|
728
|
+
/** Cursor pagination. See the implementation for why it is not paginate(). */
|
|
719
729
|
seek(
|
|
720
730
|
column: ColumnString<S, J>,
|
|
721
731
|
cursor: unknown,
|
|
@@ -742,7 +752,7 @@ export namespace DB {
|
|
|
742
752
|
valueOrRef?: WhereValue<ColumnString<S, J>>,
|
|
743
753
|
): IQBHaving<S, J, P>
|
|
744
754
|
/**
|
|
745
|
-
* `SELECT DISTINCT
|
|
755
|
+
* `SELECT DISTINCT`. See the runtime method for the semantics.
|
|
746
756
|
*
|
|
747
757
|
* Declared here too, unlike the sibling stages, because this one has no
|
|
748
758
|
* `limit`: `distinct()` was added everywhere `limit` already appeared, and
|
|
@@ -766,11 +776,11 @@ export namespace DB {
|
|
|
766
776
|
colStr: keyof P | ColumnString<S, J>,
|
|
767
777
|
direction?: 'ASC' | 'DESC',
|
|
768
778
|
): IQBOrderBy<S, J, P>
|
|
769
|
-
/** `SELECT DISTINCT
|
|
779
|
+
/** `SELECT DISTINCT`. See the runtime method for the semantics. */
|
|
770
780
|
distinct(): this
|
|
771
781
|
limit(count: number, offset?: number): IQBLimit<S, J, P>
|
|
772
782
|
paginate(page: number, pageSize: number): IQBLimit<S, J, P>
|
|
773
|
-
/** Cursor pagination
|
|
783
|
+
/** Cursor pagination. See the implementation for why it is not paginate(). */
|
|
774
784
|
seek(
|
|
775
785
|
column: ColumnString<S, J>,
|
|
776
786
|
cursor: unknown,
|
|
@@ -782,8 +792,8 @@ export namespace DB {
|
|
|
782
792
|
export interface IQBSelect<S extends TableSchemas, J extends string, P = any>
|
|
783
793
|
extends QBObject<P> {
|
|
784
794
|
/**
|
|
785
|
-
* Grouping after selecting, which always worked at runtime
|
|
786
|
-
* assembled at `parse()`, so call order is irrelevant
|
|
795
|
+
* Grouping after selecting, which always worked at runtime (clauses are
|
|
796
|
+
* assembled at `parse()`, so call order is irrelevant), and was simply
|
|
787
797
|
* missing from this stage of the chain. The same gap `IQBTable` had for
|
|
788
798
|
* `orderBy`/`limit`, found by writing the query that motivates it:
|
|
789
799
|
* `.select({ n: DB.count.distinct(c) }).groupBy(x).having(…)`.
|
|
@@ -806,8 +816,8 @@ export namespace DB {
|
|
|
806
816
|
): IQBHaving<S, J, P>
|
|
807
817
|
|
|
808
818
|
/**
|
|
809
|
-
* Filtering after projection is ordinary builder usage
|
|
810
|
-
* assembled, not emitted in call order
|
|
819
|
+
* Filtering after projection is ordinary builder usage (the clauses are
|
|
820
|
+
* assembled, not emitted in call order), and worked at runtime already.
|
|
811
821
|
* The projection `P` is preserved, so the row type survives the call.
|
|
812
822
|
*/
|
|
813
823
|
where(
|
|
@@ -827,11 +837,11 @@ export namespace DB {
|
|
|
827
837
|
colStr: keyof P | ColumnString<S, J>,
|
|
828
838
|
direction?: 'ASC' | 'DESC',
|
|
829
839
|
): IQBOrderBy<S, J, P>
|
|
830
|
-
/** `SELECT DISTINCT
|
|
840
|
+
/** `SELECT DISTINCT`. See the runtime method for the semantics. */
|
|
831
841
|
distinct(): this
|
|
832
842
|
limit(count: number, offset?: number): IQBLimit<S, J, P>
|
|
833
843
|
paginate(page: number, pageSize: number): IQBLimit<S, J, P>
|
|
834
|
-
/** Cursor pagination
|
|
844
|
+
/** Cursor pagination. See the implementation for why it is not paginate(). */
|
|
835
845
|
seek(
|
|
836
846
|
column: ColumnString<S, J>,
|
|
837
847
|
cursor: unknown,
|
|
@@ -846,11 +856,11 @@ export namespace DB {
|
|
|
846
856
|
colStr: keyof P | ColumnString<S, J>,
|
|
847
857
|
direction?: 'ASC' | 'DESC',
|
|
848
858
|
): IQBOrderBy<S, J, P>
|
|
849
|
-
/** `SELECT DISTINCT
|
|
859
|
+
/** `SELECT DISTINCT`. See the runtime method for the semantics. */
|
|
850
860
|
distinct(): this
|
|
851
861
|
limit(count: number, offset?: number): IQBLimit<S, J, P>
|
|
852
862
|
paginate(page: number, pageSize: number): IQBLimit<S, J, P>
|
|
853
|
-
/** Cursor pagination
|
|
863
|
+
/** Cursor pagination. See the implementation for why it is not paginate(). */
|
|
854
864
|
seek(
|
|
855
865
|
column: ColumnString<S, J>,
|
|
856
866
|
cursor: unknown,
|
|
@@ -861,11 +871,11 @@ export namespace DB {
|
|
|
861
871
|
|
|
862
872
|
export interface IQBLimit<S extends TableSchemas, J extends string, P = any>
|
|
863
873
|
extends QBObject<P> {
|
|
864
|
-
/** `SELECT DISTINCT
|
|
874
|
+
/** `SELECT DISTINCT`. See the runtime method for the semantics. */
|
|
865
875
|
distinct(): this
|
|
866
876
|
limit(count: number, offset?: number): IQBLimit<S, J, P>
|
|
867
877
|
paginate(page: number, pageSize: number): IQBLimit<S, J, P>
|
|
868
|
-
/** Cursor pagination
|
|
878
|
+
/** Cursor pagination. See the implementation for why it is not paginate(). */
|
|
869
879
|
seek(
|
|
870
880
|
column: ColumnString<S, J>,
|
|
871
881
|
cursor: unknown,
|
|
@@ -885,23 +895,23 @@ export namespace DB {
|
|
|
885
895
|
/**
|
|
886
896
|
* A compound select: two or more queries joined by `UNION` and friends.
|
|
887
897
|
*
|
|
888
|
-
* The emission rules below are not style choices
|
|
898
|
+
* The emission rules below are not style choices: each one is the only form
|
|
889
899
|
* all three dialects accept, measured against live servers rather than read
|
|
890
900
|
* off a standard:
|
|
891
901
|
*
|
|
892
|
-
* - **Operands are bare, never
|
|
893
|
-
* `(SELECT …) UNION (SELECT …)`; **SQLite rejects it outright
|
|
894
|
-
*
|
|
902
|
+
* - **Operands are bare, never parenthesized.** MySQL and Postgres take
|
|
903
|
+
* `(SELECT …) UNION (SELECT …)`; **SQLite rejects it outright**: a
|
|
904
|
+
* parenthesized select is not a legal operand of a compound there.
|
|
895
905
|
* - **A branch carrying its own `ORDER BY`/`LIMIT` is wrapped as a derived
|
|
896
906
|
* table** instead. `SELECT id FROM a LIMIT 2 UNION …` is a syntax error on
|
|
897
|
-
* all three, the
|
|
907
|
+
* all three, the parenthesized fix works on two of them, and
|
|
898
908
|
* `SELECT * FROM (SELECT id FROM a LIMIT 2) AS b0` works on all three. The
|
|
899
909
|
* alias is required by MySQL and harmless elsewhere.
|
|
900
910
|
* - **`ORDER BY` and `LIMIT` on the set go at the very end**, unwrapped,
|
|
901
911
|
* where every dialect reads them as applying to the whole compound.
|
|
902
912
|
* - **`INTERSECT ALL` / `EXCEPT ALL` are gated.** MySQL 8.0.31+ and Postgres
|
|
903
|
-
* have them; SQLite does not, and its message
|
|
904
|
-
*
|
|
913
|
+
* have them; SQLite does not, and its message (`near "ALL": syntax error`
|
|
914
|
+
* ) does not say which construct it means.
|
|
905
915
|
*/
|
|
906
916
|
export class QBSet<P = any> extends QBExecutable<P> {
|
|
907
917
|
private _orderBy: string[] = []
|
|
@@ -980,7 +990,7 @@ export namespace DB {
|
|
|
980
990
|
) {
|
|
981
991
|
throws(
|
|
982
992
|
`${op} is not supported by this database. ` +
|
|
983
|
-
`Use ${op.replace(' ALL', '')} instead
|
|
993
|
+
`Use ${op.replace(' ALL', '')} instead: it removes duplicates.`,
|
|
984
994
|
)
|
|
985
995
|
}
|
|
986
996
|
parts.push(op)
|
|
@@ -1053,8 +1063,8 @@ export namespace DB {
|
|
|
1053
1063
|
}
|
|
1054
1064
|
|
|
1055
1065
|
/**
|
|
1056
|
-
* Emit one WHERE/HAVING clause. Captures nothing from the builder
|
|
1057
|
-
* `params` as an argument
|
|
1066
|
+
* Emit one WHERE/HAVING clause. Captures nothing from the builder (it takes
|
|
1067
|
+
* `params` as an argument), so it lives here rather than being re-created as
|
|
1058
1068
|
* a closure on every `parse()`, which is once per executed query.
|
|
1059
1069
|
*/
|
|
1060
1070
|
function formatClause(
|
|
@@ -1072,16 +1082,9 @@ export namespace DB {
|
|
|
1072
1082
|
if (op === 'IS NULL' || op === 'IS NOT NULL') {
|
|
1073
1083
|
return `${left} ${op}`
|
|
1074
1084
|
}
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
// passes an explicit null means the SQL spelling of it. Only literal
|
|
1079
|
-
// values: a column reference on the right is a different comparison and
|
|
1080
|
-
// is left alone.
|
|
1081
|
-
if (rightArg === null && !isRightColumn) {
|
|
1082
|
-
if (op === '=') return `${left} IS NULL`
|
|
1083
|
-
if (op === '!=' || op === '<>') return `${left} IS NOT NULL`
|
|
1084
|
-
}
|
|
1085
|
+
// One rule, three callers. See `nullComparison`.
|
|
1086
|
+
const nullOp = nullComparison(op, rightArg, isRightColumn)
|
|
1087
|
+
if (nullOp) return `${left} ${nullOp}`
|
|
1085
1088
|
if (op === 'BETWEEN' && Array.isArray(rightArg)) {
|
|
1086
1089
|
const min = evalOperands(rightArg[0], params, false)
|
|
1087
1090
|
const max = evalOperands(rightArg[1], params, false)
|
|
@@ -1215,14 +1218,14 @@ export namespace DB {
|
|
|
1215
1218
|
|
|
1216
1219
|
/**
|
|
1217
1220
|
* Both columns, the joined table and the alias go through the convention-8
|
|
1218
|
-
* guards here, at the call site
|
|
1221
|
+
* guards here, at the call site: the same thing `orderBy` and `groupBy`
|
|
1219
1222
|
* do, and for the same reason: the `ColumnString` union is compile-time
|
|
1220
1223
|
* only, so a value taken off a request reaches this method as a plain
|
|
1221
1224
|
* string.
|
|
1222
1225
|
*
|
|
1223
1226
|
* It previously concatenated the raw arguments into an ON clause and left
|
|
1224
1227
|
* `parse()` to run a `word.word` regex over the result, quoting what
|
|
1225
|
-
* matched and passing everything else through untouched
|
|
1228
|
+
* matched and passing everything else through untouched, so
|
|
1226
1229
|
* `join("users.id = 1 OR 1=1 UNION SELECT password FROM secrets --", …)`
|
|
1227
1230
|
* was emitted verbatim.
|
|
1228
1231
|
*/
|
|
@@ -1249,8 +1252,8 @@ export namespace DB {
|
|
|
1249
1252
|
}
|
|
1250
1253
|
|
|
1251
1254
|
// Always qualified, never the raw argument. An undotted right column
|
|
1252
|
-
// means "that table's `id`"
|
|
1253
|
-
// emitted
|
|
1255
|
+
// means "that table's `id`" (which is what the aliased form already
|
|
1256
|
+
// emitted), but the unaliased path passed `strRight` straight through, so
|
|
1254
1257
|
// `join('teachers.campusId', 'campuses')` produced
|
|
1255
1258
|
// `ON "teachers"."campus_id" = campuses`: a bare table name in a value
|
|
1256
1259
|
// position, invalid on every dialect and only discovered at execution.
|
|
@@ -1268,8 +1271,8 @@ export namespace DB {
|
|
|
1268
1271
|
throws(`Invalid join type: ${type}`)
|
|
1269
1272
|
}
|
|
1270
1273
|
// Refused at the call site rather than at the server, because MySQL's
|
|
1271
|
-
// message for it
|
|
1272
|
-
// whole statement
|
|
1274
|
+
// message for it ("You have an error in your SQL syntax" pointing at the
|
|
1275
|
+
// whole statement) says nothing about which construct is unsupported.
|
|
1273
1276
|
if (joinType === 'FULL' && !getActiveDb()?.supportsFullOuterJoin) {
|
|
1274
1277
|
throws(
|
|
1275
1278
|
'FULL OUTER JOIN is not supported by this database (MySQL has no ' +
|
|
@@ -1300,7 +1303,7 @@ export namespace DB {
|
|
|
1300
1303
|
}
|
|
1301
1304
|
|
|
1302
1305
|
/**
|
|
1303
|
-
* `FULL OUTER JOIN
|
|
1306
|
+
* `FULL OUTER JOIN`: every row from both sides, matched where possible.
|
|
1304
1307
|
*
|
|
1305
1308
|
* SQLite (3.39+) and Postgres have it; **MySQL does not**, at any version,
|
|
1306
1309
|
* so this throws there rather than emitting SQL the server will reject.
|
|
@@ -1390,7 +1393,7 @@ export namespace DB {
|
|
|
1390
1393
|
}
|
|
1391
1394
|
|
|
1392
1395
|
/**
|
|
1393
|
-
* `SELECT DISTINCT`. Applies to the whole select list, not one column
|
|
1396
|
+
* `SELECT DISTINCT`. Applies to the whole select list, not one column:
|
|
1394
1397
|
* SQL has no per-column distinct, and offering one would imply otherwise.
|
|
1395
1398
|
*
|
|
1396
1399
|
* Idempotent, so `.distinct().distinct()` is one keyword rather than two.
|
|
@@ -1416,7 +1419,7 @@ export namespace DB {
|
|
|
1416
1419
|
}
|
|
1417
1420
|
|
|
1418
1421
|
/**
|
|
1419
|
-
* Cursor (keyset) pagination
|
|
1422
|
+
* Cursor (keyset) pagination: the next `pageSize` rows *after* `cursor`.
|
|
1420
1423
|
*
|
|
1421
1424
|
* const first = await DB.from('posts').seek('id', null, 20).all()
|
|
1422
1425
|
* const next = await DB.from('posts')
|
|
@@ -1424,15 +1427,15 @@ export namespace DB {
|
|
|
1424
1427
|
*
|
|
1425
1428
|
* `paginate()` is offset-based, and an offset is not free: `LIMIT 20 OFFSET
|
|
1426
1429
|
* 200000` makes the server walk and discard 200,000 rows, so page 10,000
|
|
1427
|
-
* costs far more than page 1. This walks nothing
|
|
1428
|
-
* the index
|
|
1430
|
+
* costs far more than page 1. This walks nothing (it seeks straight into
|
|
1431
|
+
* the index), so every page costs the same.
|
|
1429
1432
|
*
|
|
1430
1433
|
* It also does not skip or repeat rows when the table is written to
|
|
1431
1434
|
* mid-scan, which offset paging does by construction: delete one row on
|
|
1432
1435
|
* page 1 and every later page shifts by one.
|
|
1433
1436
|
*
|
|
1434
|
-
* The trade is that pages are only reachable in order
|
|
1435
|
-
* to page 500"
|
|
1437
|
+
* The trade is that pages are only reachable in order (there is no "jump
|
|
1438
|
+
* to page 500"), and the column must be **unique and ordered**, which in
|
|
1436
1439
|
* practice means a primary key or something monotonic. A non-unique cursor
|
|
1437
1440
|
* column silently drops the rows that tie on the boundary value, which is
|
|
1438
1441
|
* why this takes one column rather than pretending to sort by several.
|
|
@@ -1455,14 +1458,14 @@ export namespace DB {
|
|
|
1455
1458
|
if (cursor !== null && cursor !== undefined) {
|
|
1456
1459
|
// `gt`/`lt`, not a `'id >'` string: the operator belongs in an operand
|
|
1457
1460
|
// ref, and folding it into the column name puts `id >` through
|
|
1458
|
-
// `safeColumn`, which rejects it
|
|
1461
|
+
// `safeColumn`, which rejects it. Correctly, since that is the guard
|
|
1459
1462
|
// stopping an operator from being smuggled into an identifier.
|
|
1460
1463
|
this._where.push({
|
|
1461
1464
|
connector: this._where.length === 0 ? 'WHERE' : 'AND',
|
|
1462
1465
|
...parseWhereArgs(column, dir === 'ASC' ? gt(cursor) : lt(cursor)),
|
|
1463
1466
|
})
|
|
1464
1467
|
}
|
|
1465
|
-
// Ordering is not optional here the way it is for `paginate
|
|
1468
|
+
// Ordering is not optional here the way it is for `paginate`: a cursor
|
|
1466
1469
|
// is meaningless without the order it is a position in. Prepended so an
|
|
1467
1470
|
// explicit `.orderBy()` still breaks ties after it.
|
|
1468
1471
|
this._orderBy.unshift(`${safeColumn(column)} ${dir}`)
|
|
@@ -1472,7 +1475,7 @@ export namespace DB {
|
|
|
1472
1475
|
// The numbered sections below are the statement grammar. Parameter push order
|
|
1473
1476
|
// is load-bearing (Postgres renumbers `?` to `$n` left to right), so the
|
|
1474
1477
|
// sequence is the correctness condition, not incidental.
|
|
1475
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: SQL assembler
|
|
1478
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: SQL assembler, one section per clause, in emission order
|
|
1476
1479
|
parse(): { sql: string; params: any[] } {
|
|
1477
1480
|
const params: any[] = []
|
|
1478
1481
|
|
|
@@ -1497,7 +1500,7 @@ export namespace DB {
|
|
|
1497
1500
|
if (Object.keys(this._select).length > 0) {
|
|
1498
1501
|
for (const [alias, colRef] of Object.entries(this._select)) {
|
|
1499
1502
|
if (colRef instanceof WindowRef || colRef instanceof SQLFunctionRef) {
|
|
1500
|
-
// `evalOperands` is the single writer for a function call
|
|
1503
|
+
// `evalOperands` is the single writer for a function call: the
|
|
1501
1504
|
// same one WHERE and HAVING go through, allow-list included.
|
|
1502
1505
|
//
|
|
1503
1506
|
// This used to re-implement it, and the copies had drifted: the
|
package/src/pool.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Nothing exposed these before: every deployment ran on Bun's defaults, which
|
|
5
5
|
* is fine until an app with more workers than the server has connection slots
|
|
6
6
|
* meets `FATAL: sorry, too many clients already`. `--threads N` makes that a
|
|
7
|
-
* realistic shape rather than a theoretical one
|
|
7
|
+
* realistic shape rather than a theoretical one: each worker opens its own
|
|
8
8
|
* pool, so the number that matters is `max x threads`.
|
|
9
9
|
*
|
|
10
10
|
* **SQLite ignores all of it**, and that is not an omission: a SQLite adapter
|
|
@@ -39,14 +39,14 @@ const ENV_KEYS: Record<keyof PoolOptions, string> = {
|
|
|
39
39
|
/**
|
|
40
40
|
* Read pool options from the environment, dropping anything unusable.
|
|
41
41
|
*
|
|
42
|
-
* Unset stays unset
|
|
42
|
+
* Unset stays unset: an option Bakery does not pass is one Bun defaults,
|
|
43
43
|
* which is different from passing Bun a zero. A non-numeric or negative value
|
|
44
44
|
* is dropped for the same reason: `DB_POOL_MAX=lots` must not become
|
|
45
45
|
* `max: NaN`, which Bun would take and then behave unpredictably around.
|
|
46
46
|
*
|
|
47
47
|
* **Only known keys are ever forwarded**, and that matters more than it looks:
|
|
48
|
-
* Bun accepts an
|
|
49
|
-
* totallyNotAnOption: 1 })` constructs and queries fine
|
|
48
|
+
* Bun accepts an unrecognized option silently (verified, `new SQL(url, {
|
|
49
|
+
* totallyNotAnOption: 1 })` constructs and queries fine), so a typo'd key
|
|
50
50
|
* would configure nothing and report nothing. Passing a fixed set means the
|
|
51
51
|
* typo lands in an env var name, where it is at least visible in one place.
|
|
52
52
|
*/
|
|
@@ -70,7 +70,7 @@ export function poolOptionsFromEnv(
|
|
|
70
70
|
/**
|
|
71
71
|
* Merge pool options into the object handed to `new SQL()`.
|
|
72
72
|
*
|
|
73
|
-
* The timeouts are **seconds here and milliseconds inside Bun
|
|
73
|
+
* The timeouts are **seconds here and milliseconds inside Bun**: it multiplies
|
|
74
74
|
* by 1000 on the way in, verified by reading `sql.options` back: `idleTimeout:
|
|
75
75
|
* 5` is stored as `5000`. Seconds is what Bun's own documented unit is, so this
|
|
76
76
|
* passes them straight through rather than converting and doubling the factor.
|
package/src/schema-registry.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Type-level extension point connecting the framework to the app's schema.
|
|
3
3
|
*
|
|
4
4
|
* The dependency must point one way: the app depends on the framework, never
|
|
5
|
-
* the reverse. Yet the ORM's types are derived from the app's `schema.ts
|
|
5
|
+
* the reverse. Yet the ORM's types are derived from the app's `schema.ts`:
|
|
6
6
|
* previously via a direct `import ... from '~/schema'`, which meant the
|
|
7
7
|
* framework's own types imported an app-owned, gitignored file, and a fresh
|
|
8
8
|
* clone could not typecheck its database layer.
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* ```
|
|
25
25
|
*
|
|
26
26
|
* With no registration, every table and column falls back to permissive
|
|
27
|
-
* `any`-shaped records
|
|
27
|
+
* `any`-shaped records. The ORM stays fully usable, just untyped. The
|
|
28
28
|
* runtime needs no registration at all: schema *values* are loaded by path
|
|
29
29
|
* (see `sync/load.ts`), from `schema` in `server.config.ts` when the app sets
|
|
30
30
|
* one and otherwise from `<cwd>/orm/index.ts` or `<cwd>/schema.ts`, whichever
|
|
@@ -45,7 +45,7 @@ import type { MapOf } from '@bakery-framework/core/types'
|
|
|
45
45
|
// fix. That rule is off repo-wide now, but `noEmptyInterface` reaches the same
|
|
46
46
|
// construct and offers the same fix, so the suppression stays. The compiler is
|
|
47
47
|
// the real guard: change this line and `apps/starter` fails to typecheck.
|
|
48
|
-
// biome-ignore lint/suspicious/noEmptyInterface: must stay mergeable
|
|
48
|
+
// biome-ignore lint/suspicious/noEmptyInterface: must stay mergeable, above
|
|
49
49
|
export interface SchemaRegistry {}
|
|
50
50
|
|
|
51
51
|
type Registered = SchemaRegistry extends { schema: infer S } ? S : never
|