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.
- package/LICENSE +21 -0
- package/README.md +950 -0
- package/dist/auth/credentials.d.ts +30 -0
- package/dist/auth/credentials.d.ts.map +1 -0
- package/dist/auth/credentials.js +23 -0
- package/dist/auth/credentials.js.map +1 -0
- package/dist/auth/login-client.d.ts +37 -0
- package/dist/auth/login-client.d.ts.map +1 -0
- package/dist/auth/login-client.js +8 -0
- package/dist/auth/login-client.js.map +1 -0
- package/dist/auth/rest-login-client.d.ts +17 -0
- package/dist/auth/rest-login-client.d.ts.map +1 -0
- package/dist/auth/rest-login-client.js +71 -0
- package/dist/auth/rest-login-client.js.map +1 -0
- package/dist/auth/token-provider.d.ts +123 -0
- package/dist/auth/token-provider.d.ts.map +1 -0
- package/dist/auth/token-provider.js +209 -0
- package/dist/auth/token-provider.js.map +1 -0
- package/dist/backup.d.ts +98 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +153 -0
- package/dist/backup.js.map +1 -0
- package/dist/cache.d.ts +90 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +121 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +406 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +724 -0
- package/dist/client.js.map +1 -0
- package/dist/column-type.d.ts +33 -0
- package/dist/column-type.d.ts.map +1 -0
- package/dist/column-type.js +50 -0
- package/dist/column-type.js.map +1 -0
- package/dist/column-value.d.ts +77 -0
- package/dist/column-value.d.ts.map +1 -0
- package/dist/column-value.js +60 -0
- package/dist/column-value.js.map +1 -0
- package/dist/config.d.ts +181 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +315 -0
- package/dist/config.js.map +1 -0
- package/dist/connection-string.d.ts +45 -0
- package/dist/connection-string.d.ts.map +1 -0
- package/dist/connection-string.js +170 -0
- package/dist/connection-string.js.map +1 -0
- package/dist/endpoint-pool.d.ts +70 -0
- package/dist/endpoint-pool.d.ts.map +1 -0
- package/dist/endpoint-pool.js +120 -0
- package/dist/endpoint-pool.js.map +1 -0
- package/dist/error-codes.d.ts +44 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +43 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/hlc.d.ts +19 -0
- package/dist/hlc.d.ts.map +1 -0
- package/dist/hlc.js +11 -0
- package/dist/hlc.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +41 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +345 -0
- package/dist/json.js.map +1 -0
- package/dist/object-id.d.ts +39 -0
- package/dist/object-id.d.ts.map +1 -0
- package/dist/object-id.js +99 -0
- package/dist/object-id.js.map +1 -0
- package/dist/options.d.ts +77 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +68 -0
- package/dist/options.js.map +1 -0
- package/dist/prepared/binder.d.ts +20 -0
- package/dist/prepared/binder.d.ts.map +1 -0
- package/dist/prepared/binder.js +32 -0
- package/dist/prepared/binder.js.map +1 -0
- package/dist/prepared/policy.d.ts +81 -0
- package/dist/prepared/policy.d.ts.map +1 -0
- package/dist/prepared/policy.js +164 -0
- package/dist/prepared/policy.js.map +1 -0
- package/dist/query-stream.d.ts +55 -0
- package/dist/query-stream.d.ts.map +1 -0
- package/dist/query-stream.js +94 -0
- package/dist/query-stream.js.map +1 -0
- package/dist/result-set.d.ts +62 -0
- package/dist/result-set.d.ts.map +1 -0
- package/dist/result-set.js +100 -0
- package/dist/result-set.js.map +1 -0
- package/dist/retry.d.ts +38 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +93 -0
- package/dist/retry.js.map +1 -0
- package/dist/routing/advice.d.ts +63 -0
- package/dist/routing/advice.d.ts.map +1 -0
- package/dist/routing/advice.js +61 -0
- package/dist/routing/advice.js.map +1 -0
- package/dist/routing/route-cache.d.ts +53 -0
- package/dist/routing/route-cache.d.ts.map +1 -0
- package/dist/routing/route-cache.js +123 -0
- package/dist/routing/route-cache.js.map +1 -0
- package/dist/routing/router.d.ts +53 -0
- package/dist/routing/router.d.ts.map +1 -0
- package/dist/routing/router.js +89 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/row-source.d.ts +48 -0
- package/dist/row-source.d.ts.map +1 -0
- package/dist/row-source.js +35 -0
- package/dist/row-source.js.map +1 -0
- package/dist/runtime.d.ts +58 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +137 -0
- package/dist/runtime.js.map +1 -0
- package/dist/shared-registry.d.ts +32 -0
- package/dist/shared-registry.d.ts.map +1 -0
- package/dist/shared-registry.js +48 -0
- package/dist/shared-registry.js.map +1 -0
- package/dist/sql-syntax.d.ts +44 -0
- package/dist/sql-syntax.d.ts.map +1 -0
- package/dist/sql-syntax.js +83 -0
- package/dist/sql-syntax.js.map +1 -0
- package/dist/statements.d.ts +15 -0
- package/dist/statements.d.ts.map +1 -0
- package/dist/statements.js +90 -0
- package/dist/statements.js.map +1 -0
- package/dist/transaction.d.ts +117 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +204 -0
- package/dist/transaction.js.map +1 -0
- package/dist/transport/authenticating-transport.d.ts +63 -0
- package/dist/transport/authenticating-transport.d.ts.map +1 -0
- package/dist/transport/authenticating-transport.js +119 -0
- package/dist/transport/authenticating-transport.js.map +1 -0
- package/dist/transport/branch-statements.d.ts +39 -0
- package/dist/transport/branch-statements.d.ts.map +1 -0
- package/dist/transport/branch-statements.js +74 -0
- package/dist/transport/branch-statements.js.map +1 -0
- package/dist/transport/error-text.d.ts +26 -0
- package/dist/transport/error-text.d.ts.map +1 -0
- package/dist/transport/error-text.js +75 -0
- package/dist/transport/error-text.js.map +1 -0
- package/dist/transport/grpc/batcher.d.ts +127 -0
- package/dist/transport/grpc/batcher.d.ts.map +1 -0
- package/dist/transport/grpc/batcher.js +399 -0
- package/dist/transport/grpc/batcher.js.map +1 -0
- package/dist/transport/grpc/codec.d.ts +40 -0
- package/dist/transport/grpc/codec.d.ts.map +1 -0
- package/dist/transport/grpc/codec.js +231 -0
- package/dist/transport/grpc/codec.js.map +1 -0
- package/dist/transport/grpc/grpc-transport.d.ts +115 -0
- package/dist/transport/grpc/grpc-transport.d.ts.map +1 -0
- package/dist/transport/grpc/grpc-transport.js +577 -0
- package/dist/transport/grpc/grpc-transport.js.map +1 -0
- package/dist/transport/grpc/messages.d.ts +221 -0
- package/dist/transport/grpc/messages.d.ts.map +1 -0
- package/dist/transport/grpc/messages.js +56 -0
- package/dist/transport/grpc/messages.js.map +1 -0
- package/dist/transport/grpc/proto.d.ts +71 -0
- package/dist/transport/grpc/proto.d.ts.map +1 -0
- package/dist/transport/grpc/proto.js +67 -0
- package/dist/transport/grpc/proto.js.map +1 -0
- package/dist/transport/http.d.ts +62 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +169 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/ndjson-row-source.d.ts +38 -0
- package/dist/transport/ndjson-row-source.d.ts.map +1 -0
- package/dist/transport/ndjson-row-source.js +197 -0
- package/dist/transport/ndjson-row-source.js.map +1 -0
- package/dist/transport/rest-decode.d.ts +33 -0
- package/dist/transport/rest-decode.d.ts.map +1 -0
- package/dist/transport/rest-decode.js +162 -0
- package/dist/transport/rest-decode.js.map +1 -0
- package/dist/transport/rest-prepared-cache.d.ts +50 -0
- package/dist/transport/rest-prepared-cache.d.ts.map +1 -0
- package/dist/transport/rest-prepared-cache.js +101 -0
- package/dist/transport/rest-prepared-cache.js.map +1 -0
- package/dist/transport/rest-transport.d.ts +88 -0
- package/dist/transport/rest-transport.d.ts.map +1 -0
- package/dist/transport/rest-transport.js +388 -0
- package/dist/transport/rest-transport.js.map +1 -0
- package/dist/transport/transport-pool.d.ts +40 -0
- package/dist/transport/transport-pool.d.ts.map +1 -0
- package/dist/transport/transport-pool.js +48 -0
- package/dist/transport/transport-pool.js.map +1 -0
- package/dist/transport/transport.d.ts +199 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +11 -0
- package/dist/transport/transport.js.map +1 -0
- package/dist/values/decode.d.ts +39 -0
- package/dist/values/decode.d.ts.map +1 -0
- package/dist/values/decode.js +75 -0
- package/dist/values/decode.js.map +1 -0
- package/dist/values/encode.d.ts +45 -0
- package/dist/values/encode.d.ts.map +1 -0
- package/dist/values/encode.js +271 -0
- package/dist/values/encode.js.map +1 -0
- package/dist/values/ticks.d.ts +31 -0
- package/dist/values/ticks.d.ts.map +1 -0
- package/dist/values/ticks.js +52 -0
- package/dist/values/ticks.js.map +1 -0
- package/dist/values/typed.d.ts +67 -0
- package/dist/values/typed.d.ts.map +1 -0
- package/dist/values/typed.js +134 -0
- package/dist/values/typed.js.map +1 -0
- package/dist/values/uuid.d.ts +27 -0
- package/dist/values/uuid.d.ts.map +1 -0
- package/dist/values/uuid.js +76 -0
- package/dist/values/uuid.js.map +1 -0
- package/dist/vector.d.ts +26 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +48 -0
- package/dist/vector.js.map +1 -0
- package/package.json +86 -0
- 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
|