@cqlite/node 0.3.1-rc1

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.
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Error wrapper module for CQLite Node.js bindings.
3
+ *
4
+ * This module provides utilities for parsing error metadata from
5
+ * native error messages and attaching them as properties.
6
+ *
7
+ * Issue #297: Error Mapping Implementation
8
+ */
9
+
10
+ /**
11
+ * Parse error metadata from a message string.
12
+ *
13
+ * The Rust layer encodes metadata in the message using null-byte separators:
14
+ * "Human-readable message\0code=IO\0category=System\0isRecoverable=true"
15
+ *
16
+ * @param {string} message - The error message from native code
17
+ * @returns {Object} Parsed metadata with code, category, isRecoverable, and message
18
+ */
19
+ function parseErrorMetadata(message) {
20
+ if (!message || typeof message !== 'string') {
21
+ return {
22
+ code: 'INTERNAL',
23
+ category: 'Internal',
24
+ isRecoverable: false,
25
+ message: String(message || 'Unknown error'),
26
+ };
27
+ }
28
+
29
+ // Split by null bytes
30
+ const parts = message.split('\0');
31
+ const humanMessage = parts[0];
32
+
33
+ // Default values
34
+ let code = 'INTERNAL';
35
+ let category = 'Internal';
36
+ let isRecoverable = false;
37
+
38
+ // Parse metadata from remaining parts
39
+ for (let i = 1; i < parts.length; i++) {
40
+ const part = parts[i];
41
+ if (part.startsWith('code=')) {
42
+ code = part.slice(5);
43
+ } else if (part.startsWith('category=')) {
44
+ category = part.slice(9);
45
+ } else if (part.startsWith('isRecoverable=')) {
46
+ isRecoverable = part.slice(14) === 'true';
47
+ }
48
+ }
49
+
50
+ return {
51
+ code,
52
+ category,
53
+ isRecoverable,
54
+ message: humanMessage,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Enhance an Error object with CQLite metadata properties.
60
+ *
61
+ * @param {Error} error - The error to enhance
62
+ * @returns {Error} The enhanced error with code, category, and isRecoverable properties
63
+ */
64
+ function enhanceError(error) {
65
+ if (!error || typeof error.message !== 'string') {
66
+ return error;
67
+ }
68
+
69
+ const metadata = parseErrorMetadata(error.message);
70
+
71
+ // Update the message to the human-readable part only
72
+ error.message = metadata.message;
73
+
74
+ // Add properties
75
+ error.code = metadata.code;
76
+ error.category = metadata.category;
77
+ error.isRecoverable = metadata.isRecoverable;
78
+
79
+ return error;
80
+ }
81
+
82
+ /**
83
+ * Wrap an async function to enhance any thrown errors.
84
+ *
85
+ * @param {Function} fn - The async function to wrap
86
+ * @returns {Function} A wrapped function that enhances errors
87
+ */
88
+ function wrapAsync(fn) {
89
+ return async function (...args) {
90
+ try {
91
+ return await fn.apply(this, args);
92
+ } catch (error) {
93
+ throw enhanceError(error);
94
+ }
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Create an async iterable wrapper around a native StreamingResult.
100
+ *
101
+ * Implements JavaScript's AsyncIterable protocol for use with `for await...of`.
102
+ * Provides automatic resource cleanup on loop completion or early termination.
103
+ *
104
+ * @param {Object} nativeStream - The native StreamingResult from Rust
105
+ * @returns {Object} An async iterable object with streaming functionality
106
+ */
107
+ function createAsyncIterator(nativeStream) {
108
+ return {
109
+ /**
110
+ * Number of rows received so far.
111
+ * @returns {number}
112
+ */
113
+ get rowsReceived() {
114
+ return nativeStream.rowsReceived;
115
+ },
116
+
117
+ /**
118
+ * Column metadata for the result set.
119
+ * @returns {Array<Object>}
120
+ */
121
+ get columns() {
122
+ return nativeStream.columns;
123
+ },
124
+
125
+ /**
126
+ * Release resources early.
127
+ * Safe to call multiple times.
128
+ */
129
+ close() {
130
+ return nativeStream.close();
131
+ },
132
+
133
+ /**
134
+ * Implement Symbol.asyncIterator for `for await...of` support.
135
+ * @returns {AsyncIterator}
136
+ */
137
+ [Symbol.asyncIterator]() {
138
+ return {
139
+ /**
140
+ * Get the next row from the stream.
141
+ * @returns {Promise<{value: Object|undefined, done: boolean}>}
142
+ */
143
+ async next() {
144
+ try {
145
+ const result = await nativeStream.next();
146
+ return result;
147
+ } catch (error) {
148
+ throw enhanceError(error);
149
+ }
150
+ },
151
+
152
+ /**
153
+ * Handle early termination (e.g., break from loop).
154
+ * Called automatically by JavaScript runtime.
155
+ * @returns {Promise<{value: undefined, done: true}>}
156
+ */
157
+ async return() {
158
+ nativeStream.close();
159
+ return { value: undefined, done: true };
160
+ },
161
+ };
162
+ },
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Create a wrapped Database class with enhanced error handling.
168
+ *
169
+ * @param {Function} NativeDatabase - The native Database class
170
+ * @param {Function} wrapPreparedStatement - Function to wrap PreparedStatement for type consistency
171
+ * @returns {Function} A wrapped Database class
172
+ */
173
+ function createWrappedDatabase(NativeDatabase, wrapPreparedStatement) {
174
+ class Database {
175
+ constructor(native, preparedStatementWrapper) {
176
+ this._native = native;
177
+ this._wrapPreparedStatement = preparedStatementWrapper;
178
+ }
179
+
180
+ static async open(dataDir, options) {
181
+ try {
182
+ const native = await NativeDatabase.open(dataDir, options);
183
+ return new Database(native, wrapPreparedStatement);
184
+ } catch (error) {
185
+ throw enhanceError(error);
186
+ }
187
+ }
188
+
189
+ async execute(query) {
190
+ try {
191
+ const result = await this._native.execute(query);
192
+ result.rowsAffected = result.rowCount; // M4 spec alias (Issue #348)
193
+ return result;
194
+ } catch (error) {
195
+ throw enhanceError(error);
196
+ }
197
+ }
198
+
199
+ async executeNative(query) {
200
+ try {
201
+ const result = await this._native.executeNative(query);
202
+ result.rowsAffected = result.rowCount; // M4 spec alias (Issue #348)
203
+ return result;
204
+ } catch (error) {
205
+ throw enhanceError(error);
206
+ }
207
+ }
208
+
209
+ async getStats() {
210
+ try {
211
+ const stats = await this._native.getStats();
212
+ // Coerce to BigInt to ensure TypeScript type guarantees hold (Issue #351)
213
+ // napi-rs returns i64 as number for small values, but TS declares bigint
214
+ return {
215
+ totalSstables: stats.totalSstables,
216
+ totalRows: BigInt(stats.totalRows),
217
+ memoryUsedBytes: BigInt(stats.memoryUsedBytes),
218
+ };
219
+ } catch (error) {
220
+ throw enhanceError(error);
221
+ }
222
+ }
223
+
224
+ async close() {
225
+ try {
226
+ return await this._native.close();
227
+ } catch (error) {
228
+ throw enhanceError(error);
229
+ }
230
+ }
231
+
232
+ get isClosed() {
233
+ return this._native.isClosed;
234
+ }
235
+
236
+ /**
237
+ * Execute a CQL query with streaming results.
238
+ *
239
+ * Returns an async iterable that yields rows one at a time for
240
+ * memory-efficient processing of large result sets.
241
+ *
242
+ * @param {string} query - CQL SELECT statement to execute
243
+ * @param {Object} [config] - Optional streaming configuration
244
+ * @param {number} [config.bufferSize=1024] - Rows to buffer in memory
245
+ * @param {number} [config.chunkSize=10000] - Rows per fetch chunk
246
+ * @returns {AsyncIterable<Object>} Async iterable of rows
247
+ * @throws {CqliteError} If the query fails (on first iteration)
248
+ *
249
+ * @example
250
+ * for await (const row of db.executeStreaming('SELECT * FROM large_table')) {
251
+ * console.log(row.name);
252
+ * }
253
+ */
254
+ executeStreaming(query, config) {
255
+ const self = this;
256
+ let nativeStreamPromise = null;
257
+ let nativeStream = null;
258
+ let initError = null;
259
+ let closed = false;
260
+
261
+ // Lazy initialization - called on first iteration
262
+ const ensureInitialized = async () => {
263
+ // Check if stream was closed before initialization
264
+ if (closed) {
265
+ return { next: async () => ({ value: undefined, done: true }) };
266
+ }
267
+ if (initError) throw initError;
268
+ if (nativeStream) return nativeStream;
269
+ if (!nativeStreamPromise) {
270
+ nativeStreamPromise = self._native.executeStreaming(query, config)
271
+ .then(stream => {
272
+ // Check if close() was called while we were initializing
273
+ if (closed) {
274
+ stream.close();
275
+ return { next: async () => ({ value: undefined, done: true }) };
276
+ }
277
+ nativeStream = stream;
278
+ return stream;
279
+ })
280
+ .catch(err => {
281
+ initError = enhanceError(err);
282
+ throw initError;
283
+ });
284
+ }
285
+ return nativeStreamPromise;
286
+ };
287
+
288
+ return {
289
+ /**
290
+ * Number of rows received so far.
291
+ * Returns 0 before iteration begins.
292
+ * @returns {number}
293
+ */
294
+ get rowsReceived() {
295
+ return nativeStream?.rowsReceived ?? 0;
296
+ },
297
+
298
+ /**
299
+ * Column metadata for the result set.
300
+ * Returns empty array before iteration begins.
301
+ * @returns {Array<Object>}
302
+ */
303
+ get columns() {
304
+ return nativeStream?.columns ?? [];
305
+ },
306
+
307
+ /**
308
+ * Release resources early.
309
+ * Safe to call multiple times.
310
+ * If called before initialization completes, prevents initialization
311
+ * from creating a zombie stream.
312
+ */
313
+ close() {
314
+ closed = true;
315
+ // Clear the promise to prevent initialization from completing
316
+ // after close() is called (prevents zombie streams)
317
+ nativeStreamPromise = null;
318
+ nativeStream?.close();
319
+ },
320
+
321
+ /**
322
+ * Implement Symbol.asyncIterator for `for await...of` support.
323
+ * @returns {AsyncIterator}
324
+ */
325
+ [Symbol.asyncIterator]() {
326
+ return {
327
+ async next() {
328
+ try {
329
+ const stream = await ensureInitialized();
330
+ return await stream.next();
331
+ } catch (error) {
332
+ throw enhanceError(error);
333
+ }
334
+ },
335
+ async return() {
336
+ closed = true;
337
+ nativeStreamPromise = null;
338
+ nativeStream?.close();
339
+ return { value: undefined, done: true };
340
+ },
341
+ };
342
+ },
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Prepare a CQL query for analysis.
348
+ *
349
+ * Returns a PreparedStatement that can be inspected for query plan
350
+ * information and statistics.
351
+ *
352
+ * @param {string} query - CQL SELECT statement to prepare
353
+ * @returns {Promise<PreparedStatement>} PreparedStatement with query plan info
354
+ * @throws {CqliteError} If the query cannot be prepared
355
+ */
356
+ async prepare(query) {
357
+ try {
358
+ const nativeStmt = await this._native.prepare(query);
359
+ // Wrap to ensure type consistency (Issue #351)
360
+ return this._wrapPreparedStatement(nativeStmt);
361
+ } catch (error) {
362
+ throw enhanceError(error);
363
+ }
364
+ }
365
+ }
366
+
367
+ return Database;
368
+ }
369
+
370
+ module.exports = {
371
+ parseErrorMetadata,
372
+ enhanceError,
373
+ wrapAsync,
374
+ createWrappedDatabase,
375
+ };