UNPKG

@minatojs/sql-utils

Version:
544 lines (542 loc) 21.2 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Builder: () => Builder, escapeId: () => escapeId, isBracketed: () => isBracketed }); module.exports = __toCommonJS(src_exports); var import_cosmokit = require("cosmokit"); var import_minato = require("minato"); function escapeId(value) { return "`" + value + "`"; } __name(escapeId, "escapeId"); function isBracketed(value) { return value.startsWith("(") && value.endsWith(")"); } __name(isBracketed, "isBracketed"); var Builder = class { static { __name(this, "Builder"); } escapeMap = {}; escapeRegExp; types = {}; createEqualQuery = this.comparator("="); queryOperators; evalOperators; state = {}; $true = "1"; $false = "0"; modifiedTable; _timezone = `+${(/* @__PURE__ */ new Date()).getTimezoneOffset() / -60}:00`.replace("+-", "-"); constructor(tables) { this.state.tables = tables; this.queryOperators = { // logical $or: (key, value) => this.logicalOr(value.map((value2) => this.parseFieldQuery(key, value2))), $and: (key, value) => this.logicalAnd(value.map((value2) => this.parseFieldQuery(key, value2))), $not: (key, value) => this.logicalNot(this.parseFieldQuery(key, value)), // existence $exists: (key, value) => this.createNullQuery(key, value), // comparison $eq: this.createEqualQuery, $ne: this.comparator("!="), $gt: this.comparator(">"), $gte: this.comparator(">="), $lt: this.comparator("<"), $lte: this.comparator("<="), // membership $in: (key, value) => this.createMemberQuery(key, value, ""), $nin: (key, value) => this.createMemberQuery(key, value, " NOT"), // regexp $regex: (key, value) => this.createRegExpQuery(key, value), $regexFor: (key, value) => `${this.escape(value)} regexp ${key}`, // bitwise $bitsAllSet: (key, value) => `${key} & ${this.escape(value)} = ${this.escape(value)}`, $bitsAllClear: (key, value) => `${key} & ${this.escape(value)} = 0`, $bitsAnySet: (key, value) => `${key} & ${this.escape(value)} != 0`, $bitsAnyClear: (key, value) => `${key} & ${this.escape(value)} != ${this.escape(value)}`, // list $el: (key, value) => { if (Array.isArray(value)) { return this.logicalOr(value.map((value2) => this.createElementQuery(key, value2))); } else if (typeof value !== "number" && typeof value !== "string") { throw new TypeError("query expr under $el is not supported"); } else { return this.createElementQuery(key, value); } }, $size: (key, value) => { if (!value) return this.logicalNot(key); if (this.state.sqlTypes?.[this.unescapeId(key)] === "json") { return `${this.jsonLength(key)} = ${this.escape(value)}`; } else { return `${key} AND LENGTH(${key}) - LENGTH(REPLACE(${key}, ${this.escape(",")}, ${this.escape("")})) = ${this.escape(value)} - 1`; } } }; this.evalOperators = { // universal $: (key) => this.getRecursive(key), $if: (args) => `if(${args.map((arg) => this.parseEval(arg)).join(", ")})`, $ifNull: (args) => `ifnull(${args.map((arg) => this.parseEval(arg)).join(", ")})`, // number $add: (args) => `(${args.map((arg) => this.parseEval(arg)).join(" + ")})`, $multiply: (args) => `(${args.map((arg) => this.parseEval(arg)).join(" * ")})`, $subtract: this.binary("-"), $divide: this.binary("/"), $modulo: this.binary("%"), // mathemetic $abs: (arg) => `abs(${this.parseEval(arg)})`, $floor: (arg) => `floor(${this.parseEval(arg)})`, $ceil: (arg) => `ceil(${this.parseEval(arg)})`, $round: (arg) => `round(${this.parseEval(arg)})`, $exp: (arg) => `exp(${this.parseEval(arg)})`, $log: (args) => `log(${args.filter((x) => !(0, import_cosmokit.isNullable)(x)).map((arg) => this.parseEval(arg)).reverse().join(", ")})`, $power: (args) => `power(${args.map((arg) => this.parseEval(arg)).join(", ")})`, $random: () => `rand()`, // string $concat: (args) => `concat(${args.map((arg) => this.parseEval(arg)).join(", ")})`, $regex: ([key, value]) => `${this.parseEval(key)} regexp ${this.parseEval(value)}`, // logical $or: (args) => this.logicalOr(args.map((arg) => this.parseEval(arg))), $and: (args) => this.logicalAnd(args.map((arg) => this.parseEval(arg))), $not: (arg) => this.logicalNot(this.parseEval(arg)), // boolean $eq: this.binary("="), $ne: this.binary("!="), $gt: this.binary(">"), $gte: this.binary(">="), $lt: this.binary("<"), $lte: this.binary("<="), // membership $in: ([key, value]) => this.createMemberQuery(this.parseEval(key), value, ""), $nin: ([key, value]) => this.createMemberQuery(this.parseEval(key), value, " NOT"), // typecast $number: (arg) => { const value = this.parseEval(arg); const res = this.state.sqlType === "raw" ? `(0+${value})` : this.state.sqlType === "time" ? `unix_timestamp(convert_tz(addtime('1970-01-01 00:00:00', ${value}), '${this._timezone}', '+0:00'))` : `unix_timestamp(convert_tz(${value}, '${this._timezone}', '+0:00'))`; this.state.sqlType = "raw"; return `ifnull(${res}, 0)`; }, // aggregation $sum: (expr) => this.createAggr(expr, (value) => `ifnull(sum(${value}), 0)`), $avg: (expr) => this.createAggr(expr, (value) => `avg(${value})`), $min: (expr) => this.createAggr(expr, (value) => `min(${value})`), $max: (expr) => this.createAggr(expr, (value) => `max(${value})`), $count: (expr) => this.createAggr(expr, (value) => `count(distinct ${value})`), $length: (expr) => this.createAggr(expr, (value) => `count(${value})`, (value) => { if (this.state.sqlType === "json") { this.state.sqlType = "raw"; return `${this.jsonLength(value)}`; } else { this.state.sqlType = "raw"; return `if(${value}, LENGTH(${value}) - LENGTH(REPLACE(${value}, ${this.escape(",")}, ${this.escape("")})) + 1, 0)`; } }), $object: (fields) => this.groupObject(fields), $array: (expr) => this.groupArray(this.parseEval(expr, false)), $exec: (sel) => this.parseSelection(sel) }; } unescapeId(value) { return value.slice(1, value.length - 1); } createNullQuery(key, value) { return `${key} is ${value ? "not " : ""}null`; } createMemberQuery(key, value, notStr = "") { if (Array.isArray(value)) { if (!value.length) return notStr ? this.$true : this.$false; return `${key}${notStr} in (${value.map((val) => this.escape(val)).join(", ")})`; } else { const res = this.jsonContains(this.parseEval(value, false), this.jsonQuote(key, true)); this.state.sqlType = "raw"; return notStr ? this.logicalNot(res) : res; } } createRegExpQuery(key, value) { return `${key} regexp ${this.escape(typeof value === "string" ? value : value.source)}`; } createElementQuery(key, value) { if (this.state.sqlTypes?.[this.unescapeId(key)] === "json") { return this.jsonContains(key, this.quote(JSON.stringify(value))); } else { return `find_in_set(${this.escape(value)}, ${key})`; } } comparator(operator) { return (key, value) => { return `${key} ${operator} ${this.escape(value)}`; }; } binary(operator) { return ([left, right]) => { return `(${this.parseEval(left)} ${operator} ${this.parseEval(right)})`; }; } logicalAnd(conditions) { if (!conditions.length) return this.$true; if (conditions.includes(this.$false)) return this.$false; return conditions.join(" AND "); } logicalOr(conditions) { if (!conditions.length) return this.$false; if (conditions.includes(this.$true)) return this.$true; return `(${conditions.join(" OR ")})`; } logicalNot(condition) { return `NOT(${condition})`; } parseSelection(sel) { const { args: [expr], ref, table, tables } = sel; const restore = this.saveState({ tables }); const inner = this.get(table, true, true); const output = this.parseEval(expr, false); restore(); if (!sel.args[0].$) { return `(SELECT ${output} AS value FROM ${inner} ${isBracketed(inner) ? ref : ""})`; } else { return `(ifnull((SELECT ${this.groupArray(output)} AS value FROM ${inner} ${isBracketed(inner) ? ref : ""}), json_array()))`; } } jsonLength(value) { return `json_length(${value})`; } jsonContains(obj, value) { return `json_contains(${obj}, ${value})`; } jsonUnquote(value, pure = false) { if (pure) return `json_unquote(${value})`; if (this.state.sqlType === "json") { this.state.sqlType = "raw"; return `json_unquote(${value})`; } return value; } jsonQuote(value, pure = false) { if (pure) return `cast(${value} as json)`; if (this.state.sqlType !== "json") { this.state.sqlType = "json"; return `cast(${value} as json)`; } return value; } createAggr(expr, aggr, nonaggr) { if (this.state.group) { this.state.group = false; const value = aggr(this.parseEval(expr, false)); this.state.group = true; return value; } else { const value = this.parseEval(expr, false); const res = nonaggr ? nonaggr(value) : `(select ${aggr(`json_unquote(${this.escapeId("value")})`)} from json_table(${value}, '$[*]' columns (value json path '$')) ${(0, import_minato.randomId)()})`; return res; } } groupObject(fields) { const parse = /* @__PURE__ */ __name((expr) => { const value = this.parseEval(expr, false); return this.state.sqlType === "json" ? `json_extract(${value}, '$')` : `${value}`; }, "parse"); const res = `json_object(` + Object.entries(fields).map(([key, expr]) => `'${key}', ${parse(expr)}`).join(",") + `)`; this.state.sqlType = "json"; return res; } groupArray(value) { this.state.sqlType = "json"; return `ifnull(json_arrayagg(${value}), json_array())`; } parseFieldQuery(key, query) { const conditions = []; if (this.modifiedTable) key = `${this.escapeId(this.modifiedTable)}.${key}`; if (Array.isArray(query)) { conditions.push(this.createMemberQuery(key, query)); } else if (query instanceof RegExp) { conditions.push(this.createRegExpQuery(key, query)); } else if ((0, import_minato.isComparable)(query)) { conditions.push(this.createEqualQuery(key, query)); } else if ((0, import_cosmokit.isNullable)(query)) { conditions.push(this.createNullQuery(key, false)); } else { for (const prop in query) { if (prop in this.queryOperators) { conditions.push(this.queryOperators[prop](key, query[prop])); } } } return this.logicalAnd(conditions); } parseQuery(query) { const conditions = []; for (const key in query) { if (key === "$not") { conditions.push(this.logicalNot(this.parseQuery(query.$not))); } else if (key === "$and") { conditions.push(this.logicalAnd(query.$and.map(this.parseQuery.bind(this)))); } else if (key === "$or") { conditions.push(this.logicalOr(query.$or.map(this.parseQuery.bind(this)))); } else if (key === "$expr") { conditions.push(this.parseEval(query.$expr)); } else { conditions.push(this.parseFieldQuery(this.escapeId(key), query[key])); } } return this.logicalAnd(conditions); } parseEvalExpr(expr) { this.state.sqlType = "raw"; for (const key in expr) { if (key in this.evalOperators) { return this.evalOperators[key](expr[key]); } } return this.escape(expr); } transformJsonField(obj, path) { this.state.sqlType = "json"; return `json_extract(${obj}, '$${path}')`; } transformKey(key, fields, prefix, fullKey) { if (key in fields || !key.includes(".")) { if (this.state.sqlTypes?.[key] || this.state.sqlTypes?.[fullKey]) { this.state.sqlType = this.state.sqlTypes[key] || this.state.sqlTypes[fullKey]; } return prefix + this.escapeId(key); } const field = Object.keys(fields).find((k) => key.startsWith(k + ".")) || key.split(".")[0]; const rest = key.slice(field.length + 1).split("."); return this.transformJsonField(`${prefix}${this.escapeId(field)}`, rest.map((key2) => `.${this.escapeKey(key2)}`).join("")); } getRecursive(args) { if (typeof args === "string") { return this.getRecursive(["_", args]); } const [table, key] = args; const fields = this.state.tables?.[table]?.fields || {}; const fkey = Object.keys(fields).find((field) => key === field || key.startsWith(field + ".")); if (fkey && fields[fkey]?.expr) { if (key === fkey) { return this.parseEvalExpr(fields[fkey]?.expr); } else { const field = this.parseEvalExpr(fields[fkey]?.expr); const rest = key.slice(fkey.length + 1).split("."); return this.transformJsonField(`${field}`, rest.map((key2) => `.${this.escapeKey(key2)}`).join("")); } } const prefix = this.modifiedTable ? `${this.escapeId(this.state.tables?.[table]?.name ?? this.modifiedTable)}.` : !this.state.tables || table === "_" || key in fields || Object.keys(this.state.tables).length === 1 && table in this.state.tables ? "" : `${this.escapeId(table)}.`; if (!(table in (this.state.tables || {})) && table in (this.state.refTables || {})) { const fields2 = this.state.refTables?.[table]?.fields || {}; const res = fields2[key]?.expr ? this.parseEvalExpr(fields2[key]?.expr) : this.transformKey(key, fields2, `${this.escapeId(table)}.`, `${table}.${key}`); if (this.state.wrappedSubquery) { if (res in (this.state.refFields ?? {})) return this.state.refFields[res]; const key2 = `minato_tvar_${(0, import_minato.randomId)()}`; (this.state.refFields ??= {})[res] = key2; this.state.sqlType = "json"; return this.escapeId(key2); } else return res; } return this.transformKey(key, fields, prefix, `${table}.${key}`); } parseEval(expr, unquote = true) { this.state.sqlType = "raw"; if (typeof expr === "string" || typeof expr === "number" || typeof expr === "boolean" || expr instanceof Date || expr instanceof RegExp) { return this.escape(expr); } return unquote ? this.jsonUnquote(this.parseEvalExpr(expr)) : this.parseEvalExpr(expr); } saveState(extra = {}) { const thisState = this.state; this.state = { refTables: { ...this.state.refTables || {}, ...this.state.tables || {} }, ...extra }; return () => { thisState.sqlType = this.state.sqlType; this.state = thisState; }; } suffix(modifier) { const { limit, offset, sort, group, having } = modifier; let sql = ""; if (group?.length) { sql += ` GROUP BY ${group.map(this.escapeId).join(", ")}`; const filter = this.parseEval(having); if (filter !== this.$true) sql += ` HAVING ${filter}`; } if (sort.length) { sql += " ORDER BY " + sort.map(([expr, dir]) => { return `${this.parseEval(expr)} ${dir.toUpperCase()}`; }).join(", "); } if (limit < Infinity) sql += " LIMIT " + limit; if (offset > 0) sql += " OFFSET " + offset; return sql; } get(sel, inline = false, group = false, addref = true) { const { args, table, query, ref, model } = sel; let prefix; if (typeof table === "string") { prefix = this.escapeId(table); this.state.sqlTypes = Object.fromEntries(Object.entries(model.fields).map(([key, field]) => { let sqlType = "raw"; if (field.type === "json") sqlType = "json"; else if (field.type === "list") sqlType = "list"; else if (import_minato.Field.date.includes(field.type)) sqlType = field.type; return [key, sqlType]; })); } else if (table instanceof import_minato.Selection) { prefix = this.get(table, true); if (!prefix) return; } else { const sqlTypes2 = {}; const joins = Object.entries(table).map(([key, table2]) => { const restore = this.saveState({ tables: table2.tables }); const t = `${this.get(table2, true, false, false)} AS ${this.escapeId(key)}`; for (const [fieldKey, fieldType] of Object.entries(this.state.sqlTypes)) { sqlTypes2[`${key}.${fieldKey}`] = fieldType; } restore(); return t; }); this.state.sqlTypes = sqlTypes2; prefix = " " + joins[0] + joins.slice(1, -1).map((join) => ` JOIN ${join} ON ${this.$true}`).join(" ") + ` JOIN ` + joins.at(-1); const filter2 = this.parseEval(args[0].having); prefix += ` ON ${filter2}`; } const filter = this.parseQuery(query); if (filter === this.$false) return; this.state.group = group || !!args[0].group; const sqlTypes = {}; const fields = args[0].fields ?? Object.fromEntries(Object.entries(model.fields).filter(([, field]) => !field.deprecated).map(([key]) => [key, { $: [ref, key] }])); const keys = Object.entries(fields).map(([key, value]) => { value = this.parseEval(value, false); sqlTypes[key] = this.state.sqlType; return this.escapeId(key) === value ? this.escapeId(key) : `${value} AS ${this.escapeId(key)}`; }).join(", "); let suffix = this.suffix(args[0]); this.state.sqlTypes = sqlTypes; if (filter !== this.$true) { suffix = ` WHERE ${filter}` + suffix; } if (inline && !args[0].fields && !suffix) { return addref && isBracketed(prefix) ? `${prefix} ${ref}` : prefix; } if (!prefix.includes(" ") || isBracketed(prefix)) { suffix = ` ${ref}` + suffix; } const result = `SELECT ${keys} FROM ${prefix}${suffix}`; return inline ? `(${result})` : result; } define(converter) { converter.types.forEach((type) => this.types[type] = converter); } dump(model, obj) { obj = model.format(obj); const result = {}; for (const key in obj) { result[key] = this.stringify(obj[key], model.fields[key]); } return result; } load(model, obj) { if (!obj) { const converter = this.types[this.state.sqlType]; return converter ? converter.load(model) : model; } const result = {}; for (const key in obj) { if (!(key in model.fields)) continue; const { type, initial } = model.fields[key]; const converter = (this.state.sqlTypes?.[key] ?? "raw") === "raw" ? this.types[type] : this.types[this.state.sqlTypes[key]]; result[key] = converter ? converter.load(obj[key], initial) : obj[key]; } return model.parse(result); } escape(value, field) { value = this.stringify(value, field); if ((0, import_cosmokit.isNullable)(value)) return "NULL"; switch (typeof value) { case "boolean": case "number": return value + ""; case "object": return this.quote(JSON.stringify(value)); default: return this.quote(value); } } escapeId(value) { return escapeId(value); } escapeKey(value) { return `"${value}"`; } stringify(value, field) { const converter = this.types[field?.type]; return converter ? converter.dump(value) : value; } quote(value) { this.escapeRegExp ??= new RegExp(`[${Object.values(this.escapeMap).join("")}]`, "g"); let chunkIndex = this.escapeRegExp.lastIndex = 0; let escapedVal = ""; let match; while (match = this.escapeRegExp.exec(value)) { escapedVal += value.slice(chunkIndex, match.index) + this.escapeMap[match[0]]; chunkIndex = this.escapeRegExp.lastIndex; } if (chunkIndex === 0) { return "'" + value + "'"; } if (chunkIndex < value.length) { return "'" + escapedVal + value.slice(chunkIndex) + "'"; } return "'" + escapedVal + "'"; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Builder, escapeId, isBracketed }); //# sourceMappingURL=index.js.map