beyondのblog

使用antlr4 解析sql语句

最近写了一个SQL DML 转换 GraphQL 的小工具, 记录一下使用antlr4 解析 sql语句的方法

  1. 在这个仓库 https://github.com/antlr/grammars-v4 找到对应的语法定义文件

  2. 实现自定义的Visitor

  3. 解析 table name, colume, comment 生成GraphQL 的定义

效果如下

[main] INFO com.beyondblog.Main - sql: CREATE TABLE `Users` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL COMMENT '用户名称',
  `type` tinyint DEFAULT NULL COMMENT '类型: xxxxxxxx',
  `note` varchar(200) DEFAULT NULL COMMENT '备注',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '0-禁用 1-启用 2-删除',
  `create_time` bigint DEFAULT NULL  COMMENT '创建时间',
  `update_time` bigint DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='用户'
[main] INFO com.beyondblog.MySqlParserSelectVisitor - 解析字段定义 `id`
[main] INFO com.beyondblog.MySqlParserSelectVisitor - 解析字段定义 `name`
[main] INFO com.beyondblog.MySqlParserSelectVisitor - 解析字段定义 `type`
[main] INFO com.beyondblog.MySqlParserSelectVisitor - 解析字段定义 `note`
[main] INFO com.beyondblog.MySqlParserSelectVisitor - 解析字段定义 `status`
[main] INFO com.beyondblog.MySqlParserSelectVisitor - 解析字段定义 `create_time`
[main] INFO com.beyondblog.MySqlParserSelectVisitor - 解析字段定义 `update_time`
[main] INFO com.beyondblog.Main - (ddlStatement (createTable CREATE TABLE (tableName (fullId (uid (simpleId (engineName `Users`))))) (createDefinitions ( (createDefinition (uid (simpleId (engineName `id`))) (columnDefinition (dataType bigint unsigned) (columnConstraint (nullNotnull NOT NULL)) (columnConstraint AUTO_INCREMENT))) , (createDefinition (uid (simpleId (engineName `name`))) (columnDefinition (dataType varchar (lengthOneDimension ( (decimalLiteral 100) ))) (columnConstraint DEFAULT (defaultValue NULL)) (columnConstraint COMMENT '用户名称'))) , (createDefinition (uid (simpleId (engineName `type`))) (columnDefinition (dataType tinyint) (columnConstraint DEFAULT (defaultValue NULL)) (columnConstraint COMMENT '类型: xxxxxxxx'))) , (createDefinition (uid (simpleId (engineName `note`))) (columnDefinition (dataType varchar (lengthOneDimension ( (decimalLiteral 200) ))) (columnConstraint DEFAULT (defaultValue NULL)) (columnConstraint COMMENT '备注'))) , (createDefinition (uid (simpleId (engineName `status`))) (columnDefinition (dataType tinyint) (columnConstraint (nullNotnull NOT NULL)) (columnConstraint DEFAULT (defaultValue (constant (stringLiteral '1')))) (columnConstraint COMMENT '0-禁用 1-启用 2-删除'))) , (createDefinition (uid (simpleId (engineName `create_time`))) (columnDefinition (dataType bigint) (columnConstraint DEFAULT (defaultValue NULL)) (columnConstraint COMMENT '创建时间'))) , (createDefinition (uid (simpleId (engineName `update_time`))) (columnDefinition (dataType bigint) (columnConstraint DEFAULT (defaultValue NULL)) (columnConstraint COMMENT '修改时间'))) , (createDefinition (tableConstraint PRIMARY KEY (indexColumnNames ( (indexColumnName (uid (simpleId (engineName `id`)))) )))) )) (tableOption ENGINE = (engineName InnoDB)) (tableOption COMMENT = '用户')))
[main] INFO com.beyondblog.Main - 结果如下:
[main] INFO com.beyondblog.Main - 类型为: Users
[main] INFO com.beyondblog.Main - 	 字段名: id, 类型: Int, 是否必须: true
[main] INFO com.beyondblog.Main - 	 字段名: name, 类型: String, 是否必须: false
[main] INFO com.beyondblog.Main - 	 字段名: type, 类型: Int, 是否必须: false
[main] INFO com.beyondblog.Main - 	 字段名: note, 类型: String, 是否必须: false
[main] INFO com.beyondblog.Main - 	 字段名: status, 类型: Int, 是否必须: true
[main] INFO com.beyondblog.Main - 	 字段名: create_time, 类型: Int, 是否必须: false
[main] INFO com.beyondblog.Main - 	 字段名: update_time, 类型: Int, 是否必须: false
[main] INFO com.beyondblog.Main - 
# 用户
type Users {
	id: Int
	# 用户名称
	name: String
	# 类型: xxxxxxxx
	type: Int
	# 备注
	note: String
	# 0-禁用 1-启用 2-删除
	status: Int
	# 创建时间
	createTime: Int
	# 修改时间
	updateTime: Int
}

解析的主要逻辑

@Slf4j
public class MySqlParserSelectVisitor extends MySqlParserBaseVisitor<Void> {

    private final GraphQLType graphQLType;

    public MySqlParserSelectVisitor(GraphQLType graphQLType) {
        this.graphQLType = graphQLType;
    }

    @Override
    public Void visitTableOptionComment(MySqlParser.TableOptionCommentContext ctx) {
        graphQLType.setDescription(getText(ctx.getChild(ctx.getChildCount() - 1)));
        return visitChildren(ctx);
    }

    @Override
    public Void visitDdlStatement(MySqlParser.DdlStatementContext ctx) {
        return super.visitDdlStatement(ctx);
    }

    @Override
    public Void visitTableName(MySqlParser.TableNameContext ctx) {
        graphQLType.setTypeName(getText(ctx));
        return visitChildren(ctx);
    }

    @Override
    public Void visitColumnDeclaration(MySqlParser.ColumnDeclarationContext ctx) {

        log.info("解析字段定义 {}", ctx.uid().getText());

        GraphQLField graphQLField = new GraphQLField();
        graphQLField.setSourceFieldName(getText(ctx.uid()));
        graphQLField.setFieldName(getText(ctx.uid()));

        var columnConstraint = ctx.columnDefinition().columnConstraint();
        var dataType = ctx.columnDefinition().dataType();
        if (dataType instanceof MySqlParser.StringDataTypeContext) {
            graphQLField.setType("String");
        } else if (dataType instanceof MySqlParser.DimensionDataTypeContext) {
            graphQLField.setType("Int");
        } else {
            log.warn("未知类型");
            graphQLField.setType("String");
        }

        for (var constraint : columnConstraint) {
            if (constraint instanceof MySqlParser.CommentColumnConstraintContext) {
                var comment = getText(((MySqlParser.CommentColumnConstraintContext) constraint).STRING_LITERAL());
                graphQLField.setDescription(comment);
            } else if (constraint instanceof MySqlParser.NullColumnConstraintContext) {
                if (((MySqlParser.NullColumnConstraintContext) constraint).nullNotnull().NOT() != null) {
                    graphQLField.setRequired(true);
                }
            }
        }
        this.graphQLType.addField(graphQLField);
        return super.visitColumnDeclaration(ctx);
    }

    private String getText(ParseTree ctx) {
        if (ctx.getText() == null) {
            return null;
        }
        return ctx.getText().replaceAll("`", "").replaceAll("'", "");
    }
}