@digitalwalletcorp/sql-builder
Version:
This is a library for building SQL
348 lines • 14 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbstractSyntaxTree = void 0;
const common = __importStar(require("./common"));
// 演算子の優先順位と結合性 (より長い演算子を先に定義)
const PRECEDENCE = {
'||': { precedence: 1, associativity: 'left' },
'&&': { precedence: 2, associativity: 'left' },
'==': { precedence: 3, associativity: 'left' },
'!=': { precedence: 3, associativity: 'left' },
'===': { precedence: 3, associativity: 'left' },
'!==': { precedence: 3, associativity: 'left' },
'<': { precedence: 4, associativity: 'left' },
'<=': { precedence: 4, associativity: 'left' },
'>': { precedence: 4, associativity: 'left' },
'>=': { precedence: 4, associativity: 'left' },
'!': { precedence: 5, associativity: 'right' } // 単項演算子は高優先度
};
/**
* SQLBuilderの条件文(*IF*)で指定された条件文字列を解析するためのAST。
* JavaScriptの文法をカバーするものではなく、SQLBuilderで利用可能な限定的な構文のみサポートする。
*/
class AbstractSyntaxTree {
/**
* 与えられた条件文字列を構文解析し、entityに対する条件として成立するか評価する
*
* @param {string} condition "params != null && params.length > 10" のような条件
* @param {Record<string, any>} entity
* @returns {boolean}
*/
evaluateCondition(condition, entity) {
try {
const tokens = this.tokenize(condition);
const rpnTokens = this.shuntingYard(tokens);
const result = this.evaluateRpn(rpnTokens, entity);
return result;
}
catch (error) {
error.condition = condition;
error.entity = entity;
throw error;
}
}
/**
* 与えられた条件文字列をトークンに分割する
*
* @param {string} condition
* @returns {Token[]}
*/
tokenize(condition) {
const tokens = [];
let i = 0;
while (i < condition.length) {
const char = condition[i];
// 空白をスキップ
if (/\s/.test(char)) {
i++;
continue;
}
// 演算子 (長いものからチェック)
// 3桁定義
const chunk3 = condition.substring(i, i + 3);
switch (chunk3) {
case '===':
case '!==':
tokens.push({ type: 'OPERATOR', value: chunk3 });
i += 3;
continue;
default:
}
// 2桁定義
const chunk2 = condition.substring(i, i + 2);
switch (chunk2) {
case '==':
case '!=':
case '<=':
case '>=':
case '&&':
case '||':
tokens.push({ type: 'OPERATOR', value: chunk2 });
i += 2;
continue;
default:
}
// 1桁定義
const chunk1 = char;
switch (chunk1) {
case '>':
case '<':
case '!':
case '=':
tokens.push({ type: 'OPERATOR', value: chunk1 });
i += 1;
continue;
case '(':
case ')':
tokens.push({ type: 'PARENTHESIS', value: chunk1 });
i += 1;
continue;
default:
}
const reststring = condition.substring(i); // 現在のインデックスから末尾までの文字列
// 数値リテラル
const numMatch = reststring.match(/^-?\d+(\.\d+)?/);
if (numMatch) {
tokens.push({ type: 'NUMBER', value: parseFloat(numMatch[0]) });
i += numMatch[0].length;
continue;
}
// 文字列リテラル
switch (chunk1) {
case '\'':
case '"':
const quote = chunk1;
let j = i + 1;
let strValue = '';
while (j < condition.length && condition[j] !== quote) {
// エスケープ文字の処理 (\' や \\) は必要に応じて追加
if (condition[j] === '\\' && j + 1 < condition.length) {
strValue += condition[j + 1];
j += 2;
}
else {
strValue += condition[j];
j++;
}
}
if (condition[j] === quote) {
tokens.push({ type: 'STRING', value: strValue });
i = j + 1;
continue;
}
else {
// クォートが閉じられていない
throw new Error(`[SQLBuilder.AbstractSyntaxTree] Unterminated string literal: '${condition}', index: ${j}`);
}
default:
}
// 識別子 (変数名, true, false, null, undefined, length, ?.を含むプロパティチェーン)
// ドットと疑問符を含んだプロパティチェーンを識別子としてパースする
const identMatch = reststring.match(/^[a-zA-Z_][a-zA-Z0-9_.]*(\?\.?[a-zA-Z0-9_]+)*/);
if (identMatch) {
const ident = identMatch[0];
switch (ident) {
case 'true':
tokens.push({ type: 'BOOLEAN', value: true });
break;
case 'false':
tokens.push({ type: 'BOOLEAN', value: false });
break;
case 'null':
tokens.push({ type: 'NULL', value: null });
break;
case 'undefined':
tokens.push({ type: 'UNDEFINED', value: undefined });
break;
default:
tokens.push({ type: 'IDENTIFIER', value: ident }); // プロパティ名
}
i += ident.length;
continue;
}
// 未知の文字
throw new Error(`[SQLBuilder.AbstractSyntaxTree] Unexpected character in condition: ${char} at index ${i}`);
}
return tokens;
}
/**
* Shunting Yardアルゴリズムで構文を逆ポーランド記法(Reverse Polish Notation)に変換する
*
* @param {Token[]} tokens
* @returns {Token[]}
*/
shuntingYard(tokens) {
const output = [];
// operatorStackにはIDENTIFIERとPARENTHESISしか格納されないので、必ず{ value: string }を持つ
const operatorStack = [];
for (const token of tokens) {
switch (token.type) {
case 'NUMBER':
case 'BOOLEAN':
case 'NULL':
case 'UNDEFINED':
case 'STRING':
case 'IDENTIFIER':
output.push(token);
break;
case 'OPERATOR':
const op1 = token;
while (operatorStack.length) {
const op2 = operatorStack[operatorStack.length - 1];
// 括弧内は処理しない
if (op2.value === '(') {
break;
}
// 優先順位のルールに従う
if (PRECEDENCE[op1.value].associativity === 'left'
&& PRECEDENCE[op1.value].precedence <= PRECEDENCE[op2.value].precedence) {
output.push(operatorStack.pop());
}
else {
break;
}
}
operatorStack.push(op1);
break;
case 'PARENTHESIS':
if (token.value === '(') {
operatorStack.push(token);
}
else if (token.value === ')') {
let foundLeftParen = false;
while (operatorStack.length) {
const op = operatorStack.pop();
if (op.value === '(') {
foundLeftParen = true;
break;
}
output.push(op);
}
if (!foundLeftParen) {
throw new Error('[SQLBuilder.AbstractSyntaxTree] Mismatched parentheses');
}
}
break;
// default:
}
}
while (operatorStack.length) {
const op = operatorStack.pop();
if (op.value === '(' || op.value === ')') {
throw new Error('[SQLBuilder.AbstractSyntaxTree] Mismatched parentheses');
}
output.push(op);
}
return output;
}
/**
* 逆ポーランド記法(Reverse Polish Notation)のトークンを評価する
*
* @param {Token[]} rpnTokens
* @param {Record<string, any>} entity
* @returns {boolean}
*/
evaluateRpn(rpnTokens, entity) {
const stack = [];
for (const token of rpnTokens) {
switch (token.type) {
case 'NUMBER':
case 'BOOLEAN':
case 'STRING':
case 'NULL':
case 'UNDEFINED':
stack.push(token.value);
break;
case 'IDENTIFIER':
// オプショナルチェイニングを考慮したgetPropertyを呼び出す
stack.push(common.getProperty(entity, token.value));
break;
case 'OPERATOR':
// 単項演算子 '!'
if (token.value === '!') {
const operand = stack.pop();
stack.push(!operand);
break;
}
// 二項演算子
const right = stack.pop();
const left = stack.pop();
switch (token.value) {
case '==':
stack.push(left == right);
break;
case '!=':
stack.push(left != right);
break;
case '===':
stack.push(left === right);
break;
case '!==':
stack.push(left !== right);
break;
case '<':
stack.push(left < right);
break;
case '<=':
stack.push(left <= right);
break;
case '>':
stack.push(left > right);
break;
case '>=':
stack.push(left >= right);
break;
case '&&':
stack.push(left && right);
break;
case '||':
stack.push(left || right);
break;
default: throw new Error(`[SQLBuilder.AbstractSyntaxTree] Unknown operator: ${token.value}`);
}
break;
// default:
}
}
if (stack.length !== 1) {
throw new Error(`[SQLBuilder.AbstractSyntaxTree] Invalid expression: ${JSON.stringify(rpnTokens)}`);
}
// undefinedやnullが返ってきた場合、falseと評価されるようにする
return !!stack[0];
}
}
exports.AbstractSyntaxTree = AbstractSyntaxTree;
//# sourceMappingURL=abstract-syntax-tree.js.map