deepbase-sqlite 3.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 mclasen
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.
22
+
23
+
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # deepbase-sqlite
2
+
3
+ SQLite driver for DeepBase.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install deepbase deepbase-sqlite
9
+ ```
10
+
11
+ ## Description
12
+
13
+ Stores data in SQLite database files. Perfect for:
14
+
15
+ - ✅ Production applications
16
+ - ✅ Medium to large datasets
17
+ - ✅ Fast queries and transactions
18
+ - ✅ ACID compliance
19
+ - ✅ Embedded database solution
20
+ - ✅ Zero configuration needed
21
+
22
+ ## Usage
23
+
24
+ ```javascript
25
+ import DeepBase from 'deepbase';
26
+ import SqliteDriver from 'deepbase-sqlite';
27
+
28
+ const db = new DeepBase(new SqliteDriver({
29
+ path: './data',
30
+ name: 'mydb'
31
+ }));
32
+
33
+ await db.connect();
34
+
35
+ await db.set('users', 'alice', { name: 'Alice', age: 30 });
36
+ const alice = await db.get('users', 'alice');
37
+ ```
38
+
39
+ ## Options
40
+
41
+ ```javascript
42
+ new SqliteDriver({
43
+ path: './data', // Directory to store database files
44
+ name: 'default', // Database filename (without .db)
45
+ nidAlphabet: 'ABC...', // Alphabet for ID generation
46
+ nidLength: 10 // Length of generated IDs
47
+ })
48
+ ```
49
+
50
+ ## Features
51
+
52
+ ### High Performance
53
+
54
+ Uses `better-sqlite3` for synchronous operations wrapped in async API:
55
+
56
+ - Prepared statements for optimal performance
57
+ - Transaction support for batch operations
58
+ - Fast lookups with indexed keys
59
+
60
+ ### Singleton Pattern
61
+
62
+ Multiple instances pointing to the same database file will share the same connection:
63
+
64
+ ```javascript
65
+ const db1 = new DeepBase(new SqliteDriver({ name: 'mydb' }));
66
+ const db2 = new DeepBase(new SqliteDriver({ name: 'mydb' }));
67
+ // Both use the same underlying database connection
68
+ ```
69
+
70
+ ### Nested Data Structure
71
+
72
+ Efficiently stores nested objects using a key-value schema:
73
+
74
+ - Keys are stored as dot-notation paths (e.g., `user.profile.name`)
75
+ - Values are stored as JSON
76
+ - Fast lookups for both exact keys and partial paths
77
+
78
+ ### ACID Compliance
79
+
80
+ SQLite provides:
81
+
82
+ - **Atomicity**: All operations complete or none do
83
+ - **Consistency**: Data remains valid across transactions
84
+ - **Isolation**: Concurrent operations don't interfere
85
+ - **Durability**: Committed data persists even after crashes
86
+
87
+ ## Database Structure
88
+
89
+ Data is stored in a simple key-value table:
90
+
91
+ ```sql
92
+ CREATE TABLE deepbase (
93
+ key TEXT PRIMARY KEY,
94
+ value TEXT NOT NULL
95
+ )
96
+ ```
97
+
98
+ Example data:
99
+
100
+ ```
101
+ key | value
102
+ -----------------------|------------------
103
+ users.alice.name | "Alice"
104
+ users.alice.age | 30
105
+ users.bob.name | "Bob"
106
+ users.bob.age | 25
107
+ config.theme | "dark"
108
+ config.lang | "en"
109
+ ```
110
+
111
+ ## Use Cases
112
+
113
+ - **Production Apps**: Reliable embedded database for web/desktop apps
114
+ - **Medium Datasets**: Handles millions of records efficiently
115
+ - **Offline First**: Works without network or external database server
116
+ - **Desktop Apps**: Perfect for Electron or Tauri applications
117
+ - **Mobile Apps**: Lightweight database for React Native/Capacitor
118
+ - **IoT Devices**: Embedded storage for edge computing
119
+ - **Serverless**: Deploy with your functions, no external DB needed
120
+
121
+ ## Performance
122
+
123
+ SQLite offers excellent performance:
124
+
125
+ - Fast reads and writes with prepared statements
126
+ - Efficient indexing for quick lookups
127
+ - Transaction batching for bulk operations
128
+ - Low memory footprint
129
+
130
+ ## Migration
131
+
132
+ Easy to migrate between SQLite and other drivers:
133
+
134
+ ```javascript
135
+ import DeepBase from 'deepbase';
136
+ import SqliteDriver from 'deepbase-sqlite';
137
+ import MongoDriver from 'deepbase-mongodb';
138
+
139
+ const db = new DeepBase([
140
+ new SqliteDriver({ path: './data' }),
141
+ new MongoDriver({ url: 'mongodb://localhost:27017' })
142
+ ]);
143
+
144
+ await db.connect();
145
+ await db.migrate(0, 1); // Migrate SQLite to MongoDB
146
+ ```
147
+
148
+ ## File Structure
149
+
150
+ Data is stored as SQLite database files:
151
+
152
+ ```
153
+ data/
154
+ mydb.db
155
+ users.db
156
+ config.db
157
+ ```
158
+
159
+ ## Comparison with JSON Driver
160
+
161
+ | Feature | SQLite | JSON |
162
+ |---------|--------|------|
163
+ | Performance | ⚡ Very Fast | 🐌 Slower for large data |
164
+ | File Size | 📦 Compact | 📄 Human readable |
165
+ | Transactions | ✅ ACID | ❌ No transactions |
166
+ | Query Speed | 🚀 Indexed | 🔍 Full scan |
167
+ | Reliability | 💪 Very High | ⚠️ File corruption risk |
168
+ | Debugging | 🔧 SQL tools | 👁️ Easy to inspect |
169
+
170
+ ## Best Practices
171
+
172
+ ### Use Transactions for Bulk Operations
173
+
174
+ ```javascript
175
+ // Better: Use root object set for bulk inserts
176
+ const data = {
177
+ user1: { name: 'Alice' },
178
+ user2: { name: 'Bob' },
179
+ user3: { name: 'Charlie' }
180
+ };
181
+ await db.set('users', data);
182
+ ```
183
+
184
+ ### Disconnect Properly
185
+
186
+ ```javascript
187
+ // Always disconnect to close database connection
188
+ await db.disconnect();
189
+ ```
190
+
191
+ ### Use Appropriate Paths
192
+
193
+ ```javascript
194
+ // Good: Organize data hierarchically
195
+ await db.set('users', userId, 'profile', data);
196
+
197
+ // Avoid: Flat structure loses benefits of nesting
198
+ await db.set(`user_${userId}_profile`, data);
199
+ ```
200
+
201
+ ## License
202
+
203
+ MIT - Copyright (c) Martin Clasen
204
+
205
+
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "deepbase-sqlite",
3
+ "version": "3.0.0",
4
+ "description": "⚡ DeepBase SQLite - SQLite database driver",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.js"
11
+ }
12
+ },
13
+ "dependencies": {
14
+ "better-sqlite3": "^11.8.1"
15
+ },
16
+ "peerDependencies": {
17
+ "deepbase": "^3.0.0"
18
+ },
19
+ "scripts": {
20
+ "test": "mocha test/test.js"
21
+ },
22
+ "devDependencies": {
23
+ "mocha": "^10.8.2"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/clasen/DeepBase.git"
28
+ },
29
+ "keywords": [
30
+ "deepbase",
31
+ "sqlite",
32
+ "sqlite3",
33
+ "driver",
34
+ "database",
35
+ "persist",
36
+ "nested",
37
+ "objects"
38
+ ],
39
+ "author": "Martin Clasen",
40
+ "license": "MIT",
41
+ "bugs": {
42
+ "url": "https://github.com/clasen/DeepBase/issues"
43
+ },
44
+ "homepage": "https://github.com/clasen/DeepBase#readme"
45
+ }
46
+
47
+
@@ -0,0 +1,303 @@
1
+ import { DeepBaseDriver } from 'deepbase';
2
+ import Database from 'better-sqlite3';
3
+ import fs from 'fs';
4
+ import * as pathModule from 'path';
5
+ import { customAlphabet } from 'nanoid';
6
+
7
+ export class SqliteDriver extends DeepBaseDriver {
8
+ static _instances = {};
9
+
10
+ constructor({name, path, ...opts} = {}) {
11
+ super(opts);
12
+
13
+ this.name = name || "default";
14
+ this.path = path || './db';
15
+ this.nanoid = customAlphabet(this.nidAlphabet, this.nidLength);
16
+
17
+ this.path = pathModule.resolve(this.path);
18
+ this.fileName = pathModule.join(this.path, `${this.name}.db`);
19
+
20
+ // Singleton pattern per file
21
+ if (SqliteDriver._instances[this.fileName]) {
22
+ return SqliteDriver._instances[this.fileName];
23
+ }
24
+
25
+ this.db = null;
26
+ SqliteDriver._instances[this.fileName] = this;
27
+ }
28
+
29
+ async connect() {
30
+ if (!fs.existsSync(this.path)) {
31
+ fs.mkdirSync(this.path, { recursive: true });
32
+ }
33
+
34
+ this.db = new Database(this.fileName);
35
+
36
+ // Create table if it doesn't exist
37
+ this.db.exec(`
38
+ CREATE TABLE IF NOT EXISTS deepbase (
39
+ key TEXT PRIMARY KEY,
40
+ value TEXT NOT NULL
41
+ )
42
+ `);
43
+
44
+ // Prepare statements for better performance
45
+ this.getStmt = this.db.prepare('SELECT value FROM deepbase WHERE key = ?');
46
+ this.setStmt = this.db.prepare('INSERT OR REPLACE INTO deepbase (key, value) VALUES (?, ?)');
47
+ this.delStmt = this.db.prepare('DELETE FROM deepbase WHERE key = ?');
48
+ this.getAllStmt = this.db.prepare('SELECT key, value FROM deepbase');
49
+ this.getKeysLikeStmt = this.db.prepare('SELECT key, value FROM deepbase WHERE key LIKE ?');
50
+ }
51
+
52
+ async disconnect() {
53
+ if (this.db) {
54
+ this.db.close();
55
+ this.db = null;
56
+ }
57
+ }
58
+
59
+ async get(...args) {
60
+ if (args.length === 0) {
61
+ // Get root object
62
+ return this._getRootObject();
63
+ }
64
+
65
+ const key = this._pathToKey(args);
66
+ const row = this.getStmt.get(key);
67
+
68
+ // Check if there are child keys (nested properties)
69
+ const childKey = key + '.';
70
+ const children = this.getKeysLikeStmt.all(childKey + '%');
71
+
72
+ // If there are children, build object from them
73
+ if (children.length > 0) {
74
+ return this._buildObjectFromChildren(key);
75
+ }
76
+
77
+ // If direct key exists and no children, return it
78
+ if (row) {
79
+ return JSON.parse(row.value);
80
+ }
81
+
82
+ // Check if we need to look into a parent object
83
+ // For example: if we're looking for 'users.abc.name' but only 'users.abc' exists as a JSON object
84
+ const parentPath = this._findParentWithValue(args);
85
+ if (parentPath) {
86
+ const parentKey = this._pathToKey(parentPath);
87
+ const parentRow = this.getStmt.get(parentKey);
88
+ if (parentRow) {
89
+ const parentValue = JSON.parse(parentRow.value);
90
+ const relativePath = args.slice(parentPath.length);
91
+ return this._getFromObject(parentValue, relativePath);
92
+ }
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ async set(...args) {
99
+ if (args.length === 0) {
100
+ throw new Error('set() requires at least one argument');
101
+ }
102
+
103
+ if (args.length === 1) {
104
+ // Setting root object
105
+ await this._setRootObject(args[0]);
106
+ return [];
107
+ }
108
+
109
+ const keys = args.slice(0, -1);
110
+ const value = args[args.length - 1];
111
+ const key = this._pathToKey(keys);
112
+
113
+ // Check if any parent path exists as an object that needs to be expanded
114
+ this._expandParentObjects(keys);
115
+
116
+ this.setStmt.run(key, JSON.stringify(value));
117
+ return keys;
118
+ }
119
+
120
+ async del(...keys) {
121
+ if (keys.length === 0) {
122
+ // Delete everything
123
+ this.db.exec('DELETE FROM deepbase');
124
+ return;
125
+ }
126
+
127
+ const key = this._pathToKey(keys);
128
+
129
+ // Delete the key itself
130
+ this.delStmt.run(key);
131
+
132
+ // Delete all children
133
+ const childKey = key + '.';
134
+ this.db.prepare('DELETE FROM deepbase WHERE key LIKE ?').run(childKey + '%');
135
+ }
136
+
137
+ async inc(...args) {
138
+ const i = args.pop();
139
+ return this.upd(...args, n => n + i);
140
+ }
141
+
142
+ async dec(...args) {
143
+ const i = args.pop();
144
+ return this.upd(...args, n => n - i);
145
+ }
146
+
147
+ async add(...keys) {
148
+ const value = keys.pop();
149
+ const id = this.nanoid();
150
+ await this.set(...[...keys, id], value);
151
+ return [...keys, id];
152
+ }
153
+
154
+ async upd(...args) {
155
+ const func = args.pop();
156
+ return this.set(...args, func(await this.get(...args)));
157
+ }
158
+
159
+ _pathToKey(path) {
160
+ return path.join('.');
161
+ }
162
+
163
+ _keyToPath(key) {
164
+ return key.split('.');
165
+ }
166
+
167
+ _getRootObject() {
168
+ const rows = this.getAllStmt.all();
169
+ const result = {};
170
+
171
+ for (const row of rows) {
172
+ const path = this._keyToPath(row.key);
173
+ const value = JSON.parse(row.value);
174
+ this._setNestedValue(result, path, value);
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ async _setRootObject(obj) {
181
+ // Clear existing data
182
+ this.db.exec('DELETE FROM deepbase');
183
+
184
+ // Flatten and insert
185
+ const entries = this._flattenObject(obj);
186
+ const insertMany = this.db.transaction((entries) => {
187
+ for (const [key, value] of entries) {
188
+ this.setStmt.run(key, JSON.stringify(value));
189
+ }
190
+ });
191
+
192
+ insertMany(entries);
193
+ }
194
+
195
+ _buildObjectFromChildren(parentKey) {
196
+ const prefix = parentKey ? parentKey + '.' : '';
197
+ const rows = this.getKeysLikeStmt.all(prefix + '%');
198
+ const result = {};
199
+
200
+ for (const row of rows) {
201
+ const fullPath = this._keyToPath(row.key);
202
+ const relativePath = parentKey
203
+ ? fullPath.slice(this._keyToPath(parentKey).length)
204
+ : fullPath;
205
+
206
+ const value = JSON.parse(row.value);
207
+ this._setNestedValue(result, relativePath, value);
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ _setNestedValue(obj, path, value) {
214
+ if (path.length === 1) {
215
+ obj[path[0]] = value;
216
+ return;
217
+ }
218
+
219
+ const key = path[0];
220
+ if (!obj.hasOwnProperty(key) || typeof obj[key] !== "object") {
221
+ obj[key] = {};
222
+ }
223
+
224
+ this._setNestedValue(obj[key], path.slice(1), value);
225
+ }
226
+
227
+ _flattenObject(obj, prefix = '') {
228
+ const entries = [];
229
+
230
+ for (const [key, value] of Object.entries(obj)) {
231
+ const fullKey = prefix ? `${prefix}.${key}` : key;
232
+
233
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
234
+ entries.push(...this._flattenObject(value, fullKey));
235
+ } else {
236
+ entries.push([fullKey, value]);
237
+ }
238
+ }
239
+
240
+ return entries;
241
+ }
242
+
243
+ _findParentWithValue(path) {
244
+ // Try to find a parent path that has a stored value
245
+ // For example: if looking for ['users', 'abc', 'name']
246
+ // and 'users.abc' exists as a stored JSON object, return ['users', 'abc']
247
+ for (let i = path.length - 1; i > 0; i--) {
248
+ const parentPath = path.slice(0, i);
249
+ const parentKey = this._pathToKey(parentPath);
250
+ const row = this.getStmt.get(parentKey);
251
+ if (row) {
252
+ const value = JSON.parse(row.value);
253
+ // Only return if it's an object (not a primitive)
254
+ if (value !== null && typeof value === 'object') {
255
+ return parentPath;
256
+ }
257
+ }
258
+ }
259
+ return null;
260
+ }
261
+
262
+ _getFromObject(obj, path) {
263
+ if (path.length === 0) return obj;
264
+ if (path.length === 1) {
265
+ return obj === null || obj[path[0]] === undefined ? null : obj[path[0]];
266
+ }
267
+
268
+ const key = path[0];
269
+ if (!obj.hasOwnProperty(key)) return null;
270
+
271
+ return this._getFromObject(obj[key], path.slice(1));
272
+ }
273
+
274
+ _expandParentObjects(path) {
275
+ // Check each parent level (not including the path itself) to see if it exists as an object that needs expanding
276
+ // For example, if path is ['config', 'theme'], check if 'config' exists as an object
277
+ for (let i = 1; i < path.length; i++) {
278
+ const parentPath = path.slice(0, i);
279
+ const parentKey = this._pathToKey(parentPath);
280
+ const parentRow = this.getStmt.get(parentKey);
281
+
282
+ if (parentRow) {
283
+ const parentValue = JSON.parse(parentRow.value);
284
+
285
+ // If it's an object (not array or primitive), expand it
286
+ if (parentValue !== null && typeof parentValue === 'object' && !Array.isArray(parentValue)) {
287
+ // Delete the parent key
288
+ this.delStmt.run(parentKey);
289
+
290
+ // Insert all properties as individual keys
291
+ const entries = this._flattenObject(parentValue, parentKey);
292
+ for (const [key, value] of entries) {
293
+ this.setStmt.run(key, JSON.stringify(value));
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ export default SqliteDriver;
302
+
303
+
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { SqliteDriver } from './SqliteDriver.js';
2
+ export { SqliteDriver as default } from './SqliteDriver.js';
3
+
4
+
package/test/test.js ADDED
@@ -0,0 +1,446 @@
1
+ import assert from 'assert';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { DeepBase } from '../../core/src/index.js';
6
+ import { SqliteDriver } from '../src/SqliteDriver.js';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const testDataPath = path.join(__dirname, 'test-data');
10
+
11
+ describe('SqliteDriver', function() {
12
+ let db;
13
+ let testCounter = 0;
14
+
15
+ before(function() {
16
+ // Clean up test data before all tests
17
+ if (fs.existsSync(testDataPath)) {
18
+ fs.rmSync(testDataPath, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ beforeEach(async function() {
23
+ // Use unique name for each test to avoid singleton conflicts
24
+ testCounter++;
25
+ db = new DeepBase(new SqliteDriver({
26
+ name: `test-${testCounter}`,
27
+ path: testDataPath
28
+ }));
29
+ await db.connect();
30
+ });
31
+
32
+ afterEach(async function() {
33
+ await db.disconnect();
34
+ if (fs.existsSync(testDataPath)) {
35
+ fs.rmSync(testDataPath, { recursive: true, force: true });
36
+ }
37
+ });
38
+
39
+ describe('Basic Operations', function() {
40
+ it('should set and get a simple value', async function() {
41
+ await db.set('key', 'value');
42
+ const result = await db.get('key');
43
+ assert.strictEqual(result, 'value');
44
+ });
45
+
46
+ it('should set and get nested values', async function() {
47
+ await db.set('user', 'name', 'Alice');
48
+ await db.set('user', 'age', 30);
49
+
50
+ const name = await db.get('user', 'name');
51
+ const age = await db.get('user', 'age');
52
+
53
+ assert.strictEqual(name, 'Alice');
54
+ assert.strictEqual(age, 30);
55
+ });
56
+
57
+ it('should get entire nested object', async function() {
58
+ await db.set('user', 'name', 'Bob');
59
+ await db.set('user', 'age', 25);
60
+
61
+ const user = await db.get('user');
62
+ assert.deepStrictEqual(user, { name: 'Bob', age: 25 });
63
+ });
64
+
65
+ it('should return null for non-existent keys', async function() {
66
+ const result = await db.get('nonexistent');
67
+ assert.strictEqual(result, null);
68
+ });
69
+
70
+ it('should handle numbers', async function() {
71
+ await db.set('number', 42);
72
+ const result = await db.get('number');
73
+ assert.strictEqual(result, 42);
74
+ });
75
+
76
+ it('should handle booleans', async function() {
77
+ await db.set('flag', true);
78
+ const result = await db.get('flag');
79
+ assert.strictEqual(result, true);
80
+ });
81
+
82
+ it('should handle arrays', async function() {
83
+ await db.set('list', [1, 2, 3]);
84
+ const result = await db.get('list');
85
+ assert.deepStrictEqual(result, [1, 2, 3]);
86
+ });
87
+
88
+ it('should handle complex objects', async function() {
89
+ const complexObj = {
90
+ name: 'Test',
91
+ items: [1, 2, 3],
92
+ meta: { created: '2024-01-01', active: true }
93
+ };
94
+ await db.set('complex', complexObj);
95
+ const result = await db.get('complex');
96
+ assert.deepStrictEqual(result, complexObj);
97
+ });
98
+ });
99
+
100
+ describe('Delete Operations', function() {
101
+ it('should delete a key', async function() {
102
+ await db.set('temp', 'value');
103
+ await db.del('temp');
104
+
105
+ const result = await db.get('temp');
106
+ assert.strictEqual(result, null);
107
+ });
108
+
109
+ it('should delete nested key', async function() {
110
+ await db.set('user', 'name', 'Alice');
111
+ await db.set('user', 'age', 30);
112
+ await db.del('user', 'age');
113
+
114
+ const user = await db.get('user');
115
+ assert.deepStrictEqual(user, { name: 'Alice' });
116
+ });
117
+
118
+ it('should delete parent and all children', async function() {
119
+ await db.set('parent', 'child1', 'value1');
120
+ await db.set('parent', 'child2', 'value2');
121
+ await db.set('parent', 'nested', 'deep', 'value3');
122
+
123
+ await db.del('parent');
124
+
125
+ const result = await db.get('parent');
126
+ assert.strictEqual(result, null);
127
+
128
+ const child1 = await db.get('parent', 'child1');
129
+ assert.strictEqual(child1, null);
130
+
131
+ const nested = await db.get('parent', 'nested', 'deep');
132
+ assert.strictEqual(nested, null);
133
+ });
134
+
135
+ it('should clear all data', async function() {
136
+ await db.set('key1', 'value1');
137
+ await db.set('key2', 'value2');
138
+ await db.del();
139
+
140
+ const all = await db.get();
141
+ assert.deepStrictEqual(all, {});
142
+ });
143
+ });
144
+
145
+ describe('Add Operation', function() {
146
+ it('should add item with auto-generated ID', async function() {
147
+ const path = await db.add('users', { name: 'Charlie' });
148
+
149
+ assert.strictEqual(path.length, 2);
150
+ assert.strictEqual(path[0], 'users');
151
+ assert.strictEqual(typeof path[1], 'string');
152
+ assert.strictEqual(path[1].length, 10);
153
+
154
+ const user = await db.get(...path);
155
+ assert.deepStrictEqual(user, { name: 'Charlie' });
156
+ });
157
+
158
+ it('should add multiple items with unique IDs', async function() {
159
+ const path1 = await db.add('items', { value: 1 });
160
+ const path2 = await db.add('items', { value: 2 });
161
+
162
+ assert.notStrictEqual(path1[1], path2[1]);
163
+
164
+ const item1 = await db.get(...path1);
165
+ const item2 = await db.get(...path2);
166
+
167
+ assert.deepStrictEqual(item1, { value: 1 });
168
+ assert.deepStrictEqual(item2, { value: 2 });
169
+ });
170
+
171
+ it('should add items at nested paths', async function() {
172
+ const path = await db.add('categories', 'electronics', 'items', { name: 'Laptop' });
173
+
174
+ assert.strictEqual(path.length, 4);
175
+ assert.strictEqual(path[0], 'categories');
176
+ assert.strictEqual(path[1], 'electronics');
177
+ assert.strictEqual(path[2], 'items');
178
+
179
+ const item = await db.get(...path);
180
+ assert.deepStrictEqual(item, { name: 'Laptop' });
181
+ });
182
+ });
183
+
184
+ describe('Increment/Decrement', function() {
185
+ it('should increment a value', async function() {
186
+ await db.set('counter', 10);
187
+ await db.inc('counter', 5);
188
+
189
+ const result = await db.get('counter');
190
+ assert.strictEqual(result, 15);
191
+ });
192
+
193
+ it('should decrement a value', async function() {
194
+ await db.set('counter', 20);
195
+ await db.dec('counter', 8);
196
+
197
+ const result = await db.get('counter');
198
+ assert.strictEqual(result, 12);
199
+ });
200
+
201
+ it('should increment nested value', async function() {
202
+ await db.set('user', 'balance', 100);
203
+ await db.inc('user', 'balance', 50);
204
+
205
+ const balance = await db.get('user', 'balance');
206
+ assert.strictEqual(balance, 150);
207
+ });
208
+
209
+ it('should handle negative increments', async function() {
210
+ await db.set('counter', 10);
211
+ await db.inc('counter', -3);
212
+
213
+ const result = await db.get('counter');
214
+ assert.strictEqual(result, 7);
215
+ });
216
+ });
217
+
218
+ describe('Update Operation', function() {
219
+ it('should update value with function', async function() {
220
+ await db.set('name', 'alice');
221
+ await db.upd('name', name => name.toUpperCase());
222
+
223
+ const result = await db.get('name');
224
+ assert.strictEqual(result, 'ALICE');
225
+ });
226
+
227
+ it('should update nested value with function', async function() {
228
+ await db.set('user', 'age', 25);
229
+ await db.upd('user', 'age', age => age + 1);
230
+
231
+ const age = await db.get('user', 'age');
232
+ assert.strictEqual(age, 26);
233
+ });
234
+
235
+ it('should update object with function', async function() {
236
+ await db.set('user', { name: 'Alice', age: 30 });
237
+ await db.upd('user', user => ({ ...user, age: user.age + 1 }));
238
+
239
+ const user = await db.get('user');
240
+ assert.deepStrictEqual(user, { name: 'Alice', age: 31 });
241
+ });
242
+ });
243
+
244
+ describe('Keys, Values, Entries', function() {
245
+ beforeEach(async function() {
246
+ await db.set('users', 'alice', { age: 30 });
247
+ await db.set('users', 'bob', { age: 25 });
248
+ });
249
+
250
+ it('should get keys', async function() {
251
+ const keys = await db.keys('users');
252
+ assert.deepStrictEqual(keys.sort(), ['alice', 'bob']);
253
+ });
254
+
255
+ it('should get values', async function() {
256
+ const values = await db.values('users');
257
+ assert.strictEqual(values.length, 2);
258
+ assert.ok(values.some(v => v.age === 30));
259
+ assert.ok(values.some(v => v.age === 25));
260
+ });
261
+
262
+ it('should get entries', async function() {
263
+ const entries = await db.entries('users');
264
+ assert.strictEqual(entries.length, 2);
265
+ assert.ok(entries.some(([k, v]) => k === 'alice' && v.age === 30));
266
+ assert.ok(entries.some(([k, v]) => k === 'bob' && v.age === 25));
267
+ });
268
+
269
+ it('should return empty arrays for non-object values', async function() {
270
+ await db.set('simple', 'string');
271
+
272
+ const keys = await db.keys('simple');
273
+ const values = await db.values('simple');
274
+ const entries = await db.entries('simple');
275
+
276
+ assert.deepStrictEqual(keys, []);
277
+ assert.deepStrictEqual(values, []);
278
+ assert.deepStrictEqual(entries, []);
279
+ });
280
+ });
281
+
282
+ describe('Persistence', function() {
283
+ it('should persist data to database file', async function() {
284
+ // Use a separate db for this test
285
+ const persistDb = new DeepBase(new SqliteDriver({
286
+ name: 'persist-test',
287
+ path: testDataPath
288
+ }));
289
+ await persistDb.connect();
290
+ await persistDb.set('persistent', 'data');
291
+ await persistDb.disconnect();
292
+
293
+ const filePath = path.join(testDataPath, 'persist-test.db');
294
+ assert.ok(fs.existsSync(filePath));
295
+ });
296
+
297
+ it('should load existing data on connect', async function() {
298
+ // Use a separate db for this test
299
+ const db1 = new DeepBase(new SqliteDriver({
300
+ name: 'reload-test',
301
+ path: testDataPath
302
+ }));
303
+ await db1.connect();
304
+ await db1.set('existing', 'value');
305
+ await db1.set('nested', 'key', 'data');
306
+ await db1.disconnect();
307
+
308
+ // Create new instance pointing to same file
309
+ const db2 = new DeepBase(new SqliteDriver({
310
+ name: 'reload-test',
311
+ path: testDataPath
312
+ }));
313
+ await db2.connect();
314
+
315
+ const result = await db2.get('existing');
316
+ assert.strictEqual(result, 'value');
317
+
318
+ const nested = await db2.get('nested', 'key');
319
+ assert.strictEqual(nested, 'data');
320
+
321
+ await db2.disconnect();
322
+ });
323
+
324
+ it('should handle reconnection', async function() {
325
+ await db.set('before', 'disconnect');
326
+ await db.disconnect();
327
+
328
+ await db.connect();
329
+ const result = await db.get('before');
330
+ assert.strictEqual(result, 'disconnect');
331
+ });
332
+ });
333
+
334
+ describe('Singleton Pattern', function() {
335
+ it('should return same instance for same file', function() {
336
+ const driver1 = new SqliteDriver({ name: 'singleton', path: testDataPath });
337
+ const driver2 = new SqliteDriver({ name: 'singleton', path: testDataPath });
338
+
339
+ assert.strictEqual(driver1, driver2);
340
+ });
341
+
342
+ it('should return different instances for different files', function() {
343
+ const driver1 = new SqliteDriver({ name: 'file1', path: testDataPath });
344
+ const driver2 = new SqliteDriver({ name: 'file2', path: testDataPath });
345
+
346
+ assert.notStrictEqual(driver1, driver2);
347
+ });
348
+ });
349
+
350
+ describe('Root Object Operations', function() {
351
+ it('should set entire root object', async function() {
352
+ const data = {
353
+ users: { alice: { age: 30 }, bob: { age: 25 } },
354
+ config: { theme: 'dark', lang: 'en' }
355
+ };
356
+
357
+ await db.set(data);
358
+ const result = await db.get();
359
+ assert.deepStrictEqual(result, data);
360
+ });
361
+
362
+ it('should get entire root object', async function() {
363
+ await db.set('key1', 'value1');
364
+ await db.set('key2', 'value2');
365
+ await db.set('nested', 'key', 'value');
366
+
367
+ const result = await db.get();
368
+ assert.deepStrictEqual(result, {
369
+ key1: 'value1',
370
+ key2: 'value2',
371
+ nested: { key: 'value' }
372
+ });
373
+ });
374
+
375
+ it('should replace root object on set', async function() {
376
+ await db.set('old', 'data');
377
+
378
+ const newData = { new: 'data' };
379
+ await db.set(newData);
380
+
381
+ const result = await db.get();
382
+ assert.deepStrictEqual(result, newData);
383
+
384
+ const old = await db.get('old');
385
+ assert.strictEqual(old, null);
386
+ });
387
+ });
388
+
389
+ describe('Deep Nesting', function() {
390
+ it('should handle deeply nested paths', async function() {
391
+ await db.set('a', 'b', 'c', 'd', 'e', 'deep value');
392
+ const result = await db.get('a', 'b', 'c', 'd', 'e');
393
+ assert.strictEqual(result, 'deep value');
394
+ });
395
+
396
+ it('should get partial deep objects', async function() {
397
+ await db.set('a', 'b', 'c', 'value1');
398
+ await db.set('a', 'b', 'd', 'value2');
399
+ await db.set('a', 'e', 'value3');
400
+
401
+ const resultB = await db.get('a', 'b');
402
+ assert.deepStrictEqual(resultB, { c: 'value1', d: 'value2' });
403
+
404
+ const resultA = await db.get('a');
405
+ assert.deepStrictEqual(resultA, {
406
+ b: { c: 'value1', d: 'value2' },
407
+ e: 'value3'
408
+ });
409
+ });
410
+ });
411
+
412
+ describe('Overwriting Object with Nested Properties', function() {
413
+ it('should handle setting object first then nested properties', async function() {
414
+ // This is the scenario from example 05-sqlite.js
415
+ await db.set('config', { lang: 'en', theme: 'dark' });
416
+ await db.set('config', 'lang', 'en');
417
+ await db.set('config', 'theme', 'light');
418
+
419
+ const config = await db.get('config');
420
+ assert.deepStrictEqual(config, { lang: 'en', theme: 'light' });
421
+ });
422
+
423
+ it('should prioritize nested properties over initial object', async function() {
424
+ await db.set('settings', { a: 1, b: 2, c: 3 });
425
+ await db.set('settings', 'b', 99);
426
+ await db.set('settings', 'd', 4);
427
+
428
+ const settings = await db.get('settings');
429
+ assert.deepStrictEqual(settings, { a: 1, b: 99, c: 3, d: 4 });
430
+ });
431
+
432
+ it('should handle nested object then deeper nesting', async function() {
433
+ await db.set('user', { name: 'Alice', meta: { role: 'admin' } });
434
+ await db.set('user', 'meta', 'role', 'user');
435
+ await db.set('user', 'meta', 'active', true);
436
+
437
+ const user = await db.get('user');
438
+ assert.deepStrictEqual(user, {
439
+ name: 'Alice',
440
+ meta: { role: 'user', active: true }
441
+ });
442
+ });
443
+ });
444
+ });
445
+
446
+