@nymphjs/driver-sqlite3
Version:
Nymph.js - SQLite3 DB Driver
1,504 lines (1,460 loc) • 74.5 kB
text/typescript
import SQLite3 from 'better-sqlite3';
import {
NymphDriver,
type EntityConstructor,
type EntityData,
type EntityObjectType,
type EntityInterface,
type EntityInstanceType,
type SerializedEntityData,
type FormattedSelector,
type Options,
type Selector,
EntityUniqueConstraintError,
InvalidParametersError,
NotConfiguredError,
QueryFailedError,
UnableToConnectError,
xor,
} from '@nymphjs/nymph';
import { makeTableSuffix } from '@nymphjs/guid';
import {
SQLite3DriverConfig,
SQLite3DriverConfigDefaults as defaults,
} from './conf/index.js';
class InternalStore {
public link: SQLite3.Database;
public linkWrite?: SQLite3.Database;
public connected: boolean = false;
public transactionsStarted = 0;
constructor(link: SQLite3.Database) {
this.link = link;
}
}
/**
* The SQLite3 Nymph database driver.
*/
export default class SQLite3Driver extends NymphDriver {
public config: SQLite3DriverConfig;
protected prefix: string;
// @ts-ignore: this is assigned in connect(), which is called by the constructor.
protected store: InternalStore;
static escape(input: string) {
if (input.indexOf('\x00') !== -1) {
throw new InvalidParametersError(
'SQLite3 identifiers (like entity ETYPE) cannot contain null characters.',
);
}
return '"' + input.replace(/"/g, () => '""') + '"';
}
constructor(config: Partial<SQLite3DriverConfig>, store?: InternalStore) {
super();
this.config = { ...defaults, ...config };
if (this.config.filename === ':memory:') {
this.config.explicitWrite = true;
}
this.prefix = this.config.prefix;
if (store) {
this.store = store;
} else {
this.connect();
}
}
/**
* This is used internally by Nymph. Don't call it yourself.
*
* @returns A clone of this instance.
*/
public clone() {
return new SQLite3Driver(this.config, this.store);
}
/**
* Connect to the SQLite3 database.
*
* @returns Whether this instance is connected to a SQLite3 database.
*/
public connect() {
if (this.store && this.store.connected) {
return Promise.resolve(true);
}
// Connecting
this._connect(false);
return Promise.resolve(this.store.connected);
}
private _connect(write: boolean) {
const { filename, fileMustExist, timeout, explicitWrite, wal, verbose } =
this.config;
try {
const setOptions = (link: SQLite3.Database) => {
// Set database and connection options.
if (wal) {
link.pragma('journal_mode = WAL;');
}
link.pragma('encoding = "UTF-8";');
link.pragma('foreign_keys = 1;');
link.pragma('case_sensitive_like = 1;');
for (let pragma of this.config.pragmas) {
link.pragma(pragma);
}
// Create the preg_match and regexp functions.
link.function('regexp', { deterministic: true }, ((
pattern: string,
subject: string,
) => (this.posixRegexMatch(pattern, subject) ? 1 : 0)) as (
...params: any[]
) => any);
};
let link: SQLite3.Database;
try {
link = new SQLite3(filename, {
readonly: !explicitWrite && !write,
fileMustExist,
timeout,
verbose,
});
} catch (e: any) {
if (
e.code === 'SQLITE_CANTOPEN' &&
!explicitWrite &&
!write &&
!this.config.fileMustExist
) {
// This happens when the file doesn't exist and we attempt to open it
// readonly.
// First open it in write mode.
const writeLink = new SQLite3(filename, {
readonly: false,
fileMustExist,
timeout,
verbose,
});
setOptions(writeLink);
writeLink.close();
// Now open in readonly.
link = new SQLite3(filename, {
readonly: true,
fileMustExist,
timeout,
verbose,
});
} else {
throw e;
}
}
if (!this.store) {
if (write) {
throw new Error(
'Tried to open in write without opening in read first.',
);
}
this.store = new InternalStore(link);
} else if (write) {
this.store.linkWrite = link;
} else {
this.store.link = link;
}
this.store.connected = true;
setOptions(link);
} catch (e: any) {
if (this.store) {
this.store.connected = false;
}
if (filename === ':memory:') {
throw new NotConfiguredError(
"It seems the config hasn't been set up correctly. Could not connect: " +
e?.message,
);
} else {
throw new UnableToConnectError('Could not connect: ' + e?.message);
}
}
}
/**
* Disconnect from the SQLite3 database.
*
* @returns Whether this instance is connected to a SQLite3 database.
*/
public async disconnect() {
if (this.store.connected) {
if (this.store.linkWrite && !this.config.explicitWrite) {
this.store.linkWrite.exec('PRAGMA optimize;');
this.store.linkWrite.close();
this.store.linkWrite = undefined;
}
if (this.config.explicitWrite) {
this.store.link.exec('PRAGMA optimize;');
}
this.store.link.close();
this.store.transactionsStarted = 0;
this.store.connected = false;
}
return this.store.connected;
}
public async inTransaction() {
return this.store.transactionsStarted > 0;
}
/**
* Check connection status.
*
* @returns Whether this instance is connected to a SQLite3 database.
*/
public isConnected() {
return this.store.connected;
}
/**
* Create entity tables in the database.
*
* @param etype The entity type to create a table for. If this is blank, the default tables are created.
*/
private createTables(etype: string | null = null) {
this.startTransaction('nymph-tablecreation');
try {
if (etype != null) {
// Create the entity table.
this.queryRun(
`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} ("guid" CHARACTER(24) PRIMARY KEY, "tags" TEXT, "cdate" REAL NOT NULL, "mdate" REAL NOT NULL);`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}_id_cdate`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} ("cdate");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}_id_mdate`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} ("mdate");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}_id_tags`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} ("tags");`,
);
// Create the data table.
this.queryRun(
`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "value" CHARACTER(1) NOT NULL, "json" BLOB, "string" TEXT, "number" REAL, "truthy" INTEGER, PRIMARY KEY("guid", "name"));`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_guid`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("guid");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_guid_name`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("guid", "name");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_guid__name_user`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("guid") WHERE "name" = \'user\';`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_guid__name_group`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("guid") WHERE "name" = \'group\';`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_name`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("name");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_name__truthy`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("name") WHERE "truthy" = 1;`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_name__falsy`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("name") WHERE "truthy" <> 1;`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_name_string`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("name", "string");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}data_${etype}_id_name_number`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} ("name", "number");`,
);
// Create the references table.
this.queryRun(
`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}references_${etype}`,
)} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "reference" CHARACTER(24) NOT NULL, PRIMARY KEY("guid", "name", "reference"));`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}references_${etype}_id_guid`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}references_${etype}`,
)} ("guid");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}references_${etype}_id_name`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}references_${etype}`,
)} ("name");`,
);
this.queryRun(
`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}references_${etype}_id_name_reference`,
)} ON ${SQLite3Driver.escape(
`${this.prefix}references_${etype}`,
)} ("name", "reference");`,
);
// Create the unique strings table.
this.queryRun(
`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}uniques_${etype}`,
)} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} ("guid") ON DELETE CASCADE, "unique" TEXT NOT NULL UNIQUE, PRIMARY KEY("guid", "unique"));`,
);
} else {
// Create the UID table.
this.queryRun(
`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
`${this.prefix}uids`,
)} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`,
);
}
} catch (e: any) {
this.rollback('nymph-tablecreation');
throw e;
}
this.commit('nymph-tablecreation');
return true;
}
private query<T extends () => any>(
runQuery: T,
query: string,
etypes: string[] = [],
): ReturnType<T> {
try {
this.nymph.config.debugInfo('sqlite3:query', query);
return runQuery();
} catch (e: any) {
const errorCode = e?.code;
const errorMsg = e?.message;
if (
errorCode === 'SQLITE_ERROR' &&
errorMsg.match(/^no such table: /) &&
this.createTables()
) {
for (let etype of etypes) {
this.createTables(etype);
}
try {
return runQuery();
} catch (e2: any) {
throw new QueryFailedError(
'Query failed: ' + e2?.code + ' - ' + e2?.message,
query,
);
}
} else if (
errorCode === 'SQLITE_CONSTRAINT_UNIQUE' &&
errorMsg.match(/^UNIQUE constraint failed: /)
) {
throw new EntityUniqueConstraintError(`Unique constraint violation.`);
} else {
throw new QueryFailedError(
'Query failed: ' + e?.code + ' - ' + e?.message,
query,
);
}
}
}
private queryArray(
query: string,
{
etypes = [],
params = {},
}: { etypes?: string[]; params?: { [k: string]: any } } = {},
) {
return this.query(
() =>
(this.store.linkWrite || this.store.link)
.prepare(query)
.iterate(params),
`${query} -- ${JSON.stringify(params)}`,
etypes,
);
}
private queryGet(
query: string,
{
etypes = [],
params = {},
}: { etypes?: string[]; params?: { [k: string]: any } } = {},
) {
return this.query(
() =>
(this.store.linkWrite || this.store.link).prepare(query).get(params),
`${query} -- ${JSON.stringify(params)}`,
etypes,
);
}
private queryRun(
query: string,
{
etypes = [],
params = {},
}: { etypes?: string[]; params?: { [k: string]: any } } = {},
) {
return this.query(
() =>
(this.store.linkWrite || this.store.link).prepare(query).run(params),
`${query} -- ${JSON.stringify(params)}`,
etypes,
);
}
public async commit(name: string) {
if (name == null || typeof name !== 'string' || name.length === 0) {
throw new InvalidParametersError(
'Transaction commit attempted without a name.',
);
}
if (this.store.transactionsStarted === 0) {
return true;
}
this.queryRun(`RELEASE SAVEPOINT ${SQLite3Driver.escape(name)};`);
this.store.transactionsStarted--;
if (
this.store.transactionsStarted === 0 &&
this.store.linkWrite &&
!this.config.explicitWrite
) {
this.store.linkWrite.exec('PRAGMA optimize;');
this.store.linkWrite.close();
this.store.linkWrite = undefined;
}
return true;
}
public async deleteEntityByID(
guid: string,
className?: EntityConstructor | string | null,
) {
let EntityClass: EntityConstructor;
if (typeof className === 'string' || className == null) {
const GetEntityClass = this.nymph.getEntityClass(className ?? 'Entity');
EntityClass = GetEntityClass;
} else {
EntityClass = className;
}
const etype = EntityClass.ETYPE;
await this.startTransaction('nymph-delete');
try {
this.queryRun(
`DELETE FROM ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} WHERE "guid"=@guid;`,
{
etypes: [etype],
params: {
guid,
},
},
);
this.queryRun(
`DELETE FROM ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} WHERE "guid"=@guid;`,
{
etypes: [etype],
params: {
guid,
},
},
);
this.queryRun(
`DELETE FROM ${SQLite3Driver.escape(
`${this.prefix}references_${etype}`,
)} WHERE "guid"=@guid;`,
{
etypes: [etype],
params: {
guid,
},
},
);
this.queryRun(
`DELETE FROM ${SQLite3Driver.escape(
`${this.prefix}uniques_${etype}`,
)} WHERE "guid"=@guid;`,
{
etypes: [etype],
params: {
guid,
},
},
);
} catch (e: any) {
this.nymph.config.debugError('sqlite3', `Delete entity error: "${e}"`);
await this.rollback('nymph-delete');
throw e;
}
await this.commit('nymph-delete');
// Remove any cached versions of this entity.
if (this.nymph.config.cache) {
this.cleanCache(guid);
}
return true;
}
public async deleteUID(name: string) {
if (!name) {
throw new InvalidParametersError('Name not given for UID');
}
await this.startTransaction('nymph-delete-uid');
this.queryRun(
`DELETE FROM ${SQLite3Driver.escape(
`${this.prefix}uids`,
)} WHERE "name"=@name;`,
{
params: {
name,
},
},
);
await this.commit('nymph-delete-uid');
return true;
}
public async *exportDataIterator(): AsyncGenerator<
{ type: 'comment' | 'uid' | 'entity'; content: string },
void,
false | undefined
> {
if (
yield {
type: 'comment',
content: `#nex2
# Nymph Entity Exchange v2
# http://nymph.io
#
# Generation Time: ${new Date().toLocaleString()}
`,
}
) {
return;
}
if (
yield {
type: 'comment',
content: `
#
# UIDs
#
`,
}
) {
return;
}
// Export UIDs.
let uids: IterableIterator<any> = this.queryArray(
`SELECT * FROM ${SQLite3Driver.escape(
`${this.prefix}uids`,
)} ORDER BY "name";`,
);
for (const uid of uids) {
if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
return;
}
}
if (
yield {
type: 'comment',
content: `
#
# Entities
#
`,
}
) {
return;
}
// Get the etypes.
const tables: IterableIterator<any> = this.queryArray(
"SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @prefix;",
{
params: {
prefix: this.prefix + 'entities_' + '%',
},
},
);
const etypes = [];
for (const table of tables) {
etypes.push(table.name.substr((this.prefix + 'entities_').length));
}
for (const etype of etypes) {
// Export entities.
const dataIterator: IterableIterator<any> = this.queryArray(
`SELECT e.*, d."name", d."value", json(d."json") as "json", d."string", d."number" FROM ${SQLite3Driver.escape(
`${this.prefix}entities_${etype}`,
)} e LEFT JOIN ${SQLite3Driver.escape(
`${this.prefix}data_${etype}`,
)} d USING ("guid") ORDER BY e."guid";`,
)[Symbol.iterator]();
let datum = dataIterator.next();
while (!datum.done) {
const guid = datum.value.guid;
const tags = datum.value.tags.slice(1, -1);
const cdate = datum.value.cdate;
const mdate = datum.value.mdate;
let currentEntityExport: string[] = [];
currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
if (datum.value.name != null) {
// This do will keep going and adding the data until the
// next entity is reached. datum will end on the next entity.
do {
const value =
datum.value.value === 'N'
? JSON.stringify(datum.value.number)
: datum.value.value === 'S'
? JSON.stringify(datum.value.string)
: datum.value.value === 'J'
? datum.value.json
: datum.value.value;
currentEntityExport.push(`\t${datum.value.name}=${value}`);
datum = dataIterator.next();
} while (!datum.done && datum.value.guid === guid);
} else {
// Make sure that datum is incremented :)
datum = dataIterator.next();
}
currentEntityExport.push('');
if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
return;
}
}
}
}
/**
* Generate the SQLite3 query.
* @param options The options array.
* @param formattedSelectors The formatted selector array.
* @param etype
* @param count Used to track internal params.
* @param params Used to store internal params.
* @param subquery Whether only a subquery should be returned.
* @returns The SQL query.
*/
private makeEntityQuery(
options: Options,
formattedSelectors: FormattedSelector[],
etype: string,
count = { i: 0 },
params: { [k: string]: any } = {},
subquery = false,
tableSuffix = '',
etypes: string[] = [],
guidSelector: string | undefined = undefined,
) {
if (typeof options.class?.alterOptions === 'function') {
options = options.class.alterOptions(options);
}
const eTable = `e${tableSuffix}`;
const dTable = `d${tableSuffix}`;
const fTable = `f${tableSuffix}`;
const ieTable = `ie${tableSuffix}`;
const sTable = `s${tableSuffix}`;
const sort = options.sort ?? 'cdate';
const queryParts = this.iterateSelectorsForQuery(
formattedSelectors,
({ key, value, typeIsOr, typeIsNot }) => {
const clauseNot = key.startsWith('!');
let curQuery = '';
for (const curValue of value) {
switch (key) {
case 'guid':
case '!guid':
for (const curGuid of curValue) {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const guid = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."guid"=@' +
guid;
params[guid] = curGuid;
}
break;
case 'tag':
case '!tag':
for (const curTag of curValue) {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const tag = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."tags" LIKE @' +
tag +
" ESCAPE '\\'";
params[tag] =
'%,' +
curTag
.replace('\\', '\\\\')
.replace('%', '\\%')
.replace('_', '\\_') +
',%';
}
break;
case 'defined':
case '!defined':
for (const curVar of curValue) {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
curQuery +=
ieTable +
'."guid" ' +
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'IN (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "name"=@' +
name +
')';
params[name] = curVar;
}
break;
case 'truthy':
case '!truthy':
for (const curVar of curValue) {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
if (curVar === 'cdate') {
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."cdate" NOT NULL)';
break;
} else if (curVar === 'mdate') {
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."mdate" NOT NULL)';
break;
} else {
const name = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "truthy"=1)';
params[name] = curVar;
}
}
break;
case 'equal':
case '!equal':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."cdate"=@' +
cdate;
params[cdate] = Number(curValue[1]);
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."mdate"=@' +
mdate;
params[mdate] = Number(curValue[1]);
break;
} else if (typeof curValue[1] === 'number') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "number"=@' +
value +
')';
params[name] = curValue[0];
params[value] = curValue[1];
} else if (typeof curValue[1] === 'string') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "string"=@' +
value +
')';
params[name] = curValue[0];
params[value] = curValue[1];
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
let svalue: string;
if (
curValue[1] instanceof Object &&
typeof curValue[1].toReference === 'function'
) {
svalue = JSON.stringify(curValue[1].toReference());
} else {
svalue = JSON.stringify(curValue[1]);
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "json"=jsonb(@' +
value +
'))';
params[name] = curValue[0];
params[value] = svalue;
}
break;
case 'contain':
case '!contain':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."cdate"=@' +
cdate;
params[cdate] = Number(curValue[1]);
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."mdate"=@' +
mdate;
params[mdate] = Number(curValue[1]);
break;
} else {
const containTableSuffix = makeTableSuffix();
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
let svalue: string;
if (
curValue[1] instanceof Object &&
typeof curValue[1].toReference === 'function'
) {
svalue = JSON.stringify(curValue[1].toReference());
} else {
svalue = JSON.stringify(curValue[1]);
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' d' +
containTableSuffix +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND json(@' +
value +
') IN (SELECT json_quote("value") FROM json_each(d' +
containTableSuffix +
'."json")))';
params[name] = curValue[0];
params[value] = svalue;
}
break;
case 'match':
case '!match':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."cdate" REGEXP @' +
cdate +
')';
params[cdate] = curValue[1];
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."mdate" REGEXP @' +
mdate +
')';
params[mdate] = curValue[1];
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "string" REGEXP @' +
value +
')';
params[name] = curValue[0];
params[value] = curValue[1];
}
break;
case 'imatch':
case '!imatch':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."cdate" REGEXP @' +
cdate +
')';
params[cdate] = curValue[1];
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."mdate" REGEXP @' +
mdate +
')';
params[mdate] = curValue[1];
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND lower("string") REGEXP lower(@' +
value +
'))';
params[name] = curValue[0];
params[value] = curValue[1];
}
break;
case 'like':
case '!like':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."cdate" LIKE @' +
cdate +
" ESCAPE '\\')";
params[cdate] = curValue[1];
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."mdate" LIKE @' +
mdate +
" ESCAPE '\\')";
params[mdate] = curValue[1];
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "string" LIKE @' +
value +
" ESCAPE '\\')";
params[name] = curValue[0];
params[value] = curValue[1];
}
break;
case 'ilike':
case '!ilike':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."cdate" LIKE @' +
cdate +
" ESCAPE '\\')";
params[cdate] = curValue[1];
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
ieTable +
'."mdate" LIKE @' +
mdate +
" ESCAPE '\\')";
params[mdate] = curValue[1];
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND lower("string") LIKE lower(@' +
value +
") ESCAPE '\\')";
params[name] = curValue[0];
params[value] = curValue[1];
}
break;
case 'gt':
case '!gt':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."cdate">@' +
cdate;
params[cdate] = Number(curValue[1]);
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."mdate">@' +
mdate;
params[mdate] = Number(curValue[1]);
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "number">@' +
value +
')';
params[name] = curValue[0];
params[value] = Number(curValue[1]);
}
break;
case 'gte':
case '!gte':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."cdate">=@' +
cdate;
params[cdate] = Number(curValue[1]);
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."mdate">=@' +
mdate;
params[mdate] = Number(curValue[1]);
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "number">=@' +
value +
')';
params[name] = curValue[0];
params[value] = Number(curValue[1]);
}
break;
case 'lt':
case '!lt':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."cdate"<@' +
cdate;
params[cdate] = Number(curValue[1]);
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."mdate"<@' +
mdate;
params[mdate] = Number(curValue[1]);
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "number"<@' +
value +
')';
params[name] = curValue[0];
params[value] = Number(curValue[1]);
}
break;
case 'lte':
case '!lte':
if (curValue[0] === 'cdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const cdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."cdate"<=@' +
cdate;
params[cdate] = Number(curValue[1]);
break;
} else if (curValue[0] === 'mdate') {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const mdate = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
ieTable +
'."mdate"<=@' +
mdate;
params[mdate] = Number(curValue[1]);
break;
} else {
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const value = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'data_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "number"<=@' +
value +
')';
params[name] = curValue[0];
params[value] = Number(curValue[1]);
}
break;
case 'ref':
case '!ref':
let curQguid: string;
if (typeof curValue[1] === 'string') {
curQguid = curValue[1];
} else if ('guid' in curValue[1]) {
curQguid = curValue[1].guid;
} else {
curQguid = `${curValue[1]}`;
}
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const name = `param${++count.i}`;
const guid = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'references_' + etype) +
' WHERE "guid"=' +
ieTable +
'."guid" AND "name"=@' +
name +
' AND "reference"=@' +
guid +
')';
params[name] = curValue[0];
params[guid] = curQguid;
break;
case 'selector':
case '!selector':
const subquery = this.makeEntityQuery(
options,
[curValue],
etype,
count,
params,
true,
tableSuffix,
etypes,
);
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'(' +
subquery.query +
')';
break;
case 'qref':
case '!qref':
const referenceTableSuffix = makeTableSuffix();
const [qrefOptions, ...qrefSelectors] = curValue[1] as [
Options,
...FormattedSelector[],
];
const QrefEntityClass = qrefOptions.class as EntityConstructor;
etypes.push(QrefEntityClass.ETYPE);
const qrefQuery = this.makeEntityQuery(
{ ...qrefOptions, return: 'guid', class: QrefEntityClass },
qrefSelectors,
QrefEntityClass.ETYPE,
count,
params,
false,
makeTableSuffix(),
etypes,
'r' + referenceTableSuffix + '."reference"',
);
if (curQuery) {
curQuery += typeIsOr ? ' OR ' : ' AND ';
}
const qrefName = `param${++count.i}`;
curQuery +=
(xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
'EXISTS (SELECT "guid" FROM ' +
SQLite3Driver.escape(this.prefix + 'references_' + etype) +
' r' +
referenceTableSuffix +
' WHERE r' +
referenceTableSuffix +
'."guid"=' +
ieTable +
'."guid" AND r' +
referenceTableSuffix +
'."name"=@' +
qrefName +
' AND EXISTS (' +
qrefQuery.query +
'))';
params[qrefName] = curValue[0];
break;
}
}
return curQuery;
},
);
let sortBy: string;
let sortByInner: string;
let sortJoin = '';
const order = options.reverse ? ' DESC' : '';
switch (sort) {
case 'mdate':
sortBy = `${eTable}."mdate"${order}`;
sortByInner = `${ieTable}."mdate"${order}`;
break;
case 'cdate':
sortBy = `${eTable}."cdate"${order}`;
sortByInner = `${ieTable}."cdate"${order}`;
break;
default:
const name = `param${++count.i}`;
sortJoin = `LEFT JOIN (
SELECT "guid", "string", "number"
FROM ${SQLite3Driver.escape(this.prefix + 'data_' + etype)}
WHERE "name"=@${name}
ORDER BY "number"${order}, "string"${order}