@telorun/sql 0.9.0 → 0.11.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 CHANGED
@@ -1,3 +1,3 @@
1
- export { SqlConnectionResource, createSqlConnection, type SqlDriver, type PlaceholderStyle, } from "./sql-connection-controller.js";
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 { SqlConnectionResource, createSqlConnection, } from "./sql-connection-controller.js";
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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?: SqlConnectionResource;
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
- * Driver-agnostic SQL connection. The kysely instance (and, for SQLite, the
6
- * underlying database handle used by `executeScript`) is built by the driver
7
- * backend (`sql-postgres`, `sql-sqlite`) and handed in via
8
- * {@link createSqlConnection}. Everything here execution, transactions,
9
- * placeholder style, row-count normalization is transport-neutral.
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 SqlConnectionResource {
12
- driver;
12
+ export class SqlConnectionBase {
13
+ dialect;
13
14
  db;
14
- sqlite;
15
- constructor(driver, db, sqlite) {
16
- this.driver = driver;
15
+ constructor(db, dialect) {
16
+ this.dialect = dialect;
17
17
  this.db = db;
18
- this.sqlite = sqlite;
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
- placeholder(index) {
58
- return this.placeholderStyle === "numbered" ? `$${index}` : "?";
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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 SqlConnectionResource;
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: SqlConnectionResource | KindRef<SqlConnectionResource> | undefined, ctx: ResourceContext, describe: () => string): SqlConnectionResource | undefined;
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
+ }
@@ -0,0 +1,4 @@
1
+ /** ANSI identifier quoting (`"name"`), for the dialects that follow the standard. */
2
+ export function quoteAnsiIdentifier(name) {
3
+ return `"${name.replace(/"/g, '""')}"`;
4
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ResourceContext, ResourceInstance } from "@telorun/sdk";
2
- import type { SqlConnectionResource } from "./sql-connection-controller.js";
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: SqlConnectionResource;
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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?: SqlConnectionResource;
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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: SqlConnectionResource, transaction: SqlTransactionResource | undefined, input: unknown, ctx: ResourceContext): Promise<QueryResult<Record<string, unknown>>>;
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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?: SqlConnectionResource;
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.driver);
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
- function buildSelect(m, where, having, limit, offset, driver) {
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", driver, addParam);
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", driver, addParam);
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, driver, addParam) {
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, driver, addParam);
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, driver, addParam) {
104
+ function buildClause(node, dialect, addParam) {
102
105
  if ("not" in node) {
103
- const inner = buildClause(node.not, driver, addParam);
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", driver, addParam);
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", driver, addParam);
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, driver, addParam);
121
+ return buildCondition(node, dialect, addParam);
119
122
  }
120
123
  return null;
121
124
  }
122
- function buildCondition(c, driver, addParam) {
123
- const col = quoteIdent(c.column);
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
- if (driver === "postgres") {
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 ? quoteIdent(c.ref) : addParam(c.value);
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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: SqlConnectionResource;
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(): SqlConnectionResource;
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.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Telo SQL module - SQL database resource kinds for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -26,38 +26,10 @@
26
26
  "types": "./dist/index.d.ts",
27
27
  "exports": {
28
28
  ".": {
29
+ "source": "./src/index.ts",
29
30
  "types": "./dist/index.d.ts",
30
31
  "bun": "./src/index.ts",
31
32
  "import": "./dist/index.js"
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
- "./sql-query": {
39
- "bun": "./src/sql-query-controller.ts",
40
- "import": "./dist/sql-query-controller.js"
41
- },
42
- "./sql-selection": {
43
- "bun": "./src/sql-selection-controller.ts",
44
- "import": "./dist/sql-selection-controller.js"
45
- },
46
- "./sql-command": {
47
- "bun": "./src/sql-command-controller.ts",
48
- "import": "./dist/sql-command-controller.js"
49
- },
50
- "./sql-transaction": {
51
- "bun": "./src/sql-transaction-controller.ts",
52
- "import": "./dist/sql-transaction-controller.js"
53
- },
54
- "./sql-migration": {
55
- "bun": "./src/sql-migration-controller.ts",
56
- "import": "./dist/sql-migration-controller.js"
57
- },
58
- "./sql-migrations": {
59
- "bun": "./src/sql-migrations-controller.ts",
60
- "import": "./dist/sql-migrations-controller.js"
61
33
  }
62
34
  },
63
35
  "files": [
@@ -69,13 +41,14 @@
69
41
  },
70
42
  "devDependencies": {
71
43
  "@types/node": "^20.0.0",
44
+ "esbuild": "^0.25.12",
72
45
  "typescript": "^5.0.0",
73
- "@telorun/sdk": "0.54.0"
46
+ "@telorun/sdk": "0.63.0"
74
47
  },
75
48
  "peerDependencies": {
76
49
  "@telorun/sdk": "*"
77
50
  },
78
51
  "scripts": {
79
- "build": "tsc -p tsconfig.lib.json"
52
+ "build": "tsc -p tsconfig.lib.json && esbuild src/sql-command-controller.ts src/sql-migration-controller.ts src/sql-migrations-controller.ts src/sql-query-controller.ts src/sql-selection-controller.ts src/sql-transaction-controller.ts --bundle --format=esm --platform=node --target=node20 --external:@telorun/sdk --conditions=source --outdir=. --banner:js='import { createRequire as __teloCreateRequire } from \"node:module\";const require = __teloCreateRequire(import.meta.url);' --out-extension:.js=.mjs"
80
53
  }
81
54
  }
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
+ export { SqlConnectionBase } from "./sql-connection-base.js";
1
2
  export {
2
- SqlConnectionResource,
3
- createSqlConnection,
4
- type SqlDriver,
3
+ quoteAnsiIdentifier,
5
4
  type PlaceholderStyle,
6
- } from "./sql-connection-controller.js";
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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?: SqlConnectionResource;
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
- * Driver-agnostic SQL connection. The kysely instance (and, for SQLite, the
16
- * underlying database handle used by `executeScript`) is built by the driver
17
- * backend (`sql-postgres`, `sql-sqlite`) and handed in via
18
- * {@link createSqlConnection}. Everything here execution, transactions,
19
- * placeholder style, row-count normalization is transport-neutral.
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 SqlConnectionResource implements ResourceInstance {
22
- private readonly db: Kysely<any>;
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
- sqlite?: SqliteDb,
20
+ readonly dialect: SqlDialect,
29
21
  ) {
30
22
  this.db = db;
31
- this.sqlite = sqlite;
32
23
  }
33
24
 
34
- async init() {
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
- private placeholder(index: number): string {
86
- return this.placeholderStyle === "numbered" ? `$${index}` : "?";
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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 SqlConnectionResource {
6
- return typeof (value as SqlConnectionResource | undefined)?.execute === "function";
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: SqlConnectionResource | KindRef<SqlConnectionResource> | undefined,
16
+ value: SqlConnection | KindRef<SqlConnection> | undefined,
17
17
  ctx: ResourceContext,
18
18
  describe: () => string,
19
- ): SqlConnectionResource | undefined {
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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: SqlConnectionResource;
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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?: SqlConnectionResource;
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: SqlConnectionResource | undefined,
43
+ connection: SqlConnection | undefined,
44
44
  transaction: SqlTransactionResource | undefined,
45
45
  ctx: ResourceContext,
46
46
  describe: () => string,
47
- ): SqlConnectionResource {
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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: SqlConnectionResource,
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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?: SqlConnectionResource;
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.driver);
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
- driver: Driver,
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", driver, addParam);
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", driver, addParam);
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
- driver: Driver,
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, driver, addParam);
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
- driver: Driver,
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, driver, addParam);
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", driver, addParam);
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", driver, addParam);
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, driver, addParam);
221
+ return buildCondition(node, dialect, addParam);
222
222
  }
223
223
  return null;
224
224
  }
225
225
 
226
- function buildCondition(c: Condition, driver: Driver, addParam: (v: unknown) => string): string {
227
- const col = quoteIdent(c.column);
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
- if (driver === "postgres") {
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 ? quoteIdent(c.ref) : addParam(c.value);
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 { SqlConnectionResource } from "./sql-connection-controller.js";
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: SqlConnectionResource;
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(): SqlConnectionResource {
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
- }