@fadhilp/stateql 0.1.2 → 0.2.2
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 +16 -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 +15 -5
- package/dist/src/stateql.js +189 -397
- package/dist/src/store.d.ts +2 -0
- package/dist/src/store.js +37 -2
- package/dist/src/types.d.ts +56 -6
- package/package.json +5 -2
package/dist/src/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ const parsed = parseArgs({
|
|
|
24
24
|
"allow-destructive": { type: "boolean" },
|
|
25
25
|
offset: { type: "string" },
|
|
26
26
|
limit: { type: "string" },
|
|
27
|
+
"timeout-ms": { type: "string" },
|
|
27
28
|
format: { type: "string" },
|
|
28
29
|
output: { type: "string" },
|
|
29
30
|
isolation: { type: "string" },
|
|
@@ -35,14 +36,21 @@ const parsed = parseArgs({
|
|
|
35
36
|
const [command, subcommand, ...rest] = parsed.positionals;
|
|
36
37
|
const values = parsed.values;
|
|
37
38
|
if (values.version) {
|
|
38
|
-
console.log(
|
|
39
|
+
console.log(packageVersion());
|
|
39
40
|
process.exit(0);
|
|
40
41
|
}
|
|
41
42
|
if (values.help || !command) {
|
|
42
43
|
console.log(helpText());
|
|
43
44
|
process.exit(0);
|
|
44
45
|
}
|
|
45
|
-
const
|
|
46
|
+
const abortController = new AbortController();
|
|
47
|
+
process.once("SIGINT", () => abortController.abort());
|
|
48
|
+
const stateql = new StateQL({
|
|
49
|
+
...(values["timeout-ms"] === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: { timeoutMs: Number(values["timeout-ms"]) }),
|
|
52
|
+
signal: abortController.signal,
|
|
53
|
+
});
|
|
46
54
|
try {
|
|
47
55
|
if (command === "batch" || command === "pipe") {
|
|
48
56
|
await runBatch();
|
|
@@ -494,7 +502,7 @@ function extractHandle(data) {
|
|
|
494
502
|
return undefined;
|
|
495
503
|
}
|
|
496
504
|
function helpText() {
|
|
497
|
-
return `StateQL
|
|
505
|
+
return `StateQL ${packageVersion()}
|
|
498
506
|
|
|
499
507
|
Usage: stql <command> [arguments] [options]
|
|
500
508
|
|
|
@@ -511,6 +519,11 @@ Commands:
|
|
|
511
519
|
pipe
|
|
512
520
|
|
|
513
521
|
SQL parameters: --params JSON, repeated --param VALUE, or --params-file FILE.
|
|
522
|
+
Deadline: --timeout-ms N (default: 30000). Ctrl+C cancels database work.
|
|
514
523
|
Output: --output agent|json|jsonl|text|silent (default: agent).
|
|
515
524
|
Batch/pipe accept JSON array files or JSONL streams. Stop on first error.`;
|
|
516
525
|
}
|
|
526
|
+
function packageVersion() {
|
|
527
|
+
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
528
|
+
return packageJson.version;
|
|
529
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConnectionRecord } from "./store.js";
|
|
2
|
+
import type { Driver, StateConfidence } from "./types.js";
|
|
3
|
+
export declare function databaseIdentity(connection: ConnectionRecord): unknown;
|
|
4
|
+
export declare function detectDriver(target: string): Driver;
|
|
5
|
+
export declare function normalizeSqliteSource(target: string): string;
|
|
6
|
+
export declare function databaseUrlHasSecret(target: string): boolean;
|
|
7
|
+
export declare function version(connection: ConnectionRecord): string;
|
|
8
|
+
export declare function confidence(connection: ConnectionRecord): StateConfidence;
|
|
9
|
+
export declare function validateProfileName(name: string): void;
|
|
10
|
+
export declare function isEnvironmentName(name: string): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { StateQLError } from "./errors.js";
|
|
3
|
+
export function databaseIdentity(connection) {
|
|
4
|
+
return {
|
|
5
|
+
driver: connection.driver,
|
|
6
|
+
database: connection.database_name,
|
|
7
|
+
source: connection.source,
|
|
8
|
+
secretEnvironment: connection.secret_env,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function detectDriver(target) {
|
|
12
|
+
if (/^postgres(?:ql)?:\/\//i.test(target))
|
|
13
|
+
return "postgres";
|
|
14
|
+
if (/^mysql:\/\//i.test(target))
|
|
15
|
+
return "mysql";
|
|
16
|
+
if (/^[a-z][a-z\d+.-]*:\/\//i.test(target)) {
|
|
17
|
+
throw new StateQLError("UNSUPPORTED_DRIVER", "Only MySQL, PostgreSQL, and SQLite are supported.");
|
|
18
|
+
}
|
|
19
|
+
return "sqlite";
|
|
20
|
+
}
|
|
21
|
+
export function normalizeSqliteSource(target) {
|
|
22
|
+
const source = target.startsWith("sqlite:") ? target.slice(7) : target;
|
|
23
|
+
if (source === ":memory:") {
|
|
24
|
+
throw new StateQLError("INVALID_COMMAND", "SQLite :memory: databases cannot persist across StateQL commands.");
|
|
25
|
+
}
|
|
26
|
+
return resolve(source);
|
|
27
|
+
}
|
|
28
|
+
export function databaseUrlHasSecret(target) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(target);
|
|
31
|
+
return (Boolean(url.password) ||
|
|
32
|
+
[...url.searchParams.keys()].some((key) => /pass|token|secret|private[_-]?key|api[_-]?key/i.test(key)));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new StateQLError("INVALID_COMMAND", "Invalid database URL.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function version(connection) {
|
|
39
|
+
return `sv_${connection.version}`;
|
|
40
|
+
}
|
|
41
|
+
export function confidence(connection) {
|
|
42
|
+
return connection.driver === "sqlite" ? "database_reported" : "ttl_based";
|
|
43
|
+
}
|
|
44
|
+
export function validateProfileName(name) {
|
|
45
|
+
if (/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name))
|
|
46
|
+
return;
|
|
47
|
+
throw new StateQLError("INVALID_COMMAND", "Profile name must be 1-64 letters, numbers, dots, underscores, or hyphens.");
|
|
48
|
+
}
|
|
49
|
+
export function isEnvironmentName(name) {
|
|
50
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
51
|
+
}
|
package/dist/src/errors.js
CHANGED
|
@@ -20,14 +20,18 @@ export function exitCodeFor(code) {
|
|
|
20
20
|
return 2;
|
|
21
21
|
if (code.startsWith("CONNECTION_"))
|
|
22
22
|
return 3;
|
|
23
|
-
if (code === "QUERY_FAILED"
|
|
23
|
+
if (code === "QUERY_FAILED" ||
|
|
24
|
+
code === "DEADLINE_EXCEEDED" ||
|
|
25
|
+
code === "OPERATION_CANCELLED")
|
|
24
26
|
return 4;
|
|
25
27
|
if (code === "READ_ONLY_CONNECTION" ||
|
|
26
28
|
code === "UNBOUNDED_MUTATION" ||
|
|
27
29
|
code === "DESTRUCTIVE_OPERATION_BLOCKED") {
|
|
28
30
|
return 5;
|
|
29
31
|
}
|
|
30
|
-
if (code === "POTENTIAL_DUPLICATE_WRITE" ||
|
|
32
|
+
if (code === "POTENTIAL_DUPLICATE_WRITE" ||
|
|
33
|
+
code === "OUTCOME_UNKNOWN" ||
|
|
34
|
+
code === "IDEMPOTENCY_CONFLICT")
|
|
31
35
|
return 6;
|
|
32
36
|
if (code.startsWith("STALE_") || code === "RESULT_EXPIRED")
|
|
33
37
|
return 7;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Column, Row, SqlParameters } from "./types.js";
|
|
2
|
+
interface PreparedFilter {
|
|
3
|
+
sql: string;
|
|
4
|
+
normalized: string;
|
|
5
|
+
tableName: string;
|
|
6
|
+
indexColumn: string;
|
|
7
|
+
columnNames: string[];
|
|
8
|
+
positionalParameters: number;
|
|
9
|
+
namedParameters: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function prepareFilterStatement(columns: Column[], predicate: string): PreparedFilter;
|
|
12
|
+
export declare function filterMaterializedRows(rows: Row[], filter: PreparedFilter, parameters: SqlParameters): Row[];
|
|
13
|
+
export declare function validateFilterParameters(filter: PreparedFilter, parameters: SqlParameters): void;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { StateQLError } from "./errors.js";
|
|
3
|
+
import { analyzeSql } from "./sql.js";
|
|
4
|
+
export function prepareFilterStatement(columns, predicate) {
|
|
5
|
+
const text = predicate.trim();
|
|
6
|
+
if (!text) {
|
|
7
|
+
throw new StateQLError("INVALID_SQL", "Filter predicate is required.");
|
|
8
|
+
}
|
|
9
|
+
const columnNames = columns.map((column) => column.name);
|
|
10
|
+
if (columnNames.length === 0) {
|
|
11
|
+
throw new StateQLError("INVALID_SQL", "Filter requires at least one result column.");
|
|
12
|
+
}
|
|
13
|
+
const names = new Set();
|
|
14
|
+
for (const name of columnNames) {
|
|
15
|
+
const normalized = name.toLowerCase();
|
|
16
|
+
if (!name || name.includes("\0") || names.has(normalized)) {
|
|
17
|
+
throw new StateQLError("INVALID_SQL", "Filter requires unique, non-empty result column names.");
|
|
18
|
+
}
|
|
19
|
+
names.add(normalized);
|
|
20
|
+
}
|
|
21
|
+
const tableName = "__stateql_filter_source";
|
|
22
|
+
let indexColumn = "__stateql_row_index";
|
|
23
|
+
while (names.has(indexColumn.toLowerCase()))
|
|
24
|
+
indexColumn += "_";
|
|
25
|
+
const sql = `SELECT ${quoteIdentifier(indexColumn)} ` +
|
|
26
|
+
`FROM ${quoteIdentifier(tableName)} WHERE (${text})`;
|
|
27
|
+
const analysis = analyzeSql(sql, "sqlite");
|
|
28
|
+
const details = analysis.ast;
|
|
29
|
+
const from = details.from;
|
|
30
|
+
const source = Array.isArray(from)
|
|
31
|
+
? from[0]
|
|
32
|
+
: undefined;
|
|
33
|
+
if (!analysis.read ||
|
|
34
|
+
!Array.isArray(from) ||
|
|
35
|
+
from.length !== 1 ||
|
|
36
|
+
source?.table !== tableName ||
|
|
37
|
+
details.with ||
|
|
38
|
+
details.groupby ||
|
|
39
|
+
details.having ||
|
|
40
|
+
details.orderby ||
|
|
41
|
+
details.limit ||
|
|
42
|
+
details.for_update ||
|
|
43
|
+
details._next ||
|
|
44
|
+
details.set_op ||
|
|
45
|
+
containsSelect(details.where)) {
|
|
46
|
+
throw new StateQLError("INVALID_SQL", "Filter accepts one scalar predicate only.");
|
|
47
|
+
}
|
|
48
|
+
validateFilterExpression(details.where, names, tableName);
|
|
49
|
+
const bindings = filterBindings(details.where);
|
|
50
|
+
return {
|
|
51
|
+
sql,
|
|
52
|
+
normalized: analysis.normalized,
|
|
53
|
+
tableName,
|
|
54
|
+
indexColumn,
|
|
55
|
+
columnNames,
|
|
56
|
+
positionalParameters: bindings.positional,
|
|
57
|
+
namedParameters: [...bindings.named],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function filterMaterializedRows(rows, filter, parameters) {
|
|
61
|
+
const db = new DatabaseSync(":memory:");
|
|
62
|
+
try {
|
|
63
|
+
const definitions = [
|
|
64
|
+
`${quoteIdentifier(filter.indexColumn)} INTEGER PRIMARY KEY`,
|
|
65
|
+
...filter.columnNames.map(quoteIdentifier),
|
|
66
|
+
];
|
|
67
|
+
db.exec(`CREATE TABLE ${quoteIdentifier(filter.tableName)} ` +
|
|
68
|
+
`(${definitions.join(", ")})`);
|
|
69
|
+
const placeholders = filter.columnNames.map(() => "?").join(", ");
|
|
70
|
+
const insert = db.prepare(`INSERT INTO ${quoteIdentifier(filter.tableName)} (` +
|
|
71
|
+
`${quoteIdentifier(filter.indexColumn)}, ` +
|
|
72
|
+
`${filter.columnNames.map(quoteIdentifier).join(", ")}) ` +
|
|
73
|
+
`VALUES (?, ${placeholders})`);
|
|
74
|
+
db.exec("BEGIN");
|
|
75
|
+
try {
|
|
76
|
+
rows.forEach((row, index) => {
|
|
77
|
+
const values = filter.columnNames.map((name) => sqliteFilterValue(row[name]));
|
|
78
|
+
insert.run(index, ...values);
|
|
79
|
+
});
|
|
80
|
+
db.exec("COMMIT");
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
db.exec("ROLLBACK");
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const statement = db.prepare(`${filter.sql}\nORDER BY ${quoteIdentifier(filter.indexColumn)}`);
|
|
87
|
+
const selected = filterAll(statement, parameters);
|
|
88
|
+
return selected.map((row) => {
|
|
89
|
+
const index = Number(row[filter.indexColumn]);
|
|
90
|
+
if (!Number.isInteger(index) || !rows[index]) {
|
|
91
|
+
throw new StateQLError("INVALID_SQL", "Filter produced an invalid source row index.");
|
|
92
|
+
}
|
|
93
|
+
return rows[index];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof StateQLError)
|
|
98
|
+
throw error;
|
|
99
|
+
throw new StateQLError("INVALID_SQL", errorMessage(error));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
db.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function filterAll(statement, parameters) {
|
|
106
|
+
if (Array.isArray(parameters)) {
|
|
107
|
+
return statement.all(...parameters);
|
|
108
|
+
}
|
|
109
|
+
return statement.all(parameters);
|
|
110
|
+
}
|
|
111
|
+
function sqliteFilterValue(value) {
|
|
112
|
+
if (value === null || value === undefined)
|
|
113
|
+
return null;
|
|
114
|
+
if (typeof value === "string" ||
|
|
115
|
+
typeof value === "number" ||
|
|
116
|
+
typeof value === "bigint") {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
if (typeof value === "boolean")
|
|
120
|
+
return value ? 1 : 0;
|
|
121
|
+
if (value instanceof Uint8Array)
|
|
122
|
+
return value;
|
|
123
|
+
try {
|
|
124
|
+
return JSON.stringify(value) ?? null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return String(value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function quoteIdentifier(value) {
|
|
131
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
132
|
+
}
|
|
133
|
+
function containsSelect(value) {
|
|
134
|
+
if (!value || typeof value !== "object")
|
|
135
|
+
return false;
|
|
136
|
+
const record = value;
|
|
137
|
+
if (record.type === "select")
|
|
138
|
+
return true;
|
|
139
|
+
return Object.values(record).some(containsSelect);
|
|
140
|
+
}
|
|
141
|
+
const FILTER_FUNCTIONS = new Set([
|
|
142
|
+
"abs",
|
|
143
|
+
"coalesce",
|
|
144
|
+
"ifnull",
|
|
145
|
+
"instr",
|
|
146
|
+
"json_extract",
|
|
147
|
+
"json_type",
|
|
148
|
+
"json_valid",
|
|
149
|
+
"length",
|
|
150
|
+
"lower",
|
|
151
|
+
"ltrim",
|
|
152
|
+
"nullif",
|
|
153
|
+
"round",
|
|
154
|
+
"rtrim",
|
|
155
|
+
"substr",
|
|
156
|
+
"substring",
|
|
157
|
+
"trim",
|
|
158
|
+
"typeof",
|
|
159
|
+
"upper",
|
|
160
|
+
]);
|
|
161
|
+
function validateFilterExpression(value, columns, tableName) {
|
|
162
|
+
if (!value || typeof value !== "object")
|
|
163
|
+
return;
|
|
164
|
+
const record = value;
|
|
165
|
+
if (record.type === "column_ref") {
|
|
166
|
+
const column = record.column;
|
|
167
|
+
const table = record.table;
|
|
168
|
+
if (typeof column !== "string" ||
|
|
169
|
+
!columns.has(column.toLowerCase()) ||
|
|
170
|
+
(table !== null && table !== undefined && table !== tableName)) {
|
|
171
|
+
throw new StateQLError("INVALID_SQL", `Unknown filter column "${String(column)}".`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (record.type === "double_quote_string") {
|
|
175
|
+
const column = String(record.value);
|
|
176
|
+
if (!columns.has(column.toLowerCase())) {
|
|
177
|
+
throw new StateQLError("INVALID_SQL", `Unknown filter column "${column}".`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (record.type === "function") {
|
|
181
|
+
const name = filterFunctionName(record);
|
|
182
|
+
if (!name || !FILTER_FUNCTIONS.has(name)) {
|
|
183
|
+
throw new StateQLError("INVALID_SQL", `Filter function "${name ?? "unknown"}" is not allowed.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
Object.values(record).forEach((item) => validateFilterExpression(item, columns, tableName));
|
|
187
|
+
}
|
|
188
|
+
function filterFunctionName(record) {
|
|
189
|
+
const name = record.name;
|
|
190
|
+
const parts = name?.name;
|
|
191
|
+
if (!Array.isArray(parts))
|
|
192
|
+
return undefined;
|
|
193
|
+
const last = parts.at(-1);
|
|
194
|
+
return typeof last?.value === "string" ? last.value.toLowerCase() : undefined;
|
|
195
|
+
}
|
|
196
|
+
function filterBindings(value) {
|
|
197
|
+
const named = new Set();
|
|
198
|
+
const prefixes = new Map();
|
|
199
|
+
let positional = 0;
|
|
200
|
+
const visit = (item) => {
|
|
201
|
+
if (!item || typeof item !== "object")
|
|
202
|
+
return;
|
|
203
|
+
const record = item;
|
|
204
|
+
if (record.type === "origin" && record.value === "?")
|
|
205
|
+
positional += 1;
|
|
206
|
+
if (record.type === "param" && typeof record.value === "string") {
|
|
207
|
+
addNamed(String(record.value), `:${String(record.value)}`);
|
|
208
|
+
}
|
|
209
|
+
if (record.type === "var" &&
|
|
210
|
+
typeof record.name === "string" &&
|
|
211
|
+
(record.prefix === "$" || record.prefix === "@")) {
|
|
212
|
+
addNamed(record.name, `${String(record.prefix)}${record.name}`);
|
|
213
|
+
}
|
|
214
|
+
Object.values(record).forEach(visit);
|
|
215
|
+
};
|
|
216
|
+
const addNamed = (name, token) => {
|
|
217
|
+
const previous = prefixes.get(name);
|
|
218
|
+
if (previous && previous !== token) {
|
|
219
|
+
throw new StateQLError("INVALID_SQL", `Filter parameter "${name}" uses conflicting prefixes.`);
|
|
220
|
+
}
|
|
221
|
+
prefixes.set(name, token);
|
|
222
|
+
named.add(name);
|
|
223
|
+
};
|
|
224
|
+
visit(value);
|
|
225
|
+
if (positional && named.size) {
|
|
226
|
+
throw new StateQLError("INVALID_SQL", "Filter cannot mix positional and named parameters.");
|
|
227
|
+
}
|
|
228
|
+
return { positional, named };
|
|
229
|
+
}
|
|
230
|
+
export function validateFilterParameters(filter, parameters) {
|
|
231
|
+
if (filter.positionalParameters) {
|
|
232
|
+
if (!Array.isArray(parameters) ||
|
|
233
|
+
parameters.length !== filter.positionalParameters) {
|
|
234
|
+
throw new StateQLError("INVALID_SQL", `Filter requires exactly ${filter.positionalParameters} positional parameters.`);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (filter.namedParameters.length) {
|
|
239
|
+
if (Array.isArray(parameters)) {
|
|
240
|
+
throw new StateQLError("INVALID_SQL", "Filter requires named parameters.");
|
|
241
|
+
}
|
|
242
|
+
const supplied = Object.keys(parameters).sort();
|
|
243
|
+
const expected = [...filter.namedParameters].sort();
|
|
244
|
+
if (JSON.stringify(supplied) !== JSON.stringify(expected)) {
|
|
245
|
+
throw new StateQLError("INVALID_SQL", `Filter requires named parameters: ${expected.join(", ")}.`);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if ((Array.isArray(parameters) && parameters.length) ||
|
|
250
|
+
(!Array.isArray(parameters) && Object.keys(parameters).length)) {
|
|
251
|
+
throw new StateQLError("INVALID_SQL", "Filter predicate has no parameters.");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function errorMessage(error) {
|
|
255
|
+
return error instanceof Error ? error.message : String(error);
|
|
256
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { StateQL } from "./stateql.js";
|
|
2
2
|
export { StateQLError, exitCodeFor } from "./errors.js";
|
|
3
|
-
export type { BatchCommand, BatchCommandName, BatchOptions, ConnectOptions, ExecOptions, Failure, FilterOptions, PlanOptions, ProfileOptions, QueryOptions, Response, RowsOptions, SqlParameters, StateQLOptions, Success, } from "./types.js";
|
|
3
|
+
export type { BatchCommand, BatchCommandName, BatchOptions, ConnectOptions, ExecOptions, ExecutionOptions, Failure, FilterOptions, HistoryEntry, PlanOptions, ProfileOptions, QueryOptions, Response, RowsOptions, SqlParameters, StateQLOptions, StateQLSnapshot, Success, } from "./types.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OperationRecord, ProfileRecord, SessionRecord, TransactionRecord } from "./store.js";
|
|
2
|
+
import type { Row, Warning } from "./types.js";
|
|
3
|
+
export declare function sessionData(session: SessionRecord): unknown;
|
|
4
|
+
export declare function profileData(profile: ProfileRecord): Record<string, unknown>;
|
|
5
|
+
export declare function operationData(operation: OperationRecord): Record<string, unknown>;
|
|
6
|
+
export declare function transactionData(transaction: TransactionRecord, statements: number): unknown;
|
|
7
|
+
export declare function paginationWarnings(ordered: boolean): Warning[];
|
|
8
|
+
export declare function rowsToCsv(rows: Row[], columns: string[]): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export function sessionData(session) {
|
|
2
|
+
return {
|
|
3
|
+
session_id: session.id,
|
|
4
|
+
name: session.name,
|
|
5
|
+
state: session.status,
|
|
6
|
+
active_connection: session.active_connection_id,
|
|
7
|
+
active_transaction: session.active_transaction_id,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function profileData(profile) {
|
|
11
|
+
return {
|
|
12
|
+
profile: profile.name,
|
|
13
|
+
target: profile.target,
|
|
14
|
+
secret_env: profile.secret_env,
|
|
15
|
+
read_only: Boolean(profile.read_only),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function operationData(operation) {
|
|
19
|
+
return {
|
|
20
|
+
operation_id: operation.id,
|
|
21
|
+
statement_type: operation.statement_type,
|
|
22
|
+
affected_rows: operation.affected_rows,
|
|
23
|
+
status: operation.status,
|
|
24
|
+
committed: operation.status === "committed",
|
|
25
|
+
transaction_id: operation.transaction_id,
|
|
26
|
+
state_version_before: operation.state_version_before,
|
|
27
|
+
state_version_after: operation.state_version_after,
|
|
28
|
+
...(operation.replay_of ? { replay_of: operation.replay_of } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function transactionData(transaction, statements) {
|
|
32
|
+
return {
|
|
33
|
+
transaction_id: transaction.id,
|
|
34
|
+
state: transaction.state,
|
|
35
|
+
connection_id: transaction.connection_id,
|
|
36
|
+
statements,
|
|
37
|
+
pending_writes: transaction.state === "active" ? statements : 0,
|
|
38
|
+
start_state_version: transaction.start_version,
|
|
39
|
+
isolation_level: transaction.isolation_level,
|
|
40
|
+
age_ms: Date.now() - Date.parse(transaction.created_at),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function paginationWarnings(ordered) {
|
|
44
|
+
if (ordered)
|
|
45
|
+
return [];
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
code: "NON_DETERMINISTIC_PAGINATION",
|
|
49
|
+
message: "Result has no explicit ORDER BY clause.",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
export function rowsToCsv(rows, columns) {
|
|
54
|
+
const encode = (value) => {
|
|
55
|
+
const text = value === null || value === undefined
|
|
56
|
+
? ""
|
|
57
|
+
: typeof value === "object"
|
|
58
|
+
? JSON.stringify(value)
|
|
59
|
+
: String(value);
|
|
60
|
+
return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
61
|
+
};
|
|
62
|
+
return [
|
|
63
|
+
columns.map(encode).join(","),
|
|
64
|
+
...rows.map((row) => columns.map((column) => encode(row[column])).join(",")),
|
|
65
|
+
].join("\n") + "\n";
|
|
66
|
+
}
|
package/dist/src/sql.js
CHANGED
|
@@ -18,7 +18,11 @@ export function analyzeSql(sql, driver) {
|
|
|
18
18
|
if (!trimmed)
|
|
19
19
|
throw new StateQLError("INVALID_SQL", "SQL is empty.");
|
|
20
20
|
try {
|
|
21
|
-
const database = driver === "postgres"
|
|
21
|
+
const database = driver === "postgres"
|
|
22
|
+
? "Postgresql"
|
|
23
|
+
: driver === "mysql"
|
|
24
|
+
? "MySQL"
|
|
25
|
+
: "Sqlite";
|
|
22
26
|
const parsed = parser.astify(trimmed, { database });
|
|
23
27
|
if (Array.isArray(parsed)) {
|
|
24
28
|
if (parsed.length !== 1) {
|
|
@@ -50,7 +54,9 @@ export function analyzeSql(sql, driver) {
|
|
|
50
54
|
statementType === "alter" ||
|
|
51
55
|
statementType === "delete" ||
|
|
52
56
|
statementType === "replace" ||
|
|
53
|
-
statementType === "truncate"
|
|
57
|
+
statementType === "truncate" ||
|
|
58
|
+
(driver === "sqlite" &&
|
|
59
|
+
/^(?:INSERT|UPDATE) OR REPLACE\b/i.test(normalized));
|
|
54
60
|
return {
|
|
55
61
|
ast,
|
|
56
62
|
normalized,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|