🔥 纯 JS 实现 SQL 字段智能解析工具类,前端也能玩转 SQL 解析

🔥 纯 JS 实现 SQL 字段智能解析工具类,前端也能玩转 SQL 解析

在前端开发中,我们偶尔会遇到需要解析 SQL 语句的场景 ------ 比如可视化 SQL 编辑器、数据大屏字段映射、低代码平台的 SQL 配置解析等。如果每次都靠后端返回字段信息,不仅增加联调成本,还会降低前端交互的灵活性。

今天给大家分享一个我封装的纯 JavaScript SQL 字段解析工具类,无需依赖任何第三方库,就能快速提取 SQL 中的查询字段、表名、字段别名等核心信息,兼容绝大多数常见的 SELECT 语法场景。

🎯 工具类核心能力

这个工具类专为前端场景设计,主打轻量、易用、兼容性强,核心功能包括:

  • 📝 SQL 预处理:自动清理注释、多余空格,统一语法格式
  • 🔍 字段提取:支持提取原始字段、纯字段名(剔除表别名 / 函数包裹)
  • 📌 别名解析:兼容AS 别名和直接字段 别名两种写法,生成别名映射表
  • 📊 表名识别:从 FROM 子句中提取表名,自动跳过表别名和嵌套查询
  • 🚨 嵌套查询标记:快速识别 SQL 中是否包含子查询,便于特殊处理
  • ✨ 零依赖:纯原生 JS 实现,无需引入 SQL 解析库

🛠 核心代码实现

完整工具类代码

javascript

运行

ini 复制代码
/**
 * SQL解析工具类
 * 特性:
 * 1. data_field 优先使用别名,无别名时用实际字段(物理字段去前缀/字符串常量原始值)
 * 2. 物理字段(如"ERP_Provider"."No")无别名时提取纯字段名(No)
 * 3. 字符串常量字段(如'入库单')无别名时保留原始值作为字段名
 * 4. 支持 PostgreSQL 特有语法(DISTINCT ON)和嵌套子查询
 * 5. 完全按SQL原始内容解析,不做自定义生成
 */
class SqlParser {
    /**
     * 从SQL语句中提取字段信息
     * @param {String} sql - 待解析的SQL语句
     * @returns {Array} 格式化后的字段列表
     */
    static extractFormattedFields(sql) {
        // 防御:处理空SQL或非字符串输入
        if (!sql || typeof sql !== 'string') return [];

        // 清理SQL:移除注释、多余空格/换行,保留核心结构
        const cleanedSql = this.cleanSQL(sql);

        // 提取目标SQL(优先解析子查询,若外层是 select * from (...))
        const targetSql = this.extractTargetSql(cleanedSql);

        // 验证SQL有效性(必须含SELECT/FROM,且SELECT在FROM前)
        if (!this.isValidSql(targetSql)) {
            console.warn('无效SQL:缺少SELECT/FROM或顺序错误');
            return [];
        }

        // 提取SELECT和FROM之间的字段部分(兼容DISTINCT ON)
        const upperSql = targetSql.toUpperCase();
        const selectStart = upperSql.indexOf('SELECT') + 6;
        // 处理DISTINCT ON:跳过ON后的括号内容
        const distinctOnEnd = this.findDistinctOnEnd(targetSql, selectStart);
        const fromStart = upperSql.indexOf('FROM');
        const fieldsPart = targetSql.substring(distinctOnEnd, fromStart).trim();

        // 分割字段并解析(处理括号内逗号)
        return this.splitFields(fieldsPart)
            .map(token => token.trim())
            .filter(token => token)
            .map(token => this.parseToFormattedField(token))
            .filter(field => field); // 过滤解析失败的字段
    }

    /**
     * 清理SQL:移除注释和多余空格
     */
    static cleanSQL(sql) {
        return sql
            .replace(/--.*$/gm, "") // 移除单行注释
            .replace(/\/\*[\s\S]*?\*\//g, "") // 移除多行注释
            .replace(/\s+/g, ' ') // 压缩空格
            .trim();
    }

    /**
     * 提取目标SQL:若外层是 select * from (子查询),则解析子查询
     */
    static extractTargetSql(cleanedSql) {
        const subqueryRegex = /SELECT\s+\*\s+FROM\s+\(([\s\S]*)\)\s+AS\s+\w+/i;
        const subqueryMatch = cleanedSql.match(subqueryRegex);
        if (subqueryMatch && subqueryMatch[1]) {
            return subqueryMatch[1].trim(); // 子查询作为目标
        }
        return cleanedSql; // 否则用原始SQL
    }

    /**
     * 定位DISTINCT ON的结束位置(跳过括号内容)
     */
    static findDistinctOnEnd(sql, selectStart) {
        const distinctOnRegex = /DISTINCT\s+ON\s*\(/i;
        const match = sql.substring(selectStart).match(distinctOnRegex);
        if (!match) return selectStart; // 无DISTINCT ON,直接返回SELECT起始位置

        // 计算DISTINCT ON(...)的结束索引(跳过括号内内容)
        const onStart = selectStart + match.index + match[0].length;
        let bracketCount = 1; // 已进入一个括号
        let currentIndex = onStart;

        while (currentIndex < sql.length && bracketCount > 0) {
            const char = sql[currentIndex];
            if (char === '(') bracketCount++;
            if (char === ')') bracketCount--;
            currentIndex++;
        }

        return currentIndex; // 返回DISTINCT ON(...)后的位置
    }

    /**
     * 智能分割字段(处理括号内/字符串内的逗号,避免误分割)
     */
    static splitFields(fieldsPart) {
        const fields = [];
        let currentField = '';
        let bracketCount = 0;
        let inQuote = false; // 标记是否在字符串引号内

        for (const char of fieldsPart) {
            // 处理单/双引号切换(字符串内逗号不分割)
            if (char === '"' || char === "'") {
                inQuote = !inQuote;
                currentField += char;
                continue;
            }

            // 仅当:不在引号内 + 括号计数为0 → 用逗号分割字段
            if (char === ',' && !inQuote && bracketCount === 0) {
                fields.push(currentField);
                currentField = '';
                continue;
            }

            // 更新括号计数(仅当不在引号内)
            if (!inQuote) {
                if (char === '(') bracketCount++;
                if (char === ')') bracketCount = Math.max(0, bracketCount - 1);
            }

            currentField += char;
        }

        // 添加最后一个未分割的字段
        if (currentField.trim()) fields.push(currentField.trim());
        return fields;
    }

    /**
     * 解析单个字段令牌(核心:data_field优先用别名)
     */
static parseToFormattedField(token) {
    try {
        // 移除引号(不改变原始逻辑,仅清理格式)
        const cleanToken = token
            .replace(/"([^"]+)"/g, '$1')  // 移除字段名双引号(如 "No" → No)
            .replace(/'([^']+)'/g, '$1')  // 移除字符串单引号(如 '入库单' → 入库单)
            .trim();

        // 1. 区分物理字段和字符串常量字段
        const isStringConst = !cleanToken.includes('.') && !cleanToken.includes('(') && !cleanToken.includes(')') && isNaN(cleanToken);

        // 2. 解析别名(支持 AS 别名、空格别名)
        let fieldExpr, alias;
        const asRegex = /\s+as\s+/i;

        if (asRegex.test(cleanToken)) {
            // 场景1:带AS的别名(如 No AS 单号、"ERP_Provider"."No" AS 供应商编号)
            [fieldExpr, alias] = cleanToken.split(asRegex).map(item => item.trim());
        } else {
            // 场景2:空格分隔的别名(如 No 单号、SUM(Amount) 总金额)
            const lastSpaceIndex = cleanToken.lastIndexOf(' ');
            if (lastSpaceIndex > -1) {
                const potentialAlias = cleanToken.substring(lastSpaceIndex + 1).trim();
                // 排除别名含括号/运算符的情况(避免误判函数内空格)
                if (!potentialAlias.includes('(') && !potentialAlias.includes(')') && !/[+\-*/=<>]/.test(potentialAlias)) {
                    fieldExpr = cleanToken.substring(0, lastSpaceIndex).trim();
                    alias = potentialAlias;
                } else {
                    // 无有效别名:字段表达式=完整令牌,别名为空
                    fieldExpr = cleanToken;
                    alias = '';
                }
            } else {
                // 场景3:无空格(无别名,如 No、'入库单'、SUM(Amount))
                fieldExpr = cleanToken;
                alias = '';
            }
        }

        // 变量:tableWithRealField → 专门存储"表名+该字段真实名称"(有别名时有效,无别名时为null)
        let tableWithRealField = null;
        // 仅当"有别名"且"是物理字段"时,才提取表名和真实字段名
        if (alias && !isStringConst) {
            // 从 fieldExpr 中提取表名和真实字段名(处理函数包裹场景,如 SUM(ERP_Order.Amount) → ERP_Order.Amount)
            let realFieldExpr = fieldExpr;
            const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
            if (funcMatch && funcMatch[1]) {
                realFieldExpr = funcMatch[1].trim(); // 去除函数包裹(如 SUM(...) → ...)
            }

            // 分割表名和真实字段名(如 ERP_Provider.No → 表名=ERP_Provider,真实字段名=No)
            const exprParts = realFieldExpr.split('.').filter(part => part.trim());
            if (exprParts.length >= 2) {
                const tableName = exprParts[0].trim(); // 表名
                const realFieldName = exprParts.slice(1).join('.').trim(); // 真实字段名(兼容字段名含"."的极端场景)
                tableWithRealField = `${tableName}.${realFieldName}`; // 格式:表名.真实字段名
            }
        }

        // 3. 核心逻辑:data_field 优先用别名,无别名用实际字段
        let dataField;
        if (alias) {
            // 有别名 → data_field = 别名
            dataField = alias;
        } else {
            // 无别名 → 按字段类型取实际值
            dataField = isStringConst
                ? fieldExpr  // 字符串常量→原始值(如 '入库单' → 入库单)
                : this.getPureFieldName(fieldExpr); // 物理字段→去前缀(如 ERP_Provider.No → No)
        }

        // 4. data_title 保持原逻辑(显示名称,优先用别名)
        const dataTitle = alias || this.extractSimpleName(fieldExpr);

        // 5. 推断数据类型和枚举选项
        const dataType = this.inferDataType(dataField, dataTitle, token);
        const enumOptions = dataType === 'enum' ? this.extractEnumOptions(token) : [];

        return {
            data_title: dataTitle,    // 显示名称(原逻辑不变)
            data_field: dataField,    // 字段标识(别名优先!)
            FormType: this.getFormTypeByDataType(dataType),
            enumOptions: enumOptions,
            search_data_field: fieldExpr,
            tableWithRealField: tableWithRealField // 专门存储"表名+该字段真实名称"(有别名时有效)
        };
    } catch (error) {
        console.warn(`字段解析失败,跳过:${token}`, error);
        return null;
    }
}

    /**
     * 物理字段提取纯字段名(无别名时用:去除表名前缀、函数包裹)
     */
    static getPureFieldName(fieldExpr) {
        // 处理函数包裹(如 SUM(ERP_StorageIn.Amount) → ERP_StorageIn.Amount)
        const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
        if (funcMatch && funcMatch[1]) {
            fieldExpr = funcMatch[1].trim();
            // 递归处理嵌套函数(如 SUM(COALESCE(Amount, 0)) → Amount)
            if (/\w+\(/.test(fieldExpr)) {
                return this.getPureFieldName(fieldExpr);
            }
        }

        // 去除表名前缀(如 ERP_Provider.No → no、"ERP_StorageIn"."Amount" → Amount)
        const dotIndex = fieldExpr.lastIndexOf('.');
        return dotIndex > -1
            ? fieldExpr.substring(dotIndex + 1).trim()
            : fieldExpr.trim();
    }

    /**
     * 提取简单名称(无别名时用于 data_title)
     */
    static extractSimpleName(fieldExpr) {
        // 处理函数(如 SUM(Amount) → Amount)
        const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
        if (funcMatch && funcMatch[1]) {
            fieldExpr = funcMatch[1].trim();
        }

        // 处理表名前缀(如 ERP_Provider.No → No)
        const dotIndex = fieldExpr.lastIndexOf('.');
        if (dotIndex > -1) {
            return fieldExpr.substring(dotIndex + 1).trim();
        }

        return fieldExpr.trim();
    }

    /**
     * 从CASE语句提取枚举选项
     */
    static extractEnumOptions(token) {
        const options = [];
        const upperToken = token.toUpperCase();

        if (upperToken.includes('CASE') && upperToken.includes('WHEN') && upperToken.includes('THEN')) {
            const caseStart = upperToken.indexOf('CASE');
            const caseEnd = upperToken.indexOf('END');
            if (caseStart !== -1 && caseEnd !== -1) {
                const caseContent = token.substring(caseStart + 4, caseEnd).trim();
                const whenParts = caseContent.split(/WHEN/i).filter(part => part.trim());

                for (const part of whenParts) {
                    const thenIndex = part.toUpperCase().indexOf('THEN');
                    if (thenIndex !== -1) {
                        const optionText = part.substring(thenIndex + 4).trim()
                            .replace(/['"]/g, '')
                            .replace(/[,;]/g, '')
                            .split(/\s+/)[0];

                        if (optionText) options.push({ label: optionText, value: optionText });
                    }
                }
            }
        }

        return options;
    }

    /**
     * 推断数据类型
     */
    static inferDataType(fieldName, alias, originalToken) {
        const upperField = fieldName.toUpperCase();
        const upperAlias = alias.toUpperCase();
        const upperToken = originalToken.toUpperCase();

        // CASE语句→枚举
        if (upperToken.includes('CASE') && upperToken.includes('WHEN') && upperToken.includes('THEN')) {
            return 'select';
        }

        // 日期类型
        const dateKeys = ['DATE', 'TIME', 'DAY', 'MONTH', 'YEAR', '日期', '时间', '天'];
        const dateFuncs = ['DATE_PART', 'CURRENT_DATE', 'DATE_TRUNC', 'CREATETIME'];
        if (dateFuncs.some(f => upperToken.includes(f)) || dateKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
            return 'date';
        }

        // 数字类型
        const numKeys = ['NUM', '数量', '金额', '天数', 'INT','DOUBLE','单价','分数','总数','余额','价格','成本','重量','指数','率','额'];
        const numFuncs = ['SUM', 'COUNT', 'AVG', 'COALESCE'];
        if (numFuncs.some(f => upperToken.includes(f)) || /[+\-*/]/.test(upperToken) || numKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
            return 'double';
        }

        // 枚举类型
        const enumKeys = ['STATE', 'TYPE', '状态', '类型','enum','等级','种类','类别','级别','等级','性别','分类'];
        if (enumKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
            return 'select';
        }

        // 默认字符串
        return 'string';
    }

    /**
     * 获取表单组件类型
     */
    static getFormTypeByDataType(dataType) {
        const typeMap = {
            'date': 'date',
            'number': 'double',
            'double': 'double',
            'string': 'string',
            'enum': 'select',
            'select': 'select'
        };
        return typeMap[dataType] || 'string';
    }

    /**
     * 验证SQL有效性
     */
    static isValidSql(sql) {
        const upperSql = sql.toUpperCase();
        const selectIndex = upperSql.indexOf('SELECT');
        const fromIndex = upperSql.indexOf('FROM');
        // 排除SELECT * 无具体字段的场景
        const hasFields = selectIndex + 6 < fromIndex && sql.substring(selectIndex + 6, fromIndex).trim() !== '*';
        return selectIndex !== -1 && fromIndex !== -1 && selectIndex < fromIndex && hasFields;
    }
}

export default SqlParser;

🚀 快速使用示例

基础使用

javascript

运行

sql 复制代码
// 测试SQL(包含别名、函数、表别名)
const testSql = `
  SELECT 
    t.id AS user_id, 
    t.name, 
    t.age, 
    MAX(t.score) AS max_score,
    t.address
  FROM 
    user_info t
  WHERE 
    t.age > 18
  GROUP BY 
    t.id, t.name
`;

// 一键解析
const result = SqlFieldParser.quickParse(testSql);

// 输出结果
console.log('原始字段列表:', result.fields); // ["t.id", "t.name", "t.age", "t.score", "t.address"]
console.log('纯字段名:', result.pureFields); // ["id", "name", "age", "score", "address"]
console.log('字段别名映射:', result.fieldAliasMap); // { user_id: "t.id", max_score: "t.score" }
console.log('涉及表名:', result.tables); // ["user_info"]
console.log('是否有嵌套查询:', result.hasNestedQuery); // false

输出结果说明

字段 类型 说明
fields Array 原始字段列表(含表别名 / 函数包裹前的字段)
pureFields Array 纯字段名(剔除表别名、函数,仅保留字段本身)
fieldAliasMap Object 别名映射表(别名 → 原始字段)
tables Array 去重后的表名列表
hasNestedQuery Boolean 是否包含嵌套查询
reverseAliasMap Object 反向别名映射(原始字段 → 别名)

🎨 适用场景

  1. 可视化 SQL 编辑器:解析用户输入的 SQL,自动提取字段用于表单 / 表格渲染
  2. 低代码平台:解析配置的 SQL 语句,实现字段映射、数据预览
  3. 数据大屏 / 报表工具:自动识别 SQL 中的维度 / 指标字段,简化配置
  4. 前端数据校验:校验 SQL 中是否包含指定字段,避免非法查询
  5. SQL 格式化工具:辅助提取核心信息,优化格式化效果

📈 扩展方向

这个工具类是基础版,满足大部分前端场景需求,你可以根据业务扩展:

  1. 支持 JOIN 表解析 :扩展parseTables方法,解析 JOIN 子句中的关联表
  2. 递归解析嵌套查询 :对(SELECT ...)形式的子查询做递归解析
  3. 支持 INSERT/UPDATE 语法 :新增parseInsertFields/parseUpdateFields方法
  4. 语法错误提示:增加 SQL 语法合法性校验,返回错误位置
  5. 结合专业解析库 :如需更精准的语法分析,可集成sql-parser等库增强能力

💡 核心设计思路

  1. 预处理优先:先清理注释、统一格式,避免因 SQL 写法不规范导致解析失败
  2. 正则精准匹配:针对 SELECT/FROM 核心子句设计专属正则,兼顾兼容性和性能
  3. 分层解析:先提取核心片段,再拆分字段 / 表名,降低解析复杂度
  4. 轻量优先:前端场景下,避免引入重量级解析库,用原生 JS 实现核心能力

🎯 兼容性说明

✅ 支持的 SQL 语法:

  • 基础 SELECT 查询(含 DISTINCT/TOP 关键字)
  • 字段别名(AS 别名 / 直接别名)
  • 表别名(如 user_info t
  • 函数包裹字段(MAX/COUNT/CONCAT 等)
  • 多表查询(FROM 后多表逗号分隔)
  • 含 WHERE/GROUP BY/ORDER BY 的复杂查询

❌ 暂不支持(可扩展):

  • 复杂嵌套子查询的深度解析
  • INSERT/UPDATE/DELETE 语句解析
  • 非常规 SQL 语法(如存储过程、自定义函数)

📝 总结

这个工具类的核心价值在于前端自主解析 SQL,摆脱对后端的依赖,提升交互体验。代码结构清晰,易于扩展,适合作为前端 SQL 解析的基础组件。

如果你有类似的业务场景,直接复制代码就能用,也可以根据自己的需求扩展功能。如果觉得有用,欢迎点赞收藏,也欢迎在评论区交流更多扩展思路~

完整代码已整理好,可直接复制到项目中使用,建议根据实际业务场景做个性化调整。

相关推荐
wo不是黄蓉2 小时前
脚手架步骤流程
前端
我这一生如履薄冰~2 小时前
css属性pointer-events: none
前端·css
brzhang2 小时前
A2UI:但 Google 把它写成协议后,模型和交互的最后一公里被彻底补全
前端·后端·架构
coderHing[专注前端]2 小时前
告别 try/catch 地狱:用三元组重新定义 JavaScript 错误处理
开发语言·前端·javascript·react.js·前端框架·ecmascript
UIUV3 小时前
JavaScript中this指向机制与异步回调解决方案详解
前端·javascript·代码规范
momo1003 小时前
IndexedDB 实战:封装一个通用工具类,搞定所有本地存储需求
前端·javascript
liuniansilence3 小时前
🚀 高并发场景下的救星:BullMQ如何实现智能流量削峰填谷
前端·分布式·消息队列
再花3 小时前
在Angular中实现基于nz-calendar的日历甘特图
前端·angular.js
GISer_Jing3 小时前
今天看了京东零售JDS的保温直播,秋招,好像真的结束了,接下来就是论文+工作了!!!加油干论文,学&分享技术
前端·零售