类型系统
在LightDB中,支持了丰富的数据类型,函数,操作符,这些功能对类型系统有很大影响。
而LightDB中的操作符等价于函数,并且以重载的方式来实现,在一些场景下,重载函数的选择结果可能会造成用户的困惑。
所以有必要深入了解类型系统的实现策略。
常量类型
在SQL语句中包含的常量是什么类型呢? 例如: SELECT 1
这里数字 1
是smallint,integer还是bigint?
对于数字类型的常量类型是按照它的值来决定的,在语法解析后,依次判断integer,
bigint, numeric是否能容纳这个数字, 所以 SELECT 1
的类型是integer 但
SELECT 1111111111111
的类型是bigint,
如果是更大的数字,则类型是numeric, 而浮点数始终是numeric。
这个特性会影响到重载函数的识别, 例如:
-- 有如下函数定义
create function tf1(numeric) ...
create function tf1(float4) ...
create function tf1(integer) ...
create function tf1(bigint) ...
select tf1(1.0); -- 调用numeric版本
select tf1(1); -- 调用integer版本
select tf1(111111111111111); -- 调用bigint版本
select tf1(1111111111111111111111111111111); -- 调用numeric版本
unknow类型
unknow类型是LightDB类型中最容易误解的一部分,因为它的定义看起来像字符串,而且在大部分时候,它也是作为字符串来处理的。
事实上,在LightDB的语法解析层面并没有字符串类型的语法单元,
所有以单引号包起来的字符串都是未知类型,在语法解析层面不能确定其真正类型,需要在语法解析完成后,
根据上下文的语义来判断出真正的类型。
它的存在是LightDB类型系统的复杂和一些奇怪行为的根源。
create function tf2(text) ...
create function tf2(integer) ...
-- 在语法层面,'1'的类型是未知的
-- 所以不能很容易确定调用哪个版本
select tf2('1');
为了解决这个问题,LightDB定义了一系列规则来实现尽可能看起来合理的行为,
比如上面的例子中,实际上是会调用text版本的函数,因为unknow类型首先看起像字符串,
所以在规则设计中,赋予了字符串类型较高的优先级,尽量符合大家对unknow类型理解为字符串的一个预期。
如果没有text类型的重载版本,则很可能会产生一个错误,例如:
create function tf3(bigint) ...
create function tf3(integer) ...
select tf3('1');
其产生的错误信息如下:
ERROR: function tf3(unknown) is not unique
LINE 1: select tf3('1');
^
HINT: Could not choose a best candidate function. You might need to add explicit type casts.
JDBC中对于unknow类型的处理
在LightDB的JDBC驱动中,默认情况下没有unknow类型,所有的String类型参数会被明确指定为varchar类型。
这种情况下,LightDB认为这个类型是明确的,不会触发unknow类型的识别机制。
例如:
create function tf4(integer) ...
select tf4('1'); -- OK
因为tf4是唯一的,所以LightDB会尝试将unknow类型识别为integer类型,但是在java中则会报错
String sql = " SELECT tf4(?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "1");
ps.execute();
ERROR: function tf4(character varying) does not exist
这个错误和直接执行 select tf4('1'::varchar);
的错误是一样的。
在JDBC中提供了stringtype选项,如果指定stringtype=unspecified则会将string类型处理成unknow类型,
这样就可以享受到LightDB的unknow类型识别机制,上面的代码就不会报错。
但是这样也带来了unknow类型的不确定性。
LightDB函数和操作符
从类型的角度上看,函数和操作符并没有太大差别,在代码实现上相同的。
函数比操作符稍微复杂些,函数需要考虑变长参数(VARIADIC),默认参数的情况。
VARIADIC参数展开
如果有函数是变长参数,则会被展开为相应个数的参数列表,参与匹配。
create function tf5(VARIADIC integer[]) ...
-- tf5会被当成tf5(integer, integer)来处理
select tf5(1,1);
变长参数函数的优先级低于普通函数,所以可以用普通函数来覆盖变长参数函数。
create function tf5(VARIADIC integer[]) ...
create function tf5(integer) ...
create function tf5(integer, integer) ...
select tf5(1) -- 调用 integer版本
select tf5(1,1) -- 调用 integer,integer版本
select tf5(1,1,1) -- 调用VARIADIC版本
默认参数
对于函数默认参数的设计需要注意的是,在创建函数的时候并没有过多的校验,
所以像下面的函数集是可以创建成功的,但是在调用时候就会出错。
create function tf6(integer) ...
create function tf6(integer, integer = 5) ...
select tf6(1); -- 错误
ERROR: function tf6(integer) is not unique
所以对于非默认参数列表相同的重载函数,是会有冲突的,并且在运行时才能检查出来。
重载和类型识别规则
下面详细讲述这个规则(在官方文档Chapter 10. Type Conversion有相关描述),
在这里需要解决的问题是根据用户输入的参数列表,从多个同名重载函数或者操作符中选择最合适的一个。
选择成功后,便可依据被选中的函数或者操作符的参数列表,确定unknow的具体类型,并为所有需要隐式转换的参数增加
隐式类型转换动作。
在实参类型有unknow类型的时候,unknow类型不能给重载函数的识别带来有效的帮助。所以需要
依赖一些特殊的规则来减少unknow和重载的矛盾。
实际匹配流程是按如下方法顺序进行的,只要某个步骤能在找到唯一的匹配,则就成功了。否则继续往下按其他方案匹配,
直到按照某个步骤找到唯一匹配。
如果下面所有步骤走完,还是没有找到唯一的匹配,则会报错。
下面的步骤在官方文档也有描述,但是比较粗略,不容易理解,我在这里尝试用更易懂的措辞来描述,
为每个步骤取了个较为明确的名字,并且都提供代码样例。
精确匹配
精确匹配肯定是最优先的,如果实参列表不包含unknow类型,且类型和某个函数形参类型列表完全一致,
那肯定就它没错。
create function tf7(int2) ...
create function tf7(int4) ...
select tf7('1'::int2); -- 调用int2版本
select tf7('1'::int4); -- 调用int4版本
select tf7(1); -- 调用int4版本
如果输入有unknow类型,则这一步肯定找不到。
隐式转换
如果能通过隐式转换找到唯一的函数,则匹配成功
create function tf8(int4) ...
create function tf8(text) ...
-- int2存在到int4的隐式转换
select tf8(1::int2); -- OK, 调用int4版本
-- 内置操作符|/的形参类型是float8,而实参20的类型是integer
-- 但是存在integer到float8的隐式转换,所以是ok的
select |/20; -- OK, 调用float8版本
如果通过隐式转换找不到唯一的一个,则这一步会匹配失败
create function tf9(int4) ...
create function tf9(int8) ...
-- int2同时存在到int4,int8的隐式转换
select tf9(1::int2);
ERROR: function tf9(smallint) is not unique
在这一步同时会尝试识别unknow类型,如果unknow类型可以匹配任何类型,能找到唯一的一个匹配的话,也匹配成功。
create function tf10(int4, int4) ...
select tf10(1, '1'); -- OK
select tf10(1, 'AB');
ERROR: invalid input syntax for type integer: "AB"
从上面的错误提示中,也可以看到,已经在尝试把unknow类型AB转换成数字了,说明函数匹配已经成功了。
类型提升
如果上面隐式转换还不能唯一识别的话,会对所有domain类型的实参做类型提升,再尝试匹配。
create domain mytext as varchar;
create function tf11(varchar) ...
create function tf11(text) ...
-- mytext类型会被提升为varchar类型
select tf11('11'::mytext); -- OK,调用varchar版本
在这里做类型提升的策略,对domain和unknow类型混用的情况不是太友好。
create domain mytext as varchar;
create function tf31(mytext, text) ...
create function tf31(varchar, text) ...
select tf31('1'::mytext, '1'); -- OK, 调用(varchar, text)版本
在这里,更准确的选择应该是(mytext, text)版本。
但是因为这个语句包含有unknow类型,所以第一步精确匹配失败,
因为mytext可以转换成varchar,所以在第二步隐式转换时,两个重载版本同时匹配,不唯一,所以第二步匹配失败。
到现在这一步,直接把mytext提升为varchar类型,导致(mytext,text)版本失去了匹配的机会。
Preferred type优先
这里有一个新概念: Preferred
type,LightDB把数据类型分为几大类, 其中,数字类型(Numeric
types)包括了int2,int4,float4等所有数字类型, 而字符串类型(String
types)包括了text,varchar,char等所有字符串类型。
对于每个大类,可以定义对应的Preferred type。默认情况下,
数字类型的Preferred type是float8, 字符串类型的Preferred type是text,
Prefered type在对应的大类中具有优先权。
create function tf12(bigint) ...
create function tf12(float8) ...
select tf12(1); -- OK, 调用float8版本
create function tf13(text) ...
create function tf13(varchar) ...
select tf13('1'::char(1)); -- OK, 调用text版本
如果不知道Preferred type概念,就无法理解上面例子中的行为。
unknow类型匹配
到这一步需要尝试利用unknow类型的灵活性做选择,即基于实际情况,让unknow优先匹配哪种类型,
LightDB基于两个原则: 字符串优先和preferred type优先。
因为unknow类型看起来更像是字符串,所以如果重载函数对应位置的参数是字符串类型的话,会优先匹配。
create function tf14(text) ...
create function tf14(float8) ...
select tf14('1'); -- OK, 调用text版本
create function tf15(bool) ...
create function tf15(float8) ...
select tf15('1'); -- ERROR
ERROR: function tf15(unknown) is not unique
如果没有字符串类型,但所有的重载函数参数都同属一个大类的话,会优先选择这个大类中的preferred
type类型。
create function tf16(int4) ...
create function tf16(float8) ...
select tf16('1'); -- OK,调用float8版本
如果有不属于同一个大类的,则会报错
create function tf17(int4) ...
create function tf17(float8) ...
create function tf17(bool) ...
select tf17('1');
ERROR: function tf17(unknown) is not unique
下面在举两个操作符的例子,说明同样的问题。
对于取绝对值的@操作符,有多个重载版本:
ltsql> \do @
Schema | Name | Right arg type | Result type
-----------+------+------------------+------------------
pg_catalog | @ | bigint | bigint
pg_catalog | @ | double precision | double precision
pg_catalog | @ | integer | integer
pg_catalog | @ | numeric | numeric
pg_catalog | @ | real | real
pg_catalog | @ | smallint | smallint
我们看到@操作符的所有重载版本,都是数字类型,并且有float8(double
precision)的重载版本, 所以对于unknow类型,会选择float8版本
select @ '1' -- OK, 调用float8版本
select @ 1 -- OK, 调用integer版本
而对于~操作符,它的重载版本包含多个类型大类
ltsql> \do ~
Schema | Name | Right arg type | Result type
-----------+------+----------------+-------------
pg_catalog | ~ | bigint | bigint
pg_catalog | ~ | bit | bit
pg_catalog | ~ | inet | inet
pg_catalog | ~ | integer | integer
pg_catalog | ~ | macaddr | macaddr
pg_catalog | ~ | macaddr8 | macaddr8
pg_catalog | ~ | smallint | smallint
所以它不支持unknow类型的调用,因为它没有text版本,重载类型包含多个大类,所以无法唯一确定。
select ~ '1';
ERROR: operator is not unique: ~ unknown
select ~ 1; -- OK, 调用integer版本
已知类型优先
如果同时存在已知类型和未知类型,且已知类型的所有实参类型一样,则假设所有的unknow类型也是这个类型,
如果能找到唯一的一个匹配的话,就OK.
create function tf29(int4, int4) ...
create function tf29(int4, bool) ...
select tf29(1, '1'); -- OK, 调用(int4, int4)版本
create function tf30(bool, int4) ...
create function tf30(bool, bool) ...
select tf30(true, '1'); -- OK, 调用(bool, bool)版本
如果经过以上步骤还是不能找到唯一的匹配,则会报错。
泛型
LightDB定义了一系列称为Pseudo-types的类型集合,包括anyelement,
anyarray, anynonarray等,
这种类型不能用来建表,只能用来定义函数或操作符的参数类型或返回值类型。
但是他的定义也很容易让人误解。假设有如下定义
create function tf19(anyelement, anyelement)
有了这个函数,不代表你就可以为所欲为了。它有个严格的限制:
两个参数的类型必须完全一样。 甚至不支持隐式转换。
select tf19(1::int2, 1::int4);
ERROR: function tf19(smallint, integer) does not exist
select tf19('1'::varchar, '1'::text);
ERROR: function tf19(character varying, text) does not exist
select tf19(1::int2, 1::int2); -- OK
select tf19('1'::varchar, '1'::varchar); -- OK
所以从语义上看,这个更像是其他语言中的泛型 void tf19(T a, T b)
对于不同Pseudo-types类型混用的情况同样存在限制,例如
create function tf20(anyelement, anyarray) ...
select tf20(1, ARRAY[1]); -- OK
select tf20('1'::varchar, ARRAY['1'::varchar]); -- OK
select tf20(1::int2, ARRAY[1]);
ERROR: function tf20(smallint, integer[]) does not exist
select tf20('1'::varchar, ARRAY[1]);
ERROR: function tf20(character varying, integer[]) does not exist
tf20函数要求数组成员的类型和第一个参数类型一致,
它的语义有点像其他语言中的定义 void tf20(T a, T[] b)
在Pseudo-types类型中还有一系列compatible类型,它对类型一致性的限制稍微宽容些,可以支持隐式转换。
create function tf21(anycompatible, anycompatible) ...
select tf21(1::int2, 1::int4); -- OK
select tf21(1::int2, '1'::varchar);
ERROR: function tf21(smallint, character varying) does not exist
Pseudo-types类型的返回值同样受到约束
create function tf22(anyelement, anyelement) returns anyelement
as
$$ select 'ab' $$
language sql;
select tf22(1,1);
ERROR: return type mismatch in function declared to return integer
select tf22('1'::text, '1'::text); -- OK
因为要确定参数类型,所以实参中,Pseudo-types位置的参数不能全部是unknow,否则无法判断泛型类型,从而报错。
create function tf23(anyelement) ...
select tf23('1');
ERROR: could not determine polymorphic type because input has type unknown