LightDB/PostgreSQL的类型系统

类型系统

在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
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值