@telorun/sql 0.8.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/README.md +2 -141
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -2
- package/dist/sql-command-controller.d.ts +2 -2
- package/dist/sql-command-controller.js +2 -1
- 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 +11 -8
- package/dist/sql-connection-ref.js +13 -22
- 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 +6 -1
- package/dist/sql-query-controller.d.ts +2 -2
- package/dist/sql-query-controller.js +5 -3
- package/dist/sql-run.d.ts +2 -2
- package/dist/sql-selection-controller.d.ts +2 -2
- package/dist/sql-selection-controller.js +24 -28
- package/dist/sql-transaction-controller.d.ts +3 -3
- package/dist/sql-transaction-controller.js +1 -2
- package/package.json +2 -7
- package/src/index.ts +6 -6
- package/src/sql-command-controller.ts +5 -3
- package/src/{sql-connection-controller.ts → sql-connection-base.ts} +22 -55
- package/src/sql-connection-ref.ts +16 -35
- package/src/sql-connection.ts +71 -0
- package/src/sql-migrations-controller.ts +15 -3
- package/src/sql-query-controller.ts +14 -6
- package/src/sql-run.ts +2 -2
- package/src/sql-selection-controller.ts +30 -33
- package/src/sql-transaction-controller.ts +8 -5
- 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
|
@@ -19,21 +19,24 @@ class SqlSelectionResource {
|
|
|
19
19
|
const having = ctx.expandValue(m.having ?? [], expandCtx);
|
|
20
20
|
const limit = m.limit != null ? ctx.expandValue(m.limit, expandCtx) : undefined;
|
|
21
21
|
const offset = m.offset != null ? ctx.expandValue(m.offset, expandCtx) : undefined;
|
|
22
|
-
const connection = resolveSqlConnection(m.connection, ctx)
|
|
22
|
+
const connection = resolveSqlConnection(m.connection, ctx, () => `Sql.Selection "${m.metadata.name}": 'connection'`) ??
|
|
23
|
+
m.transaction?.getConnection();
|
|
23
24
|
if (!connection) {
|
|
24
25
|
throw new Error("Sql.Selection: either 'connection' or 'transaction' must be set");
|
|
25
26
|
}
|
|
26
|
-
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.
|
|
27
|
+
const { sql, params } = buildSelect(m, where, having, limit, offset, connection.dialect);
|
|
27
28
|
const result = await connection.execute(sql, params, m.transaction);
|
|
28
29
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
// ── SQL building ──────────────────────────────────────────────────────────────
|
|
33
|
+
function buildSelect(m, where, having, limit, offset, dialect) {
|
|
32
34
|
const params = [];
|
|
33
35
|
const addParam = (value) => {
|
|
34
36
|
params.push(value);
|
|
35
|
-
return `$${params.length}
|
|
37
|
+
return dialect.placeholderStyle === "numbered" ? `$${params.length}` : "?";
|
|
36
38
|
};
|
|
39
|
+
const quoteIdent = (name) => dialect.quoteIdentifier(name);
|
|
37
40
|
const parts = [];
|
|
38
41
|
// SELECT [DISTINCT [ON (...)]]
|
|
39
42
|
let selectClause = "SELECT";
|
|
@@ -43,12 +46,12 @@ function buildSelect(m, where, having, limit, offset, driver) {
|
|
|
43
46
|
else if (m.distinctOn && m.distinctOn.length > 0) {
|
|
44
47
|
selectClause += ` DISTINCT ON (${m.distinctOn.map(quoteIdent).join(", ")})`;
|
|
45
48
|
}
|
|
46
|
-
const colList = m.columns && m.columns.length > 0 ? buildColumns(m.columns) : "*";
|
|
49
|
+
const colList = m.columns && m.columns.length > 0 ? buildColumns(m.columns, dialect) : "*";
|
|
47
50
|
parts.push(`${selectClause} ${colList}`);
|
|
48
51
|
// FROM
|
|
49
52
|
parts.push(`FROM ${quoteIdent(m.from)}`);
|
|
50
53
|
// WHERE
|
|
51
|
-
const whereStr = buildClauses(where, "AND",
|
|
54
|
+
const whereStr = buildClauses(where, "AND", dialect, addParam);
|
|
52
55
|
if (whereStr)
|
|
53
56
|
parts.push(`WHERE ${whereStr}`);
|
|
54
57
|
// GROUP BY
|
|
@@ -56,7 +59,7 @@ function buildSelect(m, where, having, limit, offset, driver) {
|
|
|
56
59
|
parts.push(`GROUP BY ${m.groupBy.map(quoteIdent).join(", ")}`);
|
|
57
60
|
}
|
|
58
61
|
// HAVING
|
|
59
|
-
const havingStr = buildClauses(having, "AND",
|
|
62
|
+
const havingStr = buildClauses(having, "AND", dialect, addParam);
|
|
60
63
|
if (havingStr)
|
|
61
64
|
parts.push(`HAVING ${havingStr}`);
|
|
62
65
|
// ORDER BY
|
|
@@ -71,7 +74,8 @@ function buildSelect(m, where, having, limit, offset, driver) {
|
|
|
71
74
|
parts.push(`OFFSET ${addParam(offset)}`);
|
|
72
75
|
return { sql: parts.join("\n"), params };
|
|
73
76
|
}
|
|
74
|
-
function buildColumns(columns) {
|
|
77
|
+
function buildColumns(columns, dialect) {
|
|
78
|
+
const quoteIdent = (name) => dialect.quoteIdentifier(name);
|
|
75
79
|
return columns
|
|
76
80
|
.map((c) => {
|
|
77
81
|
if (typeof c === "string")
|
|
@@ -82,12 +86,12 @@ function buildColumns(columns) {
|
|
|
82
86
|
})
|
|
83
87
|
.join(", ");
|
|
84
88
|
}
|
|
85
|
-
function buildClauses(clauses, join,
|
|
89
|
+
function buildClauses(clauses, join, dialect, addParam) {
|
|
86
90
|
const parts = [];
|
|
87
91
|
for (const clause of clauses) {
|
|
88
92
|
if (clause.when === false)
|
|
89
93
|
continue;
|
|
90
|
-
const built = buildClause(clause,
|
|
94
|
+
const built = buildClause(clause, dialect, addParam);
|
|
91
95
|
if (built !== null)
|
|
92
96
|
parts.push(built);
|
|
93
97
|
}
|
|
@@ -97,43 +101,38 @@ function buildClauses(clauses, join, driver, addParam) {
|
|
|
97
101
|
return parts[0];
|
|
98
102
|
return parts.join(` ${join} `);
|
|
99
103
|
}
|
|
100
|
-
function buildClause(node,
|
|
104
|
+
function buildClause(node, dialect, addParam) {
|
|
101
105
|
if ("not" in node) {
|
|
102
|
-
const inner = buildClause(node.not,
|
|
106
|
+
const inner = buildClause(node.not, dialect, addParam);
|
|
103
107
|
return inner ? `NOT (${inner})` : null;
|
|
104
108
|
}
|
|
105
109
|
if ("or" in node) {
|
|
106
|
-
const inner = buildClauses(node.or, "OR",
|
|
110
|
+
const inner = buildClauses(node.or, "OR", dialect, addParam);
|
|
107
111
|
return inner ? `(${inner})` : null;
|
|
108
112
|
}
|
|
109
113
|
if ("and" in node) {
|
|
110
|
-
const inner = buildClauses(node.and, "AND",
|
|
114
|
+
const inner = buildClauses(node.and, "AND", dialect, addParam);
|
|
111
115
|
return inner ? `(${inner})` : null;
|
|
112
116
|
}
|
|
113
117
|
if ("sql" in node) {
|
|
114
118
|
return renumberFragment(node.sql, node.bindings ?? [], addParam);
|
|
115
119
|
}
|
|
116
120
|
if ("column" in node) {
|
|
117
|
-
return buildCondition(node,
|
|
121
|
+
return buildCondition(node, dialect, addParam);
|
|
118
122
|
}
|
|
119
123
|
return null;
|
|
120
124
|
}
|
|
121
|
-
function buildCondition(c,
|
|
122
|
-
const col =
|
|
125
|
+
function buildCondition(c, dialect, addParam) {
|
|
126
|
+
const col = dialect.quoteIdentifier(c.column);
|
|
123
127
|
switch (c.op) {
|
|
124
128
|
case "is_null":
|
|
125
129
|
return `${col} IS NULL`;
|
|
126
130
|
case "is_not_null":
|
|
127
131
|
return `${col} IS NOT NULL`;
|
|
128
|
-
case "in":
|
|
129
|
-
|
|
130
|
-
return `${col} = ANY(${addParam(c.value)})`;
|
|
131
|
-
}
|
|
132
|
-
const placeholders = c.value.map((v) => addParam(v)).join(", ");
|
|
133
|
-
return `${col} IN (${placeholders})`;
|
|
134
|
-
}
|
|
132
|
+
case "in":
|
|
133
|
+
return dialect.renderIn(col, c.value, addParam);
|
|
135
134
|
default: {
|
|
136
|
-
const rhs = c.ref !== undefined ?
|
|
135
|
+
const rhs = c.ref !== undefined ? dialect.quoteIdentifier(c.ref) : addParam(c.value);
|
|
137
136
|
return `${col} ${opToSql(c.op)} ${rhs}`;
|
|
138
137
|
}
|
|
139
138
|
}
|
|
@@ -141,9 +140,6 @@ function buildCondition(c, driver, addParam) {
|
|
|
141
140
|
function renumberFragment(sql, bindings, addParam) {
|
|
142
141
|
return sql.replace(/\$(\d+)/g, (_, idx) => addParam(bindings[Number(idx) - 1]));
|
|
143
142
|
}
|
|
144
|
-
function quoteIdent(name) {
|
|
145
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
146
|
-
}
|
|
147
143
|
function opToSql(op) {
|
|
148
144
|
const map = {
|
|
149
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
|
}
|
|
@@ -8,8 +8,7 @@ export class SqlTransactionResource {
|
|
|
8
8
|
this.ctx = ctx;
|
|
9
9
|
}
|
|
10
10
|
getConnection() {
|
|
11
|
-
return (resolveSqlConnection(this.manifest.connection, this.ctx) ??
|
|
12
|
-
failMissingConnection(this.manifest.metadata.name));
|
|
11
|
+
return (resolveSqlConnection(this.manifest.connection, this.ctx, () => `Sql.Transaction "${this.manifest.metadata.name}": 'connection'`) ?? failMissingConnection(this.manifest.metadata.name));
|
|
13
12
|
}
|
|
14
13
|
assertActive() {
|
|
15
14
|
if (!currentTxId()) {
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
type SqlConnection,
|
|
6
|
+
type SqlDialect,
|
|
7
|
+
} from "./sql-connection.js";
|
|
8
|
+
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 { 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;
|
|
@@ -25,7 +25,9 @@ class SqlCommandResource implements ResourceInstance {
|
|
|
25
25
|
const m = this.manifest;
|
|
26
26
|
const ctx = this.ctx;
|
|
27
27
|
|
|
28
|
-
const connection =
|
|
28
|
+
const connection =
|
|
29
|
+
resolveSqlConnection(m.connection, ctx, () => `Sql.Command "${m.metadata.name}": 'connection'`) ??
|
|
30
|
+
m.transaction?.getConnection();
|
|
29
31
|
if (!connection) {
|
|
30
32
|
throw new Error("Sql: either 'connection' or 'transaction' must be set");
|
|
31
33
|
}
|
|
@@ -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,41 +1,22 @@
|
|
|
1
|
-
import type { ResourceContext } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
1
|
+
import type { KindRef, ResourceContext } from "@telorun/sdk";
|
|
2
|
+
import type { SqlConnection } from "./sql-connection.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/** True when a value already exposes the connection contract (Phase-5 injected). */
|
|
5
|
+
export function isSqlConnection(value: unknown): value is SqlConnection {
|
|
6
|
+
return typeof (value as SqlConnection | undefined)?.execute === "function";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a `connection` `!ref` field to a live connection. The slot is optional
|
|
11
|
+
* — an unset one yields `undefined` so the caller can fall back to a `transaction`
|
|
12
|
+
* — but a slot that IS set must resolve. `describe` names the owning resource and
|
|
13
|
+
* slot, so the failure points at a concrete manifest location.
|
|
14
|
+
*/
|
|
9
15
|
export function resolveSqlConnection(
|
|
10
|
-
value:
|
|
16
|
+
value: SqlConnection | KindRef<SqlConnection> | undefined,
|
|
11
17
|
ctx: ResourceContext,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (typeof (value as SqlConnectionResource).execute === "function") {
|
|
18
|
-
return value as SqlConnectionResource;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const ref = value as ConnectionRef;
|
|
22
|
-
if (typeof ref.name !== "string") {
|
|
23
|
-
throw new Error("Sql: invalid connection reference");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Cross-module reference (`!ref Alias.Connection`): a connection resolved
|
|
27
|
-
// inside a nested library is not Phase-5-injected, so the controller receives
|
|
28
|
-
// the raw `{name, alias}` ref and must route through the import's exported
|
|
29
|
-
// scope rather than a bare local lookup.
|
|
30
|
-
if (ref.alias && ref.alias !== "Self") {
|
|
31
|
-
const instance = ctx.moduleContext.resolveImportedInstance(ref.alias, ref.name);
|
|
32
|
-
if (typeof (instance as SqlConnectionResource | undefined)?.execute !== "function") {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`Sql: connection reference '${ref.alias}.${ref.name}' did not resolve to an exported connection instance.`,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return instance as unknown as SqlConnectionResource;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return ctx.moduleContext.getInstance(ref.name) as SqlConnectionResource;
|
|
18
|
+
describe: () => string,
|
|
19
|
+
): SqlConnection | undefined {
|
|
20
|
+
if (!value) return undefined;
|
|
21
|
+
return ctx.resolveRef(value, isSqlConnection, describe, "std/sql#Connection");
|
|
41
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
|
|
|
@@ -56,7 +56,11 @@ class SqlMigrationsResource implements ResourceInstance {
|
|
|
56
56
|
|
|
57
57
|
async run(): Promise<void> {
|
|
58
58
|
const conn =
|
|
59
|
-
resolveSqlConnection(
|
|
59
|
+
resolveSqlConnection(
|
|
60
|
+
this.manifest.connection,
|
|
61
|
+
this.ctx,
|
|
62
|
+
() => `Sql.Migrations "${this.manifest.metadata.name}": 'connection'`,
|
|
63
|
+
) ?? failMissingConnection();
|
|
60
64
|
|
|
61
65
|
const migrations: Record<string, string[]> = {};
|
|
62
66
|
// Legacy: standalone `Sql.Migration` resources in the same module scope.
|
|
@@ -78,6 +82,14 @@ class SqlMigrationsResource implements ResourceInstance {
|
|
|
78
82
|
migrations[name] = statements;
|
|
79
83
|
}
|
|
80
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
|
+
|
|
81
93
|
const migrator = new Migrator({
|
|
82
94
|
db: conn.kysely,
|
|
83
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;
|
|
@@ -28,19 +28,27 @@ class SqlQueryResource implements ResourceInstance {
|
|
|
28
28
|
async invoke(input: unknown): Promise<SqlResult> {
|
|
29
29
|
const m = this.manifest;
|
|
30
30
|
const ctx = this.ctx;
|
|
31
|
-
const connection = resolveConnection(
|
|
31
|
+
const connection = resolveConnection(
|
|
32
|
+
m.connection,
|
|
33
|
+
m.transaction,
|
|
34
|
+
ctx,
|
|
35
|
+
() => `Sql.Query "${m.metadata.name}": 'connection'`,
|
|
36
|
+
);
|
|
32
37
|
const result = await runSql(connection, m.transaction, input, ctx);
|
|
33
38
|
return { rows: result.rows, rowCount: result.rows.length };
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function resolveConnection(
|
|
38
|
-
connection:
|
|
43
|
+
connection: SqlConnection | undefined,
|
|
39
44
|
transaction: SqlTransactionResource | undefined,
|
|
40
45
|
ctx: ResourceContext,
|
|
41
|
-
|
|
46
|
+
describe: () => string,
|
|
47
|
+
): SqlConnection {
|
|
42
48
|
return (
|
|
43
|
-
resolveSqlConnection(connection, ctx
|
|
49
|
+
resolveSqlConnection(connection, ctx, describe) ??
|
|
50
|
+
transaction?.getConnection() ??
|
|
51
|
+
failMissingConnection()
|
|
44
52
|
);
|
|
45
53
|
}
|
|
46
54
|
|
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,
|