bun-query-builder 0.2.29 → 0.2.31
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/bin/cli.js +173 -20
- package/dist/db.d.ts +29 -4
- package/dist/drivers/mysql.d.ts +4 -0
- package/dist/drivers/postgres.d.ts +15 -0
- package/dist/drivers/sqlite.d.ts +4 -0
- package/dist/src/index.js +172 -19
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -11862,6 +11862,68 @@ import { Database } from "bun:sqlite";
|
|
|
11862
11862
|
import { mkdirSync as mkdirSync5 } from "fs";
|
|
11863
11863
|
import { dirname as dirname5 } from "path";
|
|
11864
11864
|
import process21 from "process";
|
|
11865
|
+
function bindNumberedPlaceholders(sql, params) {
|
|
11866
|
+
if (params.length === 0 || !/\$\d/.test(sql))
|
|
11867
|
+
return { sql, params };
|
|
11868
|
+
const out = [];
|
|
11869
|
+
let rewritten = "";
|
|
11870
|
+
let highest = 0;
|
|
11871
|
+
for (let i = 0;i < sql.length; i++) {
|
|
11872
|
+
const char = sql[i];
|
|
11873
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
11874
|
+
const end = skipQuoted(sql, i, char);
|
|
11875
|
+
rewritten += sql.slice(i, end);
|
|
11876
|
+
i = end - 1;
|
|
11877
|
+
continue;
|
|
11878
|
+
}
|
|
11879
|
+
if (char === "-" && sql[i + 1] === "-") {
|
|
11880
|
+
const end = sql.indexOf(`
|
|
11881
|
+
`, i);
|
|
11882
|
+
const stop = end === -1 ? sql.length : end;
|
|
11883
|
+
rewritten += sql.slice(i, stop);
|
|
11884
|
+
i = stop - 1;
|
|
11885
|
+
continue;
|
|
11886
|
+
}
|
|
11887
|
+
if (char === "/" && sql[i + 1] === "*") {
|
|
11888
|
+
const end = sql.indexOf("*/", i + 2);
|
|
11889
|
+
const stop = end === -1 ? sql.length : end + 2;
|
|
11890
|
+
rewritten += sql.slice(i, stop);
|
|
11891
|
+
i = stop - 1;
|
|
11892
|
+
continue;
|
|
11893
|
+
}
|
|
11894
|
+
if (char === "$") {
|
|
11895
|
+
const digits = /^\d+/.exec(sql.slice(i + 1))?.[0];
|
|
11896
|
+
if (digits) {
|
|
11897
|
+
const index = Number(digits);
|
|
11898
|
+
highest = Math.max(highest, index);
|
|
11899
|
+
if (index >= 1 && index <= params.length) {
|
|
11900
|
+
out.push(params[index - 1]);
|
|
11901
|
+
rewritten += "?";
|
|
11902
|
+
i += digits.length;
|
|
11903
|
+
continue;
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
}
|
|
11907
|
+
rewritten += char;
|
|
11908
|
+
}
|
|
11909
|
+
if (highest > params.length)
|
|
11910
|
+
throw new Error(`SQL references $${highest} but only ${params.length} value${params.length === 1 ? " was" : "s were"} provided`);
|
|
11911
|
+
if (out.length === 0)
|
|
11912
|
+
return { sql, params };
|
|
11913
|
+
return { sql: rewritten, params: out };
|
|
11914
|
+
}
|
|
11915
|
+
function skipQuoted(sql, start, quote) {
|
|
11916
|
+
for (let i = start + 1;i < sql.length; i++) {
|
|
11917
|
+
if (sql[i] !== quote)
|
|
11918
|
+
continue;
|
|
11919
|
+
if (sql[i + 1] === quote) {
|
|
11920
|
+
i++;
|
|
11921
|
+
continue;
|
|
11922
|
+
}
|
|
11923
|
+
return i + 1;
|
|
11924
|
+
}
|
|
11925
|
+
return sql.length;
|
|
11926
|
+
}
|
|
11865
11927
|
|
|
11866
11928
|
class SQLiteWrapper {
|
|
11867
11929
|
db;
|
|
@@ -11876,12 +11938,14 @@ class SQLiteWrapper {
|
|
|
11876
11938
|
applySqliteBootstrapPragmas(this.db);
|
|
11877
11939
|
}
|
|
11878
11940
|
query(sql, params = []) {
|
|
11879
|
-
const
|
|
11880
|
-
|
|
11941
|
+
const bound = bindNumberedPlaceholders(sql, params);
|
|
11942
|
+
const stmt = this.db.prepare(bound.sql);
|
|
11943
|
+
return stmt.all(...bound.params);
|
|
11881
11944
|
}
|
|
11882
11945
|
run(sql, params = []) {
|
|
11883
|
-
const
|
|
11884
|
-
|
|
11946
|
+
const bound = bindNumberedPlaceholders(sql, params);
|
|
11947
|
+
const stmt = this.db.prepare(bound.sql);
|
|
11948
|
+
return stmt.run(...bound.params);
|
|
11885
11949
|
}
|
|
11886
11950
|
close() {
|
|
11887
11951
|
this.db.close();
|
|
@@ -12650,6 +12714,21 @@ function renderSelectColumn(col) {
|
|
|
12650
12714
|
}
|
|
12651
12715
|
throw new TypeError(`[query-builder] select(): unsupported column ${String(col)} \u2014 pass a column name, a string[], or a SQL fragment (e.g. sql\`count(*) as c\`)`);
|
|
12652
12716
|
}
|
|
12717
|
+
function hasTopLevelOrderBy(s) {
|
|
12718
|
+
const re = /\(|\)|\bORDER\s+BY\b/gi;
|
|
12719
|
+
let depth = 0;
|
|
12720
|
+
let m;
|
|
12721
|
+
while (m = re.exec(s)) {
|
|
12722
|
+
if (m[0] === "(") {
|
|
12723
|
+
depth++;
|
|
12724
|
+
} else if (m[0] === ")") {
|
|
12725
|
+
depth = Math.max(0, depth - 1);
|
|
12726
|
+
} else if (depth === 0) {
|
|
12727
|
+
return true;
|
|
12728
|
+
}
|
|
12729
|
+
}
|
|
12730
|
+
return false;
|
|
12731
|
+
}
|
|
12653
12732
|
function validateIdentifier(name, context) {
|
|
12654
12733
|
if (!SQL_PATTERNS.IDENTIFIER.test(name)) {
|
|
12655
12734
|
const contextMsg = context ? ` in ${context}` : "";
|
|
@@ -13160,8 +13239,8 @@ function createQueryBuilder(state) {
|
|
|
13160
13239
|
}
|
|
13161
13240
|
return -1;
|
|
13162
13241
|
};
|
|
13163
|
-
const currentSql = () => {
|
|
13164
|
-
const body = renderWhereTerms(whereTerms);
|
|
13242
|
+
const currentSql = (extra) => {
|
|
13243
|
+
const body = renderWhereTerms(extra ? [...whereTerms, extra] : whereTerms);
|
|
13165
13244
|
if (!body)
|
|
13166
13245
|
return text;
|
|
13167
13246
|
if (whereTail) {
|
|
@@ -14888,25 +14967,33 @@ function createQueryBuilder(state) {
|
|
|
14888
14967
|
return { data: hasMore ? data.slice(0, perPage) : data, meta: { perPage, page: p, hasMore } };
|
|
14889
14968
|
},
|
|
14890
14969
|
async cursorPaginate(perPage, cursor, column = "id", direction = "asc") {
|
|
14891
|
-
|
|
14970
|
+
if (!Number.isInteger(perPage) || perPage <= 0)
|
|
14971
|
+
throw new TypeError(`[query-builder] cursorPaginate(perPage): expected positive integer, got ${perPage}`);
|
|
14972
|
+
const cols = (Array.isArray(column) ? column : [column]).map(String);
|
|
14973
|
+
for (const c of cols)
|
|
14974
|
+
validateIdentifier(c, "cursorPaginate(column)");
|
|
14975
|
+
const cmp = direction === "asc" ? ">" : "<";
|
|
14976
|
+
const dir = direction === "asc" ? "ASC" : "DESC";
|
|
14977
|
+
const params = [...whereParams];
|
|
14978
|
+
let predicate = "";
|
|
14892
14979
|
if (cursor !== undefined && cursor !== null) {
|
|
14893
14980
|
if (Array.isArray(column)) {
|
|
14894
|
-
const
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14981
|
+
const phs = cursor.map((v) => {
|
|
14982
|
+
params.push(v);
|
|
14983
|
+
return getPlaceholder(params.length);
|
|
14984
|
+
});
|
|
14985
|
+
predicate = `(${cols.join(", ")}) ${cmp} (${phs.join(", ")})`;
|
|
14899
14986
|
} else {
|
|
14900
|
-
|
|
14987
|
+
params.push(cursor);
|
|
14988
|
+
predicate = `${cols[0]} ${cmp} ${getPlaceholder(params.length)}`;
|
|
14901
14989
|
}
|
|
14902
14990
|
}
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
} else {
|
|
14908
|
-
q = sql`${q} ORDER BY ${sql(String(column))} ${direction === "asc" ? sql`ASC` : sql`DESC`} LIMIT ${perPage + 1}`;
|
|
14991
|
+
const base2 = reorderSelectClauses(predicate ? currentSql({ conn: "AND", sql: predicate }) : currentSql());
|
|
14992
|
+
const order = cols.map((c) => `${c} ${dir}`).join(", ");
|
|
14993
|
+
if (hasTopLevelOrderBy(base2)) {
|
|
14994
|
+
throw new TypeError(`[query-builder] cursorPaginate() cannot be combined with orderBy() \u2014 it orders by ` + `${order} itself, and a cursor is only meaningful in that order. ` + `Remove the orderBy(), or pass the column and direction to cursorPaginate().`);
|
|
14909
14995
|
}
|
|
14996
|
+
const q = params.length > 0 ? _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`, params) : _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`);
|
|
14910
14997
|
const rows = await runWithHooks(q, "select", { signal: abortSignal, timeoutMs });
|
|
14911
14998
|
const hasMore = rows.length > perPage;
|
|
14912
14999
|
const data = rows.slice(0, perPage);
|
|
@@ -18431,6 +18518,28 @@ ${sql};` : `${sql};`;
|
|
|
18431
18518
|
recordMigrationQuery() {
|
|
18432
18519
|
return "INSERT INTO migrations (migration) VALUES (?)";
|
|
18433
18520
|
}
|
|
18521
|
+
jsonExtract(column, key) {
|
|
18522
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(${column}, CONCAT('$.', ${key})))`;
|
|
18523
|
+
}
|
|
18524
|
+
dateBucket(column, grain, offsetHours = 0) {
|
|
18525
|
+
const shifted = offsetHours === 0 ? column : `DATE_ADD(${column}, INTERVAL ${offsetHours} HOUR)`;
|
|
18526
|
+
switch (grain) {
|
|
18527
|
+
case "hour":
|
|
18528
|
+
return `DATE_FORMAT(${shifted}, '%Y-%m-%dT%H:00:00.000Z')`;
|
|
18529
|
+
case "week":
|
|
18530
|
+
return `DATE_FORMAT(DATE_SUB(${shifted}, INTERVAL WEEKDAY(${shifted}) DAY), '%Y-%m-%dT00:00:00.000Z')`;
|
|
18531
|
+
case "month":
|
|
18532
|
+
return `DATE_FORMAT(${shifted}, '%Y-%m-01T00:00:00.000Z')`;
|
|
18533
|
+
case "year":
|
|
18534
|
+
return `DATE_FORMAT(${shifted}, '%Y-01-01T00:00:00.000Z')`;
|
|
18535
|
+
case "day":
|
|
18536
|
+
default:
|
|
18537
|
+
return `DATE_FORMAT(${shifted}, '%Y-%m-%dT00:00:00.000Z')`;
|
|
18538
|
+
}
|
|
18539
|
+
}
|
|
18540
|
+
booleanLiteral(value) {
|
|
18541
|
+
return value ? "TRUE" : "FALSE";
|
|
18542
|
+
}
|
|
18434
18543
|
renderColumn(column) {
|
|
18435
18544
|
const typeSql = this.getColumnType(column);
|
|
18436
18545
|
const parts = [this.quoteIdentifier(column.name), typeSql];
|
|
@@ -18628,6 +18737,28 @@ ${sql};` : `${sql};`;
|
|
|
18628
18737
|
recordMigrationQuery() {
|
|
18629
18738
|
return "INSERT INTO migrations (migration) VALUES ($1)";
|
|
18630
18739
|
}
|
|
18740
|
+
jsonExtract(column, key) {
|
|
18741
|
+
return `(${column}::jsonb ->> ${key})`;
|
|
18742
|
+
}
|
|
18743
|
+
dateBucket(column, grain, offsetHours = 0) {
|
|
18744
|
+
const shifted = offsetHours === 0 ? `${column}::timestamp` : `(${column}::timestamp ${offsetHours >= 0 ? "+" : "-"} interval '${Math.abs(offsetHours)} hours')`;
|
|
18745
|
+
const truncated = grain === "week" ? `date_trunc('week', ${shifted})` : shifted;
|
|
18746
|
+
switch (grain) {
|
|
18747
|
+
case "hour":
|
|
18748
|
+
return `to_char(${truncated}, 'YYYY-MM-DD"T"HH24:00:00.000"Z"')`;
|
|
18749
|
+
case "month":
|
|
18750
|
+
return `to_char(${truncated}, 'YYYY-MM-01"T"00:00:00.000"Z"')`;
|
|
18751
|
+
case "year":
|
|
18752
|
+
return `to_char(${truncated}, 'YYYY-01-01"T"00:00:00.000"Z"')`;
|
|
18753
|
+
case "week":
|
|
18754
|
+
case "day":
|
|
18755
|
+
default:
|
|
18756
|
+
return `to_char(${truncated}, 'YYYY-MM-DD"T"00:00:00.000"Z"')`;
|
|
18757
|
+
}
|
|
18758
|
+
}
|
|
18759
|
+
booleanLiteral(value) {
|
|
18760
|
+
return value ? "TRUE" : "FALSE";
|
|
18761
|
+
}
|
|
18631
18762
|
renderColumn(column) {
|
|
18632
18763
|
const typeSql = column.isPrimaryKey ? this.getPrimaryKeyType(column) : this.getColumnType(column);
|
|
18633
18764
|
const parts = [this.quoteIdentifier(column.name), typeSql];
|
|
@@ -18911,6 +19042,28 @@ class SQLiteDriver {
|
|
|
18911
19042
|
recordMigrationQuery() {
|
|
18912
19043
|
return "INSERT INTO migrations (migration) VALUES (?)";
|
|
18913
19044
|
}
|
|
19045
|
+
jsonExtract(column, key) {
|
|
19046
|
+
return `json_extract(${column}, '$.' || ${key})`;
|
|
19047
|
+
}
|
|
19048
|
+
dateBucket(column, grain, offsetHours = 0) {
|
|
19049
|
+
const shifted = offsetHours === 0 ? column : `datetime(${column}, '${offsetHours >= 0 ? "+" : "-"}${Math.abs(offsetHours)} hours')`;
|
|
19050
|
+
switch (grain) {
|
|
19051
|
+
case "hour":
|
|
19052
|
+
return `strftime('%Y-%m-%dT%H:00:00.000Z', ${shifted})`;
|
|
19053
|
+
case "week":
|
|
19054
|
+
return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted}, '-6 days', 'weekday 1')`;
|
|
19055
|
+
case "month":
|
|
19056
|
+
return `strftime('%Y-%m-01T00:00:00.000Z', ${shifted})`;
|
|
19057
|
+
case "year":
|
|
19058
|
+
return `strftime('%Y-01-01T00:00:00.000Z', ${shifted})`;
|
|
19059
|
+
case "day":
|
|
19060
|
+
default:
|
|
19061
|
+
return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted})`;
|
|
19062
|
+
}
|
|
19063
|
+
}
|
|
19064
|
+
booleanLiteral(value) {
|
|
19065
|
+
return value ? "TRUE" : "FALSE";
|
|
19066
|
+
}
|
|
18914
19067
|
renderColumn(column) {
|
|
18915
19068
|
const typeSql = this.getColumnType(column);
|
|
18916
19069
|
const parts = [this.quoteIdentifier(column.name), typeSql];
|
|
@@ -31980,7 +32133,7 @@ function getPrefix() {
|
|
|
31980
32133
|
}
|
|
31981
32134
|
var prefix = getPrefix();
|
|
31982
32135
|
// package.json
|
|
31983
|
-
var version2 = "0.2.
|
|
32136
|
+
var version2 = "0.2.31";
|
|
31984
32137
|
|
|
31985
32138
|
// bin/cli.ts
|
|
31986
32139
|
init_actions();
|
package/dist/db.d.ts
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
2
|
import { SQL } from 'bun';
|
|
3
3
|
import type { DatabaseConfig, PoolConfig, SupportedDialect } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* SQLite wrapper that provides a SQL-like tagged template literal interface
|
|
6
|
+
* using bun:sqlite's Database class for better compiled binary support.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Give `$1`-style placeholders their Postgres meaning on SQLite: `$n` is the
|
|
10
|
+
* n-th value, wherever it appears and however often.
|
|
11
|
+
*
|
|
12
|
+
* bun:sqlite treats `$1` as a NAME, not an index. Handed an array it binds the
|
|
13
|
+
* values to those names in ORDER OF FIRST APPEARANCE and ignores the numbers
|
|
14
|
+
* entirely, so a query whose placeholders are not already in ascending order
|
|
15
|
+
* binds the wrong values and says nothing:
|
|
16
|
+
*
|
|
17
|
+
* sql.unsafe('SELECT $2 AS a, $1 AS b', ['ONE', 'TWO'])
|
|
18
|
+
* -> { a: 'ONE', b: 'TWO' } // both backwards, no error
|
|
19
|
+
*
|
|
20
|
+
* Every caller writing `$n` means the Postgres semantics, because the same
|
|
21
|
+
* query text is expected to run on both drivers. The failure is silent, and it
|
|
22
|
+
* lands hardest in exactly the queries that repeat a parameter - authorisation
|
|
23
|
+
* checks of the shape "owner_id = $2 OR EXISTS (... user_id = $2)" - where the
|
|
24
|
+
* result is a permission answer computed from the wrong values.
|
|
25
|
+
*
|
|
26
|
+
* Rewritten to ordinal `?` here, once, at the only place both `query` and `run`
|
|
27
|
+
* pass through. Placeholders inside string literals, quoted identifiers and
|
|
28
|
+
* comments are left alone: `'$1'` is data, not a parameter. SQL with no
|
|
29
|
+
* numbered placeholders is returned untouched, so `?` queries and Bun's own
|
|
30
|
+
* tagged-template output are unaffected.
|
|
31
|
+
*/
|
|
32
|
+
export declare function bindNumberedPlaceholders(sql: string, params: any[]): { sql: string, params: any[] };
|
|
4
33
|
export declare function splitSqlStatements(sql: string): string[];
|
|
5
34
|
/**
|
|
6
35
|
* Creates a database connection string based on the configured dialect and database settings.
|
|
@@ -98,10 +127,6 @@ export declare interface DriverConnection {
|
|
|
98
127
|
}
|
|
99
128
|
/** An `unsafe(...)` result: a query object that is ALSO directly awaitable. */
|
|
100
129
|
export type AwaitableDriverQuery = DriverQuery & PromiseLike<any>;
|
|
101
|
-
/**
|
|
102
|
-
* SQLite wrapper that provides a SQL-like tagged template literal interface
|
|
103
|
-
* using bun:sqlite's Database class for better compiled binary support.
|
|
104
|
-
*/
|
|
105
130
|
declare class SQLiteWrapper {
|
|
106
131
|
constructor(filename: string);
|
|
107
132
|
query(sql: string, params?: any[]): any[];
|
package/dist/drivers/mysql.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnPlan, IndexPlan, RebuildTableSpec, TablePlan } from '../migrations';
|
|
2
|
+
import type { DateBucketGrain } from './postgres';
|
|
2
3
|
export declare interface DialectDriver {
|
|
3
4
|
createEnumType: (enumTypeName: string, values: string[]) => string
|
|
4
5
|
createTable: (table: TablePlan) => string
|
|
@@ -39,5 +40,8 @@ export declare class MySQLDriver implements DialectDriver {
|
|
|
39
40
|
createMigrationsTable(): string;
|
|
40
41
|
getExecutedMigrationsQuery(): string;
|
|
41
42
|
recordMigrationQuery(): string;
|
|
43
|
+
jsonExtract(column: string, key: string): string;
|
|
44
|
+
dateBucket(column: string, grain: DateBucketGrain, offsetHours?: number): string;
|
|
45
|
+
booleanLiteral(value: boolean): string;
|
|
42
46
|
protected renderColumn(column: ColumnPlan): string;
|
|
43
47
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { ColumnPlan, IndexPlan, RebuildTableSpec, TablePlan } from '../migrations';
|
|
2
|
+
/**
|
|
3
|
+
* The ISO shape every driver's {@link DialectDriver.dateBucket} must produce.
|
|
4
|
+
*
|
|
5
|
+
* Exported so a driver's tests can assert against one definition rather than
|
|
6
|
+
* each restating the format it happens to emit.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DATE_BUCKET_FORMAT: 'YYYY-MM-DDTHH:MM:SS.000Z';
|
|
2
9
|
export declare interface DialectDriver {
|
|
3
10
|
createEnumType: (enumTypeName: string, values: string[]) => string
|
|
4
11
|
createTable: (table: TablePlan) => string
|
|
@@ -16,7 +23,12 @@ export declare interface DialectDriver {
|
|
|
16
23
|
createMigrationsTable: () => string
|
|
17
24
|
getExecutedMigrationsQuery: () => string
|
|
18
25
|
recordMigrationQuery: () => string
|
|
26
|
+
jsonExtract: (column: string, key: string) => string
|
|
27
|
+
dateBucket: (column: string, grain: DateBucketGrain, offsetHours?: number) => string
|
|
28
|
+
booleanLiteral: (value: boolean) => string
|
|
19
29
|
}
|
|
30
|
+
/** The buckets {@link DialectDriver.dateBucket} can truncate to. */
|
|
31
|
+
export type DateBucketGrain = 'hour' | 'day' | 'week' | 'month' | 'year';
|
|
20
32
|
export declare class PostgresDriver implements DialectDriver {
|
|
21
33
|
createEnumType(enumTypeName: string, values: string[]): string;
|
|
22
34
|
createTable(table: TablePlan): string;
|
|
@@ -34,4 +46,7 @@ export declare class PostgresDriver implements DialectDriver {
|
|
|
34
46
|
createMigrationsTable(): string;
|
|
35
47
|
getExecutedMigrationsQuery(): string;
|
|
36
48
|
recordMigrationQuery(): string;
|
|
49
|
+
jsonExtract(column: string, key: string): string;
|
|
50
|
+
dateBucket(column: string, grain: DateBucketGrain, offsetHours?: number): string;
|
|
51
|
+
booleanLiteral(value: boolean): string;
|
|
37
52
|
}
|
package/dist/drivers/sqlite.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnPlan, IndexPlan, RebuildTableSpec, TablePlan } from '../migrations';
|
|
2
|
+
import type { DateBucketGrain } from './postgres';
|
|
2
3
|
/**
|
|
3
4
|
* Whether a plan type belongs to the numeric family. Used by the `_id`
|
|
4
5
|
* safety net (numeric ids must store as INTEGER on SQLite) — non-numeric
|
|
@@ -40,4 +41,7 @@ export declare class SQLiteDriver implements DialectDriver {
|
|
|
40
41
|
createMigrationsTable(): string;
|
|
41
42
|
getExecutedMigrationsQuery(): string;
|
|
42
43
|
recordMigrationQuery(): string;
|
|
44
|
+
jsonExtract(column: string, key: string): string;
|
|
45
|
+
dateBucket(column: string, grain: DateBucketGrain, offsetHours?: number): string;
|
|
46
|
+
booleanLiteral(value: boolean): string;
|
|
43
47
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -11861,6 +11861,68 @@ import { Database } from "bun:sqlite";
|
|
|
11861
11861
|
import { mkdirSync as mkdirSync5 } from "fs";
|
|
11862
11862
|
import { dirname as dirname5 } from "path";
|
|
11863
11863
|
import process21 from "process";
|
|
11864
|
+
function bindNumberedPlaceholders(sql, params) {
|
|
11865
|
+
if (params.length === 0 || !/\$\d/.test(sql))
|
|
11866
|
+
return { sql, params };
|
|
11867
|
+
const out = [];
|
|
11868
|
+
let rewritten = "";
|
|
11869
|
+
let highest = 0;
|
|
11870
|
+
for (let i = 0;i < sql.length; i++) {
|
|
11871
|
+
const char = sql[i];
|
|
11872
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
11873
|
+
const end = skipQuoted(sql, i, char);
|
|
11874
|
+
rewritten += sql.slice(i, end);
|
|
11875
|
+
i = end - 1;
|
|
11876
|
+
continue;
|
|
11877
|
+
}
|
|
11878
|
+
if (char === "-" && sql[i + 1] === "-") {
|
|
11879
|
+
const end = sql.indexOf(`
|
|
11880
|
+
`, i);
|
|
11881
|
+
const stop = end === -1 ? sql.length : end;
|
|
11882
|
+
rewritten += sql.slice(i, stop);
|
|
11883
|
+
i = stop - 1;
|
|
11884
|
+
continue;
|
|
11885
|
+
}
|
|
11886
|
+
if (char === "/" && sql[i + 1] === "*") {
|
|
11887
|
+
const end = sql.indexOf("*/", i + 2);
|
|
11888
|
+
const stop = end === -1 ? sql.length : end + 2;
|
|
11889
|
+
rewritten += sql.slice(i, stop);
|
|
11890
|
+
i = stop - 1;
|
|
11891
|
+
continue;
|
|
11892
|
+
}
|
|
11893
|
+
if (char === "$") {
|
|
11894
|
+
const digits = /^\d+/.exec(sql.slice(i + 1))?.[0];
|
|
11895
|
+
if (digits) {
|
|
11896
|
+
const index = Number(digits);
|
|
11897
|
+
highest = Math.max(highest, index);
|
|
11898
|
+
if (index >= 1 && index <= params.length) {
|
|
11899
|
+
out.push(params[index - 1]);
|
|
11900
|
+
rewritten += "?";
|
|
11901
|
+
i += digits.length;
|
|
11902
|
+
continue;
|
|
11903
|
+
}
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
rewritten += char;
|
|
11907
|
+
}
|
|
11908
|
+
if (highest > params.length)
|
|
11909
|
+
throw new Error(`SQL references $${highest} but only ${params.length} value${params.length === 1 ? " was" : "s were"} provided`);
|
|
11910
|
+
if (out.length === 0)
|
|
11911
|
+
return { sql, params };
|
|
11912
|
+
return { sql: rewritten, params: out };
|
|
11913
|
+
}
|
|
11914
|
+
function skipQuoted(sql, start, quote) {
|
|
11915
|
+
for (let i = start + 1;i < sql.length; i++) {
|
|
11916
|
+
if (sql[i] !== quote)
|
|
11917
|
+
continue;
|
|
11918
|
+
if (sql[i + 1] === quote) {
|
|
11919
|
+
i++;
|
|
11920
|
+
continue;
|
|
11921
|
+
}
|
|
11922
|
+
return i + 1;
|
|
11923
|
+
}
|
|
11924
|
+
return sql.length;
|
|
11925
|
+
}
|
|
11864
11926
|
|
|
11865
11927
|
class SQLiteWrapper {
|
|
11866
11928
|
db;
|
|
@@ -11875,12 +11937,14 @@ class SQLiteWrapper {
|
|
|
11875
11937
|
applySqliteBootstrapPragmas(this.db);
|
|
11876
11938
|
}
|
|
11877
11939
|
query(sql, params = []) {
|
|
11878
|
-
const
|
|
11879
|
-
|
|
11940
|
+
const bound = bindNumberedPlaceholders(sql, params);
|
|
11941
|
+
const stmt = this.db.prepare(bound.sql);
|
|
11942
|
+
return stmt.all(...bound.params);
|
|
11880
11943
|
}
|
|
11881
11944
|
run(sql, params = []) {
|
|
11882
|
-
const
|
|
11883
|
-
|
|
11945
|
+
const bound = bindNumberedPlaceholders(sql, params);
|
|
11946
|
+
const stmt = this.db.prepare(bound.sql);
|
|
11947
|
+
return stmt.run(...bound.params);
|
|
11884
11948
|
}
|
|
11885
11949
|
close() {
|
|
11886
11950
|
this.db.close();
|
|
@@ -12649,6 +12713,21 @@ function renderSelectColumn(col) {
|
|
|
12649
12713
|
}
|
|
12650
12714
|
throw new TypeError(`[query-builder] select(): unsupported column ${String(col)} \u2014 pass a column name, a string[], or a SQL fragment (e.g. sql\`count(*) as c\`)`);
|
|
12651
12715
|
}
|
|
12716
|
+
function hasTopLevelOrderBy(s) {
|
|
12717
|
+
const re = /\(|\)|\bORDER\s+BY\b/gi;
|
|
12718
|
+
let depth = 0;
|
|
12719
|
+
let m;
|
|
12720
|
+
while (m = re.exec(s)) {
|
|
12721
|
+
if (m[0] === "(") {
|
|
12722
|
+
depth++;
|
|
12723
|
+
} else if (m[0] === ")") {
|
|
12724
|
+
depth = Math.max(0, depth - 1);
|
|
12725
|
+
} else if (depth === 0) {
|
|
12726
|
+
return true;
|
|
12727
|
+
}
|
|
12728
|
+
}
|
|
12729
|
+
return false;
|
|
12730
|
+
}
|
|
12652
12731
|
function validateIdentifier(name, context) {
|
|
12653
12732
|
if (!SQL_PATTERNS.IDENTIFIER.test(name)) {
|
|
12654
12733
|
const contextMsg = context ? ` in ${context}` : "";
|
|
@@ -13159,8 +13238,8 @@ function createQueryBuilder(state) {
|
|
|
13159
13238
|
}
|
|
13160
13239
|
return -1;
|
|
13161
13240
|
};
|
|
13162
|
-
const currentSql = () => {
|
|
13163
|
-
const body = renderWhereTerms(whereTerms);
|
|
13241
|
+
const currentSql = (extra) => {
|
|
13242
|
+
const body = renderWhereTerms(extra ? [...whereTerms, extra] : whereTerms);
|
|
13164
13243
|
if (!body)
|
|
13165
13244
|
return text;
|
|
13166
13245
|
if (whereTail) {
|
|
@@ -14887,25 +14966,33 @@ function createQueryBuilder(state) {
|
|
|
14887
14966
|
return { data: hasMore ? data.slice(0, perPage) : data, meta: { perPage, page: p, hasMore } };
|
|
14888
14967
|
},
|
|
14889
14968
|
async cursorPaginate(perPage, cursor, column = "id", direction = "asc") {
|
|
14890
|
-
|
|
14969
|
+
if (!Number.isInteger(perPage) || perPage <= 0)
|
|
14970
|
+
throw new TypeError(`[query-builder] cursorPaginate(perPage): expected positive integer, got ${perPage}`);
|
|
14971
|
+
const cols = (Array.isArray(column) ? column : [column]).map(String);
|
|
14972
|
+
for (const c of cols)
|
|
14973
|
+
validateIdentifier(c, "cursorPaginate(column)");
|
|
14974
|
+
const cmp = direction === "asc" ? ">" : "<";
|
|
14975
|
+
const dir = direction === "asc" ? "ASC" : "DESC";
|
|
14976
|
+
const params = [...whereParams];
|
|
14977
|
+
let predicate = "";
|
|
14891
14978
|
if (cursor !== undefined && cursor !== null) {
|
|
14892
14979
|
if (Array.isArray(column)) {
|
|
14893
|
-
const
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14980
|
+
const phs = cursor.map((v) => {
|
|
14981
|
+
params.push(v);
|
|
14982
|
+
return getPlaceholder(params.length);
|
|
14983
|
+
});
|
|
14984
|
+
predicate = `(${cols.join(", ")}) ${cmp} (${phs.join(", ")})`;
|
|
14898
14985
|
} else {
|
|
14899
|
-
|
|
14986
|
+
params.push(cursor);
|
|
14987
|
+
predicate = `${cols[0]} ${cmp} ${getPlaceholder(params.length)}`;
|
|
14900
14988
|
}
|
|
14901
14989
|
}
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
} else {
|
|
14907
|
-
q = sql`${q} ORDER BY ${sql(String(column))} ${direction === "asc" ? sql`ASC` : sql`DESC`} LIMIT ${perPage + 1}`;
|
|
14990
|
+
const base2 = reorderSelectClauses(predicate ? currentSql({ conn: "AND", sql: predicate }) : currentSql());
|
|
14991
|
+
const order = cols.map((c) => `${c} ${dir}`).join(", ");
|
|
14992
|
+
if (hasTopLevelOrderBy(base2)) {
|
|
14993
|
+
throw new TypeError(`[query-builder] cursorPaginate() cannot be combined with orderBy() \u2014 it orders by ` + `${order} itself, and a cursor is only meaningful in that order. ` + `Remove the orderBy(), or pass the column and direction to cursorPaginate().`);
|
|
14908
14994
|
}
|
|
14995
|
+
const q = params.length > 0 ? _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`, params) : _sql.unsafe(`${base2} ORDER BY ${order} LIMIT ${perPage + 1}`);
|
|
14909
14996
|
const rows = await runWithHooks(q, "select", { signal: abortSignal, timeoutMs });
|
|
14910
14997
|
const hasMore = rows.length > perPage;
|
|
14911
14998
|
const data = rows.slice(0, perPage);
|
|
@@ -18430,6 +18517,28 @@ ${sql};` : `${sql};`;
|
|
|
18430
18517
|
recordMigrationQuery() {
|
|
18431
18518
|
return "INSERT INTO migrations (migration) VALUES (?)";
|
|
18432
18519
|
}
|
|
18520
|
+
jsonExtract(column, key) {
|
|
18521
|
+
return `JSON_UNQUOTE(JSON_EXTRACT(${column}, CONCAT('$.', ${key})))`;
|
|
18522
|
+
}
|
|
18523
|
+
dateBucket(column, grain, offsetHours = 0) {
|
|
18524
|
+
const shifted = offsetHours === 0 ? column : `DATE_ADD(${column}, INTERVAL ${offsetHours} HOUR)`;
|
|
18525
|
+
switch (grain) {
|
|
18526
|
+
case "hour":
|
|
18527
|
+
return `DATE_FORMAT(${shifted}, '%Y-%m-%dT%H:00:00.000Z')`;
|
|
18528
|
+
case "week":
|
|
18529
|
+
return `DATE_FORMAT(DATE_SUB(${shifted}, INTERVAL WEEKDAY(${shifted}) DAY), '%Y-%m-%dT00:00:00.000Z')`;
|
|
18530
|
+
case "month":
|
|
18531
|
+
return `DATE_FORMAT(${shifted}, '%Y-%m-01T00:00:00.000Z')`;
|
|
18532
|
+
case "year":
|
|
18533
|
+
return `DATE_FORMAT(${shifted}, '%Y-01-01T00:00:00.000Z')`;
|
|
18534
|
+
case "day":
|
|
18535
|
+
default:
|
|
18536
|
+
return `DATE_FORMAT(${shifted}, '%Y-%m-%dT00:00:00.000Z')`;
|
|
18537
|
+
}
|
|
18538
|
+
}
|
|
18539
|
+
booleanLiteral(value) {
|
|
18540
|
+
return value ? "TRUE" : "FALSE";
|
|
18541
|
+
}
|
|
18433
18542
|
renderColumn(column) {
|
|
18434
18543
|
const typeSql = this.getColumnType(column);
|
|
18435
18544
|
const parts = [this.quoteIdentifier(column.name), typeSql];
|
|
@@ -18627,6 +18736,28 @@ ${sql};` : `${sql};`;
|
|
|
18627
18736
|
recordMigrationQuery() {
|
|
18628
18737
|
return "INSERT INTO migrations (migration) VALUES ($1)";
|
|
18629
18738
|
}
|
|
18739
|
+
jsonExtract(column, key) {
|
|
18740
|
+
return `(${column}::jsonb ->> ${key})`;
|
|
18741
|
+
}
|
|
18742
|
+
dateBucket(column, grain, offsetHours = 0) {
|
|
18743
|
+
const shifted = offsetHours === 0 ? `${column}::timestamp` : `(${column}::timestamp ${offsetHours >= 0 ? "+" : "-"} interval '${Math.abs(offsetHours)} hours')`;
|
|
18744
|
+
const truncated = grain === "week" ? `date_trunc('week', ${shifted})` : shifted;
|
|
18745
|
+
switch (grain) {
|
|
18746
|
+
case "hour":
|
|
18747
|
+
return `to_char(${truncated}, 'YYYY-MM-DD"T"HH24:00:00.000"Z"')`;
|
|
18748
|
+
case "month":
|
|
18749
|
+
return `to_char(${truncated}, 'YYYY-MM-01"T"00:00:00.000"Z"')`;
|
|
18750
|
+
case "year":
|
|
18751
|
+
return `to_char(${truncated}, 'YYYY-01-01"T"00:00:00.000"Z"')`;
|
|
18752
|
+
case "week":
|
|
18753
|
+
case "day":
|
|
18754
|
+
default:
|
|
18755
|
+
return `to_char(${truncated}, 'YYYY-MM-DD"T"00:00:00.000"Z"')`;
|
|
18756
|
+
}
|
|
18757
|
+
}
|
|
18758
|
+
booleanLiteral(value) {
|
|
18759
|
+
return value ? "TRUE" : "FALSE";
|
|
18760
|
+
}
|
|
18630
18761
|
renderColumn(column) {
|
|
18631
18762
|
const typeSql = column.isPrimaryKey ? this.getPrimaryKeyType(column) : this.getColumnType(column);
|
|
18632
18763
|
const parts = [this.quoteIdentifier(column.name), typeSql];
|
|
@@ -18910,6 +19041,28 @@ class SQLiteDriver {
|
|
|
18910
19041
|
recordMigrationQuery() {
|
|
18911
19042
|
return "INSERT INTO migrations (migration) VALUES (?)";
|
|
18912
19043
|
}
|
|
19044
|
+
jsonExtract(column, key) {
|
|
19045
|
+
return `json_extract(${column}, '$.' || ${key})`;
|
|
19046
|
+
}
|
|
19047
|
+
dateBucket(column, grain, offsetHours = 0) {
|
|
19048
|
+
const shifted = offsetHours === 0 ? column : `datetime(${column}, '${offsetHours >= 0 ? "+" : "-"}${Math.abs(offsetHours)} hours')`;
|
|
19049
|
+
switch (grain) {
|
|
19050
|
+
case "hour":
|
|
19051
|
+
return `strftime('%Y-%m-%dT%H:00:00.000Z', ${shifted})`;
|
|
19052
|
+
case "week":
|
|
19053
|
+
return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted}, '-6 days', 'weekday 1')`;
|
|
19054
|
+
case "month":
|
|
19055
|
+
return `strftime('%Y-%m-01T00:00:00.000Z', ${shifted})`;
|
|
19056
|
+
case "year":
|
|
19057
|
+
return `strftime('%Y-01-01T00:00:00.000Z', ${shifted})`;
|
|
19058
|
+
case "day":
|
|
19059
|
+
default:
|
|
19060
|
+
return `strftime('%Y-%m-%dT00:00:00.000Z', ${shifted})`;
|
|
19061
|
+
}
|
|
19062
|
+
}
|
|
19063
|
+
booleanLiteral(value) {
|
|
19064
|
+
return value ? "TRUE" : "FALSE";
|
|
19065
|
+
}
|
|
18913
19066
|
renderColumn(column) {
|
|
18914
19067
|
const typeSql = this.getColumnType(column);
|
|
18915
19068
|
const parts = [this.quoteIdentifier(column.name), typeSql];
|
package/package.json
CHANGED