开源之夏
项目经验分享
2023 #03
开源之夏个人专访与项目经验分享持续开放中,欢迎已从开源之夏毕业或正在参与开源之夏活动的学生、导师一同加入专访行动,扫描文末二维码填写专访问卷,与大家分享你眼中的开源之夏!本期项目经验分享来自 Apache ShardingSphere 社区中选学生——倪匀博,在开源之夏 2023 中承担的项目是Apache ShardingSphere: 增强 SQLNodeConverterEngine,支持更多的 MySQL/PostgreSQL/opengaus SQL 语句。
ShardingSphere 于 2018 年正式进入 Apache 基金会孵化器,2019 年进入 CNCF 全景图,于 2020 年成为 Apache 顶级项目。2020 年度国人主导最活跃的 Apache 软件基金会项目,2021 年度 Apache 基金会年度报告中 ShardingSphere 代码提交数量位列前十。目前 ShardingSphere 通过了中国信通院的《可信开源项目》评估认证,并获得了《OSCAR 尖峰开源项目及开源社区》。
官网:https://shardingsphere.apache.org/# 项目基本信息项目名称:Apache ShardingSphere: 增强 SQLNodeConverterEngine,支持更多的 MySQL/PostgreSQL/opengaus SQL语句项目导师:端正强,SphereEx Senior Java Engineer, ShardingSphere PMC Member项目描述:ShardingSphere SQL federation 引擎提供了对复杂 SQL 语句的支持,它可以很好地支持跨数据库连接查询、子查询、聚合查询和其他语句。SQL federation 引擎的一个重要组成部分就是将 ShardingSphere 解析的 SQL 语句通过 SQLNodeConverterEngine 转换为 SqlNode,利用 Calcite 实现 SQL 优化和联邦查询。本项目主要解决 SQLNodeConverterEngine 转换过程中出现 MySQL/PostgreSQL/OpenGauss SQL 语句转换异常的问题。项目链接:
https://summer-ospp.ac.cn/org/prodetail/235930182
# 项目经验分享项目开发方案项目开发方案大致如下:1. 增加 test case
在 /test/it/optimizer/resources/converter 中增加对应的 test case。2. 启动测试对比差异需要启动 SQLNodeConverterEngineIT 进行测试,比较预期与实际的差异。测试核心代码如下: @ParameterizedTest(name = “{0} ({1}) -> {2}”) @ArgumentsSource(TestCaseArgumentsProvider.class) void assertConvert(final String sqlCaseId, final SQLCaseType sqlCaseType, final String databaseType) { String expected; try { expected = SQL_NODE_CONVERTER_TEST_CASES.get(sqlCaseId, sqlCaseType, databaseType).getExpectedSQL(); } catch (final IllegalStateException ex) { log.warn(ex.getMessage()); return; } String sql = SQL_CASES.getSQL(sqlCaseId, sqlCaseType, SQL_PARSER_TEST_CASES.get(sqlCaseId).getParameters()); String actual = SQLNodeConverterEngine.convert(parseSQLStatement(databaseType, sql)).toSqlString(SQLDialectFactory.getSQLDialect(databaseType)).getSql().replace(“\n”, ” “).replace(“\r”, “”); assertThat(actual, is(expected)); }assertConvert 方法首先获取 expected SQL,然后使用 SQLNodeConverterEngine 的 convert 方法获得经过转换得到的 actual SQL,最后进行比较得出差异。3. 修改转换逻辑使用 calcite 获得正确的 SQLNode 列表。 String sql = “some sql”; SqlParser.Config parserConfig = SqlParser.configBuilder().setCaseSensitive(false).build(); SqlParser sqlParser = SqlParser.create(sql, parserConfig); try { SqlNodeList nodeList = sqlParser.parseStmtList(); for (SqlNode node: nodeList) { System.out.println(node); } } catch (SqlParseException e) { System.out.println(“Error while parsing SQL: “ + e.getMessage()); }对比后进行修改,具体修改逻辑依据 case 异常情况而有所不同。项目进度已完成工作
31个异常 case 全部修复完成并通过 SQLNodeConverterEngineIT,修改涉及的部分如下:
1. MySQL/PostgreSQL/OpenGauss 的 StatementVistor
2. Segment、Expression,如 UnaryOperationExpression、WindowsSegment等
3. ExpressionConverter 及其子类
4. ExpectedExpressionSegment 及其实现
5. Converter 与 Parser 对应的 test case遇到的问题及解决的方案
项目开发过程中,有难有易,可以将问题大致分为以下四类:1. 先前已被修复,仅需增加 test case
2. Parser 语法解析正确,Converter 转换错误
3. Lexer 词法解析正确,Parser 语法解析错误,Converter 相继出错
4. Lexer 词法解析错误,Parser 和 Converter相继出错
接下来,将结合一些修改逻辑较为简单的 case 的修复过程,对每类问题的解决方案进行概括性说明:1. 增加 test case对应 PR:#28311 https://github.com/apache/shardingsphere/pull/28311修复 case:select_where_with_simple_expr_with_not开发过程:增加 test case,在 selection-expression.xml 中增加:<test-cases sql-case–id=“select_where_with_simple_expr_with_not” expected-sql=“SELECT * FROM `t_order` WHERE ! `t_order`.`order_id`” db-types=“MySQL” />2. 修改 Converter 逻辑对应 PR:#27394 https://github.com/apache/shardingsphere/pull/27394/files修复 case:select_with_database_name_and_schema_name_in_table开发过程:修改 SimpleTableConverter 的 convert() 方法,补充 addOwnerNames() 方法递归获取多级 owner,而不限制于两层: @Override public Optional<SqlNode> convert(final SimpleTableSegment segment) { TableNameSegment tableName = segment.getTableName(); List<String> names = new ArrayList<>(); if (segment.getOwner().isPresent()) { addOwnerNames(names, segment.getOwner().get()); } // … } private void addOwnerNames(List<String> names, OwnerSegment owner) { if (null != owner) { addOwnerNames(names, owner.getOwner().orElse(null)); names.add(owner.getIdentifier().getValue()); } }3. 修改 Parser 逻辑对应 PR:#28321 https://github.com/apache/shardingsphere/pull/28321修复 case:select_where_with_simple_expr_with_odbc_escape_syntax开发过程:测试后发现 MySQLStatementVisitor 的 visitRemainSimpleExpr() 方法中没有 odbc 的转义符情况,增加新的情况: if (null != ctx.LBE_()) { return visit(ctx.expr(0)); }4. 修改 Lexer 词法对应 PR:#28319 https://github.com/apache/shardingsphere/pull/28319
修复 case:select_with_json_value_return_type
开发过程:
主要针对 Lexer 部分说明:在 BaseRule.g4 中,由于 regularFunction 在 jsonFunction 前面,而 jsonFunction 的语法规则包含于 regularFunction,导致 jsonFunction 无法被识别。
因此,修改 BaseRule.g4 中的 functionCall:
functionCall : aggregationFunction | specialFunction | jsonFunction | regularFunction | udfFunction ;后续工作安排
回炉重造:检查转换结果1. 安排缘由
在开发的初始阶段,由于自己对项目结构的不熟悉,忽略了对 calcite 的对比使用,因此,在部分 Converter 的修改中,所返回的 SQLNode 可能会存在类型上的差错,由于 getSQL() 方法并不会进行类型检查,所以可能没有被检查出来。2. 后续计划安排
对异常 case 重复方案描述中的开发过程,修改有差错的返回类型。据导师指导,可以在 e2e 中增加完整测试以判断是否正确。长路漫漫:联邦查询完善
1. 安排缘由
开发完成后,导师与我后续沟通中提到如果后续感兴趣,可以继续参加联邦查询的开发,自己本身想要继续投入 Apache ShardingSphere 社区的开发建设,因此将继续投入开发。2. 后续计划安排开启联邦查询:
sqlFederation: sqlFederationEnabled: true executionPlanCache: initialCapacity: 2000 maximumSize: 65535主要的使用场景是 sharding 分片场景下,跨库 join 和一些复杂的子查询,聚合查询。进行测试,查看可优化的查询并进行修改。# 参与全过程与 OSPP 的结缘始于三月份与学长的一次闲聊,当聊起自己项目实践缺乏的困扰时,学长建议我去试试开源之夏。
“听说是和开源社区合作,挺硬核的。”这就是我对 OSPP 的初印象:开源、硬核。
自此,我开始时不时关注 OSPP 的动态,同时,我也告诉了两位小伙伴,他们也在后来成为我申请时的极大助力。但我其实也陷入了一种恐惧中:硬核的项目,我真能扛下来吗?与小伙伴的交流中,我们都有着这样类似的想法:开源,离一位大二学生来说似乎很远。
带着这样的预设心理障碍,我四月初注册的账号到五月中旬都没有交出一份申请。我不止一次焦虑地翻看着长长的项目列表,麻木地盯着一些陌生的名词再呆滞地关闭,现在回想起来,其实是由于自己没有正确评估自身情况进行筛选。开源没有门槛,是我自己为自己设限。
就这样拖延到了五月中旬,看着六月初的 ddl,我和小伙伴都坐不住了,最后还是 ddl 战士的形状。终于我下定决心,想着哪怕被全聚德了也好歹试一次。首先调整了自己的心态,分析了自己的技术栈:优先级最高的是 Java 相关的项目,其次 C++,C, Python 都可以接受,还有这学期刚学的编译原理课用到的 Antlr4(最后起了极其重要的作用)。根据分析结果开始筛选后,我开始疯狂发邮件。
ShardingSphere社区送的马克杯,陪伴自己度过一个个开发夜
offer!
开发的具体内容之前已经做了介绍,我这里着重分享一些开发期间的心态与技巧经验。本次开发我需要去了解三种甚至更多的 SQL,其实在先前我也只是对 MySQL 的基本使用有所了解,所以显然,我需要去了解其他未知领域。在大学两年的拷打后,我深谙 STFW 和 RTFM 的重要性,因此读手册和源码成了我主要的手段。问答社区也是一个主要的获取渠道,当然由于中文转义信息的流失和某些不完善,英文资料的优先级更高些。在开发期间,我也逐步克服了自己的胆怯,这主要体现在我开始积极主动地去和导师交流提问,我也和同社区不同任务的同学在环境配置等方面有所交流,我想这也是一个对我助力很大的方面。总的来说,自己的心态变得不再是因为自己不了解而不敢迈步,而是在自己研究和与人交流后自信前行,前者会完全阻断后续发展,而后者则会互相促进,不断提升自信和能力。在项目的整体安排上,我原本计划是暑假每周完成固定量任务,在 8 月中旬基本完成,预留的时间一方面做缓冲,一方面也是为了做进一步完善。这样的安排源于自己先前对类似 OSPP 类的开源项目的调研,不少分享提到每年都会有不能按时完成、开发中途失踪的情况,我自己想尽量避免这样的情况出现,因此给自己设置了一个相对较紧张的 ddl。计划赶不上变化,我在暑假开始时还有着两周的暑期课程,自己在 5 月初还报名参加了一个长战线的编译器开发比赛。这里想说的是:计划永远要留足变化的余地。我在重新评估了自己的暑假情况后,对计划做了重新调整:首先是暑期课程的两周,因为课程安排很满,每一门课程都有大作业,因此前两周我大概率无法专心开发,但为了不让自己生疏,我会每天抽出小部分时间去继续熟悉开发,对产出的要求不高,能解决多少是多少。而在课程结束后,除了四门课程的大作业开发,还有将持续到八月中后旬的比赛,我的计划是同步进行,为 OSPP 留出至少一个晚上的时间,由于比赛要到广州进行四天的线下决赛,因此比赛期间应该是无法进行开发了,所以我为自己划定在比赛结束前的产出要求。在线下决赛结束后,我将所有精力投入了剩余任务点的开发,最终在八月底完成了所有 PR 的提交,在提交 PR 的同时,我也已经与导师同步进行 review 对接,所以最后在 9.4,我和导师完成了所有 PR 的 review 和修改,并且完成了 18 个 PR 的合并,在开发前一个月完成了开发。回顾整个开发过程,最大的感受就是要学会逼自己。从开始对开源的原生恐惧,到后来安排爆满的不断的计划调整,对我而言都是前所未有的挑战,所幸在最后,在一次次逼自己后,我才知道自己的极限不止于此,也是顺利地完成了整个 OSPP 的开发过程。最后,我要特别感谢我的导师端正强先生在我开发全程中的悉心指导,感谢所有陪我走过这条开发路的人,感谢 Apache ShardingSphere 社区和开源之夏组委会给予我这样一次宝贵的开源经历!# 开源之夏个人随访–项目经历–OSPP:请简单介绍一下自己,并说一下自己的开源经历吧。倪匀博:大家好,我是倪匀博,来自南京大学软件学院的大三学生。我之前没有任何开源的经历,开源在我最开始的想象中遥不可及,真正接触后才发现是自己为它预设了很高的门槛。OSPP:这是你第一次参加开源之夏活动,体验如何?最开始是如何了解到并决定参与开源之夏活动的?又是如何选择项目、与导师建立沟通、准备项目申请书的?倪匀博:可以用自己的总结博客里出现数次的词来概括:值得,带给自己一次硬核不水的履历。相对于自己先前参加的各种工程项目比赛,离真实的工业界开发更近,一对一的导师制度,活跃的社区氛围,对自己而言都是一次全新体验。最早是在与学长的一次交谈结识开源之夏,当时自己迫切希望一份足够硬核的履历,学长因此向我强烈推荐了开源之夏。决定参与是在自己进一步了解开源之夏后,自己当时对于一段贴近工业界的实践经历有着强烈的渴望,当看到开源之夏繁多的社区与丰富的项目列表后,我确定可能我找到了。在选择项目时,我自己也经历过纠结,刚开始没有清晰自己的定位,随意地翻找着不符合自己技术栈的项目列表,产生了不配的恐慌。后来根据自己擅长的技术检索,并且有优先级地进行逐级筛选,最终锁定了几个社区。确定好自己心仪的社区后,我试着去发邮件联系导师,在邮件中写清楚自己与项目适配的优势所在,也表达了自己对社区和开源的兴趣。在与现在的导师沟通过程中,我也去完成了社区的一些 good first issue 并提交 PR 被成功合并。由于自己申请的整体时间较晚,这些都是在与导师联系后才做的,我想如果准备时间充足,最好可以先去自己心仪的社区内解决一些基本的 issue,能在这样的过程中对社区整体更了解,更利于做出最后的选择。毕竟开源也不只是限制于 OSPP 2023,多多参与能让自己在开源之路上走得更远。最后准备项目申请书参考了官方模板,并且将自己整个思考和准备的流程清晰地表达出来,包括对于背景的调研,一些相关文献文档的阅读和总结,以及一些对于任务的初尝试和预备计划,这些都能让导师更加确定你有完成这个任务的能力。这里值得一提的是,整个申请流程我是与身边的两个小伙伴结伴完成的,也参考了很多平台的经验分享,这些都给我的申请助力不少。OSPP:你是如何在紧凑的暑假安排完成甚至提前完成项目开发任务的?有什么方法或tips可以分享给大家么?倪匀博:我想提前完成的关键在于安排的灵活变化,因为我们暑期学校的报名确认都较晚,所以在六月初申请结束时,我并不知道暑期课程与其大作业需要的时间,同时我还有另一个从五月份持续到八月中旬的比赛,我因此预留了时间给这部分不确定事件。当开发真正开始后,我才确定我的暑校会持续满满当当的两周,伴随而来的是四门大作业,参加的另一项比赛也需要持续的开发和去广州线下决赛四天,我在暑假开始时就花了半天时间规划整个开发计划,核心是将时间切块,一段时间只专注一件事,并且严格遵循自己的开发计划。当然,我曾经也有过不少拖延的案底,所以我逼自己在八月底前完成全部开发,自己为自己设限,同时这也能为我可能出现的意外情况预留一个月的缓冲时间。开发过程中,因为我有着不同的任务点,所以在每完成部分任务点后,我会和导师及时沟通进行 code review,一方面分摊了导师 review 的压力,另一方面也是在自己最为熟悉改动的时候进行二次更改,效果更好。最后,在紧凑的时间中,我也按时完成了所有任务,并且在九月初全部完成合并,也是将到十月结束前的整个计划都提前完成了。