@telorun/sql 0.9.0 → 0.10.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/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/sql-command-controller.d.ts +2 -2
- package/dist/sql-connection-base.d.ts +29 -0
- package/dist/{sql-connection-controller.js → sql-connection-base.js} +18 -36
- package/dist/sql-connection-ref.d.ts +3 -3
- package/dist/sql-connection.d.ts +45 -0
- package/dist/sql-connection.js +4 -0
- package/dist/sql-migrations-controller.d.ts +2 -2
- package/dist/sql-migrations-controller.js +5 -0
- package/dist/sql-query-controller.d.ts +2 -2
- package/dist/sql-run.d.ts +2 -2
- package/dist/sql-selection-controller.d.ts +2 -2
- package/dist/sql-selection-controller.js +22 -27
- package/dist/sql-transaction-controller.d.ts +3 -3
- package/package.json +2 -7
- package/src/index.ts +5 -5
- package/src/sql-command-controller.ts +2 -2
- package/src/{sql-connection-controller.ts → sql-connection-base.ts} +22 -55
- package/src/sql-connection-ref.ts +5 -5
- package/src/sql-connection.ts +71 -0
- package/src/sql-migrations-controller.ts +10 -2
- package/src/sql-query-controller.ts +4 -4
- package/src/sql-run.ts +2 -2
- package/src/sql-selection-controller.ts +27 -32
- package/src/sql-transaction-controller.ts +3 -3
- package/dist/sql-connection-controller.d.ts +0 -43
- package/dist/sqlite-driver-interface.d.ts +0 -14
- package/dist/sqlite-driver-interface.js +0 -1
- package/src/sqlite-driver-interface.ts +0 -15
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { SqlConnectionBase } from "./sql-connection-base.js";
|
|
2
|
+
export { quoteAnsiIdentifier, type PlaceholderStyle, type SqlConnection, type SqlDialect, } from "./sql-connection.js";
|
|
2
3
|
export { isSqlConnection, resolveSqlConnection } from "./sql-connection-ref.js";
|
|
3
|
-
export type { SqliteDb, SqliteStatement } from "./sqlite-driver-interface.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { SqlConnectionBase } from "./sql-connection-base.js";
|
|
2
|
+
export { quoteAnsiIdentifier, } from "./sql-connection.js";
|
|
2
3
|
export { isSqlConnection, resolveSqlConnection } from "./sql-connection-ref.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
5
|
interface SqlCommandManifest {
|
|
@@ -7,7 +7,7 @@ interface SqlCommandManifest {
|
|
|
7
7
|
name: string;
|
|
8
8
|
module: string;
|
|
9
9
|
};
|
|
10
|
-
connection?:
|
|
10
|
+
connection?: SqlConnection;
|
|
11
11
|
transaction?: SqlTransactionResource;
|
|
12
12
|
inputs: {
|
|
13
13
|
sql: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Kysely, type QueryResult } from "kysely";
|
|
2
|
+
import type { SqlConnection, SqlDialect } from "./sql-connection.js";
|
|
3
|
+
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
|
+
/**
|
|
5
|
+
* The dialect-neutral half of a connection: statement execution, transaction
|
|
6
|
+
* scoping, template binding and row-count normalization over a kysely instance.
|
|
7
|
+
*
|
|
8
|
+
* Backends extend it, supply their {@link SqlDialect}, and override only what is
|
|
9
|
+
* genuinely theirs — `teardown` for resources kysely does not own, `executeScript`
|
|
10
|
+
* where the driver has a native multi-statement path.
|
|
11
|
+
*/
|
|
12
|
+
export declare abstract class SqlConnectionBase implements SqlConnection {
|
|
13
|
+
readonly dialect: SqlDialect;
|
|
14
|
+
protected readonly db: Kysely<any>;
|
|
15
|
+
constructor(db: Kysely<any>, dialect: SqlDialect);
|
|
16
|
+
get kysely(): Kysely<any>;
|
|
17
|
+
init(): Promise<void>;
|
|
18
|
+
teardown(): Promise<void>;
|
|
19
|
+
transaction<T>(cb: () => Promise<T>): Promise<T>;
|
|
20
|
+
execute<T>(sql: string, params?: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
21
|
+
executeTemplate<T>(fragments: string[], values: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
22
|
+
/** Hand the whole script to the driver as one statement. Backends whose driver
|
|
23
|
+
* needs a dedicated multi-statement entry point override this. */
|
|
24
|
+
executeScript(sql: string): Promise<void>;
|
|
25
|
+
toRowCount(result: QueryResult<unknown>): number;
|
|
26
|
+
snapshot(): Record<string, unknown>;
|
|
27
|
+
private placeholder;
|
|
28
|
+
private resolveExecutor;
|
|
29
|
+
}
|
|
@@ -2,20 +2,22 @@ import { randomUUID } from "crypto";
|
|
|
2
2
|
import { CompiledQuery } from "kysely";
|
|
3
3
|
import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* {@link
|
|
9
|
-
*
|
|
5
|
+
* The dialect-neutral half of a connection: statement execution, transaction
|
|
6
|
+
* scoping, template binding and row-count normalization over a kysely instance.
|
|
7
|
+
*
|
|
8
|
+
* Backends extend it, supply their {@link SqlDialect}, and override only what is
|
|
9
|
+
* genuinely theirs — `teardown` for resources kysely does not own, `executeScript`
|
|
10
|
+
* where the driver has a native multi-statement path.
|
|
10
11
|
*/
|
|
11
|
-
export class
|
|
12
|
-
|
|
12
|
+
export class SqlConnectionBase {
|
|
13
|
+
dialect;
|
|
13
14
|
db;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this.driver = driver;
|
|
15
|
+
constructor(db, dialect) {
|
|
16
|
+
this.dialect = dialect;
|
|
17
17
|
this.db = db;
|
|
18
|
-
|
|
18
|
+
}
|
|
19
|
+
get kysely() {
|
|
20
|
+
return this.db;
|
|
19
21
|
}
|
|
20
22
|
async init() {
|
|
21
23
|
await this.db.connection().execute(async () => {
|
|
@@ -41,12 +43,6 @@ export class SqlConnectionResource {
|
|
|
41
43
|
const executor = this.resolveExecutor(transaction);
|
|
42
44
|
return executor.executeQuery(CompiledQuery.raw(sql, params));
|
|
43
45
|
}
|
|
44
|
-
get placeholderStyle() {
|
|
45
|
-
return this.driver === "postgres" ? "numbered" : "qmark";
|
|
46
|
-
}
|
|
47
|
-
/** Assemble SQL from literal fragments by interleaving driver-native
|
|
48
|
-
* placeholders, then bind `values` positionally. `fragments.length` must
|
|
49
|
-
* equal `values.length + 1`. */
|
|
50
46
|
async executeTemplate(fragments, values, transaction) {
|
|
51
47
|
let sql = fragments[0] ?? "";
|
|
52
48
|
for (let i = 1; i < fragments.length; i++) {
|
|
@@ -54,14 +50,9 @@ export class SqlConnectionResource {
|
|
|
54
50
|
}
|
|
55
51
|
return this.execute(sql, values, transaction);
|
|
56
52
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
53
|
+
/** Hand the whole script to the driver as one statement. Backends whose driver
|
|
54
|
+
* needs a dedicated multi-statement entry point override this. */
|
|
60
55
|
async executeScript(sql) {
|
|
61
|
-
if (this.driver === "sqlite") {
|
|
62
|
-
this.sqlite?.exec(sql);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
56
|
await this.execute(sql);
|
|
66
57
|
}
|
|
67
58
|
toRowCount(result) {
|
|
@@ -70,12 +61,12 @@ export class SqlConnectionResource {
|
|
|
70
61
|
}
|
|
71
62
|
return result.rows.length;
|
|
72
63
|
}
|
|
73
|
-
get kysely() {
|
|
74
|
-
return this.db;
|
|
75
|
-
}
|
|
76
64
|
snapshot() {
|
|
77
65
|
return {};
|
|
78
66
|
}
|
|
67
|
+
placeholder(index) {
|
|
68
|
+
return this.dialect.placeholderStyle === "numbered" ? `$${index}` : "?";
|
|
69
|
+
}
|
|
79
70
|
resolveExecutor(transaction) {
|
|
80
71
|
if (transaction) {
|
|
81
72
|
transaction.assertActive();
|
|
@@ -90,12 +81,3 @@ export class SqlConnectionResource {
|
|
|
90
81
|
return this.db;
|
|
91
82
|
}
|
|
92
83
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Build a connection from a driver-constructed kysely instance. Driver backends
|
|
95
|
-
* (`sql-postgres`, `sql-sqlite`) own dialect construction and call this; the
|
|
96
|
-
* `sqlite` handle is required only for SQLite (its `executeScript` runs through
|
|
97
|
-
* the native handle).
|
|
98
|
-
*/
|
|
99
|
-
export function createSqlConnection(driver, db, sqlite) {
|
|
100
|
-
return new SqlConnectionResource(driver, db, sqlite);
|
|
101
|
-
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { KindRef, ResourceContext } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
/** True when a value already exposes the connection contract (Phase-5 injected). */
|
|
4
|
-
export declare function isSqlConnection(value: unknown): value is
|
|
4
|
+
export declare function isSqlConnection(value: unknown): value is SqlConnection;
|
|
5
5
|
/**
|
|
6
6
|
* Resolve a `connection` `!ref` field to a live connection. The slot is optional
|
|
7
7
|
* — an unset one yields `undefined` so the caller can fall back to a `transaction`
|
|
8
8
|
* — but a slot that IS set must resolve. `describe` names the owning resource and
|
|
9
9
|
* slot, so the failure points at a concrete manifest location.
|
|
10
10
|
*/
|
|
11
|
-
export declare function resolveSqlConnection(value:
|
|
11
|
+
export declare function resolveSqlConnection(value: SqlConnection | KindRef<SqlConnection> | undefined, ctx: ResourceContext, describe: () => string): SqlConnection | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ResourceInstance } from "@telorun/sdk";
|
|
2
|
+
import type { Kysely, QueryResult } from "kysely";
|
|
3
|
+
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
|
+
/** Native bind-placeholder syntax: SQLite binds anonymous `?`, PostgreSQL binds
|
|
5
|
+
* numbered `$1`, `$2`, … */
|
|
6
|
+
export type PlaceholderStyle = "qmark" | "numbered";
|
|
7
|
+
/**
|
|
8
|
+
* Renders the SQL constructs that differ between database dialects. A backend
|
|
9
|
+
* supplies one; the `Sql.*` operations build statements through it and never
|
|
10
|
+
* branch on which database sits behind the connection.
|
|
11
|
+
*/
|
|
12
|
+
export interface SqlDialect {
|
|
13
|
+
readonly placeholderStyle: PlaceholderStyle;
|
|
14
|
+
/** Quote an identifier — table, column, alias. */
|
|
15
|
+
quoteIdentifier(name: string): string;
|
|
16
|
+
/** Render set membership. Dialects disagree: PostgreSQL binds the whole array
|
|
17
|
+
* to a single placeholder, SQLite binds one per element. `column` arrives
|
|
18
|
+
* already quoted. */
|
|
19
|
+
renderIn(column: string, values: unknown[], addParam: (value: unknown) => string): string;
|
|
20
|
+
}
|
|
21
|
+
/** ANSI identifier quoting (`"name"`), for the dialects that follow the standard. */
|
|
22
|
+
export declare function quoteAnsiIdentifier(name: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The contract every SQL backend satisfies and every `Sql.*` operation programs
|
|
25
|
+
* against. Backends (`sql-postgres`, `sql-sqlite`) own their own implementation
|
|
26
|
+
* — usually by extending {@link SqlConnectionBase} — so nothing in this module
|
|
27
|
+
* knows which databases exist.
|
|
28
|
+
*/
|
|
29
|
+
export interface SqlConnection extends ResourceInstance {
|
|
30
|
+
readonly dialect: SqlDialect;
|
|
31
|
+
/** The underlying kysely instance, when the backend is built on one —
|
|
32
|
+
* {@link SqlConnectionBase} always provides it. Optional because the contract
|
|
33
|
+
* must stay implementable by a driver kysely does not support; a consumer
|
|
34
|
+
* that needs it (`Sql.Migrations`) checks and fails with a clear message. */
|
|
35
|
+
readonly kysely?: Kysely<any>;
|
|
36
|
+
execute<T>(sql: string, params?: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
37
|
+
/** Assemble SQL from literal fragments by interleaving dialect-native
|
|
38
|
+
* placeholders, then bind `values` positionally. */
|
|
39
|
+
executeTemplate<T>(fragments: string[], values: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
40
|
+
/** Run a multi-statement script. */
|
|
41
|
+
executeScript(sql: string): Promise<void>;
|
|
42
|
+
transaction<T>(cb: () => Promise<T>): Promise<T>;
|
|
43
|
+
/** Rows affected by a write, normalized across drivers. */
|
|
44
|
+
toRowCount(result: QueryResult<unknown>): number;
|
|
45
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
interface MigrationEntry {
|
|
4
4
|
statement?: string;
|
|
5
5
|
statements?: string[];
|
|
@@ -9,7 +9,7 @@ interface SqlMigrationsManifest {
|
|
|
9
9
|
name: string;
|
|
10
10
|
module: string;
|
|
11
11
|
};
|
|
12
|
-
connection:
|
|
12
|
+
connection: SqlConnection;
|
|
13
13
|
migrations?: Record<string, MigrationEntry>;
|
|
14
14
|
}
|
|
15
15
|
declare class SqlMigrationsResource implements ResourceInstance {
|
|
@@ -50,6 +50,11 @@ class SqlMigrationsResource {
|
|
|
50
50
|
}
|
|
51
51
|
migrations[name] = statements;
|
|
52
52
|
}
|
|
53
|
+
if (!conn.kysely) {
|
|
54
|
+
throw new Error(`Sql.Migrations '${this.manifest.metadata.name}': the referenced connection is not ` +
|
|
55
|
+
`built on kysely, which this kind's migration runner requires. Use a backend that ` +
|
|
56
|
+
`extends SqlConnectionBase, or run the statements through Sql.Command.`);
|
|
57
|
+
}
|
|
53
58
|
const migrator = new Migrator({
|
|
54
59
|
db: conn.kysely,
|
|
55
60
|
provider: new TeloMigrationProvider(migrations),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
4
|
interface SqlQueryManifest {
|
|
5
5
|
metadata: {
|
|
6
6
|
name: string;
|
|
7
7
|
module: string;
|
|
8
8
|
};
|
|
9
|
-
connection?:
|
|
9
|
+
connection?: SqlConnection;
|
|
10
10
|
transaction?: SqlTransactionResource;
|
|
11
11
|
inputs: {
|
|
12
12
|
sql: string;
|
package/dist/sql-run.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ResourceContext } from "@telorun/sdk";
|
|
2
2
|
import type { QueryResult } from "kysely";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
5
|
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
6
6
|
*
|
|
@@ -13,4 +13,4 @@ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
|
13
13
|
*
|
|
14
14
|
* Mixing a `!sql` template with `bindings` is rejected. A plain string `sql`
|
|
15
15
|
* with neither is executed verbatim. */
|
|
16
|
-
export declare function runSql(connection:
|
|
16
|
+
export declare function runSql(connection: SqlConnection, transaction: SqlTransactionResource | undefined, input: unknown, ctx: ResourceContext): Promise<QueryResult<Record<string, unknown>>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
5
|
type ColumnDef = string | {
|
|
@@ -44,7 +44,7 @@ interface SelectManifest {
|
|
|
44
44
|
name: string;
|
|
45
45
|
module: string;
|
|
46
46
|
};
|
|
47
|
-
connection?:
|
|
47
|
+
connection?: SqlConnection;
|
|
48
48
|
transaction?: SqlTransactionResource;
|
|
49
49
|
from: string;
|
|
50
50
|
columns?: ColumnDef[];
|
|
@@ -24,17 +24,19 @@ class SqlSelectionResource {
|
|
|
24
24
|
if (!connection) {
|
|
25
25
|
throw new Error("Sql.Selection: either 'connection' or 'transaction' must be set");
|
|
26
26
|
}
|
|
27
|
-
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.
|
|
27
|
+
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.dialect);
|
|
28
28
|
const result = await connection.execute(sql, params, m.transaction);
|
|
29
29
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
// ── SQL building ──────────────────────────────────────────────────────────────
|
|
33
|
+
function buildSelect(m, where, having, limit, offset, dialect) {
|
|
33
34
|
const params = [];
|
|
34
35
|
const addParam = (value) => {
|
|
35
36
|
params.push(value);
|
|
36
|
-
return `$${params.length}
|
|
37
|
+
return dialect.placeholderStyle === "numbered" ? `$${params.length}` : "?";
|
|
37
38
|
};
|
|
39
|
+
const quoteIdent = (name) => dialect.quoteIdentifier(name);
|
|
38
40
|
const parts = [];
|
|
39
41
|
// SELECT [DISTINCT [ON (...)]]
|
|
40
42
|
let selectClause = "SELECT";
|
|
@@ -44,12 +46,12 @@ function buildSelect(m, where, having, limit, offset, driver) {
|
|
|
44
46
|
else if (m.distinctOn && m.distinctOn.length > 0) {
|
|
45
47
|
selectClause += ` DISTINCT ON (${m.distinctOn.map(quoteIdent).join(", ")})`;
|
|
46
48
|
}
|
|
47
|
-
const colList = m.columns && m.columns.length > 0 ? buildColumns(m.columns) : "*";
|
|
49
|
+
const colList = m.columns && m.columns.length > 0 ? buildColumns(m.columns, dialect) : "*";
|
|
48
50
|
parts.push(`${selectClause} ${colList}`);
|
|
49
51
|
// FROM
|
|
50
52
|
parts.push(`FROM ${quoteIdent(m.from)}`);
|
|
51
53
|
// WHERE
|
|
52
|
-
const whereStr = buildClauses(where, "AND",
|
|
54
|
+
const whereStr = buildClauses(where, "AND", dialect, addParam);
|
|
53
55
|
if (whereStr)
|
|
54
56
|
parts.push(`WHERE ${whereStr}`);
|
|
55
57
|
// GROUP BY
|
|
@@ -57,7 +59,7 @@ function buildSelect(m, where, having, limit, offset, driver) {
|
|
|
57
59
|
parts.push(`GROUP BY ${m.groupBy.map(quoteIdent).join(", ")}`);
|
|
58
60
|
}
|
|
59
61
|
// HAVING
|
|
60
|
-
const havingStr = buildClauses(having, "AND",
|
|
62
|
+
const havingStr = buildClauses(having, "AND", dialect, addParam);
|
|
61
63
|
if (havingStr)
|
|
62
64
|
parts.push(`HAVING ${havingStr}`);
|
|
63
65
|
// ORDER BY
|
|
@@ -72,7 +74,8 @@ function buildSelect(m, where, having, limit, offset, driver) {
|
|
|
72
74
|
parts.push(`OFFSET ${addParam(offset)}`);
|
|
73
75
|
return { sql: parts.join("\n"), params };
|
|
74
76
|
}
|
|
75
|
-
function buildColumns(columns) {
|
|
77
|
+
function buildColumns(columns, dialect) {
|
|
78
|
+
const quoteIdent = (name) => dialect.quoteIdentifier(name);
|
|
76
79
|
return columns
|
|
77
80
|
.map((c) => {
|
|
78
81
|
if (typeof c === "string")
|
|
@@ -83,12 +86,12 @@ function buildColumns(columns) {
|
|
|
83
86
|
})
|
|
84
87
|
.join(", ");
|
|
85
88
|
}
|
|
86
|
-
function buildClauses(clauses, join,
|
|
89
|
+
function buildClauses(clauses, join, dialect, addParam) {
|
|
87
90
|
const parts = [];
|
|
88
91
|
for (const clause of clauses) {
|
|
89
92
|
if (clause.when === false)
|
|
90
93
|
continue;
|
|
91
|
-
const built = buildClause(clause,
|
|
94
|
+
const built = buildClause(clause, dialect, addParam);
|
|
92
95
|
if (built !== null)
|
|
93
96
|
parts.push(built);
|
|
94
97
|
}
|
|
@@ -98,43 +101,38 @@ function buildClauses(clauses, join, driver, addParam) {
|
|
|
98
101
|
return parts[0];
|
|
99
102
|
return parts.join(` ${join} `);
|
|
100
103
|
}
|
|
101
|
-
function buildClause(node,
|
|
104
|
+
function buildClause(node, dialect, addParam) {
|
|
102
105
|
if ("not" in node) {
|
|
103
|
-
const inner = buildClause(node.not,
|
|
106
|
+
const inner = buildClause(node.not, dialect, addParam);
|
|
104
107
|
return inner ? `NOT (${inner})` : null;
|
|
105
108
|
}
|
|
106
109
|
if ("or" in node) {
|
|
107
|
-
const inner = buildClauses(node.or, "OR",
|
|
110
|
+
const inner = buildClauses(node.or, "OR", dialect, addParam);
|
|
108
111
|
return inner ? `(${inner})` : null;
|
|
109
112
|
}
|
|
110
113
|
if ("and" in node) {
|
|
111
|
-
const inner = buildClauses(node.and, "AND",
|
|
114
|
+
const inner = buildClauses(node.and, "AND", dialect, addParam);
|
|
112
115
|
return inner ? `(${inner})` : null;
|
|
113
116
|
}
|
|
114
117
|
if ("sql" in node) {
|
|
115
118
|
return renumberFragment(node.sql, node.bindings ?? [], addParam);
|
|
116
119
|
}
|
|
117
120
|
if ("column" in node) {
|
|
118
|
-
return buildCondition(node,
|
|
121
|
+
return buildCondition(node, dialect, addParam);
|
|
119
122
|
}
|
|
120
123
|
return null;
|
|
121
124
|
}
|
|
122
|
-
function buildCondition(c,
|
|
123
|
-
const col =
|
|
125
|
+
function buildCondition(c, dialect, addParam) {
|
|
126
|
+
const col = dialect.quoteIdentifier(c.column);
|
|
124
127
|
switch (c.op) {
|
|
125
128
|
case "is_null":
|
|
126
129
|
return `${col} IS NULL`;
|
|
127
130
|
case "is_not_null":
|
|
128
131
|
return `${col} IS NOT NULL`;
|
|
129
|
-
case "in":
|
|
130
|
-
|
|
131
|
-
return `${col} = ANY(${addParam(c.value)})`;
|
|
132
|
-
}
|
|
133
|
-
const placeholders = c.value.map((v) => addParam(v)).join(", ");
|
|
134
|
-
return `${col} IN (${placeholders})`;
|
|
135
|
-
}
|
|
132
|
+
case "in":
|
|
133
|
+
return dialect.renderIn(col, c.value, addParam);
|
|
136
134
|
default: {
|
|
137
|
-
const rhs = c.ref !== undefined ?
|
|
135
|
+
const rhs = c.ref !== undefined ? dialect.quoteIdentifier(c.ref) : addParam(c.value);
|
|
138
136
|
return `${col} ${opToSql(c.op)} ${rhs}`;
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -142,9 +140,6 @@ function buildCondition(c, driver, addParam) {
|
|
|
142
140
|
function renumberFragment(sql, bindings, addParam) {
|
|
143
141
|
return sql.replace(/\$(\d+)/g, (_, idx) => addParam(bindings[Number(idx) - 1]));
|
|
144
142
|
}
|
|
145
|
-
function quoteIdent(name) {
|
|
146
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
147
|
-
}
|
|
148
143
|
function opToSql(op) {
|
|
149
144
|
const map = {
|
|
150
145
|
eq: "=",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Invocable, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
interface SqlTransactionManifest {
|
|
4
4
|
metadata: {
|
|
5
5
|
name: string;
|
|
6
6
|
module: string;
|
|
7
7
|
};
|
|
8
|
-
connection:
|
|
8
|
+
connection: SqlConnection;
|
|
9
9
|
steps: Invocable;
|
|
10
10
|
inputs?: Record<string, unknown>;
|
|
11
11
|
}
|
|
@@ -13,7 +13,7 @@ export declare class SqlTransactionResource implements ResourceInstance {
|
|
|
13
13
|
private readonly manifest;
|
|
14
14
|
private readonly ctx;
|
|
15
15
|
constructor(manifest: SqlTransactionManifest, ctx: ResourceContext);
|
|
16
|
-
getConnection():
|
|
16
|
+
getConnection(): SqlConnection;
|
|
17
17
|
assertActive(): void;
|
|
18
18
|
invoke(input: unknown): Promise<unknown>;
|
|
19
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -30,11 +30,6 @@
|
|
|
30
30
|
"bun": "./src/index.ts",
|
|
31
31
|
"import": "./dist/index.js"
|
|
32
32
|
},
|
|
33
|
-
"./connection": {
|
|
34
|
-
"types": "./dist/sql-connection-controller.d.ts",
|
|
35
|
-
"bun": "./src/sql-connection-controller.ts",
|
|
36
|
-
"import": "./dist/sql-connection-controller.js"
|
|
37
|
-
},
|
|
38
33
|
"./sql-query": {
|
|
39
34
|
"bun": "./src/sql-query-controller.ts",
|
|
40
35
|
"import": "./dist/sql-query-controller.js"
|
|
@@ -70,7 +65,7 @@
|
|
|
70
65
|
"devDependencies": {
|
|
71
66
|
"@types/node": "^20.0.0",
|
|
72
67
|
"typescript": "^5.0.0",
|
|
73
|
-
"@telorun/sdk": "0.
|
|
68
|
+
"@telorun/sdk": "0.58.0"
|
|
74
69
|
},
|
|
75
70
|
"peerDependencies": {
|
|
76
71
|
"@telorun/sdk": "*"
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
export { SqlConnectionBase } from "./sql-connection-base.js";
|
|
1
2
|
export {
|
|
2
|
-
|
|
3
|
-
createSqlConnection,
|
|
4
|
-
type SqlDriver,
|
|
3
|
+
quoteAnsiIdentifier,
|
|
5
4
|
type PlaceholderStyle,
|
|
6
|
-
|
|
5
|
+
type SqlConnection,
|
|
6
|
+
type SqlDialect,
|
|
7
|
+
} from "./sql-connection.js";
|
|
7
8
|
export { isSqlConnection, resolveSqlConnection } from "./sql-connection-ref.js";
|
|
8
|
-
export type { SqliteDb, SqliteStatement } from "./sqlite-driver-interface.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
5
5
|
import { runSql } from "./sql-run.js";
|
|
@@ -7,7 +7,7 @@ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
|
7
7
|
|
|
8
8
|
interface SqlCommandManifest {
|
|
9
9
|
metadata: { name: string; module: string };
|
|
10
|
-
connection?:
|
|
10
|
+
connection?: SqlConnection;
|
|
11
11
|
transaction?: SqlTransactionResource;
|
|
12
12
|
inputs: {
|
|
13
13
|
sql: string;
|
|
@@ -1,37 +1,32 @@
|
|
|
1
|
-
import type { ResourceInstance } from "@telorun/sdk";
|
|
2
1
|
import { randomUUID } from "crypto";
|
|
3
|
-
import { CompiledQuery, Kysely, type QueryResult, type Transaction } from "kysely";
|
|
2
|
+
import { CompiledQuery, type Kysely, type QueryResult, type Transaction } from "kysely";
|
|
3
|
+
import type { SqlConnection, SqlDialect } from "./sql-connection.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
|
-
import type { SqliteDb } from "./sqlite-driver-interface.js";
|
|
6
5
|
import { currentTxId, deleteTx, getTx, setTx, txStorage } from "./transaction-store.js";
|
|
7
6
|
|
|
8
|
-
export type SqlDriver = "postgres" | "sqlite";
|
|
9
|
-
|
|
10
|
-
/** Native bind-placeholder syntax per driver: SQLite binds anonymous `?`,
|
|
11
|
-
* PostgreSQL binds numbered `$1`, `$2`, … */
|
|
12
|
-
export type PlaceholderStyle = "qmark" | "numbered";
|
|
13
|
-
|
|
14
7
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* {@link
|
|
19
|
-
*
|
|
8
|
+
* The dialect-neutral half of a connection: statement execution, transaction
|
|
9
|
+
* scoping, template binding and row-count normalization over a kysely instance.
|
|
10
|
+
*
|
|
11
|
+
* Backends extend it, supply their {@link SqlDialect}, and override only what is
|
|
12
|
+
* genuinely theirs — `teardown` for resources kysely does not own, `executeScript`
|
|
13
|
+
* where the driver has a native multi-statement path.
|
|
20
14
|
*/
|
|
21
|
-
export class
|
|
22
|
-
|
|
23
|
-
private readonly sqlite?: SqliteDb;
|
|
15
|
+
export abstract class SqlConnectionBase implements SqlConnection {
|
|
16
|
+
protected readonly db: Kysely<any>;
|
|
24
17
|
|
|
25
18
|
constructor(
|
|
26
|
-
readonly driver: SqlDriver,
|
|
27
19
|
db: Kysely<any>,
|
|
28
|
-
|
|
20
|
+
readonly dialect: SqlDialect,
|
|
29
21
|
) {
|
|
30
22
|
this.db = db;
|
|
31
|
-
this.sqlite = sqlite;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
|
-
|
|
25
|
+
get kysely(): Kysely<any> {
|
|
26
|
+
return this.db;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async init(): Promise<void> {
|
|
35
30
|
await this.db.connection().execute(async () => {
|
|
36
31
|
// just checking
|
|
37
32
|
});
|
|
@@ -63,13 +58,6 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
63
58
|
return executor.executeQuery<T>(CompiledQuery.raw(sql, params));
|
|
64
59
|
}
|
|
65
60
|
|
|
66
|
-
get placeholderStyle(): PlaceholderStyle {
|
|
67
|
-
return this.driver === "postgres" ? "numbered" : "qmark";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Assemble SQL from literal fragments by interleaving driver-native
|
|
71
|
-
* placeholders, then bind `values` positionally. `fragments.length` must
|
|
72
|
-
* equal `values.length + 1`. */
|
|
73
61
|
async executeTemplate<T>(
|
|
74
62
|
fragments: string[],
|
|
75
63
|
values: unknown[],
|
|
@@ -82,16 +70,9 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
82
70
|
return this.execute<T>(sql, values, transaction);
|
|
83
71
|
}
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
73
|
+
/** Hand the whole script to the driver as one statement. Backends whose driver
|
|
74
|
+
* needs a dedicated multi-statement entry point override this. */
|
|
89
75
|
async executeScript(sql: string): Promise<void> {
|
|
90
|
-
if (this.driver === "sqlite") {
|
|
91
|
-
this.sqlite?.exec(sql);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
76
|
await this.execute(sql);
|
|
96
77
|
}
|
|
97
78
|
|
|
@@ -103,14 +84,14 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
103
84
|
return result.rows.length;
|
|
104
85
|
}
|
|
105
86
|
|
|
106
|
-
get kysely(): Kysely<any> {
|
|
107
|
-
return this.db;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
87
|
snapshot(): Record<string, unknown> {
|
|
111
88
|
return {};
|
|
112
89
|
}
|
|
113
90
|
|
|
91
|
+
private placeholder(index: number): string {
|
|
92
|
+
return this.dialect.placeholderStyle === "numbered" ? `$${index}` : "?";
|
|
93
|
+
}
|
|
94
|
+
|
|
114
95
|
private resolveExecutor(transaction?: SqlTransactionResource): Kysely<any> {
|
|
115
96
|
if (transaction) {
|
|
116
97
|
transaction.assertActive();
|
|
@@ -127,17 +108,3 @@ export class SqlConnectionResource implements ResourceInstance {
|
|
|
127
108
|
return this.db;
|
|
128
109
|
}
|
|
129
110
|
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Build a connection from a driver-constructed kysely instance. Driver backends
|
|
133
|
-
* (`sql-postgres`, `sql-sqlite`) own dialect construction and call this; the
|
|
134
|
-
* `sqlite` handle is required only for SQLite (its `executeScript` runs through
|
|
135
|
-
* the native handle).
|
|
136
|
-
*/
|
|
137
|
-
export function createSqlConnection(
|
|
138
|
-
driver: SqlDriver,
|
|
139
|
-
db: Kysely<any>,
|
|
140
|
-
sqlite?: SqliteDb,
|
|
141
|
-
): SqlConnectionResource {
|
|
142
|
-
return new SqlConnectionResource(driver, db, sqlite);
|
|
143
|
-
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { KindRef, ResourceContext } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
|
|
4
4
|
/** True when a value already exposes the connection contract (Phase-5 injected). */
|
|
5
|
-
export function isSqlConnection(value: unknown): value is
|
|
6
|
-
return typeof (value as
|
|
5
|
+
export function isSqlConnection(value: unknown): value is SqlConnection {
|
|
6
|
+
return typeof (value as SqlConnection | undefined)?.execute === "function";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -13,10 +13,10 @@ export function isSqlConnection(value: unknown): value is SqlConnectionResource
|
|
|
13
13
|
* slot, so the failure points at a concrete manifest location.
|
|
14
14
|
*/
|
|
15
15
|
export function resolveSqlConnection(
|
|
16
|
-
value:
|
|
16
|
+
value: SqlConnection | KindRef<SqlConnection> | undefined,
|
|
17
17
|
ctx: ResourceContext,
|
|
18
18
|
describe: () => string,
|
|
19
|
-
):
|
|
19
|
+
): SqlConnection | undefined {
|
|
20
20
|
if (!value) return undefined;
|
|
21
21
|
return ctx.resolveRef(value, isSqlConnection, describe, "std/sql#Connection");
|
|
22
22
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ResourceInstance } from "@telorun/sdk";
|
|
2
|
+
import type { Kysely, QueryResult } from "kysely";
|
|
3
|
+
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
|
+
|
|
5
|
+
/** Native bind-placeholder syntax: SQLite binds anonymous `?`, PostgreSQL binds
|
|
6
|
+
* numbered `$1`, `$2`, … */
|
|
7
|
+
export type PlaceholderStyle = "qmark" | "numbered";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders the SQL constructs that differ between database dialects. A backend
|
|
11
|
+
* supplies one; the `Sql.*` operations build statements through it and never
|
|
12
|
+
* branch on which database sits behind the connection.
|
|
13
|
+
*/
|
|
14
|
+
export interface SqlDialect {
|
|
15
|
+
readonly placeholderStyle: PlaceholderStyle;
|
|
16
|
+
|
|
17
|
+
/** Quote an identifier — table, column, alias. */
|
|
18
|
+
quoteIdentifier(name: string): string;
|
|
19
|
+
|
|
20
|
+
/** Render set membership. Dialects disagree: PostgreSQL binds the whole array
|
|
21
|
+
* to a single placeholder, SQLite binds one per element. `column` arrives
|
|
22
|
+
* already quoted. */
|
|
23
|
+
renderIn(
|
|
24
|
+
column: string,
|
|
25
|
+
values: unknown[],
|
|
26
|
+
addParam: (value: unknown) => string,
|
|
27
|
+
): string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** ANSI identifier quoting (`"name"`), for the dialects that follow the standard. */
|
|
31
|
+
export function quoteAnsiIdentifier(name: string): string {
|
|
32
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The contract every SQL backend satisfies and every `Sql.*` operation programs
|
|
37
|
+
* against. Backends (`sql-postgres`, `sql-sqlite`) own their own implementation
|
|
38
|
+
* — usually by extending {@link SqlConnectionBase} — so nothing in this module
|
|
39
|
+
* knows which databases exist.
|
|
40
|
+
*/
|
|
41
|
+
export interface SqlConnection extends ResourceInstance {
|
|
42
|
+
readonly dialect: SqlDialect;
|
|
43
|
+
|
|
44
|
+
/** The underlying kysely instance, when the backend is built on one —
|
|
45
|
+
* {@link SqlConnectionBase} always provides it. Optional because the contract
|
|
46
|
+
* must stay implementable by a driver kysely does not support; a consumer
|
|
47
|
+
* that needs it (`Sql.Migrations`) checks and fails with a clear message. */
|
|
48
|
+
readonly kysely?: Kysely<any>;
|
|
49
|
+
|
|
50
|
+
execute<T>(
|
|
51
|
+
sql: string,
|
|
52
|
+
params?: unknown[],
|
|
53
|
+
transaction?: SqlTransactionResource,
|
|
54
|
+
): Promise<QueryResult<T>>;
|
|
55
|
+
|
|
56
|
+
/** Assemble SQL from literal fragments by interleaving dialect-native
|
|
57
|
+
* placeholders, then bind `values` positionally. */
|
|
58
|
+
executeTemplate<T>(
|
|
59
|
+
fragments: string[],
|
|
60
|
+
values: unknown[],
|
|
61
|
+
transaction?: SqlTransactionResource,
|
|
62
|
+
): Promise<QueryResult<T>>;
|
|
63
|
+
|
|
64
|
+
/** Run a multi-statement script. */
|
|
65
|
+
executeScript(sql: string): Promise<void>;
|
|
66
|
+
|
|
67
|
+
transaction<T>(cb: () => Promise<T>): Promise<T>;
|
|
68
|
+
|
|
69
|
+
/** Rows affected by a write, normalized across drivers. */
|
|
70
|
+
toRowCount(result: QueryResult<unknown>): number;
|
|
71
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type Migration,
|
|
7
7
|
type MigrationProvider,
|
|
8
8
|
} from "kysely";
|
|
9
|
-
import type {
|
|
9
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
10
10
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
11
11
|
|
|
12
12
|
// A migration entry is one statement or an ordered list of statements; both
|
|
@@ -18,7 +18,7 @@ interface MigrationEntry {
|
|
|
18
18
|
|
|
19
19
|
interface SqlMigrationsManifest {
|
|
20
20
|
metadata: { name: string; module: string };
|
|
21
|
-
connection:
|
|
21
|
+
connection: SqlConnection;
|
|
22
22
|
migrations?: Record<string, MigrationEntry>;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -82,6 +82,14 @@ class SqlMigrationsResource implements ResourceInstance {
|
|
|
82
82
|
migrations[name] = statements;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
if (!conn.kysely) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Sql.Migrations '${this.manifest.metadata.name}': the referenced connection is not ` +
|
|
88
|
+
`built on kysely, which this kind's migration runner requires. Use a backend that ` +
|
|
89
|
+
`extends SqlConnectionBase, or run the statements through Sql.Command.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
const migrator = new Migrator({
|
|
86
94
|
db: conn.kysely,
|
|
87
95
|
provider: new TeloMigrationProvider(migrations),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import { runSql } from "./sql-run.js";
|
|
5
5
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
6
6
|
|
|
7
7
|
interface SqlQueryManifest {
|
|
8
8
|
metadata: { name: string; module: string };
|
|
9
|
-
connection?:
|
|
9
|
+
connection?: SqlConnection;
|
|
10
10
|
transaction?: SqlTransactionResource;
|
|
11
11
|
inputs: {
|
|
12
12
|
sql: string;
|
|
@@ -40,11 +40,11 @@ class SqlQueryResource implements ResourceInstance {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function resolveConnection(
|
|
43
|
-
connection:
|
|
43
|
+
connection: SqlConnection | undefined,
|
|
44
44
|
transaction: SqlTransactionResource | undefined,
|
|
45
45
|
ctx: ResourceContext,
|
|
46
46
|
describe: () => string,
|
|
47
|
-
):
|
|
47
|
+
): SqlConnection {
|
|
48
48
|
return (
|
|
49
49
|
resolveSqlConnection(connection, ctx, describe) ??
|
|
50
50
|
transaction?.getConnection() ??
|
package/src/sql-run.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InvokeError, isParameterizedSql, type ResourceContext } from "@telorun/sdk";
|
|
2
2
|
import type { QueryResult } from "kysely";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
4
4
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
5
5
|
|
|
6
6
|
/** Execute the `sql` input of a Query/Exec resource against `connection`.
|
|
@@ -15,7 +15,7 @@ import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
|
15
15
|
* Mixing a `!sql` template with `bindings` is rejected. A plain string `sql`
|
|
16
16
|
* with neither is executed verbatim. */
|
|
17
17
|
export async function runSql(
|
|
18
|
-
connection:
|
|
18
|
+
connection: SqlConnection,
|
|
19
19
|
transaction: SqlTransactionResource | undefined,
|
|
20
20
|
input: unknown,
|
|
21
21
|
ctx: ResourceContext,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection, SqlDialect } from "./sql-connection.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import type { SqlResult } from "./sql-query-controller.js";
|
|
5
5
|
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
@@ -59,7 +59,7 @@ interface OrderByItem {
|
|
|
59
59
|
|
|
60
60
|
interface SelectManifest {
|
|
61
61
|
metadata: { name: string; module: string };
|
|
62
|
-
connection?:
|
|
62
|
+
connection?: SqlConnection;
|
|
63
63
|
transaction?: SqlTransactionResource;
|
|
64
64
|
from: string;
|
|
65
65
|
columns?: ColumnDef[];
|
|
@@ -103,7 +103,7 @@ class SqlSelectionResource implements ResourceInstance {
|
|
|
103
103
|
throw new Error("Sql.Selection: either 'connection' or 'transaction' must be set");
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.
|
|
106
|
+
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.dialect);
|
|
107
107
|
const result = await connection.execute<Record<string, unknown>>(sql, params, m.transaction);
|
|
108
108
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
109
109
|
}
|
|
@@ -111,21 +111,20 @@ class SqlSelectionResource implements ResourceInstance {
|
|
|
111
111
|
|
|
112
112
|
// ── SQL building ──────────────────────────────────────────────────────────────
|
|
113
113
|
|
|
114
|
-
type Driver = "postgres" | "sqlite";
|
|
115
|
-
|
|
116
114
|
function buildSelect(
|
|
117
115
|
m: SelectManifest,
|
|
118
116
|
where: WhereNode[],
|
|
119
117
|
having: WhereNode[],
|
|
120
118
|
limit: unknown,
|
|
121
119
|
offset: unknown,
|
|
122
|
-
|
|
120
|
+
dialect: SqlDialect,
|
|
123
121
|
): { sql: string; params: unknown[] } {
|
|
124
122
|
const params: unknown[] = [];
|
|
125
123
|
const addParam = (value: unknown): string => {
|
|
126
124
|
params.push(value);
|
|
127
|
-
return `$${params.length}
|
|
125
|
+
return dialect.placeholderStyle === "numbered" ? `$${params.length}` : "?";
|
|
128
126
|
};
|
|
127
|
+
const quoteIdent = (name: string): string => dialect.quoteIdentifier(name);
|
|
129
128
|
|
|
130
129
|
const parts: string[] = [];
|
|
131
130
|
|
|
@@ -136,14 +135,14 @@ function buildSelect(
|
|
|
136
135
|
} else if (m.distinctOn && m.distinctOn.length > 0) {
|
|
137
136
|
selectClause += ` DISTINCT ON (${m.distinctOn.map(quoteIdent).join(", ")})`;
|
|
138
137
|
}
|
|
139
|
-
const colList = m.columns && m.columns.length > 0 ? buildColumns(m.columns) : "*";
|
|
138
|
+
const colList = m.columns && m.columns.length > 0 ? buildColumns(m.columns, dialect) : "*";
|
|
140
139
|
parts.push(`${selectClause} ${colList}`);
|
|
141
140
|
|
|
142
141
|
// FROM
|
|
143
142
|
parts.push(`FROM ${quoteIdent(m.from)}`);
|
|
144
143
|
|
|
145
144
|
// WHERE
|
|
146
|
-
const whereStr = buildClauses(where, "AND",
|
|
145
|
+
const whereStr = buildClauses(where, "AND", dialect, addParam);
|
|
147
146
|
if (whereStr) parts.push(`WHERE ${whereStr}`);
|
|
148
147
|
|
|
149
148
|
// GROUP BY
|
|
@@ -152,7 +151,7 @@ function buildSelect(
|
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
// HAVING
|
|
155
|
-
const havingStr = buildClauses(having, "AND",
|
|
154
|
+
const havingStr = buildClauses(having, "AND", dialect, addParam);
|
|
156
155
|
if (havingStr) parts.push(`HAVING ${havingStr}`);
|
|
157
156
|
|
|
158
157
|
// ORDER BY
|
|
@@ -170,7 +169,8 @@ function buildSelect(
|
|
|
170
169
|
return { sql: parts.join("\n"), params };
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
function buildColumns(columns: ColumnDef[]): string {
|
|
172
|
+
function buildColumns(columns: ColumnDef[], dialect: SqlDialect): string {
|
|
173
|
+
const quoteIdent = (name: string): string => dialect.quoteIdentifier(name);
|
|
174
174
|
return columns
|
|
175
175
|
.map((c) => {
|
|
176
176
|
if (typeof c === "string") return quoteIdent(c);
|
|
@@ -183,13 +183,13 @@ function buildColumns(columns: ColumnDef[]): string {
|
|
|
183
183
|
function buildClauses(
|
|
184
184
|
clauses: WhereNode[],
|
|
185
185
|
join: "AND" | "OR",
|
|
186
|
-
|
|
186
|
+
dialect: SqlDialect,
|
|
187
187
|
addParam: (v: unknown) => string,
|
|
188
188
|
): string | null {
|
|
189
189
|
const parts: string[] = [];
|
|
190
190
|
for (const clause of clauses) {
|
|
191
191
|
if (clause.when === false) continue;
|
|
192
|
-
const built = buildClause(clause,
|
|
192
|
+
const built = buildClause(clause, dialect, addParam);
|
|
193
193
|
if (built !== null) parts.push(built);
|
|
194
194
|
}
|
|
195
195
|
if (parts.length === 0) return null;
|
|
@@ -199,46 +199,45 @@ function buildClauses(
|
|
|
199
199
|
|
|
200
200
|
function buildClause(
|
|
201
201
|
node: WhereNode,
|
|
202
|
-
|
|
202
|
+
dialect: SqlDialect,
|
|
203
203
|
addParam: (v: unknown) => string,
|
|
204
204
|
): string | null {
|
|
205
205
|
if ("not" in node) {
|
|
206
|
-
const inner = buildClause(node.not,
|
|
206
|
+
const inner = buildClause(node.not, dialect, addParam);
|
|
207
207
|
return inner ? `NOT (${inner})` : null;
|
|
208
208
|
}
|
|
209
209
|
if ("or" in node) {
|
|
210
|
-
const inner = buildClauses(node.or, "OR",
|
|
210
|
+
const inner = buildClauses(node.or, "OR", dialect, addParam);
|
|
211
211
|
return inner ? `(${inner})` : null;
|
|
212
212
|
}
|
|
213
213
|
if ("and" in node) {
|
|
214
|
-
const inner = buildClauses(node.and, "AND",
|
|
214
|
+
const inner = buildClauses(node.and, "AND", dialect, addParam);
|
|
215
215
|
return inner ? `(${inner})` : null;
|
|
216
216
|
}
|
|
217
217
|
if ("sql" in node) {
|
|
218
218
|
return renumberFragment(node.sql, node.bindings ?? [], addParam);
|
|
219
219
|
}
|
|
220
220
|
if ("column" in node) {
|
|
221
|
-
return buildCondition(node,
|
|
221
|
+
return buildCondition(node, dialect, addParam);
|
|
222
222
|
}
|
|
223
223
|
return null;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
function buildCondition(
|
|
227
|
-
|
|
226
|
+
function buildCondition(
|
|
227
|
+
c: Condition,
|
|
228
|
+
dialect: SqlDialect,
|
|
229
|
+
addParam: (v: unknown) => string,
|
|
230
|
+
): string {
|
|
231
|
+
const col = dialect.quoteIdentifier(c.column);
|
|
228
232
|
switch (c.op) {
|
|
229
233
|
case "is_null":
|
|
230
234
|
return `${col} IS NULL`;
|
|
231
235
|
case "is_not_null":
|
|
232
236
|
return `${col} IS NOT NULL`;
|
|
233
|
-
case "in":
|
|
234
|
-
|
|
235
|
-
return `${col} = ANY(${addParam(c.value)})`;
|
|
236
|
-
}
|
|
237
|
-
const placeholders = (c.value as unknown[]).map((v) => addParam(v)).join(", ");
|
|
238
|
-
return `${col} IN (${placeholders})`;
|
|
239
|
-
}
|
|
237
|
+
case "in":
|
|
238
|
+
return dialect.renderIn(col, c.value as unknown[], addParam);
|
|
240
239
|
default: {
|
|
241
|
-
const rhs = c.ref !== undefined ?
|
|
240
|
+
const rhs = c.ref !== undefined ? dialect.quoteIdentifier(c.ref) : addParam(c.value);
|
|
242
241
|
return `${col} ${opToSql(c.op)} ${rhs}`;
|
|
243
242
|
}
|
|
244
243
|
}
|
|
@@ -252,10 +251,6 @@ function renumberFragment(
|
|
|
252
251
|
return sql.replace(/\$(\d+)/g, (_, idx) => addParam(bindings[Number(idx) - 1]));
|
|
253
252
|
}
|
|
254
253
|
|
|
255
|
-
function quoteIdent(name: string): string {
|
|
256
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
254
|
function opToSql(op: Op): string {
|
|
260
255
|
const map: Record<string, string> = {
|
|
261
256
|
eq: "=",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Invocable, ResourceContext, ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
import { resolveSqlConnection } from "./sql-connection-ref.js";
|
|
4
4
|
import { currentTxId } from "./transaction-store.js";
|
|
5
5
|
|
|
6
6
|
interface SqlTransactionManifest {
|
|
7
7
|
metadata: { name: string; module: string };
|
|
8
|
-
connection:
|
|
8
|
+
connection: SqlConnection;
|
|
9
9
|
steps: Invocable;
|
|
10
10
|
inputs?: Record<string, unknown>;
|
|
11
11
|
}
|
|
@@ -16,7 +16,7 @@ export class SqlTransactionResource implements ResourceInstance {
|
|
|
16
16
|
private readonly ctx: ResourceContext,
|
|
17
17
|
) {}
|
|
18
18
|
|
|
19
|
-
getConnection():
|
|
19
|
+
getConnection(): SqlConnection {
|
|
20
20
|
return (
|
|
21
21
|
resolveSqlConnection(
|
|
22
22
|
this.manifest.connection,
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ResourceInstance } from "@telorun/sdk";
|
|
2
|
-
import { Kysely, type QueryResult } from "kysely";
|
|
3
|
-
import type { SqlTransactionResource } from "./sql-transaction-controller.js";
|
|
4
|
-
import type { SqliteDb } from "./sqlite-driver-interface.js";
|
|
5
|
-
export type SqlDriver = "postgres" | "sqlite";
|
|
6
|
-
/** Native bind-placeholder syntax per driver: SQLite binds anonymous `?`,
|
|
7
|
-
* PostgreSQL binds numbered `$1`, `$2`, … */
|
|
8
|
-
export type PlaceholderStyle = "qmark" | "numbered";
|
|
9
|
-
/**
|
|
10
|
-
* Driver-agnostic SQL connection. The kysely instance (and, for SQLite, the
|
|
11
|
-
* underlying database handle used by `executeScript`) is built by the driver
|
|
12
|
-
* backend (`sql-postgres`, `sql-sqlite`) and handed in via
|
|
13
|
-
* {@link createSqlConnection}. Everything here — execution, transactions,
|
|
14
|
-
* placeholder style, row-count normalization — is transport-neutral.
|
|
15
|
-
*/
|
|
16
|
-
export declare class SqlConnectionResource implements ResourceInstance {
|
|
17
|
-
readonly driver: SqlDriver;
|
|
18
|
-
private readonly db;
|
|
19
|
-
private readonly sqlite?;
|
|
20
|
-
constructor(driver: SqlDriver, db: Kysely<any>, sqlite?: SqliteDb);
|
|
21
|
-
init(): Promise<void>;
|
|
22
|
-
teardown(): Promise<void>;
|
|
23
|
-
transaction<T>(cb: () => Promise<T>): Promise<T>;
|
|
24
|
-
execute<T>(sql: string, params?: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
25
|
-
get placeholderStyle(): PlaceholderStyle;
|
|
26
|
-
/** Assemble SQL from literal fragments by interleaving driver-native
|
|
27
|
-
* placeholders, then bind `values` positionally. `fragments.length` must
|
|
28
|
-
* equal `values.length + 1`. */
|
|
29
|
-
executeTemplate<T>(fragments: string[], values: unknown[], transaction?: SqlTransactionResource): Promise<QueryResult<T>>;
|
|
30
|
-
private placeholder;
|
|
31
|
-
executeScript(sql: string): Promise<void>;
|
|
32
|
-
toRowCount(result: QueryResult<unknown>): number;
|
|
33
|
-
get kysely(): Kysely<any>;
|
|
34
|
-
snapshot(): Record<string, unknown>;
|
|
35
|
-
private resolveExecutor;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Build a connection from a driver-constructed kysely instance. Driver backends
|
|
39
|
-
* (`sql-postgres`, `sql-sqlite`) own dialect construction and call this; the
|
|
40
|
-
* `sqlite` handle is required only for SQLite (its `executeScript` runs through
|
|
41
|
-
* the native handle).
|
|
42
|
-
*/
|
|
43
|
-
export declare function createSqlConnection(driver: SqlDriver, db: Kysely<any>, sqlite?: SqliteDb): SqlConnectionResource;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export interface SqliteStatement {
|
|
2
|
-
readonly reader: boolean;
|
|
3
|
-
all(params: ReadonlyArray<unknown>): unknown[];
|
|
4
|
-
run(params: ReadonlyArray<unknown>): {
|
|
5
|
-
changes: number | bigint;
|
|
6
|
-
lastInsertRowid: number | bigint;
|
|
7
|
-
};
|
|
8
|
-
iterate(params: ReadonlyArray<unknown>): IterableIterator<unknown>;
|
|
9
|
-
}
|
|
10
|
-
export interface SqliteDb {
|
|
11
|
-
prepare(sql: string): SqliteStatement;
|
|
12
|
-
exec(sql: string): void;
|
|
13
|
-
close(): void;
|
|
14
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface SqliteStatement {
|
|
2
|
-
readonly reader: boolean;
|
|
3
|
-
all(params: ReadonlyArray<unknown>): unknown[];
|
|
4
|
-
run(params: ReadonlyArray<unknown>): {
|
|
5
|
-
changes: number | bigint;
|
|
6
|
-
lastInsertRowid: number | bigint;
|
|
7
|
-
};
|
|
8
|
-
iterate(params: ReadonlyArray<unknown>): IterableIterator<unknown>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface SqliteDb {
|
|
12
|
-
prepare(sql: string): SqliteStatement;
|
|
13
|
-
exec(sql: string): void;
|
|
14
|
-
close(): void;
|
|
15
|
-
}
|