@comity/sql-kysely 0.9.0 → 0.9.1

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 CHANGED
@@ -53,7 +53,3 @@ No exhaustive reference; see docs for constraints.
53
53
  ## Status
54
54
 
55
55
  Stable
56
-
57
- _Review Completed: 2026-07-25_
58
- _Reviewer: Hobiri MAGI (DeepSeek v4 Pro)_
59
- _Compliance Score: 99.5% (Green)_
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeAtomicBatch = executeAtomicBatch;
4
+ const errors_1 = require("@comity/sql/errors");
5
+ const map_sql_error_js_1 = require("../internal/map-sql-error.js");
6
+ /**
7
+ * Validate the complete batch before any execution is attempted.
8
+ *
9
+ * @param queries - Immutable SQL query payloads
10
+ * @param adapter - Adapter name for diagnostics
11
+ *
12
+ * @returns The normalised statements, or a failure when validation fails
13
+ */
14
+ function prepareBatch(queries, adapter) {
15
+ const statements = [];
16
+ for (const query of queries) {
17
+ // Named parameters are unsupported by the existing execution path, so the
18
+ // batch path MUST NOT introduce a second parameter model.
19
+ if (query.params && !Array.isArray(query.params)) {
20
+ return {
21
+ success: false,
22
+ error: new errors_1.SqlError("invalid_query", {
23
+ details: { retriable: false, adapter, operation: "batch" },
24
+ context: { violation: "named_parameters" },
25
+ }),
26
+ };
27
+ }
28
+ statements.push({
29
+ statement: query.statement,
30
+ params: (query.params ?? []),
31
+ });
32
+ }
33
+ return { success: true, value: statements };
34
+ }
35
+ /**
36
+ * Map an executor statement result onto the generic SqlResult representation.
37
+ *
38
+ * @param result - Executor result for a single statement
39
+ *
40
+ * @returns SqlResult with mapped row count
41
+ */
42
+ function toSqlResult(result) {
43
+ return {
44
+ rows: result.rows,
45
+ rowCount: typeof result.numAffectedRows === "bigint"
46
+ ? Number(result.numAffectedRows)
47
+ : result.rows.length,
48
+ };
49
+ }
50
+ /**
51
+ * Execute a batch of statements atomically via the injected executor.
52
+ *
53
+ * @param queries - Immutable SQL query payloads, executed in order
54
+ * @param executor - Atomic batch executor supplied by composition
55
+ * @param adapter - Adapter name for diagnostics
56
+ *
57
+ * @returns Operation result wrapping a SqlBatchResult aligned with `queries`
58
+ *
59
+ * @throws Errors are not thrown; failures are returned via SqlOperationResult
60
+ *
61
+ * @remarks
62
+ * Validation of the whole batch completes before the executor is invoked, so an
63
+ * invalid statement can never result in a partial execution.
64
+ *
65
+ * Atomicity is provided by the executor, not by this function. This function
66
+ * MUST NOT fall back to sequential execution or to a transaction, because either
67
+ * would violate the all-or-nothing guarantee. A failure yields exactly one
68
+ * SqlError and no partial result; the failing statement index is not reported
69
+ * because the contract does not define one.
70
+ */
71
+ async function executeAtomicBatch(queries, executor, adapter) {
72
+ // An empty batch is rejected before the executor is consulted, so behaviour is
73
+ // deterministic and independent of whether an executor was supplied.
74
+ if (queries.length === 0) {
75
+ return {
76
+ success: false,
77
+ error: new errors_1.SqlError("invalid_query", {
78
+ details: { retriable: false, adapter, operation: "batch" },
79
+ context: { violation: "empty_batch" },
80
+ }),
81
+ };
82
+ }
83
+ // An unsupported capability is a configuration failure, not an exception.
84
+ if (!executor) {
85
+ return {
86
+ success: false,
87
+ error: new errors_1.SqlError("invalid_configuration", {
88
+ details: { retriable: false, adapter, operation: "batch" },
89
+ context: { violation: "atomic_batch_executor_missing" },
90
+ }),
91
+ };
92
+ }
93
+ const prepared = prepareBatch(queries, adapter);
94
+ if (!prepared.success) {
95
+ return prepared;
96
+ }
97
+ try {
98
+ const results = await executor.execute(prepared.value);
99
+ // The executor guarantees positional alignment; a mismatch is a defect in
100
+ // the injected implementation and is surfaced rather than silently accepted.
101
+ if (results.length !== queries.length) {
102
+ return {
103
+ success: false,
104
+ error: new errors_1.SqlError("query_failed", {
105
+ details: { retriable: false, adapter, operation: "batch" },
106
+ context: { violation: "executor_result_count_mismatch" },
107
+ }),
108
+ };
109
+ }
110
+ return {
111
+ success: true,
112
+ value: results.map(toSqlResult),
113
+ };
114
+ }
115
+ catch (e) {
116
+ return {
117
+ success: false,
118
+ error: (0, map_sql_error_js_1.mapSqlError)(e, "batch", adapter),
119
+ };
120
+ }
121
+ }
122
+ //# sourceMappingURL=execute.js.map
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createKyselySqlClient = createKyselySqlClient;
4
- const execute_js_1 = require("../query/execute.js");
5
- const execute_js_2 = require("../transaction/execute.js");
4
+ const execute_js_1 = require("../batch/execute.js");
5
+ const execute_js_2 = require("../query/execute.js");
6
+ const execute_js_3 = require("../transaction/execute.js");
6
7
  /**
7
8
  * Create a SqlClient backed by a Kysely database instance.
8
9
  *
@@ -21,9 +22,11 @@ const execute_js_2 = require("../transaction/execute.js");
21
22
  function createKyselySqlClient(options) {
22
23
  return {
23
24
  /** @inheritdoc */
24
- query: (q) => (0, execute_js_1.executeQuery)(options.db, q, options.adapter),
25
+ query: (q) => (0, execute_js_2.executeQuery)(options.db, q, options.adapter),
25
26
  /** @inheritdoc */
26
- begin: () => (0, execute_js_2.executeTransaction)(options.db, options.adapter),
27
+ begin: () => (0, execute_js_3.executeTransaction)(options.db, options.adapter),
28
+ /** @inheritdoc */
29
+ atomicBatch: (queries) => (0, execute_js_1.executeAtomicBatch)(queries, options.atomicBatchExecutor, options.adapter),
27
30
  };
28
31
  }
29
32
  //# sourceMappingURL=create-kysely.js.map
@@ -0,0 +1,119 @@
1
+ import { SqlError } from "@comity/sql/errors";
2
+ import { mapSqlError } from "../internal/map-sql-error.js";
3
+ /**
4
+ * Validate the complete batch before any execution is attempted.
5
+ *
6
+ * @param queries - Immutable SQL query payloads
7
+ * @param adapter - Adapter name for diagnostics
8
+ *
9
+ * @returns The normalised statements, or a failure when validation fails
10
+ */
11
+ function prepareBatch(queries, adapter) {
12
+ const statements = [];
13
+ for (const query of queries) {
14
+ // Named parameters are unsupported by the existing execution path, so the
15
+ // batch path MUST NOT introduce a second parameter model.
16
+ if (query.params && !Array.isArray(query.params)) {
17
+ return {
18
+ success: false,
19
+ error: new SqlError("invalid_query", {
20
+ details: { retriable: false, adapter, operation: "batch" },
21
+ context: { violation: "named_parameters" },
22
+ }),
23
+ };
24
+ }
25
+ statements.push({
26
+ statement: query.statement,
27
+ params: (query.params ?? []),
28
+ });
29
+ }
30
+ return { success: true, value: statements };
31
+ }
32
+ /**
33
+ * Map an executor statement result onto the generic SqlResult representation.
34
+ *
35
+ * @param result - Executor result for a single statement
36
+ *
37
+ * @returns SqlResult with mapped row count
38
+ */
39
+ function toSqlResult(result) {
40
+ return {
41
+ rows: result.rows,
42
+ rowCount: typeof result.numAffectedRows === "bigint"
43
+ ? Number(result.numAffectedRows)
44
+ : result.rows.length,
45
+ };
46
+ }
47
+ /**
48
+ * Execute a batch of statements atomically via the injected executor.
49
+ *
50
+ * @param queries - Immutable SQL query payloads, executed in order
51
+ * @param executor - Atomic batch executor supplied by composition
52
+ * @param adapter - Adapter name for diagnostics
53
+ *
54
+ * @returns Operation result wrapping a SqlBatchResult aligned with `queries`
55
+ *
56
+ * @throws Errors are not thrown; failures are returned via SqlOperationResult
57
+ *
58
+ * @remarks
59
+ * Validation of the whole batch completes before the executor is invoked, so an
60
+ * invalid statement can never result in a partial execution.
61
+ *
62
+ * Atomicity is provided by the executor, not by this function. This function
63
+ * MUST NOT fall back to sequential execution or to a transaction, because either
64
+ * would violate the all-or-nothing guarantee. A failure yields exactly one
65
+ * SqlError and no partial result; the failing statement index is not reported
66
+ * because the contract does not define one.
67
+ */
68
+ export async function executeAtomicBatch(queries, executor, adapter) {
69
+ // An empty batch is rejected before the executor is consulted, so behaviour is
70
+ // deterministic and independent of whether an executor was supplied.
71
+ if (queries.length === 0) {
72
+ return {
73
+ success: false,
74
+ error: new SqlError("invalid_query", {
75
+ details: { retriable: false, adapter, operation: "batch" },
76
+ context: { violation: "empty_batch" },
77
+ }),
78
+ };
79
+ }
80
+ // An unsupported capability is a configuration failure, not an exception.
81
+ if (!executor) {
82
+ return {
83
+ success: false,
84
+ error: new SqlError("invalid_configuration", {
85
+ details: { retriable: false, adapter, operation: "batch" },
86
+ context: { violation: "atomic_batch_executor_missing" },
87
+ }),
88
+ };
89
+ }
90
+ const prepared = prepareBatch(queries, adapter);
91
+ if (!prepared.success) {
92
+ return prepared;
93
+ }
94
+ try {
95
+ const results = await executor.execute(prepared.value);
96
+ // The executor guarantees positional alignment; a mismatch is a defect in
97
+ // the injected implementation and is surfaced rather than silently accepted.
98
+ if (results.length !== queries.length) {
99
+ return {
100
+ success: false,
101
+ error: new SqlError("query_failed", {
102
+ details: { retriable: false, adapter, operation: "batch" },
103
+ context: { violation: "executor_result_count_mismatch" },
104
+ }),
105
+ };
106
+ }
107
+ return {
108
+ success: true,
109
+ value: results.map(toSqlResult),
110
+ };
111
+ }
112
+ catch (e) {
113
+ return {
114
+ success: false,
115
+ error: mapSqlError(e, "batch", adapter),
116
+ };
117
+ }
118
+ }
119
+ //# sourceMappingURL=execute.js.map
@@ -1,3 +1,4 @@
1
+ import { executeAtomicBatch } from "../batch/execute.js";
1
2
  import { executeQuery } from "../query/execute.js";
2
3
  import { executeTransaction } from "../transaction/execute.js";
3
4
  /**
@@ -21,6 +22,8 @@ export function createKyselySqlClient(options) {
21
22
  query: (q) => executeQuery(options.db, q, options.adapter),
22
23
  /** @inheritdoc */
23
24
  begin: () => executeTransaction(options.db, options.adapter),
25
+ /** @inheritdoc */
26
+ atomicBatch: (queries) => executeAtomicBatch(queries, options.atomicBatchExecutor, options.adapter),
24
27
  };
25
28
  }
26
29
  //# sourceMappingURL=create-kysely.js.map
@@ -0,0 +1,24 @@
1
+ import type { SqlBatchResult, SqlOperationResult, SqlQuery } from "@comity/sql";
2
+ import type { KyselyAtomicBatchExecutor } from "../client/types.js";
3
+ /**
4
+ * Execute a batch of statements atomically via the injected executor.
5
+ *
6
+ * @param queries - Immutable SQL query payloads, executed in order
7
+ * @param executor - Atomic batch executor supplied by composition
8
+ * @param adapter - Adapter name for diagnostics
9
+ *
10
+ * @returns Operation result wrapping a SqlBatchResult aligned with `queries`
11
+ *
12
+ * @throws Errors are not thrown; failures are returned via SqlOperationResult
13
+ *
14
+ * @remarks
15
+ * Validation of the whole batch completes before the executor is invoked, so an
16
+ * invalid statement can never result in a partial execution.
17
+ *
18
+ * Atomicity is provided by the executor, not by this function. This function
19
+ * MUST NOT fall back to sequential execution or to a transaction, because either
20
+ * would violate the all-or-nothing guarantee. A failure yields exactly one
21
+ * SqlError and no partial result; the failing statement index is not reported
22
+ * because the contract does not define one.
23
+ */
24
+ export declare function executeAtomicBatch(queries: readonly Readonly<SqlQuery>[], executor: KyselyAtomicBatchExecutor | undefined, adapter: string): Promise<SqlOperationResult<SqlBatchResult>>;
@@ -1,5 +1,6 @@
1
1
  import type { SqlClient, SqlClientOptions } from "@comity/sql";
2
2
  import type { Kysely } from "kysely";
3
+ import type { KyselyAtomicBatchExecutor } from "./types.js";
3
4
  /**
4
5
  * Kysely client factory options.
5
6
  *
@@ -10,6 +11,15 @@ export interface KyselySqlClientOptions<DB> extends SqlClientOptions {
10
11
  readonly db: Kysely<DB>;
11
12
  /** Adapter name for diagnostics */
12
13
  readonly adapter: string;
14
+ /**
15
+ * Executor providing atomic all-or-nothing multi-statement execution.
16
+ *
17
+ * @remarks
18
+ * Injected by composition because Kysely exposes no such primitive. When
19
+ * omitted, `atomicBatch` reports `invalid_configuration` rather than falling
20
+ * back to sequential execution.
21
+ */
22
+ readonly atomicBatchExecutor?: KyselyAtomicBatchExecutor;
13
23
  }
14
24
  /**
15
25
  * Create a SqlClient backed by a Kysely database instance.
@@ -1,4 +1,58 @@
1
1
  import type { Compilable } from "kysely";
2
+ /**
3
+ * A single already-parameterised statement submitted to an atomic batch executor.
4
+ *
5
+ * @remarks
6
+ * Deliberately free of Comity and database-engine concepts. It carries only
7
+ * normalised SQL text and positional parameters.
8
+ */
9
+ export interface KyselyAtomicStatement {
10
+ /** Normalised SQL statement text */
11
+ readonly statement: string;
12
+ /** Positional parameters bound to `statement` */
13
+ readonly params: readonly unknown[];
14
+ }
15
+ /**
16
+ * Outcome of a single statement within an atomic batch.
17
+ */
18
+ export interface KyselyAtomicStatementResult {
19
+ /** Rows produced by the statement */
20
+ readonly rows: readonly unknown[];
21
+ /** Optional affected rows count */
22
+ readonly numAffectedRows?: bigint;
23
+ }
24
+ /**
25
+ * Executes a set of already-parameterised statements as one all-or-nothing
26
+ * operation and returns one result per statement in input order.
27
+ *
28
+ * @remarks
29
+ * This abstraction exists because Kysely's public API does not expose an atomic
30
+ * multi-statement primitive. Composition injects the implementation that
31
+ * provides one, for example by delegating to a database-specific batch call.
32
+ *
33
+ * Implementations MUST provide all-or-nothing semantics: either every statement
34
+ * takes effect, or none does. They MUST NOT apply retries, sequential fallback,
35
+ * or compensation.
36
+ *
37
+ * Implementations MUST NOT assume atomicity on their own behalf; an
38
+ * implementation that cannot guarantee it must not be injected here.
39
+ *
40
+ * The adapter maps `numAffectedRows` to `SqlResult.rowCount`. A statement that
41
+ * affects zero rows is still a successful result; the executor MUST NOT treat a
42
+ * zero count as a failure.
43
+ */
44
+ export interface KyselyAtomicBatchExecutor {
45
+ /**
46
+ * Execute every statement atomically.
47
+ *
48
+ * @param statements - Statements to execute, in execution order
49
+ *
50
+ * @returns One result per statement, positionally aligned with `statements`
51
+ *
52
+ * @throws Underlying driver failures. The adapter maps them to SqlError.
53
+ */
54
+ execute(statements: readonly KyselyAtomicStatement[]): Promise<readonly KyselyAtomicStatementResult[]>;
55
+ }
2
56
  /**
3
57
  * Minimal Kysely database interface used by this adapter.
4
58
  */
@@ -1 +1,2 @@
1
+ export type { KyselyAtomicBatchExecutor, KyselyAtomicStatement, KyselyAtomicStatementResult, } from "./client/types.js";
1
2
  export { createKyselySqlClient } from "./client/create-kysely.js";
@@ -22,5 +22,5 @@ export declare const DRIVER_ERROR_MAPPINGS: Record<string, Record<string, ErrorD
22
22
  *
23
23
  * @returns A standardized SqlError instance
24
24
  */
25
- export declare function mapSqlError(cause: unknown, operation: "connect" | "query" | "transaction", adapter: string): SqlError;
25
+ export declare function mapSqlError(cause: unknown, operation: "connect" | "query" | "transaction" | "batch", adapter: string): SqlError;
26
26
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comity/sql-kysely",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Kysely ORM adapter for @comity/sql contracts",
5
5
  "type": "module",
6
6
  "private": false,
@@ -58,13 +58,13 @@
58
58
  "sideEffects": false,
59
59
  "peerDependencies": {
60
60
  "kysely": "^0.28.0",
61
- "@comity/sql": "0.9.0"
61
+ "@comity/sql": "0.9.1"
62
62
  },
63
63
  "dependencies": {
64
- "@comity/sql": "0.9.0"
64
+ "@comity/sql": "0.9.1"
65
65
  },
66
66
  "devDependencies": {
67
- "@types/node": "^24.13.4",
67
+ "@types/node": "^24.19.1",
68
68
  "kysely": "^0.28.17",
69
69
  "typescript": "^5.9.3"
70
70
  },