@linked-db/linked-ql
Version:
A query client that extends standard SQL with new syntax sugars and enables auto-versioning capabilities on any database
1,078 lines (900 loc) • 82 kB
JavaScript
import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
use(chaiAsPromised);
import '../src/lang/index.js';
import { matchRelationSelector, normalizeRelationSelectorArg } from '../src/entry/abstracts/util.js';
import { StorageEngine } from '../src/flash/StorageEngine.js';
import { FlashClient } from '../src/flash/FlashClient.js';
import { TableStorage } from '../src/flash/TableStorage.js';
describe('Util', () => {
it('should normalize selector forms', () => {
expect(() => normalizeRelationSelectorArg()).to.throw(/Given selector .* invalid/);
expect(() => normalizeRelationSelectorArg(null)).to.throw(/Given selector .* invalid/);
expect(() => normalizeRelationSelectorArg({})).to.throw(/Given selector .* invalid/);
expect(() => normalizeRelationSelectorArg([{ namespace: 'b' }, { a: 'b' }])).to.throw(/Given selector .* invalid at index 1/);
const a = normalizeRelationSelectorArg('*');
expect(a).to.deep.eq({ ['*']: ['*'] });
const b = normalizeRelationSelectorArg({ a: 'b' });
expect(b).to.deep.eq({ a: ['b'] });
const c = normalizeRelationSelectorArg([{ namespace: 'b' }]);
expect(c).to.deep.eq({ b: ['*'] });
});
it('should match plain db selector', () => {
const a = matchRelationSelector('lq_test_public', ['lq_test_public', 'lq_test_private']);
const b = matchRelationSelector('lq_test_public', ['lq_test_public2', 'lq_test_private']);
expect(a).to.be.true;
expect(b).to.be.false;
});
it('should match negated plain db selector', () => {
const a = matchRelationSelector('lq_test_public', ['!lq_test_public', 'lq_test_private']);
const b = matchRelationSelector('lq_test_public', ['!lq_test_public2', 'lq_test_public']);
const c = matchRelationSelector('lq_test_public', ['!lq_test_public2', '!lq_test_private']);
expect(a).to.be.false;
expect(b).to.be.true;
expect(c).to.be.true;
});
it('should match wildcard db selector', () => {
const a = matchRelationSelector('lq_test_public', ['%ublic', 'lq_test_private']);
const b = matchRelationSelector('lq_test_public', ['publi%']);
const c = matchRelationSelector('lq_test_public', ['publo%']);
expect(a).to.be.true;
expect(b).to.be.true;
expect(c).to.be.false;
});
it('should match negated wildcard db selector', () => {
const a = matchRelationSelector('lq_test_public', ['!%ublic', 'lq_test_private']);
const b = matchRelationSelector('lq_test_public', ['!publi%']);
const c = matchRelationSelector('lq_test_public', ['!publo%']);
expect(a).to.be.false;
expect(b).to.be.false;
expect(c).to.be.true;
});
});
describe('StorageEngine - Basic CRUD', () => {
let storageEngine, lq_test_public, tbl1;
describe('SCHEMA', () => {
it('should create basic table namespace', async () => {
storageEngine = new StorageEngine({ defaultNamespace: 'lq_test_public' });
lq_test_public = await storageEngine.getNamespace('lq_test_public');
tbl1 = await lq_test_public.createTable('tbl1');
expect(tbl1).to.be.instanceOf(TableStorage);
});
it('should reject creating an existing table namespace', async () => {
expect(lq_test_public.createTable('tbl1')).to.be.rejected;
});
it('should retrieve just-created table namespace', async () => {
const tableNames = await lq_test_public.tableNames();
expect(tableNames).to.include('tbl1');
const tblSchema = tbl1.schema;
expect(tblSchema).to.be.an('object');
});
});
describe('INSERT', () => {
it('should do basic INSERT', async () => {
const row = await tbl1.insert({ id: 34, name: 'John' });
expect(row).to.deep.eq({ id: 34, name: 'John' });
});
it('should reject duplicate-key INSERT', async () => {
expect(tbl1.insert({ id: 34, name: 'John' })).to.be.rejected;
});
it('should do auto-increment', async () => {
const row1 = await tbl1.insert({ name: 'John' });
expect(row1).to.deep.eq({ name: 'John', id: 1 });
const row2 = await tbl1.insert({ name: 'John' });
expect(row2).to.deep.eq({ name: 'John', id: 2 });
});
});
describe('UPDATE', () => {
it('should do basic UPDATE', async () => {
const row = await tbl1.update({ id: 34 }, { id: 34, name: 'John2' });
expect(row).to.deep.eq({ id: 34, name: 'John2' });
});
});
describe('READ', () => {
it('should do basic READ', async () => {
const record = await tbl1.get({ id: 34 });
expect(record).to.deep.eq({ id: 34, name: 'John2' });
});
it('should do basic scan', async () => {
const records = tbl1;
const _records = [];
for await (const record of records) {
_records.push(record);
}
expect(_records).to.have.length(3);
expect(_records[0]).to.deep.eq({ id: 34, name: 'John2' });
});
});
describe('DELETE', () => {
it('should do basic DELETE', async () => {
const row = await tbl1.delete({ id: 34 });
expect(row).to.deep.eq({ id: 34, name: 'John2' });
const record = await tbl1.get({ id: 34 });
expect(record).to.be.undefined;
});
});
});
const createClient = async (defaultNamespace = undefined, otherOptions = {}) => {
const client = new FlashClient({ defaultNamespace, ...otherOptions });
await client.connect();
return client;
};
describe('FlashClient - Basic DDL', () => {
let client;
before(async () => {
client = await createClient();
});
after(async () => {
await client.disconnect();
});
// ---------- CREATE/DROP
describe('CREATE SCHEMA', () => {
it('should create namespace with IF NOT EXISTS', async () => {
const result = await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_namespace');
expect(result).to.exist;
const namespaces = await client.storageEngine.namespaceNames();
expect(namespaces).to.include('lq_test_namespace');
});
it('should not create namespace if already exists', async () => {
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_namespace');
await expect(client.query('CREATE SCHEMA lq_test_namespace')).to.be.rejected;
});
// Advanced: create namespace with AUTHORIZATION (Postgres syntax)
it('should support CREATE SCHEMA ... AUTHORIZATION', async () => {
const result = await client.query('CREATE SCHEMA IF NOT EXISTS lq_auth AUTHORIZATION current_user');
expect(result).to.exist;
const namespaces = await client.storageEngine.namespaceNames();
expect(namespaces).to.include('lq_auth');
});
});
describe('DROP SCHEMA', () => {
before(async () => {
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_drop');
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_drop2');
});
it('should drop namespace with IF EXISTS', async () => {
const result = await client.query('DROP SCHEMA IF EXISTS lq_test_drop CASCADE');
expect(result).to.exist;
const namespaces = await client.storageEngine.namespaceNames();
expect(namespaces).to.not.include('lq_test_drop');
});
it('should not fail dropping non-existent namespace', async () => {
await expect(client.query('DROP SCHEMA IF EXISTS lq_nonexistent CASCADE')).to.not.be.rejected;
});
// Advanced: drop multiple namespaces
it('should drop multiple namespaces in one command', async () => {
await client.query('CREATE SCHEMA IF NOT EXISTS lq_multi1');
await client.query('CREATE SCHEMA IF NOT EXISTS lq_multi2');
const result = await client.query('DROP SCHEMA IF EXISTS lq_multi1, lq_multi2 CASCADE');
expect(result).to.exist;
const namespaces = await client.storageEngine.namespaceNames();
expect(namespaces).to.not.include('lq_multi1');
expect(namespaces).to.not.include('lq_multi2');
});
// Advanced: RESTRICT should prevent drop if namespace not empty
it('should reject DROP SCHEMA ... RESTRICT when namespace not empty', async () => {
await client.query('CREATE SCHEMA IF NOT EXISTS lq_restrict');
await client.query('CREATE TABLE IF NOT EXISTS lq_restrict.tbl1 (id INT PRIMARY KEY)');
await expect(client.query('DROP SCHEMA lq_restrict RESTRICT')).to.be.rejected;
const result = await client.query('DROP SCHEMA lq_restrict CASCADE');
expect(result).to.exist;
const namespaces = await client.storageEngine.namespaceNames();
expect(namespaces).to.not.include('lq_restrict');
});
});
describe('CREATE TABLE', () => {
before(async () => {
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_table');
});
it('should create table in namespace', async () => {
const result = await client.query('CREATE TABLE lq_test_table.tbl1 (id INT PRIMARY KEY, name TEXT)');
const lq_test_table = await client.storageEngine.getNamespace('lq_test_table');
expect(result).to.exist;
const tables = await lq_test_table.tableNames();
expect(tables).to.include('tbl1');
});
it('should not create table if already exists', async () => {
await expect(client.query('CREATE TABLE lq_test_table.tbl1 (id INT PRIMARY KEY, name TEXT)')).to.be.rejected;
});
it('should support IF NOT EXISTS', async () => {
await expect(client.query('CREATE TABLE IF NOT EXISTS lq_test_table.tbl1 (id INT PRIMARY KEY, name TEXT)')).to.not.be.rejected;
});
// Advanced: TEMPORARY keyword should throw on in-mem engine
it('should throw on CREATE TEMPORARY TABLE', async () => {
await expect(client.query('CREATE TEMPORARY TABLE lq_test_table.temp_tbl (id INT)')).to.be.rejected;
});
});
describe('DROP TABLE', () => {
before(async () => {
await client.query('CREATE TABLE IF NOT EXISTS lq_test_table.tbl2 (id INT PRIMARY KEY)');
});
it('should drop table with IF EXISTS', async () => {
const result = await client.query('DROP TABLE IF EXISTS lq_test_table.tbl2');
expect(result).to.exist;
const lq_test_table = await client.storageEngine.getNamespace('lq_test_table');
const tables = await lq_test_table.tableNames();
expect(tables).to.not.include('tbl2');
});
it('should not fail dropping non-existent table', async () => {
await expect(client.query('DROP TABLE IF EXISTS lq_test_table.tbl2')).to.not.be.rejected;
});
it('should support dropping multiple tables', async () => {
await client.query('CREATE TABLE IF NOT EXISTS lq_test_table.tbl3 (id INT PRIMARY KEY)');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_table.tbl4 (id INT PRIMARY KEY)');
const result = await client.query('DROP TABLE IF EXISTS lq_test_table.tbl3, lq_test_table.tbl4');
expect(result).to.exist;
const lq_test_table = await client.storageEngine.getNamespace('lq_test_table');
const tables = await lq_test_table.tableNames();
expect(tables).to.not.include('tbl3');
expect(tables).to.not.include('tbl4');
});
// Advanced: CASCADE should drop dependent objects
it('should drop table with CASCADE when dependencies exist', async () => {
await client.query('CREATE TABLE IF NOT EXISTS lq_test_table.parent (id INT PRIMARY KEY)');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_table.child (id INT PRIMARY KEY, pid INT REFERENCES lq_test_table.parent(id))');
await expect(client.query('DROP TABLE lq_test_table.parent CASCADE')).to.not.be.rejected;
const lq_test_table = await client.storageEngine.getNamespace('lq_test_table');
const tables = await lq_test_table.tableNames();
expect(tables).to.not.include('parent');
//expect(tables).to.not.include('child'); // TODO
});
// Advanced: TEMPORARY keyword should throw on in-mem engine
it('should throw on DROP TEMPORARY TABLE', async () => {
await expect(client.query('DROP TEMPORARY TABLE lq_test_table.nonexistent', { dialect: 'mysql' })).to.be.rejected;
});
});
after(async () => {
await client.query('DROP SCHEMA lq_test_table CASCADE');
});
// ---------- ALTER (TODO)
});
describe('FlashClient - DDL Inference', () => {
let client;
before(async () => {
client = await createClient();
});
after(async () => {
await client.disconnect();
});
before(async () => {
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_show');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_show.tbl1 (id INT PRIMARY KEY)');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_show.tbl2 (id INT PRIMARY KEY)');
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_public');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_public.tbl1 (id INT PRIMARY KEY)');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_public.tbl2 (id INT PRIMARY KEY)');
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_private');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_private.tbl1 (id INT PRIMARY KEY)');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_private.tbl2 (id INT PRIMARY KEY)');
});
describe('SHOW CREATE', () => {
it('should show create for namespace', async () => {
const result = await client.showCreate({ lq_test_show: ['*'] }, true);
expect(result).to.have.lengthOf(1);
expect(result[0].name().value()).to.eq('lq_test_show');
expect(result[0].tables()).to.have.lengthOf(2);
});
it('should show create for specific table', async () => {
const result = await client.showCreate({ lq_test_show: ['tbl1'] }, true);
expect(result).to.have.lengthOf(1);
expect(result[0].tables()).to.have.lengthOf(1);
expect(result[0].tables()[0].name().value()).to.eq('tbl1');
});
it('should show create for negated table', async () => {
const result = await client.showCreate({ lq_test_show: ['!tbl1'] }, true);
expect(result).to.have.lengthOf(1);
expect(result[0].tables()).to.have.lengthOf(1);
expect(result[0].tables()[0].name().value()).to.eq('tbl2');
});
it('should show create for wildcard namespace', async () => {
const result = await client.showCreate({ ['*']: ['tbl1'] }, true);
expect(result.some(s => s.tables().some(t => t.name().value() === 'tbl1'))).to.be.true;
});
// --- Extended usage patterns ---
it('should showCreate() for given selector (1)', async () => {
const a = await client.showCreate({ lq_test_public: ['*'] }, true);
expect(a).to.have.lengthOf(1);
expect(a[0].name().value()).to.eq('lq_test_public');
const b = await client.showCreate([{ namespace: 'lq_test_public', tables: ['*'] }], true);
const c = await client.showCreate({ lq_test_public: ['*'] }, true);
expect(b).to.have.lengthOf(1);
expect(c).to.have.lengthOf(1);
expect(b[0].tables()).to.have.lengthOf(2);
expect(c[0].tables()).to.have.lengthOf(2);
expect(b[0].tables().map((t) => t.name().value())).to.deep.eq(['tbl1', 'tbl2']);
expect(c[0].tables().map((t) => t.name().value())).to.deep.eq(['tbl1', 'tbl2']);
});
it('should showCreate() for given selector (2)', async () => {
const b = await client.showCreate({ lq_test_public: ['tbl1'] }, true);
const c = await client.showCreate({ lq_test_public: ['!tbl1'] }, true);
expect(b).to.have.lengthOf(1);
expect(c).to.have.lengthOf(1);
expect(b[0].tables()).to.have.lengthOf(1);
expect(c[0].tables()).to.have.lengthOf(1);
expect(b[0].tables()[0].name().value()).to.eq('tbl1');
expect(c[0].tables()[0].name().value()).to.eq('tbl2');
});
it('should showCreate() for given selector (3)', async () => {
const b = await client.showCreate({ ['*']: ['tbl1'] }, true);
const c = await client.showCreate({ ['*']: ['!tbl1'] }, true);
expect(b).to.have.lengthOf(4); // Plus the default "public" namespace
expect(c).to.have.lengthOf(4); // Plus the default "public" namespace
expect(b[1].tables()).to.have.lengthOf(1);
expect(b[1].tables()[0].name().value()).to.eq('tbl1');
expect(c[2].tables()).to.have.lengthOf(1);
expect(b[2].tables()[0].name().value()).to.eq('tbl1');
expect(c[2].tables()[0].name().value()).to.eq('tbl2');
});
it('should showCreate() for given selector (4)', async () => {
const b = await client.showCreate({ ['*']: ['tbl1'] });
const c = await client.showCreate({ ['*']: ['*'] });
expect(b.map((t) => t.name().value())).to.deep.eq(['tbl1', 'tbl1', 'tbl1']);
expect(c.map((t) => t.name().value())).to.deep.eq(['tbl1', 'tbl2', 'tbl1', 'tbl2', 'tbl1', 'tbl2']);
});
});
describe('PROVIDE', () => {
it('should provide() the specified namespace', async () => {
const resultCode = await client.schemaInference.provide([{ namespace: 'lq_test_%', tables: ['tbl1'] }]);
const catalog = [...client.schemaInference.catalog];
expect(resultCode).to.eq(1);
expect(catalog).to.have.lengthOf(3);
const lq_test_show = catalog.find((s) => s.identifiesAs('lq_test_show'));
const lq_test_public = catalog.find((s) => s.identifiesAs('lq_test_public'));
const lq_test_private = catalog.find((s) => s.identifiesAs('lq_test_private'));
expect(lq_test_show.tables()).to.have.lengthOf(1);
expect(lq_test_public.tables()).to.have.lengthOf(1);
expect(lq_test_private.tables()).to.have.lengthOf(1);
// ----------------- heuristic caching
const resultCode2 = await client.schemaInference.provide([{ namespace: 'lq_test_%', tables: ['tbl1'] }]);
expect(resultCode2).to.eq(0);
const resultCode3 = await client.schemaInference.provide([{ namespace: 'lq_test_private', tables: ['tbl1'] }]);
expect(resultCode3).to.eq(0);
const resultCode4 = await client.schemaInference.provide([{ namespace: 'lq_test_foo', tables: ['tbl1'] }]);
expect(resultCode4).to.eq(0);
const resultCode5 = await client.schemaInference.provide([{ namespace: 'lq_test_%', tables: ['tbl1', 'tbl_1'] }]);
expect(resultCode5).to.eq(2);
});
it('should incrementally provide() the specified namespace', async () => {
const resultCode = await client.schemaInference.provide([{ namespace: 'lq_test_%', tables: ['tbl2'] }]);
const catalog = [...client.schemaInference.catalog];
expect(resultCode).to.eq(1);
expect(catalog).to.have.lengthOf(3);
const lq_test_show = catalog.find((s) => s.identifiesAs('lq_test_show'));
const lq_test_public = catalog.find((s) => s.identifiesAs('lq_test_public'));
const lq_test_private = catalog.find((s) => s.identifiesAs('lq_test_private'));
expect(lq_test_show.tables()).to.have.lengthOf(2);
expect(lq_test_public.tables()).to.have.lengthOf(2);
expect(lq_test_private.tables()).to.have.lengthOf(2);
});
});
});
describe('FlashClient - DML', () => {
let client;
// helper to read table storage rows (values)
async function tableRows(tableName, namespace = 'lq_test_dml') {
const namespaceObject = await await client.storageEngine.getNamespace(namespace);
const tableStorage = await namespaceObject.getTable(tableName);
const rows = [];
for await (const row of tableStorage) rows.push(row);
return rows;
}
// helper to clear tables by name
async function clearTable(tableName, namespace = 'lq_test_dml') {
const namespaceObject = await await client.storageEngine.getNamespace(namespace);
const tableStorage = await namespaceObject.getTable(tableName);
await tableStorage.truncate();
}
before(async () => {
client = await createClient();
// prepare namespace + tables used across the tests
await client.query('CREATE SCHEMA IF NOT EXISTS lq_test_dml');
// people: PK (manual), used for many tests
await client.query('CREATE TABLE IF NOT EXISTS lq_test_dml.people (id INT PRIMARY KEY, name TEXT, age INT)');
// identity / defaults tests
await client.query('CREATE TABLE IF NOT EXISTS lq_test_dml.auto_people (id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name TEXT)');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_dml.defaults (id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, cnt INT DEFAULT 7)');
// for update-from / join-based tests
await client.query('CREATE TABLE IF NOT EXISTS lq_test_dml.updates (person_id INT PRIMARY KEY, new_name TEXT)');
// for multi-table update/delete tests
await client.query('CREATE TABLE IF NOT EXISTS lq_test_dml.multi_a (id INT PRIMARY KEY, val INT)');
await client.query('CREATE TABLE IF NOT EXISTS lq_test_dml.multi_b (id INT PRIMARY KEY, val INT)');
// ensure tables are empty before tests start
for (const t of ['people', 'auto_people', 'defaults', 'updates', 'multi_a', 'multi_b']) {
try { await clearTable(t); } catch (e) { /* ignore */ }
}
});
after(async () => {
await client.disconnect();
});
// ---------- INSERT ----------
describe('INSERT variants', () => {
beforeEach(async () => {
// ensure clean slate
await clearTable('people');
await clearTable('auto_people');
await clearTable('defaults');
});
it('INSERT ... VALUES (single row)', async () => {
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (1, 'Alice', 30)");
const rows = await tableRows('people');
expect(rows).to.have.lengthOf(1);
expect(rows[0]).to.deep.include({ id: 1, name: 'Alice', age: 30 });
});
it('INSERT ... VALUES (multiple rows)', async () => {
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (2, 'Bob', 25), (3, 'Carol', 28)");
const rows = await tableRows('people');
expect(rows.map(r => r.id).sort()).to.deep.eq([2, 3]);
});
it('INSERT ... DEFAULT VALUES (Postgres) uses defaults', async () => {
// defaults table has id identity and cnt default 7
const res = await client.query("INSERT INTO lq_test_dml.defaults DEFAULT VALUES RETURNING *");
// client.query should return returning rows; fallback to storage inspection if not
if (res && res.rows) {
expect(res.rows[0]).to.have.property('cnt', 7);
expect(res.rows[0]).to.have.property('id');
} else {
const rows = await tableRows('defaults');
expect(rows[0]).to.have.property('cnt', 7);
}
});
it('MySQL: INSERT ... SET syntax', async () => {
// use mysql dialect for SET form
await client.query("INSERT INTO lq_test_dml.people SET id = 10, name = 'Zed', age = 50", { dialect: 'mysql' });
const rows = await tableRows('people');
expect(rows.some(r => r.id === 10 && r.name === 'Zed')).to.be.true;
});
it('INSERT ... RETURNING (Postgres)', async () => {
await clearTable('people');
const r = await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (20, 'X', 99) RETURNING id, name");
expect(r.rows).to.have.lengthOf(1);
expect(r.rows[0]).to.deep.eq({ id: 20, name: 'X' });
});
it('INSERT ... ON CONFLICT DO NOTHING (Postgres)', async () => {
await clearTable('people');
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (30, 'Sam', 40)");
const r = await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (30, 'SamX', 41) ON CONFLICT (id) DO NOTHING RETURNING *");
// returning should be empty
expect(r.rows).to.have.lengthOf(0);
// underlying row unchanged
const rows = await tableRows('people');
expect(rows.find(row => row.id === 30).name).to.eq('Sam');
});
it('INSERT ... ON CONFLICT DO UPDATE (Postgres)', async () => {
await clearTable('people');
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (31, 'Ann', 23)");
const r = await client.query(`
INSERT INTO lq_test_dml.people (id, name, age) VALUES (31, 'AnnX', 24)
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, age = EXCLUDED.age
RETURNING *`);
expect(r.rows[0]).to.deep.include({ id: 31, name: 'AnnX', age: 24 });
const rows = await tableRows('people');
expect(rows.find(x => x.id === 31).name).to.eq('AnnX');
});
it('MySQL: INSERT ... ON DUPLICATE KEY UPDATE', async () => {
await clearTable('people');
// Insert initial row
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (200, 'M', 60)", { dialect: 'mysql' });
// Duplicate insert with ON DUPLICATE KEY UPDATE
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (200, 'M2', 61) ON DUPLICATE KEY UPDATE name = VALUES(name), age = VALUES(age)", { dialect: 'mysql' });
const rows = await tableRows('people');
expect(rows.find(r => r.id === 200)).to.deep.include({ name: 'M2', age: 61 });
});
});
// ---------- UPDATE ----------
describe('UPDATE variants', () => {
beforeEach(async () => {
// reset tables used in updates
await clearTable('people');
await clearTable('updates');
await clearTable('multi_a');
await clearTable('multi_b');
// seed baseline data
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (401, 'U1', 20), (402, 'U2', 25)");
await client.query("INSERT INTO lq_test_dml.multi_a (id, val) VALUES (1, 10), (2, 20)");
await client.query("INSERT INTO lq_test_dml.multi_b (id, val) VALUES (1, 100), (2, 200)");
});
it('UPDATE ... SET ... WHERE (basic)', async () => {
await client.query("UPDATE lq_test_dml.people SET age = 21 WHERE id = 401");
const rows = await tableRows('people');
expect(rows.find(r => r.id === 401).age).to.eq(21);
});
it('UPDATE (Postgres) ... FROM join', async () => {
// prepare updates table
await client.query("INSERT INTO lq_test_dml.updates (person_id, new_name) VALUES (401, 'UpdatedU1')");
const r = await client.query(`
UPDATE lq_test_dml.people p
SET name = u.new_name
FROM lq_test_dml.updates u
WHERE p.id = u.person_id
RETURNING p.*`);
// returning should show updated name
expect(r.rows[0].name).to.eq('UpdatedU1');
const rows = await tableRows('people');
expect(rows.find(r => r.id === 401).name).to.eq('UpdatedU1');
});
it('UPDATE (Postgres) tuple assignment: SET (a,b) = (x,y)', async () => {
await client.query("UPDATE lq_test_dml.people SET (name, age) = ('Tupleed', 99) WHERE id = 402");
const rows = await tableRows('people');
expect(rows.find(r => r.id === 402)).to.deep.include({ name: 'Tupleed', age: 99 });
});
it('MySQL multi-table UPDATE (a, b syntax)', async () => {
// update multi_a.val from multi_b.val using mysql multi-table update
await client.query("UPDATE lq_test_dml.multi_a a, lq_test_dml.multi_b b SET a.val = b.val WHERE a.id = b.id", { dialect: 'mysql' });
const aRows = await tableRows('multi_a');
expect(aRows.find(r => r.id === 1).val).to.eq(100);
expect(aRows.find(r => r.id === 2).val).to.eq(200);
});
it('UPDATE returns empty when no rows match', async () => {
const r = await client.query("UPDATE lq_test_dml.people SET age = 999 WHERE id = 9999 RETURNING *");
expect(r.rows).to.have.lengthOf(0);
});
});
// ---------- DELETE ----------
describe('DELETE variants', () => {
beforeEach(async () => {
// reset multi tables and people
await clearTable('people');
await clearTable('multi_a');
await clearTable('multi_b');
await client.query("INSERT INTO lq_test_dml.people (id, name, age) VALUES (601, 'D1', 40), (602, 'D2', 50)");
await client.query("INSERT INTO lq_test_dml.multi_a (id, val) VALUES (10, 1), (11, 2)");
await client.query("INSERT INTO lq_test_dml.multi_b (id, val) VALUES (10, 1), (11, 2)");
});
it('DELETE ... WHERE (basic)', async () => {
await client.query("DELETE FROM lq_test_dml.people WHERE id = 601");
const rows = await tableRows('people');
expect(rows.find(r => r.id === 601)).to.be.undefined;
expect(rows.some(r => r.id === 602)).to.be.true;
});
it('DELETE ... RETURNING (Postgres)', async () => {
const r = await client.query("DELETE FROM lq_test_dml.people WHERE id = 602 RETURNING *");
expect(r.rows[0]).to.deep.include({ id: 602, name: 'D2' });
const rows = await tableRows('people');
expect(rows.find(r => r.id === 602)).to.be.undefined;
});
it('MySQL: multi-table DELETE a,b FROM ... JOIN ...', async () => {
// delete both multi_a and multi_b rows that match id=10
await client.query("DELETE a, b FROM lq_test_dml.multi_a a JOIN lq_test_dml.multi_b b ON a.id = b.id WHERE a.id = 10", { dialect: 'mysql' });
const aRows = await tableRows('multi_a');
const bRows = await tableRows('multi_b');
expect(aRows.find(r => r.id === 10)).to.be.undefined;
expect(bRows.find(r => r.id === 10)).to.be.undefined;
// ensure other rows remain
expect(aRows.find(r => r.id === 11)).to.exist;
expect(bRows.find(r => r.id === 11)).to.exist;
});
it('Postgres: DELETE ... USING ... (delete from a where exists in b)', async () => {
// re-seed
await clearTable('multi_a');
await clearTable('multi_b');
await client.query("INSERT INTO lq_test_dml.multi_a (id, val) VALUES (20, 1), (21, 2)");
await client.query("INSERT INTO lq_test_dml.multi_b (id, val) VALUES (20, 1)");
await client.query("DELETE FROM lq_test_dml.multi_a a USING lq_test_dml.multi_b b WHERE a.id = b.id");
const aRows = await tableRows('multi_a');
expect(aRows.find(r => r.id === 20)).to.be.undefined;
expect(aRows.find(r => r.id === 21)).to.exist;
});
it('DELETE non-existent rows does not fail', async () => {
const r = await client.query("DELETE FROM lq_test_dml.people WHERE id = 9999 RETURNING *");
expect(r.rows).to.have.lengthOf(0);
});
});
});
describe("FlashClient - DQL", () => {
describe("SELECT - Basics", () => {
let client, namespaceName = 'lq_test_dgl', t1 = "t1";
before(async () => {
client = await createClient(namespaceName);
// prepare namespace + tables used across the tests
await client.query(`CREATE SCHEMA IF NOT EXISTS ${namespaceName}`);
await client.query(`CREATE TABLE ${t1} (id INT PRIMARY KEY, val TEXT)`);
await client.query(`INSERT INTO ${t1} (id, val) VALUES (1, 'a'), (2, 'b'), (3, NULL)`);
});
it("should select a literal", async () => {
const { rows } = await client.query(`SELECT 1 AS x`);
expect(rows).to.deep.equal([{ x: 1 }]);
});
it("should select a single column", async () => {
const { rows } = await client.query(`SELECT id FROM ${t1}`);
expect(rows.map(r => r.id)).to.deep.equal([1, 2, 3]);
});
it("should alias a column", async () => {
const { rows } = await client.query(`SELECT id AS ident FROM ${t1}`);
expect(Object.keys(rows[0])).to.include("ident");
});
it("should select multiple columns", async () => {
const { rows } = await client.query(`SELECT id, val FROM ${t1}`);
expect(rows).to.have.length(3);
expect(rows[0]).to.have.keys(["id", "val"]);
});
it("should select all columns with *", async () => {
const { rows } = await client.query(`SELECT * FROM ${t1}`);
expect(rows[0]).to.have.keys(["id", "val"]);
});
it("should apply DISTINCT", async () => {
const { rows } = await client.query(`SELECT DISTINCT val FROM ${t1}`);
const values = rows.map(r => r.val);
expect(values).to.deep.equal(["a", "b", null]);
});
it("should filter with WHERE conditions", async () => {
const { rows } = await client.query(`SELECT id FROM ${t1} WHERE id > 1`);
expect(rows.map(r => r.id)).to.deep.equal([2, 3]);
});
it("should handle boolean expressions", async () => {
const { rows } = await client.query(`SELECT id FROM ${t1} WHERE id > 1 AND val IS NOT NULL`);
expect(rows.map(r => r.id)).to.deep.equal([2]);
});
it("should check for nulls correctly", async () => {
const { rows } = await client.query(`SELECT id FROM ${t1} WHERE val IS NULL`);
expect(rows.map(r => r.id)).to.deep.equal([3]);
});
});
describe("SELECT - FROM Variants", () => {
let client, namespaceName = 'lq_test_dgl', t1 = "t1";
before(async () => {
client = await createClient(namespaceName);
// prepare namespace + tables used across the tests
await client.query(`CREATE SCHEMA IF NOT EXISTS ${namespaceName}`);
await client.query(`CREATE TABLE ${t1} (id INT, val TEXT)`);
await client.query(`INSERT INTO ${t1} (id, val) VALUES (1, 'a'), (2, 'b')`);
});
it("should select from a table", async () => {
const { rows } = await client.query(`SELECT * FROM ${t1}`);
expect(rows).to.have.length(2);
});
it("should select from VALUES with alias and column names", async () => {
const { rows } = await client.query(`SELECT * FROM (VALUES (1, 'x'), (2, 'y')) AS v(c1, c2)`);
expect(rows).to.deep.equal([{ c1: 1, c2: "x" }, { c1: 2, c2: "y" }]);
});
it("should select from a subquery with alias", async () => {
const { rows } = await client.query(`SELECT sub.* FROM (SELECT id FROM ${t1}) AS sub`);
expect(rows).to.deep.eq([{ id: 1 }, { id: 2 }]);
});
it("should not error even when subquery has no alias", async () => {
const { rows } = await client.query(`SELECT * FROM (SELECT id FROM ${t1})`);
expect(rows).to.deep.eq([{ id: 1 }, { id: 2 }]);
});
it("should select from a function call (simulate unnest)", async () => {
const { rows } = await client.query(`SELECT * FROM unnest(ARRAY[10,20,30]) AS t(x)`);
expect(rows).to.deep.eq([{ x: 10 }, { x: 20 }, { x: 30 }]);
});
it("should select from ROWS FROM (multiple funcs)", async () => {
const { rows } = await client.query(`
SELECT * FROM ROWS FROM (generate_series(1, 2), unnest(ARRAY['a','b'])) AS t(c1, c2)
`);
expect(rows).to.deep.equal([{ c1: 1, c2: "a" }, { c1: 2, c2: "b" }]);
});
it("should select with WITH ORDINALITY", async () => {
const { rows } = await client.query(`
SELECT * FROM unnest(ARRAY['x','y']) WITH ORDINALITY AS t(val, ord)
`);
expect(rows).to.deep.equal([{ val: "x", ord: 1 }, { val: "y", ord: 2 }]);
});
});
describe("SELECT - Expressions & Operators", () => {
let client, namespaceName = 'lq_test_exprs';
before(async () => {
client = await createClient(namespaceName);
await client.query(`CREATE SCHEMA IF NOT EXISTS ${namespaceName}`);
// tables
await client.query(`CREATE TABLE expr_nums (id INT PRIMARY KEY, a INT, b INT, txt TEXT)`);
await client.query(`INSERT INTO expr_nums (id, a, b, txt) VALUES
(1, 10, 3, 'alpha'),
(2, 5, NULL, 'beta'),
(3, -2, 4, NULL)`); // includes NULLs, negative, etc.
});
after(async () => {
await client.disconnect();
});
it('arithmetic operators (+, -, *, /, %)', async () => {
const { rows } = await client.query(`SELECT id, a + 1 AS ap, a - b AS am, a * 2 AS amul, a / 2.0 AS adiv, a % 3 AS amod FROM expr_nums ORDER BY id`);
expect(rows).to.have.lengthOf(3);
expect(rows[0]).to.include({ id: 1, ap: 11, amul: 20 });
// division and modulo sanity
expect(rows.every(r => 'adiv' in r)).to.be.true;
});
it('comparison operators and NULL behavior', async () => {
const { rows } = await client.query(`
SELECT id FROM expr_nums
WHERE (a > 0 AND (b IS NULL OR a > b)) OR (a < 0)
ORDER BY id
`);
// rows: id=1 (10>3), id=2 (5>0 and b IS NULL), id=3 (a<0)
expect(rows.map(r => r.id)).to.deep.equal([1, 2, 3]);
});
it('BETWEEN and IN operators', async () => {
const { rows: r1 } = await client.query(`SELECT id FROM expr_nums WHERE a BETWEEN 0 AND 10 ORDER BY id`);
expect(r1.map(r => r.id)).to.deep.equal([1, 2]);
const { rows: r2 } = await client.query(`SELECT id FROM expr_nums WHERE id IN (1,3) ORDER BY id`);
expect(r2.map(r => r.id)).to.deep.equal([1, 3]);
});
it('LIKE and pattern matching', async () => {
const { rows } = await client.query(`SELECT id FROM expr_nums WHERE txt LIKE 'a%'`);
expect(rows.map(r => r.id)).to.deep.equal([1]);
});
it('COALESCE and NULLIF', async () => {
const { rows } = await client.query(`SELECT id, COALESCE(txt, 'missing') AS t, NULLIF(a, 10) AS nul FROM expr_nums ORDER BY id`);
expect(rows[0].t).to.eq('alpha');
expect(rows[2].t).to.eq('missing'); // txt NULL -> 'missing'
// NULLIF(a,10) should be null for id=1
expect(rows[0].nul).to.be.null;
});
it('CASE expressions (simple and searched)', async () => {
const { rows } = await client.query(`
SELECT id,
CASE id WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'other' END AS simpl,
CASE WHEN a > 0 THEN 'pos' WHEN a < 0 THEN 'neg' ELSE 'zero' END AS searched
FROM expr_nums ORDER BY id
`);
expect(rows.map(r => r.simpl)).to.deep.equal(['one', 'two', 'other']);
expect(rows.map(r => r.searched)).to.deep.equal(['pos', 'pos', 'neg']);
});
it('ANY / ALL with ARRAY (where available)', async () => {
// this is Postgres-style but many test engines emulate ARRAY syntax used elsewhere in tests
const { rows } = await client.query(`SELECT id FROM expr_nums WHERE a = ANY(ARRAY[10,5]) ORDER BY id`);
expect(rows.map(r => r.id)).to.deep.equal([1, 2]);
});
it('boolean precedence and parentheses', async () => {
const { rows: r1 } = await client.query(`SELECT id FROM expr_nums WHERE a > 0 AND b IS NULL OR a < 0 ORDER BY id`);
// Without parentheses this is (a>0 AND b IS NULL) OR (a<0) -> expect id=2 (5, null) and id=3 (a<0)
expect(r1.map(r => r.id)).to.deep.equal([2, 3]);
const { rows: r2 } = await client.query(`SELECT id FROM expr_nums WHERE a > 0 AND (b IS NULL OR a < 0) ORDER BY id`);
// With parentheses: a>0 AND (b IS NULL OR a<0) -> only id=2
expect(r2.map(r => r.id)).to.deep.equal([2]);
});
});
describe("SELECT - Joins (incl. LATERAL)", () => {
let client, namespaceName = 'lq_test_joins';
before(async () => {
client = await createClient(namespaceName);
await client.query(`CREATE SCHEMA IF NOT EXISTS ${namespaceName}`);
// join tables
await client.query(`CREATE TABLE ja (id INT PRIMARY KEY, aname TEXT)`);
await client.query(`CREATE TABLE jb (id INT PRIMARY KEY, bval TEXT)`);
await client.query(`INSERT INTO ja (id, aname) VALUES (1, 'A1'), (2, 'A2'), (3, 'A3')`);
await client.query(`INSERT INTO jb (id, bval) VALUES (1, 'B1'), (3, 'B3'), (4, 'B4')`);
// tables to test USING/duplicate column names
await client.query(`CREATE TABLE jc (id INT PRIMARY KEY, val INT)`);
await client.query(`CREATE TABLE jd (id INT PRIMARY KEY, val2 INT)`);
await client.query(`INSERT INTO jc (id,val) VALUES (1,10), (2,20)`);
await client.query(`INSERT INTO jd (id,val2) VALUES (1,100), (3,300)`);
// lateral table for expansion tests
await client.query(`CREATE TABLE lateral_nums (id INT PRIMARY KEY, n INT)`);
await client.query(`INSERT INTO lateral_nums (id, n) VALUES (1, 2), (2, 1), (3, 0)`);
});
after(async () => {
await client.disconnect();
});
it('INNER JOIN (explicit ON) yields only matching rows', async () => {
const { rows } = await client.query(`
SELECT a.id AS aid, a.aname, b.id AS bid, b.bval
FROM ja a JOIN jb b ON a.id = b.id
ORDER BY aid
`);
// matches on id=1 and id=3
expect(rows).to.have.lengthOf(2);
expect(rows.map(r => r.aid)).to.deep.equal([1, 3]);
expect(rows.some(r => r.bval === 'B3')).to.be.true;
});
it('implicit join (comma + WHERE) equals inner join', async () => {
const { rows } = await client.query(`
SELECT a.id AS aid, b.bval
FROM ja a, jb b
WHERE a.id = b.id
ORDER BY aid
`);
expect(rows).to.have.lengthOf(2);
expect(rows.map(r => r.aid)).to.deep.equal([1, 3]);
});
it('LEFT JOIN produces null-filled non-matching rows', async () => {
const { rows } = await client.query(`
SELECT a.id AS aid, b.bval
FROM ja a LEFT JOIN jb b ON a.id = b.id
ORDER BY a.id
`);
// should include all ja rows; for id=2, b.bval should be null
expect(rows.map(r => r.aid)).to.deep.equal([1, 2, 3]);
const row2 = rows.find(r => r.aid === 2);
expect(row2.bval).to.be.null;
});
it('RIGHT JOIN preserves right side rows (if supported)', async () => {
const { rows } = await client.query(`
SELECT a.id AS aid, b.id AS bid
FROM ja a RIGHT JOIN jb b ON a.id = b.id
ORDER BY bid
`);
// jb contains id 1,3,4 -> expect 3 rows, with aid null for id=4
expect(rows.map(r => r.bid)).to.deep.equal([1, 3, 4]);
expect(rows.find(r => r.bid === 4).aid).to.be.null;
});
it('FULL JOIN returns union of both sides (if supported)', async () => {
const { rows } = await client.query(`
SELECT COALESCE(a.id, b.id) AS id
FROM ja a FULL JOIN jb b ON a.id = b.id
ORDER BY id
`);
// ids should be 1,2,3,4
expect(rows.map(r => r.id)).to.deep.equal([1, 2, 3, 4]);
});
it('CROSS JOIN produces Cartesian product', async () => {
const { rows } = await client.query(`SELECT a.id AS aid, b.id AS bid FROM ja a CROSS JOIN jb b`);
// product size = 3 * 3 = 9
expect(rows).to.have.lengthOf(9);
});
it('NATURAL JOIN auto-matches common column names', async () => {
// natural join on id between jc and jd should return rows for id=1 only (both have id 1)
const { rows } = await client.query(`SELECT * FROM jc NATURAL JOIN jd ORDER BY id`);
// expect columns at least ['id','val','val2'] and single row
expect(rows).to.have.lengthOf(1);
expect(rows[0]).to.have.property('id', 1);
expect(rows[0]).to.have.property('val', 10);
expect(rows[0]).to.have.property('val2', 100);
});
it('JOIN ... USING merges join key into single column', async () => {
const { rows } = await client.query(`
SELECT id, val, val2 FROM jc JOIN jd USING (id) ORDER BY id
`);
// should have one row for id=1
expect(rows).to.have.lengthOf(1);
expect(rows[0]).to.deep.include({ id: 1, val: 10, val2: 100 });
});
it('JOIN ... ON vs JOIN ... USING behavior for column names', async () => {
const { rows: r1 } = await client.query(`
SELECT a.id AS aid, a.val AS aval, b.id AS bid, b.val2 AS bval FROM jc a JOIN jd b ON a.id = b.id ORDER BY a.id
`);
expect(r1).to.have.lengthOf(1);
expect(r1[0]).to.deep.include({ aid: 1, bid: 1, aval: 10, bval: 100 });
});
// ---------- LATERAL tests ----------
it('CROSS JOIN LATERAL with generate_series expands per-row', async () => {
// expects: (1,1),(1,2),(2,1) — id=3 with n=0 yields no rows
const { rows } = await client.query(`
SELECT t.id, s.val
FROM lateral_nums t
CROSS JOIN LATERAL generate_series(1, t.n) AS s(val)
ORDER BY t.id, s.val
`);
expect(rows).to.deep.equal([{ id: 1, val: 1 }, { id: 1, val: 2 }, { id: 2, val: 1 }]);
});
it('LEFT JOIN LATERAL keeps outer row when lateral yields nothing', async () => {
const { rows } = await client.query(`
SELECT t.id, s.val
FROM lateral_nums t
LEFT JOIN LATERAL generate_series(1, t.n) AS s(val) ON true
ORDER BY t.id, s.val
`);
// rows should include id=3 with val = null
const grouped = rows.reduce((acc, r) => {
(acc[r.id] || (acc[r.id] = [])).push(r.val);
return acc;
}, {});
expect(grouped[1]).to.deep.equal([1, 2]);
expect(grouped[2]).to.deep.equal([1]);
expect(grouped[3]).to.deep.equal([null]);
});
it('LATERAL subquery can reference outer columns', async () => {
const { rows } = await client.query(`
SELECT t.id, sub.dbl FROM lateral_nums t
JOIN LATERAL (SELECT t.n * 2 AS dbl) sub ON true
ORDER BY t.id
`);
// each outer row should have dbl = n*2
expect(rows).to.deep.equal([{ id: 1, dbl: 4 }, { id: 2, dbl: 2 }, { id: 3, dbl: 0 }]);
});
it('LATERAL with ROWS FROM and WITH ORDINALITY interplay', async () => {
// combine a function (generate_series) with unnest in ROWS FROM, lateralized
const { rows } = await client.query(`
SELECT t.id, r.x, r.y, r.ordinal
FROM lateral_nums t
CROSS JOIN LATERAL ROWS FROM (
generate_series(1, t.n),
unnest(ARRAY['u','v','w']) -- this will be padded/zip behaviour per engine
) WITH ORDINALITY AS r(x, y, ordinal)
WHERE t.n > 0
ORDER BY t.id, r.ordinal
`)
expect(rows.every(r => 'x' in r && 'y' in r && 'ordinal' in r)).to.be.true;
});
});
describe("SELECT - Ordering & Pagination", () => {
let client, namespa