UNPKG

unsqlite

Version:
718 lines (712 loc) 21.9 kB
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 };