写点什么

Superset 兼容 ADB(AnalyticDB-MySQL)

用户头像
data_y
关注
发布于: 2021 年 03 月 11 日
Superset 兼容ADB(AnalyticDB-MySQL)

一、环境

对比了 GP 和 ADB,考虑使用 ADB 搭建数仓,然后在 metabase 和 superset 之间选了一个可视化工具,感觉 superset 友好一些。环境如下,已经搭建完成:

---

AnalyticDB-MySQL 3.0

apache-superset 1.0.1

# 以下是安装 superset 的时候自带安装的

Flask-SQLAlchemy 2.4.4

PyMySQL 1.0.2

SQLAlchemy 1.3.23

SQLAlchemy-Utils 0.36.84.4


二、前期调研

在 ADB 官网上,找到过 superset 对 ADB 的兼容性测试,在 data 里面添加了 database,然后在 SQL query 或者在 SQL Lab --> SQL Editor 下面用裸 SQL 查询,是没问题的。

https://help.aliyun.com/document_detail/162419.html?spm=a2c4g.11186623.6.594.565f7188zPdWTT

三、Registering a new table(添加 datasets)

有两个方式可以添加 datasets

1、通过界面直接添加

如下如


点击添加之后报错如下:


Error while saving dataset:Table [xxx] could not be found,xxxxxxxx
复制代码



看了下 superset 后台的报错,如下:


/data/project/superset/env/lib/python3.8/site-packages/sqlalchemy/dialects/mysql/reflection.py:64: SAWarning: Unknown schema content: 'Create Table `t_d_id` ('  util.warn("Unknown schema content: %r" % line)/data/project/superset/env/lib/python3.8/site-packages/sqlalchemy/dialects/mysql/reflection.py:64: SAWarning: Unknown schema content: ' `id` int NOT NULL,'  util.warn("Unknown schema content: %r" % line)/data/project/superset/env/lib/python3.8/site-packages/sqlalchemy/dialects/mysql/reflection.py:64: SAWarning: Unknown schema content: ' primary key (`id`)'  util.warn("Unknown schema content: %r" % line)) DISTRIBUTE BY BROADCAST INDEX_ALL='Y' STORAGE_POLICY='COLD' BLOCK_SIZE=4096 COMMENT='自增id表'Got an error t_d_id validating table: t_d_idWARNING:superset.datasets.dao:Got an error t_d_id validating table: t_d_id
复制代码


看来是 superset 解析 ADB 表结构元数据的问题,兼容性有点问题。

下面会讲怎么处理这个问题。

2、通过 SQL Query 方式添加 datasets

第一种方式报错,可以尝试一下第二种方式。

  1. 打开 SQL Query 或者 SQL Editor;

  2. 输入查询语句执行;

  3. 显示结果后,点击下面的 EXPLORE;

  4. 会弹出 Save or Overwrite Dataset 对话框,输入即可;(如下图)

在 datasets 里面,已经添加上了,但是 Type 是 Virtual。我理解可能是类似数据库的视图一样。


Virtual Datasets 在自己新建 Charts 里面,好像没法选择,只能在 datasets 里面点进去,里面有一个 Save chart。还是要解决第一个异常。


四、解决元数据兼容性问题

回到第一个报错。看报错应该是 superset 解析 ADB 元数据的问题,尝试下看看能不能解决。

1、sqlalchemy 是怎么解析的

直接定位到报错代码,如下

/data/project/superset/env/lib/python3.8/site-packages/sqlalchemy/dialects/mysql/reflection.py

@log.class_loggerclass MySQLTableDefinitionParser(object):    """Parses the results of a SHOW CREATE TABLE statement."""
def __init__(self, dialect, preparer): self.dialect = dialect self.preparer = preparer self._prep_regexes()
def parse(self, show_create, charset): state = ReflectedState() state.charset = charset for line in re.split(r"\r?\n", show_create): if line.startswith(" " + self.preparer.initial_quote): self._parse_column(line, state) # a regular table options line elif line.startswith(") "): self._parse_table_options(line, state) # an ANSI-mode table options line elif line == ")": pass elif line.startswith("CREATE "): self._parse_table_name(line, state) # Not present in real reflection, but may be if # loading from a file. elif not line: pass else: type_, spec = self._parse_constraints(line) if type_ is None: util.warn("Unknown schema content: %r" % line) elif type_ == "key": state.keys.append(spec) elif type_ == "fk_constraint": state.fk_constraints.append(spec) elif type_ == "ck_constraint": state.ck_constraints.append(spec) else: pass return state
复制代码


调用 sqlalchemy 解析表定义,是通过 show create table xxx 的输入,去解析表结构的(后面用大量正则,去解析字段,约束索引之类的),代码含义如下。


2、MySQL 和 ADB 的差异

大概找到问题,简单对比了下 MySQL 和 ADB 对于 show create table 的输出;

差异如下:

1、大小写差异

MySQL:保留字是大写(CREATE TABLE/KEY/UNIQUE/PRIMARY);

ADB:规则不统一(比如 Create Table/key/primary);

sqlalchemy:用 CREATE 开头去解析表名的;

2、字段缩进差异

MySQL:字段定义,索引定义之类的,统一是两个空格缩进;

ADB:缩进规则不统一,比如字段缩进一个空格,key 缩进两个空格,primary key 又缩进一个空格;

sqlalchemy:字段解析用前面两个缩进;

3、key 的定义差异

MySQL:KEY \`xxx\` (xxx) ,索引名有反引号引用,索引名和字段之间有空格;

ADB:key xxx(xxx) ,索引名没反引号引用,索引名和字段之间没空格;

sqlalchemy:索引名要用反引号,索引名和字段之间有空格;

4、其他差异

还有一些其他差异,比如数据类型(varchar 和 varchar(N)),比如表的一些参数(engine=xxx 和 DISTRIBUTE BY xxx 之类的)。看了下,都不影响。这些差异没有去适配。


如下图,可能会直观一些(上面是 MySQL,下面是 ADB):



3、问题怎么解决

通过以上分析,大概知道什么问题了。

解决思路:

1、解析 ADB 结构的之前,适配成 MySQL 的那种格式;

2、在解析行的时候,修改正则去适配 ADB;


看了下去解析字段/索引之类的正则,放弃了第二种方案,改不动。

通过第一种方案,简单适配以下先。

如下(就修改的比较粗糙了):

@log.class_loggerclass MySQLTableDefinitionParser(object):    """Parses the results of a SHOW CREATE TABLE statement."""
def __init__(self, dialect, preparer): self.dialect = dialect self.preparer = preparer self._prep_regexes()
def parse(self, show_create, charset): state = ReflectedState() state.charset = charset
for line in re.split(r"\r?\n", show_create): if (line.startswith(" " + self.preparer.initial_quote)): self._parse_column(line, state) # 后面兼容ADB elif (line.startswith(" " + self.preparer.initial_quote)): self._parse_column(" " +line, state) # a regular table options line elif line.startswith(") "): self._parse_table_options(line, state) # an ANSI-mode table options line elif line == ")": pass # 后面兼容ADB elif (line.startswith("CREATE ")) or (line.startswith("Create ")): self._parse_table_name(line, state) # Not present in real reflection, but may be if # loading from a file. elif not line: pass else: # 兼容ADB if not line.startswith(" "): line = " " + line # 兼容ADB 把key ix_job_name(`job_name`) 替换成KEY `ix_job_name` (`job_name`) ,要兼顾primary key 和 其他xxx key line=line.replace("(","` (") line=line.replace(" key "," KEY `") line=line.replace("key `","key ") line=line.replace(" primary "," PRIMARY ")
type_, spec = self._parse_constraints(line) if type_ is None: util.warn("Unknown schema content: %r" % line) elif type_ == "key": state.keys.append(spec) elif type_ == "fk_constraint": state.fk_constraints.append(spec) elif type_ == "ck_constraint": state.ck_constraints.append(spec) else: pass return state

# 参考一个匹配字段的正则,改这个也可以,就是第一个方案,看了下,有点难度,放弃了。 """
# `colname` <type> [type opts] # (NOT NULL | NULL) # DEFAULT ('value' | CURRENT_TIMESTAMP...) # COMMENT 'comment' # COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT) # STORAGE (DISK|MEMORY) self._re_column = _re_compile( r" " r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +" r"(?P<coltype>\w+)" r"(?:\((?P<arg>(?:\d+|\d+,\d+|" r"(?:'(?:''|[^'])*',?)+))\))?" r"(?: +(?P<unsigned>UNSIGNED))?" r"(?: +(?P<zerofill>ZEROFILL))?" r"(?: +CHARACTER SET +(?P<charset>[\w_]+))?" r"(?: +COLLATE +(?P<collate>[\w_]+))?" r"(?: +(?P<notnull>(?:NOT )?NULL))?" r"(?: +DEFAULT +(?P<default>" r"(?:NULL|'(?:''|[^'])*'|[\-\w\.\(\)]+" r"(?: +ON UPDATE [\-\w\.\(\)]+)?)" r"))?" r"(?: +(?:GENERATED ALWAYS)? ?AS +(?P<generated>\(" r".*\))? ?(?P<persistence>VIRTUAL|STORED)?)?" r"(?: +(?P<autoincr>AUTO_INCREMENT))?" r"(?: +COMMENT +'(?P<comment>(?:''|[^'])*)')?" r"(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?" r"(?: +STORAGE +(?P<storage>\w+))?" r"(?: +(?P<extra>.*))?" r",?$" % quotes )
"""
复制代码


添加的如下(和上面的代码一样):



改完后,问题解决,直接添加 datasets 成功。


也把问题同步给了阿里云 ADB 的支持,得到的答复是 ADB-MySQL 和 MySQL 没法 100%兼容,建议表结构从 information_schema.tables 或者 information_schema.columns 里面拿。但是这种小细节,可以严谨一点。希望后面的版本可以解决这个问题。


发布于: 2021 年 03 月 11 日阅读数: 61
用户头像

data_y

关注

还未添加个人签名 2018.12.14 加入

数据打工人

评论 (1 条评论)

发布
用户头像
厉害了
2021 年 03 月 11 日 14:50
回复
没有更多了
Superset 兼容ADB(AnalyticDB-MySQL)