类型有什么作用, 类型可以提供编译期检查, 避免到运行期才报错.

类型

首先Flink中自己定义了一套类型, 有LogicalTypeDataType两个表示

LogicalType

LogicalType表示的逻辑类型, 并不涉及类型的物理表示, 会包含nullable属性.

还有两个衍生的概念 LogicalTypeFamilyLogicalTypeRoot

LogicalTypeRoot表示的LogicalType的元类型, 例如TIMSTAMP类型可以包含多种精度, 但是多种精度的TIMESTAMP(3)``TIMSTAMP(6)都属于 TIMESTAMP_WITHOUT_TIME_ZONE root类型. 一个LogicalType只属于一个LogicalTypeRoot

LogicalTypeFamily表示的是LogicalTypeRoot的归类, 一个LogicalTypeRoot可以同时属于多个LogicalTypeFamily

例如 这就表示 DECIMAL属于 PREDEFINED, NUMERIC, EXACT_NUMERIC的分类.

DECIMAL(
LogicalTypeFamily.PREDEFINED,
LogicalTypeFamily.NUMERIC,
LogicalTypeFamily.EXACT_NUMERIC),

DataType

DataType就在LogicalType之上添加了类型的物理表示, 可以看到他的成员变量就是由 logicalType + conversionClass组成, conversionClass表示Flink该怎么去读写这个类型.

他主要用在和外部系统交互的时候, 比如我定义一个TIMSTAMP类型, 我可以用java.sql.Timestamp表示, 也可以用 java.time.LocalDateTime来表示, 这时候就可以通过显示的指定 conversionClass来实现, 如果不指定, 就使用LogicalType中的默认的conversionClass.

protected final LogicalType logicalType;

protected final Class<?> conversionClass;

例如在表示Row类型时, Flink内部都是表示为RowData, 并且内部的String都是表示为StringData, 那么通过LogicalTypeUtils#toInternalConversionClass就可以将相应的conversionClass转化成内部的表示. 并 DataType#bridgeTo用来修改此DataType的物理类型表示.

当然并不是随便bridge一个类型就能生效, 他会通过LogicalType supportsInputConversionsupportsOutputConversion来校验你设置的conversionClass在不在这个范围之内

比如 VarcharType所支持的输入输出类型是

String.class.getName(), byte[].class.getName(), StringData.class.getName()

主要需要校验的点就是Source/LookupJoin/Sink/UDF 这些和外部用户定义类型需要交互的地方.

在 DynamicTableSource.Context中的 createDataStructureConverter就是将外部系统中的数据转化成内部的数据, 而这里首先就需要表示这些外部数据类型的conversionClass是满足supportsInputConversion的 然后通过DataStructureConverters.getConverter(producedDataType)创建外部Object到内部类型的映射, 这样外部Java类型系统就和SQL中的RowData关联上了.

举个例子

putConverter(
LogicalTypeRoot.VARCHAR, String.class, constructor(StringStringConverter::new));
putConverter(
LogicalTypeRoot.VARCHAR, byte[].class, constructor(StringByteArrayConverter::new));
putConverter(LogicalTypeRoot.VARCHAR, StringData.class, identity());

这里对于VARCHAR类型, 如果你在插件端中的实际Java object类型是String.class那么就通过StringStringConverter转化成内部的StringData类型. 如果是byte[].class那通过StringByteArrayConverter来转化, 输出侧也是一样.

对于UDF呢也是如此, 不过是通过Codegen的方式来生成converter, 将用户UDF中的外部类型, 转化成内部类型. 如果用户类型是非标准SQL的类型, 也支持指定成RAW来表示成 RawValueData

RowData

RowData是Flink内部的数据类型, 可以看到上面, toInternal转化的时候就是要将外部的ROW 转成RowData, 他和SQL类型的映射关系, 这可以理解成数据类型的内部物理表示层. 内部SQL算子, Codegen代码都是面向RowData编程, 而RowData 也有多种实现有基于Object的GenericRowData, 有基于二进制表示的BinaryRowData, 也有基于列式视图的 ColumnarRowData

 * +--------------------------------+-----------------------------------------+
* | SQL Data Types | Internal Data Structures |
* +--------------------------------+-----------------------------------------+
* | BOOLEAN | boolean |
* +--------------------------------+-----------------------------------------+
* | CHAR / VARCHAR / STRING | {@link StringData} |
* +--------------------------------+-----------------------------------------+
* | BINARY / VARBINARY / BYTES | byte[] |
* +--------------------------------+-----------------------------------------+
* | DECIMAL | {@link DecimalData} |
* +--------------------------------+-----------------------------------------+
* | TINYINT | byte |
* +--------------------------------+-----------------------------------------+
* | SMALLINT | short |
* +--------------------------------+-----------------------------------------+
* | INT | int |
* +--------------------------------+-----------------------------------------+
* | BIGINT | long |
* +--------------------------------+-----------------------------------------+
* | FLOAT | float |
* +--------------------------------+-----------------------------------------+
* | DOUBLE | double |
* +--------------------------------+-----------------------------------------+
* | DATE | int (number of days since epoch) |
* +--------------------------------+-----------------------------------------+
* | TIME | int (number of milliseconds of the day) |
* +--------------------------------+-----------------------------------------+
* | TIMESTAMP | {@link TimestampData} |
* +--------------------------------+-----------------------------------------+
* | TIMESTAMP WITH LOCAL TIME ZONE | {@link TimestampData} |
* +--------------------------------+-----------------------------------------+
* | INTERVAL YEAR TO MONTH | int (number of months) |
* +--------------------------------+-----------------------------------------+
* | INTERVAL DAY TO MONTH | long (number of milliseconds) |
* +--------------------------------+-----------------------------------------+
* | ROW / structured types | {@link RowData} |
* +--------------------------------+-----------------------------------------+
* | ARRAY | {@link ArrayData} |
* +--------------------------------+-----------------------------------------+
* | MAP / MULTISET | {@link MapData} |
* +--------------------------------+-----------------------------------------+
* | RAW | {@link RawValueData} |
* +--------------------------------+-----------------------------------------+

TypeInformation

Flink还有一套比较早的类型系统 基于TypeInfomation, 用于兼容较早的Source/Sink 以及UDF的类型接口, 通过LegacyTypeInfoDataTypeConverter将TypeInfomation转化成新的DataType来使用.

RelDataType

以上这三种类型都是Flink中的类型体系, 最终Flink还是通过Calcite来进行sql validate, 而Calcite中的类型是RelDataType, 所以需要有这样的一个映射关系. 通过FlinkTypeFactory#createFieldTypeFromLogicalType可以将LogicalType转化成RelDataType

Validate

首先在validate阶段, calcite系统就会去推断各个字段, 函数入参, 返回值的类型, 来进行语法的校验.

表的schema类型

flink中的catalog和Calcite的CalciteSchema通过CatalogManagerCalciteSchema来打通, 将CatalogManager封装到SimpleCalciteSchema中, 所以最后的表的查找都会通过 FlinkCalciteCatalogReader#getTable路由到

CatalogManagerCalciteSchema -> CatalogCalciteSchema -> DatabaseCalciteSchema 最终是从Catalog中查找到相应的表(CatalogSchemaTable) 这个是Flink Catalog table 和Calcite Table的中间桥接.

而此时就需要将Flink Catalog table 的表类型 (DataType) 转化成 Calcite中的类型RelDataType. 在CatalogSchemaTable#getRowType中就会完成这一转化, 最终源表产出的是一个ROW struct type.

final List<String> fieldNames = schema.getColumnNames();
final List<LogicalType> fieldTypes =
schema.getColumnDataTypes().stream()
.map(DataType::getLogicalType)
.map(PlannerTypeUtils::removeLegacyTypes)
.collect(Collectors.toList());
return flinkTypeFactory.buildRelNodeRowType(fieldNames, fieldTypes);

函数定义

基于Calcite SqlFunction的定义

首先Flink从Calcite中集成了很多函数定义例如在 FlinkSqlOperatorTable

public static final SqlOperator AND = SqlStdOperatorTable.AND;
public static final SqlOperator AS = SqlStdOperatorTable.AS;
  • SqlOperandTypeInference推断入参的类型 他的入参是 SqlCallBinding, RelDataType returnType, RelDataType[] operandTypes (这个入参都是unknown, 需要实现者去填充). InferTypes内置入参类型推断

  • SqlReturnTypeInference推断函数的返回类型. 入参是SqlOperatorBindingbinding实际上就是把校验时的上下文, 如入参类型, 入参个数, 入参的常量提供给你, 方便你去做类型推断.Calcite ReturnTypes和 FlinkReturnTypes提供了很多预置的返回类型推断的实现

  • SqlOperandTypeChecker 检查入参类型 个数等等是否合法

  • 例如我写一个 SELECT A and B from myTable 测试a和b不是boolean类型. 首先在validate的过程中需要推断SELECT列表的类型, A and B 是一个 SqlOperator 首先会调用上面的SqlOperandTypeInference推导参数类型. 对于AND操作, 推导入参都是boolean

    然后会根据每一个SqlNode#deriveType 推导其返回类型. 而递归查找的过程中最后会查找到source table schema 中找到相应的字段推断出A和B分别是什么类型. 所以感觉第一步的SqlOperandTypeInference并没有什么用 ?

    最后推导出入参类型之后会通过SqlOperandTypeChecker校验入参类型是否符合预期.

    因此当A和B类型不是Boolean时, 通过Checker就会校验报错

基于BuiltInSqlFunction接口

里面所使用的接口其实还是前面 Calcite里面定义的几种推导和校验的接口. BuiltInSqlFunction的接口的主要作用是承接一些 BuiltInFunctionDefinition 不支持的定义方式. 作者希望的主要内置函数的入口还是第三种

基于BuiltInFunctionDefinition

这个就是Flink自己完全新定义了一套类型的dsl. 如 InputTypeStrategy, OutputTypeStrategy. 这个构建出来的Function并不是直接的Calcite的SqlFunction. 最终这些函数都会注册到FunctionCatalogOperatorTable中. 而默认的Calcite初始化的OperatorTable就是 FunctionCatalogOperatorTableFlinkSqlOperatorTable

return SqlOperatorTables.chain(
new FunctionCatalogOperatorTable(
context.getFunctionCatalog(),
context.getCatalogManager().getDataTypeFactory(),
typeFactory),
FlinkSqlOperatorTable.instance());

所以查找函数的时候会首先去FunctionCatalogOperatorTable new stack中查找. 查找到的BuiltInFunctionDefinition会通过convertToSqlFunction转化成Calcite的接口. 最终转化成为BridgingSqlFunction, BridgingSqlAggFunction

基于CallExpression

  • CallExpresssion的定义包含了函数的主体 FunctionDefinition, 参数 ResolvedExpression

    • 这一套接口主要可以透传到插件端, 比如Filter下推时下推的就是ResolvedExpression , 以及Watermark表达式
    • 以及在Java scala的table api [[ImplicitExpressionOperations]]中提供dsl api
  • 最后这套CallExpression会通过一系列的转化规则FunctionDefinitionConvertRule DirectConvertRule等等转化成Calcite的RexNode, 用于在Plan阶段将这些边缘节点(用户交互界面)的Expression编译成RexNode. 里面的转化逻辑实际上和上面的三种基本映射

自定义函数

  • 注册用户自定义函数的入口是TableEnvironmentImpl#createSystemFunction 最终实例化成FunctionDefinition
  • 类型校验则是通过 ScalarFunction#getTypeInference 来完成的, 用户实际上也可以覆写这个方法来完成类型的校验, 他也提供了一个默认的实现. 这个默认的实现就是一套基于函数hint的类型推导. FunctionMappingExtractor 完成对自定义函数的分析, 如入参类型, 返回值, 函数名称等等. 这里面用户就可以通过DataTypeHint 来对参数和返回值进行标记. 完成提取后, 用户参数的校验逻辑就和前面的 BuiltInFunctionDefinition 一致了

函数实现

  • 函数实现分为两种Old stack中, 对于Calcite中定义的函数, Flink提供运行时实现基本是通过Codegen代码来生成的, 所以之前新增一个函数比较繁琐, 需要修改Codegen的逻辑, 主要涉及的地方就是 ExprCodeGenerator
  • 而针对New stack的函数, 一般只需要开发者提供一个runtime class, 通过 BridgingSqlFunctionCallGen 完成函数实现的注入.

时间属性类型

  • 时间属性主要是用作窗口时间的特殊标记, 标记是proctime或者eventime. 所以有一种特殊的类型叫做TimeIndicatorRelDataType  这里主要是在源表定义的地方会对时间属性字段做特殊处理插入相应的TimeKind.
  • 需要注意的一点是针对时间属性字段需要做一些物化处理. 在 RelTimeIndicatorConverter 完成 例如
    • 聚合参数或者group维度 中的时间属性字段
    • Sink节点下发前
    • Calc计算节点

FlinkSQL类型系统的更多相关文章

  1. javascript中15种原生对象类型系统综述

    前面的话 在编程语言中,能够表示并操作的值的类型称做数据类型,编程语言最基本的特性就是能够支持多种数据类型.javascript拥有强大的类型系统,主要包括原生对象.宿主对象和浏览器拓展对象,本文主要 ...

  2. TypeScript - 基本类型系统

    对于程序来说我们需要基本的数据单元,如:numbers, strings, structures, boolean 等数据结构.在TypeScript中我们支持很多你所期望在JavaScript中所拥 ...

  3. 基于类型系统的面向对象编程语言Go

    (整理自网络) 面向对象编程 Go语言的面向对象编程(OOP)非常简洁而优雅.说它简洁,在于它没有了OOP中很多概念,比如:继承.虚函数.构造函数和析构函数.隐藏的this指针等等.说它优雅,是它的面 ...

  4. 04.C#类型系统、值类型和引用类型(二章2.2-2.3)

    今天要写的东西都是书中一些概念性的东西,就当抄笔记,以提问对话的方式将其写出来吧,说不定以后面试能有点谈资~~~ Q1.C#1系统类型包含哪三点特性? A1.C#1类型系统是静态的.显式的和安全的. ...

  5. javascript类型系统之Array

    原文:javascript类型系统之Array 目录 [1]数组创建 [2]数组操作 [3]继承的方法 [4]实例方法 数组转换 数组检测 栈和队列 排序方法 操作方法 位置方法 前面的话 数组是一组 ...

  6. .NET Framework 中的类型系统的两个基本点

    它支持继承原则. 类型可从称为基类型的其他类型派生. 派生类型继承基类型的方法.属性和其他成员(存在一些限制). 之后,基类型可从某些其他类型派生,这种情况下,派生类型继承其层次结构中这两个基类型的成 ...

  7. 《InsideUE4》UObject(三)类型系统设定和结构

    垃圾分类,从我做起! 引言 上篇我们谈到了为何设计一个Object系统要从类型系统开始做起,并探讨了C#的实现,以及C++中各种方案的对比,最后得到的结论是UE采用UHT的方式搜集并生成反射所需代码. ...

  8. c#1所搭建的核心基础之类型系统的特征

    类型系统的特征简介 几乎每种编程语言都有某种形式的一个类型系统.类型系统大致被分为:强/弱,安全/不安全,静态/动态,显式/隐式等类型. c#在类型系统世界中的位置 c#1的类型系统是静态的.显式的和 ...

  9. Haskell 笔记(三)类型系统

    类型 (Type) Haskell的类型系统式静态类型系统,在编译的时候就知道数据类型,所以不同类型的值运算在编译的时候就会报错,比如用布尔值和整数运算,在C语言中这种运算就不会报错. Haskell ...

  10. 《InsideUE4》UObject(四)类型系统代码生成

    你想要啊?想要你就说出来嘛,你不说我怎么知道你想要呢? 引言 上文讲到了UE的类型系统结构,以及UHT分析源码的一些宏标记设定.在已经进行了类型系统整体的设计之后,本文将开始讨论接下来的步骤.暂时不讨 ...

随机推荐

  1. 案例分享-full gc导致k8s pod重启

    在之前的记一次k8s pod频繁重启的优化之旅中分享过对于pod频繁重启的一些案例,最近又遇到一例,继续分享出来希望能给大家带来些许收获. 问题现象 报警群里突然显示某pod频繁重启,我随即上去查看日 ...

  2. vue【解决方案】页面/路由跳转后,滚动条消失,页面无法滚动

    原因解析: vue项目中,页面/路由跳转后,body 的内联样式变成 overflow:hidden 解决方案: 使用路由守卫,在页面/路由跳转后,将body 的overflow设置为auto src ...

  3. 2022-01-06:N个结点之间,表世界存在双向通行的道路,里世界存在双向通行的传送门. 若走表世界的道路,花费一分钟. 若走里世界的传送门,不花费时间,但是接下来一分钟不能走传送门. 输入: T为

    2022-01-06:N个结点之间,表世界存在双向通行的道路,里世界存在双向通行的传送门. 若走表世界的道路,花费一分钟. 若走里世界的传送门,不花费时间,但是接下来一分钟不能走传送门. 输入: T为 ...

  4. y总算法基础课+算法提高课+算法进阶课超全模板

    y总超全算法模板 y总模板自取 喜欢的可以点个赞支持一下^-^ 模板展示

  5. 鼠标移入select options会触发mouseleave 事件处理方案

    近来遇到一项目有一侧边工具菜单,在鼠标mouseenter事件打开对应的详细操作列表,当mouseleave时进行关闭,然操作列表中有一个select , 每当鼠标移入select options 时 ...

  6. 拒绝conda, 用virtualenv构建多版本的python开发环境

    本文章转载自公众号 "生信码农笔记(ID:bio-coder)",已获得原作者授权. 1. 不喜欢用 conda 特别不喜欢 bioconda, miniconda, Anacon ...

  7. C# 版本特性一览

    前言 使用 C# 作为开发语言已经 15 个年头了,受惠于 C# 的不断更新,伴随着大量的新特性与大量语法糖,让我更加容易写出简洁.高效的代码.日常中大量特性早已信手拈来,当然从未尝试过的特性更是难以 ...

  8. Hyperledger Fabric系统链码介绍

    在Hyperledger Fabric中,LSCC(Lifecycle System Chaincode).CSCC(Chaincode System Chaincode).QSCC(Query Sy ...

  9. Python 九九乘法表的多种实现方式

    简介 九九乘法表是初学者学习编程的必要练手题目之一,因此各种语言都有对应的实现方式,而 Python 也不例外.在 Python 中,我们可以使用多种方式来生成一个简单的九九乘法表. 本文共介绍了七种 ...

  10. 【Netty实战】1~3章学习笔记

    1. Netty总体结构 1.1 Netty简介 ​ Netty是一款用于创建高性能网络应用程序的高级框架.它的基于 Java NIO 的异步的和事件驱动的实现,保证了高负载下应用程序性能的最大化和可 ...