camusdb 0.1.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.
Files changed (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +950 -0
  3. package/dist/auth/credentials.d.ts +30 -0
  4. package/dist/auth/credentials.d.ts.map +1 -0
  5. package/dist/auth/credentials.js +23 -0
  6. package/dist/auth/credentials.js.map +1 -0
  7. package/dist/auth/login-client.d.ts +37 -0
  8. package/dist/auth/login-client.d.ts.map +1 -0
  9. package/dist/auth/login-client.js +8 -0
  10. package/dist/auth/login-client.js.map +1 -0
  11. package/dist/auth/rest-login-client.d.ts +17 -0
  12. package/dist/auth/rest-login-client.d.ts.map +1 -0
  13. package/dist/auth/rest-login-client.js +71 -0
  14. package/dist/auth/rest-login-client.js.map +1 -0
  15. package/dist/auth/token-provider.d.ts +123 -0
  16. package/dist/auth/token-provider.d.ts.map +1 -0
  17. package/dist/auth/token-provider.js +209 -0
  18. package/dist/auth/token-provider.js.map +1 -0
  19. package/dist/backup.d.ts +98 -0
  20. package/dist/backup.d.ts.map +1 -0
  21. package/dist/backup.js +153 -0
  22. package/dist/backup.js.map +1 -0
  23. package/dist/cache.d.ts +90 -0
  24. package/dist/cache.d.ts.map +1 -0
  25. package/dist/cache.js +121 -0
  26. package/dist/cache.js.map +1 -0
  27. package/dist/client.d.ts +406 -0
  28. package/dist/client.d.ts.map +1 -0
  29. package/dist/client.js +724 -0
  30. package/dist/client.js.map +1 -0
  31. package/dist/column-type.d.ts +33 -0
  32. package/dist/column-type.d.ts.map +1 -0
  33. package/dist/column-type.js +50 -0
  34. package/dist/column-type.js.map +1 -0
  35. package/dist/column-value.d.ts +77 -0
  36. package/dist/column-value.d.ts.map +1 -0
  37. package/dist/column-value.js +60 -0
  38. package/dist/column-value.js.map +1 -0
  39. package/dist/config.d.ts +181 -0
  40. package/dist/config.d.ts.map +1 -0
  41. package/dist/config.js +315 -0
  42. package/dist/config.js.map +1 -0
  43. package/dist/connection-string.d.ts +45 -0
  44. package/dist/connection-string.d.ts.map +1 -0
  45. package/dist/connection-string.js +170 -0
  46. package/dist/connection-string.js.map +1 -0
  47. package/dist/endpoint-pool.d.ts +70 -0
  48. package/dist/endpoint-pool.d.ts.map +1 -0
  49. package/dist/endpoint-pool.js +120 -0
  50. package/dist/endpoint-pool.js.map +1 -0
  51. package/dist/error-codes.d.ts +44 -0
  52. package/dist/error-codes.d.ts.map +1 -0
  53. package/dist/error-codes.js +43 -0
  54. package/dist/error-codes.js.map +1 -0
  55. package/dist/errors.d.ts +24 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +33 -0
  58. package/dist/errors.js.map +1 -0
  59. package/dist/hlc.d.ts +19 -0
  60. package/dist/hlc.d.ts.map +1 -0
  61. package/dist/hlc.js +11 -0
  62. package/dist/hlc.js.map +1 -0
  63. package/dist/index.d.ts +55 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +51 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/json.d.ts +41 -0
  68. package/dist/json.d.ts.map +1 -0
  69. package/dist/json.js +345 -0
  70. package/dist/json.js.map +1 -0
  71. package/dist/object-id.d.ts +39 -0
  72. package/dist/object-id.d.ts.map +1 -0
  73. package/dist/object-id.js +99 -0
  74. package/dist/object-id.js.map +1 -0
  75. package/dist/options.d.ts +77 -0
  76. package/dist/options.d.ts.map +1 -0
  77. package/dist/options.js +68 -0
  78. package/dist/options.js.map +1 -0
  79. package/dist/prepared/binder.d.ts +20 -0
  80. package/dist/prepared/binder.d.ts.map +1 -0
  81. package/dist/prepared/binder.js +32 -0
  82. package/dist/prepared/binder.js.map +1 -0
  83. package/dist/prepared/policy.d.ts +81 -0
  84. package/dist/prepared/policy.d.ts.map +1 -0
  85. package/dist/prepared/policy.js +164 -0
  86. package/dist/prepared/policy.js.map +1 -0
  87. package/dist/query-stream.d.ts +55 -0
  88. package/dist/query-stream.d.ts.map +1 -0
  89. package/dist/query-stream.js +94 -0
  90. package/dist/query-stream.js.map +1 -0
  91. package/dist/result-set.d.ts +62 -0
  92. package/dist/result-set.d.ts.map +1 -0
  93. package/dist/result-set.js +100 -0
  94. package/dist/result-set.js.map +1 -0
  95. package/dist/retry.d.ts +38 -0
  96. package/dist/retry.d.ts.map +1 -0
  97. package/dist/retry.js +93 -0
  98. package/dist/retry.js.map +1 -0
  99. package/dist/routing/advice.d.ts +63 -0
  100. package/dist/routing/advice.d.ts.map +1 -0
  101. package/dist/routing/advice.js +61 -0
  102. package/dist/routing/advice.js.map +1 -0
  103. package/dist/routing/route-cache.d.ts +53 -0
  104. package/dist/routing/route-cache.d.ts.map +1 -0
  105. package/dist/routing/route-cache.js +123 -0
  106. package/dist/routing/route-cache.js.map +1 -0
  107. package/dist/routing/router.d.ts +53 -0
  108. package/dist/routing/router.d.ts.map +1 -0
  109. package/dist/routing/router.js +89 -0
  110. package/dist/routing/router.js.map +1 -0
  111. package/dist/row-source.d.ts +48 -0
  112. package/dist/row-source.d.ts.map +1 -0
  113. package/dist/row-source.js +35 -0
  114. package/dist/row-source.js.map +1 -0
  115. package/dist/runtime.d.ts +58 -0
  116. package/dist/runtime.d.ts.map +1 -0
  117. package/dist/runtime.js +137 -0
  118. package/dist/runtime.js.map +1 -0
  119. package/dist/shared-registry.d.ts +32 -0
  120. package/dist/shared-registry.d.ts.map +1 -0
  121. package/dist/shared-registry.js +48 -0
  122. package/dist/shared-registry.js.map +1 -0
  123. package/dist/sql-syntax.d.ts +44 -0
  124. package/dist/sql-syntax.d.ts.map +1 -0
  125. package/dist/sql-syntax.js +83 -0
  126. package/dist/sql-syntax.js.map +1 -0
  127. package/dist/statements.d.ts +15 -0
  128. package/dist/statements.d.ts.map +1 -0
  129. package/dist/statements.js +90 -0
  130. package/dist/statements.js.map +1 -0
  131. package/dist/transaction.d.ts +117 -0
  132. package/dist/transaction.d.ts.map +1 -0
  133. package/dist/transaction.js +204 -0
  134. package/dist/transaction.js.map +1 -0
  135. package/dist/transport/authenticating-transport.d.ts +63 -0
  136. package/dist/transport/authenticating-transport.d.ts.map +1 -0
  137. package/dist/transport/authenticating-transport.js +119 -0
  138. package/dist/transport/authenticating-transport.js.map +1 -0
  139. package/dist/transport/branch-statements.d.ts +39 -0
  140. package/dist/transport/branch-statements.d.ts.map +1 -0
  141. package/dist/transport/branch-statements.js +74 -0
  142. package/dist/transport/branch-statements.js.map +1 -0
  143. package/dist/transport/error-text.d.ts +26 -0
  144. package/dist/transport/error-text.d.ts.map +1 -0
  145. package/dist/transport/error-text.js +75 -0
  146. package/dist/transport/error-text.js.map +1 -0
  147. package/dist/transport/grpc/batcher.d.ts +127 -0
  148. package/dist/transport/grpc/batcher.d.ts.map +1 -0
  149. package/dist/transport/grpc/batcher.js +399 -0
  150. package/dist/transport/grpc/batcher.js.map +1 -0
  151. package/dist/transport/grpc/codec.d.ts +40 -0
  152. package/dist/transport/grpc/codec.d.ts.map +1 -0
  153. package/dist/transport/grpc/codec.js +231 -0
  154. package/dist/transport/grpc/codec.js.map +1 -0
  155. package/dist/transport/grpc/grpc-transport.d.ts +115 -0
  156. package/dist/transport/grpc/grpc-transport.d.ts.map +1 -0
  157. package/dist/transport/grpc/grpc-transport.js +577 -0
  158. package/dist/transport/grpc/grpc-transport.js.map +1 -0
  159. package/dist/transport/grpc/messages.d.ts +221 -0
  160. package/dist/transport/grpc/messages.d.ts.map +1 -0
  161. package/dist/transport/grpc/messages.js +56 -0
  162. package/dist/transport/grpc/messages.js.map +1 -0
  163. package/dist/transport/grpc/proto.d.ts +71 -0
  164. package/dist/transport/grpc/proto.d.ts.map +1 -0
  165. package/dist/transport/grpc/proto.js +67 -0
  166. package/dist/transport/grpc/proto.js.map +1 -0
  167. package/dist/transport/http.d.ts +62 -0
  168. package/dist/transport/http.d.ts.map +1 -0
  169. package/dist/transport/http.js +169 -0
  170. package/dist/transport/http.js.map +1 -0
  171. package/dist/transport/ndjson-row-source.d.ts +38 -0
  172. package/dist/transport/ndjson-row-source.d.ts.map +1 -0
  173. package/dist/transport/ndjson-row-source.js +197 -0
  174. package/dist/transport/ndjson-row-source.js.map +1 -0
  175. package/dist/transport/rest-decode.d.ts +33 -0
  176. package/dist/transport/rest-decode.d.ts.map +1 -0
  177. package/dist/transport/rest-decode.js +162 -0
  178. package/dist/transport/rest-decode.js.map +1 -0
  179. package/dist/transport/rest-prepared-cache.d.ts +50 -0
  180. package/dist/transport/rest-prepared-cache.d.ts.map +1 -0
  181. package/dist/transport/rest-prepared-cache.js +101 -0
  182. package/dist/transport/rest-prepared-cache.js.map +1 -0
  183. package/dist/transport/rest-transport.d.ts +88 -0
  184. package/dist/transport/rest-transport.d.ts.map +1 -0
  185. package/dist/transport/rest-transport.js +388 -0
  186. package/dist/transport/rest-transport.js.map +1 -0
  187. package/dist/transport/transport-pool.d.ts +40 -0
  188. package/dist/transport/transport-pool.d.ts.map +1 -0
  189. package/dist/transport/transport-pool.js +48 -0
  190. package/dist/transport/transport-pool.js.map +1 -0
  191. package/dist/transport/transport.d.ts +199 -0
  192. package/dist/transport/transport.d.ts.map +1 -0
  193. package/dist/transport/transport.js +11 -0
  194. package/dist/transport/transport.js.map +1 -0
  195. package/dist/values/decode.d.ts +39 -0
  196. package/dist/values/decode.d.ts.map +1 -0
  197. package/dist/values/decode.js +75 -0
  198. package/dist/values/decode.js.map +1 -0
  199. package/dist/values/encode.d.ts +45 -0
  200. package/dist/values/encode.d.ts.map +1 -0
  201. package/dist/values/encode.js +271 -0
  202. package/dist/values/encode.js.map +1 -0
  203. package/dist/values/ticks.d.ts +31 -0
  204. package/dist/values/ticks.d.ts.map +1 -0
  205. package/dist/values/ticks.js +52 -0
  206. package/dist/values/ticks.js.map +1 -0
  207. package/dist/values/typed.d.ts +67 -0
  208. package/dist/values/typed.d.ts.map +1 -0
  209. package/dist/values/typed.js +134 -0
  210. package/dist/values/typed.js.map +1 -0
  211. package/dist/values/uuid.d.ts +27 -0
  212. package/dist/values/uuid.d.ts.map +1 -0
  213. package/dist/values/uuid.js +76 -0
  214. package/dist/values/uuid.js.map +1 -0
  215. package/dist/vector.d.ts +26 -0
  216. package/dist/vector.d.ts.map +1 -0
  217. package/dist/vector.js +48 -0
  218. package/dist/vector.js.map +1 -0
  219. package/package.json +86 -0
  220. package/proto/camus_sql.proto +566 -0
package/dist/client.js ADDED
@@ -0,0 +1,724 @@
1
+ /**
2
+ * This file is part of CamusDB
3
+ *
4
+ * For the full copyright and license information, please view the LICENSE.txt
5
+ * file that was distributed with this source code.
6
+ */
7
+ import { CamusBackupClient } from './backup.js';
8
+ import { evictAllCacheStatement, evictCacheStatement } from './cache.js';
9
+ import { describeConfig, resolveConnectionString, resolveOptions } from './config.js';
10
+ import { CamusError } from './errors.js';
11
+ import { CamusErrorCode } from './error-codes.js';
12
+ import { withDefaults } from './options.js';
13
+ import { PrepareDecision } from './prepared/policy.js';
14
+ import { CamusQueryStream } from './query-stream.js';
15
+ import { RowMapper } from './result-set.js';
16
+ import { delay, isRetryable, withRetry } from './retry.js';
17
+ import { ClientRuntime } from './runtime.js';
18
+ import { ROUTING_ACCEPT_VERSION } from './routing/advice.js';
19
+ import { CamusRouteOpKind } from './routing/route-cache.js';
20
+ import { isDdlStatement, isPreparableStatement, runsInOwnTransaction } from './statements.js';
21
+ import { CamusTransaction } from './transaction.js';
22
+ import { encodeParameters } from './values/encode.js';
23
+ /**
24
+ * A connection to one CamusDB database.
25
+ *
26
+ * A client is cheap to create and safe to share. It holds no socket of its own: the endpoint
27
+ * rotation, the bearer token, the wire transport, and the learned routes are shared process-wide
28
+ * by what they actually depend on, so building a client per request costs almost nothing and still
29
+ * reuses the connections, the token, and the prepared statements a long-lived one would.
30
+ *
31
+ * ```ts
32
+ * const client = CamusClient.fromConnectionString('Endpoint=http://localhost:8082;Database=test');
33
+ *
34
+ * const { rows } = await client.query<Robot>('SELECT * FROM robots WHERE year = @year', { year: 1974 });
35
+ * ```
36
+ *
37
+ * Every method that reaches the server accepts an `AbortSignal` and a per-statement timeout.
38
+ */
39
+ export class CamusClient {
40
+ runtime;
41
+ backupClient;
42
+ /**
43
+ * Builds a client from an options object, or from a connection string.
44
+ *
45
+ * ```ts
46
+ * new CamusClient({ endpoint: 'http://localhost:8082', database: 'test' });
47
+ * new CamusClient('Endpoint=http://localhost:8082;Database=test');
48
+ * ```
49
+ */
50
+ constructor(options) {
51
+ this.runtime = new ClientRuntime(typeof options === 'string' ? resolveConnectionString(options) : resolveOptions(options));
52
+ }
53
+ /**
54
+ * Builds a client from a `key=value;key=value` connection string.
55
+ *
56
+ * ```ts
57
+ * CamusClient.fromConnectionString('Endpoint=http://localhost:8082;Database=test;User=app;Password=secret');
58
+ * ```
59
+ *
60
+ * Keys are matched without regard to case. Wrap a value in quotes to keep a semicolon or
61
+ * surrounding spaces in it, and double the quote character to include one.
62
+ */
63
+ static fromConnectionString(connectionString) {
64
+ return new CamusClient(connectionString);
65
+ }
66
+ /** @internal The resolved configuration, for tests and diagnostics. */
67
+ get config() {
68
+ return this.runtime.config;
69
+ }
70
+ /** The database statements run against. */
71
+ get database() {
72
+ return this.runtime.database;
73
+ }
74
+ /** The wire protocol this client speaks. */
75
+ get protocol() {
76
+ return this.runtime.transport.protocol;
77
+ }
78
+ /** The bearer token currently held, or `undefined`. Reading it never triggers a login. */
79
+ get accessToken() {
80
+ return this.runtime.auth.currentToken;
81
+ }
82
+ /**
83
+ * The node's online backup administration API.
84
+ *
85
+ * It is server-level and node-wide, not scoped to this client's database, and it needs a
86
+ * superuser token when authentication is on.
87
+ */
88
+ get backups() {
89
+ this.backupClient ??= new CamusBackupClient(this.runtime);
90
+ return this.backupClient;
91
+ }
92
+ /** How many statements this client currently keeps prepared on the server. */
93
+ get preparedStatementCount() {
94
+ return this.runtime.preparedStatements.preparedCount;
95
+ }
96
+ /** How many destinations learned routing currently holds. Zero when routing is off. */
97
+ get learnedRouteCount() {
98
+ return this.runtime.router?.learnedRouteCount ?? 0;
99
+ }
100
+ /** Points this client at another database. It sends nothing. */
101
+ changeDatabase(database) {
102
+ if (database.trim().length === 0) {
103
+ throw new CamusError(CamusErrorCode.Generic, 'A database name cannot be empty.');
104
+ }
105
+ this.runtime.database = database;
106
+ }
107
+ /** The configuration as text, with every secret masked. Safe to log. */
108
+ toString() {
109
+ return `CamusClient(${describeConfig(this.runtime.config)})`;
110
+ }
111
+ // ─── Statements ───────────────────────────────────────────────────────────
112
+ /**
113
+ * Runs a `SELECT` and reads every row.
114
+ *
115
+ * ```ts
116
+ * const { rows } = await client.query<Robot>(
117
+ * 'SELECT id, name, year FROM robots WHERE year > @year',
118
+ * { year: 1970 },
119
+ * );
120
+ * ```
121
+ *
122
+ * Give a type argument to describe the row shape. It is not checked against the server's schema —
123
+ * nothing on this side can check that — so it states what you expect, and the driver maps each
124
+ * column to the JavaScript value its declared type calls for.
125
+ *
126
+ * Use `queryStream` when the result may be large: this method holds every row in memory.
127
+ */
128
+ async query(sql, parameters, options = {}) {
129
+ const encoded = encodeParameters(parameters);
130
+ const transaction = options.transaction;
131
+ const route = await this.route(sql, CamusRouteOpKind.Query, transaction, options.signal);
132
+ const request = await this.buildRequest(sql, encoded, route, transaction, options, {
133
+ negotiateRouting: route.router !== undefined,
134
+ });
135
+ const result = await this.runtime.transport.executeQuery(request);
136
+ route.router?.learn(request.database, sql, CamusRouteOpKind.Query, result.routing, route.revision);
137
+ const mapper = new RowMapper(result.resultSet.columnNames, this.decodeOptions(options));
138
+ return {
139
+ rows: mapper.mapAll(result.resultSet),
140
+ columns: result.resultSet.columns,
141
+ rowCount: result.resultSet.rowCount,
142
+ cacheMetadata: result.cacheMetadata,
143
+ routingAdvice: result.routing,
144
+ };
145
+ }
146
+ /** Runs a `SELECT` and reads its first row, or `undefined` when it returned none. */
147
+ async queryOne(sql, parameters, options = {}) {
148
+ const result = await this.query(sql, parameters, options);
149
+ return result.rows[0];
150
+ }
151
+ /**
152
+ * Runs a `SELECT` and reads the first column of its first row, or `undefined`.
153
+ *
154
+ * ```ts
155
+ * const total = await client.scalar<number>('SELECT COUNT(*) FROM robots');
156
+ * ```
157
+ */
158
+ async scalar(sql, parameters, options = {}) {
159
+ const result = await this.query(sql, parameters, options);
160
+ const row = result.rows[0];
161
+ const firstColumn = result.columns[0];
162
+ if (row === undefined || firstColumn === undefined)
163
+ return undefined;
164
+ return row[firstColumn.name];
165
+ }
166
+ /**
167
+ * Runs a `SELECT` and reports its rows one at a time, so a large result never fully materializes
168
+ * on this side.
169
+ *
170
+ * ```ts
171
+ * await using stream = await client.queryStream<Robot>('SELECT * FROM robots');
172
+ *
173
+ * for await (const robot of stream) {
174
+ * process(robot);
175
+ * }
176
+ * ```
177
+ *
178
+ * It is genuinely incremental over REST. Over gRPC the data plane multiplexes results over
179
+ * shared streams that decode a whole result before returning, so this buffers and replays: the
180
+ * caller's code is the same, but the memory saving is not there.
181
+ *
182
+ * The streaming path gives up the transparent retry of a serializable conflict that `query` has;
183
+ * see `CamusQueryStream`.
184
+ */
185
+ async queryStream(sql, parameters, options = {}) {
186
+ const encoded = encodeParameters(parameters);
187
+ const transaction = options.transaction;
188
+ // A learned route still steers the send, which costs nothing, but the streaming endpoint's
189
+ // trailer carries no routing metadata, so nothing is negotiated or learned.
190
+ const route = await this.route(sql, CamusRouteOpKind.Query, transaction, options.signal);
191
+ const request = await this.buildRequest(sql, encoded, route, transaction, options, {
192
+ negotiateRouting: false,
193
+ });
194
+ const source = await this.runtime.transport.executeQueryStream(request);
195
+ return new CamusQueryStream(source, this.decodeOptions(options));
196
+ }
197
+ /**
198
+ * Runs an `INSERT`, `UPDATE`, or `DELETE` and reports how many rows it changed.
199
+ *
200
+ * ```ts
201
+ * const { affectedRows } = await client.execute(
202
+ * 'UPDATE robots SET year = @year WHERE id = @id',
203
+ * { year: 1984, id: robotId },
204
+ * );
205
+ * ```
206
+ *
207
+ * A DDL statement is accepted here too and is sent to the DDL route, where it reports zero rows.
208
+ */
209
+ async execute(sql, parameters, options = {}) {
210
+ if (isDdlStatement(sql)) {
211
+ await this.executeDdl(sql, options);
212
+ return { affectedRows: 0 };
213
+ }
214
+ return this.executeNonQuery(sql, parameters, options);
215
+ }
216
+ /**
217
+ * Runs a schema statement: `CREATE TABLE`, `ALTER TABLE`, `DROP INDEX`, a view definition, and
218
+ * so on.
219
+ *
220
+ * `TRUNCATE` is refused inside an explicit transaction. It commits a replicated schema entry that
221
+ * a rollback cannot undo, so the server refuses it rather than promise a rollback it cannot
222
+ * deliver, and the driver refuses it before the round trip.
223
+ */
224
+ async executeDdl(sql, options = {}) {
225
+ const transaction = options.transaction;
226
+ if (transaction !== undefined && runsInOwnTransaction(sql)) {
227
+ throw new CamusError(CamusErrorCode.StatementNotAllowedInTransaction, 'TRUNCATE runs in its own internal transaction and is refused inside an explicit one. It ' +
228
+ 'commits a replicated schema entry that a rollback cannot undo. Commit or roll back this ' +
229
+ 'transaction first, then run TRUNCATE.');
230
+ }
231
+ const endpoint = transaction !== undefined
232
+ ? await transaction.ensureStarted(undefined, options.signal)
233
+ : this.runtime.nextEndpoint();
234
+ return this.runtime.transport.executeDdl({
235
+ endpoint,
236
+ database: this.runtime.database,
237
+ sql,
238
+ timeoutSeconds: options.timeoutSeconds ?? this.runtime.timeoutSeconds,
239
+ prepared: false,
240
+ routingAcceptVersion: 0,
241
+ ...(transaction === undefined
242
+ ? { autocommitOptions: this.resolveTransactionOptions(options.transactionOptions) }
243
+ : {
244
+ txnIdPT: transaction.txnIdPT,
245
+ txnIdCounter: transaction.txnIdCounter,
246
+ streamSlot: transaction.streamSlot,
247
+ }),
248
+ signal: options.signal,
249
+ });
250
+ }
251
+ /**
252
+ * Inserts one row through the server's typed row-level route, naming a table and a column-to-value
253
+ * map instead of composing SQL.
254
+ *
255
+ * ```ts
256
+ * await client.insert('robots', { id: CamusObjectId.generateAsString(), name: 'r1', year: 1974 });
257
+ * ```
258
+ *
259
+ * It performs exactly what `INSERT INTO` performs, and takes the same authorization, endpoint
260
+ * health, and error translation. Use it to avoid building statement text for a hot write path.
261
+ */
262
+ async insert(table, values, options = {}) {
263
+ const transaction = options.transaction;
264
+ const endpoint = transaction !== undefined
265
+ ? await transaction.ensureStarted(undefined, options.signal)
266
+ : this.runtime.nextEndpoint();
267
+ return this.runtime.transport.insert({
268
+ endpoint,
269
+ database: this.runtime.database,
270
+ table,
271
+ values: encodeUnprefixed(values),
272
+ timeoutSeconds: options.timeoutSeconds ?? this.runtime.timeoutSeconds,
273
+ ...(transaction === undefined
274
+ ? {}
275
+ : {
276
+ txnIdPT: transaction.txnIdPT,
277
+ txnIdCounter: transaction.txnIdCounter,
278
+ streamSlot: transaction.streamSlot,
279
+ }),
280
+ signal: options.signal,
281
+ });
282
+ }
283
+ /** Reports whether the server answers. */
284
+ ping(options = {}) {
285
+ return this.runtime.transport.ping(this.runtime.nextEndpoint(), options.timeoutSeconds ?? this.runtime.timeoutSeconds, options.signal);
286
+ }
287
+ // ─── Transactions ─────────────────────────────────────────────────────────
288
+ /**
289
+ * Begins an explicit transaction.
290
+ *
291
+ * Prefer `transaction`, which commits, rolls back, and retries for you. Use this when the unit of
292
+ * work does not fit inside one function — a saga driven by external events, say.
293
+ *
294
+ * ```ts
295
+ * await using txn = await client.beginTransaction();
296
+ *
297
+ * await client.execute('UPDATE accounts SET balance = balance - 10 WHERE id = @id', { id: from }, { transaction: txn });
298
+ * await client.execute('UPDATE accounts SET balance = balance + 10 WHERE id = @id', { id: to }, { transaction: txn });
299
+ *
300
+ * await txn.commit();
301
+ * ```
302
+ *
303
+ * With `await using`, a transaction that is left without a commit is rolled back as the block
304
+ * unwinds.
305
+ */
306
+ async beginTransaction(options = {}, signal) {
307
+ const effective = this.resolveTransactionOptions(options);
308
+ const router = this.runtime.router;
309
+ if (router === undefined) {
310
+ // Routing off: begin here, on the rotation, right now.
311
+ const endpoint = this.runtime.nextEndpoint();
312
+ const result = await this.runtime.transport.startTransaction(endpoint, this.runtime.database, effective, this.runtime.timeoutSeconds, signal);
313
+ return new CamusTransaction(this.runtime, effective, {
314
+ txnIdPT: result.txnIdPT,
315
+ txnIdCounter: result.txnIdCounter,
316
+ endpoint,
317
+ streamSlot: result.streamSlot,
318
+ });
319
+ }
320
+ const deferred = new CamusTransaction(this.runtime, effective);
321
+ // An explicit affinity begins the transaction here, on the named statement's learned endpoint,
322
+ // falling back to rotation when nothing is learned. Without one, the first statement chooses.
323
+ if (effective.affinity !== undefined) {
324
+ const learned = router.selectEndpoint(this.runtime.database, effective.affinity, CamusRouteOpKind.Query);
325
+ await deferred.ensureStarted(learned.endpoint, signal);
326
+ }
327
+ return deferred;
328
+ }
329
+ /**
330
+ * Runs a unit of work inside a transaction, committing it when the work returns and rolling it
331
+ * back when the work throws.
332
+ *
333
+ * ```ts
334
+ * await client.transaction(async (txn) => {
335
+ * await client.execute('UPDATE accounts SET balance = balance - @amount WHERE id = @id',
336
+ * { amount, id: from }, { transaction: txn });
337
+ * await client.execute('UPDATE accounts SET balance = balance + @amount WHERE id = @id',
338
+ * { amount, id: to }, { transaction: txn });
339
+ * });
340
+ * ```
341
+ *
342
+ * A serializable conflict is retried: the whole function runs again on a fresh transaction, with
343
+ * an exponential back-off. So the function must be safe to run more than once — it must not send
344
+ * an email, charge a card, or mutate state outside the database on a path that could repeat.
345
+ *
346
+ * A commit that reports an unresolved outcome is **not** retried this way. That case is resolved
347
+ * inside `CamusTransaction`, by re-issuing the same commit on the same handle, because replaying
348
+ * the work could apply an already-durable commit twice.
349
+ */
350
+ async transaction(work, options = {}) {
351
+ const retryOptions = {
352
+ maxAttempts: options.maxAttempts ?? 5,
353
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
354
+ };
355
+ return withRetry(async () => {
356
+ const transaction = await this.beginTransaction(options.transactionOptions ?? {}, options.signal);
357
+ let result;
358
+ try {
359
+ result = await work(transaction);
360
+ }
361
+ catch (error) {
362
+ await rollbackQuietly(transaction);
363
+ throw error;
364
+ }
365
+ // A commit that lost a conflict leaves nothing to roll back, and one whose outcome was
366
+ // unknown was already resolved inside `commit`. Either way the transaction is over, and the
367
+ // retry loop above decides whether to run the whole unit of work again.
368
+ await transaction.commit(options.signal);
369
+ return result;
370
+ }, retryOptions);
371
+ }
372
+ // ─── Prepared statements ──────────────────────────────────────────────────
373
+ /**
374
+ * Registers a statement with the server, so this and every later execution of the same SQL sends
375
+ * only a handle and its parameter values.
376
+ *
377
+ * Calling it is optional. The driver prepares a statement on its own once it has seen the same
378
+ * SQL a few times; see the `maxAutoPrepare` and `autoPrepareMinUsages` settings. Call it to skip
379
+ * that warm-up for a statement you already know is hot.
380
+ *
381
+ * Registration is idempotent per endpoint, database, and SQL, so preparing twice costs nothing. A
382
+ * statement that cannot be prepared — schema, administration, or a server with no support for it
383
+ * — is remembered as such and keeps running inline. That is not an error, and this method does
384
+ * not report one for it.
385
+ */
386
+ async prepare(sql, options = {}) {
387
+ if (!isPreparableStatement(sql))
388
+ return;
389
+ const policy = this.runtime.preparedStatements;
390
+ const database = this.runtime.database;
391
+ const endpoint = options.transaction !== undefined
392
+ ? await options.transaction.ensureStarted(undefined, options.signal)
393
+ : this.runtime.nextEndpoint();
394
+ const { decision, evicted } = policy.pin(database, sql);
395
+ if (decision === PrepareDecision.Register) {
396
+ await this.register(endpoint, database, sql, options);
397
+ }
398
+ this.release(endpoint, evicted);
399
+ }
400
+ /** Whether a statement is currently kept prepared for this client's database. */
401
+ isPrepared(sql) {
402
+ return this.runtime.preparedStatements.isPrepared(this.runtime.database, sql);
403
+ }
404
+ // ─── Authentication ───────────────────────────────────────────────────────
405
+ /**
406
+ * Authenticates as a user and caches the resulting bearer token for every later statement.
407
+ *
408
+ * It is only needed when the credentials are not already in the configuration, which
409
+ * authenticates on first use, or to switch identity. Prefer it over putting a password in a
410
+ * connection string when the password comes from a secret manager at run time.
411
+ *
412
+ * Against a server with authentication off this still performs a real login and will fail, so do
413
+ * not call it unconditionally.
414
+ *
415
+ * @returns the minted token. The driver already holds it; it is reported for a caller that wants
416
+ * to pass it to another process.
417
+ */
418
+ login(user, password, signal) {
419
+ if (user.trim().length === 0) {
420
+ throw new CamusError(CamusErrorCode.Generic, 'A user name cannot be empty.');
421
+ }
422
+ return this.runtime.auth.login(user, password, signal);
423
+ }
424
+ /**
425
+ * Revokes this client's bearer token on the server.
426
+ *
427
+ * The configured credentials are kept, so a later statement authenticates again on its own: this
428
+ * ends a session, it does not switch authentication off. It does nothing when no token has been
429
+ * minted.
430
+ */
431
+ logout(signal) {
432
+ return this.runtime.auth.logout(signal);
433
+ }
434
+ // ─── Databases ────────────────────────────────────────────────────────────
435
+ /**
436
+ * Creates a database.
437
+ *
438
+ * Concurrent creations can collide transiently while the server allocates the shared database id
439
+ * sequence, which is what several environments provisioning in parallel produce. That is reported
440
+ * as a retryable condition and is retried here.
441
+ *
442
+ * With `ifNotExists`, a creation that loses a registration race is treated as success. The caller
443
+ * asked for the database to exist, and it does.
444
+ */
445
+ async createDatabase(database = this.runtime.database, options = {}) {
446
+ const ifNotExists = options.ifNotExists === true;
447
+ await this.withTransientRetry(async () => {
448
+ try {
449
+ await this.runtime.transport.createDatabase(this.runtime.nextEndpoint(), database, ifNotExists, this.runtime.timeoutSeconds, options.signal);
450
+ }
451
+ catch (error) {
452
+ // The server's idempotent existence check has a gap against concurrent registration: two
453
+ // racing creations for the same name can both pass it, and the one that loses is refused
454
+ // as already registered. The caller asked for the database to exist, and it does.
455
+ if (ifNotExists && CamusError.is(error) && error.code === CamusErrorCode.DatabaseAlreadyExists) {
456
+ return;
457
+ }
458
+ throw error;
459
+ }
460
+ }, options.signal);
461
+ }
462
+ /** Drops a database and everything in it. */
463
+ dropDatabase(database = this.runtime.database, options = {}) {
464
+ return this.runtime.transport.dropDatabase(this.runtime.nextEndpoint(), database, this.runtime.timeoutSeconds, options.signal);
465
+ }
466
+ /**
467
+ * Creates a copy-on-write branch of a database. It is the same operation as
468
+ * `CREATE DATABASE branch BRANCH FROM source`.
469
+ */
470
+ createBranchDatabase(branchName, sourceDatabaseName, options = {}) {
471
+ return this.withTransientRetry(() => this.runtime.transport.createBranchDatabase(this.runtime.nextEndpoint(), branchName, sourceDatabaseName, options.ifNotExists === true, this.runtime.timeoutSeconds, options.signal), options.signal);
472
+ }
473
+ /**
474
+ * Every transitive descendant of a database, ordered by depth and then by name. A database with
475
+ * no descendants reports an empty list.
476
+ */
477
+ showBranches(database = this.runtime.database, options = {}) {
478
+ return this.runtime.transport.showBranches(this.runtime.nextEndpoint(), database, this.runtime.timeoutSeconds, options.signal);
479
+ }
480
+ /**
481
+ * The ancestry chain of a database, from its nearest parent to the root. A root database reports
482
+ * an empty list.
483
+ */
484
+ showAncestors(database = this.runtime.database, options = {}) {
485
+ return this.runtime.transport.showAncestors(this.runtime.nextEndpoint(), database, this.runtime.timeoutSeconds, options.signal);
486
+ }
487
+ // ─── Query result cache ───────────────────────────────────────────────────
488
+ /** Drops every cached query result in one family, for the current database. */
489
+ async evictCache(name, options = {}) {
490
+ await this.executeNonQuery(evictCacheStatement(name), undefined, options);
491
+ }
492
+ /** Drops every cached query result for the current database. It never touches another one's. */
493
+ async evictAllCache(options = {}) {
494
+ await this.executeNonQuery(evictAllCacheStatement(), undefined, options);
495
+ }
496
+ // ─── Lifetime ─────────────────────────────────────────────────────────────
497
+ /**
498
+ * Releases what this client holds on its own, which today is nothing: the transport, the token,
499
+ * and the endpoint rotation are shared, and outlive any one client.
500
+ *
501
+ * Call `CamusTransportPool.closeAll` to release the shared gRPC channels when a process is
502
+ * shutting down.
503
+ */
504
+ async close() {
505
+ await Promise.resolve();
506
+ }
507
+ async [Symbol.asyncDispose]() {
508
+ await this.close();
509
+ }
510
+ // ─── Internals ────────────────────────────────────────────────────────────
511
+ async executeNonQuery(sql, parameters, options) {
512
+ const encoded = encodeParameters(parameters);
513
+ const transaction = options.transaction;
514
+ const route = await this.route(sql, CamusRouteOpKind.NonQuery, transaction, options.signal);
515
+ const request = await this.buildRequest(sql, encoded, route, transaction, options, {
516
+ negotiateRouting: route.router !== undefined,
517
+ autocommitOptions: transaction === undefined ? this.resolveTransactionOptions(options.transactionOptions) : undefined,
518
+ });
519
+ const result = await this.runtime.transport.executeNonQuery(request);
520
+ route.router?.learn(request.database, sql, CamusRouteOpKind.NonQuery, result.routing, route.revision);
521
+ return { affectedRows: result.affectedRows, routingAdvice: result.routing };
522
+ }
523
+ /**
524
+ * Resolves where one statement is sent, in strict order of precedence.
525
+ *
526
+ * An explicit transaction's pinned endpoint always wins: its operations must land in one
527
+ * server-side ordering chain, and advice may inform the next transaction but never relocates
528
+ * this one. Then a fresh learned route, when routing is on. Then the pool's ordinary rotation.
529
+ *
530
+ * It also reports the router and the route entry's revision, so the reply's advice can be applied
531
+ * conditionally: a late reply must never overwrite a route a faster reply already refreshed. A
532
+ * router of `undefined` means "do not negotiate", and a client with routing off then sends
533
+ * requests identical to a pre-routing driver's.
534
+ *
535
+ * A deferred transaction — routing on, no statement yet — is started here by its first statement,
536
+ * on that statement's learned endpoint when one is fresh and on rotation otherwise. Every later
537
+ * statement finds the pin already set.
538
+ */
539
+ async route(sql, kind, transaction, signal) {
540
+ const router = this.runtime.router;
541
+ if (transaction !== undefined) {
542
+ if (router === undefined) {
543
+ return {
544
+ endpoint: await transaction.ensureStarted(undefined, signal),
545
+ router: undefined,
546
+ revision: 0,
547
+ };
548
+ }
549
+ // Consulted even when the pin is already set: the observed revision is what lets the reply's
550
+ // advice refresh the route conditionally, instead of losing to the entry it would refresh.
551
+ const learned = router.selectEndpoint(this.runtime.database, sql, kind);
552
+ const pinned = await transaction.ensureStarted(learned.endpoint, signal);
553
+ return { endpoint: pinned, router, revision: learned.revision };
554
+ }
555
+ if (router === undefined) {
556
+ return { endpoint: this.runtime.nextEndpoint(), router: undefined, revision: 0 };
557
+ }
558
+ const learned = router.selectEndpoint(this.runtime.database, sql, kind);
559
+ return {
560
+ endpoint: learned.endpoint ?? this.runtime.nextEndpoint(),
561
+ router,
562
+ revision: learned.revision,
563
+ };
564
+ }
565
+ async buildRequest(sql, parameters, route, transaction, options, extra) {
566
+ return {
567
+ endpoint: route.endpoint,
568
+ database: this.runtime.database,
569
+ sql,
570
+ parameters,
571
+ timeoutSeconds: options.timeoutSeconds ?? this.runtime.timeoutSeconds,
572
+ prepared: await this.shouldPrepare(sql, route.endpoint, options),
573
+ routingAcceptVersion: extra.negotiateRouting ? ROUTING_ACCEPT_VERSION : 0,
574
+ ...(transaction === undefined
575
+ ? extra.autocommitOptions === undefined
576
+ ? {}
577
+ : { autocommitOptions: extra.autocommitOptions }
578
+ : {
579
+ txnIdPT: transaction.txnIdPT,
580
+ txnIdCounter: transaction.txnIdCounter,
581
+ streamSlot: transaction.streamSlot,
582
+ }),
583
+ signal: options.signal,
584
+ };
585
+ }
586
+ /**
587
+ * Whether this execution should name a prepared statement instead of carrying its SQL,
588
+ * registering it first when this is the execution that tips it over the threshold.
589
+ *
590
+ * The registration is awaited rather than started in the background, because the whole point is
591
+ * that this execution and the ones after it are cheap. Firing it off and running inline anyway
592
+ * would leave a busy statement racing its own warm-up. It costs one extra round trip, once per
593
+ * statement per endpoint.
594
+ *
595
+ * The caller passes the endpoint it already resolved. A REST handle is node-local and the pool
596
+ * rotates, so registering against a freshly drawn endpoint would routinely prepare on one node
597
+ * and execute on another — correct, because the transport re-registers where it lands, but a
598
+ * wasted round trip every single time.
599
+ */
600
+ async shouldPrepare(sql, endpoint, options) {
601
+ const policy = this.runtime.preparedStatements;
602
+ if (policy.isDisabled || !isPreparableStatement(sql))
603
+ return false;
604
+ const { decision, evicted } = policy.decide(this.runtime.database, sql);
605
+ this.release(endpoint, evicted);
606
+ switch (decision) {
607
+ case PrepareDecision.Yes:
608
+ return true;
609
+ case PrepareDecision.Register:
610
+ return this.register(endpoint, this.runtime.database, sql, options);
611
+ default:
612
+ return false;
613
+ }
614
+ }
615
+ /**
616
+ * Registers a statement and records what happened, reporting whether it may now run prepared.
617
+ *
618
+ * A registration failure never reaches the caller: preparing is an optimization, and the
619
+ * statement runs inline exactly as it would have if the driver had not tried. What the failure
620
+ * does decide is whether to ask again. A refusal specific to this statement stops asking for that
621
+ * statement; anything that says the node has no prepared-statement support at all stops asking
622
+ * for every statement, because one round trip per distinct SQL to relearn that is worse than not
623
+ * trying.
624
+ */
625
+ async register(endpoint, database, sql, options) {
626
+ const policy = this.runtime.preparedStatements;
627
+ try {
628
+ await this.runtime.transport.prepare(endpoint, database, sql, options.timeoutSeconds ?? this.runtime.timeoutSeconds, options.signal);
629
+ policy.markPrepared(database, sql);
630
+ return true;
631
+ }
632
+ catch (error) {
633
+ // The caller's own cancellation is not a verdict on the statement, but the entry must not be
634
+ // left mid-registration, or it would never be reconsidered.
635
+ if (options.signal?.aborted === true) {
636
+ policy.forget(database, sql);
637
+ throw error;
638
+ }
639
+ // Deliberately broad. Whatever went wrong, the statement is about to run inline and will
640
+ // report any real problem itself; the only decision left here is whether asking again is
641
+ // worth a round trip, and for this statement it is not.
642
+ policy.markRefused(database, sql);
643
+ if (CamusError.is(error) && isUnsupported(error))
644
+ policy.disable();
645
+ return false;
646
+ }
647
+ }
648
+ /**
649
+ * Releases a statement the policy evicted, so the server stops holding a handle this client has
650
+ * stopped using.
651
+ *
652
+ * It is best effort and off the caller's path: it is bookkeeping, not part of the statement being
653
+ * run. It takes the endpoint the caller already resolved rather than drawing a new one, which
654
+ * would rotate the pool as a side effect of housekeeping.
655
+ */
656
+ release(endpoint, evicted) {
657
+ if (evicted === undefined)
658
+ return;
659
+ void this.runtime.transport.closePrepared(endpoint, evicted.database, evicted.sql).catch(() => undefined);
660
+ }
661
+ /**
662
+ * The effective options for a transaction or an autocommit statement: the caller's explicit knobs
663
+ * win, then this client's defaults, then the server default for any knob still unset.
664
+ */
665
+ resolveTransactionOptions(requested) {
666
+ return withDefaults(requested ?? {}, this.runtime.config.defaultTransactionOptions);
667
+ }
668
+ decodeOptions(options) {
669
+ return { int64: options.int64 ?? this.runtime.config.decode.int64 };
670
+ }
671
+ async withTransientRetry(operation, signal) {
672
+ const maxAttempts = 5;
673
+ for (let attempt = 1;; attempt++) {
674
+ try {
675
+ return await operation();
676
+ }
677
+ catch (error) {
678
+ if (attempt >= maxAttempts || !isRetryable(error))
679
+ throw error;
680
+ const base = Math.min(50 * 2 ** (attempt - 1), 800);
681
+ const jitter = base * 0.25 * (2 * Math.random() - 1);
682
+ await delay(Math.max(1, base + jitter), signal);
683
+ }
684
+ }
685
+ }
686
+ }
687
+ /**
688
+ * True when a failure says the node does not implement prepared statements at all, rather than
689
+ * declining this particular statement.
690
+ *
691
+ * Both spellings surface under the generic code, because a server old enough not to have the
692
+ * feature is also too old to have a specific code for refusing it. A server that does have it
693
+ * declines an individual statement with a `CADB05xx` code instead.
694
+ *
695
+ * It is a heuristic, and it is only ever used to answer "is asking again worth a round trip?".
696
+ * Reading it wrong costs at most one wasted registration attempt per statement, never a failed
697
+ * statement.
698
+ */
699
+ function isUnsupported(error) {
700
+ return (error.code === CamusErrorCode.Generic &&
701
+ (error.message.includes('404') || error.message.toLowerCase().includes('unimplemented')));
702
+ }
703
+ /**
704
+ * The typed row insert names columns as they are, with no `@` placeholder prefix, because it binds
705
+ * to column names rather than to placeholders.
706
+ */
707
+ function encodeUnprefixed(values) {
708
+ const encoded = encodeParameters(values) ?? new Map();
709
+ const columns = new Map();
710
+ for (const [name, value] of encoded) {
711
+ columns.set(name.startsWith('@') ? name.slice(1) : name, value);
712
+ }
713
+ return columns;
714
+ }
715
+ async function rollbackQuietly(transaction) {
716
+ try {
717
+ await transaction.rollback();
718
+ }
719
+ catch {
720
+ // The work already failed, and that is the failure worth reporting. An abandoned transaction
721
+ // is ended by the server's own session timeout.
722
+ }
723
+ }
724
+ //# sourceMappingURL=client.js.map