@kineviz/ladybug-lite 0.15.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2025 Kùzu Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # ladybug-lite
2
+
3
+ A lightweight fork of the [Ladybug](https://github.com/LadybugDB/ladybug) (formerly [Kùzu](https://github.com/kuzudb/kuzu)) embedded graph database, optimized for faster installation and broader compatibility.
4
+
5
+ ## What is Ladybug?
6
+
7
+ Ladybug is a high-performance embedded graph database management system designed for efficient graph data storage and querying. It supports property graphs and the openCypher query language. Ladybug is the renamed continuation of the Kùzu project.
8
+
9
+ ## Why We Forked Ladybug
10
+
11
+ - **Large Package Size:** The official Ladybug (`@ladybugdb/core`) npm package can exceed 100MB, resulting in slow downloads and build times, particularly outside Europe and North America. **ladybug-lite** strips it down to essential binaries for a smaller, faster package.
12
+
13
+ - **No Alpine Linux Support:** The official Ladybug package doesn't ship Alpine Linux binaries out of the box, which is critical for lightweight Docker containers. **ladybug-lite** includes musl libc-compatible binaries to work seamlessly with Alpine Linux environments.
14
+
15
+ ## Benefits
16
+
17
+ - **Smaller Footprint:** Significantly reduced package size for faster downloads and deployments.
18
+
19
+ - **Broader Compatibility:** Full support for Alpine Linux and musl libc environments.
20
+
21
+ - **Faster Integration:** Reduced build times in CI/CD pipelines and development workflows.
22
+
23
+ - **Same Core Power:** Retains all of Ladybug's essential functionality and performance in a leaner package.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install @kineviz/ladybug-lite
29
+ # or
30
+ yarn add @kineviz/ladybug-lite
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```javascript
36
+ const lbug = require('@kineviz/ladybug-lite');
37
+ const path = require("path");
38
+
39
+ (async () => {
40
+ // Create an empty on-disk database and connect to it
41
+ const db = new lbug.Database(path.join(__dirname, "./demo_db"));
42
+ const conn = new lbug.Connection(db);
43
+ try {
44
+ await conn.query(`
45
+ CREATE NODE TABLE Movie (name STRING, PRIMARY KEY(name));
46
+ CREATE NODE TABLE Person (name STRING, birthDate STRING, PRIMARY KEY(name));
47
+ CREATE REL TABLE ActedIn (FROM Person TO Movie);
48
+ CREATE (:Person {name: 'Al Pacino', birthDate: '1940-04-25'});
49
+ CREATE (:Person {name: 'Robert De Nero', birthDate: '1943-08-17'});
50
+ CREATE (:Movie {name: 'The Godfather: Part II'});
51
+ MATCH (p:Person), (m:Movie) WHERE p.name = 'Al Pacino' AND m.name = 'The Godfather: Part II' CREATE (p)-[:ActedIn]->(m);
52
+ MATCH (p:Person), (m:Movie) WHERE p.name = 'Robert De Nero' AND m.name = 'The Godfather: Part II' CREATE (p)-[:ActedIn]->(m);
53
+ `)
54
+ } catch (e) {
55
+ console.error("Create DB failed:",e.message);
56
+ }
57
+
58
+ const queryResult = await conn.query("MATCH (p)-[:ActedIn]->(m) RETURN *");
59
+
60
+ // Get all rows from the query result
61
+ const rows = await queryResult.getAll();
62
+
63
+ // Print the rows
64
+ for (const row of rows) {
65
+ console.log(row);
66
+ }
67
+
68
+ })();
69
+ ```
70
+
71
+ ## Compatibility
72
+
73
+ ladybug-lite is tested on:
74
+ - Linux (glibc and musl libc) — x64 and arm64
75
+ - macOS — Apple Silicon (arm64) and Intel (x64). The Intel build is produced from source on a `macos-26-intel` GitHub Actions runner; Apple Silicon uses upstream's prebuilt.
76
+ - Windows — x64
77
+
78
+ ## Contributing
79
+
80
+ Contributions are welcome! Please feel free to submit a Pull Request.
81
+
82
+ ## License
83
+
84
+ This project is licensed under the same license as Ladybug. See [LICENSE](LICENSE) file for details.
package/connection.js ADDED
@@ -0,0 +1,471 @@
1
+ "use strict";
2
+
3
+ const LbugNative = require("./lbug_native.js");
4
+ const QueryResult = require("./query_result.js");
5
+ const PreparedStatement = require("./prepared_statement.js");
6
+
7
+ class Connection {
8
+ /**
9
+ * Initialize a new Connection object. Note that the initialization is done
10
+ * lazily, so the connection is not initialized until the first query is
11
+ * executed. To initialize the connection immediately, call the `init()`
12
+ * function on the returned object.
13
+ *
14
+ * @param {lbug.Database} database the database object to connect to.
15
+ * @param {Number} numThreads the maximum number of threads to use for query execution.
16
+ */
17
+ constructor(database, numThreads = null) {
18
+ if (
19
+ typeof database !== "object" ||
20
+ database.constructor.name !== "Database"
21
+ ) {
22
+ throw new Error("database must be a valid Database object.");
23
+ }
24
+ this._database = database;
25
+ this._connection = null;
26
+ this._isInitialized = false;
27
+ this._initPromise = null;
28
+ this._isClosed = false;
29
+ numThreads = parseInt(numThreads);
30
+ if (numThreads && numThreads > 0) {
31
+ this._numThreads = numThreads;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Initialize the connection. Calling this function is optional, as the
37
+ * connection is initialized automatically when the first query is executed.
38
+ */
39
+ async init() {
40
+ if (this._isClosed) {
41
+ throw new Error("Connection is closed.");
42
+ }
43
+ if (!this._isInitialized) {
44
+ if (!this._initPromise) {
45
+ if (!this._connection) {
46
+ const database = await this._database._getDatabase();
47
+ this._connection = new LbugNative.NodeConnection(database);
48
+ }
49
+ this._initPromise = new Promise((resolve, reject) => {
50
+ this._connection.initAsync((err) => {
51
+ if (err) {
52
+ reject(err);
53
+ } else {
54
+ this._isInitialized = true;
55
+ if (this._numThreads) {
56
+ this._connection.setMaxNumThreadForExec(this._numThreads);
57
+ }
58
+ if (this._queryTimeout) {
59
+ this._connection.setQueryTimeout(this._queryTimeout);
60
+ }
61
+ resolve();
62
+ }
63
+ });
64
+ });
65
+ }
66
+ await this._initPromise;
67
+ this._initPromise = null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Initialize the connection synchronously. Calling this function is optional, as the
73
+ * connection is initialized automatically when the first query is executed. This function
74
+ * may block the main thread, so use it with caution.
75
+ */
76
+ initSync() {
77
+ if (this._isClosed) {
78
+ throw new Error("Connection is closed.");
79
+ }
80
+ if (this._isInitialized) {
81
+ return;
82
+ }
83
+ if (this._initPromise) {
84
+ throw new Error("There is an ongoing asynchronous initialization. Please wait for it to finish.");
85
+ }
86
+ if (!this._connection) {
87
+ const database = this._database._getDatabaseSync();
88
+ this._connection = new LbugNative.NodeConnection(database);
89
+ }
90
+ this._connection.initSync();
91
+ this._isInitialized = true;
92
+ }
93
+
94
+ /**
95
+ * Internal function to get the underlying native connection object.
96
+ * @returns {LbugNative.NodeConnection} the underlying native connection.
97
+ * @throws {Error} if the connection is closed.
98
+ */
99
+ async _getConnection() {
100
+ if (this._isClosed) {
101
+ throw new Error("Connection is closed.");
102
+ }
103
+ await this.init();
104
+ return this._connection;
105
+ }
106
+
107
+ /**
108
+ * Internal function to get the underlying native connection object synchronously.
109
+ * @returns {LbugNative.NodeConnection} the underlying native connection.
110
+ * @throws {Error} if the connection is closed.
111
+ */
112
+ _getConnectionSync() {
113
+ if (this._isClosed) {
114
+ throw new Error("Connection is closed.");
115
+ }
116
+ this.initSync();
117
+ return this._connection;
118
+ }
119
+
120
+ /**
121
+ * Execute a prepared statement with the given parameters.
122
+ * @param {lbug.PreparedStatement} preparedStatement the prepared statement to execute.
123
+ * @param {Object} params a plain object mapping parameter names to values.
124
+ * @param {Function} [progressCallback] - Optional callback function that is invoked with the progress of the query execution. The callback receives three arguments: pipelineProgress, numPipelinesFinished, and numPipelines.
125
+ * @returns {Promise<lbug.QueryResult>} a promise that resolves to the query result. The promise is rejected if there is an error.
126
+ */
127
+ execute(preparedStatement, params = {}, progressCallback) {
128
+ return new Promise((resolve, reject) => {
129
+ if (
130
+ !typeof preparedStatement === "object" ||
131
+ preparedStatement.constructor.name !== "PreparedStatement"
132
+ ) {
133
+ return reject(
134
+ new Error(
135
+ "preparedStatement must be a valid PreparedStatement object."
136
+ )
137
+ );
138
+ }
139
+ if (!preparedStatement.isSuccess()) {
140
+ return reject(new Error(preparedStatement.getErrorMessage()));
141
+ }
142
+ if (params.constructor !== Object) {
143
+ return reject(new Error("params must be a plain object."));
144
+ }
145
+ const paramArray = [];
146
+ for (const key in params) {
147
+ const value = params[key];
148
+ paramArray.push([key, value]);
149
+ }
150
+ if (progressCallback && typeof progressCallback !== "function") {
151
+ return reject(new Error("progressCallback must be a function."));
152
+ }
153
+ this._getConnection()
154
+ .then((connection) => {
155
+ const nodeQueryResult = new LbugNative.NodeQueryResult();
156
+ try {
157
+ connection.executeAsync(
158
+ preparedStatement._preparedStatement,
159
+ nodeQueryResult,
160
+ paramArray,
161
+ (err) => {
162
+ if (err) {
163
+ return reject(err);
164
+ }
165
+ this._unwrapMultipleQueryResults(nodeQueryResult)
166
+ .then((queryResults) => {
167
+ return resolve(queryResults);
168
+ })
169
+ .catch((err) => {
170
+ return reject(err);
171
+ });
172
+ },
173
+ progressCallback
174
+ );
175
+ } catch (e) {
176
+ return reject(e);
177
+ }
178
+ })
179
+ .catch((err) => {
180
+ return reject(err);
181
+ });
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Execute a prepared statement with the given parameters synchronously. This function blocks the main thread for the duration of the query, so use it with caution.
187
+ * @param {lbug.PreparedStatement} preparedStatement the prepared statement
188
+ * @param {Object} params a plain object mapping parameter names to values.
189
+ * @returns {Array<lbug.QueryResult> | lbug.QueryResult} an array of query results. If there is only one query result, the function returns the query result directly.
190
+ * @throws {Error} if there is an error.
191
+ */
192
+ executeSync(preparedStatement, params = {}) {
193
+ if (
194
+ !typeof preparedStatement === "object" ||
195
+ preparedStatement.constructor.name !== "PreparedStatement"
196
+ ) {
197
+ throw new Error("preparedStatement must be a valid PreparedStatement object.");
198
+ }
199
+ if (!preparedStatement.isSuccess()) {
200
+ throw new Error(preparedStatement.getErrorMessage());
201
+ }
202
+ if (params.constructor !== Object) {
203
+ throw new Error("params must be a plain object.");
204
+ }
205
+ const paramArray = [];
206
+ for (const key in params) {
207
+ const value = params[key];
208
+ paramArray.push([key, value]);
209
+ }
210
+ const connection = this._getConnectionSync();
211
+ const nodeQueryResult = new LbugNative.NodeQueryResult();
212
+ connection.executeSync(preparedStatement._preparedStatement, nodeQueryResult, paramArray);
213
+ return this._unwrapMultipleQueryResultsSync(nodeQueryResult);
214
+ }
215
+
216
+ /**
217
+ * Prepare a statement for execution.
218
+ * @param {String} statement the statement to prepare.
219
+ * @returns {Promise<lbug.PreparedStatement>} a promise that resolves to the prepared statement. The promise is rejected if there is an error.
220
+ */
221
+ prepare(statement) {
222
+ return new Promise((resolve, reject) => {
223
+ if (typeof statement !== "string") {
224
+ return reject(new Error("statement must be a string."));
225
+ }
226
+ this._getConnection()
227
+ .then((connection) => {
228
+ const preparedStatement = new LbugNative.NodePreparedStatement(
229
+ connection,
230
+ statement
231
+ );
232
+ preparedStatement.initAsync((err) => {
233
+ if (err) {
234
+ return reject(err);
235
+ }
236
+ return resolve(new PreparedStatement(this, preparedStatement));
237
+ });
238
+ })
239
+ .catch((err) => {
240
+ return reject(err);
241
+ });
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Prepare a statement for execution synchronously. This function blocks the main thread so use it with caution.
247
+ * @param {String} statement the statement to prepare.
248
+ * @returns {lbug.PreparedStatement} the prepared statement.
249
+ * @throws {Error} if there is an error.
250
+ */
251
+ prepareSync(statement) {
252
+ if (typeof statement !== "string") {
253
+ throw new Error("statement must be a string.");
254
+ }
255
+ const connection = this._getConnectionSync();
256
+ const preparedStatement = new LbugNative.NodePreparedStatement(
257
+ connection,
258
+ statement
259
+ );
260
+ preparedStatement.initSync();
261
+ return new PreparedStatement(this, preparedStatement);
262
+ }
263
+
264
+ /**
265
+ * Execute a query.
266
+ * @param {String} statement the statement to execute.
267
+ * @param {Function} [progressCallback] - Optional callback function that is invoked with the progress of the query execution. The callback receives three arguments: pipelineProgress, numPipelinesFinished, and numPipelines.
268
+ * @returns {Promise<lbug.QueryResult>} a promise that resolves to the query result. The promise is rejected if there is an error.
269
+ */
270
+ query(statement, progressCallback) {
271
+ return new Promise((resolve, reject) => {
272
+ if (typeof statement !== "string") {
273
+ return reject(new Error("statement must be a string."));
274
+ }
275
+ if (progressCallback && typeof progressCallback !== "function") {
276
+ return reject(new Error("progressCallback must be a function."));
277
+ }
278
+ this._getConnection()
279
+ .then((connection) => {
280
+ const nodeQueryResult = new LbugNative.NodeQueryResult();
281
+ try {
282
+ connection.queryAsync(statement, nodeQueryResult, (err) => {
283
+ if (err) {
284
+ return reject(err);
285
+ }
286
+ this._unwrapMultipleQueryResults(nodeQueryResult)
287
+ .then((queryResults) => {
288
+ return resolve(queryResults);
289
+ })
290
+ .catch((err) => {
291
+ return reject(err);
292
+ });
293
+ },
294
+ progressCallback);
295
+ } catch (e) {
296
+ return reject(e);
297
+ }
298
+ })
299
+ .catch((err) => {
300
+ return reject(err);
301
+ });
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Execute a query synchronously.
307
+ * @param {String} statement the statement to execute. This function blocks the main thread for the duration of the query, so use it with caution.
308
+ * @returns {Array<lbug.QueryResult> | lbug.QueryResult} an array of query results. If there is only one query result, the function returns the query result directly.
309
+ * @throws {Error} if there is an error.
310
+ * @throws {Error} if the statement is not a string.
311
+ * @throws {Error} if the connection is closed.
312
+ */
313
+ querySync(statement) {
314
+ if (typeof statement !== "string") {
315
+ throw new Error("statement must be a string.");
316
+ }
317
+ const connection = this._getConnectionSync();
318
+ const nodeQueryResult = new LbugNative.NodeQueryResult();
319
+ connection.querySync(statement, nodeQueryResult);
320
+ return this._unwrapMultipleQueryResultsSync(nodeQueryResult);
321
+ }
322
+
323
+ /**
324
+ * Internal function to get the next query result for multiple query results.
325
+ * @param {LbugNative.NodeQueryResult} nodeQueryResult the current node query result.
326
+ * @returns {Promise<lbug.QueryResult>} a promise that resolves to the next query result. The promise is rejected if there is an error.
327
+ */
328
+ _getNextQueryResult(nodeQueryResult) {
329
+ return new Promise((resolve, reject) => {
330
+ nodeQueryResult.getNextQueryResultAsync((err, nextNodeQueryResult) => {
331
+ if (err) {
332
+ return reject(err);
333
+ }
334
+ if (!nextNodeQueryResult) {
335
+ return resolve(undefined);
336
+ }
337
+ return resolve(new QueryResult(this, nextNodeQueryResult));
338
+ });
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Internal function to unwrap multiple query results into an array of query results.
344
+ * @param {LbugNative.NodeQueryResult} nodeQueryResult the node query result.
345
+ * @returns {Promise<Array<lbug.QueryResult>> | lbug.QueryResult} a promise that resolves to an array of query results. The promise is rejected if there is an error.
346
+ */
347
+ async _unwrapMultipleQueryResults(nodeQueryResult) {
348
+ const wrappedQueryResult = new QueryResult(this, nodeQueryResult);
349
+ if (!nodeQueryResult.hasNextQueryResult()) {
350
+ return wrappedQueryResult;
351
+ }
352
+ const queryResults = [wrappedQueryResult];
353
+ let currentQueryResult = nodeQueryResult;
354
+ while (currentQueryResult.hasNextQueryResult()) {
355
+ queryResults.push(await this._getNextQueryResult(currentQueryResult));
356
+ currentQueryResult = queryResults[queryResults.length - 1]._queryResult;
357
+ }
358
+ return queryResults;
359
+ }
360
+
361
+ /**
362
+ * Internal function to unwrap multiple query results into an array of query results synchronously.
363
+ * @param {LbugNative.NodeQueryResult} nodeQueryResult the node query result.
364
+ * @returns {Array<lbug.QueryResult> | lbug.QueryResult} an array of query results.
365
+ * @throws {Error} if there is an error.
366
+ */
367
+ _unwrapMultipleQueryResultsSync(nodeQueryResult) {
368
+ const wrappedQueryResult = new QueryResult(this, nodeQueryResult);
369
+ if (!nodeQueryResult.hasNextQueryResult()) {
370
+ return wrappedQueryResult;
371
+ }
372
+ const queryResults = [wrappedQueryResult];
373
+ let currentQueryResult = nodeQueryResult;
374
+ while (currentQueryResult.hasNextQueryResult()) {
375
+ const nextNodeQueryResult = currentQueryResult.getNextQueryResultSync();
376
+ const nextQueryResult = new QueryResult(this, nextNodeQueryResult);
377
+ queryResults.push(nextQueryResult);
378
+ currentQueryResult = nextNodeQueryResult;
379
+ }
380
+ return queryResults;
381
+ }
382
+
383
+ /**
384
+ * Set the maximum number of threads to use for query execution.
385
+ * @param {Number} numThreads the maximum number of threads to use for query execution.
386
+ */
387
+ setMaxNumThreadForExec(numThreads) {
388
+ // If the connection is not initialized yet, store the logging level
389
+ // and defer setting it until the connection is initialized.
390
+ if (typeof numThreads !== "number" || !numThreads || numThreads < 0) {
391
+ throw new Error("numThreads must be a positive number.");
392
+ }
393
+ if (this._isInitialized) {
394
+ this._connection.setMaxNumThreadForExec(numThreads);
395
+ } else {
396
+ this._numThreads = numThreads;
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Set the timeout for queries. Queries that take longer than the timeout
402
+ * will be aborted.
403
+ * @param {Number} timeoutInMs the timeout in milliseconds.
404
+ */
405
+ setQueryTimeout(timeoutInMs) {
406
+ if (
407
+ typeof timeoutInMs !== "number" ||
408
+ isNaN(timeoutInMs) ||
409
+ timeoutInMs <= 0
410
+ ) {
411
+ throw new Error("timeoutInMs must be a positive number.");
412
+ }
413
+ if (this._isInitialized) {
414
+ this._connection.setQueryTimeout(timeoutInMs);
415
+ } else {
416
+ this._queryTimeout = timeoutInMs;
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Close the connection.
422
+ *
423
+ * Note: Call to this method is optional. The connection will be closed
424
+ * automatically when the object goes out of scope.
425
+ */
426
+ async close() {
427
+ if (this._isClosed) {
428
+ return;
429
+ }
430
+ if (!this._isInitialized) {
431
+ if (this._initPromise) {
432
+ // Connection is initializing, wait for it to finish first.
433
+ await this._initPromise;
434
+ } else {
435
+ // Connection is not initialized, simply mark it as closed and initialized.
436
+ this._isInitialized = true;
437
+ this._isClosed = true;
438
+ delete this._connection;
439
+ return;
440
+ }
441
+ }
442
+ // Connection is initialized, close it.
443
+ this._connection.close();
444
+ delete this._connection;
445
+ this._isClosed = true;
446
+ }
447
+
448
+ /**
449
+ * Close the connection synchronously.
450
+ * @throws {Error} if there is an undergoing asynchronous initialization.
451
+ */
452
+ closeSync() {
453
+ if (this._isClosed) {
454
+ return;
455
+ }
456
+ if (!this._isInitialized) {
457
+ if (this._initPromise) {
458
+ throw new Error("There is an ongoing asynchronous initialization. Please wait for it to finish.");
459
+ }
460
+ this._isInitialized = true;
461
+ this._isClosed = true;
462
+ delete this._connection;
463
+ return;
464
+ }
465
+ this._connection.close();
466
+ delete this._connection;
467
+ this._isClosed = true;
468
+ }
469
+ }
470
+
471
+ module.exports = Connection;