拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SQLFluff 架构解析:从模板渲染到自动修复的四阶段流水线

SQLFluff 架构解析从模板渲染到自动修复的四阶段流水线【免费下载链接】sqlfluffA modular SQL linter and auto-formatter with support for multiple dialects and templated code.项目地址: https://gitcode.com/GitHub_Trending/sq/sqlfluffSQLFluff 是一个模块化的 SQL 代码检查Linter与自动格式化Auto-formatter工具支持多方言与模板化代码。无论是执行sqlfluff lint、sqlfluff fix还是sqlfluff parse其内部都遵循同一条统一流水线模板化Templater→ 词法分析Lexer→ 语法解析Parser→ 规则检查Linter。本文基于仓库中的 架构文档结合 templaters、parser、rules 等核心源码逐阶段剖析这条流水线的实现原理帮助读者理解 SQLFluff 如何处理模板化 SQL、如何构建语法树、如何在解析失败时兜底以及规则修复是如何安全落回原文件的。统一流水线三条命令一条主线从架构层面看sqlfluff lint、sqlfluff fix与sqlfluff parse的差异只体现在流水线的末端——是否把规则检查结果展示为报告、是否将修复写回文件、是否只输出解析树。而流水线的前半段渲染、词法、解析是完全共享的。在源码中这一流程由 linter/linter.py 中的方法链承载render_string()调用 templater 渲染模板→parse_string()调用 lexer 与 parser→lint_string()运行规则例如lint_string会先通过parse_string拿到完整解析树再交给规则引擎。理解这一点后下面按阶段深入。Stage 1Templater —— 先翻译模板再谈检查模板化是 SQLFluff 区别于普通 SQL 检查器的核心能力。现代数据工程中SQL 很少是纯文本dbt 模型、Jinja 宏、SQLAlchemy 占位参数、Python format 字符串无处不在。这些模板代码不是合法 SQL无法直接被解析器处理。为什么必须渲染两遍要 lint 模板化 SQLSQLFluff 必须先经过 Templater 阶段将原始raw / pre-templated代码转换成可解析的合法 SQL。关键设计是templater 同时返回原始 SQL 与模板化后的 SQL见 base.py 中TemplatedFile同时持有source_str与templated_str两个字段。这样做的目的有两个发生在模板段内部如 Jinja 表达式求值产生的 SQL的规则违规可以被忽略因为那部分内容并非用户手写的 SQL其余违规可以被映射回原始文件的真实行号让用户反馈落在自己写的代码上而不是渲染后的产物上。这背后的位置映射机制由TemplatedFileSlice模板化文件切片与RawFileSlice原始文件切片两个 NamedTuple 协作完成二者通过source_slice与templated_slice相互对应。TemplatedFile还提供了templated_slice_to_source_slice()、get_line_pos_of_char_pos()、is_source_slice_literal()等方法用于在模板切片与源码位置之间双向换算以及判断某段源码是否为字面量非模板生成。支持的模板引擎架构文档列出的模板引擎在源码 templaters 目录中一一对应Jinjajinja.py中的JinjaTemplatername jinja基于 Jinja2 实现SQL 占位符placeholder.py可处理如 SQLAlchemy 参数风格的占位符Python format 字符串python.py中的PythonTemplaterdbtbuiltins/dbt.py经由独立的sqlfluff-templater-dbt插件提供见 plugins/sqlfluff-templater-dbt。值得注意的是dbt 底层虽然也使用 Jinja但 SQLFluff 中的 dbt templater并不复用 JinjaTemplater而是采用一套独立机制直接与 dbt 的 Python 包对接读取其 manifest 等信息因此在并发模型上也做了特殊处理RawTemplater类上的templates_in_worker属性默认为True而 dbt templater 会将其设为False因为 dbt 需要主进程内的 manifest 状态见 base.py。大文件保护源码中还内置了大文件防护large_file_check装饰器会读取配置中的large_file_skip_char_limit当文件长度超过阈值时抛出SQLFluffSkipFile跳过该文件避免解析器被超大文件锁死用户可以调大该值或设为 0 禁用源码注释同时提示该配置未来将由large_file_skip_byte_limit取代。关于各模板引擎的具体配置方式请参考 Templating Configuration 与 jinja 配置、placeholder 配置、python 配置、dbt 配置。Stage 2Lexer —— 把文本切成一串有类型的 RawSegment模板渲染完成后纯 SQL 则直接跳过 Stage 1进入词法分析阶段。Lexer 的职责是将 SQL 文本切分为空白与代码两类片段并对片段尽可能赋予高层语义但此时产物仍然是一个扁平的、有序的、带类型的 segment 序列——所有 segment 都是RawSegment的子类。从 segments/raw.py 可以看到RawSegment是没有子段的段This is a segment without any subsegments.type raw且_is_code True。而 segments/meta.py 中还定义了EndOfFile等元段MetaSegment用于标记文件结束等特殊位置。词法实现位于 parser/lexer.py。其中值得注意的设计是BlockTracker它借助 UUID 栈来跟踪模板块如if/for的进入与退出即使在循环导致同一模板块被多次渲染时也能为同一源码位置的块复用相同的 UUID方便后续规则引擎对模板块做统一处理。词法错误则以SQLLexError形式抛出定义于 core/errors.py。Stage 3Parser —— 最复杂的一环把扁平序列变成嵌套树架构文档明确指出Parser 是 SQLFluff 中最复杂的组件也是其余所有环节依赖的重体力劳动者。它的目标是把 Stage 2 的扁平 segment 序列按指定方言的 grammar 规则组织成一棵嵌套的语法树。树形结构FileSegment 与 StatementSegment在 SQLFluff 中segment 形成树状结构顶层是FileSegment包含零个或多个StatementSegment以此类推。解析之前这些 segment 是raw的——除了字面值外没有任何分类解析之后它们被按类型命名与组织。源码印证根段类是 segments/file.py 中的BaseFileSegment它是整个文件或脚本的表示也是方言默认的根 segmentroot_segment通常被直接实例化本身没有match_grammar。Parser类parser/parser.py通过RootSegment.root_parse(...)启动解析并以check_still_complete()做基本校验确保没有 segment 在解析中被意外丢弃。Segment 的 .match() 与 Grammar 的组合每个segment都必须实现match_grammar当对 segment 调用.match()时就是用这个 grammar 来决定是否匹配。而grammar则按预定义方式组合其他 segment 或 grammar——例如OneOfgrammar 在其任一子元素匹配时即匹配。Segment 基类见 segments/base.py其SegmentMetaclass在类定义时就预计算了类型集合_class_types与缓存键避免运行时重复计算Grammar 基类见 grammar/base.pyBaseGrammar是所有组合匹配器的基类其构造函数接收任意数量的元素支持直接传 Matchable 或字符串——字符串会被_resolve_ref快捷解析为Ref.keyword组合类 grammar 分布在 grammar 目录下OneOfanyof.py、Sequencesequence.py、Delimiteddelimited.py、Conditionalconditional.py、NonCodenoncode.py、LookBehindlookbehind.py等。SQLFluff 对文件采用单遍递归匹配以FileSegment为例它先把查询切分为若干语句再由这些语句 segment 的.match()方法解析其内部结构。递归最终到达没有子元素的 raw segment单个 token自然终止。MatchResult解析的施工图.match()方法的返回结果是MatchResultparser/match_result.py它包含把扁平 raw segment 序列改造成嵌套树的指令matched_slice命中范围、matched_class要创建的 segment 类型、insert_segments需插入的元段、以及递归的child_matches。直到匹配过程结束才调用.apply()最终真正创建嵌套结构。这种先描述、后施工的设计把匹配逻辑与树构建逻辑解耦。解析失败兜底UnparsableSegment 与 ParseMode如果某个 segment 没有匹配到任何 grammar其内容会被包进UnparsableSegmentsegments/base.pytype unparsable稍后作为解析错误被捕获上报。这一行为通常由 grammar 上的ParseMode控制parser/types.pySTRICT默认只有当全部内容完整匹配时才返回匹配不匹配就完全不返回也不制造 unparsable 段GREEDY只要终止符前至少有一个代码元素就总是返回匹配未匹配的内容被贪婪地捕获为 unparsable——这对应旧的GreedyUntil语义GREEDY_ONCE_STARTED混合模式什么都没匹配到时表现如 STRICT一旦开始匹配就表现如 GREEDY对应StartsWith语义。架构文档举例括号bracketed区块通常配置为贪婪模式把括号内任何意外内容捕获为 unparsable而不是因为内容比预期多而整体匹配失败那是 STRICT 的默认行为。这样用户能得到括号内部哪里解析失败的精确反馈而非整条语句失败。此外BaseGrammar还支持terminators与reset_terminators参数terminator 可让 grammar 提前终止reset_terminators在括号表达式内部暂时清除外层继承的终止符。方言继承体系与 Ref grammarGrammar 被收纳在**方言Dialect**中根方言是ansi。需要澄清的是ansi 方言被用来承载所有方言的公共逻辑因此并不严格遵循 ANSI 规范本身。其他方言从 ansi 继承源码中即dialect_ansi.py被各dialect_*.py继承按需替换replace或修补patch自己的 segment。Refgrammar 存在的关键理由之一就是运行时的名称解析Ref不直接持有被引用对象而是在匹配时才通过parse_context.dialect调用dialect.ref(name)取回实际元素见 grammar/base.py 中Ref._get_elem。因此一个被 patch 过的 grammar即使部分元素已被子方言覆盖仍能继续依赖该方言中未重新声明的底层元素。方言对象本身dialects/base.py通过copy_as()实现继承、add()/replace()增减元素、expand()把可调用引用展开为具体对象返回副本以避免污染原始方言并通过get_root_segment()提供根 segment。两条设计原则架构文档强调了开发 parser 时的两条原则源码中均有体现最大匹配所有 grammar 和 segment 都会尽量多匹配并在可能时返回部分匹配结果由调用方决策由调用方的 grammar/segment 根据自身匹配上下文决定这次需要部分匹配还是完整匹配。这保证了 grammar 的通用性与组合性——同一个 grammar 在不同上下文中可以被灵活复用。Stage 4Linter —— 遍历树、发现问题、生成修复拿到完整的解析树后进入规则检查阶段。规则如何发现问题规则类rule通过遍历解析树来检查违规寻找目标 segment 与可疑模式。规则基类是 rules/base.py 中的BaseRule其crawl()方法负责按规则声明的遍历方式如遍历所有子段、按类型匹配等递归扫描树并针对每个上下文调用规则的_eval(context)方法做具体判定。一旦发现违规规则返回一个LintResult同样定义于 rules/base.py其中anchor字段指向引发违规的 segment作为向用户汇报问题位置的依据。所有内置规则分布在 rules 目录下按主题分组aliasing别名、ambiguous歧义、capitalisation大小写、convention约定、layout布局、structure结构、jinja、postgres、oracle、tsql等。规则的行为大多可通过配置文件开关或调整见 rule_configuration.rst 与 docsv/configuration/rules.md。可修复规则LintFix 的四种编辑类型部分规则不仅能发现问题还能自动修复。这类规则会返回一组 fix描述要对树做的修改包括编辑edit、插入insert、删除delete。修复类LintFix定义在 rules/fix.py其edit_type支持四种操作edit_type语义create_before在 anchor 段之前创建内容create_after在 anchor 段之后创建内容replace替换 anchor 段delete删除 anchor 段LintFix还带source字段——用于replace/create时标识提供代码的源 segmentlinter 借此防止从模板区域复制内容到字面量区域从而避免自动修复破坏模板结构。BaseRule中还内置了discard_unsafe_fixes()之类的安全机制配合RawFileSlice.block_idx判断修复是否跨多个模板块进一步保证跨模板块的修复会被拒绝。修复如何落回原文件fix 应用后更新后的树会写回原始文件。这正是 Templater 阶段双份输出设计价值的最终体现所有 fix 最终都要通过位置映射换算回原始raw文件中的source_slice可参考 segments/base.py 中的SourceFix它同时保存source_slice与templated_slice再对原文件做补丁应用。因此用户运行sqlfluff fix后看到的是自己原始模板代码中的修改而不是渲染后 SQL 的修改。总结一条流水线如何支撑 lint、fix 与 parse回顾整条流水线Templater 把模板 SQL 渲染为可解析 SQL 并保留双向位置映射 → Lexer 切出带类型的扁平 RawSegment 序列 → Parser 按方言 grammar 递归组装成嵌套语法树失败处由 UnparsableSegment 兜底→ Linter 遍历树发现违规、必要时产出 LintFix 并映射回原文件写回。这条流水线的模块化设计templater / dialect / grammar / rule 均可扩展正是 SQLFluff 能够同时支持多方言 模板化代码的根基。若想继续深入可以从以下仓库路径入手模板引擎实现src/sqlfluff/core/templaters含 jinja、placeholder、python、dbt 内置支持解析器核心src/sqlfluff/core/parsergrammar 组合、MatchResult、ParseMode、Lexer方言体系src/sqlfluff/core/dialectsansi 根方言与各子方言规则引擎src/sqlfluff/core/rulesBaseRule、LintResult、LintFix官方文档RST 版见 docs/source/guides/contributing/architecture.rst、docs/source/guides/contributing/dialect.rst、docs/source/guides/contributing/plugins.rst、docs/source/guides/contributing/rules.rstMarkdown 版见 docsv/development解析与词法测试用例test/core/parser、方言 fixture 见 test/fixtures/dialects【免费下载链接】sqlfluffA modular SQL linter and auto-formatter with support for multiple dialects and templated code.项目地址: https://gitcode.com/GitHub_Trending/sq/sqlfluff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门