22 个版本 (4 个破坏性更新)

0.4.4 2021 年 10 月 10 日
0.4.3 2021 年 10 月 10 日
0.4.2 2021 年 9 月 6 日
0.3.1 2021 年 9 月 1 日
0.0.9 2021 年 8 月 23 日

#56 in 数据库实现

Download history 112/week @ 2024-03-31 2/week @ 2024-04-07 3/week @ 2024-05-19

每月 79 次下载
2 crate 中使用

MIT/Apache

310KB
7K SLoC

魔咕

moogle 是一个 Rust 关系编程库。

关系编程是 SQL 数据库使用的底层模型。在关系模型中,所有数据都使用全局集合来表示。

大多数语言可以很好地支持这个模型,因为它们支持并发写入器和迭代器,只要写入器不改变底层数据结构的形状。

moogle 的目标是使关系代码在 Rust 中的编写尽可能容易,就像在 Python 或 SQL 中编写一样。

动机

假设你正在开发一个游戏,NPC 收集物品,你想要跟踪每个 NPC 背包中的物品。

let mut inventory: HashMap<NPC, Vec<Item>> = BTreeMap::new();

这种表示有以下问题

  • 如果 NPC 没有物品,你必须手动初始化 Vec
  • 如果 NPC 没有物品了,你必须手动移除 Vec

许多人通过创建一个 MultiMap 类型来解决这两个问题

let mut inventory: MultiMap<NPC, Item> = MultiMap::new();

这个系统中的一些示例代码可能看起来像这样

inventory.insert(russell, stick);
inventory.insert(russell, beetle);
inventory.insert(russell, pokemon_card);
inventory.insert(jochen, pizza);

println!("Russell's items: {:?}", inventory.get(russell)); 
    // => stick, beetle, pokemon_card
println!("Jochen's items: {:?}", inventory.get(jochen));  
    // => pizza

然而,这个系统可能存在一些潜在的问题。

首先,多映射的简单实现可能在 Jochen 取走物品后未能从 Russell 的库存中取出物品

inventory.insert(jochen, stick);

println!("Russell's items: {:?}", inventory.get(russell)); 
    // => stick, beetle, pokemon_card
println!("Jochen's items: {:?}", inventory.get(jochen));  
    // => pizza, stick

这并不是理想的情况。你可以通过在调用 inventory.insert() 之前跟踪棍子被取走前的背包来解决这个问题,但最好是不必通过改变你的游戏逻辑来跟踪质量守恒。

moogle 通过跟踪“谁以前拥有这根棍子?”的答案来解决此问题,然后使用这个答案来清除以前的拥有者

let inventory: OneToMany<NPC, Item> = OneToMany::new();
inventory.fwd().insert(russell, stick);
inventory.fwd().insert(russell, beetle);
inventory.fwd().insert(russell, pokemon_card);
inventory.fwd().insert(jochen, pizza);

println!("Russell's items: {:?}", inventory.fwd().get(russell)); 
    // => stick, beetle, pokemon_card
println!("Jochen's items: {:?}", inventory.fwd().get(jochen));  
    // => pizza

println!("Who owns the stick? {:?}", inventory.bwd().get(stick));
    // => russell

inventory.fwd().insert(jochen, stick);

println!("Russell's items: {:?}", inventory.fwd().get(russell)); 
    // => beetle, pokemon_card
println!("Jochen's items: {:?}", inventory.fwd().get(jochen));  
    // => pizza, stick

moogle 还解决了其他一些问题:所有对 moogle 数据结构的操作都是确定性的,尽管它们支持共享,但 moogle 数据结构不允许你创建让 RefCell 发生恐慌的条件。

盒子里有什么?

moogle 提供了八个数据结构。每个都代表可能在关系数据库中看到的不同表格类型,每个都提供了自然的类型安全 Rust 接口。

基本数据结构是 Pom,一种表格类型。 Pom 是一个容器,其唯一目的是存储东西——向 Pom 添加某物会为其分配一个 Id(一个不透明的整数值),你可以使用它再次检索。

这些 Id 值满足 IdLike 特性,这是大多数其他 moogle 数据结构所必需的。它们的顺序是,新的元素具有更大的 Id

此外,moogle 还提供了三种熟悉数据结构的表格表示

  • ToOneToMany:类似于 Map<K, V>Map<K, Set<V>> 的单向映射类型
  • Set:类似于 BTreeSet<K> 的泛型有序集合类型

它提供了四种类型,这些类型是通过这些类型实现的,称为连接,表示实体之间的关系。每个都是一对保持同步的映射

  • OneToOne<A, B>Map<A, B>Map<B, A>
  • OneToMany<A, B>Map<A, Set<B>>Map<B, A>
  • ManyToOne<A, B>Map<A, B>Map<B, Set<A>>
  • ManyToMany<A, B>Map<A, Set<B>>Map<B, Set<A>>

你可以通过思考 A 集合中的每个元素可以与 B 集合中的多少项相关联,反之亦然,来确定你需要哪种关系。

例如,这里是对每种关系的示例,应用在吸血鬼蝙蝠上

  • 每只蝙蝠有一个真正的名字。(并且只有这一个)每个真正的名字属于一只蝙蝠。(并且只有这一个)(OneToOne<Bat, TrueName>)
  • 每只蝙蝠有许多秘密。每个秘密属于一只蝙蝠。(并且只有这一个)(OneToMany<Bat, Secret>)
  • 每只蝙蝠有一个洞穴。(并且只有这一个)每个洞穴属于许多蝙蝠。(ManyToOne<Bat, Cave>)
  • 每只蝙蝠有许多受害者。每个受害者属于许多蝙蝠。(ManyToMany<Bat, Victim>)

Moogle还提供了一种额外的Pom特殊化,称为FloatingPom。它为每个元素分配一次,这并不高效,但它的整个接口都可以从不可变的引用中使用,这使得它更容易使用。

Moogle数据结构有哪些属性?

moogle数据结构的设计是牺牲性能以提供更简单的API或实现更高的一致性。以下是moogle所做的设计决策的简要总结。

API

所有moogle数据结构都支持SetMap接口。(它们与BTreeSetBTreeMap的API大致兼容,但由于它们的对称结构,有一些额外的样板代码。)

moogle数据结构不会引发恐慌。

确定性

所有moogle数据结构都是有序的。这意味着它们可以通过调用Ord来按顺序迭代,通过调用.iter()。它们还可以通过调用.iter().rev()按相反顺序迭代。

moogle数据结构上的所有内置操作都是确定的。这是由于它们是有序的。

一些建议:为了确保排序和确定的ID值,mooglePom牺牲了显著的性能。如果您根本不在乎这一点,请尝试使用slot_map库:它非常快!

并发

所有moogle数据结构都允许同时进行迭代器和写入操作。对于Pom,任何不改变键数的操作都是允许的。对于所有其他数据结构,无法保持对内部值的悬空引用,因此允许执行每个操作。

所有moogle数据结构都附带一个独立的Raw版本——这个版本不支持并发迭代器和写入操作,但性能更快。对于除Pom之外的所有类型,都存在一个.raw()访问器,可以临时借用结构作为Raw类型的实例,这样您可以在借用期间获得相同性能。

(不幸的是,由于性能原因,对于Pom,原始表示形式非常不同,并且无法进行此类借用。)

性能

moogle 数据结构作为 BTreeMapBTreeSet 的轻量级包装来实现。

然而,为了限制分配的数量,moogle 不使用嵌套集合。例如,BTreeMap<K, BTreeSet<V>> 表示为 (BTreeSet<K>, BTreeSet<(K, V)>)

一个 moogle 数据结构(FloatingPom)内部使用 RcRefCell,因为没有其他方法允许在任意时刻进行 insert()remove() 操作,而不会使持有的引用无效。

对称性

moogle 连接(OneToOneOneToManyManyToOneManyToMany)具有额外的对称性属性。

对于任何连接,如果 j.fwd().iter() 包含 (a, b),则 j.bwd().iter() 包含 (b, a)(反之亦然)

并发特定

许多编程语言允许程序员编写同时在迭代数据结构时修改该数据结构的代码。通常,这种情况下定义的行为是,一旦发现这种情况,就会崩溃。

例如,尝试将以下代码输入到 Python 3

>>> xs = {1: "a", 2: "b"}
>>> for x, y in xs.items():
...   xs[3] = "c"
...   print(x, y)
...
1 a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

Rust 中的情况更为紧张,其中禁止更多的事情。在数据结构内部或该数据结构的迭代器中保持对条目的引用会阻止对数据结构任何部分的修改,即使这种修改不会改变数据结构的形状。这意味着这个良性的 Python 程序不允许在 Rust 中编写

>>> xs = {1: "scarf", 2: "ghost", "beware the": []}
>>> for x, y in xs.items():
...   if x in [1, 2]:
...     xs["beware the"].append(y)
...   else:
...     print(x, y)
...
beware the ['scarf', 'ghost']

(还有许多其他原因使得此程序无法在 Rust 中编写,其中许多涉及类型系统,但 .append() 特别违反了 .items() 执行的借用。)

moogle 中,所有这些操作都通过 RefCell 进行门控,以确保安全,并给出可预测的行为。特别是

  • 如果迭代器按序通过了一个元素,那么它无法观察到该元素的变化。否则,它可以。
  • 您允许持有的唯一内部数据结构指针是 FwdSet/BwdSet,它会在发生时立即看到对底层存储的所有更改。

这种行为基本上与Redis的ZSCAN行为相同。如果您对此行为感到不安,可以使用RawManyToMany等代替ManyToMany等,这些与大多数Rust数据结构一样,在迭代器作用域内不允许写入,并且可能会提供性能提升。

实现相当合理(直接使用底层Raw类型),但它使用unsafe在底层数据未更改时缓存迭代器引用。不安全代码经过模糊测试以确保安全。

要快速演示通行规则,请参见以下内容

    let letters: OneToMany<Alphabet, char> = OneToMany::new();
    letters.fwd().insert(english, 'a');
    letters.fwd().insert(english, 'b');
    letters.fwd().insert(english, 'd');
    letters.fwd().insert(english, 'e');

    let asc = letters.fwd().items();

    asc.next();  // (english, 'a')
    asc.next();  // (english, 'b')
    asc.next();  // (english, 'd')
    letters.fwd().insert(english, 'c');

    // don't see the 'c', we already passed it
    asc.next();  // (english, 'e')
    asc.next();  // None

这对于逆序迭代器也有效——在这种情况下,如果我们以逆序传递,我们看不到它。

要快速演示保留集合规则

    let possessions: OneToMany<Ghost, Item> = OneToMany::new();
    let sylvian_possessions = possessions.fwd().get(sylvian);
    println!("{:?}", sylvian_possessions);  // nothing
    possessions.fwd().insert(sylvian, plush)
    println!("{:?}", sylvian_possessions);  // {plush}
    sylvian_possessions.insert(fangs);
    possessions.iter(); // {(sylvian, plush), (sylvian, fangs)}

这两个规则不会带来意外

    let letters: OneToMany<Alphabet, char> = OneToMany::new();
    let english_letters = letters.fwd().get(english);
    letters.fwd().insert(english, 'a');
    letters.fwd().insert(english, 'b'):
    letters.fwd().insert(english, 'c');
    letters.fwd().insert(english, 'd');

    let desc = english_letters.items().rev();

    desc.next();  // 'd'
    desc.next();  // 'c'
    desc.next();  // 'b'
    letters.fwd().insert(english, 'f')

    // don't see the 'f', we already passed it
    desc.next();  // 'a'
    desc.next();  // None

依赖关系

~170KB