一、环境
对比了 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_id
WARNING:superset.datasets.dao:Got an error t_d_id validating table: t_d_id
复制代码
看来是 superset 解析 ADB 表结构元数据的问题,兼容性有点问题。
下面会讲怎么处理这个问题。
2、通过 SQL Query 方式添加 datasets
第一种方式报错,可以尝试一下第二种方式。
打开 SQL Query 或者 SQL Editor;
输入查询语句执行;
显示结果后,点击下面的 EXPLORE;
会弹出 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_logger
class 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_logger
class 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 里面拿。但是这种小细节,可以严谨一点。希望后面的版本可以解决这个问题。
评论 (1 条评论)