Velox Type System
Velox 的类型系统把查询数据的逻辑含义与执行时的值表示联系起来。Operator 的输入输出 schema、Vector 的 type()、函数签名中的参数与返回类型描述逻辑类型;内部按物理表示选择存储和访问实现,再用 C++ 值类型执行具体计算。 逻辑类型、物理类型和 C++ 值类型分别回答:数据是什么意思、采用哪种基础表示、实现中用什么类型承载和操作值。
例如一列 DATE:逻辑类型是 DATE(),物理类别是 TypeKind::INTEGER,C++ 值类型是 int32_t。Vector 携带这份逻辑类型描述和一批值,因此 vector->type() 返回 DATE 类型,vector->typeKind() 返回 INTEGER;函数体可以用 int32_t 操作日期值,结果 Vector 仍按已确定的逻辑返回类型构造。
C++ 值类型是物理表示的实现类型,不能与 Velox 的 physical type 直接画等号。 VARCHAR 和 VARBINARY 是不同的 physical kind,但它们的 NativeType 都是 StringView。而内部计算仍会使用逻辑类型信息:DECIMAL 加法需要根据 scale 对齐整数,某些类型的比较与 hash 也必须遵守其逻辑语义。仅知道物理表示,并不足以确定运算规则。
这些职责通过同一套 Type 类体系连接起来。用于 schema 时,Type 对象描述逻辑类型,同时提供物理 kind;函数元数据也会用基础 Type 对象描述物理签名,例如用 INTEGER() 表示 Date 实现所需的物理类型。DateType 继承 IntegerType 来复用 INTEGER 表示,但 DATE 与 INTEGER 并不逻辑等价。因而不能仅凭 TypePtr 或某个 C++ 基类名,就断定某处讨论的是逻辑类型还是物理类型。
一、类型体系与使用边界
1.1 Velox 类型系统描述什么
Velox Type System 是查询执行层的数据类型体系。它描述一列数据的值域、语义和结构,并把这些描述连接到实际的存储与计算实现。输入数据的 schema、表达式的参数和返回类型、算子的输出 schema,都使用这套体系表达。
它支持 BOOLEAN、整数、浮点数、字符串、时间等标量类型,也支持由子类型递归组成的 ARRAY、MAP 和 ROW。DATE、DECIMAL 等类型还有额外的语义或参数:DATE 表示日期,DECIMAL 的 precision 和 scale 决定十进制数的范围与小数位数。由此构成的 schema 可以用于判断函数是否接受一列数据、表达式返回什么类型,以及一批输出是否满足下游要求。
类型描述与数据分开保存。一列金额的类型可以是 DECIMAL(12,2),这一批中的值可以是 123.45、6.78 或 null;换一批值仍然可以使用相同的类型描述。类型本身不保存这些行,也不决定这一批数据有多少行。
Velox 的运行时类型描述以 Type 对象为核心。用于 schema 时,一个具体 Type 对象描述逻辑类型,包含其逻辑身份、参数和子类型,并提供底层物理表示的类别。源码也用基础 Type 对象构造物理签名,例如用 INTEGER() 表示 Date 函数实现要求的物理类型。因此,“Velox type 是逻辑还是物理”要落实到用途:schema 中的 Type 表达逻辑类型,函数的 physical signature 中的 Type 表达物理类型;两者复用同一套 Type 类体系。
1.2 Logical type、physical type、C++ type 的定义
逻辑类型(logical type)定义数据的含义和可采用的运算语义。 BIGINT 是整数;DATE 是日期;DECIMAL(12,2) 是具有给定精度和小数位的十进制数;ARRAY(DATE) 规定数组元素是日期。对复杂类型,它还包括子类型以及 ROW 的字段结构。函数签名、表达式类型和 schema 需要这一层信息。
Velox 的物理类型(physical type)描述执行侧使用的基础表示。 对标量,它主要由 TypeKind 标识:DATE 的物理类型是 INTEGER,短 DECIMAL 是 BIGINT,长 DECIMAL 是 HUGEINT。对复杂类型,还要递归描述子类型的物理表示,例如 ARRAY(DATE) 对应的物理类型树是 ARRAY(INTEGER)。顶层 TypeKind::ARRAY 本身并不能描述元素的表示。源码既使用 TypeKind 做分派,也使用 TypePtr 保存这样的物理类型树。
C++ 值类型是承载或访问一个值时,C++ 实现实际使用的类型。 INTEGER 通常对应 int32_t,BIGINT 对应 int64_t,TIMESTAMP 对应 Timestamp,字符串值通常通过 StringView 访问。复杂值可以通过 Array/Map/Row 的 view 访问,输出则使用相应 writer。C++ 编译器据此确定参数类型、运算和访问方式。
本文说的“C++ type”主要指这一层的值类型或访问接口。Type 自身当然也是一个 C++ 类,TypeKind 也是用 C++ enum 实现的;但它们分别是描述类型的对象和表示类别的标签,与装载一条 SQL 值的 int64_t 不属于同一个讨论层次。
| 逻辑类型 | Velox physical kind | 常用 C++ 值表示 | 仅凭该 C++ 表示会丢失什么 |
|---|---|---|---|
| INTEGER | INTEGER | int32_t |
无法判断同表示的值是否其实具有日期等语义 |
| DATE | INTEGER | int32_t |
日期身份、以天为单位的解释 |
| BIGINT | BIGINT | int64_t |
无法与基于 BIGINT 的其他逻辑类型区分 |
| DECIMAL(12,2) | BIGINT | int64_t |
decimal 身份、precision=12、scale=2 |
| DECIMAL(30,6) | HUGEINT | int128_t |
decimal 身份、precision=30、scale=6 |
| VARCHAR | VARCHAR | StringView |
文本语义 |
| VARBINARY | VARBINARY | StringView |
字节串语义 |
| TIMESTAMP | TIMESTAMP | Timestamp |
需要结合具体时间类型与方言规则解释 |
例如整数 12345,按 BIGINT 解释就是 12345,按 DECIMAL(12,2) 解释是 123.45。两者可以用相同的 int64_t 承载,但函数匹配、格式化和与另一列相加时需要的语义信息不同。
1.3 三者的层级关系:语义、基础表示、语言实现
三者形成从数据语义到实现的抽象关系:逻辑类型选择一种基础表示,基础表示关联到 C++ 值类型或访问实现。 这条关系中的箭头表示表示方式的关联,不表示数据依次经历三个处理阶段,也不表示三个 C++ 类之间存在继承关系。
这个关联通常是多对一的。DATE 与 INTEGER 都可以进入 INTEGER backing;BIGINT 与多种短 decimal 都可以进入 BIGINT backing。进一步,VARCHAR 与 VARBINARY 虽然是不同 kind,却都关联到 StringView。因此 physical kind 也不能简单定义为“C++ 类型名”或“占几个字节”。
向下只保留 kind 或 C++ 值类型,会减少类型信息;从 int64_t 无法反推它原来是 BIGINT、DECIMAL(12,2) 还是另一种基于 BIGINT 的逻辑类型。完整 Type 需要在使用这些值的地方继续存在。
Type 的子类不能按“全是逻辑类型”或“全是物理类型”分类。 Type 是统一的类型描述接口;TypeBase<KIND>、CanProvideCustomComparisonType<KIND> 是实现基类,本身不对应一项具体 SQL 类型。IntegerType、DateType、ArrayType 等具体类实现可实例化的类型描述。它们的使用关系是:
- 基本类型的逻辑名和物理名相同。
INTEGER()描述 SQL 逻辑 INTEGER,它的物理 kind 也是 INTEGER。IntegerType并不是只允许用于“物理类型”的专用类。 - 另一些逻辑类型在基础表示上增加语义。
DATE()是DateType对象;DateType继承IntegerType,逻辑名改为 DATE,继承来的物理 kind 仍为 INTEGER。它没有一个指向独立 PhysicalType 对象的成员。 - 基础 Type 还可以被用于物理描述。 Date UDF 的元数据保存
physicalType = INTEGER(),此处这个IntegerType对象表示实现需要 INTEGER backing;实际输入 Vector 的逻辑类型仍然是DATE()。 - 复杂逻辑类型由对象树表达。
ARRAY(DATE())和ARRAY(INTEGER())的外层对象都是ArrayType,差别在保存的 child Type。后者既可以是“整数数组”的逻辑类型,也可以被用作“日期数组”的物理签名。
所以这里存在两种关系:逻辑类型与物理表示是多对一的表示关联;C++ 类之间则通过继承和对象组合复用实现。 这两种关系不等同。尤其是 DateType : IntegerType 不代表 SQL DATE 等价于 INTEGER,也不意味着所有接受 INTEGER 的 SQL 函数都接受 DATE。第 2.1 节会展开真实继承链、构造函数与对象内容。
此外还要区分两条独立的关系。第一条是类型树:ARRAY 有元素类型,MAP 有 key/value 类型,ROW 有字段类型。第二条是batch 布局:Flat、Dictionary、Constant、Lazy 等编码,以及 values、indices、null bitmap、offsets 和 children。一列 DATE 可以采用不同 Vector 编码;变更编码并不必然改变日期语义。类型树、抽象层级和数据布局在后文分别展开。
1.4 Function、Vector、Operator 分别用哪一层
Vector 的 type()、表达式的类型和 Operator 的输入输出 schema 使用逻辑类型;typeKind() 暴露物理类别;值访问和计算使用 C++ 值类型。Function 还显式保存一份物理签名,用于检查 C++ 实现能否处理已绑定的逻辑类型。 下面按实际 API 分开列出,避免把一个组件的所有接口归为一种类型。
| 组件与具体位置 | 这里使用哪种类型 | 源码中的载体或接口 | 例子与含义 |
|---|---|---|---|
| Vector:列的类型 | 逻辑类型 | BaseVector::type() 返回 TypePtr |
DATE()、DECIMAL(12,2);保留日期身份或 decimal 参数 |
| Vector:分派类别 | 物理类别 | BaseVector::typeKind() 返回 TypeKind |
DATE 返回 INTEGER,短 decimal 返回 BIGINT |
| Vector:读取和保存单个标量 | C++ 值类型 | FlatVector<T> 的 T,DecodedVector::valueAt<T>() |
日期用 int32_t,短 decimal 的 unscaled value 用 int64_t;buffer 与编码决定批布局 |
| Simple Function:声明与绑定 | 逻辑签名 / 逻辑类型 | FunctionSignature 描述签名;绑定时输入、返回类型用 TypePtr |
date → date;decimal 签名还含 precision/scale 变量与约束 |
| Simple Function:实现匹配 | 物理类型描述 | 元数据的 argPhysicalTypes()、resultPhysicalType(),仍使用 TypePtr |
Date 对应 INTEGER();Array<Date> 对应 ARRAY(INTEGER()) |
| Simple Function:逐行计算 | C++ 执行类型 | UDF call 的 arg_type<Tag>、out_type<Tag> |
Date 映射为 int32_t;字符串和复杂值映射为 native / view / writer |
| Vector Function:输入与结果类型 | 逻辑类型 | apply() 的 args[i]->type() 与 outputType |
输入 DATE,返回 DATE;分配结果必须使用已绑定的逻辑类型 |
| Vector Function:批处理内核 | 物理类别 + C++ 值类型 | 按 kind 分派,再通过具体 Vector / DecodedVector 读写值 | 可以处理一批 int32_t,但不能由此把结果逻辑类型改成 INTEGER |
| Expression:表达式的结果类型 | 逻辑类型 | typed expression 的 TypePtr |
日期函数的返回类型为 DATE;金额表达式的返回类型带新的 precision/scale |
| Operator:输入输出 schema | 逻辑 ROW 类型 | PlanNode::outputType()、Operator::outputType_、输入 / 输出 RowVector::type() |
ROW(d DATE, amount DECIMAL(12,2)),子列继续携带各自逻辑类型 |
| Operator:列访问、存储与比较路径 | 物理类别用于分派,逻辑类型约束语义 | 从列 Type 取 kind,并在需要时调用 Type 的自定义 compare/hash | BIGINT backing 可以复用存储;带时区时间的比较仍须遵守该逻辑类型的规则 |
| Operator:算法状态 | C++ 数据结构与值类型 | RowContainer、hash table、聚合 accumulator 等 | 与输入 / 输出逻辑 schema 有映射,但不要求状态本身是一列 Vector |
| Reader / Writer:Velox schema | 逻辑类型 | 映射后的 fileType->type()、请求类型 / 输出 schema |
文件中的 decimal 注解映射为 DECIMAL(p,s),并按目标逻辑类型读写 |
| Reader / Writer:格式编解码 | 文件物理类型 + Velox 物理类别 + C++ 值表示 | 例如 Parquet INT64 枚举、Velox kind、解码器模板参数 |
三者分别描述磁盘值表示、执行表示类别和实现中的值类型,不能混称 |
TypePtr 出现在“逻辑类型”和“物理类型描述”两行是源码的实际用法:载体相同,用途不同。 FunctionSignature 则主要保存类型签名结构与变量,不是把所有签名都预先实例化成 Type 对象。绑定之后才得到具体的逻辑 TypePtr。
1.5 Vector:逻辑 schema 与数据表示同时存在
Vector 的 type() 返回逻辑类型描述,以 Velox TypePtr 承载;typeKind() 返回物理类别;值访问使用 C++ 类型。 因此一列 DATE 的 type() 是 DATE(),typeKind() 是 INTEGER,值可用 int32_t 访问。这三项事实同时成立,type() 不会因底层存整数就退化成 INTEGER()。
Vector 保存完整类型,是因为下游需要知道列的含义。若 Reader 读出金额后只提供一个整数 buffer,后续表达式就无法确定 scale,输出端也无法恢复正确的 decimal schema。Vector 的 C++ 模板参数只负责一部分实现信息,不能独立承担这个契约。
例如 FlatVector<int64_t> 可以承载 BIGINT,也可以承载 DECIMAL(12,2)。这里 int64_t 表示值的 C++ 承载方式,type() 才能区分具体逻辑类型。同一个 DECIMAL(12,2) Vector 又可以被 dictionary 包装;读值时先通过索引找到底层整数,再按照相同的 decimal 类型解释。
null 和行数属于数据状态。ARRAY(BIGINT) 可以有 NULL、空数组和含 null 元素的数组;它们使用同一类型描述,却有不同的外层 null、长度和子向量状态。因此 schema 类型、值访问类型和 encoding 都需要分别确认。
1.6 Function:逻辑签名与 C++ 调用接口
Function 用逻辑类型声明参数和结果,用 C++ 类型实现具体计算。 date → date 是函数的数据契约;C++ 函数体可以读取和写入 int32_t。同一个 int32_t 也能用于 integer 函数,但这不会让两个逻辑签名变成同一个签名。
Simple Function 的注册声明提供逻辑签名信息。完成类型绑定后,框架还要确认所选实现能承载这些参数与结果。DECIMAL 是典型例子:签名含 precision/scale,具体实例可能使用 64 位或 128 位整数。函数执行时如果还需要 scale 等参数,可以从完整的输入 Type 中取得,并保存到函数实例中。
进入逐行 call 的参数通常是原生值或轻量访问接口,不是每行都传一个 Type 对象。以 decimal 加法为例,参数可以是两个整数;它们需要怎样对齐小数位、返回结果按哪个 scale 解释,由已经确定的逻辑类型决定。
Vector Function 的接口直接接收输入 Vector 与逻辑输出 Type。args[i]->type() 是输入列的逻辑类型,outputType 是绑定得到的逻辑返回类型;具体实现再按物理 kind 与 encoding 选择批处理路径。Simple Function 也通过适配器进入这一执行框架。两种接口的区别是实现面对单个值还是一批值,输入输出仍须保持各自声明的逻辑类型。
聚合函数还多出中间结果类型:输入类型、partial/intermediate 类型、final 类型,以及进程内累加器结构可以不同。它们不能仅按一个 C++ 参数类型统一概括。
这里可以直接对照 Date 函数:签名是逻辑 date → date;元数据中的物理输入、输出类型都是 INTEGER();call 的输入、输出值都是 C++ int32_t;结果 Vector 的 type() 又明确是 DATE()。最后一步使用的是绑定阶段保留的逻辑返回类型,并不是根据 int32_t 反向猜出来的。
1.7 Operator:逻辑 schema 约束数据,内部状态由算法决定
Operator 的输入输出 schema 是逻辑 ROW 类型,以 RowTypePtr 表达;内部用 C++ 数据结构执行算法。 例如 ROW(d DATE, amount DECIMAL(12,2)) 保留两个字段的逻辑类型,不会在进入 Operator 后变成 ROW(INTEGER, BIGINT)。RowVector 同时携带这一批数据及其逻辑 schema。Filter、Project、Join、Aggregation、OrderBy 等根据自己的语义产生满足输出 schema 的结果。
Filter 可以选择部分行而保留列类型;Project 可以输出函数计算得到的新类型;Join 组合两侧的字段;Aggregation 可以把输入列转换为不同类型的聚合结果。类型系统描述这些数据边界,C++ 算法负责完成实际处理。
内部实现可能采用逐列访问、RowContainer、hash table 或 accumulator。Physical kind 可以帮助选择读写和计算路径,但逻辑类型仍可能参与操作本身。例如某个类型虽然用 int64_t 存储,其相等和 hash 规则却不同于普通整数。Join 或 GROUP BY 必须遵守这份逻辑语义,否则存储正确也会产生错误结果。
因此,“Operator 使用物理类型”只能描述它选择某些底层实现的场景;“Operator 使用逻辑类型”描述它的 schema 和语义约束;“Operator 用 C++ 类型”描述它的实现语言和数据结构。三者分别对应不同位置。
1.8 从文件到计算:变化的是表示,保留的是类型契约
文件格式定义自己的 schema 与物理类型。例如 Parquet 的 INT64 + DECIMAL(12,2) 同时包含基础存储表示和十进制语义。Reader 识别这两部分,构造 Velox 类型,并按照请求 schema 解码或转换数据,最后产出带 Type 的 Vector。
之后,表达式用这些逻辑类型匹配函数并确定返回类型;函数从 Vector 取出 C++ 值进行计算;Operator 按输入输出 schema 组织结果。写出时,Writer 再把输出 Type 和数据映射到目标格式。逻辑 Type 与值的实现表示在这条路径中持续配合。
文件物理类型与 Velox physical kind 也不相同。例如 Parquet 的 INT64 时间计数可以读成 Velox TIMESTAMP,计算侧用 Timestamp 值表示,而不是继续把它作为普通 BIGINT。文件 page encoding、Velox Vector encoding 和 SQL 类型转换,分别处理不同的问题。
至此可以确定执行层的边界:schema、函数签名和结果契约使用逻辑类型;physical kind 连接基础表示与实现选择;C++ 类型承担值访问与计算。 完整 Velox Type 将逻辑描述与 backing kind 保存在一起,Vector 再把这份描述和一批实际数据连接起来。接下来再看这些关系如何在代码中建立。
二、类型描述、实现关联与转换
执行中的类型职责已经确定,接下来可以沿着实现关系阅读代码:先看 Type 如何保存逻辑身份与 backing kind,再看 traits 和工厂如何关联 C++ 类型;随后进入 Vector、函数绑定、算子与 Reader,最后区分类型比较、转换与扩展接口。
2.1 Type 对象如何保存逻辑身份与基础表示
最直接的证据是 Type.h 的接口注释:name() 返回唯一的逻辑类型名,kindName() 返回底层 physical type 名,多个逻辑类型可以共享后者。下面只摘出与本文有关的成员:
using TypePtr = std::shared_ptr<const Type>;
class Type : public Tree<const TypePtr>, public ISerializable {
public:
TypeKind kind() const;
virtual const char* name() const = 0; // logical type name
virtual const char* kindName() const = 0; // physical type name
virtual std::span<const TypeParameter> parameters() const = 0;
bool providesCustomComparison() const;
// ...
};TypePtr 指向只读 Type,Type 本身不保存某一行的值,也不拥有某一列的 values buffer。它是一棵可组合的类型树:叶子描述 scalar,ARRAY/MAP/ROW 等节点通过 child type 递归描述结构。DECIMAL 的 precision/scale 是整数参数,不是两个 child type;ROW 的字段名也不是数据。
2.1.1 一套 Type 继承树,两种描述用途
源码中没有一个名为 LogicalType 的基类和另一个名为 PhysicalType 的基类。这里的核心是同一套 Type 继承树。下图画出本文涉及的真实继承路径;箭头由子类指向父类,别名写在对应模板实例旁,不把别名误画成一个新类。
TypeBase<KIND> 把编译期 kind 对应的 traits 暴露成运行时虚函数;CanProvideCustomComparisonType<KIND> 增加针对 NativeType 值的 compare/hash 接口;ScalarType<KIND> 补齐无 child 的标量描述。IntegerType 是 ScalarType<TypeKind::INTEGER> 的别名,BigintType 同理。DATE 通过继承 IntegerType 复用实现;short / long decimal 通过 DecimalType<BIGINT / HUGEINT> 复用对应整数表示。自定义逻辑类型也可沿这些基础类扩展。
因此,并不是“上面的类是物理类型,下面的类才是逻辑类型”。INTEGER() 自身就是有效的 SQL 逻辑 INTEGER 描述,同时也是代码描述 INTEGER 物理表示时使用的基础 Type。中间实现基类与最终类型对象的用途,必须分别讨论。
2.1.2 kind 保存在基类,逻辑差异由子类实现
Type 实际保存 const TypeKind kind_。其构造函数 Type(TypeKind kind, bool providesCustomComparison = false) 初始化这个字段;kind() 直接返回它。TypeBase<KIND> 的默认构造函数是 TypeBase() : Type{KIND, false} {},因而把模板参数写入继承来的字段。kindName() 则返回 TypeTraits<KIND>::name。
构造 DateType 时,这条继承链最终调用 TypeBase<INTEGER>,所以同一个 DATE 对象里的 kind_ 为 INTEGER。DateType 覆盖 name() 返回 DATE,却不覆盖 kind() 或 kindName();等价判断、格式化与序列化也按 DATE 的身份实现。这是一个带 INTEGER backing 的 DateType 对象,不是由一个 LogicalType 对象包住另一个 PhysicalType 对象。
| 观察对象 | 具体 C++ 类型描述类 | 逻辑类型信息 | 物理表示信息来自哪里 |
|---|---|---|---|
INTEGER() |
IntegerType,即 ScalarType<INTEGER> |
name() 为 INTEGER,无附加参数 |
TypeBase<INTEGER> 初始化 kind_=INTEGER |
DATE() |
DateType |
覆盖 name() 为 DATE;日期专用等价、格式化等行为 |
继承 IntegerType 的 INTEGER kind 与基础值表示 |
DECIMAL(12,2) |
ShortDecimalType |
DecimalType 提供 DECIMAL 名称及参数 12、2 |
继承 ScalarType<BIGINT> 的 BIGINT kind |
DECIMAL(30,6) |
LongDecimalType |
DECIMAL 名称及参数 30、6 | 继承 ScalarType<HUGEINT> 的 HUGEINT kind |
ARRAY(DATE()) |
ArrayType |
child 是 DateType 对象,形成“日期数组”逻辑类型 |
外层 kind 为 ARRAY;元素物理 kind 从 child 的 INTEGER 取得 |
ARRAY(INTEGER()) |
ArrayType |
用作 schema 时表示“整数数组” | 用作 physical signature 时表示 ARRAY 容器与 INTEGER 元素的物理结构 |
两个 decimal 对象可以属于同一个 ShortDecimalType 类却具有不同 precision/scale;两个数组对象可以属于同一个 ArrayType 类却具有不同 child Type。具体 C++ 类名也不足以确定完整逻辑类型,必须连同参数和子类型一起看。
C++ 允许把 DateType 对象当作其 IntegerType 基类访问,但不会因此把它变成普通 INTEGER 的逻辑类型。比如 DATE()->as<TypeKind::INTEGER>() 得到基类引用,虚调用 name() 仍返回 DATE;DATE()->kind() == INTEGER()->kind() 成立,DATE()->equivalent(*INTEGER()) 不成立。前者确认基础表示兼容,后者判断逻辑类型是否等价;两者不是同一个问题。
2.1.3 DATE 的 name、kind 与 NativeType
| 观察接口 | DATE() |
DECIMAL(12,2) |
VARCHAR() |
|---|---|---|---|
name() |
DATE | DECIMAL | VARCHAR |
kind() |
INTEGER | BIGINT | VARCHAR |
kindName() |
INTEGER | BIGINT | VARCHAR |
toString() |
DATE | DECIMAL(12, 2) | VARCHAR |
| 重要附加信息 | 日期语义 | precision=12,scale=2 | 文本语义 |
| backing 的 NativeType | int32_t |
int64_t |
StringView |
name() 本身也不是完整 schema:两个 decimal 的 name 都是 DECIMAL,仍要检查参数。基本类型通常逻辑名与 backing 名相同,这只是最简单的情况。
上面的对象关系也可以从基础类接口中看到。TypeBase 的默认 name() 与 kindName() 都取自 traits;具体类型再覆盖逻辑上有差异的成员。下面保留相关接口摘录,并标出中间继承关系:
// 结构摘录;省略与本节无关的成员。
template <TypeKind KIND>
class TypeBase : public Type {
public:
using NativeType = typename TypeTraits<KIND>::NativeType;
bool isPrimitiveType() const override {
return TypeTraits<KIND>::isPrimitiveType;
}
bool isFixedWidth() const override {
return TypeTraits<KIND>::isFixedWidth;
}
const char* kindName() const override { return TypeTraits<KIND>::name; }
const char* name() const override { return TypeTraits<KIND>::name; }
};
// 继承关系示意。
// CanProvideCustomComparisonType<K> : TypeBase<K>
// ScalarType<K> : CanProvideCustomComparisonType<K>
// DateType : IntegerType // INTEGER backing
// DecimalType<BIGINT> : ScalarType<BIGINT> // short decimal
// ShortDecimalType : DecimalType<BIGINT>
// ArrayType : TypeBase<ARRAY>DateType 继承 IntegerType,覆盖 name()、等价判断、格式化与 serde 等方法。因此 DATE()->isInteger() 这种按 kind 判断的接口,不能代替 isDate() 这种逻辑身份判断;判断“这是普通 INTEGER 还是日期”要保留完整类型。当前 ScalarType 与 TypeBase 之间还经过 CanProvideCustomComparisonType,它为需要覆盖值比较/hash 的逻辑类型提供接口,后面的带时区时间例子会用到它。
2.1.4 DECIMAL:复用整数表示,保留十进制契约
DECIMAL(p,s) 工厂根据声明的 precision 选择具体的 DecimalType 分支及其 backing:
| precision | Velox 类 | kind | NativeType |
|---|---|---|---|
| 1…18 | ShortDecimalType |
BIGINT | int64_t |
| 19…38 | LongDecimalType |
HUGEINT | int128_t |
DECIMAL(p,s) 的存储整数称为 unscaled value,数学值为 unscaled × 10^-s。例如 DECIMAL(12,2) 的 123.45 存作 12345。precision 限制十进制总位数;scale 指定小数位数,满足 0 ≤ s ≤ p。
这里按声明的 precision选择短/长 decimal,不会因为某一批值恰好很小就把 DECIMAL(30,2) 临时变成短 decimal。类型要在批次之间保持契约稳定,值的统计与编码优化另行处理。
两列都是 FlatVector<int64_t>,一列是 DECIMAL(10,2),另一列是 DECIMAL(12,4),它们并不能直接对 unscaled value 相加。123 + 456 只有在尺度已经一致时才对应期望的十进制运算。后面会看到函数初始化怎样取得 scale 并对齐。
2.1.5 physicalType 为什么仍然是 TypePtr
core::TypeAnalysisResults 有一个明确命名为 physicalType 的 TypePtr 字段。分析 Date UDF tag 时,代码一方面写出逻辑签名字符串 date,另一方面调用 createScalarType(TypeKind::INTEGER),得到用于物理签名的 INTEGER()。分析 Array<Date> 时,再把 child 的物理 Type 包装为 ARRAY(INTEGER())。这个结果是用 Type 对象表示的物理类型树。
例如 ARRAY(DATE()) 与 ARRAY(INTEGER()) 的顶层 kind 都是 ARRAY,递归 kind 结构也相同;但元素的逻辑身份不同,所以两者不逻辑等价。物理匹配需要遍历整棵 kind 树,而不能只比较一个顶层枚举。复杂 Type 的 child 成员是组合关系,也不是类继承关系。
这份物理描述用于筛选函数实现。SimpleFunctionRegistry 先让 SignatureBinder 绑定逻辑签名,再通过 physicalTypeMatches() 递归检查 kind 与 children,确认参数和返回值满足该实现的物理要求;其中 UNKNOWN physical 占位有通配含义。物理匹配成功既不替代逻辑签名匹配,也不修改输入 Vector 的逻辑 Type。 第 2.7 节会继续追踪分析与绑定的具体调用路径。
下面这个独立示例把继承关系和对象观察放在一起。static_assert 核对的是 C++ 类型关系;函数体中的检查描述运行时预期,并展示相应 API。随文提供完整源文件。本次完成了语法与模板实例化检查,未链接执行这些运行时断言;Date / Array<Date> 的物理类型结果也已对照源码中的 TypeAnalysisTest。
static_assert(std::is_same_v<
IntegerType, ScalarType<TypeKind::INTEGER>>);
static_assert(std::is_base_of_v<IntegerType, DateType>);
static_assert(std::is_same_v<
TypeTraits<TypeKind::INTEGER>::NativeType, int32_t>);
void inspectTypeModel() {
const TypePtr date = DATE();
assert(date->kind() == TypeKind::INTEGER);
assert(std::string(date->name()) == "DATE");
assert(std::string(date->kindName()) == "INTEGER");
assert(!date->equivalent(*INTEGER()));
// C++ 基类引用不会抹去动态对象的逻辑身份。
const IntegerType& asInteger = date->as<TypeKind::INTEGER>();
assert(std::string(asInteger.name()) == "DATE");
core::TypeAnalysisResults analysis;
core::TypeAnalysis<Array<Date>>{}.run(analysis);
assert(analysis.typeAsString() == "array(date)");
assert(analysis.physicalType->equivalent(*ARRAY(INTEGER())));
const auto logical = ARRAY(DATE());
assert(logical->kindEquals(analysis.physicalType));
assert(!logical->equivalent(*analysis.physicalType));
}
2.2 TypeTraits 如何关联 kind 与 C++ 类型
TypeKind.h 中枚举的关键成员如下,注意没有 DATE 和 DECIMAL:
enum class TypeKind : int8_t {
BOOLEAN = 0, TINYINT = 1, SMALLINT = 2, INTEGER = 3,
BIGINT = 4, REAL = 5, DOUBLE = 6, VARCHAR = 7,
VARBINARY = 8, TIMESTAMP = 9, HUGEINT = 10,
ARRAY = 30, MAP = 31, ROW = 32, UNKNOWN = 33,
FUNCTION = 34, OPAQUE = 35, INVALID = 36
};TypeTraits<KIND> 是以枚举值为非类型模板参数的特化表。编译器知道 KIND 后,就能选中特化,读取其中的别名和常量。这就是这里“萃取 / traits”的含义:信息由库作者显式写进特化,并非运行时扫描任意 C++ 对象。
template <TypeKind KIND>
struct TypeTraits {};
template <>
struct TypeTraits<TypeKind::BIGINT> {
using ImplType = ScalarType<TypeKind::BIGINT>;
using NativeType = int64_t;
using DeepCopiedType = NativeType;
static constexpr uint32_t minSubTypes = 0;
static constexpr uint32_t maxSubTypes = 0;
static constexpr TypeKind typeKind = TypeKind::BIGINT;
static constexpr bool isPrimitiveType = true;
static constexpr bool isFixedWidth = true;
static constexpr const char* name = "BIGINT";
};
template <>
struct TypeTraits<TypeKind::ARRAY> {
using ImplType = ArrayType;
using NativeType = void;
using DeepCopiedType = void;
static constexpr uint32_t minSubTypes = 1;
static constexpr uint32_t maxSubTypes = 1;
static constexpr TypeKind typeKind = TypeKind::ARRAY;
static constexpr bool isPrimitiveType = false;
static constexpr bool isFixedWidth = false;
static constexpr const char* name = "ARRAY";
};2.2.1 ImplType 与 NativeType 分别是什么
ImplType 是类型描述对象的 C++ 类。ScalarType<BIGINT> 负责实现 BIGINT 这个 schema 节点;它不是一条 SQL BIGINT 值。
NativeType 是值访问或计算的 C++ 表示。BIGINT 对应 int64_t,TIMESTAMP 对应 Timestamp。模板函数可以通过 using T = typename TypeTraits<KIND>::NativeType 选取合适的值类型。
DeepCopiedType 给出需要独立值表示时的对应类型,例如 VARCHAR 的 NativeType 是 StringView,DeepCopiedType 是 std::string。这个别名本身不会执行深拷贝,也不承诺所有 Vector API 都自动转换成 owning string。
2.2.2 traits 的 primitive 与 fixed-width 属性
| kind | NativeType | primitive / leaf | fixed width | 说明 |
|---|---|---|---|---|
| BOOLEAN | bool |
是 | 是 | FlatVector 的 bool 值可按 bit 存储 |
| TINYINT / SMALLINT / INTEGER / BIGINT | 对应有符号整数 | 是 | 是 | DATE 等逻辑类型复用其中的表示 |
| HUGEINT | int128_t |
是 | 是 | 长 decimal 复用 |
| REAL / DOUBLE | float / double |
是 | 是 | SQL 比较还要考虑 NaN 等规则 |
| TIMESTAMP | Timestamp |
是 | 是 | C++ 类,仍是类型树中的叶子 |
| VARCHAR / VARBINARY | StringView |
是 | 否 | 两个 kind,共用一种 C++ 值表示 |
| ARRAY / MAP / ROW | void |
否 | 否 | 由复杂 Vector 和 UDF view 提供值访问 |
源码对 TIMESTAMP 专门说明:traits 中的 primitive 表示叶子类型,即不包含子类型,不是 C++ 的 fundamental type。VARCHAR 虽然使用大小固定的 StringView 对象,字符串内容长度仍可变,所以 isFixedWidth=false。
ARRAY 的 NativeType=void 更不能解释成“数组没有值”。它表示没有一个通用 scalar NativeType 能描述这种嵌套列值;在 batch 层使用 ArrayVector,在 Simple Function 逐行接口中使用 ArrayView。std::vector<T> 既不是默认的 ARRAY 存储,也不是默认的 UDF 输入参数。
2.3 类型工厂与 CppToType 的映射路径
2.3.1 已知业务语义时,直接创建完整 Type
auto id = BIGINT();
auto amount = DECIMAL(18, 4);
auto dates = ARRAY(DATE());
auto attributes = MAP(VARCHAR(), VARBINARY());
auto row = ROW({"id", "amount"}, {id, amount});这些工厂创建类型描述,不分配列数据。TypeFactory 提供泛型入口。标量通常转发到 ImplType::create(),ARRAY/MAP/ROW 的特化负责接收各自需要的子类型或字段名:
template <TypeKind KIND>
struct TypeFactory {
static std::shared_ptr<const typename TypeTraits<KIND>::ImplType> create() {
return TypeTraits<KIND>::ImplType::create();
}
};
template <>
struct TypeFactory<TypeKind::ARRAY> {
static ArrayTypePtr create(TypePtr elementType) {
return std::make_shared<const ArrayType>(std::move(elementType));
}
};以 BIGINT 为例,TypeFactory<BIGINT>::create() 选出 ScalarType<BIGINT>,然后调用它的 create()。当前基本 ScalarType 使用静态实例,避免反复创建相同的无参数描述。但不能把“所有 Type 都是单例”当作接口保证,带参数类型和嵌套类型需要保存各自的信息。
把标量工厂和复杂类型工厂连起来,可以直接观察返回的类型描述:
auto bigIntVT = TypeFactory<TypeKind::BIGINT>::create();
auto arrayVT = TypeFactory<TypeKind::ARRAY>::create(bigIntVT);
std::cout << bigIntVT->toString() << '\n'; // BIGINT
std::cout << arrayVT->toString() << '\n'; // ARRAY<BIGINT>第二次调用把已经创建的 BIGINT TypePtr 作为 ARRAY 的 element type。这里既没有创建 int64_t 值,也没有分配数组元素;toString() 输出的是类型树的描述。注释中的字符串依据当前类型格式化实现。
Type::create<KIND>(...) 也是工厂入口。运行时的 createType(kind, children) 则只能处理它所支持的类别与参数形式。当前 createType(TypeKind::ROW, children) 明确报不支持,不是自动恢复字段名;OPAQUE 也需要额外身份信息。只给 BIGINT kind 不可能凭空恢复 DECIMAL(12,2)。
2.3.2 只有 C++ 类型时,CppToType<T> 选择一个约定映射
CppToType.h 的主干很短:
template <typename T>
struct CppToType {};
template <TypeKind KIND>
struct CppToTypeBase : TypeTraits<KIND> {
static auto create() {
return TypeFactory<KIND>::create();
}
};
template <>
struct CppToType<int64_t> : CppToTypeBase<TypeKind::BIGINT> {};完整推导过程是:T=int64_t 命中特化 → 继承 CppToTypeBase<BIGINT> → 通过 TypeTraits<BIGINT> 得到 NativeType、isPrimitiveType、typeKind → create() 调用 TypeFactory<BIGINT> → 得到 BIGINT Type 对象。
这个过程中,前面的特化、别名选择是编译期行为;真正调用 create() 得到 TypePtr 是运行时 API。不能用运行时变量 kind 直接写 TypeTraits<kind>,第 2.6 节会解释分派宏如何跨过这个边界。
static_assert(CppToType<int64_t>::typeKind == TypeKind::BIGINT);
static_assert(std::is_same_v<
TypeTraits<TypeKind::BIGINT>::NativeType, int64_t>);
auto type = CppToType<int64_t>::create(); // BIGINT()2.3.3 映射的信息损失与反向构造
INTEGER → int32_t → INTEGER 可以回到基础类型;DATE → int32_t → INTEGER 却丢掉了 DATE。DECIMAL(12,2) → int64_t → BIGINT 同样丢掉 precision、scale 与十进制身份。
VARCHAR 与 VARBINARY 是两个不同 kind,但 NativeType 都是 StringView。同一个 CppToType<StringView> 不可能同时有两个相同特化;源码明确选择 VARCHAR。std::string、std::string_view、const char*、folly::StringPiece 也映射到 VARCHAR。
同宽的有符号与无符号整数还会映射到相同 kind,例如 uint64_t 也映射 BIGINT。这是类型适配约定,不能据此认为 SQL BIGINT 获得了完整 unsigned 取值范围,更不是自动检查、转换任意 unsigned 值的算法。
2.3.4 递归容器特化与一个容易误读的 Date 细节
SimpleFunctionApi.h 额外提供了 UDF tag 的适配,它不在基础 CppToType.h 中:
template <typename ELEMENT>
struct CppToType<Array<ELEMENT>> : TypeTraits<TypeKind::ARRAY> {
static auto create() {
return ARRAY(CppToType<ELEMENT>::create());
}
};
template <>
struct CppToType<Date> : CppToTypeBase<TypeKind::INTEGER> {};于是 CppToType<Array<int64_t>>::create() 递归得到 ARRAY(BIGINT())。这里的 Array<E> 是 Velox 的 UDF tag,不是 std::vector<E>。
更值得注意的是:**当前 CppToType<Date>::create() 得到 INTEGER,而非 DATE;CppToType<Array<Date>>::create() 也相应得到 ARRAY(INTEGER)**。TypeTest 明确检查了前一行为。构造日期 schema 应显式用 DATE() / ARRAY(DATE()),函数的逻辑签名则通过后面的 SimpleTypeTrait、TypeAnalysis 路径生成。
也不能把这个行为推广为“所有 tag 的 create 都只返回 backing”:Time / TimeMicroUtc 的特化覆盖 create(),返回对应逻辑 Type;std::shared_ptr<T> 返回带 T 身份的 OpaqueType。具体映射取决于特化实现,CppToType 不是统一的逻辑类型反射协议。
2.4 复杂 schema、数据树与 null
auto schema = ROW(
{"user_id", "events", "attrs"},
{BIGINT(),
ARRAY(ROW({"ts", "cost"}, {TIMESTAMP(), DECIMAL(12, 2)})),
MAP(VARCHAR(), VARCHAR())});这棵树描述:每行有 user_id、events、attrs 三个字段;events 的每个元素是一个含 ts/cost 的 ROW;attrs 的 key/value 都是文本。它没有定义这一批有多少行,也没有创建任何 event 或字符串。
Type::size() 表示子类型个数:ARRAY 是 1,MAP 是 2,ROW 为字段数;BaseVector::size() 才是该向量逻辑行数。某个数组含 100 个元素,不会让 ARRAY(BIGINT())->size() 变成 100。
类型树中的 ARRAY 对应数据层的 offsets/sizes 与一个 elements vector;MAP 对应 offsets/sizes、keys vector 和 values vector;ROW 对应按字段排列的 children。嵌套结构递归组合,元素总数可以与外层行数不同。没有必要把每一行数组转换成独立的 std::vector 才能计算。
2.4.1 null 不写在 TypeKind 里
BIGINT() 可以描述既有 null 又有非 null 值的一列,不需要 Nullable<BigintType>。null 是 Vector 中的数据状态,通常通过 bitmap 及包装层的规则表示。没有 null buffer 经常表示这一层没有 null,具体访问必须尊重 encoding。
ARRAY 的三个值 NULL、[]、[NULL] 的 Type 都可以是 ARRAY(BIGINT()):第一个数组自身为 null;第二个非 null 且长度为 0;第三个非 null、长度为 1,但 child value 为 null。ROW 自身为 null,也不同于“ROW 非 null,而全部字段为 null”。
UNKNOWN 是独立的 TypeKind,常用于尚未定型的 null 等场景,不是所有类型的可空包装。在 UDF 里,callNullable、callNullFree 又是调用约定;是否允许某个参数为 null、是否递归没有 null,需要 adapter 检查,不能从 kind 直接推断。
2.5 Vector 如何同时保存类型描述和数据
BaseVector 的 type() 返回完整 TypePtr;构造时还缓存 typeKind_ 与是否需要自定义比较。逻辑类型与执行所需的基础属性在同一个 Vector 中并存。
// BaseVector 的相关成员与接口,省略其他成员。
TypePtr type_;
const TypeKind typeKind_;
const TypePtr& type() const { return type_; }
TypeKind typeKind() const { return typeKind_; }2.5.1 BaseVector::create 的类型分派
BaseVector::createInternal 先取 type->kind()。ROW、ARRAY、MAP 走各自的复杂 Vector 构造,递归传入原来的 child TypePtr;标量走动态分派,选定 TypeTraits<kind>::NativeType,然后创建 FlatVector<T>。
关键在于它选择 T 后,仍把原始 TypePtr传给 FlatVector,而不是从 T 重新构造一个基础类型:
// createEmpty<kind>() 的核心逻辑。
using T = typename TypeTraits<kind>::NativeType;
// 分配 values buffer;StringView 还需要相应初始化。
return std::make_shared<FlatVector<T>>(
pool, type, nullptr, size, values, std::vector<BufferPtr>{});因此:
auto dates = BaseVector::create(DATE(), 3, pool);
auto amounts = BaseVector::create(DECIMAL(12, 2), 3, pool);
// dates 的具体值访问类是 FlatVector<int32_t>,type() 仍是 DATE。
// amounts 的具体值访问类是 FlatVector<int64_t>,type() 仍是 DECIMAL(12,2)。
auto* values = amounts->asFlatVector<int64_t>();
values->set(0, 12345); // 按 type() 解释为 123.45;set 不替你做十进制缩放。这里确认 create 返回 flat scalar,才直接访问 FlatVector。对任意输入 Vector 不能照搬这种强转,后者可能是 dictionary、constant 或 lazy。
旧版本阅读笔记曾用 CppToType<T>::create() 解释 SimpleVector 的类型推导构造。当前 SimpleVector 构造函数 接收显式 TypePtr 并传给 BaseVector。理解旧例子的价值是看到模板映射怎样被使用;当前实现的关键则是显式类型避免把 DATE/DECIMAL 等逻辑身份降成基础整数。
2.5.2 FlatVector 的标量表示与专用布局
对于普通固定宽度数值,values buffer 可按 T 访问。但 FlatVector<bool> 使用专门的 bit 访问逻辑,不能当成一个字节一个 bool 的数组;FlatVector<StringView> 包含字符串视图及用于长字符串的 buffers,StringView 自身的大小不等于字符串 payload 大小。
复杂列还涉及 offsets、sizes、children 和每层 null。Type::cppSizeInBytes() 或 sizeof(NativeType) 都不能作为整列内存估算公式;实际内存受编码、嵌套元素数量、字符串长度和 buffer 共享情况影响。
2.5.3 Dictionary 与 DecodedVector 的访问路径
假设底层值为 [12345, 67890, null],dictionary indices 为 [1,0,2,1]。逻辑上得到 [678.90,123.45,null,678.90],而不是 BIGINT 列。包装层还可能有自己的 null bitmap;底层为非 null 不代表外层一定非 null。
DecodedVector 把包装层的索引与 null 规则统一成访问接口。多个 dictionary 的索引可以组合;constant 可形成常量映射。这个过程通常不要求复制全部值、物化成一个新的 FlatVector。lazy 输入是否加载、加载哪些行,则与 decode 的参数和表达式执行路径有关。
对于已确认 kind 为 BIGINT 的值访问,典型写法是先 decode,再对选中且非 null 的行用 valueAt<int64_t>(row)。这解决编码访问问题,没有把 DECIMAL cast 成 BIGINT;后续数学含义仍由原 Type 和运算契约决定。
2.6 运行时 kind 如何选择编译期模板
type->kind() 是运行时枚举值,TypeTraits<K> 的 K 必须是编译期常量。动态分派宏做的核心工作,是用 switch 为每个支持的 kind 选择已经编译好的模板实例。
// 等价思路示意;实际宏支持的 kind 集合因宏而异。
switch (vector->typeKind()) {
case TypeKind::INTEGER:
return visit<TypeKind::INTEGER>(vector);
case TypeKind::BIGINT:
return visit<TypeKind::BIGINT>(vector);
case TypeKind::DOUBLE:
return visit<TypeKind::DOUBLE>(vector);
default:
// 处理其余支持的 kind 或报错。
break;
}
// visit<K>() 内部可以写:
// using T = typename TypeTraits<K>::NativeType;这里没有运行时生成一个新 C++ 类型,也没有做数值转换。VELOX_DYNAMIC_TYPE_DISPATCH、VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH_ALL 等宏只是帮助选择实现;使用前要看宏覆盖哪些 kind,不能假定普通 scalar 模板能接受 NativeType=void 的复杂类型。
至少有三个决策维度需要分开:逻辑语义决定该做什么;kind / NativeType决定用哪类值实现;encoding决定怎样取得这些值。只依据 kind 的整数 kernel 可以处理搬运、宽度相关操作,但不能自动替代所有基于 INTEGER/BIGINT 的日期、decimal 或自定义类型运算。
Type::as<KIND>() 又是另一回事:它通过 TypeTraits<KIND>::ImplType 查看 Type 对象的具体接口,例如取得 RowType 的字段。BaseVector::asFlatVector<T>() 是查看 Vector 实现类。两者都不是 SQL CAST,不会修改列中的数学值。
2.7 函数签名分析、绑定与执行参数
Velox 既有逐行接口的 Simple Function,也有直接操作向量的 Vector Function。Simple Function 最终通过 adapter 接入向量化表达式执行,不能因为 call() 每次接收一个值,就把它理解成另一套无 schema 的计算引擎。
2.7.1 tag 负责表达签名,resolver 负责形成 C++ 调用参数
SimpleFunctionTags.h 中 Date、Varchar、Varbinary 是 tag 类型,甚至不允许普通调用者随意构造它们。它们的任务是作为模板参数携带语义,而不是装一条数据。
template <typename TExec>
struct IdentityDate {
VELOX_DEFINE_FUNCTION_TYPES(TExec);
bool call(out_type<Date>& out, const arg_type<Date>& in) {
out = in;
return true;
}
};
// 注册声明的返回类型与参数类型是 Date tag。
registerFunction<IdentityDate, Date, Date>({"identity_date"});VELOX_DEFINE_FUNCTION_TYPES 定义的别名可化简为:
template <typename T>
using arg_type = typename TExec::template resolver<T>::in_type;
template <typename T>
using out_type = typename TExec::template resolver<T>::out_type;向量执行环境 VectorExec 使用 UdfTypeResolver 的特化。于是这个 UDF 的 call 实际接收 const int32_t&,写出 int32_t&;它的 SQL 签名仍是 date → date。bool 返回值表示该次调用是否产出非 null 值,真正的数据结果写到 out,并不是声明 SQL 返回 BOOLEAN。
| 注册 / 模板中的类型 | 逻辑签名含义 | arg_type |
out_type |
|---|---|---|---|
int64_t |
bigint | int64_t |
int64_t |
Date |
date | int32_t |
int32_t |
Varchar / Varbinary |
varchar / varbinary | StringView |
StringWriter |
ShortDecimal<P,S> |
decimal(p,s),短表示 | int64_t |
int64_t |
LongDecimal<P,S> |
decimal(p,s),长表示 | int128_t |
int128_t |
Array<E> |
array(element signature) | NullableArrayView<E> |
ArrayWriter<E> |
Map<K,V> |
map(key,value) | NullableMapView<K,V> |
MapWriter<K,V> |
Row<T...> |
row(field types) | NullableRowView<T...> |
RowWriter<T...> |
Generic<T> |
类型变量 | GenericView |
GenericWriter |
输出字符串用 writer,是因为创建 payload 需要和结果 Vector 的内存所有权协作;返回一个指向临时字符串的 view 会产生生命周期问题。Array/Map view 则在读取时桥接复杂 Vector,不必先复制成 STL 容器。
NullableArrayView 表示元素可以为 null;外层参数是否为 null 另由 adapter 的默认 null 行为、callNullable 等决定。null_free_arg_type 会映射到相应 null-free view,前提是执行路径已满足其 null 条件。
2.7.2 TypeAnalysis 生成逻辑与物理签名
SimpleFunctionMetadata 的 TypeAnalysis<T> 根据 SimpleTypeTrait<T> 等信息生成签名描述。对 Date,SimpleTypeTrait<Date>::name 是 DATE,而它继承的基础 kind 是 INTEGER。因此分析同时得到两份信息:逻辑签名 date,以及用于匹配实现的 physical Type INTEGER()。
递归的 TypeAnalysis<Array<Date>> 相应得到逻辑签名 array(date) 和 physical Type ARRAY(INTEGER())。这与前文 CppToType<Array<Date>>::create() 的行为并不冲突:它们的职责不同。当前 TypeAnalysisTest 对这两项 backing 结果有明确断言。
源码 TypeAnalysisResults::physicalType 附近有一处注释把 Date 举成 BIGINT;实际 Date trait、实现和测试都指向 INTEGER。阅读这条路径时应以三者为准,不能据这处注释推出“Velox DATE 是 int64”。
2.7.3 SignatureBinder 与 Registry 的候选选择
SimpleFunctionRegistry::resolveFunction 大致经历以下步骤:
- 取得函数调用的实际
std::vector<TypePtr>,遍历候选FunctionSignature。 SignatureBinder绑定类型变量、decimal 的整数参数等。普通具体类型会比较逻辑name(),再检查或绑定参数;如果允许 coercion,还会咨询传入的TypeCoercer。- 对绑定成功的候选,递归核对参数的 physical kind 与该 C++ 实现记录的
argPhysicalTypes。其中用于 generic 等适配的 UNKNOWN physical 占位可匹配任意类型,不等于要求实际输入必须是 UNKNOWN。 - 解析完整返回 Type,再核对其 physical kind 是否与该实现的
resultPhysicalType一致,结合优先级与 coercion 代价选择实现。
所以 DATE 不能因为 kind=INTEGER 就自动匹配所有 integer 函数。DECIMAL 则说明了反过来的必要性:逻辑签名都是 decimal(p,s),但同一公式可能需要 short→short、short→long、long→long 等不同 C++ 实现组合。逻辑签名和物理实现都要匹配。
2.7.4 DECIMAL 的参数怎样真正参与计算
DecimalPlusFunction 是完整的例子。它的 initialize(inputTypes, ...) 从两个 Type 中读取 scale,计算各自向共同 scale 对齐所需的差值;call() 再用预先保存的差值做整数缩放、加法及溢出检查。
Presto 这份加减法注册规则给出:
resultScale = max(s1, s2)
resultPrecision = min(38, max(p1 - s1, p2 - s2) + resultScale + 1)这两个公式属于注册时的签名约束。函数模板中的 P1/S1/P2/S2/P3/S3 是参数变量 tag,并不要求为每一个具体 precision/scale 组合生成独立的 C++ 值类。
例如 DECIMAL(12,2) 的 123.45 与 DECIMAL(8,3) 的 6.789 相加,返回 DECIMAL(14,3)。原始整数 12345 先乘 10,得到 123450;另一个整数为 6789;相加得到 130239,按返回 scale=3 解释为 130.239。普通 C++ 加法负责最后的整数运算,完整 Type 决定它之前的缩放和之后的解释。
2.7.5 Vector Function 直接拿到向量与完整输出类型
VectorFunction::apply 的接口包含:
virtual void apply(
const SelectivityVector& rows,
std::vector<VectorPtr>& args,
const TypePtr& outputType,
EvalCtx& context,
VectorPtr& result) const = 0;输入逻辑类型可通过 args[i]->type() 获取,输出 Type 通过 outputType 显式传入。实现可以按类型、encoding 和选择行挑选批处理算法,复用 buffer,或调用 DecodedVector 统一读取。它同样不能忽略 decimal 参数或自定义比较语义。
Simple Function 的 adapter 实现这个接口,使用 VectorReader<tag> / VectorWriter<tag> 把向量与 call 参数连接起来。函数的 initialize 可以收到完整 inputTypes;结果分配使用已绑定的 outputType。于是逻辑类型、模板 tag、实际 C++ 参数与结果 Vector 形成闭环。
另外,TypeKind::FUNCTION / FunctionType 用于描述 lambda 的参数与返回类型,例如 LambdaTypedExpr;它不是 FunctionSignature 注册表对象,也不是 VectorFunction 的 C++ 实现类。这三个“function type”语境需要分别看。
2.8 表达式、算子与自定义比较
2.8.1 计划与表达式携带的是 Velox Type
Velox 计划中的 PlanNode::outputType 返回 RowTypePtr,描述输出 batch 的列名与完整列类型。CallTypedExpr 接收返回 TypePtr,每个子表达式也携带自身类型。
宿主把查询转换成 Velox 的 typed plan / typed expressions 时,必须提供或解析这些信息。Velox 的函数注册表、类型绑定器和 coercer 可以参与解析,但不能把它想象成“只收到 C++ 模板参数就自动推导整个 SQL 查询”。
2.8.2 Operator 的输入输出仍然是带 Type 的 RowVector
Operator 持有 RowTypePtr outputType_,通过 addInput(RowVectorPtr) 接收 batch。FilterProject 根据表达式求 filter/projection,输出列可能复用输入列、加 dictionary 包装,或由表达式产生新 Vector;这些路径都要满足对应的输出 schema。
HashJoin、Aggregation、OrderBy 等内部可以把数据放入 RowContainer、构建 hash table、维护累加器。这里确实有不同于输入 Vector 的 C++ 布局,但内部布局变化并不使输入和输出 schema 消失。
Aggregation 尤其要区分 raw input type、partial/intermediate type、final result type,以及进程内 accumulator struct。它们可能不同。Aggregate 的 addRawInput / addIntermediateResults 和 resultType 接口区分这些阶段;不能用一句“聚合都用 C++ type”概括,也不能把 accumulator 的 sizeof 当成 SQL 结果类型。
2.8.3 带时区时间的 compare/hash 调用路径
TimestampWithTimeZoneType 以 BIGINT 为 backing,把 UTC 毫秒与 timezone key 打包到 int64_t。低 12 bit 存 timezone key,其余部分承载时间信息。
同一 UTC 毫秒、不同 timezone key 的两个 packed integer 不同;但这个逻辑类型的比较与 hash 只使用解包后的 UTC 毫秒。因此按它的语义,两个值可以相等,尽管原始整数不相等。
这个 Type 设置 providesCustomComparison(),提供自己的 compare(value,value) 和 hash(value)。Vector 的比较/hash 路径与 RowContainer::hash 会根据该标记选择对应实现;RowContainer 同时保存 Type 与 kind,按 kind 实例化代码,再在需要时调用 Type 提供的语义。
如果一个新算子只是看到 BIGINT 就直接用 std::hash<int64_t> 和整数相等,它可能错误地把同一时刻分成不同组。类似地,浮点 NaN、嵌套值、null 的比较也有执行语义,不能默认所有值操作都等同原生运算符。
当前自定义比较支持有范围限制,相关基类只允许指定的 primitive、fixed-width kind。注册一个新名字并不自动使所有类型都支持任意比较实现;应检查对应基类、函数适配和算子访问路径。
2.9 文件 schema 映射与 Reader 值转换
文件格式有自己的 schema、物理类型和编码。Reader 必须同时知道“文件里是什么”以及“查询请求得到什么”,才能选解码器并产出符合要求的 Vector。
2.9.1 Parquet 物理类型与逻辑注解的映射
Parquet 可能用 INT32 存 DATE,用 INT64 存带时间单位注解的 TIMESTAMP,用 INT32/INT64/BYTE_ARRAY/FIXED_LEN_BYTE_ARRAY 的合法组合存 DECIMAL。相同文件物理类型可以有不同逻辑含义,同一逻辑含义也可能有多种文件物理表示。
ReaderBase::convertType 读取 SchemaElement,校验物理类型与注解的组合,并构造 Velox Type。例如 DATE 的 converted type 要求 INT32,再返回 DATE();DECIMAL 读取 precision/scale,构造 DECIMAL(p,s);支持的 timestamp 路径返回 TIMESTAMP()。
| 文件侧的例子 | Reader 得到的 Velox 逻辑类型 | Velox kind | 计算侧常用 C++ 值 |
|---|---|---|---|
| INT32 + DATE | DATE | INTEGER | int32_t,epoch days |
| INT64 + TIMESTAMP_MICROS | TIMESTAMP | TIMESTAMP | Timestamp |
| INT64 + DECIMAL(12,2) | DECIMAL(12,2) | BIGINT | int64_t,unscaled |
| FIXED_LEN_BYTE_ARRAY + DECIMAL(30,6) | DECIMAL(30,6) | HUGEINT | int128_t,unscaled |
| BYTE_ARRAY + UTF8 | VARCHAR | VARCHAR | StringView |
表中描述的是具体可支持路径,不表示 Reader 无条件接受任意格式注解组合。还要检查 requested schema、读取选项、字段映射和格式兼容性。
TIMESTAMP 最能说明两套 physical 不能混同:文件 INT64 里的计数要根据时间单位解释,再形成 Velox Timestamp 表示,不能因为“都是时间”就把文件 bytes 当成 Timestamp*。
2.9.2 fileType 与 requestedType 同时存在
ParquetTypeWithId 在 Velox Type 树之外,保留 parquetType、logicalType、convertedType、precision/scale、column id、definition/repetition level 等格式信息。这里字段名 logicalType_ 指的是 Parquet thrift 注解对象,不是另一棵 Velox logical Type。
SelectiveColumnReader 同时保存 fileType 与 requestedType。后者可能来自查询要求的 schema;前者说明实际文件列。投影、schema evolution、合法的数值扩大或 decimal 缩放,需要在这两者之间明确处理,不能简单换一个 Type 标签。
例如当前 Reader 允许满足条件的 decimal 扩大:scaleIncrease ≥ 0 且 precisionIncrease ≥ scaleIncrease。DECIMAL(12,2) → DECIMAL(14,3) 合法,因为小数多一位后仍能保留原整数位。Reader 的整数取值路径按 requestedType 形成目标表示,再根据 scale 差乘相应 10 的幂;短 decimal 到长 decimal 还涉及目标值宽度。
这些是本提交 Parquet Reader 支持的读取转换,不等于任意 SQL CAST。读时不支持的 narrowing 不能靠“C++ static_cast 能写出来”就视为合法。
2.9.3 按 Velox kind 与文件物理类型选择 Reader
ParquetColumnReader::build 的选择逻辑体现了两层需求:INTEGER/BIGINT/HUGEINT 等可共用 IntegerColumnReader;REAL 根据 requestedType 选择 float→float 或 float→double;TIMESTAMP 还要查看 parquetType,分别选择 INT64 或 INT96 输入的读取实现。
在更底层,page encoding 决定如何从压缩块、dictionary indices、definition/repetition levels 中恢复值与嵌套/null 结构。文件 dictionary 和 Velox DictionaryVector 是不同对象;Reader 是否保留某种向量编码由具体路径决定,不能仅因文件有 dictionary 就假定输出也必然是 DictionaryVector。
2.9.4 边界之后,算子通常面对统一的 Velox Vector
Connector/TableScan 把读取结果以 RowVector 形式交给执行管线。Function 和大多数 Operator 无需直接理解 Parquet 的 INT96 或 page encoding;它们面对的是 Velox Type、Vector encoding 和选择行。predicate pushdown、lazy loading 可能把一部分过滤或取值提前到 Reader,但不会建立一条“从此不再需要逻辑 schema”的路径。
输出也是类似边界:向文件写入、经 Arrow 交换、做 exchange/spill 时,需要相应的类型映射和值序列化。比如 Arrow Bridge 的 schema export 会显式检查 decimal 参数和 DATE 身份,再生成 Arrow format;只看 NativeType 会丢掉这些信息。schema 转换成功也不代表 buffers 必然零拷贝,仍需满足布局、所有权和实现支持条件。
2.10 DECIMAL 从文件到计算结果的完整过程
设 Parquet 的 amount 为 INT64 + DECIMAL(12,2),某行存 12345。查询请求把它读成 DECIMAL(14,3),再与 DECIMAL(8,3) 的 6.789 相加。下面固定这些类型,是为了观察各层职责;实际查询由宿主计划和 Reader 配置提供对应 schema。
- 文件 schema:INT64 规定文件基础表示;DECIMAL(12,2) 规定它是 123.45。page encoding 还可能是 PLAIN 或 dictionary 等。
- Reader 类型转换:构造 file Type
DECIMAL(12,2),校验 requested TypeDECIMAL(14,3)。两者都是 BIGINT backing,并不意味着值可以原样转交。 - Reader 值转换:解码出 unscaled 12345,再按 scale 差乘 10,得到 123450。输出 Vector 保存
DECIMAL(14,3),因此数学值仍是 123.450。 - 函数绑定:两个输入逻辑类型为 DECIMAL(14,3) 与 DECIMAL(8,3)。Presto plus 的公式解析出返回
DECIMAL(15,3);选择兼容的 short/short→short 实现。 - 逐行执行:参数以 int64_t 进入 call;这次两个 scale 都是 3,不需要再次对齐。函数仍按其实现使用中间整数、检查溢出,产生 130239。
- 结果 Vector:用已绑定的
DECIMAL(15,3)创建/复用结果 Vector,存入 130239。结果含义是 130.239。 - 下游与输出:Project 的输出 RowType 保存结果列 Type。后续比较、聚合或写出继续使用这一 schema,并在需要时进入各自的 C++ 值操作或格式映射。
这个例子里没有“先从 Velox logical type 转成 C++ type,计算完成再凭 C++ type 猜回 logical type”的往返。逻辑 Type 在绑定和数据边界一直存在,C++ 类型只提供承载及操作这些值的实现方式。
2.11 类型比较、coercion、CAST 与重标记
| 操作 | 它实际做什么 | 会不会转换列值 |
|---|---|---|
TypeTraits<K> / resolver |
编译期选别名与接口 | 不会 |
CppToType<T>::create() / schema import |
构造或映射类型描述 | 单独做这一步不会 |
| kind dispatch | 运行时选择已有模板实例 | 不会自动转换 |
DecodedVector / encoding 解包 |
统一值的索引、null 与访问路径 | 可能加载或整理访问状态;不改变 SQL 数学含义 |
| coercion / common supertype | 判断允许的隐式转换与目标类型 | 描述规则本身不执行值转换 |
CastExpr / Reader 数值转换 |
按明确语义转换实际值 | 需要时做缩放、解析、宽度变化和错误处理 |
2.11.1 kindEquals、equivalent 与 equals
Type::kindEquals 递归比较 kind 与子类型结构;equivalent 是类型类定义的弱匹配;operator== 调用强匹配 equals,ROW 还比较字段名,标准 OPAQUE 还比较 C++ type_index。
| A 与 B | kindEquals |
equivalent |
*A == *B |
|---|---|---|---|
| DATE / INTEGER | true | false | false |
| DECIMAL(10,2) / DECIMAL(12,2) | true | false | false |
| ROW(a BIGINT) / ROW(b BIGINT) | true | true | false |
| ARRAY(BIGINT) / ARRAY(INTEGER) | false | false | false |
| 标准 OpaqueType<A> / OpaqueType<B>,A≠B | true | true | false |
ROW 的弱匹配忽略字段名,但递归检查子类型等价;强匹配的字段名当前区分大小写。Opaque 的 weak match 不比较 T 身份,绝不能作为安全转换任意 shared_ptr<T> 的依据。
a == b 如果比较的是两个 TypePtr,判断的是地址;*a == *b 才是类型强相等。某些 Type 使用单例,所以实现内部可做特定地址优化;通用调用者仍应选择符合意图的类型比较接口。
类型对象的 hashKind() / hash() 也要与这种区分对应。hashKind 按 kind 树计算,允许不同逻辑类型碰撞;hash 与强相等一致,考虑相应参数、ROW 字段名和 opaque 身份。注意 Type 的无参 hash() 是给类型描述算 hash;自定义 hash(value) 是给 SQL 值算 hash,两者不是同一个问题。
2.11.2 coercion 与公共类型推导
INTEGER 与 BIGINT 不等价,但方言可允许无损扩大。DECIMAL(10,2) 与 DECIMAL(12,4) 也不等价,却可能通过缩放进行保值转换。TypeCoercer 处理允许关系、公共类型与代价;函数绑定据此选择候选并记录所需转换。最终还需要执行层对值实施转换。
反方向也不能随意允许:scale 变小可能需要舍入,precision 变小可能溢出,字符串转日期可能解析失败。CAST 与 TRY_CAST 的错误结果、日期时间的时区处理等由具体 CastExpr 及方言 hooks 定义,不应由 kindEquals 推断。
2.11.3 setType 与 CastExpr 的重标记条件
BaseVector::setType 允许在底层 kind 树相同的前提下更换逻辑 Type,并要求调用者保证 vector 未被共享。它不会把所有整数乘 10,也不会自动检查每个值是否符合新的 precision。
把存有 12345 的 DECIMAL(12,2) Vector 直接改标 DECIMAL(12,3),数学含义会从 123.45 变成 12.345。要保值必须转换 unscaled value,不能只换 Type。
另一方面,特定转换确实可以只换逻辑描述。CastExpr 的 decimal reinterpret 判断 要求源/目标同为短或同为长 decimal、scale 相同、目标 precision 不小于源 precision。满足这些条件才可能走重标记优化,且还要处理 Vector encoding 与所有权。优化成立的理由来自语义证明,不能倒因为“底层都是 int64_t,所以都能重标记”。
2.12 自定义类型与类型、值的序列化
新增一种基于现有 backing 的逻辑类型,可以复用 TypeBase/ScalarType 和已有 Vector 存储,但仍要明确以下工作:
| 需要定义的契约 | 影响的使用方 |
|---|---|
| 唯一逻辑名、参数、child types、基础 kind | schema 构建、签名绑定、Vector 创建 |
| equivalent / equals / 类型 hash | schema 比较、缓存键、计划与元数据处理 |
| 值的 compare / hash,若语义与 backing 不同 | 排序、GROUP BY、JOIN、相关函数 |
| UDF tag / TypeAnalysis / resolver 或 VectorFunction | 函数签名与调用参数 |
| cast、coercion、解析和格式化规则 | SQL 表达式、边界转换 |
| 类型描述 serde 与值 serde | 计划传输、exchange、spill、外部格式 |
类型描述的序列化只回答“这是什么类型”;值的序列化回答“具体数据怎样编码和恢复”。把 DECIMAL(12,2) 的 Type 写出去,不等于已经写出了列中的 unscaled values、null 和行数。
OPAQUE 更需要注意边界:进程内可以承载 shared_ptr<T>,但指针地址不是跨进程协议。OpaqueType::serialize 依赖注册的稳定名称;对象值能否序列化、怎样恢复,需要相应 serde 支持。注册类型名不自动获得文件存储、比较、函数或跨进程传输能力。
三、从执行需求推导设计取舍
3.1 语义信息必须保留到使用它的地方
查询执行首先要求结果正确。判断一条数据的含义,必须拥有足够的信息:日期需要日期身份,decimal 需要 precision/scale,复杂值需要子类型,自定义类型可能需要专门的比较规则。这些信息不能普遍从原始 bytes 或 C++ 值类型中恢复。
只保留 C++ 类型会直接产生歧义。两个参数都是 int64_t,并不能判断应该执行 BIGINT 加法,还是对齐 scale 后执行 DECIMAL 加法;同样,使用 packed integer 存储的带时区时间,也不能直接按整数 hash 定义分组。这不是接口命名问题,而是缺少了决定结果所需的信息。
完整 Type 因而需要沿着 Vector、表达式和算子的边界传递。它可以由多批数据共享,也可以在函数初始化时被读取并转换为预计算参数;没有必要在每一行重复构造。共享方式可以优化,逻辑语义不能因为换了存储结构就消失。
3.2 基础表示值得复用,但复用有明确边界
如果每一种逻辑类型都要求一套独立的整数 buffer、dictionary 实现、读写内核和模板实例,DATE、各种 interval、几十种 DECIMAL 参数组合都会重复大量代码。复用基础表示,可以复用内存分配、向量包装、数值读取和部分执行路径。
但如果只留下 C++ 类型,信息又不够:int64_t 不知道 decimal 的 scale,不知道时间单位,不知道函数应匹配 bigint 还是 decimal(p,s)。所以 Velox 保留完整 TypePtr,在需要高效访问值时才按 kind 选取 C++ 实现。共享存储实现,不等于共享所有运算语义。
这个分工也避免把每个 SQL 值做成“带虚函数的对象”。类型元数据通常按列、按表达式或按函数实例共享,热循环处理原生值或轻量 view;需要参数的函数可在初始化时读取 Type 并预计算。
由这些约束可以推导出三种方案的差异:
| 方案 | 能保留什么 | 需要承担什么 |
|---|---|---|
| 只用 C++ 原生值类型 | 简单、直接的值操作 | 另找位置保存逻辑身份与参数,否则无法正确绑定和计算 |
| 为每种逻辑类型建立独立的值类和存储实现 | 逻辑差异可以直接体现在类上 | 重复实现存储、编码和访问,类型组合与模板实例更多 |
| 共享基础表示,保留独立 Type 元数据 | 复用底层实现,同时维持完整语义 | 每条执行路径都要明确哪些操作能共享,哪些必须检查逻辑类型 |
Velox 采用第三种分工,但它不会自动保证所有新代码都正确。一个按 kind 实例化的 kernel 仍需证明其操作适用于这个 kind 下的相关逻辑类型。搬运整数与对整数进行具有业务含义的加减、比较,不是同等强度的假设。
3.3 运行时 schema 与编译期内核的连接
查询的字段组合、decimal 参数以及具体输入类型在运行时才确定;C++ 模板实例则必须在编译期形成。只采用其中一侧,都不足以完成通用查询执行。
Velox 用 Type 对象表达运行时 schema,用有限的 kind 集合及 traits 关联到编译期实现,再由分派选择合适的实例。DECIMAL 的 p/s 不必成为每种值类的 C++ 模板常量:同一整数实现可以服务多个参数组合,具体语义通过绑定结果和函数初始化补齐。
这种结构把类型判断放在适合的粒度。函数签名绑定、类型检查和部分参数计算可以在执行准备或实例初始化时完成;随后对一批值执行同一类原生操作。它为减少每个值上的动态处理提供了条件,也便于复用向量访问和批处理路径。具体 kernel 能获得多少收益,仍取决于实现和数据特征,类型分层本身不是性能结论。
代价是代码需要维护运行时与编译期信息的一致性。traits 特化、UDF tag、物理签名和真实 outputType 必须相互吻合;分派范围越大,模板实例和编译成本也可能越高。源码中保留的检查,以及逻辑绑定后再核对 physical signature,正是为了约束这条边界。
3.4 类型语义与编码独立,执行路径才能灵活组合
一列相同类型的数据在不同阶段可能适合不同表示:常量可以复用一个值,dictionary 可以通过索引共享底层数据,lazy vector 可以推迟加载,flat buffer 则便于直接批处理。若把这些选择写进逻辑类型,函数签名和 schema 就会随着中间表示变化而膨胀。
保持逻辑 Type 稳定,让编码成为 Vector 的职责,可以使相同函数面对多种表示。DecodedVector 和 reader/view 等接口承担编码适配;具体函数也可以在接口约定允许时选择更直接的路径。复杂类型同样把 schema 树与 offsets/sizes/children 数据结构分开,便于跨行共享和批量访问。
独立不等于彼此无关。C++ 值表示、Vector 编码、null 规则和 buffers 的生命周期必须共同满足访问条件。已经知道一列是 BIGINT,不代表已经证明它是 flat;已经获得 StringView,也不代表它独立拥有字符串内容。
3.5 类型转换应显式承担信息和数值变化
逻辑类型到 kind、kind 到 C++ 值类型的映射通常是多对一的。由此自然得到一个工程约束:从较少的信息不能普遍重建较多的信息。CppToType<int64_t> 可以给出约定的 BIGINT,但无法推回原来的 decimal 参数;只保存 kind 的序列化格式也无法完整恢复复杂逻辑 schema。
修改描述和转换值也必须区分。DECIMAL 改 scale 时,若目标是保持数学值,通常需要缩放 unscaled integer;修改 Vector 的 Type 标签本身不会完成这个工作。相反,同 scale、可容纳原值的 precision 扩大,在满足表示和所有权条件时可以复用数据。重标记优化应来自语义与表示条件的证明。
同样的原则适用于 I/O。文件 schema 映射规定输出应该是什么;解码和数值转换负责产出符合要求的值。一个文件类型能够映射为 Velox Type,并不能单独证明文件 buffers 可以零拷贝交给执行层,还需要满足布局和所有权要求。
3.6 扩展类型是在扩展执行契约
基于现有 kind 新增逻辑类型时,需要先确定哪些行为可以继承,哪些必须覆盖。若只是展示名称不同,所需工作可能较少;若比较、hash、时间单位或允许的转换发生变化,影响范围会进入函数绑定、算子以及跨进程边界。
因此扩展工作应围绕可观察行为展开:Type 的相等关系是否合适,函数签名能否区分它,值比较与 hash 是否一致,Vector 能否保持完整描述,cast 是否有定义,类型和值的 serde 能否恢复同一契约。新增一个类型名或一个 traits 特化,只完成了其中一部分。
这套设计的取舍落在一个明确边界上:类型语义按 schema 保留,基础表示按实现复用;任何跨越这条边界的操作,都要说明自己依赖哪些信息、允许丢掉哪些信息,以及是否真的改变了值。 前面区分的 logical type、physical kind、C++ type,以及绑定、分派、解码和 cast,都可以据此判断各自的职责。
附录:源码索引与参考资料
| 要回答的问题 | 优先阅读 |
|---|---|
| 某逻辑类型基于哪个 kind,有什么参数? | type/Type.h、具体 custom type 类 |
| kind 的 NativeType / ImplType 是什么? | type/TypeKind.h |
| 某个 C++ T 会创建什么 Type? | type/CppToType.h 与 type/SimpleFunctionApi.h |
| UDF tag 如何形成逻辑签名? | type/SimpleFunctionTags.h、core/SimpleFunctionMetadata.h |
| tag 如何形成 call 参数? | expression/UdfTypeResolver.h、VectorReaders.h、VectorWriters.h |
| 重载、decimal 参数和返回类型怎样确定? | expression/SignatureBinder.cpp、SimpleFunctionRegistry.cpp |
| Vector 保存什么 Type,如何按 kind 创建? | vector/BaseVector.{h,cpp}、SimpleVector.h |
| dictionary/null 怎样读取? | vector/DecodedVector.h 与具体 encoding 类 |
| 算子是否尊重自定义比较? | vector/SimpleVector.h、exec/RowContainer.cpp、具体算子 |
| 文件类型与请求类型怎样映射? | dwio/parquet/reader/ParquetReader.cpp、ParquetColumnReader.cpp |
| 转换是否真的处理了值? | expression/CastExpr.cpp、具体 ColumnReader / 函数 |
写代码时可以用三个检查帮助定位错误:只有 T 时,是否已经丢了逻辑语义;只有 kind 时,是否遗漏参数或自定义行为;拿到 Vector 时,是否错误假定它一定 flat、一定没有 null。 这三个问题分别对应本文的 traits、Type 和 encoding 分工。
文中源码摘录省略了无关成员,switch 示例是分派原理示意。随文提供的 C++ 接口核对示例 单独检查 traits、schema/Vector 构造与 Date UDF 注册接口;验证范围见文末说明。文中的十进制数字也可直接按给出的 scale 公式逐步核算。
参考资料与验证范围
源码范围:2026-09-20 本地 Velox 提交 1d1b765678702e,源码链接固定到该提交。函数例子使用 Presto 方言,文件读取例子使用 Parquet;具体 SQL 转换和函数规则由方言与宿主集成决定。
- Velox Type 开发文档
- Type 与比较测试、UDF TypeAnalysis 测试
- Presto decimal 函数实现与注册约束
- C++ 非类型模板参数
- Enumeration as a template parameter
- Enum reflection 与模板特化背景
- Concepts / placeholder type deduction 讨论
- 相关博客:Velox 架构、Velox Primer。
验证说明:源码关系逐项核对到固定提交。随文 C++ 示例使用当前工程的编译选项完成语法与模板实例化检查,未链接执行查询;未运行整个 Velox 测试集。配图经过 SVG 几何检查与 Chromium 渲染检查,文章经过本地 Hexo 生成及桌面、平板、手机布局检查。