pg-sql2
Version:
Generate safe Postgres-compliant SQL with tagged template literals
1,148 lines • 42.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.true = exports.TRUE = exports.query = exports.null = exports.NULL = exports.fragment = exports.false = exports.FALSE = exports.sql = exports.dot = exports.blank = exports.$$symbolToIdentifier = exports.$$toSQL = exports.version = void 0;
exports.escapeSqlIdentifier = escapeSqlIdentifier;
exports.compile = compile;
exports.raw = raw;
exports.identifier = identifier;
exports.value = value;
exports.json = json;
exports.literal = literal;
exports.join = join;
exports.indent = indent;
exports.indentIf = indentIf;
exports.parens = parens;
exports.symbolAlias = symbolAlias;
exports.placeholder = placeholder;
exports.comment = comment;
exports.arraysMatch = arraysMatch;
exports.isEquivalent = isEquivalent;
exports.replaceSymbol = replaceSymbol;
exports.withTransformer = withTransformer;
exports.isSQL = isSQL;
const tslib_1 = require("tslib");
const lru_1 = tslib_1.__importDefault(require("@graphile/lru"));
const assert = tslib_1.__importStar(require("assert"));
const util_1 = require("util");
const thereCanBeOnlyOne_ts_1 = require("./thereCanBeOnlyOne.js");
var version_ts_1 = require("./version.js");
Object.defineProperty(exports, "version", { enumerable: true, get: function () { return version_ts_1.version; } });
/** Use this to enable coercing objects to SQL to make composing SQL fragments more ergonomic */
exports.$$toSQL = Symbol("toSQL");
function exportAs(thing, exportName) {
const existingExport = thing.$$export;
if (existingExport) {
if (existingExport.exportName !== exportName) {
throw new Error(`Attempted to export same thing under multiple names '${existingExport.exportName}' and '${exportName}'`);
}
}
else {
Object.defineProperty(thing, "$$export", {
value: { moduleName: "pg-sql2", exportName },
});
}
return thing;
}
/** Experimental! */
exports.$$symbolToIdentifier = Symbol("symbolToIdentifier");
const isDev = typeof process !== "undefined" &&
(process.env.GRAPHILE_ENV === "development" ||
process.env.GRAPHILE_ENV === "test");
const defaultIncludeComments = isDev || (typeof process !== "undefined" && process.env.PGSQL2_DEBUG === "1");
const nodeInspect = function (depth, options) {
if (this[thereCanBeOnlyOne_ts_1.$$type] === "VALUE") {
if (depth < 0) {
return `sql.value(...)`;
}
return `sql.value(${(0, util_1.inspect)(this.v, {
...options,
depth: options.depth == null ? null : options.depth - 1,
})})`;
}
else if (this[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
return `sql\`${this.t}\``;
}
else if (this[thereCanBeOnlyOne_ts_1.$$type] === "IDENTIFIER") {
return `sql.identifier(${JSON.stringify(this.n)})`;
}
else if (this[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
if (depth < 0) {
return `sql\`...\``;
}
else {
return `sql\`${this.n.map((n) => (n[thereCanBeOnlyOne_ts_1.$$type] === "RAW" ? n.t : "${" + nodeInspect.call(n, depth + 1, options) + "}")).join("")}\``;
}
}
else {
return `sql{${this[thereCanBeOnlyOne_ts_1.$$type]}}`;
}
};
const pgSQL2Proto = Object.assign(Object.create(null), {
[util_1.inspect.custom]: nodeInspect,
});
const FLAG_HAS_PARENS = 1 << 0;
/**
* This helps us to avoid GC overhead of allocating new raw nodes all the time
* when they're likely to be the same values over and over. The average raw
* string is likely to be around 20 bytes; allowing for 50 bytes once this has
* been turned into an object, 10000 would mean 500kB which seems an acceptable
* amount of memory to consume for this.
*/
const CACHE_RAW_NODES = new lru_1.default({ maxLength: 10000 });
/**
* Given a string, this will return an SQL-identifier-safe version of the
* string using only the characters [0-9a-z_], at least 1 and at most 50
* characters in length, with no leading, trailing or consecutive underscores.
* Information loss is likely, this is only to aid with debugging.
*/
function mangleName(str) {
return (str
// Keep the identifier simple so it doesn't need escaping.
.replace(/[A-Z]/g, (l) => `_${l.toLowerCase()}`)
.replace(/[^a-z0-9_]+/g, "_")
// Avoid double-underscores.
.replace(/__+/g, "_")
.replace(/^_/, "")
// Maximum identifier length in PostgreSQL is 63. We need 5 underscores
// (`__desc_number__`) and the number might be up to, say, 8 digits long.
// (It'll never be anywhere near this, surely?) This leaves 50 characters
// for description.
.substring(0, 50)
// Don't end in an underscore, since we'll be adding one anyway (and we
// don't want double-underscores). This must be done after length limiting
// otherwise we might prune to a length that ends in an underscore.
.replace(/_$/, "") || "local");
}
// Alas we cannot use WeakMap to cache this because symbols cannot be WeakMap
// keys (and we wouldn't want to open the user to memory exhaustion via a map).
// Symbol.prototype.description is available since Node 11.15.0, 12.4.0.
function getSymbolName(symbol) {
return mangleName(symbol.description || "local");
}
// Copied from https://github.com/brianc/node-postgres/blob/860cccd53105f7bc32fed8b1de69805f0ecd12eb/lib/client.js#L285-L302
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
// Trivial performance optimizations by Benjie.
// Replaced with regexp because it's 11x faster by Benjie.
function escapeSqlIdentifier(str) {
return `"${str.replace(/["\0]/g, '""')}"`;
}
function makeRawNode(text, exportName) {
const n = CACHE_RAW_NODES.get(text);
if (n) {
return n;
}
if (typeof text !== "string") {
throw new Error(`[pg-sql2] Invalid argument to makeRawNode - expected string, but received '${(0, util_1.inspect)(text)}'`);
}
const newNode = {
__proto__: pgSQL2Proto,
[thereCanBeOnlyOne_ts_1.$$type]: "RAW",
t: text,
};
if (exportName) {
exportAs(newNode, exportName);
}
// Cannot freeze here, otherwise the SQL node cannot be marked EXPORTABLE later. Maybe wait a tick?
// Object.freeze(newNode);
CACHE_RAW_NODES.set(text, newNode);
return newNode;
}
function makeCommentNode(rawCommentText) {
if (typeof rawCommentText !== "string") {
throw new Error(`[pg-sql2] Invalid argument to makeCommentNode - expected string, but received '${(0, util_1.inspect)(rawCommentText)}'`);
}
const commentText = rawCommentText
.replace(/\*\//g, "* /")
.replace(/\/\*/g, "/ *");
const newNode = {
__proto__: pgSQL2Proto,
[thereCanBeOnlyOne_ts_1.$$type]: "COMMENT",
commentText,
};
return newNode;
}
// Simple function to help V8 optimize it.
function makeIdentifierNode(s, n = getSymbolName(s)) {
return Object.freeze({
__proto__: pgSQL2Proto,
[thereCanBeOnlyOne_ts_1.$$type]: "IDENTIFIER",
s,
n,
});
}
// Simple function to help V8 optimize it.
function makeValueNode(rawValue) {
return Object.freeze({
__proto__: pgSQL2Proto,
[thereCanBeOnlyOne_ts_1.$$type]: "VALUE",
v: rawValue,
});
}
function makeIndentNode(content) {
const flags = content[thereCanBeOnlyOne_ts_1.$$type] === "QUERY" ? content.f : 0;
return Object.freeze({
[thereCanBeOnlyOne_ts_1.$$type]: "INDENT",
f: flags,
c: content[thereCanBeOnlyOne_ts_1.$$type] === "QUERY"
? content
: makeQueryNode([enforceValidNode(content)]),
});
}
function makeSymbolAliasNode(a, b) {
return Object.freeze({
[thereCanBeOnlyOne_ts_1.$$type]: "SYMBOL_ALIAS",
a: a,
b: b,
});
}
function makePlaceholderNode(symbol, fallback) {
return Object.freeze({
[thereCanBeOnlyOne_ts_1.$$type]: "PLACEHOLDER",
s: symbol,
k: fallback,
});
}
function makeQueryNode(nodes, flags = 0) {
let checksum = 0;
for (const node of nodes) {
switch (node[thereCanBeOnlyOne_ts_1.$$type]) {
case "RAW": {
const { t } = node;
// Max value of charCodeAt is 65535. 65535 * 10000 < 2^30.
for (let i = 0, l = t.length, l2 = l > 10000 ? 10000 : l; i < l2; i++) {
checksum += t.charCodeAt(i);
}
break;
}
case "COMMENT": {
break;
}
case "VALUE": {
checksum += 211;
break;
}
case "IDENTIFIER": {
checksum -= 53;
break;
}
case "INDENT": {
checksum += node.c.c;
break;
}
case "SYMBOL_ALIAS": {
checksum += 93;
break;
}
case "PLACEHOLDER": {
checksum += 87;
break;
}
default: {
const never = node[thereCanBeOnlyOne_ts_1.$$type];
throw new Error(`Unrecognized node type ${never}`);
}
}
}
return Object.freeze({
__proto__: pgSQL2Proto,
[thereCanBeOnlyOne_ts_1.$$type]: "QUERY",
n: nodes,
f: flags,
c: checksum,
});
}
function isSQL(node) {
return (typeof node === "object" &&
node !== null &&
typeof node[thereCanBeOnlyOne_ts_1.$$type] === "string");
}
function isSQLable(value) {
return (typeof value === "object" &&
value !== null &&
typeof value[exports.$$toSQL] === "function");
}
function enforceValidNode(node, where) {
if (isSQL(node)) {
return node;
}
for (let i = _userTransformers.length - 1; i >= 0; i--) {
const transformer = _userTransformers[i];
const transformed = transformer(exports.sql, node, where);
if (transformed !== node) {
return enforceValidNode(transformed);
}
else {
// Continue onward; `node` is unchanged
}
}
if (isSQLable(node)) {
const sqlifiedNode = node[exports.$$toSQL]();
if (isSQL(sqlifiedNode)) {
return sqlifiedNode;
}
else {
throw new Error(`[pg-sql2] Invalid $$toSQL method (on ${String(node)}), expected to return an SQL item${where ? ` at ${where}` : ""} but received '${(0, util_1.inspect)(sqlifiedNode)}'. This may mean that there is an issue in the SQL expression where a dynamic value was not escaped via 'sql.value(...)', an identifier wasn't wrapped with 'sql.identifier(...)', or a SQL expression was added without using the \`sql\` tagged template literal. Alternatively, perhaps you forgot to call sql.withTransformer() when building your SQL?`);
}
}
throw new Error(`[pg-sql2] Invalid expression. Expected an SQL item${where ? ` at ${where}` : ""} but received '${(0, util_1.inspect)(node)}'. This may mean that there is an issue in the SQL expression where a dynamic value was not escaped via 'sql.value(...)', an identifier wasn't wrapped with 'sql.identifier(...)', or a SQL expression was added without using the \`sql\` tagged template literal. Alternatively, perhaps you forgot to call sql.withTransformer() when building your SQL?`);
}
/**
* Accepts an sql`...` expression and compiles it out to SQL text with
* placeholders, and the values to substitute for these values.
*/
function compile(sql, options) {
const placeholderValues = options?.placeholderValues;
/**
* Values hold the JavaScript values that are represented in the query string
* by placeholders. They are eager because they were provided before compile
* time.
*/
const values = [];
let valueCount = 0;
/**
* When we come across a symbol in our identifier, we create a unique alias
* for it that shouldn’t be in the users schema. This helps maintain sanity
* when constructing large SQL queries with many aliases.
*/
const symbolToIdentifier = new Map();
const valueToPlaceholder = new Map();
/**
* When the same description is used more than once in different symbols we
* must generate different identifiers, so we keep a counter of each symbol
* usage here.
*/
const descCounter = Object.create(null);
/**
* Makes a friendly name to use in the query for the given SymbolAndName.
*/
function makeIdentifierForSymbol(symbol, safeDesc) {
if (!descCounter[safeDesc]) {
descCounter[safeDesc] = 0;
}
const number = ++descCounter[safeDesc];
// NOTE: we don't omit the suffix for the first instance because safeDesc
// might end in, e.g., `_1` and cause conflicts later; however we do
// replace `_1` with `__`.
const identifierForSymbol = `__${safeDesc}_${number === 1 ? "_" : number}`;
// Store so this symbol gets the same identifier next time
symbolToIdentifier.set(symbol, identifierForSymbol);
return identifierForSymbol;
}
function print(untrustedInput, indent = 0) {
/**
* Join this to generate the SQL query
*/
const sqlFragments = [];
const trustedInput = untrustedInput?.[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? untrustedInput
: enforceValidNode(untrustedInput, ``);
const items = trustedInput[thereCanBeOnlyOne_ts_1.$$type] === "QUERY"
? expandQueryNodes(trustedInput)
: [trustedInput];
const itemCount = items.length;
for (let itemIndex = 0; itemIndex < itemCount; itemIndex++) {
const itemAtIndex = items[itemIndex];
const item = itemAtIndex?.[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? itemAtIndex
: enforceValidNode(itemAtIndex, `item ${itemIndex}`);
switch (item[thereCanBeOnlyOne_ts_1.$$type]) {
case "RAW": {
if (item.t === "") {
// No need to add blank raw text!
break;
}
// Special-case `;` - remove the previous \n
if (isDev && itemIndex === itemCount - 1 && item.t === ";") {
const prevIndex = sqlFragments.length - 1;
const prev = sqlFragments[prevIndex];
if (prev.endsWith("\n")) {
sqlFragments[prevIndex] = prev.substring(0, prev.length - 1);
}
}
sqlFragments.push(isDev ? item.t.replace(/\n/g, "\n" + " ".repeat(indent)) : item.t);
break;
}
case "COMMENT": {
sqlFragments.push(`/* ${item.commentText} */`);
break;
}
case "IDENTIFIER": {
// Get the correct identifier string for this symbol.
let identifierForSymbol = symbolToIdentifier.get(item.s);
// If there is no identifier, create one and set it.
if (!identifierForSymbol) {
identifierForSymbol = makeIdentifierForSymbol(item.s, item.n);
}
// Return the identifier. Since we create it, we won’t have to
// escape it because we know all of the characters are safe.
sqlFragments.push(identifierForSymbol);
break;
}
case "VALUE": {
const existing = valueToPlaceholder.get(item);
if (existing != null) {
sqlFragments.push(existing);
}
else {
valueCount++;
const sqlString = `$${valueCount}`;
valueToPlaceholder.set(item, sqlString);
if (valueCount > 65535) {
throw new Error("[pg-sql2] This SQL statement would contain too many placeholders; PostgreSQL supports at most 65535 placeholders. To solve this, consider refactoring the query to use arrays/unnest where possible, or split it into multiple queries.");
}
values[valueCount - 1] = item.v;
sqlFragments.push(sqlString);
}
break;
}
case "INDENT": {
assert.ok(isDev, "INDENT nodes only allowed in development mode");
sqlFragments.push("\n" +
" ".repeat(indent + 1) +
print(item.c, indent + 1) +
"\n" +
" ".repeat(indent));
break;
}
case "SYMBOL_ALIAS": {
const symbol1 = item.a;
const symbol2 = item.b;
const name1 = symbolToIdentifier.get(symbol1);
const name2 = symbolToIdentifier.get(symbol2);
if (name1 && name2 && name1 !== name2) {
throw new Error("ERROR: sql.symbolAlias was used after the symbols were used, they already have (non-matching) aliases");
}
if (name1) {
symbolToIdentifier.set(symbol2, name1);
}
else if (name2) {
symbolToIdentifier.set(symbol1, name2);
}
else {
const identifierForSymbol = makeIdentifierForSymbol(symbol1, getSymbolName(symbol1));
symbolToIdentifier.set(symbol1, identifierForSymbol);
symbolToIdentifier.set(symbol2, identifierForSymbol);
}
break;
}
case "PLACEHOLDER": {
// TODO: should `item.s` be checked in symbol substitutes?
const resolvedPlaceholder = placeholderValues?.get(item.s) ?? item.k;
if (!resolvedPlaceholder) {
throw new Error("ERROR: sql.placeholder was used in this query, but no value was supplied for it, and it has no fallback.");
}
const resolved = print(resolvedPlaceholder, indent);
if (resolved !== "") {
sqlFragments.push(resolved);
}
break;
}
default: {
const never = item;
// This cannot happen
throw new Error(`Unsupported node found in SQL: ${(0, util_1.inspect)(never)}`);
}
}
}
return sqlFragments.join("");
}
const text = isDev ? print(sql).replace(/\n\s*\n/g, "\n") : print(sql);
return {
text,
values,
[exports.$$symbolToIdentifier]: symbolToIdentifier,
};
}
// LRU not necessary
const CACHE_SIMPLE_FRAGMENTS = new Map();
/**
* A template string tag that creates a `SQL` query out of some strings and
* some values. Use this to construct all PostgreSQL queries to avoid SQL
* injection.
*
* Note that using this function, the user *must* specify if they are injecting
* raw text. This makes a SQL injection vulnerability harder to create.
*/
const sqlBase = function sql(strings, ...values) {
if (!Array.isArray(strings) || !strings.raw) {
throw new Error("[pg-sql2] sql should be used as a template literal, not a function call.");
}
const stringsLength = strings.length;
const first = strings[0];
// Reduce memory churn with a cache
if (stringsLength === 1) {
if (first === "") {
return exports.blank;
}
const existing = CACHE_SIMPLE_FRAGMENTS.get(first);
if (existing)
return existing;
const node = makeRawNode(first);
CACHE_SIMPLE_FRAGMENTS.set(first, node);
return node;
}
// Special case sql`${...}` - just return the node directly
if (stringsLength === 2 && strings[0] === "" && strings[1] === "") {
return enforceValidNode(values[0]);
}
const items = [];
let currentText = "";
for (let i = 0, l = stringsLength; i < l; i++) {
const text = strings[i];
currentText += text;
if (i < l - 1) {
const rawVal = values[i];
const valid = rawVal?.[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? rawVal
: enforceValidNode(rawVal, `template literal placeholder ${i}`);
if (valid[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
currentText += valid.t;
}
else if (valid[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
// NOTE: this clears the flags
const nodes = expandQueryNodes(valid);
const nodeCount = nodes.length;
for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
const node = nodes[nodeIndex];
if (node[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
currentText += node.t;
}
else {
if (currentText !== "") {
items.push(makeRawNode(currentText));
currentText = "";
}
items.push(node);
}
}
}
else {
if (currentText !== "") {
items.push(makeRawNode(currentText));
currentText = "";
}
items.push(valid);
}
}
}
if (currentText !== "") {
items.push(makeRawNode(currentText));
currentText = "";
}
return items.length === 1 ? items[0] : makeQueryNode(items);
};
let sqlRawWarningOutput = false;
/**
* Creates a SQL item for some raw SQL text. Just plain ol‘ raw SQL. This
* method is dangerous though because it involves no escaping, so proceed with
* caution! It's very very rarely warranted - there is likely a safer way of
* achieving your goal.
*/
function raw(text) {
if (!sqlRawWarningOutput) {
sqlRawWarningOutput = true;
try {
throw new Error("sql.raw first invoked here");
}
catch (e) {
console.warn(`[pg-sql2] WARNING: you're using the sql.raw escape hatch, usage of this API is rarely required and is highly discouraged. Please be sure this is what you intend. ${e.stack}`);
}
}
if (typeof text !== "string") {
throw new Error(`[pg-sql2] sql.raw must be passed a string, but it was passed '${(0, util_1.inspect)(text)}'.`);
}
return makeRawNode(text);
}
/**
* Creates a SQL item for a SQL identifier. A SQL identifier is anything like
* a table, schema, or column name. An identifier may also have a namespace,
* thus why many names are accepted.
*/
function identifier(...names) {
// By doing the validation here rather than at compile time we can reduce
// redundant work - the same sql.identifier can be used in multiple queries.
const nameCount = names.length;
if (nameCount === 0) {
throw new Error("[pg-sql2] Invalid call to sql.identifier() - you must pass at least one argument.");
}
const items = [];
let currentText = "";
for (let i = 0; i < nameCount; i++) {
if (i > 0) {
currentText += ".";
}
const name = names[i];
if (typeof name === "string") {
// By escaping here rather than during compile we can reduce redundant computation.
const escaped = escapeSqlIdentifier(name);
currentText += escaped;
}
else if (typeof name === "symbol") {
if (currentText !== "") {
items.push(makeRawNode(currentText));
currentText = "";
}
// The final name has to be evaluated at compile-time, but we can at least mangle it up front.
items.push(makeIdentifierNode(name));
}
else {
throw new Error(`[pg-sql2] Invalid argument to sql.identifier - argument ${i} (0-indexed) should be a string or a symbol, but was '${(0, util_1.inspect)(name)}'`);
}
}
if (currentText !== "") {
items.push(makeRawNode(currentText));
currentText = "";
}
return items.length === 1 ? items[0] : makeQueryNode(items);
}
/**
* Creates a SQL item for a value that will be included in our final query.
* This value will be added in a way which avoids SQL injection.
*/
function value(val) {
return makeValueNode(val);
}
/** Equivalent to
*
* ```
* sql`${JSON.stringify(val)}::json`
* ```
*/
function json(val) {
return (0, exports.sql) `${value(JSON.stringify(val))}::json`;
}
const trueNode = makeRawNode(`TRUE`, "true");
exports.TRUE = trueNode;
exports.true = trueNode;
const falseNode = makeRawNode(`FALSE`, "false");
exports.FALSE = falseNode;
exports.false = falseNode;
const nullNode = makeRawNode(`NULL`, "null");
exports.NULL = nullNode;
exports.null = nullNode;
exports.blank = makeRawNode(``, "blank");
exports.dot = makeRawNode(`.`, "dot");
const OPEN_PARENS = makeRawNode(`(`);
const CLOSE_PARENS = makeRawNode(`)`);
/**
* If the value is simple will inline it into the query, otherwise will defer
* to `sql.value`.
*/
function literal(val) {
if (typeof val === "string" && val.match(/^[-a-zA-Z0-9_@!$ :".]*$/)) {
// Examples of things we'd like to be included raw:
// - 1
// - myFieldName
// - @@myFieldName
// - $myField$
// - YYYY-MM-DD"T"HH24:MI:SS.USTZH:TZM
return makeRawNode(`'${val}'`);
}
else if (typeof val === "number" && Number.isFinite(val)) {
if (Number.isInteger(val)) {
return makeRawNode("" + val);
}
else {
return makeRawNode(`'${0 + val}'::float`);
}
}
else if (typeof val === "boolean") {
return val ? trueNode : falseNode;
}
else if (val == null) {
return nullNode;
}
else {
return value(val);
}
}
/**
* Join some SQL items together, optionally separated by a string. Useful when
* dealing with lists of SQL items, for example a dynamic list of columns or
* variadic SQL function arguments.
*/
function join(items, separator = "") {
if (!Array.isArray(items)) {
throw new Error(`[pg-sql2] Invalid sql.join call - the first argument should be an array, but it was '${(0, util_1.inspect)(items)}'.`);
}
if (typeof separator !== "string") {
throw new Error(`[pg-sql2] Invalid separator passed to sql.join - must be a string, but we received '${(0, util_1.inspect)(separator)}'`);
}
// Short circuit joins of size <= 1
if (items.length === 0) {
return exports.blank;
}
else if (items.length === 1) {
const rawNode = items[0];
const node = rawNode?.[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? rawNode
: enforceValidNode(rawNode, `join item ${0}`);
return node;
}
const hasSeparator = separator.length > 0;
let currentText = "";
const currentItems = [];
for (let i = 0, l = items.length; i < l; i++) {
const rawNode = items[i];
const addSeparator = i > 0 && hasSeparator;
const node = rawNode?.[thereCanBeOnlyOne_ts_1.$$type] !== undefined
? rawNode
: enforceValidNode(rawNode, `join item ${i}`);
if (addSeparator) {
currentText += separator;
}
if (node[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
for (const innerNode of expandQueryNodes(node)) {
if (innerNode[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
currentText += innerNode.t;
}
else {
if (currentText !== "") {
currentItems.push(makeRawNode(currentText));
currentText = "";
}
currentItems.push(innerNode);
}
}
}
else if (node[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
currentText += node.t;
}
else {
if (currentText !== "") {
currentItems.push(makeRawNode(currentText));
currentText = "";
}
currentItems.push(node);
}
}
if (currentText !== "") {
currentItems.push(makeRawNode(currentText));
currentText = "";
}
return currentItems.length === 1
? currentItems[0]
: makeQueryNode(currentItems);
}
function expandQueryNodes(node) {
const parens = (node.f & FLAG_HAS_PARENS) === FLAG_HAS_PARENS;
if (parens) {
return [OPEN_PARENS, ...node.n, CLOSE_PARENS];
}
else {
return node.n;
}
}
function indent(fragmentOrStrings, ...values) {
const fragment = values.length > 0
? (0, exports.sql)(fragmentOrStrings, ...values)
: fragmentOrStrings;
if (!isDev) {
return fragment;
}
return makeIndentNode(fragment);
}
function indentIf(condition, fragment) {
return isDev && condition ? makeIndentNode(fragment) : fragment;
}
const QUOTED_IDENTIFIER_REGEX = /^"[^"]+"$/;
const UNQUOTED_IDENTIFIER_REGEX = /^[a-zA-Z0-9_]+$/;
const NUMBER_REGEX_1 = /^[0-9]+(?:\.[0-9]+)?$/;
const NUMBER_REGEX_2 = /^\.[0-9]+$/;
const STRING_REGEX = /^'[^']+'$/;
/** For determining if a raw SQL string looks like an identifier (for parens reasons) */
function isIdentifierLike(p) {
return p.match(QUOTED_IDENTIFIER_REGEX) || p.match(UNQUOTED_IDENTIFIER_REGEX);
}
function parenthesize(fragment, inFlags = 0) {
const flags = inFlags | FLAG_HAS_PARENS;
if (fragment[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
return makeQueryNode(fragment.n, flags);
}
else {
return makeQueryNode([fragment], flags);
}
}
/**
* Wraps the given fragment in parens if necessary (or if forced, e.g. for a
* subquery or maybe stylistically a join condition).
*
* Returns the input SQL fragment if it does not need parenthesis to be
* inserted into another expression, otherwise a parenthesised fragment if not
* doing so could cause ambiguity. We're relying on the user to be sensible
* here, this is not fool-proof.
*
* @remarks The following are all parens safe:
*
* - A placeholder `$1`
* - A number `0.123456`
* - A string `'Foo bar'` / `E'Foo bar'`
* - An identifier `table.column` / `"MyTaBlE"."MyCoLuMn"`
*
* The following might seem but are not parens safe:
*
* - A function call `schema.func(param)` - reason: `schema.func(param).*`
* should be `(schema.func(param)).*`
* - A simple expression `1 = 2` - reason: `1 = 2 = false` is invalid; whereas
* `(1 = 2) = false` is fine. Similarly `1 = 2::text` differs from `(1 = 2)::text`.
* - An identifier `table.column.attribute` / `"MyTaBlE"."MyCoLuMn"."MyAtTrIbUtE"` (this needs to be `(table.column).attribute`)
*/
function parens(frag, force) {
if (frag[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
if ((frag.f & FLAG_HAS_PARENS) === FLAG_HAS_PARENS) {
return frag;
}
const { n: nodes } = frag;
const nodeCount = nodes.length;
if (nodeCount === 0) {
throw new Error(`You're wrapping an empty fragment in parens; this is likely an error. If this is deliberate, please explicitly use parenthesis.`);
}
else if (nodeCount === 1) {
return parens(nodes[0], force);
}
else if (force) {
return parenthesize(frag);
}
else if (nodeCount === 2) {
// Check for `IDENTIFIER.rawtext`
// ENHANCE: check for 'rawtext.IDENTIFIER' too
const [identifier, rawtext] = nodes;
if (identifier[thereCanBeOnlyOne_ts_1.$$type] !== "IDENTIFIER" ||
rawtext[thereCanBeOnlyOne_ts_1.$$type] !== "RAW" ||
!rawtext.t.startsWith(".")) {
return parenthesize(frag);
}
const parts = rawtext.t.slice(1).split(".");
if (!parts.every(isIdentifierLike)) {
return parenthesize(frag);
}
return frag;
}
else if (nodeCount === 3) {
// Check for `IDENTIFIER.IDENTIFIER`
for (let i = 0; i < nodeCount; i++) {
const node = nodes[i];
if (i % 2 === 0) {
if (node[thereCanBeOnlyOne_ts_1.$$type] !== "IDENTIFIER" &&
(node[thereCanBeOnlyOne_ts_1.$$type] !== "RAW" || !isIdentifierLike(node.t))) {
return parenthesize(frag);
}
}
else {
if (node[thereCanBeOnlyOne_ts_1.$$type] !== "RAW" || node.t !== ".") {
return parenthesize(frag);
}
}
}
return frag;
}
else {
return parenthesize(frag);
}
}
else if (frag[thereCanBeOnlyOne_ts_1.$$type] === "INDENT") {
const inner = parens(frag.c, force);
if (inner[thereCanBeOnlyOne_ts_1.$$type] === "QUERY" &&
(inner.f & FLAG_HAS_PARENS) === FLAG_HAS_PARENS) {
// Move the parens to outside
return parenthesize(indent(makeQueryNode(inner.n)), inner.f);
}
else if (inner === frag.c) {
return frag;
}
else {
return makeIndentNode(inner);
}
}
else if (force) {
return parenthesize(frag);
}
else if (frag[thereCanBeOnlyOne_ts_1.$$type] === "VALUE") {
return frag;
}
else if (frag[thereCanBeOnlyOne_ts_1.$$type] === "IDENTIFIER") {
return frag;
}
else if (frag[thereCanBeOnlyOne_ts_1.$$type] === "COMMENT") {
return frag;
}
else if (frag[thereCanBeOnlyOne_ts_1.$$type] === "RAW") {
const expr = frag.t;
if (expr.match(NUMBER_REGEX_1) || expr.match(NUMBER_REGEX_2)) {
return frag;
}
else if (expr.match(STRING_REGEX)) {
return frag;
}
else {
// Identifiers
const parts = expr.split(".");
if (parts.every(isIdentifierLike)) {
return frag;
}
}
}
return parenthesize(frag);
}
function symbolAlias(symbol1, symbol2) {
return makeSymbolAliasNode(symbol1, symbol2);
}
function placeholder(symbol, fallback) {
return makePlaceholderNode(symbol, fallback);
}
function comment(text, include = defaultIncludeComments) {
if (include) {
return makeCommentNode(text);
}
else {
return exports.blank;
}
}
function arraysMatch(array1, array2, comparator) {
if (array1 === array2)
return true;
const l = array1.length;
if (l !== array2.length) {
return false;
}
if (comparator != null) {
for (let i = 0; i < l; i++) {
const a = array1[i];
const b = array2[i];
if (a !== b && !comparator(a, b)) {
return false;
}
}
}
else {
for (let i = 0; i < l; i++) {
if (array1[i] !== array2[i]) {
return false;
}
}
}
return true;
}
function isEquivalent(sql1, sql2, options) {
if (sql1 === sql2) {
return true;
}
else if (sql1[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
if (sql2[thereCanBeOnlyOne_ts_1.$$type] !== "QUERY" || sql2.f !== sql1.f || sql2.c !== sql1.c) {
return false;
}
return arraysMatch(sql1.n, sql2.n, (a, b) => isEquivalent(a, b, options));
}
else if (sql2[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
return false;
}
else {
switch (sql1[thereCanBeOnlyOne_ts_1.$$type]) {
case "RAW": {
if (sql2[thereCanBeOnlyOne_ts_1.$$type] !== sql1[thereCanBeOnlyOne_ts_1.$$type]) {
return false;
}
return sql1.t === sql2.t;
}
case "VALUE": {
if (sql2[thereCanBeOnlyOne_ts_1.$$type] !== sql1[thereCanBeOnlyOne_ts_1.$$type]) {
return false;
}
return sql1.v === sql2.v;
}
case "INDENT": {
if (sql2[thereCanBeOnlyOne_ts_1.$$type] !== sql1[thereCanBeOnlyOne_ts_1.$$type]) {
return false;
}
return isEquivalent(sql1.c, sql2.c, options);
}
case "COMMENT": {
return true;
}
case "IDENTIFIER": {
if (sql2[thereCanBeOnlyOne_ts_1.$$type] !== sql1[thereCanBeOnlyOne_ts_1.$$type]) {
return false;
}
const { n: ids1n, s: ids1s } = sql1;
const { n: ids2n, s: ids2s } = sql2;
const namesMatch = ids1n === ids2n;
const symbolSubstitutes = options?.symbolSubstitutes;
const symbol1 = getSubstitute(ids1s, symbolSubstitutes);
const symbol2 = getSubstitute(ids2s, symbolSubstitutes);
const symbolsMatch = symbol1 === symbol2;
return namesMatch && symbolsMatch;
}
case "PLACEHOLDER": {
if (sql2[thereCanBeOnlyOne_ts_1.$$type] !== sql1[thereCanBeOnlyOne_ts_1.$$type]) {
return false;
}
const symbolSubstitutes = options?.symbolSubstitutes;
const symbol1 = getSubstitute(sql1.s, symbolSubstitutes);
const symbol2 = getSubstitute(sql2.s, symbolSubstitutes);
return symbol1 === symbol2;
}
case "SYMBOL_ALIAS": {
// TODO
return false;
}
default: {
const never = sql1;
console.error(`Unhandled node type: ${(0, util_1.inspect)(never)}`);
return false;
}
}
}
}
function replaceSymbolInNode(frag, needle, replacement) {
switch (frag[thereCanBeOnlyOne_ts_1.$$type]) {
case "COMMENT":
case "RAW": {
return frag;
}
case "IDENTIFIER": {
if (frag.s === needle) {
return makeIdentifierNode(replacement);
}
else {
return frag;
}
}
case "VALUE": {
return frag.v === needle
? makeValueNode(replacement)
: frag;
}
case "INDENT": {
return makeIndentNode(replaceSymbol(frag.c, needle, replacement));
}
case "SYMBOL_ALIAS": {
const { a, b } = frag;
const newA = a === needle ? replacement : a;
const newB = b === needle ? replacement : b;
if (newA !== a || newB !== b) {
return makeSymbolAliasNode(newA, newB);
}
else {
return frag;
}
}
case "PLACEHOLDER": {
if (frag.s === needle) {
return makePlaceholderNode(replacement, frag.k);
}
else {
return frag;
}
}
default: {
const never = frag;
throw new Error(`Unhandled SQL type ${never[thereCanBeOnlyOne_ts_1.$$type]}`);
}
}
}
/**
* @experimental
*/
function replaceSymbol(frag, needle, replacement) {
if (frag[thereCanBeOnlyOne_ts_1.$$type] === "QUERY") {
let changed = false;
const newNodes = frag.n.map((node) => {
const newNode = replaceSymbolInNode(node, needle, replacement);
if (newNode !== node) {
changed = true;
}
return newNode;
});
return changed ? makeQueryNode(newNodes, frag.f) : frag;
}
else {
return replaceSymbolInNode(frag, needle, replacement);
}
}
/**
* @internal
*/
function getSubstitute(initialSymbol, symbolSubstitutes) {
const path = [];
let symbol = initialSymbol;
for (let i = 0; i < 1000; i++) {
if (path.includes(symbol)) {
throw new Error(`symbolSubstitute cycle detected: ${path
.map((s) => (0, util_1.inspect)(s))
.join(" -> ")} -> ${(0, util_1.inspect)(symbol)}`);
}
const sub = symbolSubstitutes?.get(symbol);
if (sub === symbol) {
throw new Error(`Invalid symbolSubstitutes - a symbol cannot be an alias for itself! Symbol: ${(0, util_1.inspect)(symbol)}`);
}
else if (sub) {
path.push(symbol);
symbol = sub;
}
else {
return symbol;
}
}
throw new Error("symbolSubstitutes depth too deep");
}
const _userTransformers = [];
function withTransformer(transformer, callback) {
_userTransformers.push(transformer);
try {
return callback(exports.sql);
}
finally {
_userTransformers.pop();
}
}
/** If node is an identifier, return its symbol, otherwise return null */
function getIdentifierSymbol(potentialIdentifier) {
if (potentialIdentifier[thereCanBeOnlyOne_ts_1.$$type] === "IDENTIFIER") {
return potentialIdentifier.s;
}
else {
return null;
}
}
exports.sql = sqlBase;
exports.fragment = exports.sql;
exports.query = exports.sql;
exports.default = exports.sql;
const attributes = {
sql: exports.sql,
escapeSqlIdentifier,
compile,
isEquivalent,
query: exports.sql,
raw,
identifier,
value,
json,
literal,
join,
indent,
indentIf,
parens,
comment,
symbolAlias,
placeholder,
blank: exports.blank,
fragment: exports.sql,
true: trueNode,
TRUE: trueNode,
false: falseNode,
FALSE: falseNode,
null: nullNode,
NULL: nullNode,
replaceSymbol,
isSQL,
withTransformer,
getIdentifierSymbol,
};
Object.entries(attributes).forEach(([exportName, value]) => {
if (!value.$$export) {
exportAs(value, exportName);
}
});
Object.assign(sqlBase, attributes);
//# sourceMappingURL=index.js.map