Gel 数据模型指南:深入理解模块(Modules)——SDL 声明、名称解析与 DDL 操作

  • 数据库
  • 图数据库
  • 关系型数据库

【免费下载链接】edgedb

Gel supercharges Postgres with a modern data model, graph queries, Auth & AI solutions, and much more.

项目地址: https://gitcode.com/gh_mirrors/ed/edgedb
点击查看 免费下载

在 Gel(前身 EdgeDB)中,每个分支(branch)的 schema 都由若干**模块(module)**组成,模块是组织大型 schema 的第一级逻辑容器:它承载类型、函数与子模块,并通过命名空间机制决定名称的解析规则。本文以官方文档 docs/reference/datamodel/modules.rst 为骨架,结合仓库中 edb/schema/modules.py 与 edb/schema/schema.py 的实现细节,系统讲解模块的概念、保留模块、SDL 声明语法、隐式模块创建,以及 create module / drop module 两类 DDL 命令。读完本文,你将掌握如何用模块组织 schema、如何跨模块引用对象(fully-qualified name),以及理解迁移文件中模块相关命令的来源与约束。

什么是模块

每个 |branch| 都有一个由若干模块组成的 schema,每个模块拥有唯一名称。模块的作用是把大型 schema 拆分为逻辑单元,类似于编程语言中的命名空间或包。不过在实践中,绝大多数用户会把整个 schema 放在名为 default 的单一模块中:

module default {
  # declare types here
}

这个习惯与源码实现保持一致:在 edb/schema/modules.py 中定义了 DEFAULT_MODULE_ALIAS = 'default',default 是 schema 层面的默认模块别名。当你在 SDL 中省略模块块、直接用未限定的名称声明对象时,对象即归属于当前默认模块(通常是 default)。

从实现上看,模块本身是 schema 中的一等对象:Module 类继承自 AnnotationSubject 与 Object,qlkind 为 MODULE,并标记 data_safe=False(见 edb/schema/modules.py)。值得注意的是,模块虽然可以嵌套,但它不是"限定(qualified)"对象——嵌套模块的完整名称始终以 UnqualName 形式表示(例如 dracula::combat 整体仍是一个未限定名称),这一点与类型、函数等对象的限定名机制不同。

名称解析:跨模块引用必须使用完全限定名

当你在一个模块中引用其他模块里的 schema 对象时,必须使用形如 other_module_name::object_name 的完全限定名(fully-qualified name):

module A {
  type User extending B::AbstractUser;
}

module B {
  abstract type AbstractUser {
    required name: str;
  }
}

上例中,A 模块内的 User 类型通过 B::AbstractUser 引用了 B 模块中的抽象类型。名称解析发生在编译阶段:Gel 编译器会把 :: 分隔的模块前缀解析到具体 schema 对象。在 edb/edgeql/compiler/normalization.py 可以看到,未限定的引用会落入当前模块的解析路径,而带模块前缀的引用则直接定位到对应模块。这意味着:

  • 模块名构成命名空间的第一段;
  • 类型、函数、链接(link)、属性(property)等对象的完整名称均为 模块名::对象名 形式;
  • 在同一模块内部引用对象时可以省略模块前缀,编译器会将其补全为当前模块限定名。

如果你定义的模块恰好叫 default,还可以在 EdgeQL 查询中使用 set module default; 显式切换当前模块上下文(例如测试用例 tests/schemas/dump_v4_setup.edgeql 中的写法)。

保留模块名:系统预定义命名空间

以下模块名由 Gel 保留,内部预置了标准类型、工具函数与运算符,不允许用户修改或删除:

模块名内容
std标准库中的标准类型、函数与运算符,见 docs/reference/stdlib/index.rst
math代数与统计函数,见 docs/reference/stdlib/math.rst
cal本地(无时区)与相对日期/时间类型和函数,见 docs/reference/stdlib/datetime.rst
schema描述 introspection(内省)schema 的类型,见 docs/reference/datamodel/introspection/index.rst
sys系统级实体,如用户角色与数据库
cfg配置与设置相关对象

值得说明的是,文档表格中的 math、cal 在真实实现中是以嵌套模块形式存在于 std 之下的。完整清单定义在 edb/schema/schema.py 的 STD_MODULES 元组中,包含 std、schema、std::math、sys、sys::perm、cfg、cfg::perm、std::cal、std::net、std::net::http、std::net::perm、std::pg、std::fts、std::lang 及其子模块、ext、std::enc 等。这些模块在启动时由 edb/lib 目录下的 EdgeQL 文件创建,例如 edb/lib/math.edgeql 中的 CREATE MODULE std::math;、edb/lib/cal.edgeql 中的 CREATE MODULE std::cal;。

源码对这些模块施加了硬性保护(见 edb/schema/modules.py):除非处于 stdmode(标准库构建模式)或 testmode,否则任何针对 STD_MODULES 中模块的 create/alter/drop 操作都会抛出 SchemaDefinitionError,提示该模块是只读的(module ... is read-only)。此外还存在一个额外的保留名 super(定义于 edb/schema/modules.py 的 RESERVED_MODULE_NAMES),用户不能创建名为 super 的模块。

另有一组 SPECIAL_MODULES(如 __derived__、__ext_casts__、__ext_index_matches__,见 edb/schema/schema.py)由编译器内部使用,用户也不应触碰。

模块是容器:类型、函数与子模块

模块可以包含类型、函数以及其他模块。下面是一个空模块:

module my_module {}

下面是一个包含类型的模块:

module my_module {
  type User {
    required name: str;
  }
}

从 schema 差异(delta)计算的视角看,模块的"容器"语义体现在 edb/schema/ddl.py 中:当比较两个 schema 版本时,系统会分别收集 added_modules 与 dropped_modules,并默认排除所有标准库模块(excluded_modules.update(s_schema.STD_MODULES),见 edb/schema/ddl.py)。这意味着你日常编写迁移时,绝不会看到针对 std、sys 等标准模块的 create/drop 操作,它们始终被隔离在用户可见的 schema 变更之外。

嵌套模块

模块可以嵌套声明,形成模块树:

module dracula {
  type Person {
    required name: str;
    multi places_visited: City;
    strength: int16;
  }

  module combat {
    function fight(
      one: dracula::Person,
      two: dracula::Person
    ) -> str
      using (
        (one.name ?? 'Fighter 1') ++ ' wins!'
        IF (one.strength ?? 0) > (two.strength ?? 0)
        ELSE (two.name ?? 'Fighter 2') ++ ' wins!'
      );
  }
}

要调用嵌套模块 combat 中的 fight 函数,需要把整条模块链拼接成完全限定名:

dracula::combat::fight(<arguments>)

嵌套模块的创建在底层同样受约束:创建 A::B 时,若外层模块 A 不存在,会抛出 UnknownModuleError("module 'A' is not in this schema");若被创建的末段名称是保留名 super,则抛出 SchemaDefinitionError(见 edb/schema/modules.py)。也就是说,嵌套模块要求父模块必须先存在。

声明模块:SDL 语法

语法

模块块(module block)是 SDL 中声明模块的标准方式:

module <ModuleName> "{" [ <schema-declarations> ] ... "}"

嵌套模块的声明形式:

module <ParentModuleName> "{" [ <schema-declarations> ]
  module <ModuleName> "{" [ <schema-declarations> ] "}"
  ...
"}"

其中 <schema-declarations> 是归属于该模块的各种 schema 项(类型、函数、链接等)声明。模块块声明等价于底层 create module 命令,但额外允许把模块内容作为嵌套声明一并写入。

同名模块块的合并

与 create module 不同,同名模块块可以在同一份 SDL 文档中出现多次,所有同名块会被合并为一个模块。例如:

module my_module {
  abstract type Named {
    required name: str;
  }
}

module my_module {
  type User extending Named;
}

等价于:

module my_module {
  abstract type Named {
    required name: str;
  }

  type User extending Named;
}

这个特性非常适合按文件拆分 schema:多个 .esdl 文件可以分别声明各自关心的 module default { ... } 片段,导入后自动合并。测试用例中也能看到这种模式,例如 tests/test_docs.py 用 module default { <snippet> } 包裹待验证的文档代码片段。

隐式声明模块

除了显式写出模块块,还可以通过完全限定名隐式声明模块:当 SDL 声明中的对象名称带有模块前缀时,该模块会自动创建。下面的声明与前面的例子完全等价,但 my_module 是隐式创建的:

abstract type my_module::Named {
    required name: str;
}

type my_module::User extending my_module::Named;

换言之,"模块是否存在"不必预先操心——只要对象的 fully-qualified name 中的模块部分尚不存在,schema 就会自动补建该模块。这大大简化了跨文件、跨模块的 schema 组织。

DDL 命令:create module 与 drop module

文档的 DDL 章节介绍底层命令。日常开发中你通常不需要直接执行它们(SDL 迁移会替你生成),但了解其语义有助于审阅迁移文件。

Create module

语法:

create module [ <parent-name>:: ] <name>
  [ if not exists ];

create module 为当前分支定义一个新模块,模块名必须与当前分支中任何已有模块不同。与 SDL 模块块不同,该命令没有子命令——模块内容是单独创建的。

参数 if not exists:默认情况下,创建已存在的模块会报错;加上该标志后命令会静默成功。这在编写"向模块追加内容、缺失则先建模块"的脚本时非常有用。

示例:

create module payments;

创建嵌套模块(要求父模块 payments 已存在):

create module payments::currencies;

Drop module

语法:

drop module <name>;

drop module 从当前分支移除一个空模块。如果模块内还包含任何 schema 项,命令会失败。

这一行为在源码中有明确实现:DeleteModule._validate_legal_command(见 edb/schema/modules.py)会通过 schema.get_objects(included_modules=[self.classname], excluded_items=[self.classname]) 显式搜索模块内的对象(模块本身不直接存储与内部对象的引用关系,因此需要全量扫描),若存在任何对象则抛出 SchemaError:"cannot drop module ... because it is not empty"。

示例:

drop module payments;

其他 DDL 限制

从 edb/schema/modules.py 的 RenameModule 实现可以看到,模块重命名目前不被支持——apply 直接抛出 SchemaError("renaming modules is not supported")。因此组织模块名时应提前规划,避免后期通过重命名来调整。

模块在实践中的组织建议

综合文档与实现,可以总结出以下实践要点:

  1. 默认用 default 模块:单模块足以覆盖绝大多数场景,这也是官方文档示例的默认假设——多数 SDL 示例省略模块块,直接描述模块内容。
  2. 多模块用于大型 schema 拆分:当 schema 规模增大、需要按业务域(如 payments、inventory、auth)隔离命名空间时,再引入模块。跨模块引用记得写 fully-qualified name。
  3. 嵌套模块表达层级:module A { module B { ... } } 会生成 A::B,配合隐式声明可以让子模块随对象声明自动出现。
  4. 善用同名块合并:把不同关注点的声明拆分到多个 SDL 文件,每个文件各自声明 module my_module { ... } 片段,最终合并为单一模块。
  5. 迁移文件里留意模块命令:create module 与 drop module 会出现在迁移中,尤其是首次引入或移除某个模块时;drop module 仅对空模块有效,删除前需先清空其中的对象。
  6. 不碰保留与特殊模块:std、sys、schema、cfg、ext 等标准模块只读,super 不可用,__derived__ 等特殊模块归编译器内部使用。

相关文档

  • 数据库
  • 图数据库
  • 关系型数据库

【免费下载链接】edgedb

Gel supercharges Postgres with a modern data model, graph queries, Auth & AI solutions, and much more.

项目地址: https://gitcode.com/gh_mirrors/ed/edgedb
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值