jsql-neo 6.5.9 → 6.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
4
4
  > MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
5
5
 
6
- > **v6.5.9** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.10** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.9 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.10 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/sql.js CHANGED
@@ -1079,6 +1079,22 @@ class Parser {
1079
1079
  }
1080
1080
 
1081
1081
  parseValueAtom() {
1082
+ // MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
1083
+ // 仅当后面确实是值起始 token 才当作运算符,避免误吞裸列名 `binary`
1084
+ // (`SELECT binary FROM t` / `WHERE binary = 1` 仍按列名解析)。
1085
+ if (this.isWord('BINARY')) {
1086
+ const nt = this.peek(1);
1087
+ const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
1088
+ (nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
1089
+ (nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
1090
+ if (startVal) {
1091
+ this.next(); // 消费 BINARY
1092
+ const lt = this.peek();
1093
+ if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
1094
+ const operand = this.parseValue();
1095
+ return { type: 'binary', operand };
1096
+ }
1097
+ }
1082
1098
  const t = this.next();
1083
1099
  if (t.type === 'number' || t.type === 'string') return t.value;
1084
1100
  // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col) —— 引用"本次本应插入的值"
@@ -1583,9 +1599,30 @@ class Parser {
1583
1599
  let where = null;
1584
1600
  if (this.isKeyword('WHERE')) { this.next(); where = this.parseExpr(); }
1585
1601
  else console.warn(`⚠️ UPDATE without WHERE clause — this will modify ALL rows in '${table}'`);
1602
+ // UPDATE ... ORDER BY ... LIMIT n(与 DELETE 对称;按排序后的前 n 行更新)
1603
+ let orderBy = null, limit = null;
1604
+ if (this.isKeyword('ORDER')) {
1605
+ this.next(); this.expectKeyword('BY');
1606
+ orderBy = [];
1607
+ for (;;) {
1608
+ const col = this.parseColumnRef();
1609
+ let dir = 'asc';
1610
+ if (this.isKeyword('ASC')) this.next();
1611
+ else if (this.isKeyword('DESC')) { this.next(); dir = 'desc'; }
1612
+ orderBy.push({ column: col, dir });
1613
+ if (this.peek().type === 'op' && this.peek().value === ',') { this.next(); continue; }
1614
+ break;
1615
+ }
1616
+ }
1617
+ if (this.isKeyword('LIMIT')) {
1618
+ this.next();
1619
+ limit = this.parseValue();
1620
+ if (typeof limit === 'bigint') limit = Number(limit <= BigInt(Number.MAX_SAFE_INTEGER) ? limit : BigInt(Number.MAX_SAFE_INTEGER));
1621
+ if (typeof limit === 'number' && limit < 0) throw new Error(`LIMIT must be a non-negative integer, got ${limit}`);
1622
+ }
1586
1623
  const returning = this.parseOptionalReturning();
1587
1624
  this.optionalTailSemicolon();
1588
- return { type: 'update', table, assignments, where, returning };
1625
+ return { type: 'update', table, assignments, where, orderBy, limit, returning };
1589
1626
  }
1590
1627
 
1591
1628
  parseDelete() {
@@ -2191,6 +2228,37 @@ class Parser {
2191
2228
  }
2192
2229
 
2193
2230
  parseOperand() {
2231
+ // MySQL 前缀运算符:BINARY expr —— 强制大小写敏感比较。
2232
+ if (this.isWord('BINARY')) {
2233
+ const nt = this.peek(1);
2234
+ const startVal = nt && (nt.type === 'number' || nt.type === 'string' || nt.type === 'ident' || nt.type === 'param' ||
2235
+ (nt.type === 'op' && (nt.value === '(' || nt.value === '-' || nt.value === '+')) ||
2236
+ (nt.type === 'keyword' && (nt.value === 'NULL' || nt.value === 'TRUE' || nt.value === 'FALSE' || nt.value === 'DEFAULT')));
2237
+ if (startVal) {
2238
+ this.next(); // 消费 BINARY
2239
+ // 字面量要包成 literal,否则 resolveOperand 把 'ABC' 当列名
2240
+ const lt = this.peek();
2241
+ if (lt.type === 'string' || lt.type === 'number') { const lit = this.next(); return { type: 'binary', operand: { type: 'literal', value: lit.value } }; }
2242
+ const operand = this.parseValue();
2243
+ return { type: 'binary', operand };
2244
+ }
2245
+ }
2246
+ // MySQL 区间字面量:INTERVAL n UNIT(date + INTERVAL 3 DAY / date - INTERVAL 1 MONTH)
2247
+ if (this.isWord('INTERVAL')) {
2248
+ this.next();
2249
+ const nTok = this.peek();
2250
+ let nVal;
2251
+ if (nTok.type === 'op' && (nTok.value === '-' || nTok.value === '+')) {
2252
+ this.next();
2253
+ const numTok = this.next();
2254
+ nVal = (nTok.value === '-' ? -1 : 1) * Number(numTok.value);
2255
+ } else {
2256
+ this.next();
2257
+ nVal = Number(nTok.value);
2258
+ }
2259
+ const unitTok = this.next();
2260
+ return { type: 'interval', value: nVal, unit: String(unitTok.value).toUpperCase() };
2261
+ }
2194
2262
  const t = this.next();
2195
2263
  // P1-20: SELECT -col 报错——parseOperand 不处理负号
2196
2264
  if (t.type === 'op' && t.value === '-') {
@@ -2871,6 +2939,8 @@ function resolveOperand(operand, row, ctx) {
2871
2939
  case 'value':
2872
2940
  case 'literal':
2873
2941
  return operand.value;
2942
+ case 'binary':
2943
+ return resolveOperand(operand.operand, row, ctx);
2874
2944
  case 'sysvar': {
2875
2945
  const s = (ctx && ctx.session && ctx.session.sysvars) || {};
2876
2946
  const name = String(operand.name).toLowerCase();
@@ -2909,6 +2979,32 @@ function resolveOperand(operand, row, ctx) {
2909
2979
  return undefined;
2910
2980
  }
2911
2981
  case 'arith': {
2982
+ // date + INTERVAL n UNIT / date - INTERVAL n UNIT(MySQL 区间算术)
2983
+ const lIv = operand.left && operand.left.type === 'interval';
2984
+ const rIv = operand.right && operand.right.type === 'interval';
2985
+ if ((operand.op === '+' || operand.op === '-') && (lIv || rIv)) {
2986
+ if (lIv && operand.op === '-') return null; // INTERVAL - date 无意义
2987
+ const ivNode = lIv ? operand.left : operand.right;
2988
+ const dateSide = lIv ? resolveOperand(operand.right, row, ctx) : resolveOperand(operand.left, row, ctx);
2989
+ const base = parseSqlDate(dateSide);
2990
+ if (!base) return null;
2991
+ const sign = operand.op === '-' ? -1 : 1;
2992
+ const isDateOnly = String(dateSide).trim().length <= 10;
2993
+ const d = new Date(base.getTime());
2994
+ const n = sign * Number(ivNode.value || 0);
2995
+ switch (String(ivNode.unit || '').toUpperCase()) {
2996
+ case 'DAY': case 'DAYS': d.setDate(d.getDate() + n); break;
2997
+ case 'WEEK': case 'WEEKS': d.setDate(d.getDate() + n * 7); break;
2998
+ case 'MONTH': case 'MONTHS': addMonthsClamped(d, n); break;
2999
+ case 'QUARTER': addMonthsClamped(d, n * 3); break;
3000
+ case 'YEAR': case 'YEARS': addMonthsClamped(d, n * 12); break;
3001
+ case 'HOUR': case 'HOURS': d.setHours(d.getHours() + n); break;
3002
+ case 'MINUTE': case 'MINUTES': d.setMinutes(d.getMinutes() + n); break;
3003
+ case 'SECOND': case 'SECONDS': d.setSeconds(d.getSeconds() + n); break;
3004
+ default: return null;
3005
+ }
3006
+ return fmtSqlDate(isDateOnly, d);
3007
+ }
2912
3008
  const l = resolveOperand(operand.left, row, ctx);
2913
3009
  const r = resolveOperand(operand.right, row, ctx);
2914
3010
  if (l === null || r === null || l === undefined || r === undefined) return null;
@@ -4313,6 +4409,19 @@ function normalizeCmpValue(a, other) {
4313
4409
  return otherIsNumeric ? (a ? 1 : 0) : a;
4314
4410
  }
4315
4411
 
4412
+ // MySQL 默认 collation(utf8mb4_general_ci)下字符串比较不区分大小写,
4413
+ // 与 LIKE 保持一致。cs=true 表示显式 BINARY/COLLATE 强制大小写敏感。
4414
+ function ciEq(a, b, cs) {
4415
+ if (cs) return a === b;
4416
+ if (typeof a === 'string' && typeof b === 'string') return a.toLowerCase() === b.toLowerCase();
4417
+ return String(a) === String(b);
4418
+ }
4419
+ function ciCompare(a, b, cs) {
4420
+ if (cs) return a === b ? 0 : (a < b ? -1 : 1);
4421
+ const as = String(a).toLowerCase(), bs = String(b).toLowerCase();
4422
+ return as === bs ? 0 : (as < bs ? -1 : 1);
4423
+ }
4424
+
4316
4425
  function evaluateExpr(expr, row, ctx) {
4317
4426
  if (expr === null || expr === undefined) return false;
4318
4427
  switch (expr.type) {
@@ -4329,12 +4438,14 @@ function evaluateExpr(expr, row, ctx) {
4329
4438
  case 'compare': {
4330
4439
  const l = resolveOperand(expr.left, row, ctx);
4331
4440
  const r = resolveOperand(expr.right, row, ctx);
4441
+ // BINARY 限定符:任一侧是 BINARY 操作数即强制大小写敏感
4442
+ const cs = !!(expr.cs || (expr.left && expr.left.type === 'binary') || (expr.right && expr.right.type === 'binary'));
4332
4443
  // MySQL NULL 安全相等:NULL <=> NULL 为真;NULL <=> 值 为假
4333
4444
  if (expr.op === '<=>') {
4334
4445
  const ln = l === null || l === undefined;
4335
4446
  const rn = r === null || r === undefined;
4336
4447
  if (ln || rn) return ln && rn;
4337
- return l === r || String(l) === String(r);
4448
+ return ciEq(l, r, cs);
4338
4449
  }
4339
4450
  // SQL 标准:与 NULL 的比较结果是 UNKNOWN,这里用 null 表示。
4340
4451
  // 不能返回 false —— 否则 NOT (a > 1) 会把 a 为 NULL 的行错误纳入结果。
@@ -4349,11 +4460,19 @@ function evaluateExpr(expr, row, ctx) {
4349
4460
  const mixedNum = (isNum(nl) && isNumStr(nr)) || (isNumStr(nl) && isNum(nr));
4350
4461
  if (expr.op === '=') {
4351
4462
  if (mixedNum) return Number(nl) === Number(nr);
4352
- return nl === nr || String(nl) === String(nr);
4463
+ return ciEq(nl, nr, cs);
4464
+ }
4465
+ if (mixedNum) { const fn = OPERATORS[expr.op]; return fn(Number(nl), Number(nr)); }
4466
+ if (typeof nl === 'number' && typeof nr === 'number') { const fn = OPERATORS[expr.op]; return fn(nl, nr); }
4467
+ const c = ciCompare(nl, nr, cs);
4468
+ switch (expr.op) {
4469
+ case '!=': case '<>': return c !== 0;
4470
+ case '<': return c < 0;
4471
+ case '<=': return c <= 0;
4472
+ case '>': return c > 0;
4473
+ case '>=': return c >= 0;
4474
+ default: return c === 0;
4353
4475
  }
4354
- const fn = OPERATORS[expr.op];
4355
- if (mixedNum) return fn(Number(nl), Number(nr));
4356
- return typeof nl === 'number' && typeof nr === 'number' ? fn(nl, nr) : fn(String(nl), String(nr));
4357
4476
  }
4358
4477
  case 'isNull': {
4359
4478
  const v = resolveOperand(expr.operand, row, ctx);
@@ -4365,6 +4484,8 @@ function evaluateExpr(expr, row, ctx) {
4365
4484
  if (v === null || v === undefined) return null; // NULL IN (...) → NULL(三值逻辑)
4366
4485
  if (expr.subquery && expr.subquery._values !== undefined) expr.list = expr.subquery._values;
4367
4486
  if (!expr.list) return expr.not ? true : false;
4487
+ const cs = !!(expr.cs || (expr.operand && expr.operand.type === 'binary') ||
4488
+ (expr.list && expr.list.some(x => x && x.type === 'binary')));
4368
4489
 
4369
4490
  // 先找匹配:只要有一个非 NULL 元素等于 v,整个 IN 就是 TRUE ——
4370
4491
  // 列表里别的位置有没有 NULL 都不影响。
@@ -4372,9 +4493,12 @@ function evaluateExpr(expr, row, ctx) {
4372
4493
  // 此前是先判「列表里有 NULL 就返回 NULL」,于是 `v IN (10, NULL)`
4373
4494
  // 连 v=10 的行都匹配不上(实测返回 0 行,应为 2 行)。
4374
4495
  // 三值逻辑里 NULL 只在**没匹配上**的时候才让结果变 UNKNOWN。
4375
- const matched = expr.list.some(x =>
4376
- x !== null && x !== undefined && (x === v || String(x) === String(v))
4377
- );
4496
+ const matched = expr.list.some(x => {
4497
+ // list 元素可能是表达式节点(如 IN (BINARY 'ABC')),需先求值;
4498
+ // 原始字面量('ABC' / 10)直接比较,不能走 resolveOperand(会把字符串当列名)。
4499
+ const xv = (x && typeof x === 'object') ? resolveOperand(x, row, ctx) : x;
4500
+ return xv !== null && xv !== undefined && (xv === v || ciEq(xv, v, cs));
4501
+ });
4378
4502
  if (matched) return expr.not ? false : true;
4379
4503
 
4380
4504
  // 没匹配上:列表里有 NULL → UNKNOWN,否则是确定的 FALSE
@@ -5255,22 +5379,38 @@ class SQLExecutor {
5255
5379
  throw createError('ER_PARSE_ERROR', `Table '${statement.table}' has no primary key, UPDATE requires a primary key`);
5256
5380
  }
5257
5381
  const all = (await this.engine.find(statement.table, {}, { limit: 1e9, offset: 0 })).map(r => normalizeRow(r, schema));
5382
+ // UPDATE ... ORDER BY ... LIMIT n:先过滤、排序、截取,再逐行更新
5383
+ let candidates = all.filter(row => !statement.where || evaluateExpr(statement.where, row, this.ctx));
5384
+ if (statement.orderBy) {
5385
+ const cmp = (a, b) => {
5386
+ for (const o of statement.orderBy) {
5387
+ const av = resolveOperand({ type: 'column', name: o.column }, a, this.ctx);
5388
+ const bv = resolveOperand({ type: 'column', name: o.column }, b, this.ctx);
5389
+ if (av === bv) continue;
5390
+ if (av === undefined || av === null) return o.dir === 'asc' ? -1 : 1;
5391
+ if (bv === undefined || bv === null) return o.dir === 'asc' ? 1 : -1;
5392
+ const d = typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv));
5393
+ if (d !== 0) return o.dir === 'asc' ? d : -d;
5394
+ }
5395
+ return 0;
5396
+ };
5397
+ candidates = candidates.slice().sort(cmp);
5398
+ }
5399
+ if (typeof statement.limit === 'number') candidates = candidates.slice(0, statement.limit);
5258
5400
  let count = 0;
5259
- for (const row of all) {
5260
- if (!statement.where || evaluateExpr(statement.where, row, this.ctx)) {
5261
- const id = this._rowPkId(row, pkCols);
5262
- if (id !== undefined) {
5263
- const data = {};
5264
- for (const [col, val] of statement.assignments) {
5265
- data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
5266
- }
5267
- // 必须 await:wasm / native client 的 updateById 是异步的,
5268
- // 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
5269
- // 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
5270
- // lib/database.js 的实现是同步的,await 同步值同样安全。
5271
- await this.engine.updateById(statement.table, id, data);
5272
- count++;
5401
+ for (const row of candidates) {
5402
+ const id = this._rowPkId(row, pkCols);
5403
+ if (id !== undefined) {
5404
+ const data = {};
5405
+ for (const [col, val] of statement.assignments) {
5406
+ data[col] = typeof val === 'object' && val !== null && val.type ? resolveOperand(val, row, this.ctx) : val;
5273
5407
  }
5408
+ // 必须 await:wasm / native client 的 updateById 是异步的,
5409
+ // 不等就会把「主键/唯一约束冲突」的异常变成未处理的 Promise 拒绝 ——
5410
+ // 表现为 UPDATE 静默成功、产生两行同主键,而调用方拿不到任何错误。
5411
+ // lib/database.js 的实现是同步的,await 同步值同样安全。
5412
+ await this.engine.updateById(statement.table, id, data);
5413
+ count++;
5274
5414
  }
5275
5415
  }
5276
5416
  await this.engine.flush();
@@ -5670,17 +5810,26 @@ class SQLExecutor {
5670
5810
  // - 带表限定的条件(如 "o.amount"):检查表名/别名是否匹配
5671
5811
  // - 不带表限定的条件(如 "amount"):检查当前表的 schema 是否包含该列
5672
5812
  const filter = {};
5813
+ // 字符串列默认 collation 为 CI(大小写不敏感),而存储层 find 的等值
5814
+ // 过滤是严格比较(CS)。字符串等值若下推会被 CS 预筛,绕过 evaluateExpr 的
5815
+ // ciEq,导致 `code = 'abc'` 匹配不到 'ABC'。所以字符串/JSON 列不参与下推,
5816
+ // 交给 evaluateExpr 按 CI 求值;数值/布尔仍下推走索引。
5817
+ const isNonPushdownCol = (d) => {
5818
+ const t = String((d && d.type) || '').toLowerCase();
5819
+ return t === 'text' || t === 'varchar' || t === 'char' || t === 'string' ||
5820
+ t === 'blob' || t === 'clob' || t === 'json' || t === 'object' || t === 'array';
5821
+ };
5673
5822
  for (const [key, val] of Object.entries(allFilters)) {
5674
5823
  if (key.includes('.')) {
5675
5824
  // 带表限定:检查是否匹配当前表的别名或表名
5676
5825
  const tablePart = key.slice(0, key.lastIndexOf('.'));
5677
5826
  if (tablePart === alias || tablePart === table) {
5678
5827
  const colPart = key.slice(key.lastIndexOf('.') + 1);
5679
- if (schema[colPart]) filter[colPart] = this._coerceValue(val, schema[colPart]);
5828
+ if (schema[colPart] && !isNonPushdownCol(schema[colPart])) filter[colPart] = this._coerceValue(val, schema[colPart]);
5680
5829
  }
5681
5830
  } else {
5682
5831
  // 不带表限定:只下推到持有该列的表
5683
- if (schema[key]) filter[key] = this._coerceValue(val, schema[key]);
5832
+ if (schema[key] && !isNonPushdownCol(schema[key])) filter[key] = this._coerceValue(val, schema[key]);
5684
5833
  }
5685
5834
  }
5686
5835
  const useIndex = Object.keys(filter).length > 0 &&
@@ -7208,24 +7357,42 @@ class SQLExecutor {
7208
7357
  * 同时按「投影列名 → 下标」解析;命中就直接 `row[idx]`,
7209
7358
  * 不再走 resolveOperand 的通用路径。
7210
7359
  */
7360
+ // 把 ORDER BY 引用的输出列(别名或位置序号)解析回该列的**底层取值**。
7361
+ // 非分组路径的行对象键是底层列名(如 'v'),不是别名('val'),
7362
+ // 直接 r['val'] 会取到 undefined,导致 ORDER BY 静默不排序。
7363
+ const resolveByColumn = (c) => {
7364
+ if (!c) return (r) => null;
7365
+ if (typeof c.expr === 'string' && c.expr !== '*') {
7366
+ const name = c.expr;
7367
+ const node = { type: 'column', name };
7368
+ return (r) => {
7369
+ const v = (r == null) ? undefined : r[name];
7370
+ return v !== undefined ? v : resolveOperand(node, r, this.ctx);
7371
+ };
7372
+ }
7373
+ if (c.scalar) return (r) => resolveOperand(c.scalar, r, this.ctx);
7374
+ if (c.aggregate) return (r) => this._aggValue([r], c.aggregate, c.column, c.distinct, c.separator, c.orderBy);
7375
+ const nm = c.alias;
7376
+ return (r) => (r == null ? null : r[nm]);
7377
+ };
7211
7378
  const ordKeys = statement.orderBy.map(o => {
7212
7379
  const dir = o.dir === 'asc' ? 1 : -1;
7213
- // 位置序号:ORDER BY 1
7380
+ // 位置序号:ORDER BY 1(1-based,指第 N 个输出列)
7214
7381
  const pos = (o.column && typeof o.column === 'object' && o.column.type === 'value' && typeof o.column.value === 'number')
7215
7382
  ? o.column.value
7216
7383
  : (typeof o.column === 'number' ? o.column : null);
7217
7384
  if (pos !== null && pos !== undefined) {
7218
- const name = ordNames[pos - 1];
7219
- return { dir, at: (name === undefined || name === null) ? () => null : (r) => (r == null ? null : r[name]) };
7385
+ return { dir, at: resolveByColumn(statement.columns[pos - 1]) };
7220
7386
  }
7221
7387
  if (typeof o.column === 'string') {
7222
7388
  const f = o.column;
7223
- // 节点对象提到循环外复用 —— 此前每次比较都新建一个 {type:'column',name}
7389
+ // 命中某个输出列的别名/列名 → 用该列的底层取值(修复别名排序)
7390
+ const idx = ordNames.findIndex(n => n === f);
7391
+ if (idx >= 0) return { dir, at: resolveByColumn(statement.columns[idx]) };
7224
7392
  const node = { type: 'column', name: f };
7225
7393
  return {
7226
7394
  dir,
7227
7395
  at: (r) => {
7228
- // 先按字段名直取;取不到再走 resolveOperand(点号路径、表达式等)
7229
7396
  const v = (r == null) ? undefined : r[f];
7230
7397
  return v !== undefined ? v : resolveOperand(node, r, this.ctx);
7231
7398
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.9",
3
+ "version": "6.5.10",
4
4
  "description": "JSQL-NEO \u2014 Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",