SQLFluff 方言开发实战:从解析原理到贡献一个方言语法修复
【免费下载链接】sqlfluffA modular SQL linter and auto-formatter with support for multiple dialects and templated code.项目地址: https://gitcode.com/GitHub_Trending/sq/sqlfluff
SQLFluff 是一个模块化的 SQL 解析器、Linter 与自动格式化工具,其全部方言语法都由可插拔的 Python 语法文件驱动。本文以仓库中的 docsv/development/dialect.md 为核心,系统讲解 SQLFluff 的 Segment/Grammar 机制、Lexer 与关键字体系,并完整演示从定位解析失败、对照数据库官方文法到修复语法、添加测试用例、最终提交 PR 的完整贡献流程。读完本文,你将掌握独立为任意 SQL 方言修复或新增语法、并为其编写可回归验证的测试夹具的完整能力。
为什么用户是方言贡献的最佳人选
SQLFluff 的维护者都是利用业余时间贡献代码的志愿者,因此“自己修复自己的问题”往往是解除阻塞、最快用上 SQLFluff 的最优路径。同时,方言使用者通常比核心维护者更了解自己所使用的数据库语法,并且拥有真实的数据库实例来验证某条 SQL 在该方言中是否可解析——这正是方言语法修复最需要的验证条件。
官方文档也一再强调:SQLFluff 拥有稳健的 CI 流水线,贡献者的改动在进入常规维护者人工 review 之前,就能通过自动化测试获得正确性信心。
SQLFluff 如何读取(解析)SQL
SQLFluff 的 Lexer(词法分析器)与 Parser(语法分析器)以高度模块化的方式构建,设计目标就是让不具备核心编程经验、不了解 SQLFluff 内部机制的贡献者也能阅读、理解与扩展。完整的架构细节见 docsv/development/architecture.md,此处只提炼出上手贡献所需的必要知识。
一切从 Segment 开始
SQLFluff 在方言文件中定义它将要使用的语法。以所有方言的基座 src/sqlfluff/dialects/dialect_ansi.py 为例,其中SelectClauseSegment(源码 dialect_ansi.py 第 1889-1904 行)的声明如下:
class SelectClauseSegment(BaseSegment): """A group of elements in a select target statement.""" type = "select_clause" match_grammar: Matchable = Sequence( "SELECT", Ref("SelectClauseModifierSegment", optional=True), Indent, Delimited( Ref("SelectClauseElementSegment"), allow_trailing=True, ), Dedent, terminators=[Ref("SelectClauseTerminatorGrammar")], parse_mode=ParseMode.GREEDY_ONCE_STARTED, )这段声明传达了三层含义:
SelectClauseSegment以SELECT开头,包含一个由SelectClauseElementSegment组成的逗号分隔列表,并在遇到FROM、WHERE、ORDER BY等子句(即terminators)时结束。match_grammar是用于匹配并解析语句的规则本体。terminators告诉解析器该 Segment 在哪里结束:解析过程是相当耗费算力的,解析器会尝试各种 Segment 组合来匹配 SQL,因此预先知道 Segment 的终止位置(这里是下一个子句的起点,如FROM或WHERE)能显著提升匹配效率。parse_mode=ParseMode.GREEDY_ONCE_STARTED指示解析器:一旦匹配到 Segment 开头,就贪婪地吸收直到终止符为止的所有内容——这会让任何意外内容以unparsable区块的形式浮出水面,而不是让整个匹配静默失败。
再看另一个短小而明确的例子,JoinOnConditionSegment(dialect_ansi.py 第 1976-1985 行)。它没有终止符、没有贪婪模式,因为其结构短且无歧义:
class JoinOnConditionSegment(BaseSegment): """The `ON` condition within a `JOIN` clause.""" type = "join_on_condition" match_grammar: Matchable = Sequence( "ON", Conditional(ImplicitIndent, indented_on_contents=True), OptionallyBracketed(Ref("ExpressionSegment")), Conditional(Dedent, indented_on_contents=True), )注意 Segment 之间可以互相引用(Ref(...)),这是把复杂的 SQL 表达式拆解为组件、分别管理与处理的关键手段。
Segment 语法选项(Grammar Options)
创建 SQL 语法时可选用的基本 Grammar 构件如下表所示:
| Grammar | 用途 | 示例 |
|---|---|---|
"KEYWORD" | 表示一个原生 SQL 关键字 | "SELECT" |
Sequence() | 表示关键字或 Segment 的已知序列 | Sequence("SELECT", Ref("SelectClauseElementSegment"), "FROM"...) |
AnyNumberOf() | 从一组可重复的候选项中选取 | "SELECT", AnyNumberOf(Ref("WildcardExpressionSegment"), Ref("ColumnReferenceSegment")...)... |
OneOf() | 比AnyNumberOf更受限:只从候选中选一个 | OneOf("INNER","OUTER","FULL"), "JOIN" |
Delimited() | 用于列表(默认是逗号分隔) | "SELECT", Delimited("SelectClauseElementSegment"), "FROM"... |
Bracketed() | 用于括号包裹的结构,如函数参数 | Ref("FunctionNameSegment"), Bracketed(Ref("FunctionContentsGrammar") |
其中一些构件还接受额外参数,最常用的是optional=True,用于进一步定义 SQL 语句的构成。DeleteStatementSegment(dialect_ansi.py 第 3790-3803 行)就是一个典型例子:
match_grammar: Matchable = Sequence( "DELETE", Ref("FromClauseSegment"), Ref("WhereClauseSegment", optional=True), )这里的WHERE子句是可选的——尽管无数人因为不带WHERE的DELETE而摇头,但 SQL 语法确实允许这样写。借助这些 Grammar 构件,即可组合出复杂的 SQL 语法定义。
Segment 与 Grammar 的辩证关系
- Segment是一段定义
type(可用于后续在规则或解析树中引用)的语法单元,可以通过类(class)或工厂函数(如TypedParser、SegmentGenerator等)创建。 - Grammar是可以被 Segment 复用的语法片段,通常用于避免在多个位置重复同样的代码。可以把 Grammar 理解为一段语法的“别名”,避免反复输入相同内容。
Grammar 的另一个价值在于:允许其他方言在不整体重定义 Segment 的前提下,只覆盖其中某一个小片段。例如 ANSI 方言定义(dialect_ansi.py 第 466 行):
NotOperatorGrammar=StringParser("NOT", KeywordSegment, type="keyword")而 MySQL 覆盖为(dialect_mysql.py 第 300-303 行):
NotOperatorGrammar=OneOf( StringParser("NOT", KeywordSegment, type="keyword"), StringParser("!", CodeSegment, type="not_operator"), ),这使 MySQL 能在所有原本使用NOT的位置使用!(前提是这些位置引用的是NotOperatorGrammar而非硬编码的NOT关键字)。这种方式让方言定制变得极其轻量,无需为了多支持一个!而复制粘贴并长期维护近乎相同的整段代码。
方言(Dialects)体系
绝大多数 SQL 语法在不同数据库中都是相同的——基础的SELECT.. FROM... WHERE语句是通用的。但随着各厂商按自身需求实现或扩展 SQL,出现了 PostgreSQL、Snowflake、Oracle 等各不相同的方言。为此,SQLFluff 允许创建语法互不相同的方言。
所有方言都位于仓库的 src/sqlfluff/dialects 目录,其中 dialect_ansi.py 是其他所有方言最终继承的基座方言。从当前仓库的实际文件来看,该目录下已实现 ansi、athena、bigquery、clickhouse、databricks、db2、doris、duckdb、exasol、flink、greenplum、hive、impala、mariadb、materialize、mysql、oracle、postgres、redshift、snowflake、soql、sparksql、sqlite、starrocks、teradata、trino、tsql、vertica 等一批方言文件(每个方言还配有一个_keywords.py关键字文件)。
在 SQLFluff 中,一个方言本质上就是一个继承 ANSI 方言全部内容、再按需新增或覆盖解析 Segment 的文件。如果某个方言的SELECT、FROM、WHERE与 ANSI 完全一致,只有ORDER BY语法不同,那么只需要覆盖ORDER BY子句即可,方言文件会非常短小;而对于差异巨大的方言(如 T-SQL),则可能需要覆盖大量内容。
Lexing(词法分析)
在 SQL 被解析之前,首先要被词法分析:拆分为符号与逻辑分组。例如内联注释在 dialect_ansi.py 第 100-105 行 中被定义为:
RegexLexer( "inline_comment", r"(--|#)[^\n]*", CommentSegment, segment_kwargs={"trim_start": ("--", "#")}, ),即--或#之后到换行符之前的所有内容。这样整段注释可以被当作一个词法块处理,无需再为其定义解析规则(这里甚至直接给它指定了解析 Segment 名称CommentSegment)。
对于简单的语法新增,通常不需要触碰词法定义,常见符号已被覆盖;但复杂一些的语法可能需要在词法层新增规则。看到词法错误(lexing errors)时,往往就需要在这里补充。
词法分析按顺序进行:从 SQL 开头读取,取到最长的词法匹配,然后“吞掉”该匹配并归档为一个符号供后续解析使用,再对剩余文本重复该过程。因此SELECT * FROM table WHERE col1 = 12345不会被拆成S、E、L……,而是被拆成SELECT、*、FROM、table等。
一个覆盖词法层的经典案例是 BigQuery 的参数化变量@variable_name。ANSI 词法器不识别@符号;与其新增一个 Grammar 或 Segment 去拼接两个部分(@与variable_name),不如直接让词法器把整个变量一次性解析为一个符号供后续使用。BigQuery 方言的做法见 dialect_bigquery.py 第 72 行起:
bigquery_dialect.insert_lexer_matchers( [ RegexLexer("atsign_literal", r"@[a-zA-Z_][\w]*", CodeSegment), ], before="equals", )其中before="equals"用来指定词法匹配的优先级顺序:例如如果还定义了用于其他独立@用法的at_sign规则,就希望atsign_literal被优先考虑,只有匹配失败时才回退到at_sign。
关键字(Keywords)
大多数方言都有关键字文件,列出该方言的全部关键字。部分方言直接继承 ANSI 关键字列表,再在此基础上增删——虽不如精确维护关键字准确,但通常更快速、更易管理。
关键字被划分为RESERVED(保留)与UNRESERVED(非保留)两类:RESERVED 关键字有额外限制,不能用作标识符。如果要在 Grammar 中使用某个关键字(例如"SELECT"),它必须存在于某一类关键字列表中,否则会出现类似下面的报错(示例来自 redshift 方言中未注册的"NAN"):
RuntimeError: Grammar refers to 'NanKeywordSegment' which was not found in the redshift dialect另外,在编辑主 ANSI 方言及其关键字列表时,需要考虑该语法是否也应同步到其他继承它的方言——通常是的,除非这些方言显式覆盖了该语法。仓库中每个方言的配套_keywords.py文件(例如 src/sqlfluff/dialects/dialect_ansi_keywords.py)正是这些列表的落点。
去哪里找到数据库的权威文法
临时拼凑的语法修复固然聊胜于无,但最健壮的贡献方式,是对照你所用方言的文法“真相来源”(source of truth),把方言映射到 SQLFluff 的 Segment 与 Grammar 上,从而穷尽该方言可能接受的全部语句。
许多计算机语言(SQL 数据库引擎也不例外)都是基于 Flex、Bison 等解析器生成器编写的。数据库引擎源码中的解析器规范是“一条 SQL 语句究竟如何被解析”的终极事实来源——由于文档可能存在缺口,你可能会惊讶于数据库引擎实际能解析的语法。参考文档则用于获取语句文法的高层概览,以及额外的限制与意图说明;对于闭源引擎,通常只能依赖参考文档——但它始终不如引擎内部真正用于代码生成的 Bison 文法准确。
把测试夹具中的查询放到真实数据库引擎里试解析也极为有用。测试查询不要求是合法查询(可以引用不存在的表名等),但必须确认它们是可解析的。不应要求 SQLFluff 解析一个真实数据库引擎会拒绝的语句——过度宽泛的匹配逻辑会在其他地方引发解析问题。
以下是文档给出、且可在仓库外自行核对的文法参考资源摘要(此处仅列出资源主题,不提供外部链接):
- ANSI SQL:ANSI 标准文档本身非免费(含文法的 Part 2 需购买);可参考 modern-sql.com 的标准分区讨论、jakewheat.github.io 的可浏览 BNF 文法、SQL:1999 旧版标准 PDF,以及 Mimer SQL-2016 校验器(用于验证查询能否按 ANSI 标准解析)。
- PostgreSQL:官网最新版 SQL 命令参考文档、PostgreSQL 仓库中的
src/backend/parser/gram.y(Bison 文法)与src/backend/parser/scan.l(Flex 扫描器)、解析阶段说明文档;验证方式是把语句粘贴进psql,若出现ERROR: syntax error说明不可解析——这类查询不应进入主测试夹具;若出现其他错误则说明解析成功(失败原因可能是列不存在等),此时最好让 SQLFluff 也能解析它。pgsql-parser 工具包装了官方源码与 Bison 文法,可用于查看 PG 眼中的精确解析树。 - MySQL:官方 8.0 参考手册的 SQL 语句章节、MySQL 源码中的
sql/sql_yacc.yy(Bison 文法);验证方式是把语句粘贴进mysql,出现ERROR 1064 (42000): You have an error in your SQL syntax即表示解析错误。
实战:贡献一个语法修复的四个完整示例
以下示例假设你已具备 Python 开发环境与 Git 使用基础(环境搭建见仓库根目录的 CONTRIBUTING.md,提交流程遵循通用的 Fork + PR 工作流)。下面以四个真实案例演示从问题到修复的完整路径。
示例 1:为 PostgreSQL 的CREATE FUNCTION增加SETOF返回类型
某 issue 报告 SQLFluff 无法解析如下语句:
CREATE OR REPLACE FUNCTION public.postgres_setof_test() RETURNS SETOF text错误信息为:
Found unparsable section: 'CREATE OR REPLACE FUNCTION crw_public.po...'问题出在postgres方言的 src/sqlfluff/dialects/dialect_postgres.py 中CreateFunctionStatementSegment的返回类型部分。修复前的文法只允许返回一个TABLE或一个普通数据类型:
Sequence( # Optional function return type "RETURNS", OneOf( Sequence( "TABLE", Bracketed( Delimited( OneOf( Ref("DatatypeSegment"), Sequence( Ref("ParameterNameSegment"), Ref("DatatypeSegment") ), ), delimiter=Ref("CommaSegment"), ) ), optional=True, ), Ref("DatatypeSegment"), ), optional=True, ),修复方式极其简单:把SETOF结构作为OneOf的另一个选项加入(该修复在 dialect_postgres.py 第 1484-1486 行 附近得以体现):
Sequence( # Optional function return type "RETURNS", OneOf( Sequence( "TABLE", Bracketed( Delimited( OneOf( Ref("DatatypeSegment"), Sequence( Ref("ParameterNameSegment"), Ref("DatatypeSegment") ), ), delimiter=Ref("CommaSegment"), ) ), optional=True, ), Sequence( "SETOF", Ref("DatatypeSegment"), ), Ref("DatatypeSegment"), ), optional=True, ),修改后上述语句即可正常解析,随后补充测试用例并提交 PR 完成修复。
示例 2:select 1 from group的终止符过宽问题
某 issue 报告select 1 from group无法解析,错误为:
==== parsing violations ==== L: 1 | P: 10 | PRS | Line 1, Position 10: Found unparsable section: 'from' L: 1 | P: 14 | PRS | Line 1, Position 14: Found unparsable section: ' group'报告者还附上了sqlfluff parse产出的解析树片段,其中from_clause下的内容全部被标记为unparsable,提示Expected: 'FromClauseSegment'。排查FromClauseSegment后发现问题出在FromClauseTerminatorGrammar:它把独立的GROUP、ORDER都当作FROM子句的终止符,导致解析器一看到GROUP就断定“FROM子句到此结束”。修复前的定义是:
FromClauseTerminatorGrammar=OneOf( "WHERE", "LIMIT", "GROUP", "ORDER", "HAVING", "QUALIFY", "WINDOW", Ref("SetOperatorSegment"), Ref("WithNoSchemaBindingClauseSegment"), ),修复方式是把"GROUP"替换为Sequence("GROUP", "BY")、"ORDER"替换为Sequence("ORDER", "BY"),让它们只在两个词同时出现时才匹配为终止符。从当前 ANSI 方言源码(dialect_ansi.py 第 542-552 行)可以看到这一修复的最终形态:
FromClauseTerminatorGrammar=OneOf( "WHERE", Ref("LimitClauseSegment"), Sequence("GROUP", "BY"), Sequence("ORDER", "BY"), "HAVING", "QUALIFY", "WINDOW", Ref("SetOperatorSegment"), Ref("WithNoSchemaBindingClauseSegment"), Ref("WithDataClauseSegment"), ... ),这一案例也展示了终止符(terminator)设计的核心思想:终止符必须足够精确,既要在正确位置截断 Segment,又不能吞噬合法内容。
示例 3:从零贡献CREATE CAST/DROP CAST语句
该示例演示如何利用参考文法从零新增一条语句。贡献新语句的第一步是确认它是否属于 ANSI 标准——如果是,应当先在 SQLFluff 的 ANSI 方言中新增一个与厂商无关的 Segment,供其他方言继承。经查,CREATE CAST确实定义在 ANSI 标准中,其 BNF 文法形如:
<user-defined cast definition> ::= CREATE CAST <left paren> <source data type> AS <target data type> <right paren> WITH <cast function> [ AS ASSIGNMENT ]据此在 dialect_ansi.py 中构建对应的CreateCastStatementSegment:
class CreateCastStatementSegment(BaseSegment): """A `CREATE CAST` statement.""" type = "create_cast_statement" match_grammar: Matchable = Sequence( "CREATE", "CAST", Bracketed( Ref("DatatypeSegment"), "AS", Ref("DatatypeSegment"), ), "WITH", Ref.keyword("SPECIFIC", optional=True), OneOf( "ROUTINE", "FUNCTION", "PROCEDURE", Sequence( OneOf("INSTANCE", "STATIC", "CONSTRUCTOR", optional=True), "METHOD", ), ), Ref("FunctionNameSegment"), Ref("FunctionParameterListGrammar", optional=True), Sequence("FOR", Ref("ObjectReferenceSegment"), optional=True), Sequence("AS", "ASSIGNMENT", optional=True), ) # 未展示:将 CreateCastStatementSegment 注册进 StatementSegment推进文法时,要时刻思考 SQL 的其他部分是否包含相似元素。这里复用了数据类型、函数名、函数参数列表等既有 Segment,既简化了新文法,也让这些区域能在其他方言中被集中修改。一个强有力的信号是:当参考文法中的某个符号被多个其他符号/语句复用时,就应当为它建立共享的 Segment 或 Grammar——引入新 Segment 与复用既有 Segment 会为 SQLFluff 解析树增加结构,方便 Lint 规则分析。
写完 ANSI Segment 后,再对照 PostgreSQL 文法的差异进行定制,关键差异包括:只能指定FUNCTION(ROUTINE、PROCEDURE会被拒绝);不支持SPECIFIC关键字;PG 还提供非标准扩展WITHOUT FUNCTION与AS IMPLICIT。查阅 Bison 文法(虽然通常冗长吓人)有一些高效技巧:
- 在符号后加冒号
:进行搜索(例如CreateCastStmt:直达定义处)。 - 从最高层的目标开始(PG 中所有语句以
Stmt结尾)。 - 逐层下钻:例如看到
function_with_argtypes就搜索function_with_argtypes:了解其含义。
Bison 文法还可能揭示文档中不存在的关键字替代拼写(实测确为合法 SQL)——因为 PG 文档中的文法是人类手工维护的,与“实际可解析”之间存在缺口。
一个需要警惕的点:Bison 文法往往高度递归,因为它没有AnyOf、Delimited、Bracketed这类高层构件;而 SQLFluff 对递归的扩展性不佳。某些递归不可避免且合理(如括号化表达式),但大量琐碎递归应当用 SQLFluff 的高层构件重写。例如下面的 Bison 文法定义了一个括号包裹的逗号分隔列表,在 SQLFluff 中应改用Bracketed+Delimited:
func_args: '(' func_args_list ')' { $$ = $2; } | '(' ')' { $$ = NIL; } ; func_args_list: func_arg { $$ = list_make1($1); } | func_args_list ',' func_arg { $$ = lappend($1, $3); } ;示例 4:用参考文法定位并修复 PostgreSQL 数组切片
某 issue 报告 PostgreSQL 中数组切片无法正确解析,简化后的用例是:
SELECT a[2:2+3];显然SELECT a;能正常解析,问题必然出在数组访问上。从 PG 的 Bison 文法SelectStmt:开始逐层下钻:SelectStmt→select_no_parens→simple_select→target_list→target_el可知涉及a_expr(PG 中全文法通用的表达式符号,SQLFluff 对应ExpressionSegment及更具体的Expression_A_Grammar);继续target_el→a_expr→c_expr→columnref,最终在indirection中找到数组访问器所在:
indirection_el: <snip> | '[' a_expr ']' { ... ai->is_slice = false; ... } | '[' opt_slice_bound ':' opt_slice_bound ']' { ... ai->is_slice = true; ... } ; opt_slice_bound: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ;从中可以观察出三条关键事实:
- 存在一个由多个 indirection 元素组成的序列。
- 简单的数组索引是一个表达式。
- 与问题最直接相关的是:每个切片边界都是可选的,若存在则是表达式。
接下来以同样的自上而下方式挖掘 SQLFluff 侧文法:postgres.SelectStatementSegment(基本是 ANSI select 的副本)→ansi.SelectStatementSegment(记住Ref总是优先选取方言特定的文法)→postgres.SelectClauseSegment.match_grammar→ansi.SelectClauseElementSegment→ansi.BaseExpressionElementGrammar→ansi.ExpressionSegment→ansi.Expression_A_Grammar→ansi.Expression_C_Grammar→ansi.Expression_D_Grammar→postgres.AccessorGrammar→postgres.ArrayAccessorSegment,最终找到了与 Bison 中indirection_el对应的部分。修复前的ArrayAccessorSegment只接受数值字面量(不允许表达式),且存在切片时必须至少有一侧边界(无法写[:])。修复后的版本(dialect_postgres.py 第 1017-1048 行)大幅简化并对齐了 Bison 文法:
class ArrayAccessorSegment(ansi.ArrayAccessorSegment): """Overwrites Array Accessor in ANSI to allow n many consecutive brackets. Postgres can also have array access like python [:2] or [2:] so numbers on either side of the slice segment are optional. """ match_grammar = Bracketed( OneOf( # These three are for a single element access: [n] Ref("QualifiedNumericLiteralSegment"), Ref("NumericLiteralSegment"), Ref("ExpressionSegment"), # This is for slice access: [n:m], [:m], [n:], and [:] Sequence( OneOf( Ref("QualifiedNumericLiteralSegment"), Ref("NumericLiteralSegment"), Ref("ExpressionSegment"), optional=True, ), Ref("SliceSegment"), OneOf( Ref("QualifiedNumericLiteralSegment"), Ref("NumericLiteralSegment"), Ref("ExpressionSegment"), optional=True, ), ), ), bracket_type="square", )修改后:单元素访问支持任意表达式;切片[n:m]、[:m]、[n:]、[:]全部合法。整个排查过程验证了一条重要方法论:先对照参考文法确认数据库引擎的真实行为,再在 SQLFluff 文法中自上而下地找到对应位置,最后动手修改。
测试你的改动
修复完成并确认解决原始问题后,还不能直接提交,还需要做两件事:
- 运行测试套件,确认改动没有破坏其他功能。
- 新增测试用例,供他人未来回归验证。
添加测试用例
添加测试用例很简单:在 test/fixtures/dialects 下对应的方言目录中新增一个 SQL 文件(可以扩展现有 SQL 文件,也可以新建)。实践建议:
- 加入 issue 中报告的原始 SQL;若官方语法文档附有示例(如 Snowflake 每个语法定义底部都有示例区),也一并复制进示例文件。
- 基于参考文法穷尽测试各种“钻牛角尖”的语法组合。测试语句不必可运行,只要能正确解析出对应结构、且能通过数据库引擎的解析阶段即可。参考文档中的简单示例往往不能覆盖现实世界的全部可能性——试着用尽可能覆盖文法分支的语句来测试。
- 务必验证测试中的 SQL 语句确实能被数据库引擎解析!简单做法是把语句粘贴进数据库控制台试运行,或使用与引擎同源的 CLI 解析工具(如 pgsql-parser)。出现“列不存在”这类错误没问题,只要不是语法解析错误即可。
从 test/dialects/dialects_test.py 的源码可以看到,该测试通过get_parse_fixtures(fail_on_missing_yml=True)自动收集 test/fixtures/dialects 下的所有文件并参数化运行;每个用例都会断言解析树无损(raw_segments拼接结果等于原始 SQL)且无词法/解析违规(not parsed_file.violations)。
YML 测试夹具文件
除了 SQL 文件,还有与之配套的自动生成 YAML 文件,其中保存了 SQL 的解析结果。将解析树纳入源码的好处是:当有人重定义语法导致解析方式变化时,SQL 不变但解析树变了——通过把 YAML 一起提交,就能在 code review 中发现并确认这类变化是否符合预期。绝大多数情况下(新增测试用例除外)不应出现无关 YML 文件的变化,这是一个很好的自检点。
新增或编辑任何测试夹具 SQL 文件后,重新生成全部 YAML:
tox -e generate-fixture-yml也可以只针对特定方言、或只针对新增和变更的文件,速度更快:
tox -e generate-fixture-yml -- --dialect postgres tox -e generate-fixture-yml -- --new-only命令运行约需几分钟,之后用git status查看差异。改动时务必检查测试输出或对应 YAML 文件中的解析后结构,确认每个查询元素类型正确。典型 bug 包括:独立的INTERVAL关键字被解析成函数名、本应是date_part的元素被解析成identifier。通常无需手写断言,但开发者有责任人工核对自动生成的 YAML 结构——不能因为没报解析错误就认为一切正常。
运行测试套件
直接运行tox会执行全部测试套件(多种 Python 版本、含与不含 dbt),耗时很长,最好留给 CI;本地只需运行与改动相关的最小集合。
测试单个夹具:dialects_test已参数化,自动收集 test/fixtures/dialects 下所有文件。例如新增或修改了dialects/hive/select_interval.sql:
tox -e py39 -- -s test/dialects/dialects_test.py -k hive-select_interval.sql-s标志让 pytest 打印解析后结构,便于快速核对各查询元素类型(与生成的夹具 YAML 内容一致)。更快的方式是直接调用 pytest(需已激活项目虚拟环境):
pytest -s test/dialects/dialects_test.py -k hive-select_interval.sql运行全部方言测试:
tox -e py39 -- test/dialects/dialects_test.py只跑特定方言:
tox -e py39 -- test/dialects/dialects_test.py -k ansi如果方言改动是为了修复某个规则误报,也可以只跑该规则的测试,例如只跑 LT01 布局规则相关测试:
tox -e py39 -- -k LT01 test提交前的最终检查
格式与 Lint 通常交给 CONTRIBUTING.md 中配置的 pre-commit 钩子即可;也可以手动运行。项目使用 ruff 对 Python 代码做 Lint 与格式化——作为 Linter 项目本身,代码质量要求更高,CI 或上述tox命令都会检查并标记问题。大多数情况下运行ruff format即可修正简单的格式问题;Lint 错误则运行ruff check查看并应用建议修复,或直接ruff check --fix。
提交前建议完整跑一次(仅单个 Python 版本、不含 dbt):
tox -e py311约需 10 分钟。若还要覆盖与 Lint:
tox -e generate-fixture-yml,cov-init,py311,cov-report,linting注意覆盖率测试在本地可能因缺少 Windows、dbt 等环境而报告覆盖率缺失,其余交给 CI 检查。无论本地测试做多少,PR 打开或更新后 GitHub 都会运行完整回归套件(首次贡献者需要维护者先触发一次测试)。
提交你的改动
仓库采用标准的 GitHub 工作流:Fork 仓库 → 本地 clone → 修改 → 推送到自己的 Fork → 向 SQLFluff 原仓库发起 PR。PR 发起后 CI 约 5-10 分钟完成;若全部通过,维护者会尽快处理。一个小而清晰、测试全绿、易于理解的 PR 更容易被快速合并。如有疑问,可以在 GitHub 上开 issue 或加入 SQLFluff 社区(Slack)向维护者/社区提问。
结语
方言贡献是 SQLFluff 用户既改善自己、又惠及他人的最佳途径。整条路径可以归纳为一个可复用的方法论闭环:
- 定位:用
sqlfluff parse复现解析失败,找到报错所在的 Segment。 - 对照:查阅数据库引擎的 Bison/Flex 文法与官方参考文档,确认真实可解析的语法范围。
- 修改:在对应方言文件中,用
Sequence、OneOf、Delimited、Bracketed、Ref、optional=True、terminators、parse_mode等构件精准调整文法;需要新符号时同步维护关键字列表与词法规则。 - 验证:在 test/fixtures/dialects 添加覆盖文法边界的测试用例,用
tox -e generate-fixture-yml重新生成 YAML 并人工核对解析树结构,运行方言测试与规则测试确保无回归。 - 提交:通过
ruff检查代码质量后,发起小而清晰的 PR。
掌握这套流程后,你不仅能解除自己使用 SQLFluff 时遇到的阻塞,也能为整个 SQL 方言生态持续贡献价值。
【免费下载链接】sqlfluffA modular SQL linter and auto-formatter with support for multiple dialects and templated code.项目地址: https://gitcode.com/GitHub_Trending/sq/sqlfluff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考