1. 1. 一、类型体系与使用边界
    1. 1.1. 1.1 Velox 类型系统描述什么
    2. 1.2. 1.2 Logical type、physical type、C++ type 的定义
    3. 1.3. 1.3 三者的层级关系:语义、基础表示、语言实现
    4. 1.4. 1.4 Function、Vector、Operator 分别用哪一层
    5. 1.5. 1.5 Vector:逻辑 schema 与数据表示同时存在
    6. 1.6. 1.6 Function:逻辑签名与 C++ 调用接口
    7. 1.7. 1.7 Operator:逻辑 schema 约束数据,内部状态由算法决定
    8. 1.8. 1.8 从文件到计算:变化的是表示,保留的是类型契约
  2. 2. 二、类型描述、实现关联与转换
    1. 2.1. 2.1 Type 对象如何保存逻辑身份与基础表示
      1. 2.1.1. 2.1.1 一套 Type 继承树,两种描述用途
      2. 2.1.2. 2.1.2 kind 保存在基类,逻辑差异由子类实现
      3. 2.1.3. 2.1.3 DATE 的 name、kind 与 NativeType
      4. 2.1.4. 2.1.4 DECIMAL:复用整数表示,保留十进制契约
      5. 2.1.5. 2.1.5 physicalType 为什么仍然是 TypePtr
    2. 2.2. 2.2 TypeTraits 如何关联 kind 与 C++ 类型
      1. 2.2.1. 2.2.1 ImplType 与 NativeType 分别是什么
      2. 2.2.2. 2.2.2 traits 的 primitive 与 fixed-width 属性
    3. 2.3. 2.3 类型工厂与 CppToType 的映射路径
      1. 2.3.1. 2.3.1 已知业务语义时,直接创建完整 Type
      2. 2.3.2. 2.3.2 只有 C++ 类型时,CppToType<T> 选择一个约定映射
      3. 2.3.3. 2.3.3 映射的信息损失与反向构造
      4. 2.3.4. 2.3.4 递归容器特化与一个容易误读的 Date 细节
    4. 2.4. 2.4 复杂 schema、数据树与 null
      1. 2.4.1. 2.4.1 null 不写在 TypeKind 里
    5. 2.5. 2.5 Vector 如何同时保存类型描述和数据
      1. 2.5.1. 2.5.1 BaseVector::create 的类型分派
      2. 2.5.2. 2.5.2 FlatVector 的标量表示与专用布局
      3. 2.5.3. 2.5.3 Dictionary 与 DecodedVector 的访问路径
    6. 2.6. 2.6 运行时 kind 如何选择编译期模板
    7. 2.7. 2.7 函数签名分析、绑定与执行参数
      1. 2.7.1. 2.7.1 tag 负责表达签名,resolver 负责形成 C++ 调用参数
      2. 2.7.2. 2.7.2 TypeAnalysis 生成逻辑与物理签名
      3. 2.7.3. 2.7.3 SignatureBinder 与 Registry 的候选选择
      4. 2.7.4. 2.7.4 DECIMAL 的参数怎样真正参与计算
      5. 2.7.5. 2.7.5 Vector Function 直接拿到向量与完整输出类型
    8. 2.8. 2.8 表达式、算子与自定义比较
      1. 2.8.1. 2.8.1 计划与表达式携带的是 Velox Type
      2. 2.8.2. 2.8.2 Operator 的输入输出仍然是带 Type 的 RowVector
      3. 2.8.3. 2.8.3 带时区时间的 compare/hash 调用路径
    9. 2.9. 2.9 文件 schema 映射与 Reader 值转换
      1. 2.9.1. 2.9.1 Parquet 物理类型与逻辑注解的映射
      2. 2.9.2. 2.9.2 fileType 与 requestedType 同时存在
      3. 2.9.3. 2.9.3 按 Velox kind 与文件物理类型选择 Reader
      4. 2.9.4. 2.9.4 边界之后,算子通常面对统一的 Velox Vector
    10. 2.10. 2.10 DECIMAL 从文件到计算结果的完整过程
    11. 2.11. 2.11 类型比较、coercion、CAST 与重标记
      1. 2.11.1. 2.11.1 kindEquals、equivalent 与 equals
      2. 2.11.2. 2.11.2 coercion 与公共类型推导
      3. 2.11.3. 2.11.3 setType 与 CastExpr 的重标记条件
    12. 2.12. 2.12 自定义类型与类型、值的序列化
  3. 3. 三、从执行需求推导设计取舍
    1. 3.1. 3.1 语义信息必须保留到使用它的地方
    2. 3.2. 3.2 基础表示值得复用,但复用有明确边界
    3. 3.3. 3.3 运行时 schema 与编译期内核的连接
    4. 3.4. 3.4 类型语义与编码独立,执行路径才能灵活组合
    5. 3.5. 3.5 类型转换应显式承担信息和数值变化
    6. 3.6. 3.6 扩展类型是在扩展执行契约
  4. 4. 附录:源码索引与参考资料
    1. 4.1. 参考资料与验证范围
Macduan Notes

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++ 类之间存在继承关系。

图 1:逻辑类型到 physical kind,再到 C++ 值类型的表示关联。DATE 与 INTEGER 共享基础表示;Type 的真实类继承另见第 2.1 节,Vector 编码负责批数据布局。
图 1:逻辑类型到 physical kind,再到 C++ 值类型的表示关联。DATE 与 INTEGER 共享基础表示;Type 的真实类继承另见第 2.1 节,Vector 编码负责批数据布局。

这个关联通常是多对一的。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。

图 2:同一列 DATE 在各组件中的类型:逻辑接口保留 DATE,物理类别为 INTEGER,C++ 值类型为 int32_t;函数元数据另外用 INTEGER() 描述实现要求。
图 2:同一列 DATE 在各组件中的类型:逻辑接口保留 DATE,物理类别为 INTEGER,C++ 值类型为 int32_t;函数元数据另外用 INTEGER() 描述实现要求。

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 类型转换,分别处理不同的问题。

图 3:从 I/O 到计算的整体关系:文件 schema 映射为 Velox schema,完整类型信息与数据共同进入 Vector、Function 和 Operator。
图 3:从 I/O 到计算的整体关系:文件 schema 映射为 Velox schema,完整类型信息与数据共同进入 Vector、Function 和 Operator。

至此可以确定执行层的边界: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 继承树。下图画出本文涉及的真实继承路径;箭头由子类指向父类,别名写在对应模板实例旁,不把别名误画成一个新类。

图 4:Type.h 中的部分实际继承结构。IntegerType 是 ScalarType<INTEGER> 的别名;DateType 继承该类。ARRAY/MAP/ROW 直接走各自的 TypeBase 实例。虚线箭头表示 C++ 继承,不表示 SQL 类型等价或值转换。
图 4:Type.h 中的部分实际继承结构。IntegerType 是 ScalarType<INTEGER> 的别名;DateType 继承该类。ARRAY/MAP/ROW 直接走各自的 TypeBase 实例。虚线箭头表示 C++ 继承,不表示 SQL 类型等价或值转换。

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 对象表示的物理类型树。

图 5:Date 与 Array<Date> 的函数分析结果。逻辑签名保留 date,physicalType 使用 INTEGER() 或 ARRAY(INTEGER());实际输入 Vector 仍携带 DATE() 或 ARRAY(DATE()),没有被重标记。
图 5:Date 与 Array<Date> 的函数分析结果。逻辑签名保留 date,physicalType 使用 INTEGER() 或 ARRAY(INTEGER());实际输入 Vector 仍携带 DATE() 或 ARRAY(DATE()),没有被重标记。

例如 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 值的算法。

图 6:TypeTraits 与 CppToType 不是互逆函数。逻辑参数一旦只投影成 kind 或 C++ T,通常无法恢复。
图 6:TypeTraits 与 CppToType 不是互逆函数。逻辑参数一旦只投影成 kind 或 C++ T,通常无法恢复。

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 或字符串。

图 7:同一个复杂 schema 的类型树与数据结构。字段名、decimal 参数属于 Type;行数、offsets、sizes 和 null 属于 Vector。
图 7:同一个复杂 schema 的类型树与数据结构。字段名、decimal 参数属于 Type;行数、offsets、sizes 和 null 属于 Vector。

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 的访问路径

图 8:DictionaryVector 仍携带 DECIMAL(12,2)。先映射逻辑行,再检查 null、读取 unscaled value,最后按 scale 解释。
图 8:DictionaryVector 仍携带 DECIMAL(12,2)。先映射逻辑行,再检查 null、读取 unscaled value,最后按 scale 解释。

假设底层值为 [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 条件。

图 9:同一个 UDF tag 同时进入签名分析与执行类型解析。Date 的 int32_t 调用参数并不会让函数签名变成 integer。
图 9:同一个 UDF tag 同时进入签名分析与执行类型解析。Date 的 int32_t 调用参数并不会让函数签名变成 integer。

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 大致经历以下步骤:

  1. 取得函数调用的实际 std::vector<TypePtr>,遍历候选 FunctionSignature。
  2. SignatureBinder 绑定类型变量、decimal 的整数参数等。普通具体类型会比较逻辑 name(),再检查或绑定参数;如果允许 coercion,还会咨询传入的 TypeCoercer。
  3. 对绑定成功的候选,递归核对参数的 physical kind 与该 C++ 实现记录的 argPhysicalTypes。其中用于 generic 等适配的 UNKNOWN physical 占位可匹配任意类型,不等于要求实际输入必须是 UNKNOWN。
  4. 解析完整返回 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 毫秒。因此按它的语义,两个值可以相等,尽管原始整数不相等。

图 10:共享 int64_t 存储不代表可以使用同一套比较。GROUP BY / JOIN 的 hash 与 equality 必须遵循逻辑类型。
图 10:共享 int64_t 存储不代表可以使用同一套比较。GROUP BY / JOIN 的 hash 与 equality 必须遵循逻辑类型。

这个 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。

  1. 文件 schema:INT64 规定文件基础表示;DECIMAL(12,2) 规定它是 123.45。page encoding 还可能是 PLAIN 或 dictionary 等。
  2. Reader 类型转换:构造 file Type DECIMAL(12,2),校验 requested Type DECIMAL(14,3)。两者都是 BIGINT backing,并不意味着值可以原样转交。
  3. Reader 值转换:解码出 unscaled 12345,再按 scale 差乘 10,得到 123450。输出 Vector 保存 DECIMAL(14,3),因此数学值仍是 123.450。
  4. 函数绑定:两个输入逻辑类型为 DECIMAL(14,3) 与 DECIMAL(8,3)。Presto plus 的公式解析出返回 DECIMAL(15,3);选择兼容的 short/short→short 实现。
  5. 逐行执行:参数以 int64_t 进入 call;这次两个 scale 都是 3,不需要再次对齐。函数仍按其实现使用中间整数、检查溢出,产生 130239。
  6. 结果 Vector:用已绑定的 DECIMAL(15,3) 创建/复用结果 Vector,存入 130239。结果含义是 130.239。
  7. 下游与输出:Project 的输出 RowType 保存结果列 Type。后续比较、聚合或写出继续使用这一 schema,并在需要时进入各自的 C++ 值操作或格式映射。
图 11:相同的 int64_t 在读取与函数执行两处都受到 Type 参数约束。12345 → 123450 是保值缩放,130239 是新计算结果。
图 11:相同的 int64_t 在读取与函数执行两处都受到 Type 参数约束。12345 → 123450 是保值缩放,130239 是新计算结果。

这个例子里没有“先从 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 使用单例,所以实现内部可做特定地址优化;通用调用者仍应选择符合意图的类型比较接口。

图 12:先确定要比较哪层信息,再选择接口。类型等价与允许转换是两个独立的问题。
图 12:先确定要比较哪层信息,再选择接口。类型等价与允许转换是两个独立的问题。

类型对象的 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 转换和函数规则由方言与宿主集成决定。

验证说明:源码关系逐项核对到固定提交。随文 C++ 示例使用当前工程的编译选项完成语法与模板实例化检查,未链接执行查询;未运行整个 Velox 测试集。配图经过 SVG 几何检查与 Chromium 渲染检查,文章经过本地 Hexo 生成及桌面、平板、手机布局检查。