unsqlite
Version:
NoSql for SQLite!
718 lines (712 loc) • 21.9 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
});
};
// src/index-compiler.ts
var exports_index_compiler = {};
__export(exports_index_compiler, {
compileIndexExpression: () => compileIndexExpression
});
function compileFieldPath(field, jsonExtract, jsonCol) {
return `${jsonExtract}(${jsonCol}, '$.${field.$}')`;
}
function sqlLiteral(val) {
if (typeof val === "string")
return `'${val.replace(/'/g, "''")}'`;
if (val === null)
return "NULL";
if (typeof val === "boolean")
return val ? "1" : "0";
return String(val);
}
function compileIndexExpression(expr, jsonExtract = "json_extract", jsonCol = "data") {
if (expr && typeof expr === "object" && "$" in expr) {
return compileFieldPath(expr, jsonExtract, jsonCol);
}
if (expr && typeof expr === "object" && "$fn" in expr) {
const [fn, ...args] = expr.$fn;
return `${fn}(${args.map((a) => compileIndexExpression(a, jsonExtract, jsonCol)).join(", ")})`;
}
if (expr && typeof expr === "object" && "$cast" in expr) {
const [arg, type] = expr.$cast;
return `CAST(${compileIndexExpression(arg, jsonExtract, jsonCol)} AS ${type})`;
}
if (expr && typeof expr === "object") {
if ("$add" in expr) {
const [a, b] = expr.$add;
return `(${compileIndexExpression(a, jsonExtract, jsonCol)} + ${compileIndexExpression(b, jsonExtract, jsonCol)})`;
}
if ("$sub" in expr) {
const [a, b] = expr.$sub;
return `(${compileIndexExpression(a, jsonExtract, jsonCol)} - ${compileIndexExpression(b, jsonExtract, jsonCol)})`;
}
if ("$mul" in expr) {
const [a, b] = expr.$mul;
return `(${compileIndexExpression(a, jsonExtract, jsonCol)} * ${compileIndexExpression(b, jsonExtract, jsonCol)})`;
}
if ("$div" in expr) {
const [a, b] = expr.$div;
return `(${compileIndexExpression(a, jsonExtract, jsonCol)} / ${compileIndexExpression(b, jsonExtract, jsonCol)})`;
}
}
return sqlLiteral(expr);
}
// src/adapters/better-sqlite3.ts
function createBetterSqlite3Adapter(db) {
return {
execute: async (sql, args) => {
if (args && args.length) {
return db.prepare(sql).run(...args);
} else {
return db.prepare(sql).run();
}
},
select: async function* (sql, args) {
const stmt = db.prepare(sql);
const rows = args && args.length ? stmt.all(...args) : stmt.all();
for (const row of rows)
yield row;
},
get: async (sql, args) => {
const stmt = db.prepare(sql);
return args && args.length ? stmt.get(...args) : stmt.get();
},
lastInsertRowid: (result) => result.lastInsertRowid
};
}
// src/query-compiler.ts
function queryCompiler(query, jsonCol = "data", jsonExtract = "json_extract") {
function expr(val) {
if (val && typeof val === "object" && "$" in val) {
return `${jsonExtract}(${jsonCol}, '$.${val.$}')`;
}
return "?";
}
function walk(q) {
if (!q || typeof q !== "object")
throw new Error("Invalid query");
const keys = Object.keys(q);
if (keys.length !== 1)
throw new Error("Query object must have exactly one operator");
const op = keys[0];
if (!op)
throw new Error("Query operator is missing");
const val = q[op];
switch (op) {
case "$eq":
case "$ne":
case "$gt":
case "$gte":
case "$lt":
case "$lte": {
if (!Array.isArray(val) || val.length !== 2)
throw new Error(`${op} expects [field, value]`);
const [a, b] = val;
let sql = "";
let params = [];
const opMap = {
$eq: "=",
$ne: "!=",
$gt: ">",
$gte: ">=",
$lt: "<",
$lte: "<="
};
const opSql = opMap[op];
const left = expr(a);
const right = expr(b);
sql = `${left} ${opSql} ${right}`;
if (left === "?")
params.push(a);
if (right === "?")
params.push(b);
return { sql, params };
}
case "$and": {
if (!Array.isArray(val))
throw new Error("$and expects array");
const parts = val.map(walk);
return {
sql: parts.map((p) => `(${p.sql})`).join(" AND "),
params: parts.flatMap((p) => p.params)
};
}
case "$or": {
if (!Array.isArray(val))
throw new Error("$or expects array");
const parts = val.map(walk);
return {
sql: parts.map((p) => `(${p.sql})`).join(" OR "),
params: parts.flatMap((p) => p.params)
};
}
case "$not": {
if (!Array.isArray(val) || val.length !== 1)
throw new Error("$not expects single-element array");
const inner = walk(val[0]);
return { sql: `NOT (${inner.sql})`, params: inner.params };
}
default:
throw new Error(`Unknown operator: ${op}`);
}
}
return walk(query);
}
// src/query-builder.ts
class QueryBuilder {
jsonExtract;
_buildSQL(options) {
const explainType = options?.explain ? options?.debugExplain ? "EXPLAIN" : "EXPLAIN QUERY PLAN" : "";
let selectClause = options?.count ? `SELECT COUNT(*) as count` : `SELECT ${this.dataCol}`;
let sql = `${explainType ? explainType + " " : ""}${selectClause} FROM ${this.table}`;
let params = [];
if (this.query) {
const where = queryCompiler(this.query, this.dataCol, this.jsonExtract);
if (where.sql) {
sql += ` WHERE ${where.sql}`;
params = where.params;
}
}
if (this._order.length && !options?.count) {
sql += " ORDER BY " + this._order.map(([f, d]) => {
if (typeof f === "object" && f !== null && "$" in f) {
return `${this.jsonExtract}(${this.dataCol}, '$.${f.$}') ${d.toUpperCase()}`;
} else {
return `${f} ${d.toUpperCase()}`;
}
}).join(", ");
}
if (options?.includeLimitOffset !== false && !options?.count) {
if (this._limit !== undefined) {
sql += ` LIMIT ${this._limit}`;
}
if (this._offset !== undefined) {
sql += ` OFFSET ${this._offset}`;
}
}
return { sql, params };
}
table;
dataCol;
db;
query;
_order = [];
_limit;
_offset;
constructor(table, dataCol, db, query, jsonExtract = "json_extract") {
this.table = table;
this.dataCol = dataCol;
this.db = db;
this.query = query;
this.jsonExtract = jsonExtract;
}
order(field, dir = "asc") {
this._order.push([field, dir]);
return this;
}
limit(n) {
this._limit = n;
return this;
}
offset(n) {
this._offset = n;
return this;
}
async all() {
const { sql, params } = this._buildSQL();
const result = [];
for await (const row of this.db.select(sql, params)) {
const val = row[this.dataCol];
if (val === undefined || val === null)
continue;
if (typeof val === "string")
result.push(JSON.parse(val));
else
result.push(val);
}
return result;
}
async first() {
const { sql, params } = this._buildSQL({ includeLimitOffset: true });
const sqlWithLimit = sql.includes("LIMIT") ? sql : sql + " LIMIT 1";
for await (const row of this.db.select(sqlWithLimit, params)) {
const val = row[this.dataCol];
if (val === undefined || val === null)
continue;
if (typeof val === "string")
return JSON.parse(val);
return val;
}
return;
}
async count() {
const { sql, params } = this._buildSQL({ count: true, includeLimitOffset: false });
for await (const row of this.db.select(sql, params)) {
return row.count ?? 0;
}
return 0;
}
iterate() {
const { sql, params } = this._buildSQL();
const self = this;
async function* gen() {
for await (const row of self.db.select(sql, params)) {
const val = row[self.dataCol];
if (val === undefined || val === null)
continue;
if (typeof val === "string")
yield JSON.parse(val);
else
yield val;
}
}
return gen();
}
async explain(debug) {
const { sql, params } = this._buildSQL({ explain: true, debugExplain: debug });
const result = [];
for await (const row of this.db.select(sql, params)) {
result.push(row);
}
return result;
}
toString() {
const { sql, params } = this._buildSQL();
return sql + " " + JSON.stringify(params);
}
}
// src/collection.ts
class Collection {
table;
idCol;
dataCol;
db;
constructor(table, db, options = {}) {
this.table = table;
let generate = options.idGenerate;
let idType = options.idType || "INTEGER PRIMARY KEY";
this.idCol = {
column: options.idColumn || "id",
type: idType,
generate
};
const format = (options.dataFormat || "JSON").toUpperCase();
this.dataCol = {
column: options.dataColumn || "data",
type: format
};
this.db = db;
}
async get(idOrIds) {
if (Array.isArray(idOrIds)) {
if (idOrIds.length === 0)
return [];
const placeholders = idOrIds.map(() => "?").join(", ");
let sql;
if (this.dataCol.type === "JSONB") {
sql = `SELECT ${this.idCol.column}, json(${this.dataCol.column}) as data FROM ${this.table} WHERE ${this.idCol.column} IN (${placeholders})`;
} else {
sql = `SELECT ${this.idCol.column}, ${this.dataCol.column} as data FROM ${this.table} WHERE ${this.idCol.column} IN (${placeholders})`;
}
const rowMap = new Map;
const iter = this.db.select(sql, idOrIds);
for await (const row of iter) {
let val = row["data"];
if (val !== undefined && val !== null && typeof val === "string")
val = JSON.parse(val);
rowMap.set(row[this.idCol.column], val);
}
return idOrIds.map((id) => rowMap.has(id) ? rowMap.get(id) : undefined);
} else {
let sql;
if (this.dataCol.type === "JSONB") {
sql = `SELECT json(${this.dataCol.column}) as data FROM ${this.table} WHERE ${this.idCol.column} = ?`;
} else {
sql = `SELECT ${this.dataCol.column} as data FROM ${this.table} WHERE ${this.idCol.column} = ?`;
}
const row = await this.db.get(sql, [idOrIds]);
if (!row)
return;
const val = row["data"];
if (val === undefined || val === null)
return;
if (typeof val === "string")
return JSON.parse(val);
return val;
}
}
async set(id, data) {
let sql;
let value;
if (this.dataCol.type === "JSONB") {
sql = `INSERT OR REPLACE INTO ${this.table} (${this.idCol.column}, ${this.dataCol.column}) VALUES (?, jsonb(?))`;
value = JSON.stringify(data);
} else {
sql = `INSERT OR REPLACE INTO ${this.table} (${this.idCol.column}, ${this.dataCol.column}) VALUES (?, json(?))`;
value = JSON.stringify(data);
}
await this.db.execute(sql, [id, value]);
}
async insert(data) {
let sql;
let value;
if (this.dataCol.type === "JSONB") {
value = JSON.stringify(data);
if (this.idCol.generate) {
const generatedId = this.idCol.generate(data);
sql = `INSERT OR REPLACE INTO ${this.table} (${this.idCol.column}, ${this.dataCol.column}) VALUES (?, jsonb(?))`;
await this.db.execute(sql, [generatedId, value]);
return generatedId;
} else {
sql = `INSERT INTO ${this.table} (${this.dataCol.column}) VALUES (jsonb(?))`;
const result = await this.db.execute(sql, [value]);
return await this.db.lastInsertRowid(result);
}
} else {
value = JSON.stringify(data);
if (this.idCol.generate) {
const generatedId = this.idCol.generate(data);
sql = `INSERT OR REPLACE INTO ${this.table} (${this.idCol.column}, ${this.dataCol.column}) VALUES (?, json(?))`;
await this.db.execute(sql, [generatedId, value]);
return generatedId;
} else {
sql = `INSERT INTO ${this.table} (${this.dataCol.column}) VALUES (json(?))`;
const result = await this.db.execute(sql, [value]);
return await this.db.lastInsertRowid(result);
}
}
}
find(query) {
const jsonExtract = this.dataCol.type === "JSONB" ? "jsonb_extract" : "json_extract";
return new QueryBuilder(this.table, this.dataCol.column, { select: this.db.select }, query, jsonExtract);
}
async index(name, expr, options = {}) {
let indexExpr = expr;
if (typeof expr === "string") {
indexExpr = { $: expr };
}
function isValidIndexExpr(e) {
if (typeof e === "string")
return true;
if (e && typeof e === "object") {
if (typeof e.$ === "string")
return true;
if (e.$fn && Array.isArray(e.$fn) && typeof e.$fn[0] === "string")
return true;
if (e.$cast && Array.isArray(e.$cast) && e.$cast.length === 2)
return true;
if (e.$add && Array.isArray(e.$add) && e.$add.length === 2)
return true;
if (e.$sub && Array.isArray(e.$sub) && e.$sub.length === 2)
return true;
if (e.$mul && Array.isArray(e.$mul) && e.$mul.length === 2)
return true;
if (e.$div && Array.isArray(e.$div) && e.$div.length === 2)
return true;
}
return false;
}
if (!isValidIndexExpr(expr)) {
throw new Error("Invalid index expression: must be a string, field path, function, cast, or arithmetic expression");
}
const { compileIndexExpression: compileIndexExpression2 } = await Promise.resolve().then(() => exports_index_compiler);
const jsonExtract = this.dataCol.type === "JSONB" ? "jsonb_extract" : "json_extract";
let compiled = compileIndexExpression2(indexExpr, jsonExtract, this.dataCol.column);
const unique = options.unique ? "UNIQUE" : "";
const type = options.type ? `USING ${options.type}` : "";
const order = options.order ? ` ${options.order}` : "";
const sql = `CREATE ${unique} INDEX IF NOT EXISTS ${name} ON ${this.table} (${compiled}${order}) ${type}`;
await this.db.execute(sql);
}
}
async function createCollection(table, db, options = {}) {
let generate = options.idGenerate;
let idType = options.idType || "INTEGER PRIMARY KEY";
const idCol = {
column: options.idColumn || "id",
type: idType,
generate
};
const format = (options.dataFormat || "JSON").toUpperCase();
const dataCol = {
column: options.dataColumn || "data",
type: format
};
let dataColType;
if (format === "JSONB") {
dataColType = "BLOB";
} else {
dataColType = "JSON";
}
const createSQL = `CREATE TABLE IF NOT EXISTS ${table} (
${idCol.column} ${idCol.type},
${dataCol.column} ${dataColType}
)`;
await db.execute(createSQL);
const pragmaSQL = `PRAGMA table_info(${table})`;
const columns = [];
for await (const row of db.select(pragmaSQL)) {
columns.push({ name: row.name, type: row.type, pk: row.pk });
}
const idColDef = columns.find((c) => c.name === idCol.column);
const dataColDef = columns.find((c) => c.name === dataCol.column);
function baseType(type) {
return type.split(" ")[0].toUpperCase();
}
const expectedIdBaseType = baseType(idCol.type);
const actualIdBaseType = idColDef ? baseType(idColDef.type) : undefined;
if (!idColDef || actualIdBaseType !== expectedIdBaseType) {
throw new Error(`Table '${table}' id column '${idCol.column}' type mismatch: expected base type '${expectedIdBaseType}', found '${idColDef ? idColDef.type : "none"}'`);
}
if (idCol.type.toUpperCase().includes("PRIMARY KEY") && idColDef.pk !== 1) {
throw new Error(`Table '${table}' id column '${idCol.column}' is not PRIMARY KEY as expected.`);
}
if (!dataColDef) {
throw new Error(`Table '${table}' data column '${dataCol.column}' type mismatch: expected '${dataCol.type}', found 'none'`);
}
if (dataCol.type === "JSONB") {
if (dataColDef.type.toUpperCase() !== "BLOB") {
throw new Error(`Table '${table}' data column '${dataCol.column}' type mismatch: expected 'BLOB', found '${dataColDef.type}'`);
}
} else {
if (dataColDef.type.toUpperCase() !== "JSON" && dataColDef.type.toUpperCase() !== "TEXT") {
throw new Error(`Table '${table}' data column '${dataCol.column}' type mismatch: expected 'JSON' or 'TEXT', found '${dataColDef.type}'`);
}
}
return new Collection(table, db, options);
}
// src/adapters/bun.ts
function createBunAdapter(db) {
return {
async collection(table, options) {
return await createCollection(table, {
execute: async (sql, args) => {
const stmt = db.prepare(sql);
return args ? stmt.run(...args) : stmt.run();
},
select: async function* (sql, args) {
const stmt = db.query(sql);
yield* stmt.iterate(...args ?? []);
},
get: async (sql, args) => {
const stmt = db.query(sql);
const rows = args ? stmt.all(...args) : stmt.all();
return rows[0] || undefined;
},
lastInsertRowid: async (result) => {
return result.lastInsertRowid;
}
}, options);
}
};
}
// src/adapters/libsql.ts
function createLibSQLAdapter(client) {
return {
async collection(table, options) {
return await createCollection(table, {
execute: async (sql, args) => {
return await client.execute({ sql, args });
},
select: async function* (sql, args) {
const res = await client.execute({ sql, args });
const rows = res.rows ?? [];
for (const row of rows)
yield row;
},
get: async (sql, args) => {
const res = await client.execute({ sql, args });
return res.rows && res.rows[0] || undefined;
},
lastInsertRowid: async (result) => {
return result.lastInsertRowid;
}
}, options);
}
};
}
// src/adapters/sqlite.ts
function createSqliteAdapter(db) {
return {
execute: async (sql, args) => {
return db.run(sql, ...args || []);
},
select: async function* (sql, args) {
const rows = await db.all(sql, ...args || []);
for (const row of rows)
yield row;
},
get: (sql, args) => {
return db.get(sql, ...args || []);
},
lastInsertRowid: (result) => result.lastID
};
}
// src/adapters/sqlite3.ts
function createSqlite3Adapter(db) {
return {
execute: async (sql, args) => {
return new Promise((resolve, reject) => {
db.run(sql, ...args || [], function(err) {
if (err)
reject(err);
else
resolve(this);
});
});
},
select: async function* (sql, args) {
const rows = await new Promise((resolve, reject) => {
db.all(sql, ...args || [], (err, rows2) => {
if (err)
reject(err);
else
resolve(rows2);
});
});
for (const row of rows)
yield row;
},
get: (sql, args) => {
return new Promise((resolve, reject) => {
db.get(sql, ...args || [], (err, row) => {
if (err)
reject(err);
else
resolve(row);
});
});
},
lastInsertRowid: (result) => result.lastID
};
}
// src/adapters/sqljs.ts
function createSqljsAdapter(db) {
return {
execute: async (sql, args) => {
db.run(sql, args || []);
return;
},
select: async function* (sql, args) {
const stmt = db.prepare(sql);
try {
if (args && args.length)
stmt.bind(args);
while (stmt.step()) {
yield stmt.getAsObject();
}
} finally {
stmt.free();
}
},
get: async (sql, args) => {
const stmt = db.prepare(sql);
try {
if (args && args.length)
stmt.bind(args);
if (stmt.step()) {
return stmt.getAsObject();
} else {
return;
}
} finally {
stmt.free();
}
},
lastInsertRowid: (_result) => {
const stmt = db.prepare("SELECT last_insert_rowid() AS id");
try {
stmt.step();
const row = stmt.getAsObject();
return row.id;
} finally {
stmt.free();
}
}
};
}
// src/operators.ts
var exports_operators = {};
__export(exports_operators, {
sub: () => sub,
or: () => or,
not: () => not,
ne: () => ne,
mul: () => mul,
lte: () => lte,
lt: () => lt,
gte: () => gte,
gt: () => gt,
fn: () => fn,
eq: () => eq,
div: () => div,
cast: () => cast,
and: () => and,
add: () => add,
$: () => $
});
function $(path) {
return { $: path };
}
function eq(a, b) {
return { $eq: [a, b] };
}
function ne(a, b) {
return { $ne: [a, b] };
}
function gt(a, b) {
return { $gt: [a, b] };
}
function gte(a, b) {
return { $gte: [a, b] };
}
function lt(a, b) {
return { $lt: [a, b] };
}
function lte(a, b) {
return { $lte: [a, b] };
}
function and(...args) {
return { $and: args };
}
function or(...args) {
return { $or: args };
}
function not(arg) {
return { $not: [arg] };
}
function fn(name, ...args) {
return { $fn: [name, ...args] };
}
function cast(expr, type) {
return { $cast: [expr, type] };
}
function add(a, b) {
return { $add: [a, b] };
}
function sub(a, b) {
return { $sub: [a, b] };
}
function mul(a, b) {
return { $mul: [a, b] };
}
function div(a, b) {
return { $div: [a, b] };
}
export {
exports_operators as operators,
createSqljsAdapter,
createSqliteAdapter,
createSqlite3Adapter,
createLibSQLAdapter,
createCollection,
createBunAdapter,
createBetterSqlite3Adapter,
Collection
};