1
0
mirror of https://github.com/nextapps-de/flexsearch.git synced 2025-09-25 12:58:59 +02:00
Files
flexsearch/dist/module-debug/db/sqlite/index.js
2025-04-14 14:22:10 +02:00

793 lines
26 KiB
JavaScript

//const sqlite3 = require("sqlite3").verbose();
import sqlite3 from "sqlite3";
import path from "path";
import StorageInterface from "../interface.js";
import { concat, toArray } from "../../common.js";
const VERSION = 1,
MAXIMUM_QUERY_VARS = 16000,
fields = ["map", "ctx", "reg", "tag", "cfg"],
types = {
text: "text",
char: "text",
varchar: "text",
string: "text",
number: "int",
numeric: "int",
integer: "int",
smallint: "int",
tinyint: "int",
mediumint: "int",
int: "int",
int8: "int",
uint8: "int",
int16: "int",
uint16: "int",
int32: "int",
uint32: "bigint",
int64: "bigint",
bigint: "bigint"
};
function sanitize(str) {
return str.toLowerCase().replace(/[^a-z0-9_]/g, "");
}
// global transaction to keep track of database lock
const TRX = Object.create(null),
Index = Object.create(null);
/**
* @constructor
* @implements StorageInterface
*/
export default function SqliteDB(name, config = {}) {
if (!this || this.constructor !== SqliteDB) {
return new SqliteDB(name, config);
}
if ("object" == typeof name) {
config = name;
name = name.name;
}
if (!name) {
console.info("Default storage space was used, because a name was not passed.");
}
//field = "Test-456";
this.id = config.path || (":memory:" === name ? name : "flexsearch" + (name ? "-" + sanitize(name) : "") + ".sqlite");
this.field = config.field ? "_" + sanitize(config.field) : "";
this.support_tag_search = /* tag? */ /* stringify */ /* stringify */ /* single param */!0 /*await rows.hasNext()*/
/*await rows.hasNext()*/ /*await rows.hasNext()*/;
this.db = config.db || Index[this.id] || null;
this.type = config.type ? types[config.type.toLowerCase()] : "string";
if (!this.type) throw new Error("Unknown type of ID '" + config.type + "'");
}
SqliteDB.prototype.mount = function (flexsearch) {
//if(flexsearch.constructor === Document){
if (flexsearch.index) {
return flexsearch.mount(this);
}
flexsearch.db = this;
return this.open();
};
SqliteDB.prototype.open = async function () {
if (!this.db) {
if (!(this.db = Index[this.id])) {
let filepath = this.id;
if (":memory:" !== filepath) {
// skip absolute path
if ("/" !== filepath[0] && "\\" !== filepath[0]) {
// current working directory
const dir = process.cwd();
filepath = path.join(dir, this.id);
}
}
this.db = Index[this.id] = new sqlite3.Database(filepath);
}
}
const db = this.db;
for (let i = 0; i < fields.length; i++) {
const exist = await this.promisfy({
method: "get",
stmt: "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?) as exist",
params: [fields[i] + ("reg" === fields[i] ? "" : this.field)]
});
if (!exist || !exist.exist) {
let stmt, stmt_index;
switch (fields[i]) {
case "map":
stmt = `
CREATE TABLE IF NOT EXISTS main.map${this.field}(
key TEXT NOT NULL,
res INTEGER NOT NULL,
id ${this.type} NOT NULL
);
`;
stmt_index = `
CREATE INDEX IF NOT EXISTS map_key_index${this.field}
ON map${this.field} (key);
CREATE INDEX IF NOT EXISTS map_id_index${this.field}
ON map${this.field} (id);
`;
break;
case "ctx":
stmt = `
CREATE TABLE IF NOT EXISTS main.ctx${this.field}(
ctx TEXT NOT NULL,
key TEXT NOT NULL,
res INTEGER NOT NULL,
id ${this.type} NOT NULL
);
`;
stmt_index = `
CREATE INDEX IF NOT EXISTS ctx_key_index${this.field}
ON ctx${this.field} (ctx, key);
CREATE INDEX IF NOT EXISTS ctx_id_index${this.field}
ON ctx${this.field} (id);
`;
break;
case "tag":
stmt = `
CREATE TABLE IF NOT EXISTS main.tag${this.field}(
tag TEXT NOT NULL,
id ${this.type} NOT NULL
);
`;
stmt_index = `
CREATE INDEX IF NOT EXISTS tag_index${this.field}
ON tag${this.field} (tag);
CREATE INDEX IF NOT EXISTS tag_id_index${this.field}
ON tag${this.field} (id);
`;
break;
case "reg":
stmt = `
CREATE TABLE IF NOT EXISTS main.reg(
id ${this.type} NOT NULL
CONSTRAINT reg_pk${this.field}
PRIMARY KEY,
doc TEXT DEFAULT NULL
);
`;
stmt_index = `
CREATE INDEX IF NOT EXISTS reg_index
ON reg (id);
`;
break;
case "cfg":
stmt = `
CREATE TABLE IF NOT EXISTS main.cfg${this.field} (
cfg TEXT NOT NULL
);
`;
break;
}
await new Promise(function (resolve, reject) {
db.exec(stmt, function (err, rows) {
if (err) return reject(err);
stmt_index ? db.exec(stmt_index, function (err, rows) {
if (err) return reject(err);
resolve(rows);
}) : resolve(rows);
});
});
}
}
db.exec("PRAGMA optimize = 0x10002");
return db;
};
SqliteDB.prototype.close = function () {
this.db && this.db.close();
this.db = null;
Index[this.id] = null;
TRX[this.id] = null;
return this;
};
SqliteDB.prototype.destroy = function () {
return this.transaction(function () {
this.db.run("DROP TABLE IF EXISTS main.map" + this.field + ";");
this.db.run("DROP TABLE IF EXISTS main.ctx" + this.field + ";");
this.db.run("DROP TABLE IF EXISTS main.tag" + this.field + ";");
this.db.run("DROP TABLE IF EXISTS main.cfg" + this.field + ";");
this.db.run("DROP TABLE IF EXISTS main.reg;");
});
};
SqliteDB.prototype.clear = function () {
return this.transaction(function () {
this.db.run("DELETE FROM main.map" + this.field + " WHERE 1;");
this.db.run("DELETE FROM main.ctx" + this.field + " WHERE 1;");
this.db.run("DELETE FROM main.tag" + this.field + " WHERE 1;");
this.db.run("DELETE FROM main.cfg" + this.field + " WHERE 1;");
this.db.run("DELETE FROM main.reg WHERE 1;");
});
};
function create_result(rows, resolve, enrich) {
if (resolve) {
for (let i = 0; i < rows.length; i++) {
if (enrich) {
if (rows[i].doc) {
rows[i].doc = JSON.parse(rows[i].doc);
}
} else {
rows[i] = rows[i].id;
}
}
return rows;
} else {
const arr = [];
for (let i = 0, row; i < rows.length; i++) {
row = rows[i];
arr[row.res] || (arr[row.res] = []);
arr[row.res].push(enrich ? row : row.id);
}
return arr;
}
}
SqliteDB.prototype.get = function (key, ctx, limit = 0, offset = 0, resolve = !0, enrich = /* suggest */!1, tags) {
let result,
stmt = '',
params = ctx ? [ctx, key] : [key],
table = "main." + (ctx ? "ctx" : "map") + this.field;
if (tags) {
for (let i = 0; i < tags.length; i += 2) {
stmt += ` AND ${table}.id IN (SELECT id FROM main.tag_${sanitize(tags[i])} WHERE tag = ?)`;
params.push(tags[i + 1]);
}
}
if (ctx) {
result = this.promisfy({
method: "all",
stmt: `
SELECT ${table}.id
${resolve ? "" : ", res"}
${enrich ? ", doc" : ""}
FROM ${table}
${enrich ? `
LEFT JOIN main.reg ON main.reg.id = ${table}.id
` : ""}
WHERE ctx = ? AND key = ? ${stmt}
ORDER BY res
${limit ? "LIMIT " + limit : ""}
${offset ? "OFFSET " + offset : ""}
`,
params
});
} else {
result = this.promisfy({
method: "all",
stmt: `
SELECT ${table}.id
${resolve ? "" : ", res"}
${enrich ? ", doc" : ""}
FROM ${table}
${enrich ? `
LEFT JOIN main.reg ON main.reg.id = ${table}.id
` : ""}
WHERE key = ? ${stmt}
ORDER BY res
${limit ? "LIMIT " + limit : ""}
${offset ? "OFFSET " + offset : ""}
`,
params
});
}
return result.then(function (rows) {
return create_result(rows, resolve, enrich);
});
};
SqliteDB.prototype.tag = function (tag, limit = 0, offset = 0, enrich = !1) {
const table = "main.tag" + this.field,
promise = this.promisfy({
method: "all",
stmt: `
SELECT ${table}.id
${enrich ? ", doc" : ""}
FROM ${table}
${enrich ? `
LEFT JOIN main.reg ON main.reg.id = ${table}.id
` : ""}
WHERE tag = ?
${limit ? "LIMIT " + limit : ""}
${offset ? "OFFSET " + offset : ""}
`,
params: [tag]
});
enrich || promise.then(function (rows) {
return create_result(rows, !0, !1);
});
return promise;
};
function build_params(length, single_param) {
let stmt = single_param ? ",(?)" : ",?";
for (let i = 1; i < length;) {
if (i <= length - i) {
stmt += stmt;
i *= 2;
} else {
stmt += stmt.substring(0, (length - i) * (single_param ? 4 : 2));
break;
}
}
return stmt.substring(1);
}
SqliteDB.prototype.enrich = function (ids) {
const result = [],
promises = [];
if ("object" != typeof ids) {
ids = [ids];
}
for (let count = 0; count < ids.length;) {
const chunk = ids.length - count > MAXIMUM_QUERY_VARS ? ids.slice(count, count + MAXIMUM_QUERY_VARS) : count ? ids.slice(count) : ids;
count += chunk.length;
// let stmt = "?";
// for(let i = 1; i < chunk.length; i++){
// stmt += ",?";
// }
// 10x faster string concatenation
let stmt = build_params(chunk.length);
promises.push(this.promisfy({
method: "all",
stmt: `SELECT id, doc FROM main.reg WHERE id IN (${stmt})`,
params: chunk
}));
}
return Promise.all(promises).then(function (promises) {
for (let i = 0, res; i < promises.length; i++) {
res = promises[i];
if (res && res.length) {
for (let i = 0, doc; i < res.length; i++) {
if (doc = res[i].doc) {
res[i].doc = JSON.parse(doc);
}
}
result.push(res);
}
}
return 1 === result.length ? result[0] : 1 < result.length ? concat(result) : result;
});
};
SqliteDB.prototype.has = function (id) {
return this.promisfy({
method: "get",
stmt: `SELECT EXISTS(SELECT 1 FROM main.reg WHERE id = ?) as exist`,
params: [id]
}).then(function (result) {
return !!(result && result.exist);
});
};
SqliteDB.prototype.search = function (flexsearch, query, limit = 100, offset = 0, suggest = !1, resolve = !0, enrich = !1, tags) {
let rows;
if (1 < query.length && flexsearch.depth) {
let stmt = "",
params = [],
keyword = query[0],
term;
for (let i = 1; i < query.length; i++) {
term = query[i];
const swap = flexsearch.bidirectional && term > keyword;
stmt += (stmt ? " OR " : "") + `(ctx = ? AND key = ?)`;
params.push(swap ? term : keyword, swap ? keyword : term);
keyword = term;
}
if (tags) {
stmt = "(" + stmt + ")";
for (let i = 0; i < tags.length; i += 2) {
stmt += ` AND id IN (SELECT id FROM main.tag_${sanitize(tags[i])} WHERE tag = ?)`;
params.push(tags[i + 1]);
}
}
rows = this.promisfy({
method: "all",
stmt: `
SELECT r.id
${resolve ? "" : ", res"}
${enrich ? ", doc" : ""}
FROM (
SELECT id, count(*) as count,
${suggest ? "SUM" : "SUM" /*"MIN"*/}(res) as res
FROM main.ctx${this.field}
WHERE ${stmt}
GROUP BY id
) as r
${enrich ? `
LEFT JOIN main.reg ON main.reg.id = r.id
` : ""}
${suggest ? "" : "WHERE count = " + (query.length - 1)}
ORDER BY ${suggest ? "count DESC, res" : "res"}
${limit ? "LIMIT " + limit : ""}
${offset ? "OFFSET " + offset : ""}
`,
params
});
// variant 1
// for(let i = 1; i < query.length; i++){
// stmt += (stmt ? " UNION ALL " : "") + `
// SELECT id, res
// FROM main.ctx${this.field}
// WHERE ctx = ? AND key = ?
// `;
// term = query[i];
// const swap = flexsearch.bidirectional && (term > keyword);
// params.push(swap ? term : keyword, swap ? keyword : term);
// keyword = term;
// }
//
// rows = await this.promisfy({
// method: "all",
// stmt: `
// SELECT id/*, res */
// FROM (
// SELECT id, ${suggest ? "SUM" : "MIN"}(res) as res, count(*) as count
// FROM (${stmt}) as t
// GROUP BY id
// ORDER BY ${suggest ? "count DESC, res" : "res"}
// LIMIT ${limit}
// OFFSET ${offset}
// ) as r
// ${suggest ? "" : "WHERE count = " + (query.length - 1)}
// `,
// params
// });
} else {
let stmt = "",
query_length = query.length;
for (let i = 0; i < query_length; i++) {
stmt += (stmt ? " OR " : "") + `key = ?`;
}
if (tags) {
stmt = "(" + stmt + ")";
for (let i = 0; i < tags.length; i += 2) {
stmt += ` AND id IN (SELECT id FROM main.tag_${sanitize(tags[i])} WHERE tag = ?)`;
query.push(tags[i + 1]);
}
}
rows = this.promisfy({
method: "all",
stmt: `
SELECT r.id
${resolve ? "" : ", res"}
${enrich ? ", doc" : ""}
FROM (
SELECT id, count(*) as count,
${suggest ? "SUM" : "SUM" /*"MIN"*/}(res) as res
FROM main.map${this.field}
WHERE ${stmt}
GROUP BY id
) as r
${enrich ? `
LEFT JOIN main.reg ON main.reg.id = r.id
` : ""}
${suggest ? "" : "WHERE count = " + query_length}
ORDER BY ${suggest ? "count DESC, res" : "res"}
${limit ? "LIMIT " + limit : ""}
${offset ? "OFFSET " + offset : ""}
`,
params: query
});
// variant 1
// for(let i = 0; i < query.length; i++){
// stmt += (stmt ? " UNION ALL " : "") + `
// SELECT id, res
// FROM main.map${ this.field }
// WHERE key = ?
// `;
// }
//
// rows = await this.promisfy({
// method: "all",
// stmt: `
// SELECT id/*, res*/
// FROM (
// SELECT id, ${suggest ? "SUM" : "MIN"}(res) as res, count(*) as count
// FROM (${stmt}) as t
// GROUP BY id
// ORDER BY ${suggest ? "count DESC, res" : "res"}
// LIMIT ${limit}
// OFFSET ${offset}
// ) as r
// ${ suggest ? "" : "WHERE count = " + query.length }
// `,
// params: query
// });
}
return rows.then(function (rows) {
return create_result(rows, resolve, enrich);
});
};
SqliteDB.prototype.info = function () {
// todo
};
SqliteDB.prototype.transaction = async function (task, callback) {
if (TRX[this.id]) {
return await task.call(this);
}
const db = this.db,
self = this;
return TRX[this.id] = new Promise(function (resolve, reject) {
db.exec("PRAGMA optimize");
db.exec('PRAGMA busy_timeout = 5000');
db.exec("BEGIN");
db.parallelize(function () {
task.call(self);
});
db.exec("COMMIT", function (err, rows) {
TRX[self.id] = null;
if (err) return reject(err);
callback && callback(rows);
resolve(rows);
db.exec("PRAGMA shrink_memory");
});
});
};
SqliteDB.prototype.commit = async function (flexsearch, _replace, _append) {
// process cleanup tasks
if (_replace) {
await this.clear();
// there are just removals in the task queue
flexsearch.commit_task = [];
} else {
let tasks = flexsearch.commit_task;
flexsearch.commit_task = [];
for (let i = 0, task; i < tasks.length; i++) {
task = tasks[i];
// there are just removals in the task queue
if (task.clear) {
await this.clear();
_replace = !0;
break;
} else {
tasks[i] = task.del;
}
}
if (!_replace) {
if (!_append) {
tasks = tasks.concat(toArray(flexsearch.reg));
}
tasks.length && (await this.remove(tasks));
}
}
if (!flexsearch.reg.size) {
return;
}
await this.transaction(function () {
for (const item of flexsearch.map) {
const key = item[0],
arr = item[1];
for (let i = 0, ids; i < arr.length; i++) {
if ((ids = arr[i]) && ids.length) {
let stmt = "",
params = [];
for (let j = 0; j < ids.length; j++) {
stmt += (stmt ? "," : "") + "(?,?,?)";
params.push(key, i, ids[j]);
// maximum count of variables supported
if (j === ids.length - 1 || params.length + 3 > MAXIMUM_QUERY_VARS) {
this.db.run("INSERT INTO main.map" + this.field + " (key, res, id) VALUES " + stmt, params);
stmt = "";
params = [];
}
//this.db.run("INSERT INTO map (key, res, id) VALUES (?, ?, ?) ON CONFLICT DO NOTHING", [key, i, ids[j]]);
}
}
}
}
//});
//await this.transaction(function(){
for (const ctx of flexsearch.ctx) {
const ctx_key = ctx[0],
ctx_value = ctx[1];
for (const item of ctx_value) {
const key = item[0],
arr = item[1];
for (let i = 0, ids; i < arr.length; i++) {
if ((ids = arr[i]) && ids.length) {
let stmt = "",
params = [];
for (let j = 0; j < ids.length; j++) {
stmt += (stmt ? "," : "") + "(?,?,?,?)";
params.push(ctx_key, key, i, ids[j]);
// maximum count of variables supported
if (j === ids.length - 1 || params.length + 4 > MAXIMUM_QUERY_VARS) {
this.db.run("INSERT INTO main.ctx" + this.field + " (ctx, key, res, id) VALUES " + stmt, params);
stmt = "";
params = [];
}
}
}
}
}
}
//});
//await this.transaction(function(){
if (flexsearch.store) {
let stmt = "",
chunk = [];
for (const item of flexsearch.store.entries()) {
const id = item[0],
doc = item[1];
stmt += (stmt ? "," : "") + "(?,?)";
chunk.push(id, "object" == typeof doc ? JSON.stringify(doc) : doc || null);
if (chunk.length + 2 > MAXIMUM_QUERY_VARS) {
this.db.run("INSERT INTO main.reg (id, doc) VALUES " + stmt, chunk);
stmt = "";
chunk = [];
}
}
if (chunk.length) {
this.db.run("INSERT INTO main.reg (id, doc) VALUES " + stmt, chunk);
}
} else if (!flexsearch.bypass) {
let ids = toArray(flexsearch.reg);
for (let count = 0; count < ids.length;) {
const chunk = ids.length - count > MAXIMUM_QUERY_VARS ? ids.slice(count, count + MAXIMUM_QUERY_VARS) : count ? ids.slice(count) : ids;
count += chunk.length;
const stmt = build_params(chunk.length, !0);
this.db.run("INSERT INTO main.reg (id) VALUES " + stmt, chunk);
}
}
//});
//await this.transaction(function(){
if (flexsearch.tag) {
let stmt = "",
chunk = [];
for (const item of flexsearch.tag) {
const tag = item[0],
ids = item[1];
if (!ids.length) continue;
for (let i = 0; i < ids.length; i++) {
stmt += (stmt ? "," : "") + "(?,?)";
chunk.push(tag, ids[i]);
}
if (chunk.length + 2 > MAXIMUM_QUERY_VARS) {
this.db.run("INSERT INTO main.tag" + this.field + " (tag, id) VALUES " + stmt, chunk);
stmt = "";
chunk = [];
}
}
if (chunk.length) {
this.db.run("INSERT INTO main.tag" + this.field + " (tag, id) VALUES " + stmt, chunk);
}
}
});
// TODO
//await this.transaction(function(){
// this.db.run("INSERT INTO main.cfg" + this.field + " (cfg) VALUES (?)", [JSON.stringify({
// "charset": flexsearch.charset,
// "tokenize": flexsearch.tokenize,
// "resolution": flexsearch.resolution,
// "fastupdate": flexsearch.fastupdate,
// "compress": flexsearch.compress,
// "encoder": {
// "minlength": flexsearch.encoder.minlength
// },
// "context": {
// "depth": flexsearch.depth,
// "bidirectional": flexsearch.bidirectional,
// "resolution": flexsearch.resolution_ctx
// }
// })]);
//});
flexsearch.map.clear();
flexsearch.ctx.clear();
flexsearch.tag && flexsearch.tag.clear();
flexsearch.store && flexsearch.store.clear();
flexsearch.document || flexsearch.reg.clear();
};
SqliteDB.prototype.remove = function (ids) {
if ("object" != typeof ids) {
ids = [ids];
}
let next;
// maximum count of variables supported
if (ids.length > MAXIMUM_QUERY_VARS) {
next = ids.slice(MAXIMUM_QUERY_VARS);
ids = ids.slice(0, MAXIMUM_QUERY_VARS);
}
const self = this;
return this.transaction(function () {
const stmt = build_params(ids.length);
this.db.run("DELETE FROM main.map" + self.field + " WHERE id IN (" + stmt + ")", ids);
this.db.run("DELETE FROM main.ctx" + self.field + " WHERE id IN (" + stmt + ")", ids);
this.db.run("DELETE FROM main.tag" + self.field + " WHERE id IN (" + stmt + ")", ids);
this.db.run("DELETE FROM main.reg WHERE id IN (" + stmt + ")", ids);
}).then(function (result) {
return next ? self.remove(next) : result;
});
};
SqliteDB.prototype.promisfy = function (opt) {
const db = this.db;
return new Promise(function (resolve, reject) {
db[opt.method](opt.stmt, opt.params || [], function (err, rows) {
opt.callback && opt.callback(rows);
err ? reject(err) : resolve(rows);
});
});
};