NiclasOlofsson/sqllens

GitHub: NiclasOlofsson/sqllens

sqllens 是一个支持八大 SQL 方言的 TypeScript 解析器与静态分析器,提供名称解析、类型推断、列血缘追踪等语义分析能力,可用于驱动编辑器智能功能和数据血缘工具。

Stars: 2 | Forks: 0

# sqllens [![npm version](https://img.shields.io/npm/v/sqllens)](https://www.npmjs.com/package/sqllens) [![license](https://img.shields.io/npm/l/sqllens)](LICENSE) 一个 TypeScript SQL 解析器和静态分析器。它将 SQL 解析为树状结构,将其降级 为与方言无关的中间表示(IR),并在该 IR 上运行语义层: 名称解析(scope)、基于 schema 的限定、类型推断 以及列血缘分析。给它一个 查询语句,它就会告诉你查询的数据源、输出的列、它们的数据类型, 以及每一列的来源。解析器是基于 [antlr4ng](https://github.com/mike-lischke/antlr4ng) 运行时生成的 TypeScript 代码。 前端具有容错能力和 token 优先的特性,因此该库可以驱动编辑器 功能(自动补全、悬停提示、诊断、跳转定义),即使在处理不完整、 编辑过程中的文本时也能正常工作。请参阅 [编辑器 / 语言工具](#editor--language-tooling)。基于它构建的 LSP (Language Server Protocol)服务器位于本仓库中,但它处于实验阶段,不属于已发布的包。 ``` npm install sqllens ``` ``` import { analyze, Schema } from "sqllens"; const schema = new Schema({ orders: { id: "int", total: "decimal" } }); const q = analyze("SELECT total FROM orders WHERE total > 100", "postgres", { schema }); q.diagnostics; // [] — names and types check against the schema q.lineage.originsOf("total"); // → orders.total ``` ## 方言 sqllens 直接实现了八种 SQL 方言,每种都有各自的语法。另外还有 七种引擎作为*派生方言*被覆盖:它们的 SQL 已经被这八种语法中的某一种解析,总共支持 15 种引擎。 | Dialect | Derived dialects | Parse + lower | Semantic layer | Notes | |---|---|---|---|---| | Databricks (Spark SQL) | Apache Spark, AWS Glue | yes | yes | grammar forked from apache/spark | | T-SQL | SQL Server, Microsoft Fabric, Azure Synapse | yes | yes | grammar forked from grammars-v4 `sql/tsql` | | Snowflake | — | yes | yes | grammar forked from grammars-v4 `sql/snowflake` | | BigQuery (GoogleSQL) | — | yes | yes | grammar forked from `bytebase/parser` `googlesql/`; gated against ZetaSQL's `.test` corpus | | Redshift | — | yes | yes | grammar forked from Bytebase's Postgres-derived Redshift grammar (BSD-3) | | PostgreSQL | — | yes | yes | grammar forked from `bytebase/parser` `postgresql/` (BSD-3, PG18 keywords) | | DuckDB | — | yes | yes | grammar forked from this repo's own postgres pair (no open ANTLR grammar exists) | | Trino | Presto, Amazon Athena | yes | yes | grammar is the first-party trinodb `SqlBase.g4` (release 482), mechanically split | 每种语法最初都是基于上述上游分支出来的,但如今大多数已经远非 原封不动的副本。它们经过了大量的扩展和修正,这些修改源于对各方言官方参考文档以及从这些文档中提取的参考语料库的全面比对,因此它们的能力早已超越了其分支起点。 **派生方言**是指本身没有独立语法,但其 SQL 会被主要语法直接解析的引擎,因为它的 SQL 是主要方言的子集(或完全相同)。Microsoft Fabric 运行的是受限的 T-SQL 子集,Amazon Athena 的引擎是 Trino,而 AWS Glue 运行的是 Spark。在加入列表之前,每一种都会根据该引擎的真实 SQL 进行检验。 在代码中,`dialect` 参数是 `"databricks" | "tsql" | "snowflake" | "bigquery" | "redshift" | "postgres" | "duckdb" | "trino"` 中的一种。`resolveDialect` 会将引擎名称(或方言名称)转换为解析它的方言:`resolveDialect("athena")` 会返回 `"trino"`。 语义层与方言无关:它运行在共享的 IR 上,在所有方言上的执行方式都完全相同。只有解析(parse)和降级(lower)阶段是特定于方言的。 ## 流水线 ``` parse → lower → resolveScopes → qualify → infer / lineage / symbols ``` 每个阶段都会产生一个值,而这个值正是特定编辑器功能所读取的内容。只有最初的两个阶段,即解析和降级,是特定于方言的;它们之后的所有内容都是共享的,并且在所有八种方言上的执行方式都完全相同。 **parse** 将 SQL 文本转换为*具体语法树*(CST):完整的解析树,包含每一个 token 和语法节点,与原文完全一致,没有任何丢失或简化。它还会返回 token 流和语法错误计数。CST 虽然忠实于原文,但非常冗长且带有方言特性,因此下游没有东西会直接读取它。它支持语法波浪线(解析错误处的下划线)和语义 token(具备方言感知能力的高亮)。 **lower** 将 CST 转换为*中间表示*(IR):这是一个小型的、与方言无关的节点树,例如 `QueryExpr`、`SelectExpr` 和 `Expr`,无论 SQL 来自 Snowflake 还是 T-SQL,它们代表的含义都是相同的。(在 API 中,这个值就是 `ast` 字段,即*抽象语法树*,是 CST 的精简对应物。)它还会为每条语句标记其类型:查询、DML(数据操作:`INSERT` / `UPDATE` / `DELETE`)或 DDL(数据定义:`CREATE` / `ALTER` / `DROP`)。lower 不会抛出异常,因此即使是输入了一半、损坏的 SQL,依然能生成供流水线其余部分运行的 IR。 **resolveScopes** 在无需 schema 的情况下,基于 IR 构建符号表。对于每个查询 scope,它会计算出可见的数据源(表、子查询和 CTE;*公共表表达式*即 `WITH name AS (…)` 临时结果集),根据它们解析名称,并计算查询的输出列。它不需要 catalog,因此它驱动的功能可以在任何文件上零配置运行:跳转定义、查找引用和文档高亮。 **qualify** 是第一个需要 *schema*(即表及其列类型的 catalog)的阶段。有了它,qualify 会将 `SELECT *` 展开为实际的列列表,抛出未知表和未知列的诊断信息,并将每个列引用绑定到其数据源以及列类型。这就是开启依赖于 schema 的语义波浪线(只有知道 schema 后才能标记未知列)的原因,并且它响应 `bindingOf`,告诉你给定的列解析到了哪个数据源。 **infer** 计算每个表达式的类型和可空性,从一个简单的列到 `a + b`、`COALESCE(…)`、`CASE` 或函数调用。它支持悬停提示(指向表达式时显示的类型)和内联提示(内联类型注释)。 **lineage** 将每个输出列通过 CTE、子查询和 joins 追溯到其派生时所依赖的基表列,并记录沿途的每一个跳转点。它支持血缘面板和跳转至源头(从输出列跳转到它最终读取的物理列)。 **symbols** 派生出 `Sym` 模型:按类型和修饰符分类的每一个命名实体(数据源、列、CTE)。它为编辑器大纲 / 文档符号列表以及 code-lens 注释提供支持。 ## 使用方法 有两种入口:用于对字符串进行一次性分析的 `analyze`,以及针对你保持打开并编辑的文档的 session。带模板的 SQL(dbt 模型)使用相同的 API,只需多加一个选项;它改变了输入,但不会改变你获取结果的形态。 ### 一次性操作:`analyze` `analyze` 运行整个流水线,并返回一个你可以直接读取的结果: ``` import { analyze, Schema } from "sqllens"; const schema = new Schema({ t: { a: "int", b: "string" } }); const a = analyze("SELECT a, b FROM t", "tsql", { schema }); a.scopes; // name resolution (ScopeTree) a.diagnostics; // unknown-table / column diagnostics a.qualification.columnsOf(a.scopes.root); // * expansion a.types.typeOf(expr, scope); // per-expression types a.lineage.originsOf("a"); // base-table origins of an output column a.symbols; // kind × modifier symbol model ``` ### 保留文档:session 编辑器会持有一个不断变化的文件。对此的入口就是 session:它在构造时进行解析,针对每条语句进行缓存,而编辑操作会复用所有未被触及的内容。 ``` import { SqlSession, Schema } from "sqllens"; const s = SqlSession.create("SELECT amount FROM sales", "databricks", { schema }); // properties — cheap reads of what construction already produced s.ast; // the dialect-neutral IR (frozen) s.tokens; // the token stream: every token, exact spans — present even mid-edit s.scopes; // name resolution; needs no schema // verbs — parentheses execute a pass (memoized against the schema's version) s.diagnostics(); // syntax + schema-fed, one document-ordered list s.lineage(); // column lineage for the output columns s.deriveSymbols(); // the outline / symbol model // cursor verbs — offset in, spans out s.completeAt(14); // completions at an offset (works on broken, mid-keystroke text) s.referencesAt(9); // declaration + every occurrence of the symbol under the cursor s.typeAt(9); // inferred type of the expression under the cursor // edits are immutable: a new session, caches carried over const next = s.withText("SELECT amount, id FROM sales"); ``` 整体惯例是:属性开销很小,括号执行实际操作。每个动词都是对自由函数(`qualify`、`lineage`、`deriveSymbols`、`referencesAt` 等)的单行委托,这些函数保持导出状态,因此轻量级使用者可以跳过 session,只导入它调用的函数,而不捆绑任何其他内容。 ### 分阶段构建块 每个阶段也是其自身的入口点,每个结果都是一个值,你可以选择停下或将其传递给下一步;将结果向前传递,只会运行缺失的步骤: ``` import { parse, qualify, lineage, deriveSymbols, toScopes, Schema } from "sqllens"; const { ast, errors, cst } = parse("SELECT a, b FROM t", "snowflake"); // ast = dialect-neutral IR (frozen); cst = the raw antlr tree (escape hatch) const scopes = toScopes(ast, { dialect: "snowflake" }); // idempotent lift qualify(scopes, schema); // reuses scopes — never re-parses or re-resolves lineage(scopes, schema); // safe on the same scopes, in any order deriveSymbols(scopes); // independent results ``` 各方言特定的入口(`parseDatabricks` ... `parseTrino`,各自的 `lower`,以及原始的 `resolveScopes` / `inferType`)为只需要单个阶段的调用者保持导出。 ### 模板化 SQL(dbt 模型) dbt 模型并不是普通的 SQL;它是 minijinja 模板化的 SQL(`{{ ref('orders') }}`,`{% if %}` ...)。sqllens 原生地解析这种原始文本,而不需要渲染:标签获得精确的跨度,FROM 位置中的 `ref` 成为一个带有其模型名称的真实表数据源,而下游的所有内容(scope、诊断、类型、血缘、自动补全)在模板化文档上的运行方式完全不变。 模板是被显式声明的,而不是被猜测的。你向 session 传递一个模板引擎;没有引擎则表示是普通的 SQL: ``` import { SqlSession } from "sqllens"; import { minijinja } from "sqllens/minijinja"; // its own entry point — plain-SQL consumers never load it const s = SqlSession.create(modelText, "databricks", { templating: minijinja(), provider, // optional — template knowledge, see extension points below schema, }); s.ast; // IR: {{ ref('orders') }} in FROM is a TableSource named "orders" s.tokens; // ONE stream: SQL tokens + template tokens (channel 2, role "minijinja") s.diagnostics(); // SQL + template + schema-fed, merged, all in document coordinates s.tags; // every tag with exact spans (ref / source / macro / var / control) s.regions; // {% if %} / {% for %} structure — folding, branch enumeration s.tagOf(node); // the tag an IR node came from; nodeOf(tag) goes the other way ``` 为什么没有自动检测:对于 dbt 来说,SQL 字符串字面量内部的 `{{ … }}` 是模板,但对其他人来说则是纯文本。没有任何扫描器能分辨出原本的意图,而 sqllens 绝不猜测。宿主环境来声明它(通过文件关联、语言 ID 或配置)。在一个结果发现没有任何标签的文件上声明一个引擎,不会产生任何开销,也不会改变任何事情:其结果与纯解析在字节上完全一致,且模板 facet 为空。 在完全没有 provider 的情况下,一切也能正常工作:内置的默认设置会尽可能回答(`ref` 是一个由字面量参数命名的关系;`config` 不进行任何渲染),其他所有内容都会报告为未知,绝无猜测。provider 只是让结果变得更精确。 ### 扩展点 sqllens 了解 SQL 和模板的*语法*。所有它无法了解的内容(你的 catalog、你的宏展开成什么、`var('x')` 包含什么)都通过三个接口输入。 它们全都是可选的,每一个接口在缺失时的响应方式都相同:缺失即意味着“未知”,绝不是猜测,并且不会因缺少信息而触发任何诊断。 `SchemaProvider` 是 catalog:存在哪些表,以及它们的列类型。`Schema` 是预置形式(一个普通的映射,如上例所示)。对于拥有活动 catalog 的宿主,`CallbackSchema` 是惰性形式:sqllens 记录它所缺失的内容,你的 `prime()` 异步解析缺失的部分并提升版本号,下一次读取就会反映这些更改。LSP 正是根据这个信号重新发布诊断信息的。 `TemplateProvider` 是模板知识:模板调用的*含义*。继承 `DefaultTemplateProvider` 并只覆盖你的宿主环境知道的部分;每个方法回答一个问题: ``` class MyDbtProvider extends DefaultTemplateProvider { relationOf(call) { /* ref/source → the physical relation, with columns */ } valueOf(call) { /* var/env_var → the scalar type it yields */ } shapeOf(call) { /* a macro's expansion shape: "expr" | "predicate" | "column-list" | "statement" … */ } columnsOf(call) { /* a column-list macro's output columns */ } } ``` 每个文档对应一个实例。答案是同步的,来自热缓存,使用与 schema 相同的缺失记录 + `prime()` + 版本协议。仅凭基类就能实现完整功能;上文中的零 provider 示例运行的就是它。每次覆盖的回报都是直接的:`relationOf` 将“`{{ ref('orders') }}` 免于检查”转变为“`orders` 有这些列,并且 `o.totall` 是一个真正的未知列诊断”;`shapeOf` 使位于语句槽中的宏能够被顺利解析;`valueOf` 给 `{{ var('limit') }}` 提供一个可供推断使用的类型。 `TemplateEngine` 是模板语法,并且很少见。该引擎拥有解析模板化文本的方式;minijinja 作为唯一的实现被发布,几乎每个使用者都只是直接传递它。支持实现你自己的引擎(SQL 上的另一种模板语言),但这是一种契约,而不是回调:你的结果必须满足一致性验证门所检查的不变量。Token 与源码在字节上完美对应,每个跨度都使用原始文档坐标,损坏的输入绝不会抛出异常,无标签的文本与纯解析完全一致。 ## 编辑器 / 语言工具 前端具有容错能力和 token 优先的特性,因此它支持在不完整、正在编辑的文本上运行的编辑器功能。它们永远不需要完美无缺的解析: - `tokenize(sql, dialect)` 和 `parse(...).tokens` 提供了一流的 token 流:每个 token 都带有其精确的跨度、角色和通道。即使在解析出错时也可用。 - `lower()` 在处理损坏或不完整的输入时绝不会抛出异常;你会得到一个带有标记的 `query` IR,因此每个下游流程依然可以完整执行。 - `SqlDocument` 是一个持久的、不可变的、可按位置寻址的单文件模型。它只运行一次 `parse → resolveScopes`(加上惰性的 `analyze(schema)`),缓存结果,并响应 `tokenAt` / `nodeAt`。一次编辑会产生一个新文档;一个 O(log n) 的 `LineIndex` 负责将位置映射到偏移量。 - `completeAt(doc, offset, schema?)`:基于 scope 知的自动补全(关键字、列、表、函数),这是通过对语法进行 ATN(Augmented Transition Network,增强转移网络,即语法状态机形式)候选遍历实现的,完全是我们自主实现的,不依赖任何第三方。 - `signatureAt(doc, offset)`:从精选的各方言函数签名表中提供参数提示;对于未覆盖的情况,会退化为仅提供名称 + 当前活动参数。 - `referencesAt(scopes, offset, schema?)`:光标下符号的每一次出现(加上其声明);支持查找引用、文档高亮以及 code-lens 引用计数。 ``` import { SqlDocument, Schema } from "sqllens"; const doc = SqlDocument.create("SELECT amount FROM sales", "databricks"); doc.tokens; // first-class token stream (spans + roles) doc.tokenAt(7); // token under an offset const next = doc.withText("SELECT amount, id FROM sales", 2); // immutable edit → new doc ``` ## 语言服务器(实验性) 基于该库构建的 LSP(Language Server Protocol)服务器位于 `src/lsp/` 中。它处于实验阶段,并且**不属于已发布的 npm 包**:该包仅包含库,而服务器是你从仓库中运行的源码。它为每个打开的文件持有一个 `SqlDocument`(在编辑时重新构建),并且仅通过公共 API 访问该库,除协议转换外,不添加任何自身的分析。 一个 SQL 服务器只需要 LSP 约 30 种请求类型的一个子集:有些不适用于 SQL(如类型层次结构、文档颜色、monikers),还有一些被推迟了(格式化、跨项目导航)。以下是当前服务器逐项功能的进度: ### 语言特性 | Feature | Status | | --- | --- | | Completion (+ resolve) | ✅ | | Hover | ✅ | | Hover — nullability | ✅ (` — not null` / ` — nullable` suffix when provable) | | Signature help | ✅ | | Go to definition | ✅ | | Find references | ✅ | | Document highlight | ✅ | | Document symbols | ✅ | | Folding range | ✅ | | Selection range | ✅ | | Semantic tokens (full / range / delta) | ✅ all three | | Inlay hints | ✅ (no resolve) | | Code lens | ✅ (no resolve) | | Go to declaration | ◻️ not yet | | Go to type definition | ◻️ not yet | | Go to implementation | ◻️ not yet — name → its defining query (view / model); needs the project model | | Call hierarchy | ◻️ not yet — the CTE / view / model dependency graph | | Document link | ◻️ not yet | | Linked editing range | ◻️ not yet — live alias / name sync-edit | | Code action (quick fixes) | ◻️ next phase | | Rename (+ prepare) | ◻️ next phase | | Formatting / range / on-type | ◻️ deferred (external formatter) | | Inline values | ◻️ debugger surface | | Type hierarchy | — n/a — SQL has no type-inheritance relation | | Document color | — n/a — no color literals | | Moniker | — n/a — LSIF / cross-repo indexing concern | ### 诊断与文档同步 | Feature | Status | | --- | --- | | Diagnostics — push (`publishDiagnostics`) | ✅ | | Diagnostics — call signature (arity / argument type) | ✅ (curated tables; never-wrong, per-dialect coercion) | | Diagnostics — pull (document) | ✅ | | Diagnostics — pull (workspace) | ◻️ not yet | | Text sync — open / change / close | ✅ (full-document) | | Incremental sync | ◻️ full-document only (fine at SQL file sizes) | | Save notifications (`didSave` / `willSave`) | ◻️ not yet | | Notebook document sync | ◻️ not yet | ### Workspace 功能 | Feature | Status | | --- | --- | | Workspace symbols | ◻️ needs a project / multi-file model | | Execute command | ◻️ not yet | | Configuration / watched-files | ◻️ not yet (protocol config; file-based `.sqllens.json` config exists) | | File operations (create / rename / delete) | ◻️ not yet | 图例:✅ 已实现 · ◻️ 暂不支持 / 已推迟 · — 不适用于 SQL。那些 被推迟的项目都是我们在持续跟踪的工作:重命名和 code actions 是下一个 LSP 阶段的内容,工作区符号需要项目模型, 而格式化预计将封装一个现有的外部格式化工具。 ## 架构 每种方言对应一个文件夹;没有共享的“核心”语法,也没有语法继承。每种方言都是由分割的 `.g4` 文件对(lexer 语法 + parser 语法)组成的独立个体,从其最佳起点分支出来并就地编辑。`lower` 下游的所有内容都是共享的且与方言无关。 ## 从源码构建与贡献 `npm install sqllens` 在你这边不需要任何构建步骤。如果你想自己从源码构建,或者要修改语法,首先要从 `.g4` 文件重新生成解析器。[CONTRIBUTING.md](CONTRIBUTING.md) 包含了配置说明、命令列表以及语料库验证门的工作流程。 ## 许可证 MIT。请参阅 [LICENSE](LICENSE)。`grammars/` 下的衍生语法保留了它们上游的许可证(Databricks 使用 Apache-2.0;T-SQL 和 Snowflake 使用 MIT;BigQuery 和 Redshift 使用 BSD-3);请参阅 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。
标签:SQL解析器, TypeScript, 云安全监控, 安全插件, 数据血缘, 暗色界面, 编译器工具链, 自动化攻击, 语言服务协议, 静态分析