"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const errors_1 = require("./errors");
const keywords_1 = require("../constants/keywords");
const sql_parser_1 = require("../lib/sql-parser");
const get_word_at_offset_1 = __importDefault(require("../lib/get-word-at-offset"));
const find_all_indexes_1 = __importDefault(require("../lib/find-all-indexes"));
class Analyzer {
    constructor(logger) {
        this.logger = logger;
        this.queryToParseResult = new Map();
    }
    analyze(context, schemaTables = []) {
        this.logger.log('analyze()');
        if (!context.text) {
            throw new errors_1.EmptyQueryError();
        }
        const cachedResult = this.queryToParseResult.get(context.text);
        if (cachedResult) {
            this.analyzeResult(context, schemaTables, cachedResult);
            return;
        }
        this.logger.log('analyze() - Query going into parser: ' + context.text);
        const result = sql_parser_1.parse(context.text);
        this.queryToParseResult.set(context.text, result);
        this.analyzeResult(context, schemaTables, result);
    }
    analyzeResult(context, schemaTables, result) {
        const { error, statements } = result;
        if (error) {
            this.analyzeSyntax(context, error);
        }
        else {
            this.analyzeSemantics(context, statements, schemaTables);
        }
    }
    getParseResult(query) {
        const result = this.queryToParseResult.get(query);
        if (result) {
            return result;
        }
        return sql_parser_1.parse(query);
    }
    /**
     * Analyze the syntax error.
     *
     *   Note: there will always be a position of the error, but not always a `near` word.
     *   See: https://github.com/vitessio/vitess/blob/master/go/vt/sqlparser/token.go#L456
     *
     * @param context
     * @param error
     */
    analyzeSyntax(context, error) {
        this.logger.log('.analyzeSyntax()' + context.rawText);
        const { near, position } = error;
        let word = near;
        // parser could not identify a nearest word, so let's try to find one ourselves
        if (!word) {
            const wordAndRange = get_word_at_offset_1.default(position, context.rawText);
            if (wordAndRange) {
                // found a nearest word, continue
                word = wordAndRange.word;
            }
            else {
                // could not find a nearest word
                throw new errors_1.SyntaxErrorGeneric({
                    start: position,
                    end: position
                });
            }
        }
        // there will always be a nearest word here
        const end = position;
        const start = position - word.length;
        if (keywords_1.keywords.includes(word.toLowerCase())) {
            throw new errors_1.SyntaxErrorKeyword({
                start,
                end,
                keyword: word
            });
        }
        throw new errors_1.SyntaxErrorNotKeyword({
            start,
            end,
            unidentifiedWord: word
        });
    }
    analyzeSemantics(context, statements, schemaTables) {
        // cannot validate without schema
        if (!schemaTables.length) {
            return;
        }
        statements.forEach((statement) => {
            if (statement.type === 'DDL') {
                return;
            }
            this.analyzeTables(context, statement.tables, schemaTables);
        });
    }
    analyzeTables(context, queryTables, schemaTables) {
        queryTables.forEach(queryTable => {
            const { name } = queryTable;
            // SQL parser returns "dual" as table name for expressions with "*"
            // https://github.com/xwb1989/sqlparser/blob/master/sql.y#L1672
            if (!name || name === 'dual') {
                return;
            }
            const schemaTable = schemaTables.find(t => t.name === name);
            if (!schemaTable) {
                const position = this.getFirstPosition(context.rawText, name);
                if (!position) {
                    return;
                }
                throw new errors_1.SemanticErrorBadTable({
                    table: name,
                    start: position.start,
                    end: position.end
                });
            }
            this.analyzeColumns(context, queryTable, schemaTable);
        });
    }
    analyzeColumns(context, queryTable, schemaTable) {
        const { name: tableName, columns: queryColumns } = queryTable;
        queryColumns.forEach(queryColumn => {
            const { name: columnName } = queryColumn;
            const schemaColumn = schemaTable.columns.find(c => c.name === columnName);
            if (schemaColumn) {
                this.analyzeColumnType(context, queryColumn, schemaColumn);
                return;
            }
            const position = this.getFirstPosition(context.rawText, columnName);
            if (!position) {
                return;
            }
            throw new errors_1.SemanticErrorBadColumn({
                column: columnName,
                table: tableName,
                start: position.start,
                end: position.end
            });
        });
    }
    analyzeColumnType(context, queryColumn, schemaColumn) {
        const { name: columnName, operator, value, tsType, inType } = queryColumn;
        if (!tsType || !inType) {
            return;
        }
        // type is correct
        if (tsType === schemaColumn.tsType) {
            return;
        }
        // special case for null
        if (tsType === 'null' && schemaColumn.optional) {
            return;
        }
        let pattern = new RegExp(value);
        // e.g. 'SELECT id from workspaces WHERE version = 1'
        // expression: version = 1, columnName: version, operator: =, value: 1
        // also matches against embedded expressions e.g. SELECT id from workspaces WHERE version = ${someVersion}
        if (inType === 'expression') {
            pattern = new RegExp(`\`{0,1}${columnName}\`{0,1}\\s*${operator}\\s*(['"]+.+['"]+)?([$\{]+[\\S]+}+)?`);
        }
        else if (inType === 'list') {
            // e.g. `INSERT INTO workspaces (id) VALUES (1) => 1 is the value
            pattern = new RegExp(value);
        }
        // catch NULL or null
        if (tsType === 'null') {
            pattern = new RegExp(value, 'i');
        }
        // match against raw text because it contains the string with the variable name,
        // which means it will work with embedded expressions
        const match = pattern.exec(context.rawText);
        if (!match) {
            return;
        }
        const start = match.index;
        if (!start) {
            return;
        }
        throw new errors_1.SemanticErrorBadColumnValue({
            expectedType: schemaColumn.tsType,
            receivedType: tsType,
            length: match[0].length,
            start: start + 1,
            end: start + String(value).length // Cast null to string because null has no length property
        });
    }
    // Get the first matching position of whole word `target` in `text`.
    getFirstPosition(text, target) {
        const indexes = find_all_indexes_1.default(target, text);
        for (let i = 0; i < indexes.length; i++) {
            const index = indexes[i];
            const wordWithOffset = get_word_at_offset_1.default(index, text);
            if (!wordWithOffset) {
                continue;
            }
            if (wordWithOffset.word !== target) {
                continue;
            }
            const start = index + 1;
            return {
                end: start + target.length,
                start
            };
        }
        return null;
    }
}
exports.default = Analyzer;
//# sourceMappingURL=index.js.map