Debezium发布历史02

原文地址: https://debezium.io/blog/2016/04/15/parsing-ddl/

解析DDL
2016 年 4 月 15 日, 兰德尔·豪奇 (Randall Hauch)
mysql sql

欢迎关注留言,我是收集整理小能手,工具翻译,仅供参考,笔芯笔芯.

当我们的MySQL 连接器读取 MySQL 服务器或集群的 binlog 时,它会解析日志中的 DDL 语句,并随着时间的推移构建每个表架构的内存中模型。此过程很重要,因为连接器使用每个事件发生时的表定义为每个表生成事件。我们无法使用数据库的当前架构,因为自连接器读取的时间点(或日志中的位置)以来,它可能已发生更改。

解析 MySQL 或任何其他主要关系数据库的 DDL 似乎是一项艰巨的任务。通常每个 DBMS 都有高度定制的 SQL 语法,尽管数据操作语言(DML) 语句通常相当接近标准,但数据定义语言(DDL) 语句通常不太接近标准,并且涉及更多 DBMS 特定的功能。

既然如此,我们为什么要为 MySQL 编写自己的 DDL 解析器呢?我们首先看一下 Debezium 需要 DDL 解析器来做什么。

在 Debezium MySQL 连接器中解析 DDL
MySQL binlog 包含各种类型的事件。例如,当向表中插入一行时,binlog 事件包含对该表的间接引用以及表中每一列的值,但没有有关组成该表的列的信息。binlog 中唯一引用表结构的是 MySQL 在处理用户提供的 DDL 语句时生成的 SQL DDL 语句。

连接器还使用 Kafka Connect 模式生成消息,这些模式是简单的数据结构,定义每个字段的各种名称和类型以及字段的组织方式。因此,当我们为表插入生成事件消息时,我们首先必须有一个包含所有适当字段的 Kafka Connect对象,然后我们必须使用字段和Schema字段将列值的有序数组转换为 Kafka Connect对象。Struct表插入事件中的各个列值。

幸运的是,当我们遇到 DDL 语句时,我们可以更新内存中模型,然后使用它来生成对象Schema。同时,我们可以创建一个组件,该组件将使用该对象从事件中出现的列值的有序数组Schema创建一个对象。Struct所有这些都可以完成一次并用于该表上的所有行事件,直到我们遇到另一个更改表架构的 DDL 语句,此时我们再次更新了模型。

因此,所有这些都需要解析所有 DDL 语句,尽管出于我们的目的,我们只需要了解DDL 语法的一小部分。然后,我们必须使用该语句子集来更新表的内存模型。由于我们的内存表模型不是 MySQL 特有的,因此生成Schema对象和将值数组转换为Struct消息中使用的对象的组件的其余功能都是通用的。

现有的 DDL 库
不幸的是,实际上并没有那么多用于解析 MySQL、PostgreSQL 或其他流行 RDBMS 的 DDL 语句的第三方开源库。JSqlParser经常被引用,但它具有单一语法,是多个 DBMS 语法的组合,因此不是针对任何特定 DBMS 的严格解析器。通过更新复合语法来添加对其他 DBMS 的支持可能很困难。

其他库(例如PrestoDB )定义了自己的 SQL 语法,无法处理 MySQL DDL 语法的复杂性和细微差别。Antlr 解析器生成器项目具有MySQL 5.6 的语法,但这仅限于 DML 的一小部分,并且不支持 DDL 或更新的 5.7 功能。Antlr 3有较旧的 SQL 相关语法,但这些语法通常很庞大、存在错误,并且仅限于特定的 DBMS。Teiid项目是一个数据虚拟化引擎,位于各种 DBMS 和数据源之上,它的工具具有一系列DDL 解析器,可以在特殊存储库中构建 AST(作者实际上帮助开发了这些)。还有 Ruby 库,例如Square 的 MySQL 解析器库。还有一种专有的商业产品。

我们的 DDL 解析器框架
由于我们找不到有用的第 3 方开源库,因此我们选择创建自己的 DDL 解析器框架来满足我们的需求:

解析 DDL 语句并更新我们的内存模型。

专注于使用那些基本语句(例如,创建、更改和删除表和视图),而完全忽略其他语句而不必解析它们。

与MySQL DDL 语法文档类似地构造解析器代码,并使用反映语法中规则的方法名称。随着时间的推移,这将使维护变得更容易。

允许根据需要为 PostgreSQL、Oracle、SQLServer 和其他 DBMS 创建解析器。

通过子类化支持自定义:能够轻松覆盖逻辑的狭窄部分,而无需复制大量代码。

使开发、调试和测试解析器变得容易。

由此产生的框架包括一个标记生成器,它将字符串中的一个或多个 DDL 语句转换为可回滚的标记序列,其中每个标记代表标点符号、带引号的字符串、不区分大小写的单词和符号、数字、关键字、注释和终止字符(例如对于;MySQL)。然后,DDL 解析器使用简单且易于阅读的流畅 API 遍历令牌流寻找模式,调用自身的方法来处理各种令牌集。解析器还使用内部数据类型解析器来处理 SQL 数据类型表达式,例如INT, VARCHAR(64), NUMERIC(32,3), TIMESTAMP(8) WITH TIME ZONE。

MySqlDdlParser类扩展了基类并提供所有特定于 MySQL 的解析逻辑。例如,DDL 语句:

Create and populate our products using a single insert with many rows

CREATE TABLE products (
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description VARCHAR(512),
weight FLOAT
);
ALTER TABLE products AUTO_INCREMENT = 101;

Create and populate the products on hand using multiple inserts

CREATE TABLE products_on_hand (
product_id INTEGER NOT NULL PRIMARY KEY,
quantity INTEGER NOT NULL,
FOREIGN KEY (product_id) REFERENCES products(id)
);
可以很容易地解析为:

String ddlStatements = …
DdlParser parser = new MySqlDdlParser();
Tables tables = new Tables();
parser.parse(ddl, tables);
在这里,Tables对象是命名表定义的内存中表示。解析器处理 DDL 语句,将每个语句应用到Tables对象内适当的表定义。

怎么运行的
每个DdlParser实现都有以下公共方法,用于解析提供的字符串中的语句:

public final void parse(String ddlContent, Tables databaseTables) {
    Tokenizer tokenizer = new DdlTokenizer(!skipComments(), this::determineTokenType);
    TokenStream stream = new TokenStream(ddlContent, tokenizer, false);
    stream.start();
    parse(stream, databaseTables);
}

在这里,该方法使用知道如何将字符串中的字符分隔成各种类型的标记对象的TokenStreama 从内容创建一个新的。DdlTokenizer然后它调用另一个parse方法来完成大部分工作:

public final void parse(TokenStream ddlContent, Tables databaseTables)
                       throws ParsingException, IllegalStateException {
    this.tokens = ddlContent;
    this.databaseTables = databaseTables;
    Marker marker = ddlContent.mark();
    try {
        while (ddlContent.hasNext()) {
            parseNextStatement(ddlContent.mark());
            // Consume the statement terminator if it is still there ...
            tokens.canConsume(DdlTokenizer.STATEMENT_TERMINATOR);
        }
    } catch (ParsingException e) {
        ddlContent.rewind(marker);
        throw e;
    } catch (Throwable t) {
        parsingFailed(ddlContent.nextPosition(),
                      "Unexpected exception (" + t.getMessage() + ") parsing", t);
    }
}

这会设置一些本地状态,标记当前起点,并尝试解析 DDL 语句,直到找不到更多语句。如果解析逻辑无法找到匹配项,它会生成一个ParsingException包含有问题的行和列的消息,以及一条消息,表明已找到的内容和预期的内容。在这种情况下,此方法会倒回令牌流(如果调用者希望尝试替代的不同解析器)。

每次parseNextStatement调用该方法时,该语句的起始位置都会传递到该方法中,从而为其提供该语句的起始位置。我们的MySqlDdlParser子类重写该parseNextStatement方法以使用语句中的第一个标记来确定 MySQL DDL 语法中允许的语句类型:

@Override
protected void parseNextStatement(Marker marker) {
    if (tokens.matches(DdlTokenizer.COMMENT)) {
        parseComment(marker);
    } else if (tokens.matches("CREATE")) {
        parseCreate(marker);
    } else if (tokens.matches("ALTER")) {
        parseAlter(marker);
    } else if (tokens.matches("DROP")) {
        parseDrop(marker);
    } else if (tokens.matches("RENAME")) {
        parseRename(marker);
    } else {
        parseUnknownStatement(marker);
    }
}

当找到匹配的标记时,该方法将调用适当的方法。例如,如果语句以 开头CREATE TABLE …​,则parseCreate使用标识语句起始位置的相同标记来调用该方法:

@Override
protected void parseCreate(Marker marker) {
    tokens.consume("CREATE");
    if (tokens.matches("TABLE") || tokens.matches("TEMPORARY", "TABLE")) {
        parseCreateTable(marker);
    } else if (tokens.matches("VIEW")) {
        parseCreateView(marker);
    } else if (tokens.matchesAnyOf("DATABASE", "SCHEMA")) {
        parseCreateUnknown(marker);
    } else if (tokens.matchesAnyOf("EVENT")) {
        parseCreateUnknown(marker);
    } else if (tokens.matchesAnyOf("FUNCTION", "PROCEDURE")) {
        parseCreateUnknown(marker);
    } else if (tokens.matchesAnyOf("UNIQUE", "FULLTEXT", "SPATIAL", "INDEX")) {
        parseCreateIndex(marker);
    } else if (tokens.matchesAnyOf("SERVER")) {
        parseCreateUnknown(marker);
    } else if (tokens.matchesAnyOf("TABLESPACE")) {
        parseCreateUnknown(marker);
    } else if (tokens.matchesAnyOf("TRIGGER")) {
        parseCreateUnknown(marker);
    } else {
        // It could be several possible things (including more
        // elaborate forms of those matches tried above),
        sequentially(this::parseCreateView,
                     this::parseCreateUnknown);
    }
}

在这里,该方法首先使用文字来使用令牌CREATE,然后尝试将令牌与令牌文字的各种模式进行匹配。如果找到匹配项,此方法将委托给其他更具体的解析方法。请注意框架的流畅 API 如何使理解匹配模式变得非常容易。

让我们更进一步。假设我们的 DDL 语句以 开头CREATE TABLE products (,那么解析器将调用该parseCreateTable方法,再次使用相同的标记来表示语句的开头:

protected void parseCreateTable(Marker start) {
    tokens.canConsume("TEMPORARY");
    tokens.consume("TABLE");
    boolean onlyIfNotExists = tokens.canConsume("IF", "NOT", "EXISTS");
    TableId tableId = parseQualifiedTableName(start);
    if ( tokens.canConsume("LIKE")) {
        TableId originalId = parseQualifiedTableName(start);
        Table original = databaseTables.forTable(originalId);
        if ( original != null ) {
            databaseTables.overwriteTable(tableId, original.columns(),
                                          original.primaryKeyColumnNames());
        }
        consumeRemainingStatement(start);
        debugParsed(start);
        return;
    }
    if (onlyIfNotExists && databaseTables.forTable(tableId) != null) {
        // The table does exist, so we should do nothing ...
        consumeRemainingStatement(start);
        debugParsed(start);
        return;
    }
    TableEditor table = databaseTables.editOrCreateTable(tableId);

    // create_definition ...
    if (tokens.matches('(')) parseCreateDefinitionList(start, table);
    // table_options ...
    parseTableOptions(start, table);
    // partition_options ...
    if (tokens.matches("PARTITION")) {
        parsePartitionOptions(start, table);
    }
    // select_statement
    if (tokens.canConsume("AS") || tokens.canConsume("IGNORE", "AS")
        || tokens.canConsume("REPLACE", "AS")) {
        parseAsSelectStatement(start, table);
    }

    // Update the table definition ...
    databaseTables.overwriteTable(table.create());
    debugParsed(start);
}

此方法尝试镜像MySQLCREATE TABLE语法规则,其开头为:

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
(create_definition,…)
[table_options]
[partition_options]

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
[(create_definition,…)]
[table_options]
[partition_options]
select_statement

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
{ LIKE old_tbl_name | (LIKE old_tbl_name) }

create_definition:

在我们开始之前,文字CREATE已经被消耗了parseCreateTable,所以它首先尝试消耗TEMPORARY文字(如果可用)、TABLE文字、IF NOT EXISTS片段(如果可用),然后消耗并解析表的限定名称。如果语句包含LIKE otherTable,它将使用databaseTables(这是对我们对象的引用Tables)用引用表的定义覆盖指定表的定义。否则,它会获得新表的编辑器,然后(与语法规则一样)解析create_definition片段的列表,后跟table_options、partition_options,可能还有select_statement。

查看完整的MySqlDdlParser类以了解更多详细信息。

包起来
这篇文章详细介绍了为什么 MySQL 连接器在 binlog 中使用 DDL 语句,尽管我们只触及了连接器如何使用其框架进行 DDL 解析的表面,以及如何在未来的其他 DBMS 方言的解析器中重用它。

尝试我们的教程来查看 MySQL 连接器的运行情况,并继续关注更多连接器、版本和新闻。

你可能感兴趣的:(debezium,CDC,FlinkCDC,大数据,java,数据库)