UNPKG

@shaxpir/sharedb-storage-node-sqlite

Version:

Node.js SQLite storage adapter for ShareDB with better-sqlite3

373 lines (315 loc) 12.5 kB
/** * Tests for AttachedBetterSqliteAdapter * Pure adapter functionality tests only - no storage or strategy tests */ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const AttachedBetterSqliteAdapter = require('../lib/adapters/attached-better-sqlite-adapter'); const BetterSqliteAdapter = require('../lib/adapters/better-sqlite-adapter'); describe('AttachedBetterSqliteAdapter', function() { const TEST_DIR = path.join(__dirname, 'test-databases'); const PRIMARY_DB = path.join(TEST_DIR, 'test-primary.db'); const ATTACHED_DB = path.join(TEST_DIR, 'test-attached.db'); // Ensure test directory exists before(function() { if (!fs.existsSync(TEST_DIR)) { fs.mkdirSync(TEST_DIR, { recursive: true }); } }); // Clean up test databases after each test afterEach(function() { try { if (fs.existsSync(PRIMARY_DB)) fs.unlinkSync(PRIMARY_DB); if (fs.existsSync(ATTACHED_DB)) fs.unlinkSync(ATTACHED_DB); if (fs.existsSync(path.join(TEST_DIR, 'test-attached2.db'))) { fs.unlinkSync(path.join(TEST_DIR, 'test-attached2.db')); } if (fs.existsSync(PRIMARY_DB + '-wal')) fs.unlinkSync(PRIMARY_DB + '-wal'); if (fs.existsSync(PRIMARY_DB + '-shm')) fs.unlinkSync(PRIMARY_DB + '-shm'); if (fs.existsSync(ATTACHED_DB + '-wal')) fs.unlinkSync(ATTACHED_DB + '-wal'); if (fs.existsSync(ATTACHED_DB + '-shm')) fs.unlinkSync(ATTACHED_DB + '-shm'); } catch (e) { // Ignore errors during cleanup } }); after(function() { // Final cleanup try { if (fs.existsSync(TEST_DIR)) { const files = fs.readdirSync(TEST_DIR); files.forEach(file => { const filePath = path.join(TEST_DIR, file); fs.unlinkSync(filePath); }); fs.rmdirSync(TEST_DIR); } } catch (e) { // Ignore cleanup errors } }); describe('Basic Attachment', function() { it('should create adapter with attachment config', function() { const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ATTACHED_DB, alias: 'sharedb' } ] }, { debug: false } ); assert(adapter); assert.strictEqual(adapter.dbPath, PRIMARY_DB); assert.strictEqual(adapter.attachmentConfig.attachments.length, 1); }); it('should connect and attach databases', async function() { // Create the attached database first const attachedAdapter = new BetterSqliteAdapter(ATTACHED_DB); await attachedAdapter.connect(); await attachedAdapter.runAsync('CREATE TABLE test_table (id INTEGER PRIMARY KEY)'); await attachedAdapter.disconnect(); // Now create attached adapter const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ATTACHED_DB, alias: 'sharedb' } ] }, { debug: false } ); await adapter.connect(); assert(adapter.isAttached('sharedb')); // Verify we can query the attached database const result = await adapter.getFirstAsync( "SELECT name FROM sharedb.sqlite_master WHERE type='table' AND name='test_table'" ); assert(result); assert.strictEqual(result.name, 'test_table'); await adapter.disconnect(); assert(!adapter.isAttached('sharedb')); }); it('should handle attachment failure gracefully', async function() { const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: '/nonexistent/database.db', alias: 'sharedb' } ] }, { debug: false } ); try { await adapter.connect(); assert.fail('Should have thrown an error'); } catch (error) { assert(error.message.includes('does not exist') || error.message.includes('unable to open')); } }); it('should create database file if createIfNotExists is true', async function() { const NEW_DB = path.join(TEST_DIR, 'new-database.db'); // Ensure it doesn't exist if (fs.existsSync(NEW_DB)) { fs.unlinkSync(NEW_DB); } const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: NEW_DB, alias: 'newdb', createIfNotExists: true } ] }, { debug: false } ); await adapter.connect(); // Verify the file was created assert(fs.existsSync(NEW_DB)); assert(adapter.isAttached('newdb')); // Should be able to create tables in it await adapter.runAsync('CREATE TABLE newdb.test_table (id INTEGER PRIMARY KEY)'); await adapter.disconnect(); // Cleanup if (fs.existsSync(NEW_DB)) { fs.unlinkSync(NEW_DB); } }); }); describe('Cross-Database Queries', function() { it('should perform cross-database queries', async function() { // Setup primary database const primaryAdapter = new BetterSqliteAdapter(PRIMARY_DB); await primaryAdapter.connect(); await primaryAdapter.runAsync('CREATE TABLE primary_table (id INTEGER PRIMARY KEY, name TEXT)'); await primaryAdapter.runAsync("INSERT INTO primary_table (id, name) VALUES (1, 'primary')"); await primaryAdapter.disconnect(); // Setup attached database const attachedAdapter = new BetterSqliteAdapter(ATTACHED_DB); await attachedAdapter.connect(); await attachedAdapter.runAsync('CREATE TABLE attached_table (id INTEGER PRIMARY KEY, name TEXT)'); await attachedAdapter.runAsync("INSERT INTO attached_table (id, name) VALUES (1, 'attached')"); await attachedAdapter.disconnect(); // Now use attached adapter to query both const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ATTACHED_DB, alias: 'sharedb' } ] }, { debug: false } ); await adapter.connect(); // Query from primary database const primaryResult = await adapter.getFirstAsync('SELECT name FROM primary_table WHERE id = 1'); assert.strictEqual(primaryResult.name, 'primary'); // Query from attached database const attachedResult = await adapter.getFirstAsync('SELECT name FROM sharedb.attached_table WHERE id = 1'); assert.strictEqual(attachedResult.name, 'attached'); // Join across databases const joinResult = await adapter.getAllAsync( 'SELECT p.name as primary_name, a.name as attached_name ' + 'FROM primary_table p, sharedb.attached_table a ' + 'WHERE p.id = a.id' ); assert.strictEqual(joinResult[0].primary_name, 'primary'); assert.strictEqual(joinResult[0].attached_name, 'attached'); await adapter.disconnect(); }); }); describe('Multiple Attachments', function() { it('should support multiple database attachments', async function() { const SECOND_ATTACHED_DB = path.join(TEST_DIR, 'test-attached2.db'); try { // Create two attached databases const attachedAdapter1 = new BetterSqliteAdapter(ATTACHED_DB); await attachedAdapter1.connect(); await attachedAdapter1.runAsync('CREATE TABLE db1_table (id INTEGER PRIMARY KEY, value TEXT)'); await attachedAdapter1.runAsync("INSERT INTO db1_table VALUES (1, 'from_db1')"); await attachedAdapter1.disconnect(); const attachedAdapter2 = new BetterSqliteAdapter(SECOND_ATTACHED_DB); await attachedAdapter2.connect(); await attachedAdapter2.runAsync('CREATE TABLE db2_table (id INTEGER PRIMARY KEY, value TEXT)'); await attachedAdapter2.runAsync("INSERT INTO db2_table VALUES (1, 'from_db2')"); await attachedAdapter2.disconnect(); // Create adapter with multiple attachments const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ATTACHED_DB, alias: 'db1' }, { path: SECOND_ATTACHED_DB, alias: 'db2' } ] }, { debug: false } ); await adapter.connect(); // Verify both are attached assert(adapter.isAttached('db1')); assert(adapter.isAttached('db2')); // Query from both attached databases const result1 = await adapter.getFirstAsync('SELECT value FROM db1.db1_table WHERE id = 1'); assert.strictEqual(result1.value, 'from_db1'); const result2 = await adapter.getFirstAsync('SELECT value FROM db2.db2_table WHERE id = 1'); assert.strictEqual(result2.value, 'from_db2'); // Join across attached databases const joinResult = await adapter.getAllAsync( 'SELECT d1.value as val1, d2.value as val2 ' + 'FROM db1.db1_table d1, db2.db2_table d2 ' + 'WHERE d1.id = d2.id' ); assert.strictEqual(joinResult[0].val1, 'from_db1'); assert.strictEqual(joinResult[0].val2, 'from_db2'); await adapter.disconnect(); } finally { // Clean up second attached database if (fs.existsSync(SECOND_ATTACHED_DB)) { fs.unlinkSync(SECOND_ATTACHED_DB); } } }); it('should detach specific databases', async function() { // Create attached database const attachedAdapter = new BetterSqliteAdapter(ATTACHED_DB); await attachedAdapter.connect(); await attachedAdapter.runAsync('CREATE TABLE test_table (id INTEGER PRIMARY KEY)'); await attachedAdapter.disconnect(); const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ATTACHED_DB, alias: 'sharedb' } ] }, { debug: false } ); await adapter.connect(); assert(adapter.isAttached('sharedb')); // Detach the database await adapter.detachDatabase('sharedb'); assert(!adapter.isAttached('sharedb')); // Should not be able to query it anymore try { await adapter.getFirstAsync("SELECT * FROM sharedb.test_table"); assert.fail('Should have thrown an error'); } catch (error) { assert(error.message.includes('no such table')); } await adapter.disconnect(); }); }); describe('Helper Methods', function() { it('should return attached aliases', function() { const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ATTACHED_DB, alias: 'sharedb' }, { path: '/path/to/other.db', alias: 'other' } ] } ); // Before connecting, no databases are attached yet const aliases = adapter.getAttachedAliases(); assert.deepStrictEqual(aliases, []); }); it('should track attached databases after connection', async function() { // Create the attached database first const attachedAdapter = new BetterSqliteAdapter(ATTACHED_DB); await attachedAdapter.connect(); await attachedAdapter.runAsync('CREATE TABLE test (id INTEGER)'); await attachedAdapter.disconnect(); const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ATTACHED_DB, alias: 'sharedb' } ] } ); await adapter.connect(); const aliases = adapter.getAttachedAliases(); assert.deepStrictEqual(aliases, ['sharedb']); await adapter.disconnect(); }); it('should handle :memory: databases', async function() { const adapter = new AttachedBetterSqliteAdapter( PRIMARY_DB, { attachments: [ { path: ':memory:', alias: 'memdb' } ] }, { debug: false } ); await adapter.connect(); assert(adapter.isAttached('memdb')); // Should be able to create tables in memory database await adapter.runAsync('CREATE TABLE memdb.test_table (id INTEGER PRIMARY KEY, value TEXT)'); await adapter.runAsync("INSERT INTO memdb.test_table VALUES (1, 'in_memory')"); const result = await adapter.getFirstAsync('SELECT value FROM memdb.test_table WHERE id = 1'); assert.strictEqual(result.value, 'in_memory'); await adapter.disconnect(); }); }); });