arn-db 0.1.0 → 0.2.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/GUIDE.md +102 -5
- package/README.md +54 -4
- package/dist/index.cjs +126 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -13
- package/dist/index.d.mts +114 -13
- package/dist/index.mjs +125 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/GUIDE.md
CHANGED
|
@@ -30,6 +30,7 @@ Contents:
|
|
|
30
30
|
[Writing related rows](#writing-related-rows) ·
|
|
31
31
|
[Transactions](#transactions) ·
|
|
32
32
|
[Read replicas](#read-replicas) ·
|
|
33
|
+
[Raw SQL fragments](#raw-sql-fragments) ·
|
|
33
34
|
[Errors](#errors) ·
|
|
34
35
|
[Not supported yet](#not-supported-yet)
|
|
35
36
|
|
|
@@ -483,7 +484,8 @@ await db.from('posts').select('user_id').distinct() // SELECT DISTINCT
|
|
|
483
484
|
```
|
|
484
485
|
|
|
485
486
|
Aggregates, `groupBy()` and `distinct()` don't combine with related tables in the
|
|
486
|
-
same select.
|
|
487
|
+
same select. For `HAVING`, use `.having(db.raw('count(*) >= ?', [2]))` (see
|
|
488
|
+
[Raw SQL fragments](#raw-sql-fragments)).
|
|
487
489
|
|
|
488
490
|
---
|
|
489
491
|
|
|
@@ -700,6 +702,98 @@ await db.from('users').select().eq('email', email).primary()
|
|
|
700
702
|
|
|
701
703
|
---
|
|
702
704
|
|
|
705
|
+
## Raw SQL fragments
|
|
706
|
+
|
|
707
|
+
When the builder has no method for it, `db.raw(sql, bindings?)` adds one SQL
|
|
708
|
+
expression to the query (same as `import { raw }`, and `tx.raw` in
|
|
709
|
+
transactions). It is knex's `whereRaw` / `orderByRaw` / `havingRaw` /
|
|
710
|
+
`knex.raw`: a piece of a query, never a whole statement. `test/raw.test.ts`
|
|
711
|
+
checks these against the test database.
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
const n = db.raw('substring(name from 6)::int') // user_42 -> 42
|
|
715
|
+
|
|
716
|
+
// where(): AND (<fragment>)
|
|
717
|
+
await db.from('users').select('name').where(db.raw('substring(name from 6)::int <= ?', [3])).order('name')
|
|
718
|
+
// [{ name: 'user_1' }, { name: 'user_2' }, { name: 'user_3' }]
|
|
719
|
+
|
|
720
|
+
// inside or() / and() / not() it is one condition of the group
|
|
721
|
+
await db.from('users').select('name')
|
|
722
|
+
.or((q) => q.where(db.raw('substring(name from 6)::int = ?', [1])).eq('name', 'user_2'))
|
|
723
|
+
// user_1, user_2
|
|
724
|
+
|
|
725
|
+
// array binding: = any(?)
|
|
726
|
+
await db.from('users').select('name').where(db.raw('lower(trim(email)) = any(?)', [['user_1@example.com', 'user_7@example.com']]))
|
|
727
|
+
|
|
728
|
+
// jsonb ? operator: \? (written '\\?' in JS) is a literal ?
|
|
729
|
+
await db.from('users').where(db.raw('meta \\? ?', ['vip'])).count() // 100
|
|
730
|
+
|
|
731
|
+
// order()
|
|
732
|
+
await db.from('users').select('name').order(n, { ascending: false }).limit(3)
|
|
733
|
+
// user_100, user_99, user_98
|
|
734
|
+
|
|
735
|
+
// select(): extra arguments are raw columns, each needs .as(name); options go last
|
|
736
|
+
await db.from('users').select('name', db.raw('char_length(name)').as('name_len')).eq('name', 'user_42').single()
|
|
737
|
+
// { name: 'user_42', name_len: 7 }
|
|
738
|
+
|
|
739
|
+
// having(), with groupBy()
|
|
740
|
+
await db.from('posts').select('user_id, n:count()').groupBy('user_id').having(db.raw('count(*) >= ?', [2]))
|
|
741
|
+
// 33 groups, each n: 2
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
As values in `insert()` / `update()` / `upsert()` rows, and in upsert `merge`,
|
|
745
|
+
where `excluded.col` is the incoming value and `table.col` the stored one:
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
await db.from('users').update({ age: db.raw('age + ?', [5]) }).eq('email', email).returning('age') // 10 -> [{ age: 15 }]
|
|
749
|
+
|
|
750
|
+
await db.from('users').upsert({ email, name: 'second', age: 5 }, {
|
|
751
|
+
onConflict: 'email',
|
|
752
|
+
merge: { age: db.raw('CASE WHEN excluded.age IS NOT NULL AND users.age IS NOT NULL THEN users.age + excluded.age ELSE ? END', [100]) },
|
|
753
|
+
})
|
|
754
|
+
// stored age 10 -> 15; name stays (only merge columns change); with age: null -> 100
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Rules:
|
|
758
|
+
|
|
759
|
+
- Each `?` is one binding, passed as a query parameter, so values are never
|
|
760
|
+
pasted into the SQL. The count must match `bindings`, else a 400 error
|
|
761
|
+
(`raw fragment: 2 placeholders but 1 binding`). Strings, numbers, booleans and
|
|
762
|
+
`null` pass through; an array becomes a Postgres array (cast it if needed:
|
|
763
|
+
`?::text[]`); an object becomes jsonb (`?::jsonb`). A `?` inside `'quoted
|
|
764
|
+
text'` is not a binding.
|
|
765
|
+
- `\?` (in a JS string: `'\\?'`) is a literal `?`, for the jsonb `?` operator.
|
|
766
|
+
- Fragments see the main table only: its columns, plain or qualified with the
|
|
767
|
+
table's name (`users.age`, `"users".age`), and `excluded.col` in `merge`.
|
|
768
|
+
Related tables are not visible, and `order(raw)` with `referencedTable` is an error.
|
|
769
|
+
- The API key must be allowed to use raw fragments. Otherwise every request
|
|
770
|
+
(or transaction) that contains one gets 403, code `RAW_NOT_ALLOWED`.
|
|
771
|
+
- The server rejects with 400, code `RAW_FRAGMENT`: `;`, `--` and `/*`
|
|
772
|
+
comments, dollar quoting (`$$`), the keywords `select`, `insert`, `update`,
|
|
773
|
+
`delete`, `with`, `union`, `into`, `copy`, `grant`, `drop`, `alter`,
|
|
774
|
+
`create`, `truncate`, `execute`, `do`, `call` outside quoted text, an empty
|
|
775
|
+
fragment or one over 2,000 characters, and more than 100 bindings.
|
|
776
|
+
Functions, `CASE WHEN`, casts, `interval '1 hour'`, operators, `count(*)`
|
|
777
|
+
and `excluded.x` are fine.
|
|
778
|
+
- In the client, `raw()` throws when `sql` is not a non-empty string or
|
|
779
|
+
`bindings` not an array. `select()` with a raw column lacking `.as()`, and
|
|
780
|
+
`where()` / `having()` with something other than `raw()`, resolve to `{ error }`
|
|
781
|
+
without sending.
|
|
782
|
+
|
|
783
|
+
Moving from knex:
|
|
784
|
+
|
|
785
|
+
| knex | arn-db |
|
|
786
|
+
|---|---|
|
|
787
|
+
| `.whereRaw(sql, b)`, `.andWhereRaw(sql, b)` | `.where(db.raw(sql, b))` |
|
|
788
|
+
| `.orWhereRaw(sql, b)` | `.or((q) => q.eq(...).where(db.raw(sql, b)))` |
|
|
789
|
+
| `.orderByRaw(sql, b)` | `.order(db.raw(sql, b), { ascending, nullsFirst })` |
|
|
790
|
+
| `.select('id', knex.raw('char_length(title) as n'))` | `.select('id', db.raw('char_length(title)').as('n'))` |
|
|
791
|
+
| `{ total: knex.raw('total + ?', [1]) }` | `{ total: db.raw('total + ?', [1]) }` |
|
|
792
|
+
| `.onConflict('email').merge({ total: knex.raw('…') })` | `.upsert(rows, { onConflict: 'email', merge: { total: db.raw('…') } })` |
|
|
793
|
+
| `.havingRaw(sql, b)` | `.having(db.raw(sql, b))` |
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
703
797
|
## Errors
|
|
704
798
|
|
|
705
799
|
`error` has `message`, and usually `code` and `details`:
|
|
@@ -712,11 +806,13 @@ await db.from('users').select().eq('email', email).primary()
|
|
|
712
806
|
| `22P02` | value has the wrong format | `.eq('id', 'abc')` on a uuid |
|
|
713
807
|
| `42703` | column doesn't exist | `select('nope')` → `column _t0.nope does not exist` (`_t0` is the main table) |
|
|
714
808
|
| `42P10` | `onConflict` columns have no unique constraint | `upsert(…, { onConflict: 'title' })` |
|
|
809
|
+
| `RAW_FRAGMENT` | (400) a `db.raw()` fragment the server refuses | `db.raw('1; drop table x')` |
|
|
810
|
+
| `RAW_NOT_ALLOWED` | (403) the API key may not use raw fragments | |
|
|
715
811
|
| `401` | missing or wrong API key | |
|
|
716
812
|
| `403` | request came from a browser | |
|
|
717
813
|
|
|
718
|
-
Only setup mistakes throw: `createClient()` without a key,
|
|
719
|
-
query when `ARN_DB_URL` or `ARN_DB_KEY` is missing.
|
|
814
|
+
Only setup mistakes throw: `createClient()` without a key, `db.raw()` without a
|
|
815
|
+
SQL string, and `db`'s first query when `ARN_DB_URL` or `ARN_DB_KEY` is missing.
|
|
720
816
|
|
|
721
817
|
Errors without a code are checks done before the database runs anything, for
|
|
722
818
|
example: `more than one relationship between "posts" and "users", use one of: users!reviewer_id, users!user_id`.
|
|
@@ -727,6 +823,7 @@ example: `more than one relationship between "posts" and "users", use one of: us
|
|
|
727
823
|
|
|
728
824
|
- Sorting main rows by a related table's column (e.g. posts by author name). Sorting *inside* a relation works.
|
|
729
825
|
- OR across tables in one group (e.g. `name = 'x' OR posts.published`): each `or()` group is on one table.
|
|
730
|
-
- Full-text search
|
|
731
|
-
functions like `lower(trim(email))
|
|
826
|
+
- Full-text search and calling Postgres functions (`rpc`) as methods. Column
|
|
827
|
+
functions like `lower(trim(email))` and `having` work through
|
|
828
|
+
[raw fragments](#raw-sql-fragments).
|
|
732
829
|
- `db.col()` across tables, and as a value in `insert()` / `update()`.
|
package/README.md
CHANGED
|
@@ -41,8 +41,8 @@ TypeScript types are included.
|
|
|
41
41
|
## Quick tour
|
|
42
42
|
|
|
43
43
|
Import the ready `db` everywhere: `db.from()`, `db.transaction()`, `db.ops.*`,
|
|
44
|
-
`db.rel
|
|
45
|
-
`tx.rel.*`, `tx.ref()`). For another key or options, make your own client with
|
|
44
|
+
`db.rel.*`, `db.raw()`. Inside transactions the same helpers are on `tx` (`tx.ops.*`,
|
|
45
|
+
`tx.rel.*`, `tx.raw()`, `tx.ref()`). For another key or options, make your own client with
|
|
46
46
|
`createClient(url, key, options)`.
|
|
47
47
|
|
|
48
48
|
```ts
|
|
@@ -90,8 +90,8 @@ await db.from('users').update({ meta: db.ops.append('x').at('address', 'lines')
|
|
|
90
90
|
|
|
91
91
|
Errors never throw: every call (and every transaction) resolves to
|
|
92
92
|
`{ data, error, count, status }`, like Supabase. Only setup mistakes throw:
|
|
93
|
-
`createClient()` without an API key or fetch,
|
|
94
|
-
`ARN_DB_URL` / `ARN_DB_KEY` is missing.
|
|
93
|
+
`createClient()` without an API key or fetch, `db.raw()` without a SQL string,
|
|
94
|
+
and `db`'s first query when `ARN_DB_URL` / `ARN_DB_KEY` is missing.
|
|
95
95
|
|
|
96
96
|
## Searching arrays (jsonb arrays and Postgres arrays)
|
|
97
97
|
|
|
@@ -214,3 +214,53 @@ await db.transaction((tx) => [
|
|
|
214
214
|
returned an array, pick one: `tx.ref('posts.0.id')`.
|
|
215
215
|
- Step names: words without dots. Number-like keys (`'1'`) are rejected because
|
|
216
216
|
JavaScript would reorder them.
|
|
217
|
+
|
|
218
|
+
## Raw SQL fragments
|
|
219
|
+
|
|
220
|
+
For the bits the builder has no method for, `db.raw(sql, bindings?)` (also
|
|
221
|
+
`import { raw }` and `tx.raw`) adds one SQL expression to the query the builder
|
|
222
|
+
makes, like knex's `whereRaw` / `orderByRaw` / `knex.raw`. It is a fragment,
|
|
223
|
+
never a whole statement.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// filters: AND (<fragment>), or one member of an or() group
|
|
227
|
+
await db.from('users').select('id').where(db.raw('lower(trim(email)) = any(?)', [emails]))
|
|
228
|
+
await db.from('accounts').select().or((q) => q.where(db.raw('sent_per_hour < limit_per_hour')).is('limit_per_hour', null))
|
|
229
|
+
|
|
230
|
+
// order, computed select columns (.as() is required), having
|
|
231
|
+
await db.from('accounts').select().order(db.raw("coalesce(last_read_time, '1970-01-01')"), { ascending: true })
|
|
232
|
+
await db.from('posts').select('id, title', db.raw('char_length(title)').as('title_len'))
|
|
233
|
+
await db.from('posts').select('user_id, n:count()').groupBy('user_id').having(db.raw('count(*) >= ?', [2]))
|
|
234
|
+
|
|
235
|
+
// values in insert / update / upsert rows, and in upsert merge (excluded.col = incoming value)
|
|
236
|
+
await db.from('accounts').update({ sent_total: db.raw('sent_total + ?', [1]) }).eq('id', id)
|
|
237
|
+
await db.from('accounts').upsert(rows, {
|
|
238
|
+
onConflict: 'email',
|
|
239
|
+
merge: { total: db.raw('CASE WHEN excluded.total IS NULL THEN accounts.total ELSE excluded.total END') },
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- Each `?` is a binding, sent as a query parameter (never pasted into the SQL).
|
|
244
|
+
Their number must match `bindings`. An array binding is a Postgres array
|
|
245
|
+
(`= any(?)`), an object is jsonb (`?::jsonb`). `?` inside `'quoted text'` is
|
|
246
|
+
not a binding.
|
|
247
|
+
- `\?` (in a JS string: `'\\?'`) is a literal `?`, for the jsonb `?` operator:
|
|
248
|
+
`db.raw('meta \\? ?', ['vip'])`.
|
|
249
|
+
- A fragment sees the main table's columns, plain or qualified with the table
|
|
250
|
+
name (`accounts.total`); not related tables.
|
|
251
|
+
- The API key must be allowed to use raw fragments; otherwise the request gets
|
|
252
|
+
403 `RAW_NOT_ALLOWED`.
|
|
253
|
+
- The server rejects (400 `RAW_FRAGMENT`): `;`, `--` and `/* */` comments,
|
|
254
|
+
dollar quoting, statement keywords (`select`, `insert`, `update`, `delete`,
|
|
255
|
+
`with`, `union`, `into`, `drop`, `alter`, `create`, `truncate`, …) outside
|
|
256
|
+
quoted text, fragments over 2,000 characters and more than 100 bindings.
|
|
257
|
+
|
|
258
|
+
| knex | arn-db |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `whereRaw(sql, b)` / `andWhereRaw(sql, b)` | `.where(db.raw(sql, b))` |
|
|
261
|
+
| `orWhereRaw(sql, b)` | `.or((q) => q.where(db.raw(sql, b)).eq(...))` |
|
|
262
|
+
| `orderByRaw(sql, b)` | `.order(db.raw(sql, b), { ascending })` |
|
|
263
|
+
| `select('id', knex.raw('… as x'))` | `.select('id', db.raw('…').as('x'))` |
|
|
264
|
+
| `{ col: knex.raw('col + ?', [1]) }` value | `{ col: db.raw('col + ?', [1]) }` |
|
|
265
|
+
| `merge({ col: knex.raw(...) })` | `upsert(rows, { merge: { col: db.raw(...) } })` |
|
|
266
|
+
| `havingRaw(sql, b)` | `.having(db.raw(sql, b))` |
|
package/dist/index.cjs
CHANGED
|
@@ -152,10 +152,66 @@ var Now = class Now {
|
|
|
152
152
|
};
|
|
153
153
|
const col = (name) => new Col(name);
|
|
154
154
|
const now = () => new Now();
|
|
155
|
+
/**
|
|
156
|
+
* A raw SQL fragment: one expression inside the query the builder makes, with
|
|
157
|
+
* `?` for each binding (`\\?` is a literal `?`, e.g. the jsonb `?` operator).
|
|
158
|
+
* Bindings are sent as query parameters, never pasted into the SQL. Use it in
|
|
159
|
+
* `where()`, `order()`, `having()`, as a `select()` column with `.as()`, and as
|
|
160
|
+
* an insert/update/upsert value or upsert `merge` value. The API key must be
|
|
161
|
+
* allowed to use raw fragments.
|
|
162
|
+
*
|
|
163
|
+
* .where(db.raw('lower(email) = any(?)', [emails]))
|
|
164
|
+
* .update({ sent_total: db.raw('sent_total + ?', [1]) })
|
|
165
|
+
*/
|
|
166
|
+
var Raw = class Raw {
|
|
167
|
+
sql;
|
|
168
|
+
bindings;
|
|
169
|
+
#as;
|
|
170
|
+
constructor(sql, bindings = [], as) {
|
|
171
|
+
if (typeof sql !== "string" || sql.trim() === "") throw new Error("arn-db: raw(sql) needs a non-empty SQL string");
|
|
172
|
+
if (!Array.isArray(bindings)) throw new Error("arn-db: raw(sql, bindings) takes the bindings as an array");
|
|
173
|
+
this.sql = sql;
|
|
174
|
+
this.bindings = bindings;
|
|
175
|
+
this.#as = as;
|
|
176
|
+
}
|
|
177
|
+
/** Name of this fragment as a `select()` column: `raw('char_length(title)').as('title_len')`. */
|
|
178
|
+
as(name) {
|
|
179
|
+
return new Raw(this.sql, this.bindings, name);
|
|
180
|
+
}
|
|
181
|
+
/** @internal The `.as()` name, if any. */
|
|
182
|
+
alias() {
|
|
183
|
+
return this.#as;
|
|
184
|
+
}
|
|
185
|
+
/** @internal The fragment without its name, for filters, order and having. */
|
|
186
|
+
fragment() {
|
|
187
|
+
return {
|
|
188
|
+
$raw: this.sql,
|
|
189
|
+
bindings: this.bindings
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** @internal Wire format. */
|
|
193
|
+
toJSON() {
|
|
194
|
+
return this.#as === void 0 ? this.fragment() : {
|
|
195
|
+
...this.fragment(),
|
|
196
|
+
as: this.#as
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* A raw SQL fragment with `?` bindings: knex's `whereRaw` / `orderByRaw` /
|
|
202
|
+
* `havingRaw` / `knex.raw` value. Throws if `sql` is not a non-empty string
|
|
203
|
+
* or `bindings` not an array; the server checks the rest.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* db.from('users').select().where(raw('lower(trim(email)) = any(?)', [emails]))
|
|
207
|
+
*/
|
|
208
|
+
const raw = (sql, bindings) => new Raw(sql, bindings);
|
|
155
209
|
// src/builder.ts
|
|
156
210
|
const embedOf = (o) => o.referencedTable ?? o.foreignTable;
|
|
157
211
|
/** Filter methods, shared by builders and groups. */
|
|
158
212
|
var Filters = class {
|
|
213
|
+
/** @internal A problem found while building, reported instead of sending. */
|
|
214
|
+
problem;
|
|
159
215
|
#filter(column, op, value) {
|
|
160
216
|
if (column instanceof Col) {
|
|
161
217
|
const ifNull = column.nullValue();
|
|
@@ -288,6 +344,24 @@ var Filters = class {
|
|
|
288
344
|
return this;
|
|
289
345
|
}
|
|
290
346
|
/**
|
|
347
|
+
* A raw SQL condition, knex's `whereRaw`: `AND (<fragment>)`, or one member
|
|
348
|
+
* of an `or()` group. Only takes `raw()`; for a column use `eq()` and friends.
|
|
349
|
+
* The fragment sees the main table's columns, not related tables.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* .where(db.raw('lower(trim(email)) = any(?)', [emails]))
|
|
353
|
+
* .where(db.raw('sent_per_hour < limit_per_hour'))
|
|
354
|
+
* .or((q) => q.where(db.raw('meta \\? ?', ['vip'])).eq('age', 30)) // jsonb ? operator
|
|
355
|
+
*/
|
|
356
|
+
where(fragment) {
|
|
357
|
+
if (fragment instanceof Raw) this.push({
|
|
358
|
+
op: "raw",
|
|
359
|
+
value: fragment.fragment()
|
|
360
|
+
});
|
|
361
|
+
else this.problem ??= "where() takes a raw() fragment; for a column use eq(), gt(), in() and friends";
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
291
365
|
* Any of the conditions.
|
|
292
366
|
*
|
|
293
367
|
* @example
|
|
@@ -314,6 +388,7 @@ var Filters = class {
|
|
|
314
388
|
#group(group, conditions) {
|
|
315
389
|
const q = new FilterGroup();
|
|
316
390
|
conditions(q);
|
|
391
|
+
if (q.problem) this.problem ??= q.problem;
|
|
317
392
|
this.push({
|
|
318
393
|
group,
|
|
319
394
|
filters: q.filters
|
|
@@ -340,8 +415,6 @@ var FilterGroup = class extends Filters {
|
|
|
340
415
|
var Builder = class extends Filters {
|
|
341
416
|
/** @internal */ transport;
|
|
342
417
|
/** @internal */ req;
|
|
343
|
-
/** @internal A problem found while building, reported instead of sending. */
|
|
344
|
-
problem;
|
|
345
418
|
#name;
|
|
346
419
|
/** @internal */
|
|
347
420
|
constructor(transport, req, name, problem) {
|
|
@@ -445,11 +518,30 @@ var QueryBuilder = class extends Builder {
|
|
|
445
518
|
* db.from('users').select('name, posts!user_id!inner(title)') // only users that have posts
|
|
446
519
|
* db.from('users').select('name, posts!user_id(count)') // posts: [{ count: 2 }]
|
|
447
520
|
* db.from('posts').select('*', { count: 'exact' }).range(0, 9) // rows + total
|
|
521
|
+
*
|
|
522
|
+
* Extra `raw(...).as(name)` arguments add SQL expressions of the main table
|
|
523
|
+
* as columns (options, if any, go last).
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* db.from('posts').select('id, title', db.raw('char_length(title)').as('title_len'))
|
|
448
527
|
*/
|
|
449
|
-
select(columns = "*",
|
|
528
|
+
select(columns = "*", ...extra) {
|
|
450
529
|
this.req.columns = columns;
|
|
451
|
-
|
|
452
|
-
|
|
530
|
+
const rawColumns = [];
|
|
531
|
+
for (const e of extra) if (e instanceof Raw) {
|
|
532
|
+
const as = e.alias();
|
|
533
|
+
if (as === void 0) this.problem ??= `raw("${e.sql}") in select() needs a column name: add .as('name')`;
|
|
534
|
+
else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(as)) this.problem ??= `raw(...).as("${as}"): the name must be letters, digits and _`;
|
|
535
|
+
else rawColumns.push({
|
|
536
|
+
...e.fragment(),
|
|
537
|
+
as
|
|
538
|
+
});
|
|
539
|
+
} else if (e) {
|
|
540
|
+
if (e.count) this.req.count = true;
|
|
541
|
+
if (e.head) this.req.head = true;
|
|
542
|
+
}
|
|
543
|
+
if (rawColumns.length) this.req.rawColumns = rawColumns;
|
|
544
|
+
else delete this.req.rawColumns;
|
|
453
545
|
return this;
|
|
454
546
|
}
|
|
455
547
|
/**
|
|
@@ -511,6 +603,18 @@ var QueryBuilder = class extends Builder {
|
|
|
511
603
|
return this;
|
|
512
604
|
}
|
|
513
605
|
/**
|
|
606
|
+
* Keep only the groups where the raw condition holds, knex's `havingRaw`.
|
|
607
|
+
* Use with `groupBy()`; several calls are joined with AND.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* db.from('posts').select('user_id, n:count()').groupBy('user_id').having(db.raw('count(*) >= ?', [2]))
|
|
611
|
+
*/
|
|
612
|
+
having(fragment) {
|
|
613
|
+
if (fragment instanceof Raw) (this.req.having ??= []).push(fragment.fragment());
|
|
614
|
+
else this.problem ??= "having() takes a raw() fragment: having(db.raw(\"count(*) >= ?\", [2]))";
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
514
618
|
* Leave out repeated rows: `SELECT DISTINCT`.
|
|
515
619
|
*
|
|
516
620
|
* @example
|
|
@@ -637,9 +741,19 @@ var QueryBuilder = class extends Builder {
|
|
|
637
741
|
* .order('active', { ascending: false }).order('age', { ascending: false }).order('name')
|
|
638
742
|
* .order('meta->>score::int', { ascending: false, nullsFirst: false })
|
|
639
743
|
* .order('title', { referencedTable: 'posts' }) // posts inside each user
|
|
744
|
+
* .order(db.raw("coalesce(last_read_time, '1970-01-01')")) // raw, main table only
|
|
640
745
|
*/
|
|
641
746
|
order(column, options = {}) {
|
|
642
747
|
const { ascending = true, nullsFirst } = options;
|
|
748
|
+
if (column instanceof Raw) {
|
|
749
|
+
if (embedOf(options)) this.problem ??= "order(raw(...)) sorts the main table only: drop referencedTable";
|
|
750
|
+
else this.req.order.push({
|
|
751
|
+
raw: column.fragment(),
|
|
752
|
+
ascending,
|
|
753
|
+
nullsFirst
|
|
754
|
+
});
|
|
755
|
+
return this;
|
|
756
|
+
}
|
|
643
757
|
this.req.order.push({
|
|
644
758
|
column,
|
|
645
759
|
ascending,
|
|
@@ -921,6 +1035,7 @@ function createClient(url, apiKey, options = {}) {
|
|
|
921
1035
|
rel,
|
|
922
1036
|
col,
|
|
923
1037
|
now,
|
|
1038
|
+
raw,
|
|
924
1039
|
async transaction(build) {
|
|
925
1040
|
const steps = toSteps(build({
|
|
926
1041
|
from: (table) => new QueryBuilder(null, table),
|
|
@@ -928,7 +1043,8 @@ function createClient(url, apiKey, options = {}) {
|
|
|
928
1043
|
ops,
|
|
929
1044
|
rel,
|
|
930
1045
|
col,
|
|
931
|
-
now
|
|
1046
|
+
now,
|
|
1047
|
+
raw
|
|
932
1048
|
}));
|
|
933
1049
|
if (typeof steps === "string") return fail(steps);
|
|
934
1050
|
const res = await transport.post("/transaction", { steps: steps.wire });
|
|
@@ -975,19 +1091,22 @@ const db = {
|
|
|
975
1091
|
ops,
|
|
976
1092
|
rel,
|
|
977
1093
|
col,
|
|
978
|
-
now
|
|
1094
|
+
now,
|
|
1095
|
+
raw
|
|
979
1096
|
};
|
|
980
1097
|
exports.Col = Col;
|
|
981
1098
|
exports.FilterGroup = FilterGroup;
|
|
982
1099
|
exports.Now = Now;
|
|
983
1100
|
exports.Op = Op;
|
|
984
1101
|
exports.QueryBuilder = QueryBuilder;
|
|
1102
|
+
exports.Raw = Raw;
|
|
985
1103
|
exports.WriteBuilder = WriteBuilder;
|
|
986
1104
|
exports.col = col;
|
|
987
1105
|
exports.createClient = createClient;
|
|
988
1106
|
exports.db = db;
|
|
989
1107
|
exports.now = now;
|
|
990
1108
|
exports.ops = ops;
|
|
1109
|
+
exports.raw = raw;
|
|
991
1110
|
exports.rel = rel;
|
|
992
1111
|
|
|
993
1112
|
//# sourceMappingURL=index.cjs.map
|