完全指南:在 STRICT 表中用纯 SQL 扩展类型系统)
Turso 自定义类型Custom Types完全指南在 STRICT 表中用纯 SQL 扩展类型系统【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso导读Turso一个用 Rust 实现的 SQL 数据库在 SQLite 兼容的 STRICT 表类型系统之上提供了一套用户自定义类型Custom Types机制你可以用纯 SQL 声明值的编码ENCODE/ 解码DECODE规则、存储层的域约束、操作符重载与默认值从而把业务类型语义直接下沉到存储引擎。读完本文你将掌握CREATE TYPE/DROP TYPE的完整语法、参数化类型、操作符与排序规则、默认值与 CHECK 约束的协作方式以及如何通过PRAGMA list_types和sqlite_turso_types虚拟表检视类型系统——所有示例均可直接在本仓库的 CLI 中运行验证。一、概述类型系统的扩展点Turso 对 SQLite 的 STRICT 表类型系统做了扩展允许用户定义自定义类型。核心思想是自定义类型定义了一个值在写盘之前的编码方式、读盘之后的解码方式并在存储层强制执行域约束、绑定操作符、提供默认值——全部用纯 SQL 声明。从源码结构看这一设计在 core/schema.rs 中有完整实现Schema维护一个type_registry: HashMapString, ArcTypeDef类型注册表core/schema.rs每个TypeDef对应一种类型定义TypeDefKind::Custom承载params类型参数、base基础存储类型、encode/decode表达式、operators操作符重载与default默认值表达式五个核心字段core/schema.rs。运行时通过get_type_def查表并把自定义类型解析为ResolvedType含从命名类型到最终原始类型的chain链见 core/schema.rs。启用前提自定义类型只在 STRICT 表上工作本仓库中 STRICT 表默认启用。文档给出的启用方式tursodb mydb.db在本仓库当前源码中自定义类型功能由一个显式开关控制CLI 的--experimental-custom-types参数声明于 cli/app.rshelp 文本为 Enable experimental custom types (CREATE TYPE / DROP TYPE)并通过with_custom_types(...)传给核心连接cli/app.rs。如果没有启用该功能以下能力全部不可用CREATE TYPEDROP TYPEsqlite_turso_types虚拟表所有内置自定义类型date、varchar、numeric 等此时PRAGMA list_types只会显示 SQLite 的五种基础类型INTEGER、REAL、TEXT、BLOB、ANY。二、创建类型CREATE TYPE 语法CREATE TYPE type_name BASE base_type ENCODE encode_expr DECODE decode_expr [OPERATOR op [function_name] ...] [DEFAULT default_expr];各子句含义BASE—— 底层 SQLite 存储类型text、integer、real、blob。这是值真正落盘时的物理表示。ENCODE—— 写盘前作用于value的表达式。DECODE—— 读盘时作用于value的表达式。OPERATOR—— 可选为类型声明操作符重载。若省略function_name则使用基础类型的内置比较见下文排序一节。DEFAULT—— 可选当该类型列未提供值时使用的默认值。特殊标识符value指代正在被编码或解码的输入值。从实现看CREATE TYPE语句经解析后由TypeDef::from_create_type构造为TypeDefcore/schema.rs其原始 SQL 会被完整保存TypeDef.sql字段用于持久化往返。三、删除类型DROP TYPEDROP TYPE type_name; DROP TYPE IF EXISTS type_name;限制只要还有任何表的列在使用该类型就不能删除它。删除操作对应源码中的type_registry.remove(...)见 core/schema.rs注册表以类型名的小写形式为键。四、基础示例4.1 恒等类型透传最简单的自定义类型——存储与读取都不改变值CREATE TYPE passthrough BASE text ENCODE value DECODE value; CREATE TABLE t1(val passthrough) STRICT; INSERT INTO t1 VALUES (hello); SELECT val FROM t1; -- hello4.2 反转文本ENCODE 在存储时反转字符串DECODE 在读取时再反转回来CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value); CREATE TABLE t1(val reversed) STRICT; INSERT INTO t1 VALUES (hello); SELECT val FROM t1; -- hello (stored on disk as olleh)4.3 分基于表达式的编解码把金额以整数分存储但查询时以整数元呈现CREATE TYPE cents BASE integer ENCODE value * 100 DECODE value / 100; CREATE TABLE prices(amount cents) STRICT; INSERT INTO prices VALUES (42); SELECT amount FROM prices; -- 42 (stored on disk as 4200)4.4 JSON 校验用json()作为编码器在插入时就拒绝格式非法的 JSONCREATE TYPE jsontype BASE text ENCODE json(value) DECODE value; CREATE TABLE t1(val jsontype) STRICT; INSERT INTO t1 VALUES ({key: 1}); -- OK INSERT INTO t1 VALUES (not json); -- Error: malformed JSON五、操作符重载自定义类型可以重载 SQL 操作符让val val或val 10这样的表达式调用用户自定义函数CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value) OPERATOR (uint) - test_uint_add OPERATOR (uint) - test_uint_lt OPERATOR (uint) - test_uint_eq; CREATE TABLE t1(val uint) STRICT; INSERT INTO t1 VALUES (20); INSERT INTO t1 VALUES (30); SELECT val val FROM t1; -- 40 -- 60 SELECT val FROM t1 WHERE val 25; -- 20操作符信息同样存放在TypeDefKind::Custom的operators字段中core/schema.rs并在PRAGMA list_types中以形如(uint) - test_uint_add的形式展示见下文检视类型。六、排序Ordering语义排序和索引永远基于编码后落盘的值而不是解码后的值。DECODE纯粹是展示层——它决定值在查询结果中如何呈现但对排序顺序和索引结构没有任何影响。支持排序的自定义类型必须声明OPERATOR 。未声明时对这类列执行ORDER BY或CREATE INDEX会被禁止并给出明确错误。6.1 裸OPERATOR 使用基础类型比较不带函数名的OPERATOR 告诉 Turso直接用基础类型对编码值做内置比较。只要编码保持所需的排序顺序这个写法就是正确的-- ENCODE value * 100 is monotonic: 10→100, 20→200, 30→300. -- Sorting encoded integers preserves numeric order. CREATE TYPE cents BASE integer ENCODE value * 100 DECODE value / 100 OPERATOR ; CREATE TABLE prices(id INTEGER PRIMARY KEY, amount cents) STRICT; INSERT INTO prices VALUES (1, 30), (2, 10), (3, 20); SELECT amount FROM prices ORDER BY amount; -- 10 -- 20 -- 30如果编码不保持顺序排序结果就会反映编码后的表示-- string_reverse is NOT monotonic: encoded text sorts differently than decoded. -- Encoded: apple→elppa, banana→ananab, cherry→yrrehc. -- Encoded text sort: ananab elppa yrrehc → display: banana, apple, cherry. CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value) OPERATOR ; CREATE TABLE t(id INTEGER PRIMARY KEY, val reversed) STRICT; INSERT INTO t VALUES (1, apple), (2, banana), (3, cherry); SELECT val FROM t ORDER BY val; -- banana -- apple -- cherry6.2 带函数的OPERATOR 自定义比较器当基础类型对编码值的比较不适用时可以提供一个自定义比较器函数。比较器在比较之前对编码值做变换-- numeric stores values as blobs; standard blob comparison is wrong. -- numeric_lt knows how to compare encoded blobs numerically. CREATE TYPE numeric(precision, scale) BASE blob ENCODE numeric_encode(value, precision, scale) DECODE numeric_decode(value) OPERATOR numeric_lt;比较器也可以从不保序的编码中恢复出期望的排序-- Same encoding as above, but the comparator reverses encoded values -- before comparing, recovering alphabetical order. CREATE TYPE reversed_alpha BASE text ENCODE string_reverse(value) DECODE string_reverse(value) OPERATOR string_reverse; CREATE TABLE t(id INTEGER PRIMARY KEY, val reversed_alpha) STRICT; INSERT INTO t VALUES (1, apple), (2, banana), (3, cherry); SELECT val FROM t ORDER BY val; -- apple -- banana -- cherry6.3 不可排序类型未声明OPERATOR 的类型不能用于ORDER BY或CREATE INDEXCREATE TYPE mytype BASE text ENCODE value DECODE value; CREATE TABLE t(val mytype) STRICT; SELECT val FROM t ORDER BY val; -- Error: cannot ORDER BY column val of type mytype: type does not declare OPERATOR CREATE INDEX idx ON t(val); -- Error: cannot create index on column val of type mytype: type does not declare OPERATOR 但基于不可排序列计算出一个普通值的表达式索引仍然允许CREATE INDEX idx ON t(length(val)); -- OK: length() returns an integer6.4 内置类型的排序能力以下内置类型声明了OPERATOR 支持ORDER BY和索引date、time、timestamp、varchar、smallint、boolean、uuid、bytea、numeric。不支持排序的内置类型json、jsonb、inet。这与源码中内置类型的引导bootstrap定义完全一致在 core/schema.rs 的bootstrap_builtin_types里date、time、timestamp、timestamptz、varchar、smallint、boolean、uuid、bytea、numeric的 SQL 定义尾部都带有OPERATOR 而json、jsonb、inet等没有。此外还注册了别名bool→boolean、int2→smallint、int8→bigintcore/schema.rs。七、默认值Defaults7.1 类型级默认值定义在类型上的默认值作用于该类型的所有列除非被覆盖CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value) DEFAULT 0; CREATE TABLE t1(id INTEGER PRIMARY KEY, val uint) STRICT; INSERT INTO t1(id) VALUES (1); SELECT id, val FROM t1; -- 1|07.2 列级覆盖列定义可以覆盖类型的默认值CREATE TABLE t1(id INTEGER PRIMARY KEY, val uint DEFAULT 42) STRICT; INSERT INTO t1(id) VALUES (1); SELECT id, val FROM t1; -- 1|427.3 函数默认值默认值可以是表达式或函数调用CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value) DEFAULT string_reverse(auto); CREATE TABLE t1(id INTEGER PRIMARY KEY, val reversed) STRICT; INSERT INTO t1(id) VALUES (1); SELECT id, val FROM t1; -- 1|otua注意示例输出默认值string_reverse(auto)得到otua写入时又经 ENCODE 反转读回时经 DECODE 反转回otua。八、用 CASE/RAISE 做校验在 ENCODE 表达式中使用CASE ... ELSE RAISE(ABORT, ...)可以在写入时校验值并拒绝非法输入同时给出清晰的错误信息CREATE TYPE positive_int BASE integer ENCODE CASE WHEN value 0 THEN value ELSE RAISE(ABORT, value must be positive) END DECODE value; CREATE TABLE t1(val positive_int) STRICT; INSERT INTO t1 VALUES (42); -- OK INSERT INTO t1 VALUES (-1); -- Error: value must be positive这正是内置类型varchar和smallint实施约束的方式-- varchar checks length against the maxlen parameter CREATE TYPE varchar(maxlen) BASE text ENCODE CASE WHEN length(value) maxlen THEN value ELSE RAISE(ABORT, value too long for varchar) END DECODE value; -- smallint checks the integer range CREATE TYPE smallint BASE integer ENCODE CASE WHEN value BETWEEN -32768 AND 32767 THEN value ELSE RAISE(ABORT, integer out of range for smallint) END DECODE value;对照 core/schema.rs 与 core/schema.rs内置varchar、smallint的引导 SQL 与上面完全同构内置date、time、timestamp则用RAISE(ABORT, invalid date/time/timestamp value)校验格式core/schema.rs并且time/timestamp的 ENCODE 还通过rtrim(rtrim(strftime(...), 0), .)保留亚秒精度、去掉尾随零与悬空小数点以贴近 PostgreSQL 的文本格式。九、参数化类型Parametric Types类型可以声明参数参数会被替换进 ENCODE/DECODE 表达式。参数写在类型名后的括号中CREATE TYPE varchar(maxlen) BASE text ENCODE CASE WHEN length(value) maxlen THEN value ELSE RAISE(ABORT, value too long for varchar) END DECODE value; CREATE TABLE t1(name varchar(10)) STRICT; INSERT INTO t1 VALUES (hello); -- OK (length 5 10) INSERT INTO t1 VALUES (toolongname); -- Error: value too long for varchar当列声明为varchar(10)时ENCODE 表达式中的参数maxlen被替换为10。参数定义在TypeDefKind::Custom.paramscore/schema.rssqlite_turso_types虚拟表在展示这类类型时会把参数拼进显示名例如varchar(value text, maxlen integer)见 core/turso_types_vtab.rs。十、编码校验的时机编码发生在约束检查NOT NULL、类型亲和性之前。如果编码函数对 NOT NULL 或 PRIMARY KEY 列返回 NULL插入会被拒绝CREATE TYPE my_uuid BASE text ENCODE uuid_blob(value) DECODE uuid_str(value); CREATE TABLE t1(id my_uuid PRIMARY KEY, name TEXT) STRICT; INSERT INTO t1 VALUES (invalid-uuid, bad); -- Error: NOT NULL constraint failed (uuid_blob returned NULL)这一编码先行的顺序意味着自定义类型的 ENCODE 本身就构成一道存储层防线——即使约束还没检查非法值也进不了存储。十一、CHECK 约束的类型匹配在 STRICT 表中CHECK 约束的比较在建表时做类型检查。自定义类型列不能直接与裸字面量比较——两侧类型必须一致。需要用CAST把字面量转换成自定义类型-- ERROR: type mismatch in CHECK constraint (cents vs INTEGER) CREATE TABLE t1(amount cents CHECK(amount 50)) STRICT; -- OK: CAST converts the literal to cents, both sides have the same type CREATE TABLE t1(amount cents CHECK(amount CAST(50 AS cents))) STRICT;这条规则适用于 STRICT 表里的所有比较而不只是自定义类型-- ERROR: type mismatch (INTEGER vs TEXT) CREATE TABLE t1(age INTEGER CHECK(age old)) STRICT; -- OK: same types CREATE TABLE t1(age INTEGER CHECK(age 18)) STRICT;CHECK 表达式中的函数调用同样需要 CAST因为函数返回类型在建表时无法确定-- ERROR: cannot determine return type of length() CREATE TABLE t1(name TEXT CHECK(length(name) 10)) STRICT; -- OK: CAST makes the type explicit CREATE TABLE t1(name TEXT CHECK(CAST(length(name) AS INTEGER) 10)) STRICT;十二、NULL 处理NULL 值完全绕过编码与解码CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value); CREATE TABLE t1(val uint) STRICT; INSERT INTO t1 VALUES (NULL); SELECT COALESCE(val, IS_NULL) FROM t1; -- IS_NULL这意味着你不需要在 ENCODE/DECODE 里处理 NULL——引擎会保证 NULL 原样通过。十三、CAST 支持可以把值 CAST 成自定义类型这会应用编码函数CREATE TYPE reversed BASE text ENCODE string_reverse(value) DECODE string_reverse(value); SELECT CAST(hello AS reversed); -- olleh十四、检视类型14.1 PRAGMA list_types列出所有可用类型内置 自定义及其元数据PRAGMA list_types; -- type | parent | encode | decode | default | operators -- INTEGER | | | | | -- REAL | | | | | -- TEXT | | | | | -- BLOB | | | | | -- ANY | | | | | -- uint | text | test_uint_encode(...) | test_uint_decode(...) | 0 | (uint) - test_uint_add注意未启用自定义类型功能时该 PRAGMA 只显示五种基础 SQLite 类型INTEGER、REAL、TEXT、BLOB、ANY启用后才会出现内置自定义类型与你创建的uint等。14.2 sqlite_turso_types 虚拟表所有类型内置 用户定义都通过sqlite_turso_types虚拟表暴露SELECT name, sql FROM sqlite_turso_types;该虚拟表在 core/turso_types_vtab.rs 中实现它实现了InternalVirtualTable表结构为CREATE TABLE sqlite_turso_types(name TEXT, sql TEXT)core/turso_types_vtab.rs游标打开时对schema.type_registry做一次快照并按键排序core/turso_types_vtab.rsname列给出带参数的类型显示名sql列给出可重建该类型的原始 SQL。由于TypeDef保存了建型语句原文这个虚拟表天然支持类型的持久化与重建——CLI 的 dump 流程也会先从内部类型表输出CREATE TYPE语句、再输出表 DDL以保证引用自定义类型的表能正确恢复见 cli/app.rs 与dump_custom_types的实现 cli/app.rs。十五、与 ALTER TABLE 配合自定义类型可与ALTER TABLE ADD COLUMN一起使用CREATE TYPE uint BASE text ENCODE test_uint_encode(value) DECODE test_uint_decode(value); CREATE TABLE t1(id INTEGER PRIMARY KEY) STRICT; ALTER TABLE t1 ADD COLUMN val uint; INSERT INTO t1 VALUES (1, 42); SELECT id, val FROM t1; -- 1|42十六、限制与注意事项自定义类型必须用于 STRICT 表。只要还有表的列在使用某类型就不能删除该类型。CREATE TYPE IF NOT EXISTS在类型已存在时静默成功。编码/解码表达式使用标识符value引用输入值。排序与索引基于编码后的值DECODE只是展示层不参与排序。在部分构建配置下内置类型是条件编译的例如uuid依赖uuidfeature、json/jsonb依赖jsonfeature见 core/schema.rs因此你环境中可用的内置类型集合以实际编译特性为准。延伸阅读手动页入口本主题在 CLI 内可用.manual custom-types或.man custom-types查看完整手册列表见 cli/manuals/index.md。类型注册表与内置类型定义core/schema.rs类型定义结构TypeDef / TypeDefKind / ResolvedTypecore/schema.rssqlite_turso_types虚拟表实现core/turso_types_vtab.rsCLI 启用开关--experimental-custom-typescli/app.rs【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考