共计 3560 个字符,预计需要花费 9 分钟才能阅读完成。
引言
类型零碎是数据库的一个重要组成部分,它提供了一种统一的形式来确定 SQL 中的数据类型。类型零碎的设计很大水平影响数据库的易用性和健壮性,一个设计正当且统一的类型零碎容易让使用者判断 SQL 的行为。反之,一个没有通过正式设计的类型零碎会带来各种暗坑和不统一行为在暗中背刺用户。咱们用编程语言举个例子,JavaScript 被诟病的类型零碎总是成为茶余饭后的谈资:
因而咱们心愿在 Databend 中实现一个易于了解而又功能强大的类型推导零碎,为此咱们借鉴了不少优良编程语言的编译器外部设计,而后从中精简出实用于 SQL 应用的子集。下文将会具体开展介绍这个零碎的设计原理。
接口设计
“ 低耦合高内聚 ” 是咱们常常说的口头禅,讲的是要把做雷同事件的代码归拢到一起,而后定义简略的接口供内部应用。类型推导作为一个绝对简单的零碎,在设计之初须要先定义好对外裸露的接口,也即能做什么以及内部怎么应用。
简略来说,咱们设计的类型推导零碎能够做三件事:
- 输出 SQL 文本(
RawExpr
),查看 SQL 是否合乎类型规定,为函数调用抉择适合重载,返回可执行的表达式 (Expr
)。 - 输出可执行的表达式和数据,执行而后返回后果。
- 输出可执行的表达式和数据取值范畴(存储在元数据中),返回后果的取值范畴。
为此调用者只需:
- 定义所有可用函数的类型签名、函数定义域到值域的映射、函数执行体。
- 在执行 SQL 或 constant folding 时调用执行器。
用布尔 and
函数举个例子,函数定义大抵如下:
registry.register_2_arg::<BooleanType, BooleanType, BooleanType, _, _>(
"and",
FunctionProperty::default(),
|lhs, rhs| {
Some(BooleanDomain {
has_false: lhs.has_false || rhs.has_false,
has_true: lhs.has_true && rhs.has_true,
})
},
|lhs, rhs| lhs && rhs,
);
一个残缺执行的例子:
// 将 SQL 表达式文本转为结构化 AST
let raw_expr = parse_raw_expr("and(true, false)");
// 获取内置函数,比方之前的 `and` 函数
let fn_registry = builtin_functions();
// 查看类型合法性
let expr = type_check::check(&raw_expr, &fn_registry).unwrap();
// 执行
let evaluator = Evaluator {input_columns: Chunk::new(vec![]),
context: FunctionContext::default(),};
let result: Value<AnyType> = evaluator.run(&raw_expr).unwrap();
assert_eq!(result, Value::Scalar(Scalar::Boolean(false)));
类型推导原理
新的类型零碎反对以下数据类型:
Null
Boolean
String
UInt8
UInt16
UInt32
UInt64
Int8
Int16
Int32
Int64
Float32
Float64
Date
Interval
Timestamp
Array<T>
Nullalbe<T>
Variant
咱们以一个例子看看类型推导零碎是如何工作的,假如内部输出了一个表达式:
1 + 'foo'
类型推导器首先会将表达式转换为函数调用:
plus(1, 'foo')
而后类型查看器能够简略地推断出常量的类型:
1 :: Int8
'foo' :: String
通过查问 FunctionRegistry,类型查看器得悉函数 plus 有这些重载:
plus(Null, Null) :: Null
plus(Int8, Int8) :: Int8
plus(Int16, Int16) :: Int16
plus(Int32, Int32) :: Int32
plus(Float32, Float32) :: Float32
plus(Timestamp, Timestamp) :: Timestamp
咱们能够发现,函数 plus 参数类型 Int8 和 String 不能匹配其中任何一个重载,因而类型查看器会返回一个错误报告:
1 + 'foo'
^ function `plus` has no overload for parameters `(Int8, String)`
available overloads:
plus(Int8, Int8) :: Int8
plus(Int16, Int16) :: Int16
plus(Int32, Int32) :: Int32
plus(Float32, Float32) :: Float32
plus(Timestamp, Timestamp) :: Timestamp
但在类型查看中咱们容许一种例外,咱们容许子类型转换为父类型(CAST),这样就能够让函数承受子类型的参数。咱们看这样一个例子:
plus(1, 2.0)
类型推导器依据规定推导出常量的类型:
1 :: Int8
2.0 :: Float32
通过查问 FunctionRegistry,咱们发现函数 plus 有两个重载看似能够应用但又不齐全匹配:
(Int8, Int8) :: Int8
plus(Float32, Float32) :: Float32
这时类型查看器会尝试启用 CAST 规定尽最大可能抉择一个重载。依据 CAST 规定,Int8 能够无损转化成 Float32,因而类型查看器会改写表达式构造而后从新查看类型:
plus(CAST(1 AS Float32), 2.0)
这样就能顺利通过类型查看了。
泛型
新的类型查看器反对在函数签名定义中蕴含泛型,用来缩小须要手动定义的重载函数的数量。比方咱们能够定义一个函数 array_get<T0>(Array<T0>, UInt64) :: T0
,它承受一个数组和一个下标,并返回数组中下标对应的元素。
相比上一节中讲到的类型查看,查看含有泛型签名的函数多了一个步骤:抉择一个适合的具体类型替换泛型,替换后的类型须要能够通过类型查看,如果不存在这样的具体类型则返回阐明起因(比方有抵触的束缚)。这个步骤个别称为 Unification,咱们也用一个例子加以阐明:
假如有两个表达式,它们的类型别离是:
ty1 :: (X, Boolean)
ty2 :: (Int32, Y)
如果咱们须要 ty1
和 ty2
领有雷同类型(比方 ty1
是入参表达式类型,ty2
类型是入参签名),unify
会尝试将 X
和 Y
替换为具体类型:
let subst: Subsitution = unify(ty1, ty2).unwrap();
assert_eq!(subst['X'], DataType::Int32]);
assert_eq!(subst['Y'], DataType::Boolean]);
对 unify
有趣味的读者能够浏览 type_check.rs
源码。在此举荐一本好书《Types and Programing Languages》,其中论述了编程语言的类型推导倒退历史,深刻探讨剖析各种推导实践的原理和取舍,各个重要概念都有配套的 toy implementation 作为例子,十分值得失眠时浏览。
总结
本文简述了新类型零碎的设计背景,介绍了运行原理和执行器应用办法。因为篇幅关系没有深刻介绍定义 SQL 函数的办法,那局部将会类型查看器一样精彩还蕴含不少 Rust 类型黑魔法,咱们下次有机会再唠。
对于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也能够做实时剖析的旧式数仓。期待您的关注,一起摸索云原生数仓解决方案,打造新一代开源 Data Cloud。
- Databend 文档:https://databend.rs/
- Twitter:https://twitter.com/Datafuse_…
- Slack:https://datafusecloud.slack.com/
- Wechat:Databend
- GitHub:https://github.com/datafusela…
文章首发于公众号:Databend