cursedbelt-server 4.38.0 → 4.39.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/server/d1/limits.d.ts +24 -0
- package/dist/server/d1/limits.js +94 -0
- package/dist/server/d1/local.js +2 -1
- package/dist/server/d1/remote.js +2 -1
- package/package.json +1 -1
- package/src/server/d1/limits.spec.ts +60 -1
- package/src/server/d1/limits.ts +89 -0
- package/src/server/d1/local.ts +2 -1
- package/src/server/d1/remote.ts +2 -1
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* · **100 bound parameters per query.** Any seed, import or bulk insert hits this. Use
|
|
11
11
|
* {@link chunkForBind}, which does the arithmetic rather than leaving it to a guess.
|
|
12
12
|
* · **2 MB per string / BLOB / row.** Nothing may store a media byte in D1.
|
|
13
|
+
* · **5 terms per compound SELECT** — `UNION`, `UNION ALL`, `INTERSECT`, `EXCEPT`, and
|
|
14
|
+
* every term of a recursive CTE counts. SQLite's own default is 500, so a sixth term is
|
|
15
|
+
* green here and `too many terms in compound SELECT` on D1. Measured against production
|
|
16
|
+
* D1 on 2026-09-26: five terms answer, six are refused, inside a CTE exactly as outside
|
|
17
|
+
* one. `family`'s "Both families" viewport was a six-term recursive CTE, and every read
|
|
18
|
+
* it scoped failed in production from the D1 cutover until then. Wrap extra seed terms in
|
|
19
|
+
* a subquery — `SELECT id FROM (… UNION …)` is ONE term of the outer compound.
|
|
13
20
|
*
|
|
14
21
|
* The limits are asserted by the LOCAL driver as well as the remote one, which is the
|
|
15
22
|
* whole point: a bulk insert that would fail in production fails on the laptop, at the
|
|
@@ -31,12 +38,29 @@ export declare const LIMITS: {
|
|
|
31
38
|
readonly columnsPerTable: 100;
|
|
32
39
|
/** Wall-clock milliseconds one query may take. */
|
|
33
40
|
readonly queryDurationMs: 30000;
|
|
41
|
+
/** Terms in one compound SELECT, recursive CTEs included. SQLite's default is 500. */
|
|
42
|
+
readonly compoundSelectTerms: 5;
|
|
34
43
|
};
|
|
35
44
|
/**
|
|
36
45
|
* Refuse a statement that D1 would refuse — checked on BOTH drivers, at `bind()` time, so
|
|
37
46
|
* the failure lands on the call site that built the query rather than in production.
|
|
38
47
|
*/
|
|
39
48
|
export declare function assertWithinLimits(sql: string, params: readonly D1LikeBindable[]): void;
|
|
49
|
+
/**
|
|
50
|
+
* The most terms any ONE compound SELECT in `sql` has — `1` for a plain SELECT.
|
|
51
|
+
*
|
|
52
|
+
* Lexical, because `bun:sqlite` exposes no `sqlite3_limit`: string literals, quoted
|
|
53
|
+
* identifiers and comments are skipped, and every parenthesis opens its own count, since a
|
|
54
|
+
* parenthesised subquery or CTE body is a compound of its own. A `;` ends a statement.
|
|
55
|
+
* Multi-row `VALUES` is not counted — SQLite stopped counting it against this limit in 3.8.8.
|
|
56
|
+
*/
|
|
57
|
+
export declare function maxCompoundSelectTerms(sql: string): number;
|
|
58
|
+
/**
|
|
59
|
+
* Refuse a statement whose compound SELECT has more terms than D1 allows — see
|
|
60
|
+
* `LIMITS.compoundSelectTerms`. Called at `prepare()`, because a statement with nothing to
|
|
61
|
+
* bind never reaches {@link assertWithinLimits}.
|
|
62
|
+
*/
|
|
63
|
+
export declare function assertCompoundSelectTerms(sql: string): void;
|
|
40
64
|
/** Refuse a `batch()` bigger than one Worker invocation may issue. */
|
|
41
65
|
export declare function assertBatchSize(count: number): void;
|
|
42
66
|
/**
|
package/dist/server/d1/limits.js
CHANGED
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* · **100 bound parameters per query.** Any seed, import or bulk insert hits this. Use
|
|
11
11
|
* {@link chunkForBind}, which does the arithmetic rather than leaving it to a guess.
|
|
12
12
|
* · **2 MB per string / BLOB / row.** Nothing may store a media byte in D1.
|
|
13
|
+
* · **5 terms per compound SELECT** — `UNION`, `UNION ALL`, `INTERSECT`, `EXCEPT`, and
|
|
14
|
+
* every term of a recursive CTE counts. SQLite's own default is 500, so a sixth term is
|
|
15
|
+
* green here and `too many terms in compound SELECT` on D1. Measured against production
|
|
16
|
+
* D1 on 2026-09-26: five terms answer, six are refused, inside a CTE exactly as outside
|
|
17
|
+
* one. `family`'s "Both families" viewport was a six-term recursive CTE, and every read
|
|
18
|
+
* it scoped failed in production from the D1 cutover until then. Wrap extra seed terms in
|
|
19
|
+
* a subquery — `SELECT id FROM (… UNION …)` is ONE term of the outer compound.
|
|
13
20
|
*
|
|
14
21
|
* The limits are asserted by the LOCAL driver as well as the remote one, which is the
|
|
15
22
|
* whole point: a bulk insert that would fail in production fails on the laptop, at the
|
|
@@ -31,6 +38,8 @@ export const LIMITS = {
|
|
|
31
38
|
columnsPerTable: 100,
|
|
32
39
|
/** Wall-clock milliseconds one query may take. */
|
|
33
40
|
queryDurationMs: 30_000,
|
|
41
|
+
/** Terms in one compound SELECT, recursive CTEs included. SQLite's default is 500. */
|
|
42
|
+
compoundSelectTerms: 5,
|
|
34
43
|
};
|
|
35
44
|
const byteLength = (v) => {
|
|
36
45
|
if (typeof v === 'string')
|
|
@@ -61,6 +70,91 @@ export function assertWithinLimits(sql, params) {
|
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The most terms any ONE compound SELECT in `sql` has — `1` for a plain SELECT.
|
|
75
|
+
*
|
|
76
|
+
* Lexical, because `bun:sqlite` exposes no `sqlite3_limit`: string literals, quoted
|
|
77
|
+
* identifiers and comments are skipped, and every parenthesis opens its own count, since a
|
|
78
|
+
* parenthesised subquery or CTE body is a compound of its own. A `;` ends a statement.
|
|
79
|
+
* Multi-row `VALUES` is not counted — SQLite stopped counting it against this limit in 3.8.8.
|
|
80
|
+
*/
|
|
81
|
+
export function maxCompoundSelectTerms(sql) {
|
|
82
|
+
const stack = [0];
|
|
83
|
+
let max = 0;
|
|
84
|
+
let i = 0;
|
|
85
|
+
const n = sql.length;
|
|
86
|
+
const skipQuoted = (close) => {
|
|
87
|
+
i++;
|
|
88
|
+
while (i < n) {
|
|
89
|
+
if (sql[i] === close) {
|
|
90
|
+
// A doubled quote is an escaped one, inside the same literal.
|
|
91
|
+
if (sql[i + 1] === close && close !== ']') {
|
|
92
|
+
i += 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
i++;
|
|
100
|
+
};
|
|
101
|
+
while (i < n) {
|
|
102
|
+
const c = sql[i];
|
|
103
|
+
if (c === "'" || c === '"' || c === '`')
|
|
104
|
+
skipQuoted(c);
|
|
105
|
+
else if (c === '[')
|
|
106
|
+
skipQuoted(']');
|
|
107
|
+
else if (c === '-' && sql[i + 1] === '-') {
|
|
108
|
+
while (i < n && sql[i] !== '\n')
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
else if (c === '/' && sql[i + 1] === '*') {
|
|
112
|
+
const end = sql.indexOf('*/', i + 2);
|
|
113
|
+
i = end === -1 ? n : end + 2;
|
|
114
|
+
}
|
|
115
|
+
else if (c === '(') {
|
|
116
|
+
stack.push(0);
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else if (c === ')') {
|
|
120
|
+
if (stack.length > 1)
|
|
121
|
+
stack.pop();
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
else if (c === ';') {
|
|
125
|
+
stack.length = 1;
|
|
126
|
+
stack[0] = 0;
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
else if (/[A-Za-z_]/.test(c)) {
|
|
130
|
+
let j = i + 1;
|
|
131
|
+
while (j < n && /[A-Za-z0-9_$]/.test(sql[j]))
|
|
132
|
+
j++;
|
|
133
|
+
const word = sql.slice(i, j).toUpperCase();
|
|
134
|
+
if (word === 'UNION' || word === 'INTERSECT' || word === 'EXCEPT') {
|
|
135
|
+
const top = stack.length - 1;
|
|
136
|
+
stack[top] = stack[top] + 1;
|
|
137
|
+
max = Math.max(max, stack[top]);
|
|
138
|
+
}
|
|
139
|
+
i = j;
|
|
140
|
+
}
|
|
141
|
+
else
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
return max + 1;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Refuse a statement whose compound SELECT has more terms than D1 allows — see
|
|
148
|
+
* `LIMITS.compoundSelectTerms`. Called at `prepare()`, because a statement with nothing to
|
|
149
|
+
* bind never reaches {@link assertWithinLimits}.
|
|
150
|
+
*/
|
|
151
|
+
export function assertCompoundSelectTerms(sql) {
|
|
152
|
+
const terms = maxCompoundSelectTerms(sql);
|
|
153
|
+
if (terms > LIMITS.compoundSelectTerms) {
|
|
154
|
+
throw new D1LimitError('terms in one compound SELECT', terms, LIMITS.compoundSelectTerms, 'D1 answers `too many terms in compound SELECT`. Fold extra terms into a subquery — ' +
|
|
155
|
+
'`SELECT id FROM (a UNION b UNION c)` is one term of the outer compound.');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
64
158
|
/** Refuse a `batch()` bigger than one Worker invocation may issue. */
|
|
65
159
|
export function assertBatchSize(count) {
|
|
66
160
|
if (count > LIMITS.queriesPerInvocation) {
|
package/dist/server/d1/local.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* refused here exactly as it is refused remotely. The point of a local driver is to fail
|
|
14
14
|
* the same way, early and on a laptop.
|
|
15
15
|
*/
|
|
16
|
-
import { assertBatchSize, assertWithinLimits } from './limits.js';
|
|
16
|
+
import { assertBatchSize, assertCompoundSelectTerms, assertWithinLimits } from './limits.js';
|
|
17
17
|
import { D1UnsupportedError, } from './types.js';
|
|
18
18
|
import { isPureReadSql, makeMeta, normalizeBinds, normalizeRows, normalizeValue } from './values.js';
|
|
19
19
|
/** Shared clock, so `meta.duration` means the same thing on both sides of the seam. */
|
|
@@ -133,6 +133,7 @@ export function createLocalD1(db) {
|
|
|
133
133
|
return {
|
|
134
134
|
flavor: 'local',
|
|
135
135
|
prepare(sql) {
|
|
136
|
+
assertCompoundSelectTerms(sql);
|
|
136
137
|
return new LocalStatement(db, sql);
|
|
137
138
|
},
|
|
138
139
|
/**
|
package/dist/server/d1/remote.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* a real binding satisfies it without a cast, and the test suite can satisfy it with a
|
|
15
15
|
* fake that has no `workerd` in sight.
|
|
16
16
|
*/
|
|
17
|
-
import { assertBatchSize, assertWithinLimits } from './limits.js';
|
|
17
|
+
import { assertBatchSize, assertCompoundSelectTerms, assertWithinLimits } from './limits.js';
|
|
18
18
|
import { D1UnsupportedError, } from './types.js';
|
|
19
19
|
import { makeMeta, normalizeBinds, normalizeRows, normalizeValue } from './values.js';
|
|
20
20
|
/**
|
|
@@ -91,6 +91,7 @@ export function createRemoteD1(binding) {
|
|
|
91
91
|
return {
|
|
92
92
|
flavor: 'd1',
|
|
93
93
|
prepare(sql) {
|
|
94
|
+
assertCompoundSelectTerms(sql);
|
|
94
95
|
return new RemoteStatement(binding.prepare(sql), sql);
|
|
95
96
|
},
|
|
96
97
|
async batch(statements) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.39.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
2
|
import { describe, expect, test } from 'bun:test';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
assertBatchSize,
|
|
5
|
+
assertWithinLimits,
|
|
6
|
+
chunkForBind,
|
|
7
|
+
inArray,
|
|
8
|
+
LIMITS,
|
|
9
|
+
maxCompoundSelectTerms,
|
|
10
|
+
} from './limits.js';
|
|
4
11
|
import { createLocalD1 } from './local.js';
|
|
12
|
+
import { createRemoteD1 } from './remote.js';
|
|
5
13
|
import { D1LimitError } from './types.js';
|
|
6
14
|
|
|
7
15
|
describe('D1 limits are enforced on the LOCAL driver too', () => {
|
|
@@ -52,6 +60,57 @@ describe('D1 limits are enforced on the LOCAL driver too', () => {
|
|
|
52
60
|
expect(() => assertWithinLimits(sql, [])).toThrow(/SQL statement length/);
|
|
53
61
|
});
|
|
54
62
|
|
|
63
|
+
describe('five terms per compound SELECT — measured against production D1, 2026-09-26', () => {
|
|
64
|
+
const union = (terms: number): string =>
|
|
65
|
+
Array.from({ length: terms }, (_, k) => `SELECT ${k + 1}`).join(' UNION ');
|
|
66
|
+
|
|
67
|
+
test('six terms are refused at prepare(), before anything is bound', () => {
|
|
68
|
+
const db = createLocalD1(new Database(':memory:'));
|
|
69
|
+
expect(() => db.prepare(union(6))).toThrow(D1LimitError);
|
|
70
|
+
expect(() => db.prepare(union(6))).toThrow(/compound SELECT/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('five terms run — the cap is not off by one', async () => {
|
|
74
|
+
const db = createLocalD1(new Database(':memory:'));
|
|
75
|
+
const out = await db.prepare(union(5)).all();
|
|
76
|
+
expect(out.results).toHaveLength(5);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a recursive CTE counts its seed AND recursive terms — the shape that broke `family`', () => {
|
|
80
|
+
const db = createLocalD1(new Database(':memory:'));
|
|
81
|
+
const six = `WITH RECURSIVE r(x) AS (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
82
|
+
UNION SELECT x + 1 FROM r WHERE x < 3 UNION SELECT x + 2 FROM r WHERE x < 3) SELECT x FROM r`;
|
|
83
|
+
expect(() => db.prepare(six)).toThrow(D1LimitError);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a subquery is ONE term of the outer compound — the documented remedy passes', async () => {
|
|
87
|
+
const db = createLocalD1(new Database(':memory:'));
|
|
88
|
+
const folded = `WITH RECURSIVE r(x) AS (SELECT x FROM (SELECT 1 AS x UNION SELECT 2 UNION SELECT 3 UNION SELECT 4)
|
|
89
|
+
UNION SELECT x + 10 FROM r WHERE x < 3 UNION SELECT x + 20 FROM r WHERE x < 3) SELECT x FROM r`;
|
|
90
|
+
expect(maxCompoundSelectTerms(folded)).toBe(4);
|
|
91
|
+
const out = await db.prepare(folded).all();
|
|
92
|
+
expect(out.results.length).toBeGreaterThan(4);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('UNION inside a string, a quoted name or a comment is not a term', () => {
|
|
96
|
+
const sql = `SELECT 'a UNION b UNION c UNION d UNION e UNION f' AS "UNION UNION UNION UNION UNION"
|
|
97
|
+
-- UNION UNION UNION UNION UNION
|
|
98
|
+
/* UNION UNION UNION UNION UNION */ FROM [UNION UNION UNION UNION UNION]`;
|
|
99
|
+
expect(maxCompoundSelectTerms(sql)).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('separate statements and sibling subqueries are counted apart', () => {
|
|
103
|
+
expect(maxCompoundSelectTerms(`${union(3)}; ${union(3)}`)).toBe(3);
|
|
104
|
+
expect(maxCompoundSelectTerms(`SELECT * FROM (${union(3)}) JOIN (${union(3)})`)).toBe(3);
|
|
105
|
+
expect(maxCompoundSelectTerms(`SELECT 1 UNION ALL SELECT 2 INTERSECT SELECT 3 EXCEPT SELECT 4`)).toBe(4);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('the remote driver refuses too, so a Worker fails with the remedy rather than D1\'s bare error', () => {
|
|
109
|
+
const binding = { prepare: () => ({}) } as unknown as Parameters<typeof createRemoteD1>[0];
|
|
110
|
+
expect(() => createRemoteD1(binding).prepare(union(6))).toThrow(D1LimitError);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
55
114
|
test('a batch over 1,000 statements is refused', () => {
|
|
56
115
|
expect(() => assertBatchSize(1_001)).toThrow(D1LimitError);
|
|
57
116
|
expect(() => assertBatchSize(1_000)).not.toThrow();
|
package/src/server/d1/limits.ts
CHANGED
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* · **100 bound parameters per query.** Any seed, import or bulk insert hits this. Use
|
|
11
11
|
* {@link chunkForBind}, which does the arithmetic rather than leaving it to a guess.
|
|
12
12
|
* · **2 MB per string / BLOB / row.** Nothing may store a media byte in D1.
|
|
13
|
+
* · **5 terms per compound SELECT** — `UNION`, `UNION ALL`, `INTERSECT`, `EXCEPT`, and
|
|
14
|
+
* every term of a recursive CTE counts. SQLite's own default is 500, so a sixth term is
|
|
15
|
+
* green here and `too many terms in compound SELECT` on D1. Measured against production
|
|
16
|
+
* D1 on 2026-09-26: five terms answer, six are refused, inside a CTE exactly as outside
|
|
17
|
+
* one. `family`'s "Both families" viewport was a six-term recursive CTE, and every read
|
|
18
|
+
* it scoped failed in production from the D1 cutover until then. Wrap extra seed terms in
|
|
19
|
+
* a subquery — `SELECT id FROM (… UNION …)` is ONE term of the outer compound.
|
|
13
20
|
*
|
|
14
21
|
* The limits are asserted by the LOCAL driver as well as the remote one, which is the
|
|
15
22
|
* whole point: a bulk insert that would fail in production fails on the laptop, at the
|
|
@@ -33,6 +40,8 @@ export const LIMITS = {
|
|
|
33
40
|
columnsPerTable: 100,
|
|
34
41
|
/** Wall-clock milliseconds one query may take. */
|
|
35
42
|
queryDurationMs: 30_000,
|
|
43
|
+
/** Terms in one compound SELECT, recursive CTEs included. SQLite's default is 500. */
|
|
44
|
+
compoundSelectTerms: 5,
|
|
36
45
|
} as const;
|
|
37
46
|
|
|
38
47
|
const byteLength = (v: D1LikeBindable): number => {
|
|
@@ -78,6 +87,86 @@ export function assertWithinLimits(sql: string, params: readonly D1LikeBindable[
|
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The most terms any ONE compound SELECT in `sql` has — `1` for a plain SELECT.
|
|
92
|
+
*
|
|
93
|
+
* Lexical, because `bun:sqlite` exposes no `sqlite3_limit`: string literals, quoted
|
|
94
|
+
* identifiers and comments are skipped, and every parenthesis opens its own count, since a
|
|
95
|
+
* parenthesised subquery or CTE body is a compound of its own. A `;` ends a statement.
|
|
96
|
+
* Multi-row `VALUES` is not counted — SQLite stopped counting it against this limit in 3.8.8.
|
|
97
|
+
*/
|
|
98
|
+
export function maxCompoundSelectTerms(sql: string): number {
|
|
99
|
+
const stack: number[] = [0];
|
|
100
|
+
let max = 0;
|
|
101
|
+
let i = 0;
|
|
102
|
+
const n = sql.length;
|
|
103
|
+
const skipQuoted = (close: string): void => {
|
|
104
|
+
i++;
|
|
105
|
+
while (i < n) {
|
|
106
|
+
if (sql[i] === close) {
|
|
107
|
+
// A doubled quote is an escaped one, inside the same literal.
|
|
108
|
+
if (sql[i + 1] === close && close !== ']') {
|
|
109
|
+
i += 2;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
i++;
|
|
117
|
+
};
|
|
118
|
+
while (i < n) {
|
|
119
|
+
const c = sql[i] as string;
|
|
120
|
+
if (c === "'" || c === '"' || c === '`') skipQuoted(c);
|
|
121
|
+
else if (c === '[') skipQuoted(']');
|
|
122
|
+
else if (c === '-' && sql[i + 1] === '-') {
|
|
123
|
+
while (i < n && sql[i] !== '\n') i++;
|
|
124
|
+
} else if (c === '/' && sql[i + 1] === '*') {
|
|
125
|
+
const end = sql.indexOf('*/', i + 2);
|
|
126
|
+
i = end === -1 ? n : end + 2;
|
|
127
|
+
} else if (c === '(') {
|
|
128
|
+
stack.push(0);
|
|
129
|
+
i++;
|
|
130
|
+
} else if (c === ')') {
|
|
131
|
+
if (stack.length > 1) stack.pop();
|
|
132
|
+
i++;
|
|
133
|
+
} else if (c === ';') {
|
|
134
|
+
stack.length = 1;
|
|
135
|
+
stack[0] = 0;
|
|
136
|
+
i++;
|
|
137
|
+
} else if (/[A-Za-z_]/.test(c)) {
|
|
138
|
+
let j = i + 1;
|
|
139
|
+
while (j < n && /[A-Za-z0-9_$]/.test(sql[j] as string)) j++;
|
|
140
|
+
const word = sql.slice(i, j).toUpperCase();
|
|
141
|
+
if (word === 'UNION' || word === 'INTERSECT' || word === 'EXCEPT') {
|
|
142
|
+
const top = stack.length - 1;
|
|
143
|
+
stack[top] = (stack[top] as number) + 1;
|
|
144
|
+
max = Math.max(max, stack[top] as number);
|
|
145
|
+
}
|
|
146
|
+
i = j;
|
|
147
|
+
} else i++;
|
|
148
|
+
}
|
|
149
|
+
return max + 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Refuse a statement whose compound SELECT has more terms than D1 allows — see
|
|
154
|
+
* `LIMITS.compoundSelectTerms`. Called at `prepare()`, because a statement with nothing to
|
|
155
|
+
* bind never reaches {@link assertWithinLimits}.
|
|
156
|
+
*/
|
|
157
|
+
export function assertCompoundSelectTerms(sql: string): void {
|
|
158
|
+
const terms = maxCompoundSelectTerms(sql);
|
|
159
|
+
if (terms > LIMITS.compoundSelectTerms) {
|
|
160
|
+
throw new D1LimitError(
|
|
161
|
+
'terms in one compound SELECT',
|
|
162
|
+
terms,
|
|
163
|
+
LIMITS.compoundSelectTerms,
|
|
164
|
+
'D1 answers `too many terms in compound SELECT`. Fold extra terms into a subquery — ' +
|
|
165
|
+
'`SELECT id FROM (a UNION b UNION c)` is one term of the outer compound.',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
81
170
|
/** Refuse a `batch()` bigger than one Worker invocation may issue. */
|
|
82
171
|
export function assertBatchSize(count: number): void {
|
|
83
172
|
if (count > LIMITS.queriesPerInvocation) {
|
package/src/server/d1/local.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Database } from 'bun:sqlite';
|
|
18
|
-
import { assertBatchSize, assertWithinLimits } from './limits.js';
|
|
18
|
+
import { assertBatchSize, assertCompoundSelectTerms, assertWithinLimits } from './limits.js';
|
|
19
19
|
import {
|
|
20
20
|
type D1LikeBindable,
|
|
21
21
|
type D1LikeDatabase,
|
|
@@ -171,6 +171,7 @@ export function createLocalD1(db: Database): D1LikeDatabase {
|
|
|
171
171
|
flavor: 'local',
|
|
172
172
|
|
|
173
173
|
prepare(sql: string): D1LikeStatement {
|
|
174
|
+
assertCompoundSelectTerms(sql);
|
|
174
175
|
return new LocalStatement(db, sql);
|
|
175
176
|
},
|
|
176
177
|
|
package/src/server/d1/remote.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* fake that has no `workerd` in sight.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { assertBatchSize, assertWithinLimits } from './limits.js';
|
|
18
|
+
import { assertBatchSize, assertCompoundSelectTerms, assertWithinLimits } from './limits.js';
|
|
19
19
|
import {
|
|
20
20
|
type D1LikeBindable,
|
|
21
21
|
type D1LikeDatabase,
|
|
@@ -141,6 +141,7 @@ export function createRemoteD1(binding: D1BindingLike): D1LikeDatabase {
|
|
|
141
141
|
flavor: 'd1',
|
|
142
142
|
|
|
143
143
|
prepare(sql: string): D1LikeStatement {
|
|
144
|
+
assertCompoundSelectTerms(sql);
|
|
144
145
|
return new RemoteStatement(binding.prepare(sql), sql);
|
|
145
146
|
},
|
|
146
147
|
|