@fadhilp/stateql 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -20
- package/dist/src/adapters.d.ts +15 -1
- package/dist/src/adapters.js +631 -181
- package/dist/src/cli.js +12 -3
- package/dist/src/connection.d.ts +10 -0
- package/dist/src/connection.js +51 -0
- package/dist/src/errors.js +6 -2
- package/dist/src/filter.d.ts +14 -0
- package/dist/src/filter.js +256 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/response-data.d.ts +8 -0
- package/dist/src/response-data.js +66 -0
- package/dist/src/sql.js +8 -2
- package/dist/src/sqlite-process.d.ts +1 -0
- package/dist/src/sqlite-process.js +240 -0
- package/dist/src/stateql.d.ts +9 -4
- package/dist/src/stateql.js +124 -389
- package/dist/src/store.d.ts +2 -0
- package/dist/src/store.js +37 -2
- package/dist/src/types.d.ts +12 -6
- package/package.json +5 -2
package/dist/src/stateql.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
|
-
import { DatabaseSync } from "node:sqlite";
|
|
4
3
|
import { env } from "node:process";
|
|
5
|
-
import { BatchWriteError, createAdapter, } from "./adapters.js";
|
|
4
|
+
import { AdapterExecutionError, AdapterWriteError, BatchWriteError, createAdapter, createAdapterContext, } from "./adapters.js";
|
|
5
|
+
import { confidence, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, normalizeSqliteSource, validateProfileName, version, } from "./connection.js";
|
|
6
6
|
import { asStateQLError, StateQLError } from "./errors.js";
|
|
7
|
+
import { filterMaterializedRows, prepareFilterStatement, validateFilterParameters, } from "./filter.js";
|
|
8
|
+
import { operationData, paginationWarnings, profileData, rowsToCsv, sessionData, transactionData, } from "./response-data.js";
|
|
7
9
|
import { analyzeSql } from "./sql.js";
|
|
8
10
|
import { StateStore, } from "./store.js";
|
|
9
11
|
import { compactRows, defaultHome, hash, parseJson, redact, } from "./util.js";
|
|
@@ -15,6 +17,9 @@ export class StateQL {
|
|
|
15
17
|
resultTtlSeconds;
|
|
16
18
|
maxCellCharacters;
|
|
17
19
|
maxResultRows;
|
|
20
|
+
maxResultBytes;
|
|
21
|
+
timeoutMs;
|
|
22
|
+
signal;
|
|
18
23
|
now;
|
|
19
24
|
constructor(options = {}) {
|
|
20
25
|
this.now = options.now ?? (() => new Date());
|
|
@@ -24,6 +29,9 @@ export class StateQL {
|
|
|
24
29
|
this.resultTtlSeconds = options.resultTtlSeconds ?? 86_400;
|
|
25
30
|
this.maxCellCharacters = options.maxCellCharacters ?? 200;
|
|
26
31
|
this.maxResultRows = positiveInteger(options.maxResultRows ?? 10_000, "maxResultRows");
|
|
32
|
+
this.maxResultBytes = positiveInteger(options.maxResultBytes ?? 16 * 1024 * 1024, "maxResultBytes");
|
|
33
|
+
this.timeoutMs = executionTimeout(options.timeoutMs ?? 30_000);
|
|
34
|
+
this.signal = options.signal;
|
|
27
35
|
if (this.maxResultRows >= Number.MAX_SAFE_INTEGER) {
|
|
28
36
|
throw new StateQLError("INVALID_COMMAND", "maxResultRows is too large.");
|
|
29
37
|
}
|
|
@@ -59,10 +67,10 @@ export class StateQL {
|
|
|
59
67
|
: "Connection target is required.");
|
|
60
68
|
}
|
|
61
69
|
const driver = detectDriver(secret);
|
|
62
|
-
if (driver
|
|
70
|
+
if (driver !== "sqlite" &&
|
|
63
71
|
!secretEnv &&
|
|
64
|
-
|
|
65
|
-
throw new StateQLError("PERMISSION_DENIED",
|
|
72
|
+
databaseUrlHasSecret(secret)) {
|
|
73
|
+
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env.`, {
|
|
66
74
|
suggestedAction: "Set the URL in an environment variable and reconnect with --env NAME.",
|
|
67
75
|
});
|
|
68
76
|
}
|
|
@@ -73,7 +81,7 @@ export class StateQL {
|
|
|
73
81
|
: secret;
|
|
74
82
|
const databaseName = driver === "sqlite"
|
|
75
83
|
? basename(source)
|
|
76
|
-
: new URL(secret).pathname.replace(/^\//, "") ||
|
|
84
|
+
: new URL(secret).pathname.replace(/^\//, "") || driver;
|
|
77
85
|
const readOnly = options.readOnly ??
|
|
78
86
|
(profile ? Boolean(profile.read_only) : true);
|
|
79
87
|
const draft = {
|
|
@@ -88,11 +96,14 @@ export class StateQL {
|
|
|
88
96
|
version: 0,
|
|
89
97
|
created_at: this.now().toISOString(),
|
|
90
98
|
};
|
|
91
|
-
const adapter = await createAdapter(draft);
|
|
99
|
+
const adapter = await createAdapter(draft, this.executionContext(options));
|
|
92
100
|
try {
|
|
93
101
|
await adapter.read("SELECT 1", []);
|
|
94
102
|
}
|
|
95
103
|
catch (error) {
|
|
104
|
+
if (error instanceof AdapterExecutionError) {
|
|
105
|
+
throw stoppedStateQLError(error, false);
|
|
106
|
+
}
|
|
96
107
|
throw new StateQLError("CONNECTION_FAILED", errorMessage(error), { retryable: true });
|
|
97
108
|
}
|
|
98
109
|
finally {
|
|
@@ -140,8 +151,8 @@ export class StateQL {
|
|
|
140
151
|
let storedTarget = target;
|
|
141
152
|
if (target) {
|
|
142
153
|
const driver = detectDriver(target);
|
|
143
|
-
if (driver
|
|
144
|
-
throw new StateQLError("PERMISSION_DENIED",
|
|
154
|
+
if (driver !== "sqlite" && databaseUrlHasSecret(target)) {
|
|
155
|
+
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env.`);
|
|
145
156
|
}
|
|
146
157
|
if (driver === "sqlite")
|
|
147
158
|
storedTarget = normalizeSqliteSource(target);
|
|
@@ -231,10 +242,10 @@ export class StateQL {
|
|
|
231
242
|
if (!name.trim()) {
|
|
232
243
|
throw new StateQLError("INVALID_COMMAND", "Session name is required.");
|
|
233
244
|
}
|
|
234
|
-
if (this.store.
|
|
245
|
+
if (this.store.getSessionByName(name)) {
|
|
235
246
|
throw new StateQLError("INVALID_COMMAND", `Active session "${name}" already exists.`);
|
|
236
247
|
}
|
|
237
|
-
const session = this.store.
|
|
248
|
+
const session = this.store.ensureSession(name);
|
|
238
249
|
return {
|
|
239
250
|
data: sessionData(session),
|
|
240
251
|
handle: session.id,
|
|
@@ -310,12 +321,13 @@ export class StateQL {
|
|
|
310
321
|
async query(sql, options = {}) {
|
|
311
322
|
return this.run("query", async (session) => {
|
|
312
323
|
const connection = this.requireConnection(session);
|
|
324
|
+
this.rejectDuringStagedTransaction(session, "Queries");
|
|
313
325
|
const analysis = analyzeSql(sql, connection.driver);
|
|
314
326
|
if (!analysis.read) {
|
|
315
327
|
throw new StateQLError("INVALID_SQL", "query accepts read statements only; use exec for writes.");
|
|
316
328
|
}
|
|
317
329
|
const parameters = options.params ?? [];
|
|
318
|
-
const adapter = await createAdapter(connection);
|
|
330
|
+
const adapter = await createAdapter(connection, this.executionContext(options));
|
|
319
331
|
try {
|
|
320
332
|
const stateVersion = version(connection);
|
|
321
333
|
const stateSignature = await adapter.signature();
|
|
@@ -353,6 +365,12 @@ export class StateQL {
|
|
|
353
365
|
if (result.rows.length > this.maxResultRows) {
|
|
354
366
|
throw new StateQLError("OUTPUT_LIMIT_EXCEEDED", `Query exceeds the ${this.maxResultRows}-row materialization limit.`, { suggestedAction: "Add a narrower WHERE clause or LIMIT." });
|
|
355
367
|
}
|
|
368
|
+
const resultBytes = Buffer.byteLength(JSON.stringify(parameters), "utf8") +
|
|
369
|
+
Buffer.byteLength(JSON.stringify(result.rows), "utf8") +
|
|
370
|
+
Buffer.byteLength(JSON.stringify(result.columns), "utf8");
|
|
371
|
+
if (resultBytes > this.maxResultBytes) {
|
|
372
|
+
throw new StateQLError("OUTPUT_LIMIT_EXCEEDED", `Query exceeds the ${this.maxResultBytes}-byte materialization limit.`, { suggestedAction: "Select fewer rows or smaller columns." });
|
|
373
|
+
}
|
|
356
374
|
const expiresAt = new Date(this.now().getTime() + this.resultTtlSeconds * 1000).toISOString();
|
|
357
375
|
const saved = this.store.saveResult({
|
|
358
376
|
sessionId: session.id,
|
|
@@ -379,6 +397,9 @@ export class StateQL {
|
|
|
379
397
|
catch (error) {
|
|
380
398
|
if (error instanceof StateQLError)
|
|
381
399
|
throw error;
|
|
400
|
+
if (error instanceof AdapterExecutionError) {
|
|
401
|
+
throw stoppedStateQLError(error, true);
|
|
402
|
+
}
|
|
382
403
|
throw new StateQLError("QUERY_FAILED", errorMessage(error), {
|
|
383
404
|
retryable: true,
|
|
384
405
|
executed: true,
|
|
@@ -529,7 +550,7 @@ export class StateQL {
|
|
|
529
550
|
async exec(sql, options = {}) {
|
|
530
551
|
return this.run("exec", async (session) => {
|
|
531
552
|
const connection = this.requireConnection(session);
|
|
532
|
-
return this.performExec(session, connection, sql, options);
|
|
553
|
+
return this.performExec(session, connection, sql, options, this.executionContext(options));
|
|
533
554
|
});
|
|
534
555
|
}
|
|
535
556
|
async receipt(id) {
|
|
@@ -587,7 +608,7 @@ export class StateQL {
|
|
|
587
608
|
};
|
|
588
609
|
});
|
|
589
610
|
}
|
|
590
|
-
async commitTransaction(id) {
|
|
611
|
+
async commitTransaction(id, options = {}) {
|
|
591
612
|
return this.run("transaction.commit", async (session) => {
|
|
592
613
|
const transaction = this.requireActiveTransaction(session, id);
|
|
593
614
|
const connection = this.store.getConnection(transaction.connection_id);
|
|
@@ -601,7 +622,7 @@ export class StateQL {
|
|
|
601
622
|
if (operations.some((operation) => operation.connection_id !== connection.id)) {
|
|
602
623
|
throw new StateQLError("TRANSACTION_FAILED", "Transaction contains writes staged for another connection.", { suggestedAction: "Roll back the transaction." });
|
|
603
624
|
}
|
|
604
|
-
const adapter = await createAdapter(connection);
|
|
625
|
+
const adapter = await createAdapter(connection, this.executionContext(options));
|
|
605
626
|
try {
|
|
606
627
|
if (!this.store.markTransactionCommitting(transaction.id)) {
|
|
607
628
|
throw new StateQLError("TRANSACTION_FAILED", "Transaction is no longer active.");
|
|
@@ -611,8 +632,12 @@ export class StateQL {
|
|
|
611
632
|
results = await adapter.writeBatch(operations, transaction.isolation_level);
|
|
612
633
|
}
|
|
613
634
|
catch (error) {
|
|
614
|
-
if (error instanceof BatchWriteError && !error.outcomeUnknown)
|
|
635
|
+
if ((error instanceof BatchWriteError && !error.outcomeUnknown) ||
|
|
636
|
+
(error instanceof AdapterExecutionError && !error.outcomeUnknown)) {
|
|
615
637
|
this.store.finishTransaction(transaction.id, session.id, "failed");
|
|
638
|
+
if (error instanceof AdapterExecutionError) {
|
|
639
|
+
throw stoppedStateQLError(error, false);
|
|
640
|
+
}
|
|
616
641
|
throw new StateQLError("TRANSACTION_FAILED", error.message, {
|
|
617
642
|
retryable: true,
|
|
618
643
|
});
|
|
@@ -690,10 +715,11 @@ export class StateQL {
|
|
|
690
715
|
};
|
|
691
716
|
});
|
|
692
717
|
}
|
|
693
|
-
async inspect(kind, table) {
|
|
718
|
+
async inspect(kind, table, options = {}) {
|
|
694
719
|
return this.run(`inspect.${kind}`, async (session) => {
|
|
695
720
|
const connection = this.requireConnection(session);
|
|
696
|
-
|
|
721
|
+
this.rejectDuringStagedTransaction(session, "Schema inspection");
|
|
722
|
+
const adapter = await createAdapter(connection, this.executionContext(options));
|
|
697
723
|
try {
|
|
698
724
|
const data = await adapter.inspect(kind, table);
|
|
699
725
|
return {
|
|
@@ -704,6 +730,9 @@ export class StateQL {
|
|
|
704
730
|
};
|
|
705
731
|
}
|
|
706
732
|
catch (error) {
|
|
733
|
+
if (error instanceof AdapterExecutionError) {
|
|
734
|
+
throw stoppedStateQLError(error, true);
|
|
735
|
+
}
|
|
707
736
|
throw new StateQLError("QUERY_FAILED", errorMessage(error), {
|
|
708
737
|
retryable: false,
|
|
709
738
|
executed: true,
|
|
@@ -717,11 +746,12 @@ export class StateQL {
|
|
|
717
746
|
async plan(sql, options = {}) {
|
|
718
747
|
return this.run("plan", async (session) => {
|
|
719
748
|
const connection = this.requireConnection(session);
|
|
749
|
+
this.rejectDuringStagedTransaction(session, "Plans");
|
|
720
750
|
const analysis = analyzeSql(sql, connection.driver);
|
|
721
751
|
if (analysis.read) {
|
|
722
752
|
throw new StateQLError("INVALID_SQL", "plan accepts write statements only.");
|
|
723
753
|
}
|
|
724
|
-
const adapter = await createAdapter(connection);
|
|
754
|
+
const adapter = await createAdapter(connection, this.executionContext(options));
|
|
725
755
|
try {
|
|
726
756
|
const stateSignature = await adapter.signature();
|
|
727
757
|
const expiresAt = new Date(this.now().getTime() + 10 * 60_000).toISOString();
|
|
@@ -762,13 +792,20 @@ export class StateQL {
|
|
|
762
792
|
confidence: adapter.confidence,
|
|
763
793
|
};
|
|
764
794
|
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
if (error instanceof AdapterExecutionError) {
|
|
797
|
+
throw stoppedStateQLError(error, true);
|
|
798
|
+
}
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
765
801
|
finally {
|
|
766
802
|
await adapter.close();
|
|
767
803
|
}
|
|
768
804
|
});
|
|
769
805
|
}
|
|
770
|
-
async apply(planId) {
|
|
806
|
+
async apply(planId, options = {}) {
|
|
771
807
|
return this.run("apply", async (session) => {
|
|
808
|
+
this.rejectDuringStagedTransaction(session, "Plans");
|
|
772
809
|
const plan = this.store.getPlan(planId);
|
|
773
810
|
if (!plan || plan.session_id !== session.id) {
|
|
774
811
|
throw new StateQLError("STALE_PLAN", `Plan "${planId}" was not found.`);
|
|
@@ -786,12 +823,19 @@ export class StateQL {
|
|
|
786
823
|
version(connection) !== plan.state_version) {
|
|
787
824
|
throw new StateQLError("STALE_PLAN", "Database state changed after this plan was created.");
|
|
788
825
|
}
|
|
789
|
-
const
|
|
826
|
+
const context = this.executionContext(options);
|
|
827
|
+
const adapter = await createAdapter(connection, context);
|
|
790
828
|
try {
|
|
791
829
|
if ((await adapter.signature()) !== plan.state_signature) {
|
|
792
830
|
throw new StateQLError("STALE_PLAN", "Database state changed after this plan was created.");
|
|
793
831
|
}
|
|
794
832
|
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
if (error instanceof AdapterExecutionError) {
|
|
835
|
+
throw stoppedStateQLError(error, true);
|
|
836
|
+
}
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
795
839
|
finally {
|
|
796
840
|
await adapter.close();
|
|
797
841
|
}
|
|
@@ -799,7 +843,7 @@ export class StateQL {
|
|
|
799
843
|
params: parseJson(plan.parameters, []),
|
|
800
844
|
allowUnbounded: Boolean(plan.allow_unbounded),
|
|
801
845
|
allowDestructive: Boolean(plan.allow_destructive),
|
|
802
|
-
});
|
|
846
|
+
}, context);
|
|
803
847
|
const operationId = String(result.data.operation_id);
|
|
804
848
|
this.store.markPlanApplied(plan.id, operationId);
|
|
805
849
|
return {
|
|
@@ -830,7 +874,7 @@ export class StateQL {
|
|
|
830
874
|
async capabilities() {
|
|
831
875
|
return this.run("capabilities", async () => ({
|
|
832
876
|
data: {
|
|
833
|
-
drivers: ["postgres", "sqlite"],
|
|
877
|
+
drivers: ["mysql", "postgres", "sqlite"],
|
|
834
878
|
features: {
|
|
835
879
|
result_handles: true,
|
|
836
880
|
write_deduplication: true,
|
|
@@ -839,6 +883,8 @@ export class StateQL {
|
|
|
839
883
|
persistent_sessions: true,
|
|
840
884
|
result_filtering: true,
|
|
841
885
|
schema_inspection: true,
|
|
886
|
+
deadlines: true,
|
|
887
|
+
cancellation: true,
|
|
842
888
|
},
|
|
843
889
|
},
|
|
844
890
|
}));
|
|
@@ -855,6 +901,7 @@ export class StateQL {
|
|
|
855
901
|
readOnly: command.read_only,
|
|
856
902
|
secretEnv: command.secret_env,
|
|
857
903
|
profile: command.profile,
|
|
904
|
+
timeoutMs: command.timeout_ms,
|
|
858
905
|
});
|
|
859
906
|
case "disconnect":
|
|
860
907
|
return this.disconnect();
|
|
@@ -885,6 +932,7 @@ export class StateQL {
|
|
|
885
932
|
const response = await this.query(batchString(command.sql, "sql"), {
|
|
886
933
|
params: command.params ?? [],
|
|
887
934
|
cache: command.cache ?? "auto",
|
|
935
|
+
timeoutMs: command.timeout_ms,
|
|
888
936
|
});
|
|
889
937
|
if (!response.ok || !command.as)
|
|
890
938
|
return response;
|
|
@@ -917,6 +965,7 @@ export class StateQL {
|
|
|
917
965
|
idempotencyKey: command.idempotency_key,
|
|
918
966
|
allowUnbounded: command.allow_unbounded ?? false,
|
|
919
967
|
allowDestructive: command.allow_destructive ?? false,
|
|
968
|
+
timeoutMs: command.timeout_ms,
|
|
920
969
|
});
|
|
921
970
|
case "show":
|
|
922
971
|
return this.show(batchString(command.handle, "handle"));
|
|
@@ -932,13 +981,17 @@ export class StateQL {
|
|
|
932
981
|
case "alias.set":
|
|
933
982
|
return this.setAlias(batchString(command.name, "name"), batchString(command.handle, "handle"));
|
|
934
983
|
case "inspect":
|
|
935
|
-
return this.inspect(batchString(command.kind, "kind"), command.table
|
|
984
|
+
return this.inspect(batchString(command.kind, "kind"), command.table, {
|
|
985
|
+
timeoutMs: command.timeout_ms,
|
|
986
|
+
});
|
|
936
987
|
case "transaction.begin":
|
|
937
988
|
return this.beginTransaction(command.isolation);
|
|
938
989
|
case "transaction.status":
|
|
939
990
|
return this.transactionStatus(command.handle);
|
|
940
991
|
case "transaction.commit":
|
|
941
|
-
return this.commitTransaction(command.handle
|
|
992
|
+
return this.commitTransaction(command.handle, {
|
|
993
|
+
timeoutMs: command.timeout_ms,
|
|
994
|
+
});
|
|
942
995
|
case "transaction.rollback":
|
|
943
996
|
return this.rollbackTransaction(command.handle);
|
|
944
997
|
case "plan":
|
|
@@ -946,9 +999,12 @@ export class StateQL {
|
|
|
946
999
|
params: command.params ?? [],
|
|
947
1000
|
allowUnbounded: command.allow_unbounded ?? false,
|
|
948
1001
|
allowDestructive: command.allow_destructive,
|
|
1002
|
+
timeoutMs: command.timeout_ms,
|
|
949
1003
|
});
|
|
950
1004
|
case "apply":
|
|
951
|
-
return this.apply(batchString(command.handle, "handle")
|
|
1005
|
+
return this.apply(batchString(command.handle, "handle"), {
|
|
1006
|
+
timeoutMs: command.timeout_ms,
|
|
1007
|
+
});
|
|
952
1008
|
case "history":
|
|
953
1009
|
return this.history(command.limit ?? 20);
|
|
954
1010
|
case "receipt":
|
|
@@ -982,7 +1038,7 @@ export class StateQL {
|
|
|
982
1038
|
return;
|
|
983
1039
|
}
|
|
984
1040
|
}
|
|
985
|
-
async performExec(session, connection, sql, options) {
|
|
1041
|
+
async performExec(session, connection, sql, options, context) {
|
|
986
1042
|
if (connection.read_only) {
|
|
987
1043
|
throw new StateQLError("READ_ONLY_CONNECTION", "Connection is read-only.", { suggestedAction: "Reconnect with --read-write." });
|
|
988
1044
|
}
|
|
@@ -996,6 +1052,10 @@ export class StateQL {
|
|
|
996
1052
|
if (analysis.destructive && !options.allowDestructive) {
|
|
997
1053
|
throw new StateQLError("DESTRUCTIVE_OPERATION_BLOCKED", "Destructive operation requires an explicit override.", { extra: { override_flag: "--allow-destructive" } });
|
|
998
1054
|
}
|
|
1055
|
+
if (options.idempotencyKey !== undefined &&
|
|
1056
|
+
!options.idempotencyKey.trim()) {
|
|
1057
|
+
throw new StateQLError("INVALID_COMMAND", "Idempotency key cannot be empty.");
|
|
1058
|
+
}
|
|
999
1059
|
const parameters = options.params ?? [];
|
|
1000
1060
|
const fingerprint = hash({
|
|
1001
1061
|
sql: analysis.normalized,
|
|
@@ -1026,6 +1086,12 @@ export class StateQL {
|
|
|
1026
1086
|
stateVersionBefore: version(connection),
|
|
1027
1087
|
});
|
|
1028
1088
|
const previous = reservation.previous;
|
|
1089
|
+
if (previous &&
|
|
1090
|
+
options.idempotencyKey &&
|
|
1091
|
+
!options.replay &&
|
|
1092
|
+
previous.fingerprint !== fingerprint) {
|
|
1093
|
+
throw new StateQLError("IDEMPOTENCY_CONFLICT", "Idempotency key was already used for a different write.", { extra: { previous_operation_id: previous.id } });
|
|
1094
|
+
}
|
|
1029
1095
|
if (previous && !reservation.operation) {
|
|
1030
1096
|
if (previous.status === "executing" ||
|
|
1031
1097
|
previous.status === "outcome_unknown") {
|
|
@@ -1066,7 +1132,7 @@ export class StateQL {
|
|
|
1066
1132
|
}
|
|
1067
1133
|
let adapter;
|
|
1068
1134
|
try {
|
|
1069
|
-
adapter = await createAdapter(connection);
|
|
1135
|
+
adapter = await createAdapter(connection, context);
|
|
1070
1136
|
}
|
|
1071
1137
|
catch (error) {
|
|
1072
1138
|
this.store.failOperation(operation.id);
|
|
@@ -1102,6 +1168,16 @@ export class StateQL {
|
|
|
1102
1168
|
catch (error) {
|
|
1103
1169
|
if (error instanceof StateQLError)
|
|
1104
1170
|
throw error;
|
|
1171
|
+
if (error instanceof AdapterExecutionError && !error.outcomeUnknown) {
|
|
1172
|
+
this.store.failOperation(operation.id);
|
|
1173
|
+
throw stoppedStateQLError(error, false);
|
|
1174
|
+
}
|
|
1175
|
+
if (error instanceof AdapterWriteError && !error.outcomeUnknown) {
|
|
1176
|
+
this.store.failOperation(operation.id);
|
|
1177
|
+
throw new StateQLError("QUERY_FAILED", error.message, {
|
|
1178
|
+
executed: true,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1105
1181
|
this.store.markOperationOutcomeUnknown(operation.id);
|
|
1106
1182
|
throw new StateQLError("OUTCOME_UNKNOWN", errorMessage(error), {
|
|
1107
1183
|
executed: true,
|
|
@@ -1134,6 +1210,11 @@ export class StateQL {
|
|
|
1134
1210
|
return action(result, session);
|
|
1135
1211
|
});
|
|
1136
1212
|
}
|
|
1213
|
+
rejectDuringStagedTransaction(session, operation) {
|
|
1214
|
+
if (!session.active_transaction_id)
|
|
1215
|
+
return;
|
|
1216
|
+
throw new StateQLError("TRANSACTION_FAILED", `${operation} cannot run while a staged transaction is active.`, { suggestedAction: "Commit or roll back the transaction first." });
|
|
1217
|
+
}
|
|
1137
1218
|
requireResult(idOrAlias, session) {
|
|
1138
1219
|
const result = this.store.getResult(idOrAlias, session.id);
|
|
1139
1220
|
if (!result) {
|
|
@@ -1162,6 +1243,9 @@ export class StateQL {
|
|
|
1162
1243
|
}
|
|
1163
1244
|
return transaction;
|
|
1164
1245
|
}
|
|
1246
|
+
executionContext(options) {
|
|
1247
|
+
return createAdapterContext(executionTimeout(options.timeoutMs ?? this.timeoutMs), options.signal ?? this.signal);
|
|
1248
|
+
}
|
|
1165
1249
|
resultData(result, cached) {
|
|
1166
1250
|
const rows = this.store.resultRows(result);
|
|
1167
1251
|
const preview = compactRows(rows.slice(0, this.previewRows), this.maxCellCharacters);
|
|
@@ -1244,256 +1328,6 @@ export class StateQL {
|
|
|
1244
1328
|
}
|
|
1245
1329
|
}
|
|
1246
1330
|
}
|
|
1247
|
-
function prepareFilterStatement(columns, predicate) {
|
|
1248
|
-
const text = predicate.trim();
|
|
1249
|
-
if (!text) {
|
|
1250
|
-
throw new StateQLError("INVALID_SQL", "Filter predicate is required.");
|
|
1251
|
-
}
|
|
1252
|
-
const columnNames = columns.map((column) => column.name);
|
|
1253
|
-
if (columnNames.length === 0) {
|
|
1254
|
-
throw new StateQLError("INVALID_SQL", "Filter requires at least one result column.");
|
|
1255
|
-
}
|
|
1256
|
-
const names = new Set();
|
|
1257
|
-
for (const name of columnNames) {
|
|
1258
|
-
const normalized = name.toLowerCase();
|
|
1259
|
-
if (!name || name.includes("\0") || names.has(normalized)) {
|
|
1260
|
-
throw new StateQLError("INVALID_SQL", "Filter requires unique, non-empty result column names.");
|
|
1261
|
-
}
|
|
1262
|
-
names.add(normalized);
|
|
1263
|
-
}
|
|
1264
|
-
const tableName = "__stateql_filter_source";
|
|
1265
|
-
let indexColumn = "__stateql_row_index";
|
|
1266
|
-
while (names.has(indexColumn.toLowerCase()))
|
|
1267
|
-
indexColumn += "_";
|
|
1268
|
-
const sql = `SELECT ${quoteIdentifier(indexColumn)} ` +
|
|
1269
|
-
`FROM ${quoteIdentifier(tableName)} WHERE (${text})`;
|
|
1270
|
-
const analysis = analyzeSql(sql, "sqlite");
|
|
1271
|
-
const details = analysis.ast;
|
|
1272
|
-
const from = details.from;
|
|
1273
|
-
const source = Array.isArray(from)
|
|
1274
|
-
? from[0]
|
|
1275
|
-
: undefined;
|
|
1276
|
-
if (!analysis.read ||
|
|
1277
|
-
!Array.isArray(from) ||
|
|
1278
|
-
from.length !== 1 ||
|
|
1279
|
-
source?.table !== tableName ||
|
|
1280
|
-
details.with ||
|
|
1281
|
-
details.groupby ||
|
|
1282
|
-
details.having ||
|
|
1283
|
-
details.orderby ||
|
|
1284
|
-
details.limit ||
|
|
1285
|
-
details.for_update ||
|
|
1286
|
-
details._next ||
|
|
1287
|
-
details.set_op ||
|
|
1288
|
-
containsSelect(details.where)) {
|
|
1289
|
-
throw new StateQLError("INVALID_SQL", "Filter accepts one scalar predicate only.");
|
|
1290
|
-
}
|
|
1291
|
-
validateFilterExpression(details.where, names, tableName);
|
|
1292
|
-
const bindings = filterBindings(details.where);
|
|
1293
|
-
return {
|
|
1294
|
-
sql,
|
|
1295
|
-
normalized: analysis.normalized,
|
|
1296
|
-
tableName,
|
|
1297
|
-
indexColumn,
|
|
1298
|
-
columnNames,
|
|
1299
|
-
positionalParameters: bindings.positional,
|
|
1300
|
-
namedParameters: [...bindings.named],
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
|
-
function filterMaterializedRows(rows, filter, parameters) {
|
|
1304
|
-
const db = new DatabaseSync(":memory:");
|
|
1305
|
-
try {
|
|
1306
|
-
const definitions = [
|
|
1307
|
-
`${quoteIdentifier(filter.indexColumn)} INTEGER PRIMARY KEY`,
|
|
1308
|
-
...filter.columnNames.map(quoteIdentifier),
|
|
1309
|
-
];
|
|
1310
|
-
db.exec(`CREATE TABLE ${quoteIdentifier(filter.tableName)} ` +
|
|
1311
|
-
`(${definitions.join(", ")})`);
|
|
1312
|
-
const placeholders = filter.columnNames.map(() => "?").join(", ");
|
|
1313
|
-
const insert = db.prepare(`INSERT INTO ${quoteIdentifier(filter.tableName)} (` +
|
|
1314
|
-
`${quoteIdentifier(filter.indexColumn)}, ` +
|
|
1315
|
-
`${filter.columnNames.map(quoteIdentifier).join(", ")}) ` +
|
|
1316
|
-
`VALUES (?, ${placeholders})`);
|
|
1317
|
-
db.exec("BEGIN");
|
|
1318
|
-
try {
|
|
1319
|
-
rows.forEach((row, index) => {
|
|
1320
|
-
const values = filter.columnNames.map((name) => sqliteFilterValue(row[name]));
|
|
1321
|
-
insert.run(index, ...values);
|
|
1322
|
-
});
|
|
1323
|
-
db.exec("COMMIT");
|
|
1324
|
-
}
|
|
1325
|
-
catch (error) {
|
|
1326
|
-
db.exec("ROLLBACK");
|
|
1327
|
-
throw error;
|
|
1328
|
-
}
|
|
1329
|
-
const statement = db.prepare(`${filter.sql}\nORDER BY ${quoteIdentifier(filter.indexColumn)}`);
|
|
1330
|
-
const selected = filterAll(statement, parameters);
|
|
1331
|
-
return selected.map((row) => {
|
|
1332
|
-
const index = Number(row[filter.indexColumn]);
|
|
1333
|
-
if (!Number.isInteger(index) || !rows[index]) {
|
|
1334
|
-
throw new StateQLError("INVALID_SQL", "Filter produced an invalid source row index.");
|
|
1335
|
-
}
|
|
1336
|
-
return rows[index];
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
catch (error) {
|
|
1340
|
-
if (error instanceof StateQLError)
|
|
1341
|
-
throw error;
|
|
1342
|
-
throw new StateQLError("INVALID_SQL", errorMessage(error));
|
|
1343
|
-
}
|
|
1344
|
-
finally {
|
|
1345
|
-
db.close();
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
function filterAll(statement, parameters) {
|
|
1349
|
-
if (Array.isArray(parameters)) {
|
|
1350
|
-
return statement.all(...parameters);
|
|
1351
|
-
}
|
|
1352
|
-
return statement.all(parameters);
|
|
1353
|
-
}
|
|
1354
|
-
function sqliteFilterValue(value) {
|
|
1355
|
-
if (value === null || value === undefined)
|
|
1356
|
-
return null;
|
|
1357
|
-
if (typeof value === "string" ||
|
|
1358
|
-
typeof value === "number" ||
|
|
1359
|
-
typeof value === "bigint") {
|
|
1360
|
-
return value;
|
|
1361
|
-
}
|
|
1362
|
-
if (typeof value === "boolean")
|
|
1363
|
-
return value ? 1 : 0;
|
|
1364
|
-
if (value instanceof Uint8Array)
|
|
1365
|
-
return value;
|
|
1366
|
-
try {
|
|
1367
|
-
return JSON.stringify(value) ?? null;
|
|
1368
|
-
}
|
|
1369
|
-
catch {
|
|
1370
|
-
return String(value);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
function quoteIdentifier(value) {
|
|
1374
|
-
return `"${value.replaceAll('"', '""')}"`;
|
|
1375
|
-
}
|
|
1376
|
-
function containsSelect(value) {
|
|
1377
|
-
if (!value || typeof value !== "object")
|
|
1378
|
-
return false;
|
|
1379
|
-
const record = value;
|
|
1380
|
-
if (record.type === "select")
|
|
1381
|
-
return true;
|
|
1382
|
-
return Object.values(record).some(containsSelect);
|
|
1383
|
-
}
|
|
1384
|
-
const FILTER_FUNCTIONS = new Set([
|
|
1385
|
-
"abs",
|
|
1386
|
-
"coalesce",
|
|
1387
|
-
"ifnull",
|
|
1388
|
-
"instr",
|
|
1389
|
-
"json_extract",
|
|
1390
|
-
"json_type",
|
|
1391
|
-
"json_valid",
|
|
1392
|
-
"length",
|
|
1393
|
-
"lower",
|
|
1394
|
-
"ltrim",
|
|
1395
|
-
"nullif",
|
|
1396
|
-
"round",
|
|
1397
|
-
"rtrim",
|
|
1398
|
-
"substr",
|
|
1399
|
-
"substring",
|
|
1400
|
-
"trim",
|
|
1401
|
-
"typeof",
|
|
1402
|
-
"upper",
|
|
1403
|
-
]);
|
|
1404
|
-
function validateFilterExpression(value, columns, tableName) {
|
|
1405
|
-
if (!value || typeof value !== "object")
|
|
1406
|
-
return;
|
|
1407
|
-
const record = value;
|
|
1408
|
-
if (record.type === "column_ref") {
|
|
1409
|
-
const column = record.column;
|
|
1410
|
-
const table = record.table;
|
|
1411
|
-
if (typeof column !== "string" ||
|
|
1412
|
-
!columns.has(column.toLowerCase()) ||
|
|
1413
|
-
(table !== null && table !== undefined && table !== tableName)) {
|
|
1414
|
-
throw new StateQLError("INVALID_SQL", `Unknown filter column "${String(column)}".`);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
else if (record.type === "double_quote_string") {
|
|
1418
|
-
const column = String(record.value);
|
|
1419
|
-
if (!columns.has(column.toLowerCase())) {
|
|
1420
|
-
throw new StateQLError("INVALID_SQL", `Unknown filter column "${column}".`);
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
else if (record.type === "function") {
|
|
1424
|
-
const name = filterFunctionName(record);
|
|
1425
|
-
if (!name || !FILTER_FUNCTIONS.has(name)) {
|
|
1426
|
-
throw new StateQLError("INVALID_SQL", `Filter function "${name ?? "unknown"}" is not allowed.`);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
Object.values(record).forEach((item) => validateFilterExpression(item, columns, tableName));
|
|
1430
|
-
}
|
|
1431
|
-
function filterFunctionName(record) {
|
|
1432
|
-
const name = record.name;
|
|
1433
|
-
const parts = name?.name;
|
|
1434
|
-
if (!Array.isArray(parts))
|
|
1435
|
-
return undefined;
|
|
1436
|
-
const last = parts.at(-1);
|
|
1437
|
-
return typeof last?.value === "string" ? last.value.toLowerCase() : undefined;
|
|
1438
|
-
}
|
|
1439
|
-
function filterBindings(value) {
|
|
1440
|
-
const named = new Set();
|
|
1441
|
-
const prefixes = new Map();
|
|
1442
|
-
let positional = 0;
|
|
1443
|
-
const visit = (item) => {
|
|
1444
|
-
if (!item || typeof item !== "object")
|
|
1445
|
-
return;
|
|
1446
|
-
const record = item;
|
|
1447
|
-
if (record.type === "origin" && record.value === "?")
|
|
1448
|
-
positional += 1;
|
|
1449
|
-
if (record.type === "param" && typeof record.value === "string") {
|
|
1450
|
-
addNamed(String(record.value), `:${String(record.value)}`);
|
|
1451
|
-
}
|
|
1452
|
-
if (record.type === "var" &&
|
|
1453
|
-
typeof record.name === "string" &&
|
|
1454
|
-
(record.prefix === "$" || record.prefix === "@")) {
|
|
1455
|
-
addNamed(record.name, `${String(record.prefix)}${record.name}`);
|
|
1456
|
-
}
|
|
1457
|
-
Object.values(record).forEach(visit);
|
|
1458
|
-
};
|
|
1459
|
-
const addNamed = (name, token) => {
|
|
1460
|
-
const previous = prefixes.get(name);
|
|
1461
|
-
if (previous && previous !== token) {
|
|
1462
|
-
throw new StateQLError("INVALID_SQL", `Filter parameter "${name}" uses conflicting prefixes.`);
|
|
1463
|
-
}
|
|
1464
|
-
prefixes.set(name, token);
|
|
1465
|
-
named.add(name);
|
|
1466
|
-
};
|
|
1467
|
-
visit(value);
|
|
1468
|
-
if (positional && named.size) {
|
|
1469
|
-
throw new StateQLError("INVALID_SQL", "Filter cannot mix positional and named parameters.");
|
|
1470
|
-
}
|
|
1471
|
-
return { positional, named };
|
|
1472
|
-
}
|
|
1473
|
-
function validateFilterParameters(filter, parameters) {
|
|
1474
|
-
if (filter.positionalParameters) {
|
|
1475
|
-
if (!Array.isArray(parameters) ||
|
|
1476
|
-
parameters.length !== filter.positionalParameters) {
|
|
1477
|
-
throw new StateQLError("INVALID_SQL", `Filter requires exactly ${filter.positionalParameters} positional parameters.`);
|
|
1478
|
-
}
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
if (filter.namedParameters.length) {
|
|
1482
|
-
if (Array.isArray(parameters)) {
|
|
1483
|
-
throw new StateQLError("INVALID_SQL", "Filter requires named parameters.");
|
|
1484
|
-
}
|
|
1485
|
-
const supplied = Object.keys(parameters).sort();
|
|
1486
|
-
const expected = [...filter.namedParameters].sort();
|
|
1487
|
-
if (JSON.stringify(supplied) !== JSON.stringify(expected)) {
|
|
1488
|
-
throw new StateQLError("INVALID_SQL", `Filter requires named parameters: ${expected.join(", ")}.`);
|
|
1489
|
-
}
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
if ((Array.isArray(parameters) && parameters.length) ||
|
|
1493
|
-
(!Array.isArray(parameters) && Object.keys(parameters).length)) {
|
|
1494
|
-
throw new StateQLError("INVALID_SQL", "Filter predicate has no parameters.");
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
1331
|
function markTransactionOutcomeUnknown(store, transactionId, sessionId) {
|
|
1498
1332
|
try {
|
|
1499
1333
|
store.markTransactionOutcomeUnknown(transactionId, sessionId);
|
|
@@ -1506,6 +1340,9 @@ function boundedReadSql(sql, limit) {
|
|
|
1506
1340
|
const statement = sql.trim().replace(/;\s*$/, "");
|
|
1507
1341
|
return `SELECT * FROM (${statement}) AS _stateql_bounded LIMIT ${limit}`;
|
|
1508
1342
|
}
|
|
1343
|
+
function databaseDisplayName(driver) {
|
|
1344
|
+
return driver === "postgres" ? "PostgreSQL" : "MySQL";
|
|
1345
|
+
}
|
|
1509
1346
|
function normalizeIsolation(isolation, driver) {
|
|
1510
1347
|
const normalized = isolation.trim().toLowerCase().replace(/[_-]+/g, " ")
|
|
1511
1348
|
.replace(/\s+/g, " ");
|
|
@@ -1523,104 +1360,6 @@ function normalizeIsolation(isolation, driver) {
|
|
|
1523
1360
|
}
|
|
1524
1361
|
return normalized;
|
|
1525
1362
|
}
|
|
1526
|
-
function databaseIdentity(connection) {
|
|
1527
|
-
return {
|
|
1528
|
-
driver: connection.driver,
|
|
1529
|
-
database: connection.database_name,
|
|
1530
|
-
source: connection.source,
|
|
1531
|
-
secretEnvironment: connection.secret_env,
|
|
1532
|
-
};
|
|
1533
|
-
}
|
|
1534
|
-
function detectDriver(target) {
|
|
1535
|
-
if (/^postgres(?:ql)?:\/\//i.test(target))
|
|
1536
|
-
return "postgres";
|
|
1537
|
-
if (/^[a-z][a-z\d+.-]*:\/\//i.test(target)) {
|
|
1538
|
-
throw new StateQLError("UNSUPPORTED_DRIVER", "Only PostgreSQL and SQLite are supported.");
|
|
1539
|
-
}
|
|
1540
|
-
return "sqlite";
|
|
1541
|
-
}
|
|
1542
|
-
function normalizeSqliteSource(target) {
|
|
1543
|
-
const source = target.startsWith("sqlite:") ? target.slice(7) : target;
|
|
1544
|
-
if (source === ":memory:")
|
|
1545
|
-
return source;
|
|
1546
|
-
return resolve(source);
|
|
1547
|
-
}
|
|
1548
|
-
function postgresUrlHasSecret(target) {
|
|
1549
|
-
try {
|
|
1550
|
-
const url = new URL(target);
|
|
1551
|
-
return (Boolean(url.password) ||
|
|
1552
|
-
[...url.searchParams.keys()].some((key) => /pass|token|secret|private[_-]?key|api[_-]?key/i.test(key)));
|
|
1553
|
-
}
|
|
1554
|
-
catch {
|
|
1555
|
-
throw new StateQLError("INVALID_COMMAND", "Invalid PostgreSQL URL.");
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
function version(connection) {
|
|
1559
|
-
return `sv_${connection.version}`;
|
|
1560
|
-
}
|
|
1561
|
-
function confidence(connection) {
|
|
1562
|
-
return connection.driver === "sqlite" ? "database_reported" : "ttl_based";
|
|
1563
|
-
}
|
|
1564
|
-
function sessionData(session) {
|
|
1565
|
-
return {
|
|
1566
|
-
session_id: session.id,
|
|
1567
|
-
name: session.name,
|
|
1568
|
-
state: session.status,
|
|
1569
|
-
active_connection: session.active_connection_id,
|
|
1570
|
-
active_transaction: session.active_transaction_id,
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
function profileData(profile) {
|
|
1574
|
-
return {
|
|
1575
|
-
profile: profile.name,
|
|
1576
|
-
target: profile.target,
|
|
1577
|
-
secret_env: profile.secret_env,
|
|
1578
|
-
read_only: Boolean(profile.read_only),
|
|
1579
|
-
};
|
|
1580
|
-
}
|
|
1581
|
-
function validateProfileName(name) {
|
|
1582
|
-
if (/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name))
|
|
1583
|
-
return;
|
|
1584
|
-
throw new StateQLError("INVALID_COMMAND", "Profile name must be 1-64 letters, numbers, dots, underscores, or hyphens.");
|
|
1585
|
-
}
|
|
1586
|
-
function isEnvironmentName(name) {
|
|
1587
|
-
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
1588
|
-
}
|
|
1589
|
-
function operationData(operation) {
|
|
1590
|
-
return {
|
|
1591
|
-
operation_id: operation.id,
|
|
1592
|
-
statement_type: operation.statement_type,
|
|
1593
|
-
affected_rows: operation.affected_rows,
|
|
1594
|
-
status: operation.status,
|
|
1595
|
-
committed: operation.status === "committed",
|
|
1596
|
-
transaction_id: operation.transaction_id,
|
|
1597
|
-
state_version_before: operation.state_version_before,
|
|
1598
|
-
state_version_after: operation.state_version_after,
|
|
1599
|
-
...(operation.replay_of ? { replay_of: operation.replay_of } : {}),
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
function transactionData(transaction, statements) {
|
|
1603
|
-
return {
|
|
1604
|
-
transaction_id: transaction.id,
|
|
1605
|
-
state: transaction.state,
|
|
1606
|
-
connection_id: transaction.connection_id,
|
|
1607
|
-
statements,
|
|
1608
|
-
pending_writes: transaction.state === "active" ? statements : 0,
|
|
1609
|
-
start_state_version: transaction.start_version,
|
|
1610
|
-
isolation_level: transaction.isolation_level,
|
|
1611
|
-
age_ms: Date.now() - Date.parse(transaction.created_at),
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
function paginationWarnings(ordered) {
|
|
1615
|
-
if (ordered)
|
|
1616
|
-
return [];
|
|
1617
|
-
return [
|
|
1618
|
-
{
|
|
1619
|
-
code: "NON_DETERMINISTIC_PAGINATION",
|
|
1620
|
-
message: "Result has no explicit ORDER BY clause.",
|
|
1621
|
-
},
|
|
1622
|
-
];
|
|
1623
|
-
}
|
|
1624
1363
|
function batchString(value, name) {
|
|
1625
1364
|
if (value?.trim())
|
|
1626
1365
|
return value;
|
|
@@ -1636,19 +1375,15 @@ function positiveInteger(value, name) {
|
|
|
1636
1375
|
return value;
|
|
1637
1376
|
throw new StateQLError("INVALID_COMMAND", `${name} must be a positive integer.`);
|
|
1638
1377
|
}
|
|
1639
|
-
function
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
};
|
|
1648
|
-
return [
|
|
1649
|
-
columns.map(encode).join(","),
|
|
1650
|
-
...rows.map((row) => columns.map((column) => encode(row[column])).join(",")),
|
|
1651
|
-
].join("\n") + "\n";
|
|
1378
|
+
function executionTimeout(value) {
|
|
1379
|
+
const timeout = positiveInteger(value, "timeoutMs");
|
|
1380
|
+
if (timeout > 2_147_483_647) {
|
|
1381
|
+
throw new StateQLError("INVALID_COMMAND", "timeoutMs cannot exceed 2147483647 milliseconds.");
|
|
1382
|
+
}
|
|
1383
|
+
return timeout;
|
|
1384
|
+
}
|
|
1385
|
+
function stoppedStateQLError(error, executed) {
|
|
1386
|
+
return new StateQLError(error.reason === "timeout" ? "DEADLINE_EXCEEDED" : "OPERATION_CANCELLED", error.message, { retryable: true, executed });
|
|
1652
1387
|
}
|
|
1653
1388
|
function errorMessage(error) {
|
|
1654
1389
|
return error instanceof Error ? error.message : String(error);
|