@edge-base/server 0.2.1 → 0.2.3

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.
Files changed (81) hide show
  1. package/admin-build/_app/immutable/chunks/{DjOEv9M9.js → A_3UuvCe.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{Dqk2TGNU.js → B-_-hJ9o.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{BFs_qStz.js → B5Nwfelm.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{B0QyxC2M.js → BxoNtYHK.js} +3 -3
  5. package/admin-build/_app/immutable/chunks/{BsFiK_FJ.js → CZ0TVkCa.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{k0CIJkw4.js → CzSAxmuj.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{D-x55wdW.js → DCKcAiQH.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{CSGrwS7E.js → DCvwWZrm.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/{BTJcQFEp.js → DRqPU3wD.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{CqUxCvs_.js → Dc1-6Po6.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{D755Tqat.js → DiyBpamp.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{BcIUK2sk.js → Dlty5069.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{BY07qVPA.js → DpVAayDG.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{BCKr7yKd.js → Du5vWVa2.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{m9QZTyVV.js → byv2rTy8.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{DnLqc9L1.js → nZvorU8i.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.BTsq3_xq.js → app.CfrmEXPD.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.BZ00WDYH.js → 0.Cn2BZ4da.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.RzSJ3yyr.js → 1.Dv4LX_Co.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.D-rsiquF.js → 10.DPVv3kat.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.l7-bgtFD.js → 11.CiCb6Ayu.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.Dkq0H7B5.js → 12.CIPyeekF.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.DtK_4oRz.js → 13.Z15Lt36e.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.BKo7-AMx.js → 14.s0l5bAq3.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.CQAj_6lq.js → 15.UwSSNO76.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.XVIG-Ffr.js → 16.qiD8i883.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.g6raZLCM.js → 17.Dy3dcSvu.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.IQz6a3T6.js → 18.DeXyPYsO.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.CAAZ8i8h.js → 19.CAbuyS6w.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.BPcX3KPj.js → 20.Bec0T7un.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.DuDYelMY.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.Br5AG_5Z.js → 22.CdVprrv2.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.KjbrdXoE.js → 23.Y8RzVLoF.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.C3n2-hgw.js → 24.CWhHYFBx.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.SFDSBzHd.js → 25.wCBplOVt.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.D95vui6E.js → 26.Cod_JRFK.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.FgLgdjwB.js → 27.BO2HVMu9.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.B9sYYm1F.js → 28.DxG-FBVQ.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.DyqZ_wbN.js → 29.CjGqWGvE.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.Bzo2yVIO.js → 3.By3_OmdZ.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.c1CiNwiS.js → 30.M_H7Htpq.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.CXty66Vh.js → 31.DEU18izM.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.BgQaXZ27.js → 4.DeYhKtzJ.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.BuJrHvxH.js → 5.9WLgxhrD.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.CkBBC94k.js → 6.BdT2i_dd.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.D2YBvNFM.js → 7.CHq0s4K6.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.D8qQWo_z.js → 8.DuvRw-XZ.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.BLDLX5hV.js → 9.C2Ub82wn.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -2
  53. package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
  54. package/src/__tests__/database-live-do.test.ts +50 -0
  55. package/src/__tests__/database-live-emitter.test.ts +116 -1
  56. package/src/__tests__/error-format.test.ts +63 -0
  57. package/src/__tests__/functions-context.test.ts +592 -35
  58. package/src/__tests__/meta-export-coverage.test.ts +1 -0
  59. package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
  60. package/src/__tests__/provider-aware-sql.test.ts +157 -0
  61. package/src/__tests__/room-auth-state-loss.test.ts +124 -0
  62. package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
  63. package/src/__tests__/sql-route.test.ts +187 -76
  64. package/src/durable-objects/database-live-do.ts +46 -1
  65. package/src/durable-objects/room-runtime-base.ts +26 -2
  66. package/src/durable-objects/rooms-do.ts +1 -1
  67. package/src/lib/admin-db-target.ts +30 -74
  68. package/src/lib/d1-handler.ts +45 -14
  69. package/src/lib/database-live-emitter.ts +57 -16
  70. package/src/lib/functions.ts +332 -454
  71. package/src/lib/internal-transport.ts +316 -0
  72. package/src/lib/plugin-migrations.ts +39 -39
  73. package/src/lib/postgres-handler.ts +39 -11
  74. package/src/lib/provider-aware-sql.ts +827 -0
  75. package/src/routes/admin.ts +7 -1
  76. package/src/routes/auth.ts +11 -12
  77. package/src/routes/sql.ts +51 -76
  78. package/src/routes/storage.ts +11 -12
  79. package/src/types.ts +2 -0
  80. package/admin-build/_app/immutable/entry/start.zXCirpgY.js +0 -1
  81. package/admin-build/_app/immutable/nodes/21.DoPabrY_.js +0 -1
@@ -0,0 +1,827 @@
1
+ import type { EdgeBaseConfig } from '@edge-base/shared';
2
+ import { executeD1Sql } from './d1-sql.js';
3
+ import { executeDoSql } from './do-sql.js';
4
+ import { getD1BindingName, shouldRouteToD1 } from './do-router.js';
5
+ import {
6
+ ensureLocalDevPostgresSchema,
7
+ getLocalDevPostgresExecOptions,
8
+ getProviderBindingName,
9
+ withPostgresConnection,
10
+ } from './postgres-executor.js';
11
+ import { ensurePgSchema } from './postgres-schema-init.js';
12
+ import type { Env } from '../types.js';
13
+
14
+ export interface ProviderAwareSqlOptions {
15
+ env?: Env;
16
+ config: EdgeBaseConfig;
17
+ databaseNamespace?: DurableObjectNamespace;
18
+ workerUrl?: string;
19
+ serviceKey?: string;
20
+ }
21
+
22
+ export interface ProviderAwareSqlResult {
23
+ columns: string[];
24
+ rows: Record<string, unknown>[];
25
+ rowCount: number;
26
+ }
27
+
28
+ function inferColumns(rows: Record<string, unknown>[]): string[] {
29
+ return rows.length > 0 ? Object.keys(rows[0] ?? {}) : [];
30
+ }
31
+
32
+ function normalizeRows(payload: {
33
+ rows?: unknown[];
34
+ items?: unknown[];
35
+ results?: unknown[];
36
+ }): Record<string, unknown>[] {
37
+ if (Array.isArray(payload.rows)) return payload.rows as Record<string, unknown>[];
38
+ if (Array.isArray(payload.items)) return payload.items as Record<string, unknown>[];
39
+ if (Array.isArray(payload.results)) return payload.results as Record<string, unknown>[];
40
+ return [];
41
+ }
42
+
43
+ function readDollarQuoteToken(query: string, index: number): string | null {
44
+ const match = query.slice(index).match(/^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/);
45
+ return match?.[0] ?? null;
46
+ }
47
+
48
+ function escapeRegExp(value: string): string {
49
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
+ }
51
+
52
+ // Keep this marker format in sync with @edge-base/core TableRef.sql().
53
+ const TABLE_SQL_PARAM_MARKER_PREFIX = '__EDGEBASE_SQL_PARAM_';
54
+ const TABLE_SQL_PARAM_MARKER_SUFFIX = '__';
55
+ const TABLE_SQL_PARAM_MARKER_RE = new RegExp(
56
+ `${escapeRegExp(TABLE_SQL_PARAM_MARKER_PREFIX)}(\\d+)${escapeRegExp(TABLE_SQL_PARAM_MARKER_SUFFIX)}`,
57
+ 'g',
58
+ );
59
+ const SQL_OPERATOR_CHARS = '+-*/<>=~!@#%^&|`?:';
60
+ const PRECEDING_WORDS_THAT_EXPECT_EXPRESSION = new Set([
61
+ 'ALL',
62
+ 'AND',
63
+ 'ANY',
64
+ 'ARRAY',
65
+ 'AS',
66
+ 'BETWEEN',
67
+ 'BY',
68
+ 'CASE',
69
+ 'DISTINCT',
70
+ 'ELSE',
71
+ 'EXISTS',
72
+ 'FILTER',
73
+ 'FROM',
74
+ 'GROUP',
75
+ 'HAVING',
76
+ 'ILIKE',
77
+ 'IN',
78
+ 'INTO',
79
+ 'IS',
80
+ 'JOIN',
81
+ 'LIKE',
82
+ 'LIMIT',
83
+ 'NOT',
84
+ 'OFFSET',
85
+ 'ON',
86
+ 'OR',
87
+ 'ORDER',
88
+ 'OVER',
89
+ 'PARTITION',
90
+ 'RETURNING',
91
+ 'SELECT',
92
+ 'SET',
93
+ 'SIMILAR',
94
+ 'THEN',
95
+ 'TO',
96
+ 'UNION',
97
+ 'UPDATE',
98
+ 'USING',
99
+ 'VALUES',
100
+ 'WHEN',
101
+ 'WHERE',
102
+ ]);
103
+ const FOLLOWING_WORDS_THAT_DO_NOT_START_EXPRESSION = new Set([
104
+ 'AND',
105
+ 'AS',
106
+ 'BY',
107
+ 'ELSE',
108
+ 'END',
109
+ 'EXCEPT',
110
+ 'FETCH',
111
+ 'FILTER',
112
+ 'FOR',
113
+ 'FROM',
114
+ 'GROUP',
115
+ 'HAVING',
116
+ 'INTERSECT',
117
+ 'INTO',
118
+ 'JOIN',
119
+ 'LIMIT',
120
+ 'NULLS',
121
+ 'OFFSET',
122
+ 'ON',
123
+ 'OR',
124
+ 'ORDER',
125
+ 'OVER',
126
+ 'PARTITION',
127
+ 'RETURNING',
128
+ 'THEN',
129
+ 'UNION',
130
+ 'USING',
131
+ 'WHEN',
132
+ 'WHERE',
133
+ 'WINDOW',
134
+ ]);
135
+
136
+ type SqlContextToken =
137
+ | { kind: 'word'; value: string }
138
+ | { kind: 'number' | 'param' | 'operator' }
139
+ | { kind: 'open-paren' | 'close-paren' | 'open-bracket' | 'close-bracket' }
140
+ | { kind: 'comma' | 'semicolon' | 'quote' | 'other' };
141
+
142
+ function isOperatorChar(char: string): boolean {
143
+ return SQL_OPERATOR_CHARS.includes(char);
144
+ }
145
+
146
+ function isWordChar(char: string): boolean {
147
+ return /[A-Za-z0-9_$]/.test(char);
148
+ }
149
+
150
+ function readPreviousSqlToken(query: string, index: number): SqlContextToken | null {
151
+ let i = index - 1;
152
+ while (i >= 0 && /\s/.test(query[i]!)) i--;
153
+ if (i < 0) return null;
154
+
155
+ const char = query[i]!;
156
+ if (char === '(') return { kind: 'open-paren' };
157
+ if (char === ')') return { kind: 'close-paren' };
158
+ if (char === '[') return { kind: 'open-bracket' };
159
+ if (char === ']') return { kind: 'close-bracket' };
160
+ if (char === ',') return { kind: 'comma' };
161
+ if (char === ';') return { kind: 'semicolon' };
162
+ if (char === "'" || char === '"') return { kind: 'quote' };
163
+
164
+ if (isOperatorChar(char)) {
165
+ let start = i;
166
+ while (start > 0 && isOperatorChar(query[start - 1]!)) start--;
167
+ return { kind: 'operator' };
168
+ }
169
+
170
+ if (isWordChar(char)) {
171
+ let start = i;
172
+ while (start > 0 && isWordChar(query[start - 1]!)) start--;
173
+ const token = query.slice(start, i + 1);
174
+ if (/^\$\d+$/.test(token)) return { kind: 'param' };
175
+ if (/^\d+(?:\.\d+)?$/.test(token)) return { kind: 'number' };
176
+ return { kind: 'word', value: token.toUpperCase() };
177
+ }
178
+
179
+ return { kind: 'other' };
180
+ }
181
+
182
+ function readNextSqlToken(query: string, index: number): SqlContextToken | null {
183
+ let i = index + 1;
184
+ while (i < query.length && /\s/.test(query[i]!)) i++;
185
+ if (i >= query.length) return null;
186
+
187
+ const char = query[i]!;
188
+ if (char === '(') return { kind: 'open-paren' };
189
+ if (char === ')') return { kind: 'close-paren' };
190
+ if (char === '[') return { kind: 'open-bracket' };
191
+ if (char === ']') return { kind: 'close-bracket' };
192
+ if (char === ',') return { kind: 'comma' };
193
+ if (char === ';') return { kind: 'semicolon' };
194
+ if (char === "'" || char === '"') return { kind: 'quote' };
195
+
196
+ if (isOperatorChar(char)) {
197
+ let end = i + 1;
198
+ while (end < query.length && isOperatorChar(query[end]!)) end++;
199
+ return { kind: 'operator' };
200
+ }
201
+
202
+ if (isWordChar(char)) {
203
+ let end = i + 1;
204
+ while (end < query.length && isWordChar(query[end]!)) end++;
205
+ const token = query.slice(i, end);
206
+ if (/^\$\d+$/.test(token)) return { kind: 'param' };
207
+ if (/^\d+(?:\.\d+)?$/.test(token)) return { kind: 'number' };
208
+ return { kind: 'word', value: token.toUpperCase() };
209
+ }
210
+
211
+ return { kind: 'other' };
212
+ }
213
+
214
+ function canTokenEndExpression(token: SqlContextToken | null): boolean {
215
+ if (!token) return false;
216
+ switch (token.kind) {
217
+ case 'word':
218
+ return !PRECEDING_WORDS_THAT_EXPECT_EXPRESSION.has(token.value);
219
+ case 'number':
220
+ case 'param':
221
+ case 'close-paren':
222
+ case 'close-bracket':
223
+ case 'quote':
224
+ return true;
225
+ default:
226
+ return false;
227
+ }
228
+ }
229
+
230
+ function canTokenStartExpression(token: SqlContextToken | null): boolean {
231
+ if (!token) return false;
232
+ switch (token.kind) {
233
+ case 'word':
234
+ return !FOLLOWING_WORDS_THAT_DO_NOT_START_EXPRESSION.has(token.value);
235
+ case 'number':
236
+ case 'param':
237
+ case 'open-paren':
238
+ case 'open-bracket':
239
+ case 'quote':
240
+ return true;
241
+ default:
242
+ return false;
243
+ }
244
+ }
245
+
246
+ function hasTaggedTemplateSqlMarkers(query: string): boolean {
247
+ return query.includes(TABLE_SQL_PARAM_MARKER_PREFIX);
248
+ }
249
+
250
+ function isIdentifierContinuationChar(char: string | undefined): boolean {
251
+ return !!char && /[A-Za-z0-9_$]/.test(char);
252
+ }
253
+
254
+ function isEscapedPostgresStringStart(query: string, quoteIndex: number): boolean {
255
+ const prefix = query[quoteIndex - 1];
256
+ if (prefix !== 'e' && prefix !== 'E') {
257
+ return false;
258
+ }
259
+ return !isIdentifierContinuationChar(query[quoteIndex - 2]);
260
+ }
261
+
262
+ function unescapeEscapedPostgresQuestionOperators(query: string): string {
263
+ let normalized = '';
264
+ let state:
265
+ | 'code'
266
+ | 'single'
267
+ | 'escaped-single'
268
+ | 'double'
269
+ | 'line-comment'
270
+ | 'block-comment'
271
+ | 'dollar-quote' = 'code';
272
+ let dollarQuoteToken = '';
273
+
274
+ for (let i = 0; i < query.length; i++) {
275
+ const char = query[i]!;
276
+ const next = query[i + 1];
277
+
278
+ if (state === 'single') {
279
+ normalized += char;
280
+ if (char === "'" && next === "'") {
281
+ normalized += next;
282
+ i++;
283
+ continue;
284
+ }
285
+ if (char === "'") state = 'code';
286
+ continue;
287
+ }
288
+
289
+ if (state === 'escaped-single') {
290
+ normalized += char;
291
+ if (char === '\\' && next) {
292
+ normalized += next;
293
+ i++;
294
+ continue;
295
+ }
296
+ if (char === "'" && next === "'") {
297
+ normalized += next;
298
+ i++;
299
+ continue;
300
+ }
301
+ if (char === "'") state = 'code';
302
+ continue;
303
+ }
304
+
305
+ if (state === 'double') {
306
+ normalized += char;
307
+ if (char === '"' && next === '"') {
308
+ normalized += next;
309
+ i++;
310
+ continue;
311
+ }
312
+ if (char === '"') state = 'code';
313
+ continue;
314
+ }
315
+
316
+ if (state === 'line-comment') {
317
+ normalized += char;
318
+ if (char === '\n') state = 'code';
319
+ continue;
320
+ }
321
+
322
+ if (state === 'block-comment') {
323
+ normalized += char;
324
+ if (char === '*' && next === '/') {
325
+ normalized += next;
326
+ i++;
327
+ state = 'code';
328
+ }
329
+ continue;
330
+ }
331
+
332
+ if (state === 'dollar-quote') {
333
+ if (query.startsWith(dollarQuoteToken, i)) {
334
+ normalized += dollarQuoteToken;
335
+ i += dollarQuoteToken.length - 1;
336
+ state = 'code';
337
+ continue;
338
+ }
339
+ normalized += char;
340
+ continue;
341
+ }
342
+
343
+ if (char === "'") {
344
+ normalized += char;
345
+ state = isEscapedPostgresStringStart(query, i) ? 'escaped-single' : 'single';
346
+ continue;
347
+ }
348
+ if (char === '\\' && next === '?') {
349
+ normalized += '?';
350
+ i++;
351
+ continue;
352
+ }
353
+ if (char === '"') {
354
+ normalized += char;
355
+ state = 'double';
356
+ continue;
357
+ }
358
+ if (char === '-' && next === '-') {
359
+ normalized += '--';
360
+ i++;
361
+ state = 'line-comment';
362
+ continue;
363
+ }
364
+ if (char === '/' && next === '*') {
365
+ normalized += '/*';
366
+ i++;
367
+ state = 'block-comment';
368
+ continue;
369
+ }
370
+ if (char === '$') {
371
+ const dollarQuote = readDollarQuoteToken(query, i);
372
+ if (dollarQuote) {
373
+ normalized += dollarQuote;
374
+ i += dollarQuote.length - 1;
375
+ state = 'dollar-quote';
376
+ dollarQuoteToken = dollarQuote;
377
+ continue;
378
+ }
379
+ }
380
+
381
+ normalized += char;
382
+ }
383
+
384
+ return normalized;
385
+ }
386
+
387
+ function replaceTaggedTemplateSqlMarkers(
388
+ query: string,
389
+ style: 'postgres' | 'question',
390
+ expectedParamCount = 0,
391
+ ): string {
392
+ let markerCount = 0;
393
+ const seenIndexes = new Set<number>();
394
+ const replaced = query.replace(TABLE_SQL_PARAM_MARKER_RE, (_match, indexText: string) => {
395
+ const index = Number(indexText);
396
+ if (!Number.isInteger(index) || index < 1) {
397
+ throw new Error('Invalid internal SQL parameter marker index.');
398
+ }
399
+ markerCount++;
400
+ seenIndexes.add(index);
401
+ return style === 'postgres' ? `$${index}` : '?';
402
+ });
403
+
404
+ if (markerCount === 0) {
405
+ return query;
406
+ }
407
+ if (style === 'postgres' && scanSqlPlaceholders(query).sawPostgresPlaceholder) {
408
+ throw new Error(
409
+ 'Cannot mix tagged template interpolation with PostgreSQL-style $n placeholders.',
410
+ );
411
+ }
412
+ if (markerCount !== expectedParamCount) {
413
+ throw new Error(
414
+ 'Internal SQL parameter markers do not match params length. Rebuild the tagged template query and try again.',
415
+ );
416
+ }
417
+ for (let index = 1; index <= expectedParamCount; index++) {
418
+ if (!seenIndexes.has(index)) {
419
+ throw new Error(
420
+ 'Internal SQL parameter markers are out of sequence. Rebuild the tagged template query and try again.',
421
+ );
422
+ }
423
+ }
424
+
425
+ return style === 'postgres' ? unescapeEscapedPostgresQuestionOperators(replaced) : replaced;
426
+ }
427
+
428
+ function isEscapedQuestionMark(query: string, index: number): boolean {
429
+ return query[index - 1] === '\\';
430
+ }
431
+
432
+ function isStandalonePostgresQuestionOperator(query: string, index: number): boolean {
433
+ const previousToken = readPreviousSqlToken(query, index);
434
+ const nextToken = readNextSqlToken(query, index);
435
+ return canTokenEndExpression(previousToken) && canTokenStartExpression(nextToken);
436
+ }
437
+
438
+ function isPrefixedPostgresQuestionOperator(query: string, index: number): boolean {
439
+ const nextChar = query[index + 1];
440
+ if (nextChar !== '|' && nextChar !== '&') {
441
+ return false;
442
+ }
443
+ return canTokenEndExpression(readPreviousSqlToken(query, index));
444
+ }
445
+
446
+ function isSuffixedPostgresQuestionOperator(query: string, index: number): boolean {
447
+ if (query[index - 1] !== '@') {
448
+ return false;
449
+ }
450
+ return (
451
+ canTokenEndExpression(readPreviousSqlToken(query, index - 1)) &&
452
+ canTokenStartExpression(readNextSqlToken(query, index))
453
+ );
454
+ }
455
+
456
+ function isQuestionBindPlaceholder(query: string, index: number): boolean {
457
+ if (isEscapedQuestionMark(query, index)) {
458
+ return false;
459
+ }
460
+ if (isSuffixedPostgresQuestionOperator(query, index)) {
461
+ return false;
462
+ }
463
+ if (isPrefixedPostgresQuestionOperator(query, index)) {
464
+ return false;
465
+ }
466
+ if (isStandalonePostgresQuestionOperator(query, index)) {
467
+ return false;
468
+ }
469
+ return true;
470
+ }
471
+
472
+ function scanSqlPlaceholders(query: string): {
473
+ questionPlaceholderIndexes: number[];
474
+ sawPostgresPlaceholder: boolean;
475
+ } {
476
+ const questionPlaceholderIndexes: number[] = [];
477
+ let sawPostgresPlaceholder = false;
478
+ let state: 'code' | 'single' | 'double' | 'line-comment' | 'block-comment' | 'dollar-quote' =
479
+ 'code';
480
+ let dollarQuoteToken = '';
481
+
482
+ for (let i = 0; i < query.length; i++) {
483
+ const char = query[i]!;
484
+ const next = query[i + 1];
485
+
486
+ if (state === 'single') {
487
+ if (char === "'" && next === "'") {
488
+ i++;
489
+ continue;
490
+ }
491
+ if (char === "'") state = 'code';
492
+ continue;
493
+ }
494
+
495
+ if (state === 'double') {
496
+ if (char === '"' && next === '"') {
497
+ i++;
498
+ continue;
499
+ }
500
+ if (char === '"') state = 'code';
501
+ continue;
502
+ }
503
+
504
+ if (state === 'line-comment') {
505
+ if (char === '\n') state = 'code';
506
+ continue;
507
+ }
508
+
509
+ if (state === 'block-comment') {
510
+ if (char === '*' && next === '/') {
511
+ i++;
512
+ state = 'code';
513
+ }
514
+ continue;
515
+ }
516
+
517
+ if (state === 'dollar-quote') {
518
+ if (query.startsWith(dollarQuoteToken, i)) {
519
+ i += dollarQuoteToken.length - 1;
520
+ state = 'code';
521
+ }
522
+ continue;
523
+ }
524
+
525
+ if (char === "'") {
526
+ state = 'single';
527
+ continue;
528
+ }
529
+ if (char === '\\' && next === '?') {
530
+ i++;
531
+ continue;
532
+ }
533
+ if (char === '"') {
534
+ state = 'double';
535
+ continue;
536
+ }
537
+ if (char === '-' && next === '-') {
538
+ i++;
539
+ state = 'line-comment';
540
+ continue;
541
+ }
542
+ if (char === '/' && next === '*') {
543
+ i++;
544
+ state = 'block-comment';
545
+ continue;
546
+ }
547
+ if (char === '$') {
548
+ const dollarQuote = readDollarQuoteToken(query, i);
549
+ if (dollarQuote) {
550
+ i += dollarQuote.length - 1;
551
+ state = 'dollar-quote';
552
+ dollarQuoteToken = dollarQuote;
553
+ continue;
554
+ }
555
+
556
+ const positionalMatch = query.slice(i).match(/^\$(\d+)/);
557
+ if (positionalMatch) {
558
+ sawPostgresPlaceholder = true;
559
+ i += positionalMatch[0].length - 1;
560
+ continue;
561
+ }
562
+ }
563
+ if (char === '?') {
564
+ if (isQuestionBindPlaceholder(query, i)) {
565
+ questionPlaceholderIndexes.push(i);
566
+ }
567
+ }
568
+ }
569
+
570
+ return { questionPlaceholderIndexes, sawPostgresPlaceholder };
571
+ }
572
+
573
+ export function normalizePostgresSqlPlaceholders(query: string, expectedParamCount = 0): string {
574
+ const { questionPlaceholderIndexes, sawPostgresPlaceholder } = scanSqlPlaceholders(query);
575
+ const questionPlaceholderCount = questionPlaceholderIndexes.length;
576
+ if (questionPlaceholderCount === 0) {
577
+ return unescapeEscapedPostgresQuestionOperators(query);
578
+ }
579
+ if (sawPostgresPlaceholder) {
580
+ throw new Error('Cannot mix ? placeholders with PostgreSQL-style $n placeholders.');
581
+ }
582
+ if (expectedParamCount === 0) {
583
+ return unescapeEscapedPostgresQuestionOperators(query);
584
+ }
585
+ if (questionPlaceholderCount !== expectedParamCount) {
586
+ throw new Error(
587
+ 'PostgreSQL raw SQL placeholders do not match params length. If your query uses the PostgreSQL ? operator, use $1, $2, ... for bind parameters.',
588
+ );
589
+ }
590
+
591
+ let normalized = '';
592
+ let paramIndex = 1;
593
+ let state: 'code' | 'single' | 'double' | 'line-comment' | 'block-comment' | 'dollar-quote' =
594
+ 'code';
595
+ let dollarQuoteToken = '';
596
+ const placeholderIndexes = new Set(questionPlaceholderIndexes);
597
+
598
+ for (let i = 0; i < query.length; i++) {
599
+ const char = query[i]!;
600
+ const next = query[i + 1];
601
+
602
+ if (state === 'single') {
603
+ normalized += char;
604
+ if (char === "'" && next === "'") {
605
+ normalized += next;
606
+ i++;
607
+ continue;
608
+ }
609
+ if (char === "'") state = 'code';
610
+ continue;
611
+ }
612
+
613
+ if (state === 'double') {
614
+ normalized += char;
615
+ if (char === '"' && next === '"') {
616
+ normalized += next;
617
+ i++;
618
+ continue;
619
+ }
620
+ if (char === '"') state = 'code';
621
+ continue;
622
+ }
623
+
624
+ if (state === 'line-comment') {
625
+ normalized += char;
626
+ if (char === '\n') state = 'code';
627
+ continue;
628
+ }
629
+
630
+ if (state === 'block-comment') {
631
+ normalized += char;
632
+ if (char === '*' && next === '/') {
633
+ normalized += next;
634
+ i++;
635
+ state = 'code';
636
+ }
637
+ continue;
638
+ }
639
+
640
+ if (state === 'dollar-quote') {
641
+ if (query.startsWith(dollarQuoteToken, i)) {
642
+ normalized += dollarQuoteToken;
643
+ i += dollarQuoteToken.length - 1;
644
+ state = 'code';
645
+ continue;
646
+ }
647
+ normalized += char;
648
+ continue;
649
+ }
650
+
651
+ if (char === "'") {
652
+ normalized += char;
653
+ state = 'single';
654
+ continue;
655
+ }
656
+ if (char === '\\' && next === '?') {
657
+ normalized += '?';
658
+ i++;
659
+ continue;
660
+ }
661
+ if (char === '"') {
662
+ normalized += char;
663
+ state = 'double';
664
+ continue;
665
+ }
666
+ if (char === '-' && next === '-') {
667
+ normalized += '--';
668
+ i++;
669
+ state = 'line-comment';
670
+ continue;
671
+ }
672
+ if (char === '/' && next === '*') {
673
+ normalized += '/*';
674
+ i++;
675
+ state = 'block-comment';
676
+ continue;
677
+ }
678
+ if (char === '$') {
679
+ const dollarQuote = readDollarQuoteToken(query, i);
680
+ if (dollarQuote) {
681
+ normalized += dollarQuote;
682
+ i += dollarQuote.length - 1;
683
+ state = 'dollar-quote';
684
+ dollarQuoteToken = dollarQuote;
685
+ continue;
686
+ }
687
+
688
+ const positionalMatch = query.slice(i).match(/^\$(\d+)/);
689
+ if (positionalMatch) {
690
+ normalized += positionalMatch[0];
691
+ i += positionalMatch[0].length - 1;
692
+ continue;
693
+ }
694
+ }
695
+ if (char === '?' && placeholderIndexes.has(i)) {
696
+ normalized += `$${paramIndex++}`;
697
+ continue;
698
+ }
699
+
700
+ normalized += char;
701
+ }
702
+
703
+ return normalized;
704
+ }
705
+
706
+ export async function executeProviderAwareSql(
707
+ opts: ProviderAwareSqlOptions,
708
+ namespace: string,
709
+ id: string | undefined,
710
+ query: string,
711
+ params: unknown[] = [],
712
+ ): Promise<ProviderAwareSqlResult> {
713
+ const dbBlock = opts.config.databases?.[namespace];
714
+ const usesTaggedTemplateMarkers = hasTaggedTemplateSqlMarkers(query);
715
+ const rewriteTaggedTemplateQuery = (style: 'postgres' | 'question') =>
716
+ usesTaggedTemplateMarkers ? replaceTaggedTemplateSqlMarkers(query, style, params.length) : query;
717
+ const isDynamicNamespace = !!(
718
+ dbBlock?.instance ||
719
+ dbBlock?.access?.canCreate ||
720
+ dbBlock?.access?.access
721
+ );
722
+ if (isDynamicNamespace && !id) {
723
+ throw new Error(
724
+ `admin.sqlProviderAware() requires an id for dynamic namespace '${namespace}'.`,
725
+ );
726
+ }
727
+
728
+ if (opts.env) {
729
+ if (!id && (dbBlock?.provider === 'neon' || dbBlock?.provider === 'postgres')) {
730
+ const bindingName = getProviderBindingName(namespace);
731
+ const envRecord = opts.env as unknown as Record<string, unknown>;
732
+ const hyperdrive = envRecord[bindingName] as { connectionString?: string } | undefined;
733
+ const envKey = dbBlock.connectionString ?? `${bindingName}_URL`;
734
+ const connectionString =
735
+ hyperdrive?.connectionString ?? (envRecord[envKey] as string | undefined);
736
+ if (!connectionString) {
737
+ throw new Error(`PostgreSQL connection '${envKey}' not found.`);
738
+ }
739
+
740
+ const normalizedSql = usesTaggedTemplateMarkers
741
+ ? rewriteTaggedTemplateQuery('postgres')
742
+ : normalizePostgresSqlPlaceholders(query, params.length);
743
+ const localDevOptions = getLocalDevPostgresExecOptions(
744
+ opts.env as unknown as Record<string, unknown>,
745
+ namespace,
746
+ );
747
+ if (localDevOptions) {
748
+ await ensureLocalDevPostgresSchema(localDevOptions);
749
+ }
750
+ return withPostgresConnection(
751
+ connectionString,
752
+ async (executor) => {
753
+ if (!localDevOptions) {
754
+ await ensurePgSchema(connectionString, namespace, dbBlock.tables ?? {}, executor);
755
+ }
756
+ return executor(normalizedSql, params);
757
+ },
758
+ localDevOptions,
759
+ );
760
+ }
761
+
762
+ if (!id && shouldRouteToD1(namespace, opts.config)) {
763
+ const bindingName = getD1BindingName(namespace);
764
+ const d1 = (opts.env as unknown as Record<string, unknown>)[bindingName] as
765
+ | D1Database
766
+ | undefined;
767
+ if (!d1) {
768
+ throw new Error(`D1 binding '${bindingName}' not found.`);
769
+ }
770
+ const result = await executeD1Sql(d1, rewriteTaggedTemplateQuery('question'), params);
771
+ const rows = result.rows;
772
+ return {
773
+ columns: inferColumns(rows),
774
+ rows,
775
+ rowCount: result.rowCount,
776
+ };
777
+ }
778
+
779
+ if (opts.databaseNamespace) {
780
+ const rows = await executeDoSql({
781
+ databaseNamespace: opts.databaseNamespace,
782
+ namespace,
783
+ id,
784
+ query: rewriteTaggedTemplateQuery('question'),
785
+ params,
786
+ internal: true,
787
+ });
788
+ return {
789
+ columns: inferColumns(rows),
790
+ rows,
791
+ rowCount: rows.length,
792
+ };
793
+ }
794
+ }
795
+
796
+ if (opts.workerUrl && opts.serviceKey) {
797
+ const res = await fetch(`${opts.workerUrl}/api/sql`, {
798
+ method: 'POST',
799
+ headers: {
800
+ 'Content-Type': 'application/json',
801
+ 'X-EdgeBase-Service-Key': opts.serviceKey,
802
+ },
803
+ body: JSON.stringify({ namespace, id, sql: rewriteTaggedTemplateQuery('question'), params }),
804
+ });
805
+ if (!res.ok) {
806
+ const err = (await res.json().catch(() => ({ message: 'SQL execution failed' }))) as {
807
+ message?: string;
808
+ };
809
+ throw new Error(err.message || 'SQL execution failed');
810
+ }
811
+ const data = (await res.json()) as {
812
+ rows?: unknown[];
813
+ items?: unknown[];
814
+ results?: unknown[];
815
+ columns?: string[];
816
+ rowCount?: number;
817
+ };
818
+ const rows = normalizeRows(data);
819
+ return {
820
+ columns: Array.isArray(data.columns) ? data.columns.map(String) : inferColumns(rows),
821
+ rows,
822
+ rowCount: typeof data.rowCount === 'number' ? data.rowCount : rows.length,
823
+ };
824
+ }
825
+
826
+ throw new Error('admin.sqlProviderAware() requires env or workerUrl.');
827
+ }