@visorcraft/mongreldb-kit 0.55.0 → 0.57.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/db.d.ts +31 -8
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +159 -27
- package/dist/db.js.map +1 -1
- package/dist/errors.d.ts +127 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +217 -26
- package/dist/errors.js.map +1 -1
- package/dist/migrate.js +1 -1
- package/dist/remote.d.ts +108 -3
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +2008 -61
- package/dist/remote.js.map +1 -1
- package/package.json +2 -2
package/dist/remote.js
CHANGED
|
@@ -1,10 +1,1126 @@
|
|
|
1
1
|
import { RemoteDatabase as NativeRemoteDatabase } from '@visorcraft/mongreldb/native.js';
|
|
2
2
|
import { tableFromIPC, tableFromJSON } from 'apache-arrow';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
|
-
import {
|
|
4
|
+
import { CommitOutcomeError, CapabilityUnsupportedError, KitError, QueryCancelledError, QueryIdConflictError, QueryExecutionError, QueryOutcomeUnknownError, RemoteProtocolError, ResultLimitExceededError, SerializationError, QueryTimeoutError, TransactionAbortedError } from './errors.js';
|
|
5
5
|
import { procedureJson } from './procedure.js';
|
|
6
6
|
import { triggerJson } from './trigger.js';
|
|
7
7
|
import { createVirtualTableSql, dropVirtualTableSql } from './external.js';
|
|
8
|
+
const SQL_RECOVERY_WINDOW_MS = 2_000;
|
|
9
|
+
const SQL_RECOVERY_REQUEST_TIMEOUT_MS = 250;
|
|
10
|
+
const SQL_RECOVERY_POLL_INTERVAL_MS = 25;
|
|
11
|
+
const MAX_U64 = 18446744073709551615n;
|
|
12
|
+
const MAX_CONTROL_JSON_RESPONSE_BYTES = 1024 * 1024;
|
|
13
|
+
const MAX_PAGE_JSON_RESPONSE_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
const TERMINAL_SQL_STATES = new Set(['completed', 'failed', 'cancelled', 'pre_cancelled']);
|
|
15
|
+
function containsHttpHeaderControl(value) {
|
|
16
|
+
return /[\u0000-\u001f\u007f]/u.test(value);
|
|
17
|
+
}
|
|
18
|
+
function parseJsonStrict(text) {
|
|
19
|
+
let offset = 0;
|
|
20
|
+
const fail = (message) => {
|
|
21
|
+
throw new SyntaxError(`${message} at JSON offset ${offset}`);
|
|
22
|
+
};
|
|
23
|
+
const whitespace = () => {
|
|
24
|
+
while (offset < text.length
|
|
25
|
+
&& [' ', '\t', '\r', '\n'].includes(text[offset]))
|
|
26
|
+
offset += 1;
|
|
27
|
+
};
|
|
28
|
+
const string = () => {
|
|
29
|
+
if (text[offset] !== '"')
|
|
30
|
+
fail('expected string');
|
|
31
|
+
const start = offset++;
|
|
32
|
+
while (offset < text.length) {
|
|
33
|
+
const char = text[offset++];
|
|
34
|
+
if (char === '"')
|
|
35
|
+
return JSON.parse(text.slice(start, offset));
|
|
36
|
+
if (char.charCodeAt(0) < 0x20)
|
|
37
|
+
fail('unescaped control character');
|
|
38
|
+
if (char !== '\\')
|
|
39
|
+
continue;
|
|
40
|
+
if (offset >= text.length)
|
|
41
|
+
fail('unterminated escape');
|
|
42
|
+
const escape = text[offset++];
|
|
43
|
+
if (escape === 'u') {
|
|
44
|
+
if (!/^[0-9a-fA-F]{4}$/.test(text.slice(offset, offset + 4))) {
|
|
45
|
+
fail('invalid unicode escape');
|
|
46
|
+
}
|
|
47
|
+
offset += 4;
|
|
48
|
+
}
|
|
49
|
+
else if (!'"\\/bfnrt'.includes(escape)) {
|
|
50
|
+
fail('invalid string escape');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return fail('unterminated string');
|
|
54
|
+
};
|
|
55
|
+
const value = () => {
|
|
56
|
+
whitespace();
|
|
57
|
+
const char = text[offset];
|
|
58
|
+
if (char === '"')
|
|
59
|
+
return string();
|
|
60
|
+
if (char === '{') {
|
|
61
|
+
offset += 1;
|
|
62
|
+
const object = Object.create(null);
|
|
63
|
+
const keys = new Set();
|
|
64
|
+
whitespace();
|
|
65
|
+
if (text[offset] === '}') {
|
|
66
|
+
offset += 1;
|
|
67
|
+
return object;
|
|
68
|
+
}
|
|
69
|
+
while (true) {
|
|
70
|
+
whitespace();
|
|
71
|
+
const key = string();
|
|
72
|
+
if (keys.has(key))
|
|
73
|
+
fail(`duplicate JSON object key ${JSON.stringify(key)}`);
|
|
74
|
+
keys.add(key);
|
|
75
|
+
whitespace();
|
|
76
|
+
if (text[offset++] !== ':')
|
|
77
|
+
fail('expected colon');
|
|
78
|
+
object[key] = value();
|
|
79
|
+
whitespace();
|
|
80
|
+
const separator = text[offset++];
|
|
81
|
+
if (separator === '}')
|
|
82
|
+
return object;
|
|
83
|
+
if (separator !== ',')
|
|
84
|
+
fail('expected comma or object end');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (char === '[') {
|
|
88
|
+
offset += 1;
|
|
89
|
+
const array = [];
|
|
90
|
+
whitespace();
|
|
91
|
+
if (text[offset] === ']') {
|
|
92
|
+
offset += 1;
|
|
93
|
+
return array;
|
|
94
|
+
}
|
|
95
|
+
while (true) {
|
|
96
|
+
array.push(value());
|
|
97
|
+
whitespace();
|
|
98
|
+
const separator = text[offset++];
|
|
99
|
+
if (separator === ']')
|
|
100
|
+
return array;
|
|
101
|
+
if (separator !== ',')
|
|
102
|
+
fail('expected comma or array end');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const [literal, parsed] of [['true', true], ['false', false], ['null', null]]) {
|
|
106
|
+
if (text.startsWith(literal, offset)) {
|
|
107
|
+
offset += literal.length;
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const number = text.slice(offset).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u)?.[0];
|
|
112
|
+
if (!number)
|
|
113
|
+
return fail('expected JSON value');
|
|
114
|
+
offset += number.length;
|
|
115
|
+
const parsed = Number(number);
|
|
116
|
+
if (!Number.isFinite(parsed))
|
|
117
|
+
return fail('non-finite JSON number');
|
|
118
|
+
return parsed;
|
|
119
|
+
};
|
|
120
|
+
const parsed = value();
|
|
121
|
+
whitespace();
|
|
122
|
+
if (offset !== text.length)
|
|
123
|
+
fail('trailing JSON data');
|
|
124
|
+
return parsed;
|
|
125
|
+
}
|
|
126
|
+
async function responseBytesBounded(response, limit) {
|
|
127
|
+
const declared = response.headers.get('content-length');
|
|
128
|
+
if (declared !== null) {
|
|
129
|
+
if (!/^\d+$/u.test(declared) || Number(declared) > limit) {
|
|
130
|
+
throw new SyntaxError(`HTTP response exceeded ${limit} bytes`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!response.body)
|
|
134
|
+
return new Uint8Array();
|
|
135
|
+
const reader = response.body.getReader();
|
|
136
|
+
const chunks = [];
|
|
137
|
+
let length = 0;
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done)
|
|
142
|
+
break;
|
|
143
|
+
length += value.byteLength;
|
|
144
|
+
if (length > limit) {
|
|
145
|
+
await reader.cancel().catch(() => undefined);
|
|
146
|
+
throw new SyntaxError(`HTTP response exceeded ${limit} bytes`);
|
|
147
|
+
}
|
|
148
|
+
chunks.push(value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
reader.releaseLock();
|
|
153
|
+
}
|
|
154
|
+
const bytes = new Uint8Array(length);
|
|
155
|
+
let offset = 0;
|
|
156
|
+
for (const chunk of chunks) {
|
|
157
|
+
bytes.set(chunk, offset);
|
|
158
|
+
offset += chunk.byteLength;
|
|
159
|
+
}
|
|
160
|
+
return bytes;
|
|
161
|
+
}
|
|
162
|
+
async function responseTextBounded(response, limit) {
|
|
163
|
+
try {
|
|
164
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(await responseBytesBounded(response, limit));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new SyntaxError('HTTP response was not valid UTF-8');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function responseJsonStrict(response, limit = MAX_CONTROL_JSON_RESPONSE_BYTES) {
|
|
171
|
+
return parseJsonStrict(await responseTextBounded(response, limit));
|
|
172
|
+
}
|
|
173
|
+
function preExecutionCancellationError(queryId, outcome) {
|
|
174
|
+
const metadata = {
|
|
175
|
+
cancelOutcome: outcome,
|
|
176
|
+
cancellationReason: 'client_request',
|
|
177
|
+
retryable: false,
|
|
178
|
+
serverState: 'pre_cancelled'
|
|
179
|
+
};
|
|
180
|
+
if (outcome === 'not_found') {
|
|
181
|
+
return new KitError('SQL pre-registration cancellation was not found by the server', 'QUERY_NOT_FOUND');
|
|
182
|
+
}
|
|
183
|
+
if (outcome === 'too_late') {
|
|
184
|
+
return new KitError('SQL cancellation arrived after the commit fence', 'CANCEL_TOO_LATE');
|
|
185
|
+
}
|
|
186
|
+
if (outcome === 'already_finished') {
|
|
187
|
+
return new KitError('SQL query ID already belongs to a finished query', 'QUERY_ALREADY_FINISHED');
|
|
188
|
+
}
|
|
189
|
+
return new QueryCancelledError(queryId, 'SQL query cancelled before execution', { committed: false, committedStatements: 0, completedStatements: 0, statementIndex: 0 }, metadata);
|
|
190
|
+
}
|
|
191
|
+
function isNonNegativeInteger(value) {
|
|
192
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
193
|
+
}
|
|
194
|
+
function isOptionalNonNegativeInteger(value) {
|
|
195
|
+
return value === undefined || value === null || isNonNegativeInteger(value);
|
|
196
|
+
}
|
|
197
|
+
function hasOnlyKeys(value, allowed) {
|
|
198
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
199
|
+
}
|
|
200
|
+
function hasAllKeys(value, required) {
|
|
201
|
+
return required.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
202
|
+
}
|
|
203
|
+
function normalizeQueryId(value) {
|
|
204
|
+
if (typeof value !== 'string' || !/^[0-9a-f]{32}$/i.test(value)) {
|
|
205
|
+
throw new TypeError('queryId must be exactly 32 hexadecimal characters');
|
|
206
|
+
}
|
|
207
|
+
return value.toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
function isOptionalEpoch(value, exactText) {
|
|
210
|
+
if (exactText !== undefined && exactText !== null
|
|
211
|
+
&& (typeof exactText !== 'string' || !/^\d+$/.test(exactText)))
|
|
212
|
+
return false;
|
|
213
|
+
if (typeof exactText === 'string'
|
|
214
|
+
&& (BigInt(exactText).toString() !== exactText || BigInt(exactText) > MAX_U64))
|
|
215
|
+
return false;
|
|
216
|
+
if (value === undefined || value === null)
|
|
217
|
+
return true;
|
|
218
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
219
|
+
return false;
|
|
220
|
+
if (typeof exactText === 'string')
|
|
221
|
+
return BigInt(value) === BigInt(exactText);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
function receiptEpoch(value, exactText) {
|
|
225
|
+
if (typeof exactText === 'string')
|
|
226
|
+
return BigInt(exactText);
|
|
227
|
+
return typeof value === 'number' ? BigInt(value) : undefined;
|
|
228
|
+
}
|
|
229
|
+
function isQueryNotFoundResponse(value, queryId) {
|
|
230
|
+
if (value === null || typeof value !== 'object')
|
|
231
|
+
return false;
|
|
232
|
+
const body = value;
|
|
233
|
+
const outcome = body.outcome;
|
|
234
|
+
const error = body.error;
|
|
235
|
+
const nullable = [
|
|
236
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
237
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
238
|
+
'statement_index'
|
|
239
|
+
];
|
|
240
|
+
return hasOnlyKeys(body, [
|
|
241
|
+
'query_id', 'status', 'terminal_state', ...nullable, 'cancel_outcome',
|
|
242
|
+
'cancellation_reason', 'retryable', 'server_state', 'outcome', 'error'
|
|
243
|
+
]) && hasAllKeys(body, [
|
|
244
|
+
'query_id', 'status', 'terminal_state', ...nullable, 'cancel_outcome',
|
|
245
|
+
'cancellation_reason', 'retryable', 'server_state', 'outcome', 'error'
|
|
246
|
+
])
|
|
247
|
+
&& outcome !== null && typeof outcome === 'object'
|
|
248
|
+
&& hasOnlyKeys(outcome, [...nullable, 'serialization'])
|
|
249
|
+
&& hasAllKeys(outcome, [...nullable, 'serialization'])
|
|
250
|
+
&& error !== null && typeof error === 'object'
|
|
251
|
+
&& hasOnlyKeys(error, ['code', 'message', 'query_id', 'committed', 'retryable'])
|
|
252
|
+
&& hasAllKeys(error, ['code', 'message', 'query_id', 'committed', 'retryable'])
|
|
253
|
+
&& body.query_id === queryId && body.status === 'unknown'
|
|
254
|
+
&& body.terminal_state === null && body.cancel_outcome === 'not_found'
|
|
255
|
+
&& body.cancellation_reason === null && body.retryable === false
|
|
256
|
+
&& body.server_state === 'not_found'
|
|
257
|
+
&& nullable.every((field) => body[field] === null && outcome[field] === null)
|
|
258
|
+
&& outcome.serialization === 'unknown'
|
|
259
|
+
&& error.code === 'QUERY_NOT_FOUND'
|
|
260
|
+
&& typeof error.message === 'string' && error.message.length > 0
|
|
261
|
+
&& error.query_id === queryId && error.committed === null && error.retryable === false;
|
|
262
|
+
}
|
|
263
|
+
function isRemoteSqlWriteReceipt(value, queryId, expectedOriginalQueryId) {
|
|
264
|
+
if (value === null || typeof value !== 'object')
|
|
265
|
+
return false;
|
|
266
|
+
const body = value;
|
|
267
|
+
const outcome = body.outcome;
|
|
268
|
+
if (outcome === null || typeof outcome !== 'object')
|
|
269
|
+
return false;
|
|
270
|
+
const durable = outcome;
|
|
271
|
+
const terminal = body.terminal_error;
|
|
272
|
+
if (!hasOnlyKeys(body, [
|
|
273
|
+
'query_id', 'original_query_id', 'status', 'terminal_state', 'server_state',
|
|
274
|
+
'cancel_outcome', 'cancellation_reason', 'committed', 'committed_statements',
|
|
275
|
+
'last_commit_epoch', 'last_commit_epoch_text', 'first_commit_statement_index',
|
|
276
|
+
'last_commit_statement_index', 'completed_statements', 'statement_index', 'retryable',
|
|
277
|
+
'idempotency_replayed', 'idempotency_persisted', 'idempotency_expires_at_ms',
|
|
278
|
+
'outcome', 'terminal_error'
|
|
279
|
+
]) || !hasOnlyKeys(durable, [
|
|
280
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
281
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
282
|
+
'statement_index', 'serialization'
|
|
283
|
+
]) || !hasAllKeys(durable, [
|
|
284
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
285
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
286
|
+
'statement_index', 'serialization'
|
|
287
|
+
]))
|
|
288
|
+
return false;
|
|
289
|
+
if (terminal !== undefined && terminal !== null
|
|
290
|
+
&& (typeof terminal !== 'object'
|
|
291
|
+
|| !hasOnlyKeys(terminal, ['code', 'category'])))
|
|
292
|
+
return false;
|
|
293
|
+
const statusCommitted = body.status === 'completed'
|
|
294
|
+
? false
|
|
295
|
+
: ['committed', 'committed_with_error', 'partially_committed',
|
|
296
|
+
'cancelled_after_commit', 'deadline_after_commit'].includes(body.status)
|
|
297
|
+
? true
|
|
298
|
+
: undefined;
|
|
299
|
+
const shapeValid = body.query_id === queryId
|
|
300
|
+
&& typeof body.original_query_id === 'string'
|
|
301
|
+
&& /^[0-9a-f]{32}$/i.test(body.original_query_id)
|
|
302
|
+
&& typeof body.status === 'string'
|
|
303
|
+
&& body.status.trim().length > 0
|
|
304
|
+
&& (body.terminal_state === undefined || body.terminal_state === null
|
|
305
|
+
|| body.terminal_state === body.status)
|
|
306
|
+
&& (body.server_state === undefined || body.server_state === null
|
|
307
|
+
|| ['completed', 'failed', 'cancelled'].includes(body.server_state))
|
|
308
|
+
&& (body.cancel_outcome === undefined || body.cancel_outcome === null
|
|
309
|
+
|| body.cancel_outcome === 'already_finished')
|
|
310
|
+
&& (body.cancellation_reason === undefined || body.cancellation_reason === null
|
|
311
|
+
|| ['none', 'client_request', 'deadline', 'client_disconnected',
|
|
312
|
+
'session_closed', 'server_shutdown'].includes(body.cancellation_reason))
|
|
313
|
+
&& typeof body.committed === 'boolean'
|
|
314
|
+
&& isNonNegativeInteger(body.committed_statements)
|
|
315
|
+
&& body.committed === durable.committed
|
|
316
|
+
&& body.committed_statements === durable.committed_statements
|
|
317
|
+
&& typeof body.retryable === 'boolean'
|
|
318
|
+
&& typeof body.idempotency_replayed === 'boolean'
|
|
319
|
+
&& typeof body.idempotency_persisted === 'boolean'
|
|
320
|
+
&& isNonNegativeInteger(body.idempotency_expires_at_ms)
|
|
321
|
+
&& typeof durable.committed === 'boolean'
|
|
322
|
+
&& isNonNegativeInteger(durable.committed_statements)
|
|
323
|
+
&& ['not_started', 'in_progress', 'succeeded', 'failed', 'unknown']
|
|
324
|
+
.includes(durable.serialization)
|
|
325
|
+
&& isOptionalEpoch(body.last_commit_epoch, body.last_commit_epoch_text)
|
|
326
|
+
&& isOptionalNonNegativeInteger(body.first_commit_statement_index)
|
|
327
|
+
&& isOptionalNonNegativeInteger(body.last_commit_statement_index)
|
|
328
|
+
&& isNonNegativeInteger(body.completed_statements)
|
|
329
|
+
&& isNonNegativeInteger(body.statement_index)
|
|
330
|
+
&& isOptionalEpoch(durable.last_commit_epoch, durable.last_commit_epoch_text)
|
|
331
|
+
&& isOptionalNonNegativeInteger(durable.first_commit_statement_index)
|
|
332
|
+
&& isOptionalNonNegativeInteger(durable.last_commit_statement_index)
|
|
333
|
+
&& (terminal === undefined || terminal === null
|
|
334
|
+
|| typeof terminal === 'object'
|
|
335
|
+
&& typeof terminal.code === 'string'
|
|
336
|
+
&& terminal.code.trim().length > 0
|
|
337
|
+
&& ['cancellation', 'deadline', 'result_limit', 'serialization', 'execution']
|
|
338
|
+
.includes(terminal.category));
|
|
339
|
+
if (!shapeValid)
|
|
340
|
+
return false;
|
|
341
|
+
if (statusCommitted === undefined || body.committed !== statusCommitted)
|
|
342
|
+
return false;
|
|
343
|
+
if (body.server_state != null) {
|
|
344
|
+
const expectedState = ['completed', 'committed'].includes(body.status)
|
|
345
|
+
? 'completed'
|
|
346
|
+
: ['committed_with_error', 'partially_committed'].includes(body.status)
|
|
347
|
+
? 'failed' : 'cancelled';
|
|
348
|
+
if (body.server_state !== expectedState)
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
if (body.cancellation_reason != null) {
|
|
352
|
+
const reasonMatches = body.status === 'cancelled_after_commit'
|
|
353
|
+
? !['none', 'deadline'].includes(body.cancellation_reason)
|
|
354
|
+
: body.status === 'deadline_after_commit'
|
|
355
|
+
? body.cancellation_reason === 'deadline'
|
|
356
|
+
: body.cancellation_reason === 'none';
|
|
357
|
+
if (!reasonMatches)
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const terminalRecord = terminal;
|
|
361
|
+
if (terminalRecord != null
|
|
362
|
+
&& (((terminalRecord.category === 'cancellation')
|
|
363
|
+
!== ['QUERY_CANCELLED', 'QUERY_CANCELLED_AFTER_COMMIT'].includes(terminalRecord.code))
|
|
364
|
+
|| ((terminalRecord.category === 'deadline')
|
|
365
|
+
!== ['DEADLINE_EXCEEDED', 'DEADLINE_AFTER_COMMIT'].includes(terminalRecord.code))
|
|
366
|
+
|| ((terminalRecord.category === 'result_limit')
|
|
367
|
+
!== (terminalRecord.code === 'RESULT_LIMIT_EXCEEDED'))
|
|
368
|
+
|| ((terminalRecord.category === 'serialization')
|
|
369
|
+
!== ['SERIALIZATION_FAILED', 'SERIALIZATION_FAILED_AFTER_COMMIT']
|
|
370
|
+
.includes(terminalRecord.code))))
|
|
371
|
+
return false;
|
|
372
|
+
const terminalMatches = body.status === 'completed' || body.status === 'committed'
|
|
373
|
+
? terminal == null
|
|
374
|
+
: body.status === 'cancelled_after_commit'
|
|
375
|
+
? terminalRecord?.code === 'QUERY_CANCELLED_AFTER_COMMIT'
|
|
376
|
+
&& terminalRecord.category === 'cancellation'
|
|
377
|
+
: body.status === 'deadline_after_commit'
|
|
378
|
+
? terminalRecord?.code === 'DEADLINE_AFTER_COMMIT'
|
|
379
|
+
&& terminalRecord.category === 'deadline'
|
|
380
|
+
: ['committed_with_error', 'partially_committed'].includes(body.status)
|
|
381
|
+
&& terminal != null;
|
|
382
|
+
if (!terminalMatches)
|
|
383
|
+
return false;
|
|
384
|
+
const topEpoch = receiptEpoch(body.last_commit_epoch, body.last_commit_epoch_text);
|
|
385
|
+
const outcomeEpoch = receiptEpoch(durable.last_commit_epoch, durable.last_commit_epoch_text);
|
|
386
|
+
if (topEpoch !== outcomeEpoch)
|
|
387
|
+
return false;
|
|
388
|
+
const topFirst = body.first_commit_statement_index;
|
|
389
|
+
const outcomeFirst = durable.first_commit_statement_index;
|
|
390
|
+
const topLast = body.last_commit_statement_index;
|
|
391
|
+
const outcomeLast = durable.last_commit_statement_index;
|
|
392
|
+
if ((topFirst ?? null) !== (outcomeFirst ?? null) || (topLast ?? null) !== (outcomeLast ?? null)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
if (body.completed_statements !== durable.completed_statements
|
|
396
|
+
|| body.statement_index !== durable.statement_index)
|
|
397
|
+
return false;
|
|
398
|
+
if (topFirst != null && topLast != null && topFirst > topLast)
|
|
399
|
+
return false;
|
|
400
|
+
if (body.committed) {
|
|
401
|
+
if (body.committed_statements === 0 || topEpoch === undefined
|
|
402
|
+
|| body.last_commit_epoch_text == null || durable.last_commit_epoch_text == null
|
|
403
|
+
|| topFirst == null || topLast == null)
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
else if (body.committed_statements !== 0 || topEpoch !== undefined
|
|
407
|
+
|| topFirst != null || topLast != null)
|
|
408
|
+
return false;
|
|
409
|
+
if (topFirst != null && topLast != null
|
|
410
|
+
&& (body.committed_statements > topLast - topFirst + 1
|
|
411
|
+
|| topLast > body.statement_index))
|
|
412
|
+
return false;
|
|
413
|
+
if (body.statement_index > body.completed_statements
|
|
414
|
+
|| body.completed_statements > body.statement_index + 1)
|
|
415
|
+
return false;
|
|
416
|
+
const originalQueryIdMatches = expectedOriginalQueryId === undefined
|
|
417
|
+
? body.idempotency_replayed === true || body.original_query_id === queryId
|
|
418
|
+
: body.idempotency_replayed === true
|
|
419
|
+
? body.original_query_id === expectedOriginalQueryId
|
|
420
|
+
: body.original_query_id === queryId;
|
|
421
|
+
return body.idempotency_persisted === true
|
|
422
|
+
&& body.idempotency_expires_at_ms > 0
|
|
423
|
+
&& body.retryable === false
|
|
424
|
+
&& originalQueryIdMatches;
|
|
425
|
+
}
|
|
426
|
+
const QUERY_STATES = new Set([
|
|
427
|
+
'queued', 'planning', 'executing', 'streaming', 'serializing', 'commit_critical',
|
|
428
|
+
'cancelling', 'completed', 'failed', 'cancelled', 'pre_cancelled', 'finished'
|
|
429
|
+
]);
|
|
430
|
+
const QUERY_STATUSES = new Set([
|
|
431
|
+
'running', 'outcome_unknown', 'completed', 'failed_before_commit',
|
|
432
|
+
'cancelled_before_commit', 'deadline_before_commit', 'cancelled_before_start',
|
|
433
|
+
'committed', 'committed_with_error', 'partially_committed',
|
|
434
|
+
'cancelled_after_commit', 'deadline_after_commit', 'finished'
|
|
435
|
+
]);
|
|
436
|
+
const COMMITTED_QUERY_STATUSES = new Set([
|
|
437
|
+
'committed', 'committed_with_error', 'partially_committed',
|
|
438
|
+
'cancelled_after_commit', 'deadline_after_commit'
|
|
439
|
+
]);
|
|
440
|
+
function sameOptional(left, right) {
|
|
441
|
+
return (left ?? null) === (right ?? null);
|
|
442
|
+
}
|
|
443
|
+
function isRemoteQueryStatus(value, queryId) {
|
|
444
|
+
if (value === null || typeof value !== 'object')
|
|
445
|
+
return false;
|
|
446
|
+
const body = value;
|
|
447
|
+
const outcome = body.outcome !== null && typeof body.outcome === 'object'
|
|
448
|
+
? body.outcome
|
|
449
|
+
: undefined;
|
|
450
|
+
if (outcome === undefined)
|
|
451
|
+
return false;
|
|
452
|
+
const serverState = body.server_state ?? '';
|
|
453
|
+
const terminal = body.terminal_error;
|
|
454
|
+
if (!hasOnlyKeys(body, [
|
|
455
|
+
'query_id', 'status', 'state', 'server_state', 'terminal_state', 'operation',
|
|
456
|
+
'started_ms_ago', 'deadline_ms_remaining', 'session_id',
|
|
457
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
458
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
459
|
+
'statement_index', 'cancel_outcome', 'retryable', 'terminal_error',
|
|
460
|
+
'cancellation_reason', 'outcome', 'trace'
|
|
461
|
+
]) || !hasOnlyKeys(outcome, [
|
|
462
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
463
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
464
|
+
'statement_index', 'serialization'
|
|
465
|
+
]) || !hasAllKeys(outcome, [
|
|
466
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
467
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
468
|
+
'statement_index', 'serialization'
|
|
469
|
+
]))
|
|
470
|
+
return false;
|
|
471
|
+
if (terminal !== undefined && terminal !== null
|
|
472
|
+
&& (typeof terminal !== 'object'
|
|
473
|
+
|| !hasOnlyKeys(terminal, ['code', 'category'])))
|
|
474
|
+
return false;
|
|
475
|
+
const trace = body.trace;
|
|
476
|
+
if (trace !== undefined && (trace === null || typeof trace !== 'object'
|
|
477
|
+
|| !hasOnlyKeys(trace, [
|
|
478
|
+
'queue_duration_us', 'planning_duration_us', 'execution_duration_us',
|
|
479
|
+
'serialization_duration_us', 'cancel_requested_phase', 'cancel_observed_phase',
|
|
480
|
+
'commit_fence_outcome'
|
|
481
|
+
])))
|
|
482
|
+
return false;
|
|
483
|
+
const traceRecord = trace;
|
|
484
|
+
const tracePhaseValid = (phase) => phase === undefined || phase === null
|
|
485
|
+
|| typeof phase === 'string' && QUERY_STATES.has(phase);
|
|
486
|
+
if (traceRecord !== undefined
|
|
487
|
+
&& (traceRecord.queue_duration_us !== undefined
|
|
488
|
+
&& !isNonNegativeInteger(traceRecord.queue_duration_us)
|
|
489
|
+
|| traceRecord.planning_duration_us !== undefined
|
|
490
|
+
&& !isNonNegativeInteger(traceRecord.planning_duration_us)
|
|
491
|
+
|| traceRecord.execution_duration_us !== undefined
|
|
492
|
+
&& !isNonNegativeInteger(traceRecord.execution_duration_us)
|
|
493
|
+
|| traceRecord.serialization_duration_us !== undefined
|
|
494
|
+
&& !isNonNegativeInteger(traceRecord.serialization_duration_us)
|
|
495
|
+
|| !tracePhaseValid(traceRecord.cancel_requested_phase)
|
|
496
|
+
|| !tracePhaseValid(traceRecord.cancel_observed_phase)
|
|
497
|
+
|| traceRecord.commit_fence_outcome !== undefined
|
|
498
|
+
&& !['not_reached', 'cancel_won', 'commit_won']
|
|
499
|
+
.includes(traceRecord.commit_fence_outcome)))
|
|
500
|
+
return false;
|
|
501
|
+
if (body.query_id !== queryId
|
|
502
|
+
|| typeof body.status !== 'string' || !QUERY_STATUSES.has(body.status)
|
|
503
|
+
|| typeof body.state !== 'string' || !QUERY_STATES.has(body.state)
|
|
504
|
+
|| typeof serverState !== 'string'
|
|
505
|
+
|| serverState !== '' && (!QUERY_STATES.has(serverState) || serverState !== body.state)
|
|
506
|
+
|| body.terminal_state != null && body.terminal_state !== body.status
|
|
507
|
+
|| body.started_ms_ago !== undefined && !isNonNegativeInteger(body.started_ms_ago)
|
|
508
|
+
|| body.deadline_ms_remaining !== undefined && body.deadline_ms_remaining !== null
|
|
509
|
+
&& !isNonNegativeInteger(body.deadline_ms_remaining)
|
|
510
|
+
|| body.session_id !== undefined && body.session_id !== null
|
|
511
|
+
&& (typeof body.session_id !== 'string' || body.session_id.length > 256)
|
|
512
|
+
|| ![undefined, null, true, false].includes(body.committed)
|
|
513
|
+
|| ![undefined, null, true, false].includes(outcome.committed)
|
|
514
|
+
|| typeof body.retryable !== 'boolean'
|
|
515
|
+
|| typeof body.cancellation_reason !== 'string'
|
|
516
|
+
|| !['not_started', 'in_progress', 'succeeded', 'failed', 'unknown']
|
|
517
|
+
.includes(outcome.serialization)
|
|
518
|
+
|| !isOptionalNonNegativeInteger(body.committed_statements)
|
|
519
|
+
|| !isOptionalNonNegativeInteger(body.first_commit_statement_index)
|
|
520
|
+
|| !isOptionalNonNegativeInteger(body.last_commit_statement_index)
|
|
521
|
+
|| !isOptionalNonNegativeInteger(body.completed_statements)
|
|
522
|
+
|| !isOptionalNonNegativeInteger(body.statement_index)
|
|
523
|
+
|| !isOptionalNonNegativeInteger(outcome.committed_statements)
|
|
524
|
+
|| !isOptionalNonNegativeInteger(outcome.first_commit_statement_index)
|
|
525
|
+
|| !isOptionalNonNegativeInteger(outcome.last_commit_statement_index)
|
|
526
|
+
|| !isOptionalNonNegativeInteger(outcome.completed_statements)
|
|
527
|
+
|| !isOptionalNonNegativeInteger(outcome.statement_index)
|
|
528
|
+
|| !isOptionalEpoch(body.last_commit_epoch, body.last_commit_epoch_text)
|
|
529
|
+
|| !isOptionalEpoch(outcome.last_commit_epoch, outcome.last_commit_epoch_text)
|
|
530
|
+
|| terminal != null && (typeof terminal !== 'object'
|
|
531
|
+
|| typeof terminal.code !== 'string'
|
|
532
|
+
|| terminal.code.trim().length === 0
|
|
533
|
+
|| !['cancellation', 'deadline', 'result_limit', 'serialization', 'execution']
|
|
534
|
+
.includes(terminal.category))) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
const topEpoch = receiptEpoch(body.last_commit_epoch, body.last_commit_epoch_text);
|
|
538
|
+
const outcomeEpoch = receiptEpoch(outcome.last_commit_epoch, outcome.last_commit_epoch_text);
|
|
539
|
+
if (topEpoch !== outcomeEpoch
|
|
540
|
+
|| !sameOptional(body.committed, outcome.committed)
|
|
541
|
+
|| !sameOptional(body.committed_statements, outcome.committed_statements)
|
|
542
|
+
|| !sameOptional(body.first_commit_statement_index, outcome.first_commit_statement_index)
|
|
543
|
+
|| !sameOptional(body.last_commit_statement_index, outcome.last_commit_statement_index)
|
|
544
|
+
|| !sameOptional(body.completed_statements, outcome.completed_statements)
|
|
545
|
+
|| !sameOptional(body.statement_index, outcome.statement_index))
|
|
546
|
+
return false;
|
|
547
|
+
const committed = body.committed ?? null;
|
|
548
|
+
const committedStatements = body.committed_statements;
|
|
549
|
+
const first = body.first_commit_statement_index;
|
|
550
|
+
const last = body.last_commit_statement_index;
|
|
551
|
+
const completed = body.completed_statements;
|
|
552
|
+
const statement = body.statement_index;
|
|
553
|
+
const stateMatchesStatus = body.status === 'running'
|
|
554
|
+
? ['queued', 'planning', 'executing', 'streaming', 'serializing',
|
|
555
|
+
'commit_critical', 'cancelling'].includes(body.state)
|
|
556
|
+
: body.status === 'committed'
|
|
557
|
+
? ['planning', 'executing', 'streaming', 'serializing',
|
|
558
|
+
'commit_critical', 'cancelling', 'completed'].includes(body.state)
|
|
559
|
+
: body.status === 'completed' ? body.state === 'completed'
|
|
560
|
+
: ['failed_before_commit', 'committed_with_error', 'partially_committed',
|
|
561
|
+
'outcome_unknown'].includes(body.status) ? body.state === 'failed'
|
|
562
|
+
: ['cancelled_before_commit', 'deadline_before_commit',
|
|
563
|
+
'cancelled_after_commit', 'deadline_after_commit'].includes(body.status)
|
|
564
|
+
? body.state === 'cancelled'
|
|
565
|
+
: body.status === 'cancelled_before_start' ? body.state === 'pre_cancelled'
|
|
566
|
+
: body.status === 'finished' && body.state === 'finished';
|
|
567
|
+
if (!stateMatchesStatus)
|
|
568
|
+
return false;
|
|
569
|
+
const expectedTerminal = body.status === 'running' || body.status === 'finished'
|
|
570
|
+
|| body.status === 'committed' && body.state !== 'completed'
|
|
571
|
+
? null : body.status;
|
|
572
|
+
if ((body.terminal_state ?? null) !== expectedTerminal)
|
|
573
|
+
return false;
|
|
574
|
+
if (committed === true) {
|
|
575
|
+
if (!COMMITTED_QUERY_STATUSES.has(body.status)
|
|
576
|
+
|| committedStatements == null || committedStatements === 0
|
|
577
|
+
|| topEpoch === undefined || body.last_commit_epoch_text == null
|
|
578
|
+
|| outcome.last_commit_epoch_text == null || first == null || last == null
|
|
579
|
+
|| completed == null || statement == null)
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
else if (committed === false) {
|
|
583
|
+
if (COMMITTED_QUERY_STATUSES.has(body.status)
|
|
584
|
+
|| body.status === 'outcome_unknown' || body.status === 'finished'
|
|
585
|
+
|| committedStatements !== 0 || topEpoch !== undefined
|
|
586
|
+
|| first != null || last != null || completed == null || statement == null)
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
else if (body.status !== 'outcome_unknown' && body.status !== 'finished'
|
|
590
|
+
|| committedStatements != null || topEpoch !== undefined
|
|
591
|
+
|| first != null || last != null || completed != null || statement != null)
|
|
592
|
+
return false;
|
|
593
|
+
if (first != null && last != null && committedStatements != null && statement != null
|
|
594
|
+
&& (first > last || committedStatements > last - first + 1 || last > statement))
|
|
595
|
+
return false;
|
|
596
|
+
if (completed != null && statement != null
|
|
597
|
+
&& (statement > completed || completed > statement + 1))
|
|
598
|
+
return false;
|
|
599
|
+
const expectedCancel = body.state === 'cancelling' ? 'accepted'
|
|
600
|
+
: body.state === 'commit_critical' ? 'too_late'
|
|
601
|
+
: ['completed', 'failed', 'cancelled', 'finished'].includes(body.state)
|
|
602
|
+
? 'already_finished'
|
|
603
|
+
: body.state === 'pre_cancelled' ? 'pre_cancelled' : null;
|
|
604
|
+
if ((body.cancel_outcome ?? null) !== expectedCancel)
|
|
605
|
+
return false;
|
|
606
|
+
const terminalError = terminal;
|
|
607
|
+
const terminalCode = terminalError?.code;
|
|
608
|
+
const terminalCategory = terminalError?.category;
|
|
609
|
+
const terminalMatches = ['running', 'completed', 'committed', 'finished']
|
|
610
|
+
.includes(body.status) ? terminal == null
|
|
611
|
+
: body.status === 'outcome_unknown'
|
|
612
|
+
? terminalCode === 'QUERY_OUTCOME_UNKNOWN' && terminalCategory === 'execution'
|
|
613
|
+
: ['cancelled_before_commit', 'cancelled_before_start'].includes(body.status)
|
|
614
|
+
? terminalCode === 'QUERY_CANCELLED' && terminalCategory === 'cancellation'
|
|
615
|
+
: body.status === 'cancelled_after_commit'
|
|
616
|
+
? terminalCode === 'QUERY_CANCELLED_AFTER_COMMIT'
|
|
617
|
+
&& terminalCategory === 'cancellation'
|
|
618
|
+
: body.status === 'deadline_before_commit'
|
|
619
|
+
? terminalCode === 'DEADLINE_EXCEEDED' && terminalCategory === 'deadline'
|
|
620
|
+
: body.status === 'deadline_after_commit'
|
|
621
|
+
? terminalCode === 'DEADLINE_AFTER_COMMIT'
|
|
622
|
+
&& terminalCategory === 'deadline'
|
|
623
|
+
: terminal != null;
|
|
624
|
+
if (!terminalMatches)
|
|
625
|
+
return false;
|
|
626
|
+
if (terminalError != null
|
|
627
|
+
&& ((terminalCategory === 'cancellation')
|
|
628
|
+
!== ['QUERY_CANCELLED', 'QUERY_CANCELLED_AFTER_COMMIT'].includes(terminalCode)
|
|
629
|
+
|| (terminalCategory === 'deadline')
|
|
630
|
+
!== ['DEADLINE_EXCEEDED', 'DEADLINE_AFTER_COMMIT'].includes(terminalCode))) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
const retryable = ['IDEMPOTENCY_STORE_FULL', 'IDEMPOTENCY_STORE_UNAVAILABLE']
|
|
634
|
+
.includes(terminalCode);
|
|
635
|
+
if (body.retryable !== retryable)
|
|
636
|
+
return false;
|
|
637
|
+
const reason = body.cancellation_reason;
|
|
638
|
+
if (!['none', 'client_request', 'deadline', 'client_disconnected',
|
|
639
|
+
'session_closed', 'server_shutdown'].includes(reason))
|
|
640
|
+
return false;
|
|
641
|
+
if (['deadline_before_commit', 'deadline_after_commit'].includes(body.status)) {
|
|
642
|
+
return reason === 'deadline';
|
|
643
|
+
}
|
|
644
|
+
if (['cancelled_before_commit', 'cancelled_before_start', 'cancelled_after_commit']
|
|
645
|
+
.includes(body.status)
|
|
646
|
+
|| ['running', 'committed'].includes(body.status) && body.state === 'cancelling') {
|
|
647
|
+
return reason !== 'none';
|
|
648
|
+
}
|
|
649
|
+
return reason === 'none' || body.state === 'commit_critical';
|
|
650
|
+
}
|
|
651
|
+
function isRemoteSqlPage(value, initialOptions) {
|
|
652
|
+
if (value === null || typeof value !== 'object')
|
|
653
|
+
return false;
|
|
654
|
+
const body = value;
|
|
655
|
+
if (body.page === null || typeof body.page !== 'object' || !Array.isArray(body.rows))
|
|
656
|
+
return false;
|
|
657
|
+
const page = body.page;
|
|
658
|
+
if (page.limits === null || typeof page.limits !== 'object')
|
|
659
|
+
return false;
|
|
660
|
+
const limits = page.limits;
|
|
661
|
+
if (!hasOnlyKeys(body, ['status', 'rows', 'next_cursor', 'page'])
|
|
662
|
+
|| !hasOnlyKeys(page, [
|
|
663
|
+
'offset', 'row_count', 'total_rows', 'byte_count', 'estimated_tokens', 'limits',
|
|
664
|
+
'projection', 'expires_at_ms', 'snapshot', 'token_estimate'
|
|
665
|
+
])
|
|
666
|
+
|| !hasOnlyKeys(limits, ['rows', 'bytes', 'tokens']))
|
|
667
|
+
return false;
|
|
668
|
+
const shapeValid = body.status === 'completed'
|
|
669
|
+
&& body.rows.every((row) => row !== null && typeof row === 'object' && !Array.isArray(row))
|
|
670
|
+
&& (body.next_cursor === undefined || body.next_cursor === null
|
|
671
|
+
|| typeof body.next_cursor === 'string' && body.next_cursor.length > 0)
|
|
672
|
+
&& isNonNegativeInteger(page.offset)
|
|
673
|
+
&& isNonNegativeInteger(page.row_count)
|
|
674
|
+
&& page.row_count === body.rows.length
|
|
675
|
+
&& isNonNegativeInteger(page.total_rows)
|
|
676
|
+
&& page.offset <= page.total_rows
|
|
677
|
+
&& page.row_count <= page.total_rows - page.offset
|
|
678
|
+
&& isNonNegativeInteger(page.byte_count)
|
|
679
|
+
&& isNonNegativeInteger(page.estimated_tokens)
|
|
680
|
+
&& isNonNegativeInteger(limits.rows)
|
|
681
|
+
&& isNonNegativeInteger(limits.bytes)
|
|
682
|
+
&& isNonNegativeInteger(limits.tokens)
|
|
683
|
+
&& Array.isArray(page.projection)
|
|
684
|
+
&& page.projection.length > 0 && page.projection.length <= 128
|
|
685
|
+
&& page.projection.every((column) => typeof column === 'string'
|
|
686
|
+
&& column.length > 0 && column !== '*'
|
|
687
|
+
&& Buffer.byteLength(column, 'utf8') <= 256)
|
|
688
|
+
&& page.projection.reduce((bytes, column) => bytes + Buffer.byteLength(column, 'utf8'), 0) <= 16 * 1024
|
|
689
|
+
&& isNonNegativeInteger(page.expires_at_ms)
|
|
690
|
+
&& page.snapshot === 'retained_result'
|
|
691
|
+
&& page.token_estimate === 'ceil(projected_json_bytes/4)';
|
|
692
|
+
if (!shapeValid)
|
|
693
|
+
return false;
|
|
694
|
+
const projection = page.projection;
|
|
695
|
+
if (new Set(projection).size !== projection.length
|
|
696
|
+
|| body.rows.some((row) => {
|
|
697
|
+
const keys = Object.keys(row);
|
|
698
|
+
return keys.length !== projection.length
|
|
699
|
+
|| projection.some((column) => !Object.prototype.hasOwnProperty.call(row, column));
|
|
700
|
+
}))
|
|
701
|
+
return false;
|
|
702
|
+
const byteCount = body.rows.reduce((bytes, row, index) => bytes + (index === 0 ? 0 : 1)
|
|
703
|
+
+ Buffer.byteLength(JSON.stringify(row), 'utf8'), 2);
|
|
704
|
+
if (page.byte_count !== byteCount
|
|
705
|
+
|| page.estimated_tokens !== Math.ceil(byteCount / 4))
|
|
706
|
+
return false;
|
|
707
|
+
if (limits.rows === 0 || limits.bytes === 0
|
|
708
|
+
|| limits.tokens === 0
|
|
709
|
+
|| limits.bytes > MAX_PAGE_JSON_RESPONSE_BYTES
|
|
710
|
+
|| page.row_count > limits.rows
|
|
711
|
+
|| page.byte_count > limits.bytes
|
|
712
|
+
|| page.estimated_tokens > limits.tokens)
|
|
713
|
+
return false;
|
|
714
|
+
const hasMore = page.offset + page.row_count < page.total_rows;
|
|
715
|
+
if (hasMore && page.row_count === 0
|
|
716
|
+
|| hasMore !== (typeof body.next_cursor === 'string'))
|
|
717
|
+
return false;
|
|
718
|
+
if (typeof body.next_cursor === 'string'
|
|
719
|
+
&& Buffer.byteLength(body.next_cursor, 'utf8') > 2_048)
|
|
720
|
+
return false;
|
|
721
|
+
if (page.expires_at_ms === 0)
|
|
722
|
+
return false;
|
|
723
|
+
if (initialOptions === undefined)
|
|
724
|
+
return true;
|
|
725
|
+
return page.offset === 0
|
|
726
|
+
&& JSON.stringify(page.projection) === JSON.stringify(initialOptions.projection)
|
|
727
|
+
&& limits.rows <= initialOptions.pageSizeRows
|
|
728
|
+
&& (initialOptions.maxPageBytes === undefined
|
|
729
|
+
|| limits.bytes <= initialOptions.maxPageBytes)
|
|
730
|
+
&& (initialOptions.maxPageTokens === undefined
|
|
731
|
+
|| limits.tokens <= initialOptions.maxPageTokens)
|
|
732
|
+
&& (initialOptions.maxOutputRows === undefined
|
|
733
|
+
|| page.total_rows <= initialOptions.maxOutputRows)
|
|
734
|
+
&& (initialOptions.maxOutputBytes === undefined
|
|
735
|
+
|| page.byte_count <= initialOptions.maxOutputBytes);
|
|
736
|
+
}
|
|
737
|
+
function remoteSqlPage(page) {
|
|
738
|
+
return {
|
|
739
|
+
status: page.status,
|
|
740
|
+
rows: page.rows,
|
|
741
|
+
nextCursor: page.next_cursor ?? undefined,
|
|
742
|
+
page: {
|
|
743
|
+
offset: page.page.offset,
|
|
744
|
+
rowCount: page.page.row_count,
|
|
745
|
+
totalRows: page.page.total_rows,
|
|
746
|
+
byteCount: page.page.byte_count,
|
|
747
|
+
estimatedTokens: page.page.estimated_tokens,
|
|
748
|
+
limits: page.page.limits,
|
|
749
|
+
projection: page.page.projection,
|
|
750
|
+
expiresAtMs: page.page.expires_at_ms,
|
|
751
|
+
snapshot: page.page.snapshot,
|
|
752
|
+
tokenEstimate: page.page.token_estimate
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
class RetryIdempotentSqlError extends Error {
|
|
757
|
+
outcome;
|
|
758
|
+
constructor(outcome) {
|
|
759
|
+
super(outcome.message);
|
|
760
|
+
this.outcome = outcome;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
class QueryNotRetainedError extends QueryOutcomeUnknownError {
|
|
764
|
+
constructor(queryId, message) {
|
|
765
|
+
super(queryId, message, { retryable: false, serverState: 'not_found' });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
class InvalidQueryStatusError extends QueryOutcomeUnknownError {
|
|
769
|
+
constructor(queryId, message) {
|
|
770
|
+
super(queryId, `invalid query status response: ${message}`, {
|
|
771
|
+
retryable: false,
|
|
772
|
+
serverState: 'invalid_status'
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function remoteEpoch(text, value) {
|
|
777
|
+
if (text !== undefined && text !== null) {
|
|
778
|
+
if (!/^\d+$/.test(text) || BigInt(text) > MAX_U64) {
|
|
779
|
+
throw new KitError('invalid last_commit_epoch_text from server');
|
|
780
|
+
}
|
|
781
|
+
return BigInt(text);
|
|
782
|
+
}
|
|
783
|
+
if (value === undefined || value === null)
|
|
784
|
+
return undefined;
|
|
785
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
786
|
+
throw new KitError('server returned an unsafe numeric commit epoch', 'QUERY_OUTCOME_UNKNOWN');
|
|
787
|
+
}
|
|
788
|
+
return BigInt(value);
|
|
789
|
+
}
|
|
790
|
+
function mergedCommitState(nested, top) {
|
|
791
|
+
if (nested === true || top === true)
|
|
792
|
+
return true;
|
|
793
|
+
return nested ?? top ?? null;
|
|
794
|
+
}
|
|
795
|
+
function recoveryStatusIsDecisive(status) {
|
|
796
|
+
return TERMINAL_SQL_STATES.has(status.state)
|
|
797
|
+
|| mergedCommitState(status.outcome?.committed, status.committed) === true;
|
|
798
|
+
}
|
|
799
|
+
function maxKnown(left, right) {
|
|
800
|
+
if (left === undefined || left === null)
|
|
801
|
+
return right ?? undefined;
|
|
802
|
+
if (right === undefined || right === null)
|
|
803
|
+
return left;
|
|
804
|
+
return Math.max(left, right);
|
|
805
|
+
}
|
|
806
|
+
function cancelOutcome(value) {
|
|
807
|
+
return {
|
|
808
|
+
accepted: 'accepted',
|
|
809
|
+
cancellation_requested: 'accepted',
|
|
810
|
+
already_cancelling: 'already_cancelling',
|
|
811
|
+
cancelling: 'already_cancelling',
|
|
812
|
+
too_late: 'too_late',
|
|
813
|
+
commit_critical: 'too_late',
|
|
814
|
+
already_finished: 'already_finished',
|
|
815
|
+
finished: 'already_finished',
|
|
816
|
+
not_found: 'not_found',
|
|
817
|
+
pre_cancelled: 'pre_cancelled'
|
|
818
|
+
}[String(value)];
|
|
819
|
+
}
|
|
820
|
+
function validateCancelResponse(value, queryId, httpStatus) {
|
|
821
|
+
if (value === null || typeof value !== 'object') {
|
|
822
|
+
throw new KitError('SQL cancellation response was not an object', 'REMOTE_PROTOCOL');
|
|
823
|
+
}
|
|
824
|
+
const body = value;
|
|
825
|
+
if (!hasOnlyKeys(body, [
|
|
826
|
+
'query_id', 'state', 'cancel_outcome', 'status', 'terminal_state', 'code',
|
|
827
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
828
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
829
|
+
'statement_index', 'cancellation_reason', 'retryable', 'server_state', 'outcome', 'error',
|
|
830
|
+
'terminal_error'
|
|
831
|
+
])) {
|
|
832
|
+
throw new KitError('SQL cancellation response has unknown fields', 'REMOTE_PROTOCOL');
|
|
833
|
+
}
|
|
834
|
+
if (body.outcome !== undefined && body.outcome !== null
|
|
835
|
+
&& (typeof body.outcome !== 'object'
|
|
836
|
+
|| !hasOnlyKeys(body.outcome, [
|
|
837
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
838
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
839
|
+
'statement_index', 'serialization'
|
|
840
|
+
]))) {
|
|
841
|
+
throw new KitError('SQL cancellation outcome has unknown fields', 'REMOTE_PROTOCOL');
|
|
842
|
+
}
|
|
843
|
+
if (body.error !== undefined && body.error !== null
|
|
844
|
+
&& (typeof body.error !== 'object'
|
|
845
|
+
|| !hasOnlyKeys(body.error, [
|
|
846
|
+
'code', 'message', 'query_id', 'committed', 'retryable'
|
|
847
|
+
]))) {
|
|
848
|
+
throw new KitError('SQL cancellation error has unknown fields', 'REMOTE_PROTOCOL');
|
|
849
|
+
}
|
|
850
|
+
if (body.terminal_error !== undefined && body.terminal_error !== null
|
|
851
|
+
&& (typeof body.terminal_error !== 'object'
|
|
852
|
+
|| !hasOnlyKeys(body.terminal_error, [
|
|
853
|
+
'code', 'category'
|
|
854
|
+
])
|
|
855
|
+
|| typeof body.terminal_error.code !== 'string'
|
|
856
|
+
|| body.terminal_error.code.length === 0
|
|
857
|
+
|| typeof body.terminal_error.category !== 'string'
|
|
858
|
+
|| body.terminal_error.category.length === 0)) {
|
|
859
|
+
throw new KitError('SQL cancellation terminal error has invalid fields', 'REMOTE_PROTOCOL');
|
|
860
|
+
}
|
|
861
|
+
const durable = body.outcome;
|
|
862
|
+
if (durable != null) {
|
|
863
|
+
const required = [
|
|
864
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
865
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
866
|
+
'statement_index', 'serialization'
|
|
867
|
+
];
|
|
868
|
+
if (!required.every((field) => Object.prototype.hasOwnProperty.call(durable, field))
|
|
869
|
+
|| ![null, true, false].includes(durable.committed)
|
|
870
|
+
|| ['committed_statements', 'first_commit_statement_index',
|
|
871
|
+
'last_commit_statement_index', 'completed_statements', 'statement_index']
|
|
872
|
+
.some((field) => !isOptionalNonNegativeInteger(durable[field]))
|
|
873
|
+
|| !isOptionalEpoch(durable.last_commit_epoch, durable.last_commit_epoch_text)
|
|
874
|
+
|| !['not_started', 'in_progress', 'succeeded', 'failed', 'unknown']
|
|
875
|
+
.includes(durable.serialization)) {
|
|
876
|
+
throw new KitError('SQL cancellation outcome fields are invalid', 'REMOTE_PROTOCOL');
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (body.committed !== undefined && ![null, true, false]
|
|
880
|
+
.includes(body.committed)) {
|
|
881
|
+
throw new KitError('SQL cancellation committed field is invalid', 'REMOTE_PROTOCOL');
|
|
882
|
+
}
|
|
883
|
+
for (const field of ['committed_statements', 'first_commit_statement_index',
|
|
884
|
+
'last_commit_statement_index', 'completed_statements', 'statement_index']) {
|
|
885
|
+
if (body[field] !== undefined && !isOptionalNonNegativeInteger(body[field])) {
|
|
886
|
+
throw new KitError(`SQL cancellation ${field} is invalid`, 'REMOTE_PROTOCOL');
|
|
887
|
+
}
|
|
888
|
+
if (durable != null && body[field] !== undefined
|
|
889
|
+
&& !sameOptional(body[field], durable[field])) {
|
|
890
|
+
throw new KitError(`SQL cancellation ${field} disagrees with outcome`, 'REMOTE_PROTOCOL');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if ((body.last_commit_epoch !== undefined || body.last_commit_epoch_text !== undefined)
|
|
894
|
+
&& !isOptionalEpoch(body.last_commit_epoch, body.last_commit_epoch_text)) {
|
|
895
|
+
throw new KitError('SQL cancellation commit epoch is invalid', 'REMOTE_PROTOCOL');
|
|
896
|
+
}
|
|
897
|
+
if (durable != null
|
|
898
|
+
&& (body.committed !== undefined && !sameOptional(body.committed, durable.committed)
|
|
899
|
+
|| (body.last_commit_epoch !== undefined || body.last_commit_epoch_text !== undefined)
|
|
900
|
+
&& receiptEpoch(body.last_commit_epoch, body.last_commit_epoch_text)
|
|
901
|
+
!== receiptEpoch(durable.last_commit_epoch, durable.last_commit_epoch_text))) {
|
|
902
|
+
throw new KitError('SQL cancellation outcome metadata disagrees', 'REMOTE_PROTOCOL');
|
|
903
|
+
}
|
|
904
|
+
if (body.status !== undefined && (typeof body.status !== 'string'
|
|
905
|
+
|| !QUERY_STATUSES.has(body.status) && body.status !== 'unknown')) {
|
|
906
|
+
throw new KitError('SQL cancellation status is invalid', 'REMOTE_PROTOCOL');
|
|
907
|
+
}
|
|
908
|
+
if (body.terminal_state !== undefined && body.terminal_state !== null
|
|
909
|
+
&& (typeof body.terminal_state !== 'string'
|
|
910
|
+
|| body.terminal_state !== body.status)) {
|
|
911
|
+
throw new KitError('SQL cancellation terminal state is invalid', 'REMOTE_PROTOCOL');
|
|
912
|
+
}
|
|
913
|
+
if (body.retryable !== undefined && typeof body.retryable !== 'boolean') {
|
|
914
|
+
throw new KitError('SQL cancellation retryable field is invalid', 'REMOTE_PROTOCOL');
|
|
915
|
+
}
|
|
916
|
+
if (body.server_state !== undefined && (typeof body.server_state !== 'string'
|
|
917
|
+
|| !QUERY_STATES.has(body.server_state) && body.server_state !== 'not_found')) {
|
|
918
|
+
throw new KitError('SQL cancellation server state is invalid', 'REMOTE_PROTOCOL');
|
|
919
|
+
}
|
|
920
|
+
if (body.cancellation_reason !== undefined && body.cancellation_reason !== null
|
|
921
|
+
&& (typeof body.cancellation_reason !== 'string'
|
|
922
|
+
|| !['none', 'client_request', 'deadline', 'client_disconnected',
|
|
923
|
+
'session_closed', 'server_shutdown'].includes(body.cancellation_reason))) {
|
|
924
|
+
throw new KitError('SQL cancellation reason is invalid', 'REMOTE_PROTOCOL');
|
|
925
|
+
}
|
|
926
|
+
const detail = body.error;
|
|
927
|
+
if (detail != null
|
|
928
|
+
&& (typeof detail.code !== 'string' || detail.code.length === 0
|
|
929
|
+
|| typeof detail.message !== 'string' || detail.message.length === 0
|
|
930
|
+
|| detail.query_id !== undefined && detail.query_id !== null
|
|
931
|
+
&& typeof detail.query_id !== 'string'
|
|
932
|
+
|| detail.committed !== undefined
|
|
933
|
+
&& ![null, true, false].includes(detail.committed)
|
|
934
|
+
|| detail.retryable !== undefined && typeof detail.retryable !== 'boolean')) {
|
|
935
|
+
throw new KitError('SQL cancellation error fields are invalid', 'REMOTE_PROTOCOL');
|
|
936
|
+
}
|
|
937
|
+
if (body.query_id !== queryId) {
|
|
938
|
+
throw new KitError('SQL cancellation query ID does not match the request', 'REMOTE_PROTOCOL');
|
|
939
|
+
}
|
|
940
|
+
const decodeField = (name) => {
|
|
941
|
+
const raw = body[name];
|
|
942
|
+
if (raw === undefined || raw === null)
|
|
943
|
+
return undefined;
|
|
944
|
+
const decoded = cancelOutcome(raw);
|
|
945
|
+
if (decoded === undefined) {
|
|
946
|
+
throw new KitError(`SQL cancellation ${name} is invalid`, 'REMOTE_PROTOCOL');
|
|
947
|
+
}
|
|
948
|
+
return decoded;
|
|
949
|
+
};
|
|
950
|
+
const outcome = decodeField('cancel_outcome');
|
|
951
|
+
let state = decodeField('state');
|
|
952
|
+
if (outcome === 'not_found' && state === undefined && body.server_state === 'not_found') {
|
|
953
|
+
state = 'not_found';
|
|
954
|
+
}
|
|
955
|
+
if (outcome === undefined || state === undefined) {
|
|
956
|
+
throw new KitError('SQL cancellation state and outcome are required', 'REMOTE_PROTOCOL');
|
|
957
|
+
}
|
|
958
|
+
if (outcome !== state) {
|
|
959
|
+
throw new KitError('SQL cancellation state and outcome disagree', 'REMOTE_PROTOCOL');
|
|
960
|
+
}
|
|
961
|
+
const result = outcome;
|
|
962
|
+
const compatible = httpStatus === 202 && ['accepted', 'pre_cancelled'].includes(result ?? '')
|
|
963
|
+
|| httpStatus === 200 && ['already_cancelling', 'already_finished'].includes(result ?? '')
|
|
964
|
+
|| httpStatus === 409 && result === 'too_late'
|
|
965
|
+
|| httpStatus === 404 && result === 'not_found';
|
|
966
|
+
if (!compatible) {
|
|
967
|
+
throw new KitError('SQL cancellation HTTP status and outcome disagree', 'REMOTE_PROTOCOL');
|
|
968
|
+
}
|
|
969
|
+
const terminal = body.terminal_error;
|
|
970
|
+
if (result === 'pre_cancelled') {
|
|
971
|
+
if (terminal != null
|
|
972
|
+
&& (terminal.code !== 'QUERY_CANCELLED' || terminal.category !== 'cancellation')) {
|
|
973
|
+
throw new KitError('SQL cancellation terminal error disagrees with outcome', 'REMOTE_PROTOCOL');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
else if (terminal != null) {
|
|
977
|
+
throw new KitError('SQL cancellation terminal error disagrees with outcome', 'REMOTE_PROTOCOL');
|
|
978
|
+
}
|
|
979
|
+
return result;
|
|
980
|
+
}
|
|
981
|
+
function exactNativeValue(values, valid) {
|
|
982
|
+
let exact = undefined;
|
|
983
|
+
for (const value of values) {
|
|
984
|
+
if (value === undefined)
|
|
985
|
+
continue;
|
|
986
|
+
if (!valid(value) || exact !== undefined && !Object.is(exact, value)) {
|
|
987
|
+
throw new Error('conflicting native response fields');
|
|
988
|
+
}
|
|
989
|
+
exact = value;
|
|
990
|
+
}
|
|
991
|
+
return exact;
|
|
992
|
+
}
|
|
993
|
+
function nativeRemoteWriteEpoch(...sources) {
|
|
994
|
+
let exact;
|
|
995
|
+
const merge = (value, text) => {
|
|
996
|
+
if (value === undefined || value === null)
|
|
997
|
+
return;
|
|
998
|
+
let parsed;
|
|
999
|
+
if (text) {
|
|
1000
|
+
if (typeof value !== 'string' || !/^\d+$/u.test(value)
|
|
1001
|
+
|| BigInt(value).toString() !== value)
|
|
1002
|
+
throw new Error('invalid exact commit epoch');
|
|
1003
|
+
parsed = BigInt(value);
|
|
1004
|
+
}
|
|
1005
|
+
else if (typeof value === 'bigint')
|
|
1006
|
+
parsed = value;
|
|
1007
|
+
else if (typeof value === 'number' && Number.isSafeInteger(value))
|
|
1008
|
+
parsed = BigInt(value);
|
|
1009
|
+
else if (typeof value === 'string' && /^\d+$/u.test(value)
|
|
1010
|
+
&& BigInt(value).toString() === value)
|
|
1011
|
+
parsed = BigInt(value);
|
|
1012
|
+
else
|
|
1013
|
+
throw new Error('invalid commit epoch');
|
|
1014
|
+
if (parsed < 0n || parsed > MAX_U64)
|
|
1015
|
+
throw new Error('commit epoch outside u64 range');
|
|
1016
|
+
if (exact !== undefined && exact !== parsed)
|
|
1017
|
+
throw new Error('conflicting commit epoch fields');
|
|
1018
|
+
exact = parsed;
|
|
1019
|
+
};
|
|
1020
|
+
for (const source of sources) {
|
|
1021
|
+
merge(source.lastCommitEpoch, false);
|
|
1022
|
+
merge(source.epoch, false);
|
|
1023
|
+
merge(source.lastCommitEpochText, true);
|
|
1024
|
+
merge(source.epochText, true);
|
|
1025
|
+
}
|
|
1026
|
+
return exact;
|
|
1027
|
+
}
|
|
1028
|
+
function nativeRemoteWriteError(error) {
|
|
1029
|
+
const original = error instanceof Error ? error : new Error(String(error));
|
|
1030
|
+
if (error instanceof KitError)
|
|
1031
|
+
return error;
|
|
1032
|
+
if (error === null || typeof error !== 'object')
|
|
1033
|
+
return original;
|
|
1034
|
+
const top = error;
|
|
1035
|
+
const details = top.remoteQueryError !== null && typeof top.remoteQueryError === 'object'
|
|
1036
|
+
? top.remoteQueryError
|
|
1037
|
+
: top;
|
|
1038
|
+
const candidateCodes = [top.code, details.code].filter((value) => typeof value === 'string');
|
|
1039
|
+
if (!candidateCodes.some((code) => code === 'COMMIT_OUTCOME'
|
|
1040
|
+
|| code === 'QUERY_OUTCOME_UNKNOWN'))
|
|
1041
|
+
return original;
|
|
1042
|
+
try {
|
|
1043
|
+
if (details !== top && !hasOnlyKeys(details, [
|
|
1044
|
+
'code', 'message', 'queryId', 'status', 'httpStatus', 'outcomeKnown', 'committed',
|
|
1045
|
+
'epoch', 'epochText', 'committedStatements', 'lastCommitEpoch',
|
|
1046
|
+
'lastCommitEpochText', 'firstCommitStatementIndex', 'lastCommitStatementIndex',
|
|
1047
|
+
'completedStatements', 'statementIndex', 'cancelOutcome', 'cancellationReason',
|
|
1048
|
+
'retryable', 'serverState', 'terminalState'
|
|
1049
|
+
]))
|
|
1050
|
+
throw new Error('unknown native response field');
|
|
1051
|
+
const code = exactNativeValue([top.code, details.code], (value) => value === 'COMMIT_OUTCOME' || value === 'QUERY_OUTCOME_UNKNOWN');
|
|
1052
|
+
const queryId = exactNativeValue([top.queryId, top.query_id, details.queryId, details.query_id], (value) => typeof value === 'string'
|
|
1053
|
+
&& (value === 'unknown' || /^[0-9a-f]{32}$/u.test(value))) ?? 'unknown';
|
|
1054
|
+
const message = exactNativeValue([top.message, details.message], (value) => typeof value === 'string') ?? original.message;
|
|
1055
|
+
const retryable = exactNativeValue([top.retryable, details.retryable], (value) => typeof value === 'boolean');
|
|
1056
|
+
const serverState = exactNativeValue([top.serverState, details.serverState], (value) => typeof value === 'string');
|
|
1057
|
+
const status = exactNativeValue([top.status, details.status], (value) => typeof value === 'string');
|
|
1058
|
+
const committed = exactNativeValue([top.committed, details.committed], (value) => value === null || typeof value === 'boolean');
|
|
1059
|
+
const outcomeKnown = exactNativeValue([top.outcomeKnown, details.outcomeKnown], (value) => typeof value === 'boolean');
|
|
1060
|
+
for (const field of [
|
|
1061
|
+
'committedStatements', 'firstCommitStatementIndex', 'lastCommitStatementIndex',
|
|
1062
|
+
'completedStatements', 'statementIndex'
|
|
1063
|
+
]) {
|
|
1064
|
+
exactNativeValue([top[field], details[field]], (value) => value === null || isNonNegativeInteger(value));
|
|
1065
|
+
}
|
|
1066
|
+
for (const field of [
|
|
1067
|
+
'cancelOutcome', 'cancellationReason', 'terminalState'
|
|
1068
|
+
]) {
|
|
1069
|
+
exactNativeValue([top[field], details[field]], (value) => value === null || typeof value === 'string');
|
|
1070
|
+
}
|
|
1071
|
+
const lastCommitEpoch = nativeRemoteWriteEpoch(details, top);
|
|
1072
|
+
const metadata = {
|
|
1073
|
+
retryable,
|
|
1074
|
+
serverState: serverState ?? status
|
|
1075
|
+
};
|
|
1076
|
+
if (code === 'QUERY_OUTCOME_UNKNOWN') {
|
|
1077
|
+
if (committed !== null || outcomeKnown !== false || retryable !== false) {
|
|
1078
|
+
throw new Error('invalid unknown outcome metadata');
|
|
1079
|
+
}
|
|
1080
|
+
return new QueryOutcomeUnknownError(queryId, message, metadata);
|
|
1081
|
+
}
|
|
1082
|
+
if (committed !== true || outcomeKnown !== true || retryable !== false
|
|
1083
|
+
|| lastCommitEpoch === undefined) {
|
|
1084
|
+
throw new Error('invalid committed outcome metadata');
|
|
1085
|
+
}
|
|
1086
|
+
return new CommitOutcomeError(queryId, message || 'remote write committed but response failed', { committed: true, lastCommitEpoch }, metadata);
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
return new QueryOutcomeUnknownError('unknown', 'invalid committed outcome metadata from server', { retryable: false, serverState: 'invalid_outcome' });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function parseRemoteWriteJson(response, operation, commitProven = true) {
|
|
1093
|
+
try {
|
|
1094
|
+
return parseJsonStrict(response);
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
if (!commitProven) {
|
|
1098
|
+
throw new QueryOutcomeUnknownError('unknown', `remote ${operation} finished but its response was invalid`, { retryable: false, serverState: 'invalid_response' });
|
|
1099
|
+
}
|
|
1100
|
+
throw new CommitOutcomeError('unknown', `remote ${operation} committed but its response was invalid`, { committed: true }, { retryable: false, serverState: 'invalid_response' });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function parseProcedureCallResponse(response) {
|
|
1104
|
+
const value = parseRemoteWriteJson(response, 'procedure call', false);
|
|
1105
|
+
const invalid = () => {
|
|
1106
|
+
throw new QueryOutcomeUnknownError('unknown', 'remote procedure call returned invalid commit metadata', { retryable: false, serverState: 'invalid_response' });
|
|
1107
|
+
};
|
|
1108
|
+
if (value === null || typeof value !== 'object')
|
|
1109
|
+
return invalid();
|
|
1110
|
+
const body = value;
|
|
1111
|
+
const fields = ['status', 'committed', 'epoch', 'epoch_text', 'result'];
|
|
1112
|
+
if (!hasOnlyKeys(body, fields) || !hasAllKeys(body, fields)
|
|
1113
|
+
|| body.status !== 'ok' || typeof body.committed !== 'boolean')
|
|
1114
|
+
return invalid();
|
|
1115
|
+
if (body.committed) {
|
|
1116
|
+
if (!isNonNegativeInteger(body.epoch) || typeof body.epoch_text !== 'string'
|
|
1117
|
+
|| !isOptionalEpoch(body.epoch, body.epoch_text))
|
|
1118
|
+
return invalid();
|
|
1119
|
+
}
|
|
1120
|
+
else if (body.epoch !== null || body.epoch_text !== null)
|
|
1121
|
+
return invalid();
|
|
1122
|
+
return value;
|
|
1123
|
+
}
|
|
8
1124
|
/**
|
|
9
1125
|
* A thin Kit client for a running `mongreldb-server` daemon.
|
|
10
1126
|
*
|
|
@@ -20,11 +1136,70 @@ import { createVirtualTableSql, dropVirtualTableSql } from './external.js';
|
|
|
20
1136
|
export class RemoteDatabase {
|
|
21
1137
|
inner;
|
|
22
1138
|
url;
|
|
1139
|
+
#authorization;
|
|
23
1140
|
capabilities;
|
|
24
1141
|
/** Connect (lazily) to a daemon at `url`, e.g. `http://127.0.0.1:8453`. */
|
|
25
|
-
constructor(url) {
|
|
26
|
-
|
|
27
|
-
|
|
1142
|
+
constructor(url, options = {}) {
|
|
1143
|
+
let parsed;
|
|
1144
|
+
try {
|
|
1145
|
+
parsed = new URL(url);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
throw new TypeError('remote URL must be a valid http:// or https:// URL');
|
|
1149
|
+
}
|
|
1150
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
|
|
1151
|
+
throw new TypeError('remote URL must use http:// or https:// and include a host');
|
|
1152
|
+
}
|
|
1153
|
+
if (parsed.username || parsed.password) {
|
|
1154
|
+
throw new TypeError('remote credentials must use the auth option, not the URL');
|
|
1155
|
+
}
|
|
1156
|
+
if (parsed.search || parsed.hash) {
|
|
1157
|
+
throw new TypeError('remote URL must not include a query or fragment');
|
|
1158
|
+
}
|
|
1159
|
+
if (options.auth && 'bearerToken' in options.auth
|
|
1160
|
+
&& (typeof options.auth.bearerToken !== 'string'
|
|
1161
|
+
|| options.auth.bearerToken.trim().length === 0
|
|
1162
|
+
|| containsHttpHeaderControl(options.auth.bearerToken))) {
|
|
1163
|
+
throw new TypeError('bearer token must not be empty');
|
|
1164
|
+
}
|
|
1165
|
+
if (options.auth && !('bearerToken' in options.auth)
|
|
1166
|
+
&& (typeof options.auth.username !== 'string'
|
|
1167
|
+
|| options.auth.username.length === 0
|
|
1168
|
+
|| options.auth.username.includes(':')
|
|
1169
|
+
|| containsHttpHeaderControl(options.auth.username)
|
|
1170
|
+
|| typeof options.auth.password !== 'string'
|
|
1171
|
+
|| containsHttpHeaderControl(options.auth.password))) {
|
|
1172
|
+
throw new TypeError('basic-auth username must be non-empty and contain no colon');
|
|
1173
|
+
}
|
|
1174
|
+
this.url = `${parsed.origin}${parsed.pathname}`.replace(/\/$/, '');
|
|
1175
|
+
const nativeOptions = options.auth && 'bearerToken' in options.auth
|
|
1176
|
+
? { bearerToken: options.auth.bearerToken }
|
|
1177
|
+
: options.auth
|
|
1178
|
+
? { username: options.auth.username, password: options.auth.password }
|
|
1179
|
+
: undefined;
|
|
1180
|
+
this.#authorization = options.auth && 'bearerToken' in options.auth
|
|
1181
|
+
? `Bearer ${options.auth.bearerToken}`
|
|
1182
|
+
: options.auth
|
|
1183
|
+
? `Basic ${Buffer.from(`${options.auth.username}:${options.auth.password}`, 'utf8').toString('base64')}`
|
|
1184
|
+
: undefined;
|
|
1185
|
+
const NativeRemote = NativeRemoteDatabase;
|
|
1186
|
+
this.inner = new NativeRemote(this.url, nativeOptions);
|
|
1187
|
+
}
|
|
1188
|
+
request(path, init = {}) {
|
|
1189
|
+
const headers = new Headers(init.headers);
|
|
1190
|
+
if (this.#authorization)
|
|
1191
|
+
headers.set('authorization', this.#authorization);
|
|
1192
|
+
return fetch(`${this.url}${path}`, { ...init, headers });
|
|
1193
|
+
}
|
|
1194
|
+
async recoveryRequest(path, init, timeoutMs) {
|
|
1195
|
+
const controller = new AbortController();
|
|
1196
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1197
|
+
try {
|
|
1198
|
+
return await this.request(path, { ...init, signal: controller.signal });
|
|
1199
|
+
}
|
|
1200
|
+
finally {
|
|
1201
|
+
clearTimeout(timer);
|
|
1202
|
+
}
|
|
28
1203
|
}
|
|
29
1204
|
/** Liveness check; returns the server's health string (throws if down). */
|
|
30
1205
|
health() {
|
|
@@ -49,119 +1224,631 @@ export class RemoteDatabase {
|
|
|
49
1224
|
}
|
|
50
1225
|
/** Run a SQL query without blocking the Node event loop. */
|
|
51
1226
|
async sql(sql, options) {
|
|
52
|
-
|
|
53
|
-
return this.startSql(sql, options).result;
|
|
54
|
-
}
|
|
55
|
-
return this.executeSql(sql);
|
|
1227
|
+
return this.startSql(sql, options).result;
|
|
56
1228
|
}
|
|
57
1229
|
async sqlRows(sql, options) {
|
|
58
1230
|
return [...(await this.sql(sql, options))].map((row) => ({ ...row }));
|
|
59
1231
|
}
|
|
60
1232
|
startSql(sql, options = {}) {
|
|
61
|
-
const queryId = options.queryId ?? randomBytes(16).toString('hex');
|
|
1233
|
+
const queryId = normalizeQueryId(options.queryId ?? randomBytes(16).toString('hex'));
|
|
62
1234
|
if (options.timeoutMs !== undefined && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) {
|
|
63
1235
|
throw new RangeError('timeoutMs must be a positive safe integer');
|
|
64
1236
|
}
|
|
1237
|
+
for (const [name, value] of [['maxOutputRows', options.maxOutputRows], ['maxOutputBytes', options.maxOutputBytes]]) {
|
|
1238
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
1239
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
65
1242
|
if (options.signal?.aborted) {
|
|
66
1243
|
return {
|
|
67
1244
|
id: queryId,
|
|
68
|
-
result: Promise.reject(
|
|
69
|
-
cancel: async () =>
|
|
1245
|
+
result: Promise.reject(preExecutionCancellationError(queryId, 'pre_cancelled')),
|
|
1246
|
+
cancel: async () => 'pre_cancelled',
|
|
1247
|
+
status: async () => ({
|
|
1248
|
+
queryId,
|
|
1249
|
+
phase: 'pre_cancelled',
|
|
1250
|
+
terminalState: 'cancelled_before_start',
|
|
1251
|
+
serverState: 'pre_cancelled',
|
|
1252
|
+
operation: 'sql',
|
|
1253
|
+
committed: false,
|
|
1254
|
+
durableOutcome: { committed: false, committedStatements: 0 },
|
|
1255
|
+
terminalErrorCode: 'QUERY_CANCELLED',
|
|
1256
|
+
completedStatements: 0,
|
|
1257
|
+
statementIndex: 0,
|
|
1258
|
+
cancellationReason: 'client_request',
|
|
1259
|
+
cancelOutcome: 'pre_cancelled',
|
|
1260
|
+
retryable: false
|
|
1261
|
+
})
|
|
70
1262
|
};
|
|
71
1263
|
}
|
|
72
1264
|
const controller = new AbortController();
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1265
|
+
let executionStarted = false;
|
|
1266
|
+
let cancelPromise;
|
|
1267
|
+
let signalCancelPromise;
|
|
1268
|
+
const applyCancelOutcome = (outcome) => {
|
|
1269
|
+
if (outcome === 'accepted' || outcome === 'already_cancelling' || outcome === 'pre_cancelled') {
|
|
1270
|
+
controller.abort();
|
|
1271
|
+
}
|
|
1272
|
+
return outcome;
|
|
1273
|
+
};
|
|
1274
|
+
const cancel = () => {
|
|
1275
|
+
cancelPromise ??= this.cancelSql(queryId).then(applyCancelOutcome);
|
|
1276
|
+
return cancelPromise;
|
|
1277
|
+
};
|
|
1278
|
+
const cancelForSignal = () => {
|
|
1279
|
+
signalCancelPromise ??= this.cancelSqlForSignal(queryId).then(applyCancelOutcome);
|
|
1280
|
+
return signalCancelPromise;
|
|
81
1281
|
};
|
|
82
1282
|
const onAbort = () => {
|
|
83
|
-
void
|
|
1283
|
+
void cancelForSignal().catch(() => controller.abort());
|
|
84
1284
|
};
|
|
85
1285
|
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
86
1286
|
const result = this.requireSqlCancellation()
|
|
87
|
-
.then(() =>
|
|
88
|
-
.
|
|
89
|
-
|
|
1287
|
+
.then(async () => {
|
|
1288
|
+
if (options.signal?.aborted) {
|
|
1289
|
+
const outcome = await cancelForSignal();
|
|
1290
|
+
throw preExecutionCancellationError(queryId, outcome);
|
|
1291
|
+
}
|
|
1292
|
+
executionStarted = true;
|
|
1293
|
+
return this.executeSql(sql, queryId, options.timeoutMs, options.maxOutputRows, options.maxOutputBytes, controller.signal);
|
|
1294
|
+
})
|
|
1295
|
+
.catch(async (error) => {
|
|
1296
|
+
const outcome = await (signalCancelPromise ?? cancelPromise)?.catch(() => undefined);
|
|
1297
|
+
if (!executionStarted && options.signal?.aborted) {
|
|
1298
|
+
if (error instanceof CapabilityUnsupportedError)
|
|
1299
|
+
throw error;
|
|
1300
|
+
if (error instanceof KitError && outcome === undefined)
|
|
1301
|
+
throw error;
|
|
1302
|
+
throw preExecutionCancellationError(queryId, outcome);
|
|
1303
|
+
}
|
|
1304
|
+
if (outcome === 'pre_cancelled') {
|
|
90
1305
|
throw new QueryCancelledError(queryId);
|
|
91
1306
|
}
|
|
92
|
-
|
|
1307
|
+
if (outcome === 'accepted' || outcome === 'already_cancelling'
|
|
1308
|
+
|| outcome === 'too_late' || outcome === 'already_finished') {
|
|
1309
|
+
return this.resolveLostQueryOutcome(queryId, error);
|
|
1310
|
+
}
|
|
1311
|
+
if (error instanceof KitError)
|
|
1312
|
+
throw error;
|
|
1313
|
+
return this.resolveLostQueryOutcome(queryId, error);
|
|
93
1314
|
})
|
|
94
1315
|
.finally(() => options.signal?.removeEventListener('abort', onAbort));
|
|
95
|
-
return { id: queryId, result, cancel };
|
|
1316
|
+
return { id: queryId, result, cancel, status: () => this.queryStatus(queryId) };
|
|
96
1317
|
}
|
|
97
1318
|
async cancelSql(queryId) {
|
|
1319
|
+
queryId = normalizeQueryId(queryId);
|
|
98
1320
|
await this.requireSqlCancellation();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1321
|
+
return this.sendCancelSql(queryId);
|
|
1322
|
+
}
|
|
1323
|
+
async cancelSqlForSignal(queryId) {
|
|
1324
|
+
await this.requireSqlCancellation();
|
|
1325
|
+
return this.sendCancelSql(queryId, SQL_RECOVERY_REQUEST_TIMEOUT_MS);
|
|
1326
|
+
}
|
|
1327
|
+
async sendCancelSql(queryId, timeoutMs) {
|
|
1328
|
+
const path = `/queries/${queryId}/cancel`;
|
|
1329
|
+
const response = timeoutMs === undefined
|
|
1330
|
+
? await this.request(path, { method: 'POST' })
|
|
1331
|
+
: await this.recoveryRequest(path, { method: 'POST' }, timeoutMs);
|
|
1332
|
+
if ([200, 202, 404, 409].includes(response.status)) {
|
|
1333
|
+
const body = await responseJsonStrict(response).catch(() => {
|
|
1334
|
+
throw new KitError('SQL cancellation response was not valid JSON', 'REMOTE_PROTOCOL');
|
|
1335
|
+
});
|
|
1336
|
+
return validateCancelResponse(body, queryId, response.status);
|
|
1337
|
+
}
|
|
102
1338
|
throw new KitError(`SQL cancellation failed with HTTP ${response.status}`);
|
|
103
1339
|
}
|
|
104
|
-
async
|
|
105
|
-
|
|
1340
|
+
async queryStatus(queryId) {
|
|
1341
|
+
queryId = normalizeQueryId(queryId);
|
|
1342
|
+
const status = await this.rawQueryStatus(queryId);
|
|
1343
|
+
const committed = mergedCommitState(status.outcome?.committed, status.committed);
|
|
1344
|
+
return {
|
|
1345
|
+
queryId: status.query_id,
|
|
1346
|
+
phase: status.server_state || status.state,
|
|
1347
|
+
terminalState: status.terminal_state ?? undefined,
|
|
1348
|
+
serverState: status.server_state || status.state,
|
|
1349
|
+
operation: status.operation ?? 'sql',
|
|
1350
|
+
committed,
|
|
1351
|
+
durableOutcome: {
|
|
1352
|
+
committed,
|
|
1353
|
+
committedStatements: maxKnown(status.outcome?.committed_statements, status.committed_statements) ?? null,
|
|
1354
|
+
lastCommitEpoch: remoteEpoch(status.outcome?.last_commit_epoch_text ?? status.last_commit_epoch_text, status.outcome?.last_commit_epoch ?? status.last_commit_epoch),
|
|
1355
|
+
firstCommitStatementIndex: status.outcome?.first_commit_statement_index
|
|
1356
|
+
?? status.first_commit_statement_index ?? undefined,
|
|
1357
|
+
lastCommitStatementIndex: status.outcome?.last_commit_statement_index
|
|
1358
|
+
?? status.last_commit_statement_index ?? undefined
|
|
1359
|
+
},
|
|
1360
|
+
terminalErrorCode: status.terminal_error?.code,
|
|
1361
|
+
terminalErrorCategory: status.terminal_error?.category,
|
|
1362
|
+
completedStatements: status.outcome?.completed_statements ?? status.completed_statements ?? null,
|
|
1363
|
+
statementIndex: status.outcome?.statement_index ?? status.statement_index ?? null,
|
|
1364
|
+
cancellationReason: status.cancellation_reason ?? 'none',
|
|
1365
|
+
cancelOutcome: status.cancel_outcome ?? undefined,
|
|
1366
|
+
retryable: status.retryable
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
async sqlPage(sql, options) {
|
|
1370
|
+
await this.requireSqlPagination();
|
|
1371
|
+
if (options.projection.length === 0 || options.projection.length > 128
|
|
1372
|
+
|| options.projection.some((column) => typeof column !== 'string'
|
|
1373
|
+
|| column.length === 0 || column === '*'
|
|
1374
|
+
|| Buffer.byteLength(column, 'utf8') > 256)
|
|
1375
|
+
|| new Set(options.projection).size !== options.projection.length
|
|
1376
|
+
|| options.projection.reduce((bytes, column) => bytes + Buffer.byteLength(column, 'utf8'), 0) > 16 * 1024) {
|
|
1377
|
+
throw new RangeError('projection must contain 1 to 128 unique explicit columns');
|
|
1378
|
+
}
|
|
1379
|
+
for (const [name, value] of [
|
|
1380
|
+
['pageSizeRows', options.pageSizeRows],
|
|
1381
|
+
['timeoutMs', options.timeoutMs],
|
|
1382
|
+
['maxPageBytes', options.maxPageBytes],
|
|
1383
|
+
['maxPageTokens', options.maxPageTokens],
|
|
1384
|
+
['maxOutputRows', options.maxOutputRows],
|
|
1385
|
+
['maxOutputBytes', options.maxOutputBytes]
|
|
1386
|
+
]) {
|
|
1387
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
1388
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const queryId = normalizeQueryId(options.queryId ?? randomBytes(16).toString('hex'));
|
|
1392
|
+
let response;
|
|
1393
|
+
try {
|
|
1394
|
+
response = await this.request('/sql', {
|
|
1395
|
+
method: 'POST',
|
|
1396
|
+
headers: { 'content-type': 'application/json' },
|
|
1397
|
+
body: JSON.stringify({
|
|
1398
|
+
sql,
|
|
1399
|
+
format: 'json',
|
|
1400
|
+
query_id: queryId,
|
|
1401
|
+
timeout_ms: options.timeoutMs,
|
|
1402
|
+
max_output_rows: options.maxOutputRows,
|
|
1403
|
+
max_output_bytes: options.maxOutputBytes,
|
|
1404
|
+
pagination: {
|
|
1405
|
+
page_size_rows: options.pageSizeRows,
|
|
1406
|
+
projection: options.projection,
|
|
1407
|
+
max_page_bytes: options.maxPageBytes,
|
|
1408
|
+
max_page_tokens: options.maxPageTokens
|
|
1409
|
+
}
|
|
1410
|
+
})
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
catch (error) {
|
|
1414
|
+
return this.resolveLostQueryOutcome(queryId, error);
|
|
1415
|
+
}
|
|
1416
|
+
if (!response.ok) {
|
|
1417
|
+
const error = await remoteSqlError(response, queryId);
|
|
1418
|
+
if (error instanceof QueryOutcomeUnknownError) {
|
|
1419
|
+
return this.resolveLostQueryOutcome(queryId, error);
|
|
1420
|
+
}
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
if (response.headers.get('x-mongreldb-query-id') !== queryId) {
|
|
1424
|
+
return this.resolveLostQueryOutcome(queryId, new Error('SQL response x-mongreldb-query-id does not match the request'));
|
|
1425
|
+
}
|
|
1426
|
+
const body = await responseJsonStrict(response, MAX_PAGE_JSON_RESPONSE_BYTES).catch((error) => this.resolveLostQueryOutcome(queryId, error));
|
|
1427
|
+
if (!isRemoteSqlPage(body, options)) {
|
|
1428
|
+
return this.resolveLostQueryOutcome(queryId, new Error('SQL pagination response was not a valid retained page'));
|
|
1429
|
+
}
|
|
1430
|
+
return remoteSqlPage(body);
|
|
1431
|
+
}
|
|
1432
|
+
async continueSqlPage(cursor) {
|
|
1433
|
+
await this.requireSqlPagination();
|
|
1434
|
+
if (cursor.length === 0 || Buffer.byteLength(cursor, 'utf8') > 2_048) {
|
|
1435
|
+
throw new RangeError('cursor must contain 1 to 2048 UTF-8 bytes');
|
|
1436
|
+
}
|
|
1437
|
+
const response = await this.request('/sql/continue', {
|
|
1438
|
+
method: 'POST',
|
|
1439
|
+
headers: { 'content-type': 'application/json' },
|
|
1440
|
+
body: JSON.stringify({ cursor })
|
|
1441
|
+
});
|
|
1442
|
+
if (!response.ok)
|
|
1443
|
+
throw await remoteSqlError(response, 'unknown');
|
|
1444
|
+
const body = await responseJsonStrict(response, MAX_PAGE_JSON_RESPONSE_BYTES).catch((error) => {
|
|
1445
|
+
throw new SerializationError('unknown', error instanceof Error ? error.message : 'SQL continuation response was not valid JSON', { committed: false });
|
|
1446
|
+
});
|
|
1447
|
+
if (!isRemoteSqlPage(body)) {
|
|
1448
|
+
throw new SerializationError('unknown', 'SQL continuation response was not a valid retained page', { committed: false });
|
|
1449
|
+
}
|
|
1450
|
+
return remoteSqlPage(body);
|
|
1451
|
+
}
|
|
1452
|
+
async executeIdempotentSql(sql, options) {
|
|
1453
|
+
await this.requireSqlIdempotency();
|
|
1454
|
+
if (options.idempotencyKey.length === 0
|
|
1455
|
+
|| Buffer.byteLength(options.idempotencyKey, 'utf8') > 256) {
|
|
1456
|
+
throw new RangeError('idempotencyKey must contain 1 to 256 bytes');
|
|
1457
|
+
}
|
|
1458
|
+
for (const [name, value] of [
|
|
1459
|
+
['timeoutMs', options.timeoutMs],
|
|
1460
|
+
['maxOutputRows', options.maxOutputRows],
|
|
1461
|
+
['maxOutputBytes', options.maxOutputBytes]
|
|
1462
|
+
]) {
|
|
1463
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
1464
|
+
throw new RangeError(`${name} must be a positive safe integer`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const queryId = normalizeQueryId(options.queryId ?? randomBytes(16).toString('hex'));
|
|
1468
|
+
try {
|
|
1469
|
+
return await this.executeIdempotentSqlOnce(sql, options, queryId);
|
|
1470
|
+
}
|
|
1471
|
+
catch (error) {
|
|
1472
|
+
if (!(error instanceof RetryIdempotentSqlError))
|
|
1473
|
+
throw error;
|
|
1474
|
+
await this.requireFreshSqlIdempotency();
|
|
1475
|
+
let replayQueryId = randomBytes(16).toString('hex');
|
|
1476
|
+
while (replayQueryId === queryId)
|
|
1477
|
+
replayQueryId = randomBytes(16).toString('hex');
|
|
1478
|
+
try {
|
|
1479
|
+
return await this.executeIdempotentSqlOnce(sql, options, replayQueryId, queryId);
|
|
1480
|
+
}
|
|
1481
|
+
catch (replayError) {
|
|
1482
|
+
if (replayError instanceof RetryIdempotentSqlError)
|
|
1483
|
+
throw replayError.outcome;
|
|
1484
|
+
throw replayError;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
async executeIdempotentSqlOnce(sql, options, queryId, expectedOriginalQueryId) {
|
|
1489
|
+
let response;
|
|
1490
|
+
try {
|
|
1491
|
+
response = await this.request('/sql', {
|
|
1492
|
+
method: 'POST',
|
|
1493
|
+
headers: { 'content-type': 'application/json' },
|
|
1494
|
+
body: JSON.stringify({
|
|
1495
|
+
sql,
|
|
1496
|
+
format: 'json',
|
|
1497
|
+
query_id: queryId,
|
|
1498
|
+
timeout_ms: options.timeoutMs,
|
|
1499
|
+
max_output_rows: options.maxOutputRows,
|
|
1500
|
+
max_output_bytes: options.maxOutputBytes,
|
|
1501
|
+
idempotency_key: options.idempotencyKey
|
|
1502
|
+
})
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
catch (error) {
|
|
1506
|
+
return this.resolveIdempotentSqlLoss(queryId, error);
|
|
1507
|
+
}
|
|
1508
|
+
if (!response.ok) {
|
|
1509
|
+
const error = await remoteSqlError(response, queryId);
|
|
1510
|
+
if (error instanceof QueryOutcomeUnknownError) {
|
|
1511
|
+
return this.resolveIdempotentSqlLoss(queryId, error);
|
|
1512
|
+
}
|
|
1513
|
+
throw error;
|
|
1514
|
+
}
|
|
1515
|
+
const body = await responseJsonStrict(response).catch((error) => this.resolveIdempotentSqlLoss(queryId, error));
|
|
1516
|
+
if (!isRemoteSqlWriteReceipt(body, queryId, expectedOriginalQueryId)) {
|
|
1517
|
+
return this.resolveIdempotentSqlLoss(queryId, new Error('SQL idempotency response was not a valid durable receipt'));
|
|
1518
|
+
}
|
|
1519
|
+
const receipt = {
|
|
1520
|
+
queryId: body.query_id,
|
|
1521
|
+
originalQueryId: body.original_query_id,
|
|
1522
|
+
status: body.status,
|
|
1523
|
+
terminalState: body.terminal_state ?? undefined,
|
|
1524
|
+
serverState: body.server_state ?? undefined,
|
|
1525
|
+
cancelOutcome: body.cancel_outcome ?? undefined,
|
|
1526
|
+
cancellationReason: body.cancellation_reason ?? undefined,
|
|
1527
|
+
committed: body.committed,
|
|
1528
|
+
committedStatements: body.committed_statements,
|
|
1529
|
+
lastCommitEpoch: remoteEpoch(body.outcome.last_commit_epoch_text ?? body.last_commit_epoch_text, body.outcome.last_commit_epoch ?? body.last_commit_epoch),
|
|
1530
|
+
firstCommitStatementIndex: body.first_commit_statement_index
|
|
1531
|
+
?? body.outcome.first_commit_statement_index
|
|
1532
|
+
?? undefined,
|
|
1533
|
+
lastCommitStatementIndex: body.last_commit_statement_index
|
|
1534
|
+
?? body.outcome.last_commit_statement_index
|
|
1535
|
+
?? undefined,
|
|
1536
|
+
completedStatements: body.completed_statements,
|
|
1537
|
+
statementIndex: body.statement_index,
|
|
1538
|
+
retryable: body.retryable,
|
|
1539
|
+
idempotencyReplayed: body.idempotency_replayed,
|
|
1540
|
+
idempotencyPersisted: body.idempotency_persisted,
|
|
1541
|
+
idempotencyExpiresAtMs: body.idempotency_expires_at_ms,
|
|
1542
|
+
terminalError: body.terminal_error ?? undefined
|
|
1543
|
+
};
|
|
1544
|
+
if (response.headers.get('x-mongreldb-query-id') !== queryId) {
|
|
1545
|
+
if (receipt.committed) {
|
|
1546
|
+
throw new CommitOutcomeError(queryId, 'SQL committed but x-mongreldb-query-id did not match the request', {
|
|
1547
|
+
committed: true,
|
|
1548
|
+
committedStatements: receipt.committedStatements,
|
|
1549
|
+
lastCommitEpoch: receipt.lastCommitEpoch,
|
|
1550
|
+
firstCommitStatementIndex: receipt.firstCommitStatementIndex,
|
|
1551
|
+
lastCommitStatementIndex: receipt.lastCommitStatementIndex,
|
|
1552
|
+
completedStatements: receipt.completedStatements,
|
|
1553
|
+
statementIndex: receipt.statementIndex
|
|
1554
|
+
}, {
|
|
1555
|
+
cancelOutcome: receipt.cancelOutcome,
|
|
1556
|
+
cancellationReason: receipt.cancellationReason,
|
|
1557
|
+
retryable: false,
|
|
1558
|
+
serverState: receipt.serverState
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
return this.resolveIdempotentSqlLoss(queryId, new Error('SQL response x-mongreldb-query-id does not match the request'));
|
|
1562
|
+
}
|
|
1563
|
+
return receipt;
|
|
1564
|
+
}
|
|
1565
|
+
async resolveIdempotentSqlLoss(queryId, cause) {
|
|
1566
|
+
let status;
|
|
1567
|
+
try {
|
|
1568
|
+
status = await this.rawQueryStatus(queryId, SQL_RECOVERY_REQUEST_TIMEOUT_MS);
|
|
1569
|
+
}
|
|
1570
|
+
catch (error) {
|
|
1571
|
+
if (error instanceof QueryNotRetainedError) {
|
|
1572
|
+
throw new RetryIdempotentSqlError(error);
|
|
1573
|
+
}
|
|
1574
|
+
if (!(error instanceof QueryOutcomeUnknownError)) {
|
|
1575
|
+
return this.resolveLostQueryOutcome(queryId, cause);
|
|
1576
|
+
}
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
return this.resolveLostQueryOutcome(queryId, cause, status);
|
|
1580
|
+
}
|
|
1581
|
+
async rawQueryStatus(queryId, recoveryTimeoutMs) {
|
|
1582
|
+
await this.requireSqlCancellation();
|
|
1583
|
+
const path = `/queries/${queryId}`;
|
|
1584
|
+
const response = recoveryTimeoutMs === undefined
|
|
1585
|
+
? await this.request(path)
|
|
1586
|
+
: await this.recoveryRequest(path, {}, recoveryTimeoutMs);
|
|
1587
|
+
if (response.status === 404) {
|
|
1588
|
+
const body = await responseJsonStrict(response).catch((error) => {
|
|
1589
|
+
throw new InvalidQueryStatusError(queryId, error instanceof Error ? error.message : 'query-not-found response was not valid JSON');
|
|
1590
|
+
});
|
|
1591
|
+
if (!isQueryNotFoundResponse(body, queryId)) {
|
|
1592
|
+
throw new InvalidQueryStatusError(queryId, 'query-not-found response fields were inconsistent');
|
|
1593
|
+
}
|
|
1594
|
+
throw new QueryNotRetainedError(queryId, `query ${queryId} is not retained`);
|
|
1595
|
+
}
|
|
1596
|
+
if (!response.ok)
|
|
1597
|
+
throw new KitError(`query status failed with HTTP ${response.status}`);
|
|
1598
|
+
const body = await responseJsonStrict(response).catch((error) => {
|
|
1599
|
+
throw new InvalidQueryStatusError(queryId, error instanceof Error ? error.message : 'response was not valid JSON');
|
|
1600
|
+
});
|
|
1601
|
+
if (!isRemoteQueryStatus(body, queryId)) {
|
|
1602
|
+
throw new InvalidQueryStatusError(queryId, 'response fields were inconsistent');
|
|
1603
|
+
}
|
|
1604
|
+
return body;
|
|
1605
|
+
}
|
|
1606
|
+
async resolveLostQueryOutcome(queryId, cause, initialStatus) {
|
|
1607
|
+
const deadline = Date.now() + SQL_RECOVERY_WINDOW_MS;
|
|
1608
|
+
let status = initialStatus ?? await this.queryStatusForRecovery(queryId, deadline);
|
|
1609
|
+
if (!status || !recoveryStatusIsDecisive(status)) {
|
|
1610
|
+
await this.cancelSqlForRecovery(queryId, deadline);
|
|
1611
|
+
}
|
|
1612
|
+
while (Date.now() < deadline) {
|
|
1613
|
+
if (status && recoveryStatusIsDecisive(status))
|
|
1614
|
+
break;
|
|
1615
|
+
status = await this.queryStatusForRecovery(queryId, deadline) ?? status;
|
|
1616
|
+
if (status && recoveryStatusIsDecisive(status))
|
|
1617
|
+
break;
|
|
1618
|
+
const remaining = deadline - Date.now();
|
|
1619
|
+
if (remaining > 0) {
|
|
1620
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(SQL_RECOVERY_POLL_INTERVAL_MS, remaining)));
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (!status || !recoveryStatusIsDecisive(status)) {
|
|
1624
|
+
throw new QueryOutcomeUnknownError(queryId, cause instanceof Error ? cause.message : 'SQL transport failed before terminal status was retained');
|
|
1625
|
+
}
|
|
1626
|
+
const outcome = status?.outcome;
|
|
1627
|
+
const message = cause instanceof Error ? cause.message : 'SQL response was lost';
|
|
1628
|
+
const committed = mergedCommitState(outcome?.committed, status.committed);
|
|
1629
|
+
const metadata = {
|
|
1630
|
+
cancelOutcome: status.cancel_outcome ?? undefined,
|
|
1631
|
+
cancellationReason: status.cancellation_reason,
|
|
1632
|
+
retryable: status.retryable,
|
|
1633
|
+
serverState: status.server_state || status.state
|
|
1634
|
+
};
|
|
1635
|
+
if (committed === null) {
|
|
1636
|
+
throw new QueryOutcomeUnknownError(queryId, message, metadata);
|
|
1637
|
+
}
|
|
1638
|
+
const durableOutcome = {
|
|
1639
|
+
committed,
|
|
1640
|
+
committedStatements: maxKnown(outcome?.committed_statements, status.committed_statements),
|
|
1641
|
+
lastCommitEpoch: remoteEpoch(outcome?.last_commit_epoch_text ?? status.last_commit_epoch_text, outcome?.last_commit_epoch ?? status.last_commit_epoch),
|
|
1642
|
+
firstCommitStatementIndex: outcome?.first_commit_statement_index
|
|
1643
|
+
?? status.first_commit_statement_index ?? undefined,
|
|
1644
|
+
lastCommitStatementIndex: outcome?.last_commit_statement_index
|
|
1645
|
+
?? status.last_commit_statement_index ?? undefined,
|
|
1646
|
+
completedStatements: outcome?.completed_statements ?? status.completed_statements ?? undefined,
|
|
1647
|
+
statementIndex: outcome?.statement_index ?? status.statement_index ?? undefined
|
|
1648
|
+
};
|
|
1649
|
+
switch (status.terminal_error?.code) {
|
|
1650
|
+
case 'QUERY_CANCELLED':
|
|
1651
|
+
case 'QUERY_CANCELLED_AFTER_COMMIT':
|
|
1652
|
+
throw new QueryCancelledError(queryId, message, durableOutcome, metadata);
|
|
1653
|
+
case 'DEADLINE_EXCEEDED':
|
|
1654
|
+
case 'DEADLINE_AFTER_COMMIT':
|
|
1655
|
+
throw new QueryTimeoutError(queryId, message, durableOutcome, metadata);
|
|
1656
|
+
case 'RESULT_LIMIT_EXCEEDED':
|
|
1657
|
+
throw new ResultLimitExceededError(queryId, message, durableOutcome, metadata);
|
|
1658
|
+
case 'SERIALIZATION_FAILED':
|
|
1659
|
+
case 'SERIALIZATION_FAILED_AFTER_COMMIT':
|
|
1660
|
+
throw new SerializationError(queryId, message, durableOutcome, metadata);
|
|
1661
|
+
default:
|
|
1662
|
+
if (durableOutcome.committed) {
|
|
1663
|
+
throw new CommitOutcomeError(queryId, message, durableOutcome, metadata);
|
|
1664
|
+
}
|
|
1665
|
+
if (status.state === 'completed') {
|
|
1666
|
+
throw new SerializationError(queryId, message, durableOutcome, metadata);
|
|
1667
|
+
}
|
|
1668
|
+
throw new QueryExecutionError(queryId, status.terminal_error?.code ?? 'QUERY_FAILED', message, durableOutcome, metadata);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
recoveryTimeout(deadline) {
|
|
1672
|
+
const remaining = deadline - Date.now();
|
|
1673
|
+
return remaining > 0
|
|
1674
|
+
? Math.min(SQL_RECOVERY_REQUEST_TIMEOUT_MS, remaining)
|
|
1675
|
+
: undefined;
|
|
1676
|
+
}
|
|
1677
|
+
async queryStatusForRecovery(queryId, deadline) {
|
|
1678
|
+
const timeout = this.recoveryTimeout(deadline);
|
|
1679
|
+
if (timeout === undefined)
|
|
1680
|
+
return undefined;
|
|
1681
|
+
return this.rawQueryStatus(queryId, timeout).catch(() => undefined);
|
|
1682
|
+
}
|
|
1683
|
+
async cancelSqlForRecovery(queryId, deadline) {
|
|
1684
|
+
const timeout = this.recoveryTimeout(deadline);
|
|
1685
|
+
if (timeout === undefined)
|
|
1686
|
+
return;
|
|
1687
|
+
const response = await this.recoveryRequest(`/queries/${queryId}/cancel`, { method: 'POST' }, timeout).catch(() => undefined);
|
|
1688
|
+
if (response && [200, 202, 404, 409].includes(response.status)) {
|
|
1689
|
+
const body = await responseJsonStrict(response).catch(() => undefined);
|
|
1690
|
+
if (body !== undefined) {
|
|
1691
|
+
try {
|
|
1692
|
+
validateCancelResponse(body, queryId, response.status);
|
|
1693
|
+
}
|
|
1694
|
+
catch {
|
|
1695
|
+
// Recovery remains outcome-unknown when control metadata is malformed.
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
async executeSql(sql, queryId, timeoutMs, maxOutputRows, maxOutputBytes, signal) {
|
|
1701
|
+
const response = await this.request('/sql', {
|
|
106
1702
|
method: 'POST',
|
|
107
1703
|
headers: { 'content-type': 'application/json' },
|
|
108
|
-
body: JSON.stringify({
|
|
1704
|
+
body: JSON.stringify({
|
|
1705
|
+
sql,
|
|
1706
|
+
format: 'arrow',
|
|
1707
|
+
query_id: queryId,
|
|
1708
|
+
timeout_ms: timeoutMs,
|
|
1709
|
+
max_output_rows: maxOutputRows,
|
|
1710
|
+
max_output_bytes: maxOutputBytes
|
|
1711
|
+
}),
|
|
109
1712
|
signal
|
|
110
1713
|
});
|
|
111
1714
|
if (!response.ok) {
|
|
112
|
-
|
|
1715
|
+
const error = await remoteSqlError(response, queryId);
|
|
1716
|
+
if (error instanceof QueryOutcomeUnknownError) {
|
|
1717
|
+
return this.resolveLostQueryOutcome(queryId, error);
|
|
1718
|
+
}
|
|
1719
|
+
throw error;
|
|
1720
|
+
}
|
|
1721
|
+
if (response.headers.get('x-mongreldb-query-id') !== queryId) {
|
|
1722
|
+
return this.resolveLostQueryOutcome(queryId, new Error('SQL response x-mongreldb-query-id does not match the request'));
|
|
113
1723
|
}
|
|
114
|
-
const bytes =
|
|
1724
|
+
const bytes = await responseBytesBounded(response, Math.min(maxOutputBytes ?? MAX_PAGE_JSON_RESPONSE_BYTES, MAX_PAGE_JSON_RESPONSE_BYTES));
|
|
115
1725
|
if (bytes.length === 0) {
|
|
116
1726
|
return tableFromJSON([]);
|
|
117
1727
|
}
|
|
118
1728
|
return tableFromIPC(bytes);
|
|
119
1729
|
}
|
|
120
|
-
async
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1730
|
+
async loadCapabilities() {
|
|
1731
|
+
const response = await this.request('/capabilities');
|
|
1732
|
+
if (response.status === 404)
|
|
1733
|
+
return null;
|
|
1734
|
+
if (!response.ok)
|
|
1735
|
+
throw new KitError(`capability request failed with HTTP ${response.status}`);
|
|
1736
|
+
const capabilities = await responseJsonStrict(response).catch(() => {
|
|
1737
|
+
throw new KitError('capability response was not valid bounded JSON', 'REMOTE_PROTOCOL');
|
|
128
1738
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1739
|
+
if (!isRemoteCapabilities(capabilities)) {
|
|
1740
|
+
throw new KitError('capability response had unknown or invalid fields', 'REMOTE_PROTOCOL');
|
|
1741
|
+
}
|
|
1742
|
+
return capabilities;
|
|
1743
|
+
}
|
|
1744
|
+
validateSqlCancellationCapability(capability) {
|
|
1745
|
+
if (capability?.version !== 2 || !capability.client_query_ids || !capability.cancel_endpoint
|
|
1746
|
+
|| !capability.query_status || !capability.pre_registration_cancel) {
|
|
1747
|
+
throw new CapabilityUnsupportedError('server does not support SQL cancellation capability version 2');
|
|
1748
|
+
}
|
|
1749
|
+
return capability;
|
|
1750
|
+
}
|
|
1751
|
+
validateSqlIdempotencyCapability(capability) {
|
|
1752
|
+
if (capability?.version !== 1
|
|
1753
|
+
|| !capability.durable_pre_execution_intent
|
|
1754
|
+
|| !capability.replay_committed_receipt
|
|
1755
|
+
|| !capability.indeterminate_never_reexecutes) {
|
|
1756
|
+
throw new CapabilityUnsupportedError('server does not support durable SQL idempotency capability version 1');
|
|
132
1757
|
}
|
|
133
1758
|
return capability;
|
|
134
1759
|
}
|
|
1760
|
+
async requireSqlCancellation() {
|
|
1761
|
+
this.capabilities ??= this.loadCapabilities();
|
|
1762
|
+
const capability = (await this.capabilities)?.sql_cancellation;
|
|
1763
|
+
return this.validateSqlCancellationCapability(capability);
|
|
1764
|
+
}
|
|
1765
|
+
async requireSqlPagination() {
|
|
1766
|
+
await this.requireSqlCancellation();
|
|
1767
|
+
const capability = (await this.capabilities)?.sql_pagination;
|
|
1768
|
+
if (capability?.version !== 1
|
|
1769
|
+
|| capability.continuation_endpoint !== '/sql/continue'
|
|
1770
|
+
|| !capability.retained_snapshot
|
|
1771
|
+
|| !capability.projection_required
|
|
1772
|
+
|| !capability.byte_and_token_hints) {
|
|
1773
|
+
throw new CapabilityUnsupportedError('server does not support SQL pagination capability version 1');
|
|
1774
|
+
}
|
|
1775
|
+
return capability;
|
|
1776
|
+
}
|
|
1777
|
+
async requireSqlIdempotency() {
|
|
1778
|
+
await this.requireSqlCancellation();
|
|
1779
|
+
const capability = (await this.capabilities)?.sql_idempotency;
|
|
1780
|
+
return this.validateSqlIdempotencyCapability(capability);
|
|
1781
|
+
}
|
|
1782
|
+
async requireFreshSqlIdempotency() {
|
|
1783
|
+
const capabilities = await this.loadCapabilities();
|
|
1784
|
+
this.validateSqlCancellationCapability(capabilities?.sql_cancellation);
|
|
1785
|
+
this.validateSqlIdempotencyCapability(capabilities?.sql_idempotency);
|
|
1786
|
+
}
|
|
135
1787
|
/** Flush/commit `table` on the server; returns the new epoch. */
|
|
136
1788
|
commit(table) {
|
|
137
|
-
|
|
1789
|
+
try {
|
|
1790
|
+
return this.inner.commit(table);
|
|
1791
|
+
}
|
|
1792
|
+
catch (error) {
|
|
1793
|
+
throw nativeRemoteWriteError(error);
|
|
1794
|
+
}
|
|
138
1795
|
}
|
|
139
1796
|
createProcedure(spec) {
|
|
140
|
-
|
|
1797
|
+
try {
|
|
1798
|
+
return parseRemoteWriteJson(this.inner.createProcedure({ json: procedureJson(spec) }), 'procedure creation');
|
|
1799
|
+
}
|
|
1800
|
+
catch (error) {
|
|
1801
|
+
throw nativeRemoteWriteError(error);
|
|
1802
|
+
}
|
|
141
1803
|
}
|
|
142
1804
|
dropProcedure(name) {
|
|
143
|
-
|
|
1805
|
+
try {
|
|
1806
|
+
this.inner.dropProcedure(name);
|
|
1807
|
+
}
|
|
1808
|
+
catch (error) {
|
|
1809
|
+
throw nativeRemoteWriteError(error);
|
|
1810
|
+
}
|
|
144
1811
|
}
|
|
145
1812
|
callProcedure(name, opts = {}) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
1813
|
+
try {
|
|
1814
|
+
return parseProcedureCallResponse(this.inner.callProcedure(name, {
|
|
1815
|
+
argsJson: JSON.stringify(opts.args ?? {}),
|
|
1816
|
+
idempotencyKey: opts.idempotencyKey
|
|
1817
|
+
}));
|
|
1818
|
+
}
|
|
1819
|
+
catch (error) {
|
|
1820
|
+
throw nativeRemoteWriteError(error);
|
|
1821
|
+
}
|
|
150
1822
|
}
|
|
151
1823
|
createTrigger(spec) {
|
|
152
|
-
|
|
1824
|
+
try {
|
|
1825
|
+
return parseRemoteWriteJson(this.inner.createTrigger({ json: triggerJson(spec) }), 'trigger creation');
|
|
1826
|
+
}
|
|
1827
|
+
catch (error) {
|
|
1828
|
+
throw nativeRemoteWriteError(error);
|
|
1829
|
+
}
|
|
153
1830
|
}
|
|
154
1831
|
replaceTrigger(name, spec) {
|
|
155
|
-
|
|
1832
|
+
try {
|
|
1833
|
+
return parseRemoteWriteJson(this.inner.replaceTrigger(name, { json: triggerJson(spec) }), 'trigger replacement');
|
|
1834
|
+
}
|
|
1835
|
+
catch (error) {
|
|
1836
|
+
throw nativeRemoteWriteError(error);
|
|
1837
|
+
}
|
|
156
1838
|
}
|
|
157
1839
|
dropTrigger(name) {
|
|
158
|
-
|
|
1840
|
+
try {
|
|
1841
|
+
this.inner.dropTrigger(name);
|
|
1842
|
+
}
|
|
1843
|
+
catch (error) {
|
|
1844
|
+
throw nativeRemoteWriteError(error);
|
|
1845
|
+
}
|
|
159
1846
|
}
|
|
160
1847
|
triggers() {
|
|
161
|
-
return
|
|
1848
|
+
return parseJsonStrict(this.inner.triggers());
|
|
162
1849
|
}
|
|
163
1850
|
trigger(name) {
|
|
164
|
-
return
|
|
1851
|
+
return parseJsonStrict(this.inner.trigger(name));
|
|
165
1852
|
}
|
|
166
1853
|
createVirtualTable(spec) {
|
|
167
1854
|
return this.sql(createVirtualTableSql(spec));
|
|
@@ -180,26 +1867,286 @@ export class RemoteDatabase {
|
|
|
180
1867
|
return this.inner.compactTable(table);
|
|
181
1868
|
}
|
|
182
1869
|
}
|
|
1870
|
+
function isRemoteCapabilities(value) {
|
|
1871
|
+
if (value === null || typeof value !== 'object')
|
|
1872
|
+
return false;
|
|
1873
|
+
const body = value;
|
|
1874
|
+
if (!hasOnlyKeys(body, ['sql_cancellation', 'sql_idempotency', 'sql_pagination']))
|
|
1875
|
+
return false;
|
|
1876
|
+
const cancellation = body.sql_cancellation;
|
|
1877
|
+
if (cancellation !== undefined && (cancellation === null || typeof cancellation !== 'object'
|
|
1878
|
+
|| !hasOnlyKeys(cancellation, [
|
|
1879
|
+
'version', 'client_query_ids', 'cancel_endpoint', 'query_status',
|
|
1880
|
+
'stream_disconnect_cancels', 'pre_registration_cancel'
|
|
1881
|
+
])
|
|
1882
|
+
|| !isNonNegativeInteger(cancellation.version) || cancellation.version > 255
|
|
1883
|
+
|| ['client_query_ids', 'cancel_endpoint', 'query_status',
|
|
1884
|
+
'stream_disconnect_cancels', 'pre_registration_cancel']
|
|
1885
|
+
.some((field) => typeof cancellation[field] !== 'boolean')))
|
|
1886
|
+
return false;
|
|
1887
|
+
const idempotency = body.sql_idempotency;
|
|
1888
|
+
if (idempotency !== undefined && (idempotency === null || typeof idempotency !== 'object'
|
|
1889
|
+
|| !hasOnlyKeys(idempotency, [
|
|
1890
|
+
'version', 'durable_pre_execution_intent', 'replay_committed_receipt',
|
|
1891
|
+
'indeterminate_never_reexecutes'
|
|
1892
|
+
])
|
|
1893
|
+
|| !isNonNegativeInteger(idempotency.version) || idempotency.version > 255
|
|
1894
|
+
|| ['durable_pre_execution_intent', 'replay_committed_receipt',
|
|
1895
|
+
'indeterminate_never_reexecutes']
|
|
1896
|
+
.some((field) => typeof idempotency[field] !== 'boolean')))
|
|
1897
|
+
return false;
|
|
1898
|
+
const pagination = body.sql_pagination;
|
|
1899
|
+
return pagination === undefined || pagination !== null && typeof pagination === 'object'
|
|
1900
|
+
&& hasOnlyKeys(pagination, [
|
|
1901
|
+
'version', 'continuation_endpoint', 'retained_snapshot', 'projection_required',
|
|
1902
|
+
'byte_and_token_hints'
|
|
1903
|
+
])
|
|
1904
|
+
&& isNonNegativeInteger(pagination.version) && pagination.version <= 255
|
|
1905
|
+
&& typeof pagination.continuation_endpoint === 'string'
|
|
1906
|
+
&& ['retained_snapshot', 'projection_required', 'byte_and_token_hints']
|
|
1907
|
+
.every((field) => typeof pagination[field] === 'boolean');
|
|
1908
|
+
}
|
|
1909
|
+
function isRemoteSqlErrorEnvelope(value, queryId) {
|
|
1910
|
+
if (value === null || typeof value !== 'object')
|
|
1911
|
+
return false;
|
|
1912
|
+
const body = value;
|
|
1913
|
+
if (body.outcome === null || typeof body.outcome !== 'object'
|
|
1914
|
+
|| body.error === null || typeof body.error !== 'object')
|
|
1915
|
+
return false;
|
|
1916
|
+
const outcome = body.outcome;
|
|
1917
|
+
const error = body.error;
|
|
1918
|
+
if (!hasOnlyKeys(body, [
|
|
1919
|
+
'query_id', 'status', 'terminal_state', 'committed', 'committed_statements',
|
|
1920
|
+
'last_commit_epoch', 'last_commit_epoch_text', 'first_commit_statement_index',
|
|
1921
|
+
'last_commit_statement_index', 'completed_statements', 'statement_index',
|
|
1922
|
+
'cancel_outcome', 'cancellation_reason', 'retryable', 'server_state', 'outcome',
|
|
1923
|
+
'error', 'max_rows', 'max_bytes'
|
|
1924
|
+
]) || !hasOnlyKeys(outcome, [
|
|
1925
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
1926
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
1927
|
+
'statement_index', 'serialization'
|
|
1928
|
+
]) || !hasOnlyKeys(error, [
|
|
1929
|
+
'code', 'message', 'query_id', 'committed', 'retryable', 'max_rows', 'max_bytes'
|
|
1930
|
+
]) || !hasAllKeys(outcome, [
|
|
1931
|
+
'committed', 'committed_statements', 'last_commit_epoch', 'last_commit_epoch_text',
|
|
1932
|
+
'first_commit_statement_index', 'last_commit_statement_index', 'completed_statements',
|
|
1933
|
+
'statement_index', 'serialization'
|
|
1934
|
+
]))
|
|
1935
|
+
return false;
|
|
1936
|
+
const code = error.code;
|
|
1937
|
+
const status = body.status;
|
|
1938
|
+
if (body.query_id !== queryId || error.query_id !== queryId
|
|
1939
|
+
|| typeof code !== 'string' || code.trim().length === 0
|
|
1940
|
+
|| typeof error.message !== 'string' || error.message.trim().length === 0
|
|
1941
|
+
|| typeof status !== 'string' || body.terminal_state !== status
|
|
1942
|
+
|| !['not_started', 'in_progress', 'succeeded', 'failed', 'unknown']
|
|
1943
|
+
.includes(outcome.serialization)
|
|
1944
|
+
|| typeof body.retryable !== 'boolean' || error.retryable !== body.retryable
|
|
1945
|
+
|| ![null, true, false].includes(body.committed)
|
|
1946
|
+
|| body.committed !== outcome.committed || body.committed !== error.committed)
|
|
1947
|
+
return false;
|
|
1948
|
+
const names = ['committed_statements', 'first_commit_statement_index',
|
|
1949
|
+
'last_commit_statement_index', 'completed_statements', 'statement_index'];
|
|
1950
|
+
if (names.some((name) => !isOptionalNonNegativeInteger(body[name])
|
|
1951
|
+
|| !sameOptional(body[name], outcome[name])))
|
|
1952
|
+
return false;
|
|
1953
|
+
for (const name of ['max_rows', 'max_bytes']) {
|
|
1954
|
+
const topLimit = body[name];
|
|
1955
|
+
const errorLimit = error[name];
|
|
1956
|
+
if (topLimit != null && (!isNonNegativeInteger(topLimit) || topLimit === 0)
|
|
1957
|
+
|| errorLimit != null && (!isNonNegativeInteger(errorLimit) || errorLimit === 0)
|
|
1958
|
+
|| topLimit != null && errorLimit != null && topLimit !== errorLimit)
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
if (!isOptionalEpoch(body.last_commit_epoch, body.last_commit_epoch_text)
|
|
1962
|
+
|| !isOptionalEpoch(outcome.last_commit_epoch, outcome.last_commit_epoch_text))
|
|
1963
|
+
return false;
|
|
1964
|
+
const topEpoch = receiptEpoch(body.last_commit_epoch, body.last_commit_epoch_text);
|
|
1965
|
+
const outcomeEpoch = receiptEpoch(outcome.last_commit_epoch, outcome.last_commit_epoch_text);
|
|
1966
|
+
if (topEpoch !== outcomeEpoch)
|
|
1967
|
+
return false;
|
|
1968
|
+
const committed = body.committed;
|
|
1969
|
+
const committedStatements = body.committed_statements;
|
|
1970
|
+
const first = body.first_commit_statement_index;
|
|
1971
|
+
const last = body.last_commit_statement_index;
|
|
1972
|
+
const completed = body.completed_statements;
|
|
1973
|
+
const statement = body.statement_index;
|
|
1974
|
+
const unknown = code === 'QUERY_OUTCOME_UNKNOWN';
|
|
1975
|
+
if (committed === true) {
|
|
1976
|
+
if (unknown || !COMMITTED_QUERY_STATUSES.has(status)
|
|
1977
|
+
|| committedStatements == null || committedStatements === 0
|
|
1978
|
+
|| topEpoch === undefined || body.last_commit_epoch_text == null
|
|
1979
|
+
|| outcome.last_commit_epoch_text == null || first == null || last == null
|
|
1980
|
+
|| completed == null || statement == null)
|
|
1981
|
+
return false;
|
|
1982
|
+
}
|
|
1983
|
+
else if (committed === false) {
|
|
1984
|
+
if (unknown || !['failed_before_commit', 'cancelled_before_commit',
|
|
1985
|
+
'deadline_before_commit', 'cancelled_before_start'].includes(status)
|
|
1986
|
+
|| committedStatements !== 0 || topEpoch !== undefined
|
|
1987
|
+
|| first != null || last != null || completed == null || statement == null)
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
else if (!unknown || status !== 'outcome_unknown'
|
|
1991
|
+
|| names.some((name) => body[name] != null) || topEpoch !== undefined
|
|
1992
|
+
|| body.retryable !== false)
|
|
1993
|
+
return false;
|
|
1994
|
+
if (first != null && last != null && committedStatements != null && statement != null
|
|
1995
|
+
&& (first > last || committedStatements > last - first + 1 || last > statement))
|
|
1996
|
+
return false;
|
|
1997
|
+
if (completed != null && statement != null
|
|
1998
|
+
&& (statement > completed || completed > statement + 1))
|
|
1999
|
+
return false;
|
|
2000
|
+
const expectedRetryable = ['QUERY_REGISTRY_FULL', 'IDEMPOTENCY_STORE_FULL',
|
|
2001
|
+
'IDEMPOTENCY_STORE_UNAVAILABLE'].includes(code);
|
|
2002
|
+
if (body.retryable !== expectedRetryable)
|
|
2003
|
+
return false;
|
|
2004
|
+
const codeMatchesStatus = {
|
|
2005
|
+
QUERY_OUTCOME_UNKNOWN: status === 'outcome_unknown',
|
|
2006
|
+
QUERY_CANCELLED_AFTER_COMMIT: status === 'cancelled_after_commit' && committed,
|
|
2007
|
+
DEADLINE_AFTER_COMMIT: status === 'deadline_after_commit' && committed,
|
|
2008
|
+
QUERY_CANCELLED: ['cancelled_before_commit', 'cancelled_before_start'].includes(status),
|
|
2009
|
+
DEADLINE_EXCEEDED: status === 'deadline_before_commit',
|
|
2010
|
+
COMMIT_OUTCOME: committed === true,
|
|
2011
|
+
SERIALIZATION_FAILED_AFTER_COMMIT: committed === true
|
|
2012
|
+
}[code] ?? true;
|
|
2013
|
+
const statusMatchesCode = {
|
|
2014
|
+
outcome_unknown: code === 'QUERY_OUTCOME_UNKNOWN',
|
|
2015
|
+
cancelled_after_commit: code === 'QUERY_CANCELLED_AFTER_COMMIT',
|
|
2016
|
+
deadline_after_commit: code === 'DEADLINE_AFTER_COMMIT',
|
|
2017
|
+
cancelled_before_commit: code === 'QUERY_CANCELLED',
|
|
2018
|
+
cancelled_before_start: code === 'QUERY_CANCELLED',
|
|
2019
|
+
deadline_before_commit: code === 'DEADLINE_EXCEEDED'
|
|
2020
|
+
}[status] ?? true;
|
|
2021
|
+
return codeMatchesStatus && statusMatchesCode;
|
|
2022
|
+
}
|
|
2023
|
+
function isSqlCursorErrorEnvelope(value) {
|
|
2024
|
+
if (value === null || typeof value !== 'object')
|
|
2025
|
+
return false;
|
|
2026
|
+
const body = value;
|
|
2027
|
+
const outcome = body.outcome;
|
|
2028
|
+
const error = body.error;
|
|
2029
|
+
const countFields = [
|
|
2030
|
+
'committed_statements', 'first_commit_statement_index',
|
|
2031
|
+
'last_commit_statement_index', 'completed_statements', 'statement_index'
|
|
2032
|
+
];
|
|
2033
|
+
return hasOnlyKeys(body, [
|
|
2034
|
+
'status', 'terminal_state', 'server_state', 'committed', ...countFields,
|
|
2035
|
+
'last_commit_epoch', 'last_commit_epoch_text', 'cancel_outcome',
|
|
2036
|
+
'cancellation_reason', 'retryable', 'outcome', 'error'
|
|
2037
|
+
]) && hasAllKeys(body, [
|
|
2038
|
+
'status', 'terminal_state', 'server_state', 'committed', ...countFields,
|
|
2039
|
+
'last_commit_epoch', 'last_commit_epoch_text', 'cancel_outcome',
|
|
2040
|
+
'cancellation_reason', 'retryable', 'outcome', 'error'
|
|
2041
|
+
])
|
|
2042
|
+
&& outcome !== null && typeof outcome === 'object'
|
|
2043
|
+
&& hasOnlyKeys(outcome, [
|
|
2044
|
+
'committed', ...countFields, 'last_commit_epoch', 'last_commit_epoch_text',
|
|
2045
|
+
'serialization'
|
|
2046
|
+
]) && hasAllKeys(outcome, [
|
|
2047
|
+
'committed', ...countFields, 'last_commit_epoch', 'last_commit_epoch_text',
|
|
2048
|
+
'serialization'
|
|
2049
|
+
])
|
|
2050
|
+
&& error !== null && typeof error === 'object'
|
|
2051
|
+
&& hasOnlyKeys(error, ['code', 'message', 'committed', 'retryable'])
|
|
2052
|
+
&& hasAllKeys(error, ['code', 'message', 'committed', 'retryable'])
|
|
2053
|
+
&& body.status === 'failed_before_commit'
|
|
2054
|
+
&& body.terminal_state === 'failed_before_commit' && body.server_state === 'failed'
|
|
2055
|
+
&& body.committed === false && outcome.committed === false
|
|
2056
|
+
&& body.committed_statements === 0 && outcome.committed_statements === 0
|
|
2057
|
+
&& body.last_commit_epoch === null && outcome.last_commit_epoch === null
|
|
2058
|
+
&& body.last_commit_epoch_text === null && outcome.last_commit_epoch_text === null
|
|
2059
|
+
&& body.first_commit_statement_index === null
|
|
2060
|
+
&& outcome.first_commit_statement_index === null
|
|
2061
|
+
&& body.last_commit_statement_index === null
|
|
2062
|
+
&& outcome.last_commit_statement_index === null
|
|
2063
|
+
&& body.completed_statements === 0 && outcome.completed_statements === 0
|
|
2064
|
+
&& body.statement_index === 0 && outcome.statement_index === 0
|
|
2065
|
+
&& body.cancel_outcome === null && body.cancellation_reason === null
|
|
2066
|
+
&& body.retryable === false && outcome.serialization === 'not_started'
|
|
2067
|
+
&& typeof error.code === 'string' && error.code.length > 0
|
|
2068
|
+
&& typeof error.message === 'string' && error.message.length > 0
|
|
2069
|
+
&& error.committed === false && error.retryable === false;
|
|
2070
|
+
}
|
|
183
2071
|
async function remoteSqlError(response, fallbackQueryId) {
|
|
184
2072
|
let body = {};
|
|
185
2073
|
try {
|
|
186
|
-
|
|
2074
|
+
const value = await responseJsonStrict(response);
|
|
2075
|
+
if (fallbackQueryId === 'unknown' && isSqlCursorErrorEnvelope(value)) {
|
|
2076
|
+
body = value;
|
|
2077
|
+
}
|
|
2078
|
+
else {
|
|
2079
|
+
const candidateQueryId = value !== null && typeof value === 'object'
|
|
2080
|
+
&& typeof value.query_id === 'string'
|
|
2081
|
+
&& /^[0-9a-f]{32}$/u.test(value.query_id)
|
|
2082
|
+
? value.query_id
|
|
2083
|
+
: undefined;
|
|
2084
|
+
const expectedQueryId = fallbackQueryId === 'unknown' ? candidateQueryId : fallbackQueryId;
|
|
2085
|
+
if (expectedQueryId === undefined || !isRemoteSqlErrorEnvelope(value, expectedQueryId)) {
|
|
2086
|
+
return fallbackQueryId === 'unknown'
|
|
2087
|
+
? new SerializationError('unknown', `SQL error response was malformed (HTTP ${response.status})`, { committed: false })
|
|
2088
|
+
: new QueryOutcomeUnknownError(fallbackQueryId, `SQL error response was malformed (HTTP ${response.status})`, { retryable: false, serverState: 'invalid_outcome' });
|
|
2089
|
+
}
|
|
2090
|
+
body = value;
|
|
2091
|
+
}
|
|
187
2092
|
}
|
|
188
2093
|
catch {
|
|
189
|
-
return
|
|
2094
|
+
return fallbackQueryId === 'unknown'
|
|
2095
|
+
? new SerializationError('unknown', `SQL error response was not valid JSON (HTTP ${response.status})`, { committed: false })
|
|
2096
|
+
: new QueryOutcomeUnknownError(fallbackQueryId, `SQL error response was not valid JSON (HTTP ${response.status})`);
|
|
190
2097
|
}
|
|
191
2098
|
const queryId = body.error?.query_id ?? fallbackQueryId;
|
|
192
2099
|
const message = body.error?.message;
|
|
193
|
-
|
|
2100
|
+
const outcome = {
|
|
2101
|
+
committed: mergedCommitState(body.outcome?.committed, body.committed),
|
|
2102
|
+
committedStatements: maxKnown(body.outcome?.committed_statements, body.committed_statements),
|
|
2103
|
+
lastCommitEpoch: remoteEpoch(body.outcome?.last_commit_epoch_text ?? body.last_commit_epoch_text, body.outcome?.last_commit_epoch ?? body.last_commit_epoch),
|
|
2104
|
+
firstCommitStatementIndex: body.outcome?.first_commit_statement_index
|
|
2105
|
+
?? body.first_commit_statement_index,
|
|
2106
|
+
lastCommitStatementIndex: body.outcome?.last_commit_statement_index
|
|
2107
|
+
?? body.last_commit_statement_index,
|
|
2108
|
+
completedStatements: maxKnown(body.outcome?.completed_statements, body.completed_statements),
|
|
2109
|
+
statementIndex: maxKnown(body.outcome?.statement_index, body.statement_index)
|
|
2110
|
+
};
|
|
2111
|
+
const metadata = {
|
|
2112
|
+
cancelOutcome: body.cancel_outcome,
|
|
2113
|
+
cancellationReason: body.cancellation_reason,
|
|
2114
|
+
retryable: body.retryable,
|
|
2115
|
+
serverState: body.server_state
|
|
2116
|
+
};
|
|
2117
|
+
const serverCode = body.error?.code;
|
|
2118
|
+
switch (serverCode) {
|
|
194
2119
|
case 'QUERY_CANCELLED':
|
|
195
|
-
|
|
2120
|
+
case 'QUERY_CANCELLED_AFTER_COMMIT':
|
|
2121
|
+
return new QueryCancelledError(queryId, message, outcome, metadata);
|
|
196
2122
|
case 'DEADLINE_EXCEEDED':
|
|
197
|
-
|
|
2123
|
+
case 'DEADLINE_AFTER_COMMIT':
|
|
2124
|
+
return new QueryTimeoutError(queryId, message, outcome, metadata);
|
|
198
2125
|
case 'QUERY_ID_CONFLICT':
|
|
199
2126
|
return new QueryIdConflictError(queryId, message);
|
|
200
2127
|
case 'TRANSACTION_ABORTED':
|
|
201
2128
|
return new TransactionAbortedError(message);
|
|
2129
|
+
case 'COMMIT_OUTCOME':
|
|
2130
|
+
return new CommitOutcomeError(queryId, message ?? 'SQL committed but response failed', { ...outcome, committed: true }, metadata);
|
|
2131
|
+
case 'SERIALIZATION_FAILED_AFTER_COMMIT':
|
|
2132
|
+
return new SerializationError(queryId, message ?? 'SQL committed but response serialization failed', { ...outcome, committed: true }, metadata);
|
|
2133
|
+
case 'QUERY_OUTCOME_UNKNOWN':
|
|
2134
|
+
return new QueryOutcomeUnknownError(queryId, message, metadata);
|
|
2135
|
+
case 'QUERY_REGISTRY_FULL':
|
|
2136
|
+
case 'CANCEL_TOO_LATE':
|
|
2137
|
+
case 'QUERY_ALREADY_FINISHED':
|
|
2138
|
+
case 'QUERY_NOT_FOUND':
|
|
2139
|
+
return new KitError(message ?? `SQL request failed with HTTP ${response.status}`, serverCode);
|
|
2140
|
+
case 'RESULT_LIMIT_EXCEEDED':
|
|
2141
|
+
return new ResultLimitExceededError(queryId, message ?? 'SQL result limit exceeded', outcome, metadata);
|
|
2142
|
+
case 'SERIALIZATION_FAILED':
|
|
2143
|
+
return new SerializationError(queryId, message ?? 'SQL response serialization failed', outcome, metadata);
|
|
2144
|
+
case 'CAPABILITY_UNSUPPORTED':
|
|
2145
|
+
return new CapabilityUnsupportedError(message ?? 'server capability is unsupported');
|
|
202
2146
|
default:
|
|
2147
|
+
if (serverCode) {
|
|
2148
|
+
return new RemoteProtocolError(serverCode, response.status, message ?? `SQL request failed with HTTP ${response.status}`, queryId === 'unknown' ? undefined : queryId, outcome, metadata);
|
|
2149
|
+
}
|
|
203
2150
|
return new KitError(message ?? `SQL request failed with HTTP ${response.status}`);
|
|
204
2151
|
}
|
|
205
2152
|
}
|