jsql-neo 4.1.1 → 4.2.0-beta.1

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/lib/database.js CHANGED
@@ -92,8 +92,8 @@ class Database {
92
92
  this._walPath = this._filePath ? this._filePath + '.wal' : null;
93
93
  this._walOps = []; // WAL 操作日志
94
94
 
95
- // 事务隔离
96
- this._isolationLevel = options.isolationLevel || 'READ_COMMITTED';
95
+ // 事务隔离:默认 REPEATABLE_READ(begin 时保存行快照,rollback 可回滚)
96
+ this._isolationLevel = options.isolationLevel || 'REPEATABLE_READ';
97
97
 
98
98
  // JSql 格式 (binary block-based)
99
99
  this._jsqlMode = this._filePath && this._filePath.endsWith('.jsql');
@@ -1286,6 +1286,49 @@ class Database {
1286
1286
  return { ok: true, count: removed };
1287
1287
  }
1288
1288
 
1289
+ insertMany(tableName, items) {
1290
+ const table = this._ensureTable(tableName);
1291
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1292
+ const arr = Array.isArray(items) ? items : [items];
1293
+ if (!this._runHooks('beforeInsert', [tableName, arr])) return [];
1294
+ const inserted = table.insertMany(arr);
1295
+ this._emit('insert', { table: tableName, count: arr.length });
1296
+ this._runHooks('afterInsert', [tableName, arr, inserted]);
1297
+ this._markDirty(tableName);
1298
+ return inserted;
1299
+ }
1300
+
1301
+ update(tableName, query, updates) {
1302
+ const table = this._ensureTable(tableName);
1303
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1304
+ if (!this._runHooks('beforeUpdate', [tableName, query, updates])) return 0;
1305
+ const count = table.update(query, updates);
1306
+ this._emit('update', { table: tableName, query, count });
1307
+ this._runHooks('afterUpdate', [tableName, query, updates, count]);
1308
+ this._markDirty(tableName);
1309
+ return count;
1310
+ }
1311
+
1312
+ delete(tableName, query = {}) {
1313
+ const table = this._ensureTable(tableName);
1314
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1315
+ if (!this._runHooks('beforeDelete', [tableName, query])) return 0;
1316
+ const count = table.remove(query);
1317
+ this._emit('delete', { table: tableName, query, count });
1318
+ this._runHooks('afterDelete', [tableName, query, count]);
1319
+ this._markDirty(tableName);
1320
+ return count;
1321
+ }
1322
+
1323
+ truncate(tableName) {
1324
+ const table = this._ensureTable(tableName);
1325
+ if (!table) throw createError('ER_NO_SUCH_TABLE', tableName);
1326
+ const count = table.truncate();
1327
+ this._emit('delete', { table: tableName, count });
1328
+ this._markDirty(tableName);
1329
+ return count;
1330
+ }
1331
+
1289
1332
  async stop() {
1290
1333
  this._runHooks('onStop', []);
1291
1334
  this._emit('stop', {});
@@ -458,6 +458,7 @@ class MysqlConnection {
458
458
  return;
459
459
  }
460
460
  try {
461
+ if (!this.server._dbExists(db)) await this.server.createDatabase(db);
461
462
  await this.server._getDatabase(db);
462
463
  this.currentDb = db;
463
464
  } catch (e) {
@@ -525,8 +526,10 @@ class MysqlConnection {
525
526
  default:
526
527
  break;
527
528
  }
528
- const engine = await this.server._getEngine(this.currentDb);
529
- const r = await executeSQL(engine, stmtSql, {
529
+ const routed = await this.server._route(stmt, stmtSql, this.currentDb, this.user);
530
+ const engine = routed.engine;
531
+ const sqlToRun = routed.sql;
532
+ const r = await executeSQL(engine, sqlToRun, {
530
533
  allowComments: this.server.allowComments,
531
534
  safety: this.server.safety,
532
535
  maxStatements: 1,
@@ -571,6 +574,7 @@ class MysqlServer {
571
574
  this._ownEngine = false;
572
575
  this._databases = new Map();
573
576
  this._dbDir = options.dataDir ? path.resolve(options.dataDir) : null;
577
+ this._defaultDbName = options.defaultDatabase || 'default';
574
578
  this._connectionCounter = 0;
575
579
  this._sockets = new Set();
576
580
  this._securityHandler = typeof options.onSecurityEvent === 'function' ? options.onSecurityEvent : null;
@@ -585,9 +589,7 @@ class MysqlServer {
585
589
  async _getEngine(dbName) {
586
590
  if (dbName) return this._getDatabase(dbName);
587
591
  if (this._dbDir) {
588
- const err = new Error(`No database selected`);
589
- err.code = 1046;
590
- throw err;
592
+ return this._getDatabase(this._defaultDbName, { autoCreate: true });
591
593
  }
592
594
  if (this._engine) return this._engine;
593
595
  if (this.options.engine || this.options.database) {
@@ -601,6 +603,64 @@ class MysqlServer {
601
603
  return this._engine;
602
604
  }
603
605
 
606
+ _dbExists(name) {
607
+ if (this._databases.has(name)) return true;
608
+ const dir = this._dbPath(name);
609
+ return !!(dir && fs.existsSync(dir));
610
+ }
611
+
612
+ // 语句路由:解析库前缀(db.table)与 SHOW TABLES FROM db,
613
+ // 决定执行引擎,并把 db.table 改写为 table 后交给对应库引擎执行。
614
+ async _route(stmt, sql, currentDb, user) {
615
+ if (stmt.type === 'showTables' && stmt.database) {
616
+ return { engine: await this._getDatabase(stmt.database), sql };
617
+ }
618
+ const tables = [];
619
+ const collect = (s) => {
620
+ if (!s) return;
621
+ if (s.type === 'createTable') tables.push(s.name);
622
+ else if (s.type === 'dropTable') tables.push(s.table);
623
+ else if (s.type === 'insert') tables.push(s.name);
624
+ else if (s.type === 'update') tables.push(s.table);
625
+ else if (s.type === 'delete') tables.push(s.table);
626
+ else if (s.type === 'describe') tables.push(s.table);
627
+ else if (s.type === 'select' && s.from) {
628
+ for (const t of s.from.tables) tables.push(t.table);
629
+ for (const j of s.from.joins) tables.push(j.item.table);
630
+ }
631
+ };
632
+ collect(stmt);
633
+ let db = null;
634
+ for (const t of tables) {
635
+ if (t && t.indexOf('.') !== -1) {
636
+ const d = t.split('.')[0];
637
+ if (db && d !== db) {
638
+ const err = new Error(`Cross-database references are not supported in one statement`);
639
+ err.code = 1105;
640
+ throw err;
641
+ }
642
+ db = d;
643
+ }
644
+ }
645
+ if (db) {
646
+ if (!this._dbExists(db)) await this.createDatabase(db);
647
+ const engine = await this._getDatabase(db);
648
+ let s = sql;
649
+ for (const t of tables) {
650
+ if (t && t.indexOf('.') !== -1) {
651
+ s = s.replace(new RegExp('\\b' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'g'), t.split('.')[1]);
652
+ }
653
+ }
654
+ return { engine, sql: s };
655
+ }
656
+ if (this._dbDir && !currentDb && this.auth && !this._canAccessDb(user, this._defaultDbName)) {
657
+ const err = new Error('No database selected');
658
+ err.code = 1046;
659
+ throw err;
660
+ }
661
+ return { engine: await this._getEngine(currentDb), sql };
662
+ }
663
+
604
664
  _safeDbName(name) {
605
665
  if (typeof name !== 'string' || name.length === 0 || name.length > 64) return false;
606
666
  if (!/^[a-zA-Z0-9_$.\-]+$/.test(name)) return false;
@@ -614,7 +674,7 @@ class MysqlServer {
614
674
  return path.join(this._dbDir, name);
615
675
  }
616
676
 
617
- async _getDatabase(name) {
677
+ async _getDatabase(name, opts = {}) {
618
678
  if (!this._safeDbName(name)) {
619
679
  const err = new Error(`Unknown database '${name}'`);
620
680
  err.code = 1049;
@@ -623,9 +683,13 @@ class MysqlServer {
623
683
  if (this._databases.has(name)) return this._databases.get(name);
624
684
  const dir = this._dbPath(name);
625
685
  if (dir && !fs.existsSync(dir)) {
626
- const err = new Error(`Unknown database '${name}'`);
627
- err.code = 1049;
628
- throw err;
686
+ if (opts.autoCreate) {
687
+ fs.mkdirSync(dir, { recursive: true });
688
+ } else {
689
+ const err = new Error(`Unknown database '${name}'`);
690
+ err.code = 1049;
691
+ throw err;
692
+ }
629
693
  }
630
694
  const engine = dir ? new Database(dir, { mode: 'hybrid' }) : new Database(':memory:');
631
695
  if (typeof engine.start === 'function') await engine.start();
package/lib/sql.js CHANGED
@@ -227,6 +227,12 @@ class Parser {
227
227
  parseTableName() {
228
228
  const t = this.next();
229
229
  if (t.type !== 'ident') throw new Error(`Expected table name, got '${t.value}'`);
230
+ if (this.peek().type === 'op' && this.peek().value === '.') {
231
+ this.next();
232
+ const t2 = this.next();
233
+ if (t2.type !== 'ident') throw new Error(`Expected table name after '.', got '${t2.value}'`);
234
+ return t.value + '.' + t2.value;
235
+ }
230
236
  return t.value;
231
237
  }
232
238
 
@@ -668,7 +674,13 @@ class Parser {
668
674
 
669
675
  parseShow() {
670
676
  this.expectKeyword('SHOW');
671
- if (this.isKeyword('TABLES')) { this.next(); this.optionalTailSemicolon(); return { type: 'showTables' }; }
677
+ if (this.isKeyword('TABLES')) {
678
+ this.next();
679
+ let database = null;
680
+ if (this.isKeyword('FROM')) { this.next(); database = this.parseTableName(); }
681
+ this.optionalTailSemicolon();
682
+ return { type: 'showTables', database };
683
+ }
672
684
  if (this.isKeyword('DATABASES')) { this.next(); this.optionalTailSemicolon(); return { type: 'showDatabases' }; }
673
685
  throw new Error('Unsupported SHOW statement');
674
686
  }
package/lib/table.js CHANGED
@@ -133,10 +133,11 @@ class Table {
133
133
  this._dateFields = {}; // { fieldName: 'date'|'datetime'|'timestamp' }
134
134
 
135
135
  for (const [field, def] of Object.entries(schema)) {
136
- if (def.primaryKey) this._primaryKey = field;
136
+ const isPk = !!(def.primaryKey || def.primary === true);
137
+ if (isPk) this._primaryKey = field;
137
138
  if (def.autoIncrement) {
138
139
  this._autoIncrementField = field;
139
- if (def.primaryKey) this._primaryKey = field;
140
+ if (isPk) this._primaryKey = field;
140
141
  }
141
142
  if (def.default !== undefined) {
142
143
  this._defaults[field] = def.default;
@@ -177,7 +178,7 @@ class Table {
177
178
  _autoCreateBTrees() {
178
179
  for (const [field, def] of Object.entries(this._schema)) {
179
180
  if (field === '_softDelete') continue;
180
- if (def.primaryKey || def.unique) {
181
+ if (def.primaryKey || def.primary === true || def.unique) {
181
182
  this._btrees[field] = new BTree(64, true);
182
183
  }
183
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "4.1.1",
3
+ "version": "4.2.0-beta.1",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "bin": {