jsql-neo 1.0.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/index.js +25 -0
- package/lib/database.js +212 -0
- package/lib/table.js +468 -0
- package/package.json +29 -0
package/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* JSQL — Pure JavaScript Embedded Database
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const jsql = require('jsql-neo');
|
|
7
|
+
* const db = new jsql.Database('mydb.json');
|
|
8
|
+
*
|
|
9
|
+
* db.createTable('users', {
|
|
10
|
+
* id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
11
|
+
* name: { type: 'string', required: true },
|
|
12
|
+
* age: { type: 'integer' }
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* db.users.insert({ name: 'Alice', age: 25 });
|
|
16
|
+
* db.users.insert({ name: 'Bob', age: 30 });
|
|
17
|
+
*
|
|
18
|
+
* const adults = db.users.find({ age: { $gte: 18 } });
|
|
19
|
+
* const sorted = db.users.where({ age: { $gte: 18 } }).orderBy('age', 'desc').limit(10).get();
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
Database: require('./lib/database'),
|
|
24
|
+
Table: require('./lib/table')
|
|
25
|
+
};
|
package/lib/database.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* JSQL Database — 数据库管理器
|
|
4
|
+
* 管理表集合、文件持久化、自动保存
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const Table = require('./table');
|
|
10
|
+
|
|
11
|
+
class Database {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} filePath - 数据库文件路径,传 null 或 ':memory:' 使用内存模式
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {boolean} options.autoSave - 是否自动保存(默认 true)
|
|
16
|
+
* @param {number} options.autoSaveInterval - 自动保存间隔 ms(默认 0,即每次操作后保存)
|
|
17
|
+
* @param {boolean} options.pretty - JSON 是否格式化(默认 true)
|
|
18
|
+
*/
|
|
19
|
+
constructor(filePath, options = {}) {
|
|
20
|
+
this._filePath = filePath;
|
|
21
|
+
this._memoryMode = !filePath || filePath === ':memory:';
|
|
22
|
+
this._options = {
|
|
23
|
+
autoSave: options.autoSave !== false,
|
|
24
|
+
autoSaveInterval: options.autoSaveInterval || 0,
|
|
25
|
+
pretty: options.pretty !== false
|
|
26
|
+
};
|
|
27
|
+
this._tables = {}; // 表名 -> Table 实例
|
|
28
|
+
this._dirty = false;
|
|
29
|
+
this._autoSaveTimer = null;
|
|
30
|
+
this._loading = false; // 加载中,不触发自动保存
|
|
31
|
+
|
|
32
|
+
// 从文件加载
|
|
33
|
+
if (!this._memoryMode && fs.existsSync(this._filePath)) {
|
|
34
|
+
this._loading = true;
|
|
35
|
+
this._load();
|
|
36
|
+
this._loading = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================
|
|
41
|
+
// 表管理
|
|
42
|
+
// ============================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 创建表
|
|
46
|
+
* @param {string} name - 表名
|
|
47
|
+
* @param {object} schema - 表结构定义
|
|
48
|
+
* @example
|
|
49
|
+
* db.createTable('users', {
|
|
50
|
+
* id: { type: 'integer', primaryKey: true, autoIncrement: true },
|
|
51
|
+
* name: { type: 'string', required: true },
|
|
52
|
+
* email: { type: 'string', unique: true }
|
|
53
|
+
* })
|
|
54
|
+
*/
|
|
55
|
+
createTable(name, schema) {
|
|
56
|
+
if (this._tables[name]) {
|
|
57
|
+
throw new Error(`Table '${name}' already exists`);
|
|
58
|
+
}
|
|
59
|
+
const table = new Table(name, schema, this);
|
|
60
|
+
this._tables[name] = table;
|
|
61
|
+
|
|
62
|
+
// 动态代理到 Database 实例上,方便 db.users.insert() 调用
|
|
63
|
+
Object.defineProperty(this, name, {
|
|
64
|
+
get: () => this._tables[name],
|
|
65
|
+
enumerable: true,
|
|
66
|
+
configurable: true
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this._markDirty();
|
|
70
|
+
return table;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 删除表
|
|
75
|
+
*/
|
|
76
|
+
dropTable(name) {
|
|
77
|
+
if (!this._tables[name]) {
|
|
78
|
+
throw new Error(`Table '${name}' does not exist`);
|
|
79
|
+
}
|
|
80
|
+
delete this._tables[name];
|
|
81
|
+
delete this[name];
|
|
82
|
+
this._markDirty();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 判断表是否存在
|
|
87
|
+
*/
|
|
88
|
+
hasTable(name) {
|
|
89
|
+
return !!this._tables[name];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 获取所有表名
|
|
94
|
+
*/
|
|
95
|
+
getTables() {
|
|
96
|
+
return Object.keys(this._tables);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================
|
|
100
|
+
// 持久化
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 手动保存到文件
|
|
105
|
+
*/
|
|
106
|
+
save() {
|
|
107
|
+
if (this._memoryMode) return;
|
|
108
|
+
|
|
109
|
+
const data = { __schema__: {} };
|
|
110
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
111
|
+
data.__schema__[name] = table._schema;
|
|
112
|
+
data[name] = table.toJSON();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const dir = path.dirname(this._filePath);
|
|
116
|
+
if (!fs.existsSync(dir)) {
|
|
117
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const json = this._options.pretty
|
|
121
|
+
? JSON.stringify(data, null, 2)
|
|
122
|
+
: JSON.stringify(data);
|
|
123
|
+
fs.writeFileSync(this._filePath, json, 'utf8');
|
|
124
|
+
this._dirty = false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 从文件加载
|
|
129
|
+
*/
|
|
130
|
+
_load() {
|
|
131
|
+
try {
|
|
132
|
+
const json = fs.readFileSync(this._filePath, 'utf8');
|
|
133
|
+
const data = JSON.parse(json);
|
|
134
|
+
|
|
135
|
+
// 先根据 schema 重建表
|
|
136
|
+
if (data.__schema__) {
|
|
137
|
+
for (const [name, schema] of Object.entries(data.__schema__)) {
|
|
138
|
+
if (!this._tables[name]) {
|
|
139
|
+
this.createTable(name, schema);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 加载数据
|
|
145
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
146
|
+
if (name !== '__schema__' && this._tables[name]) {
|
|
147
|
+
this._tables[name]._loadRows(rows);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
throw new Error(`Failed to load database from '${this._filePath}': ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 标记脏数据(触发自动保存)
|
|
157
|
+
*/
|
|
158
|
+
_markDirty() {
|
|
159
|
+
this._dirty = true;
|
|
160
|
+
if (this._loading) return; // 加载中不保存
|
|
161
|
+
if (this._options.autoSave && !this._autoSaveTimer) {
|
|
162
|
+
if (this._options.autoSaveInterval > 0) {
|
|
163
|
+
this._autoSaveTimer = setTimeout(() => {
|
|
164
|
+
this._autoSaveTimer = null;
|
|
165
|
+
if (this._dirty) this.save();
|
|
166
|
+
}, this._options.autoSaveInterval);
|
|
167
|
+
} else {
|
|
168
|
+
// 立即保存(同步)
|
|
169
|
+
this.save();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 关闭数据库(保存并清理)
|
|
176
|
+
*/
|
|
177
|
+
close() {
|
|
178
|
+
if (this._autoSaveTimer) {
|
|
179
|
+
clearTimeout(this._autoSaveTimer);
|
|
180
|
+
this._autoSaveTimer = null;
|
|
181
|
+
}
|
|
182
|
+
if (this._dirty && !this._memoryMode) {
|
|
183
|
+
this.save();
|
|
184
|
+
}
|
|
185
|
+
this._tables = {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 导出数据库为 JSON 对象
|
|
190
|
+
*/
|
|
191
|
+
export() {
|
|
192
|
+
const data = {};
|
|
193
|
+
for (const [name, table] of Object.entries(this._tables)) {
|
|
194
|
+
data[name] = table.toJSON();
|
|
195
|
+
}
|
|
196
|
+
return data;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 从 JSON 对象导入数据
|
|
201
|
+
*/
|
|
202
|
+
import(data) {
|
|
203
|
+
for (const [name, rows] of Object.entries(data)) {
|
|
204
|
+
if (this._tables[name]) {
|
|
205
|
+
this._tables[name]._loadRows(rows);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
this._markDirty();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = Database;
|
package/lib/table.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// © Vexify 2026 All Rights Reserved.
|
|
2
|
+
/**
|
|
3
|
+
* JSQL Table — 表操作引擎
|
|
4
|
+
* 每张表是一个独立的数据集合,支持 CRUD、查询、索引
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class Table {
|
|
8
|
+
constructor(name, schema, db) {
|
|
9
|
+
this._name = name;
|
|
10
|
+
this._schema = schema;
|
|
11
|
+
this._db = db;
|
|
12
|
+
this._rows = []; // 数据行
|
|
13
|
+
this._indexes = {}; // 字段名 -> Map<value, [rowIndex, ...]>
|
|
14
|
+
this._autoIncrement = 0; // 自增计数器
|
|
15
|
+
this._dirty = false; // 是否有未保存的修改
|
|
16
|
+
|
|
17
|
+
// 解析主键和自增字段
|
|
18
|
+
this._primaryKey = null;
|
|
19
|
+
this._autoIncrementField = null;
|
|
20
|
+
for (const [field, def] of Object.entries(schema)) {
|
|
21
|
+
if (def.primaryKey) this._primaryKey = field;
|
|
22
|
+
if (def.autoIncrement) {
|
|
23
|
+
this._autoIncrementField = field;
|
|
24
|
+
if (def.primaryKey) this._primaryKey = field;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================
|
|
30
|
+
// 插入
|
|
31
|
+
// ============================================================
|
|
32
|
+
|
|
33
|
+
insert(data) {
|
|
34
|
+
// 校验必填字段
|
|
35
|
+
for (const [field, def] of Object.entries(this._schema)) {
|
|
36
|
+
if (def.required && (data[field] === undefined || data[field] === null)) {
|
|
37
|
+
throw new Error(`Field '${field}' is required in table '${this._name}'`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 唯一约束检查
|
|
42
|
+
for (const [field, def] of Object.entries(this._schema)) {
|
|
43
|
+
if (def.unique && data[field] !== undefined) {
|
|
44
|
+
for (const row of this._rows) {
|
|
45
|
+
if (row[field] === data[field]) {
|
|
46
|
+
throw new Error(`Duplicate value for unique field '${field}': ${data[field]}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 自增
|
|
53
|
+
if (this._autoIncrementField) {
|
|
54
|
+
if (data[this._autoIncrementField] === undefined) {
|
|
55
|
+
this._autoIncrement++;
|
|
56
|
+
data[this._autoIncrementField] = this._autoIncrement;
|
|
57
|
+
} else if (data[this._autoIncrementField] > this._autoIncrement) {
|
|
58
|
+
this._autoIncrement = data[this._autoIncrementField];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rowIndex = this._rows.length;
|
|
63
|
+
this._rows.push({ ...data });
|
|
64
|
+
|
|
65
|
+
// 更新索引
|
|
66
|
+
this._updateIndexes(rowIndex, data);
|
|
67
|
+
|
|
68
|
+
this._dirty = true;
|
|
69
|
+
this._db._markDirty();
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
insertMany(items) {
|
|
74
|
+
return items.map(item => this.insert(item));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// 查询
|
|
79
|
+
// ============================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 查找所有匹配的行
|
|
83
|
+
*/
|
|
84
|
+
find(query = {}) {
|
|
85
|
+
let rows = this._rows;
|
|
86
|
+
|
|
87
|
+
if (Object.keys(query).length > 0) {
|
|
88
|
+
rows = this._applyFilter(rows, query);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return rows.map(r => ({ ...r }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 查找第一条匹配的行
|
|
96
|
+
*/
|
|
97
|
+
findOne(query = {}) {
|
|
98
|
+
const rows = this.find(query);
|
|
99
|
+
return rows.length > 0 ? rows[0] : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 按主键查找
|
|
104
|
+
*/
|
|
105
|
+
findById(id) {
|
|
106
|
+
if (!this._primaryKey) {
|
|
107
|
+
throw new Error(`Table '${this._name}' has no primary key`);
|
|
108
|
+
}
|
|
109
|
+
return this.findOne({ [this._primaryKey]: id });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 获取所有行
|
|
114
|
+
*/
|
|
115
|
+
findAll() {
|
|
116
|
+
return this._rows.map(r => ({ ...r }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 计数
|
|
121
|
+
*/
|
|
122
|
+
count(query = {}) {
|
|
123
|
+
if (Object.keys(query).length === 0) {
|
|
124
|
+
return this._rows.length;
|
|
125
|
+
}
|
|
126
|
+
return this._applyFilter(this._rows, query).length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================
|
|
130
|
+
// 链式查询
|
|
131
|
+
// ============================================================
|
|
132
|
+
|
|
133
|
+
where(conditions) {
|
|
134
|
+
return new Query(this, conditions);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// 更新
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
update(query, updates) {
|
|
142
|
+
const matched = this._applyFilter(this._rows, query);
|
|
143
|
+
let count = 0;
|
|
144
|
+
|
|
145
|
+
for (const row of matched) {
|
|
146
|
+
const index = this._rows.indexOf(row);
|
|
147
|
+
if (index === -1) continue;
|
|
148
|
+
|
|
149
|
+
const oldData = { ...this._rows[index] };
|
|
150
|
+
|
|
151
|
+
// 唯一约束检查
|
|
152
|
+
for (const [field, def] of Object.entries(this._schema)) {
|
|
153
|
+
if (def.unique && updates[field] !== undefined) {
|
|
154
|
+
for (let i = 0; i < this._rows.length; i++) {
|
|
155
|
+
if (i !== index && this._rows[i][field] === updates[field]) {
|
|
156
|
+
throw new Error(`Duplicate value for unique field '${field}': ${updates[field]}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 更新
|
|
163
|
+
Object.assign(this._rows[index], updates);
|
|
164
|
+
|
|
165
|
+
// 更新索引
|
|
166
|
+
this._removeFromIndexes(index, oldData);
|
|
167
|
+
this._updateIndexes(index, this._rows[index]);
|
|
168
|
+
|
|
169
|
+
count++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (count > 0) {
|
|
173
|
+
this._dirty = true;
|
|
174
|
+
this._db._markDirty();
|
|
175
|
+
}
|
|
176
|
+
return count;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
updateById(id, updates) {
|
|
180
|
+
if (!this._primaryKey) {
|
|
181
|
+
throw new Error(`Table '${this._name}' has no primary key`);
|
|
182
|
+
}
|
|
183
|
+
return this.update({ [this._primaryKey]: id }, updates);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================
|
|
187
|
+
// 删除
|
|
188
|
+
// ============================================================
|
|
189
|
+
|
|
190
|
+
remove(query = {}) {
|
|
191
|
+
if (Object.keys(query).length === 0) {
|
|
192
|
+
// 删除全部
|
|
193
|
+
const count = this._rows.length;
|
|
194
|
+
this._rows = [];
|
|
195
|
+
this._indexes = {};
|
|
196
|
+
this._autoIncrement = 0;
|
|
197
|
+
this._dirty = true;
|
|
198
|
+
this._db._markDirty();
|
|
199
|
+
return count;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const toRemove = this._applyFilter(this._rows, query);
|
|
203
|
+
for (const row of toRemove) {
|
|
204
|
+
const index = this._rows.indexOf(row);
|
|
205
|
+
if (index !== -1) {
|
|
206
|
+
this._removeFromIndexes(index, row);
|
|
207
|
+
this._rows.splice(index, 1);
|
|
208
|
+
// 调整后续索引
|
|
209
|
+
for (const idxName of Object.keys(this._indexes)) {
|
|
210
|
+
const idx = this._indexes[idxName];
|
|
211
|
+
for (const [key, positions] of idx) {
|
|
212
|
+
const newPositions = positions
|
|
213
|
+
.filter(p => p !== index)
|
|
214
|
+
.map(p => p > index ? p - 1 : p);
|
|
215
|
+
if (newPositions.length === 0) {
|
|
216
|
+
idx.delete(key);
|
|
217
|
+
} else {
|
|
218
|
+
idx.set(key, newPositions);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (toRemove.length > 0) {
|
|
226
|
+
this._dirty = true;
|
|
227
|
+
this._db._markDirty();
|
|
228
|
+
}
|
|
229
|
+
return toRemove.length;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
removeById(id) {
|
|
233
|
+
if (!this._primaryKey) {
|
|
234
|
+
throw new Error(`Table '${this._name}' has no primary key`);
|
|
235
|
+
}
|
|
236
|
+
return this.remove({ [this._primaryKey]: id });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============================================================
|
|
240
|
+
// 索引
|
|
241
|
+
// ============================================================
|
|
242
|
+
|
|
243
|
+
createIndex(field) {
|
|
244
|
+
this._indexes[field] = new Map();
|
|
245
|
+
for (let i = 0; i < this._rows.length; i++) {
|
|
246
|
+
const value = this._rows[i][field];
|
|
247
|
+
if (value !== undefined) {
|
|
248
|
+
if (!this._indexes[field].has(value)) {
|
|
249
|
+
this._indexes[field].set(value, []);
|
|
250
|
+
}
|
|
251
|
+
this._indexes[field].get(value).push(i);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return this._indexes[field];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
dropIndex(field) {
|
|
258
|
+
delete this._indexes[field];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================
|
|
262
|
+
// 内部方法
|
|
263
|
+
// ============================================================
|
|
264
|
+
|
|
265
|
+
_applyFilter(rows, query) {
|
|
266
|
+
const result = [];
|
|
267
|
+
for (let i = 0; i < rows.length; i++) {
|
|
268
|
+
if (this._matchRow(rows[i], query)) {
|
|
269
|
+
result.push(rows[i]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_matchRow(row, query) {
|
|
276
|
+
for (const [field, condition] of Object.entries(query)) {
|
|
277
|
+
const value = row[field];
|
|
278
|
+
|
|
279
|
+
// 操作符对象: { $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $like, $regex }
|
|
280
|
+
if (condition !== null && typeof condition === 'object' && !Array.isArray(condition)) {
|
|
281
|
+
for (const [op, target] of Object.entries(condition)) {
|
|
282
|
+
if (!this._matchOperator(value, op, target)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// 简单等值匹配
|
|
288
|
+
if (value !== condition) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_matchOperator(value, op, target) {
|
|
297
|
+
switch (op) {
|
|
298
|
+
case '$eq': return value === target;
|
|
299
|
+
case '$ne': return value !== target;
|
|
300
|
+
case '$gt': return value > target;
|
|
301
|
+
case '$gte': return value >= target;
|
|
302
|
+
case '$lt': return value < target;
|
|
303
|
+
case '$lte': return value <= target;
|
|
304
|
+
case '$in': return Array.isArray(target) && target.includes(value);
|
|
305
|
+
case '$nin': return Array.isArray(target) && !target.includes(value);
|
|
306
|
+
case '$like':
|
|
307
|
+
if (typeof value !== 'string' || typeof target !== 'string') return false;
|
|
308
|
+
const regex = new RegExp(target.replace(/%/g, '.*').replace(/_/g, '.'), 'i');
|
|
309
|
+
return regex.test(value);
|
|
310
|
+
case '$regex':
|
|
311
|
+
return new RegExp(target).test(String(value));
|
|
312
|
+
case '$exists':
|
|
313
|
+
return target ? value !== undefined : value === undefined;
|
|
314
|
+
case '$between':
|
|
315
|
+
return Array.isArray(target) && target.length === 2
|
|
316
|
+
&& value >= target[0] && value <= target[1];
|
|
317
|
+
case '$or':
|
|
318
|
+
return Array.isArray(target) && target.some(sub => this._matchRow({ [op]: value }, { [op]: sub }));
|
|
319
|
+
default:
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_updateIndexes(rowIndex, data) {
|
|
325
|
+
for (const field of Object.keys(this._indexes)) {
|
|
326
|
+
const value = data[field];
|
|
327
|
+
if (value !== undefined) {
|
|
328
|
+
if (!this._indexes[field].has(value)) {
|
|
329
|
+
this._indexes[field].set(value, []);
|
|
330
|
+
}
|
|
331
|
+
this._indexes[field].get(value).push(rowIndex);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_removeFromIndexes(rowIndex, data) {
|
|
337
|
+
for (const field of Object.keys(this._indexes)) {
|
|
338
|
+
const value = data[field];
|
|
339
|
+
if (value !== undefined && this._indexes[field].has(value)) {
|
|
340
|
+
const positions = this._indexes[field].get(value).filter(p => p !== rowIndex);
|
|
341
|
+
if (positions.length === 0) {
|
|
342
|
+
this._indexes[field].delete(value);
|
|
343
|
+
} else {
|
|
344
|
+
this._indexes[field].set(value, positions);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
_loadRows(rows) {
|
|
351
|
+
this._rows = rows;
|
|
352
|
+
// 重建自增
|
|
353
|
+
if (this._autoIncrementField) {
|
|
354
|
+
for (const row of rows) {
|
|
355
|
+
const val = row[this._autoIncrementField];
|
|
356
|
+
if (typeof val === 'number' && val > this._autoIncrement) {
|
|
357
|
+
this._autoIncrement = val;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// 重建所有索引
|
|
362
|
+
for (const field of Object.keys(this._indexes)) {
|
|
363
|
+
this.createIndex(field);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ============================================================
|
|
368
|
+
// 导出
|
|
369
|
+
// ============================================================
|
|
370
|
+
|
|
371
|
+
toJSON() {
|
|
372
|
+
return this._rows;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ============================================================
|
|
377
|
+
// 链式查询构建器
|
|
378
|
+
// ============================================================
|
|
379
|
+
|
|
380
|
+
class Query {
|
|
381
|
+
constructor(table, conditions) {
|
|
382
|
+
this._table = table;
|
|
383
|
+
this._conditions = conditions;
|
|
384
|
+
this._orderBy = null;
|
|
385
|
+
this._orderDir = 'asc';
|
|
386
|
+
this._limitCount = null;
|
|
387
|
+
this._offsetCount = 0;
|
|
388
|
+
this._selectFields = null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
orderBy(field, dir = 'asc') {
|
|
392
|
+
this._orderBy = field;
|
|
393
|
+
this._orderDir = dir;
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
limit(n) {
|
|
398
|
+
this._limitCount = n;
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
offset(n) {
|
|
403
|
+
this._offsetCount = n;
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
select(fields) {
|
|
408
|
+
this._selectFields = Array.isArray(fields) ? fields : [fields];
|
|
409
|
+
return this;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
get() {
|
|
413
|
+
let rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
414
|
+
|
|
415
|
+
// 排序
|
|
416
|
+
if (this._orderBy) {
|
|
417
|
+
rows = rows.sort((a, b) => {
|
|
418
|
+
const va = a[this._orderBy];
|
|
419
|
+
const vb = b[this._orderBy];
|
|
420
|
+
if (va < vb) return this._orderDir === 'asc' ? -1 : 1;
|
|
421
|
+
if (va > vb) return this._orderDir === 'asc' ? 1 : -1;
|
|
422
|
+
return 0;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 分页
|
|
427
|
+
if (this._offsetCount > 0) {
|
|
428
|
+
rows = rows.slice(this._offsetCount);
|
|
429
|
+
}
|
|
430
|
+
if (this._limitCount !== null) {
|
|
431
|
+
rows = rows.slice(0, this._limitCount);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 字段选择
|
|
435
|
+
rows = rows.map(r => ({ ...r }));
|
|
436
|
+
if (this._selectFields) {
|
|
437
|
+
rows = rows.map(r => {
|
|
438
|
+
const obj = {};
|
|
439
|
+
for (const f of this._selectFields) {
|
|
440
|
+
obj[f] = r[f];
|
|
441
|
+
}
|
|
442
|
+
return obj;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return rows;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
first() {
|
|
450
|
+
const rows = this.limit(1).get();
|
|
451
|
+
return rows.length > 0 ? rows[0] : null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
count() {
|
|
455
|
+
const rows = this._table._applyFilter(this._table._rows, this._conditions);
|
|
456
|
+
return rows.length;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
update(updates) {
|
|
460
|
+
return this._table.update(this._conditions, updates);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
remove() {
|
|
464
|
+
return this._table.remove(this._conditions);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
module.exports = Table;
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jsql-neo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "JSQL — Pure JavaScript embedded database. SQL-like API, zero native dependencies, file-based or in-memory storage.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"lib/",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"database",
|
|
14
|
+
"sql",
|
|
15
|
+
"embedded",
|
|
16
|
+
"json",
|
|
17
|
+
"javascript",
|
|
18
|
+
"jsql",
|
|
19
|
+
"nosql",
|
|
20
|
+
"sql-like",
|
|
21
|
+
"in-memory",
|
|
22
|
+
"file-based"
|
|
23
|
+
],
|
|
24
|
+
"author": "Vexify",
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=14.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|