Build Status Platforms Documentation Matrix

帝国 (Empire)

Swift 的记录存储 (A record store for Swift)

警告 (Warning)

此库仍处于早期阶段,尚无大量的实际应用测试。(This library is still pretty new and doesn't have great deal of real-world testing yet.)

import Empire

@IndexKeyRecord("name")
struct Person {
    let name: String
    let age: Int
}

let store = try Store(path: "/path/to/store")

try await store.withTransaction { context in
    try context.insert(Person(name: "Korben", age: 45))
    try context.insert(Person(name: "Leeloo", age: 2000))
}

let records = try await store.withTransaction { context in
    try Person.select(in: context, limit: 1, name: .lessThan("Zorg"))
}

print(record.first!) // Person(name: "Leeloo", age: 2000)

集成 (Integration)

dependencies: [
    .package(url: "https://github.com/mattmassicotte/Empire", branch: "main")
]

数据建模和查询 (Data Modeling and Queries)

Empire 使用的数据模型与传统的 SQL 支持的数据存储极其不同。它相当严格,即使您对此很熟悉,也可能是一个挑战。(Empire uses a data model that is extremely different from a traditional SQL-backed data store. It is pretty unforgiving and can be a challenge, even if you are familiar with it.)

从概念上讲,您可以将每个记录视为拆分为两个元组:“索引键”和“字段”。(Conceptually, you can think of every record as being split into two tuples: the "index key" and "fields".)

键 (Keys)

索引键是记录的关键组成部分。查询可能在索引键的组件上进行。(The index key is a critical component of your record. Queries are only possible on components of the index key.)

@IndexKeyRecord("lastName", "firstName")
struct Person {
    let lastName: String
    let firstName: String
    let age: Int
}

@IndexKeyRecord 宏的参数定义了构成索引键的属性。Person 记录首先按 lastName 排序,然后按 firstName 排序。键组件的顺序非常重要。查询的最后一个组件才能是非等式比较。如果您想查找某个键组件的范围,则必须限制所有之前的组件。(The arguments to the @IndexKeyRecord macro define the properties that make up the index key. The Person records are sorted first by lastName, and then by firstName. The ordering of key components is very important. Only the last component of a query can be a non-equality comparison. If you want to look for a range of a key component, you must restrict all previous components.)

// scan query on the first component
store.select(lastName: .greaterThan("Dallas"))

// constrain first component, scan query on the second
store.select(lastName: "Dallas", firstName: .lessThanOrEqual("Korben"))

// ERROR: an unsupported key arrangement
store.select(lastName: .lessThan("Zorg"), firstName: .lessThanOrEqual("Jean-Baptiste"))

@IndexKeyRecord 类型生成的代码使其成为编译时错误,以防止编写无效的查询。(The code generated for a @IndexKeyRecord type makes it a compile-time error to write invalid queries.)

由于查询能力的限制,您必须通过从您需要支持的查询开始来建模您的数据。这可能需要反规范化,这可能适用于也可能不适用于您预期的记录数量。(As a consequence of the limited query capability, you must model your data by starting with the queries you need to support. This can require denormalization, which may or may not be appropriate for your expected number of records.)

格式 (Format)

您的类型就是 schema。类型的数据直接序列化为宏生成的二进制形式。更改您的类型将导致未迁移数据的反序列化失败。存储在记录中的所有类型都必须符合 SerializationDeserialization 协议。但是,所有索引键成员还必须在序列化时通过直接二进制比较进行排序。这不是许多类型都具有的属性,但可以通过符合 IndexKeyComparable 来表达。(Your types are the schema. The type's data is serialized directly to a binary form using code generated by the macro. Making changes to your types will make deserialization of unmigrated data fail. All types that are stored in a record must conform to both the Serialization and Deserialization protocols. However, all index key members must also be sortable via direct binary comparison when serialized. This is not a property many types have, but can be expressed with a conformance to IndexKeyComparable.)

类型 (Type) 键 (Key) 限制 (Limitations)
字符串 (String) 是 (yes) 无 (none)
无符号整数 (UInt) 是 (yes) 无 (none)
整数 (Int) 是 (yes) (Int.min + 1)...(Int.max)
UUID 是 (yes) 无 (none)
数据 (Data) 是 (yes) 无 (none)
日期 (Date) 是 (yes) 毫秒精度 (millisecond precision)

IndexKeyRecord 一致性 (IndexKeyRecord Conformance)

@IndexKeyRecord 宏扩展为符合 IndexKeyRecord 协议。您可以直接使用它,但这并不容易。您必须处理所有字段的二进制序列化和反序列化。对您类型的序列化格式进行版本控制也至关重要。(The @IndexKeyRecord macro expands to a conformance to the IndexKeyRecord protocol. You can use this directly, but it isn't easy. You have to handle binary serialization and deserialization of all your fields. It's also critical that you version your type's serialization format.)

@IndexKeyRecord("name")
struct Person {
    let name: String
    let age: Int
}

// Equivalent to this:
extension Person: IndexKeyRecord {
    public typealias IndexKey = Tuple<String, Int>
    public associatedtype Fields: Tuple<Int>

    public static var keyPrefix: Int {
        1
    }

    public static var fieldsVersion: Int {
        1
    }

    public var fieldsSerializedSize: Int {
        age.serializedSize
    }

    public var indexKey: IndexKey {
        Tuple(name)
    }

    public func serialize(into buffer: inout SerializationBuffer) {
        name.serialize(into: &buffer.keyBuffer)
        age.serialize(into: &buffer.valueBuffer)
    }

    public init(_ buffer: inout DeserializationBuffer) throws {
        self.name = try String(buffer: &buffer.keyBuffer)
        self.age = try UInt(buffer: &buffer.valueBuffer)
    }
}

extension Person {
    public static func select(in context: TransactionContext, limit: Int? = nil, name: ComparisonOperator<String>) throws -> [Self] {
        try context.select(query: Query(last: name, limit: limit))
    }
    public static func select(in context: TransactionContext, limit: Int? = nil, name: String) throws -> [Self] {
        try context.select(query: Query(last: .equals(name), limit: limit))
    }
}

CloudKitRecord 一致性 (CloudKitRecord Conformance)

Empire 通过 CloudKitRecord 宏支持 CloudKit 的 CKRecord 类型。您也可以独立使用相关的协议。(Empire supports CloudKit's CKRecord type via the CloudKitRecord macro. You can also use the associated protocol independently.)

@CloudKitRecord
struct Person {
    let name: String
    let age: Int
}

// Equivalent to this:
extension Person: CloudKitRecord {
    public init(ckRecord: CKRecord) throws {
        try ckRecord.validateRecordType(Self.ckRecordType)

        self.name = try ckRecord.getTypedValue(for: "name")
        self.age = try ckRecord.getTypedValue(for: "age")
    }

    public func ckRecord(with recordId: CKRecord.ID) -> CKRecord {
        let record = CKRecord(recordType: Self.ckRecordType, recordID: recordId)

        record["name"] = name
        record["age"] = age

        return record
    }
}

您可以选择覆盖 ckRecordType 以自定义使用的 CloudKit 记录的名称。如果您的类型也使用 IndexKeyRecord,您可以访问 (Optionally, you can override ckRecordType to customize the name of the CloudKit record used. If your type also uses IndexKeyRecord, you get access to)

func ckRecord(in zoneId: CKRecordZone.ID)

问题 (Questions)

为什么存在这个? (Why does this exist?)

我不确定!我没有过多使用过 CoreDataSwiftData。但我使用过分布式数据库 Cassandra 很多,也用过一点 DynamoDB。然后有一天我发现了 LMDB。它的数据模型与 Cassandra 非常相似,我对玩玩它产生了兴趣。这只是从那些实验中逐渐形成的。(I'm not sure! I haven't used CoreData or SwiftData too much. But I have used the distributed database Cassandra quite a lot and DynamoDB a bit. Then one day I discovered LMDB. Its data model is quite similar to Cassandra and I got interested in playing around with it. This just kinda materialized from those experiments.)

我可以使用这个吗? (Can I use this?)

当然! (Sure!)

我应该使用这个吗? (Should I use this?)

用户数据很重要。这个库有很多测试,但还没有实际应用测试。我计划自己使用它,但即使我自己也还没有开始使用。它应该被认为是功能性的,但仍处于实验阶段。(User data is important. This library has a bunch of tests, but it has no real-world testing. I plan on using this myself, but even I haven't gotten to that yet. It should be considered functional, but experimental.)

贡献和协作 (Contributing and Collaboration)

我很乐意听到您的声音!问题或拉取请求都很棒。 Matrix 空间Discord 都可用于实时帮助,但我强烈倾向于以文档的形式回答。您也可以在 mastodon 上找到我。(I would love to hear from you! Issues or pull requests work great. Both a Matrix space and Discord are available for live help, but I have a strong bias towards answering in the form of documentation. You can also find me on mastodon.)

我更喜欢协作,如果您有类似的项目,我很乐意找到合作方式。(I prefer collaboration, and would love to find ways to work together if you have a similar project.)

我更喜欢使用制表符进行缩进以提高可访问性。但是,我宁愿您使用您想要的系统并创建一个 PR,而不是因为空格而犹豫不决。(I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace.)

参与此项目即表示您同意遵守 贡献者行为准则。(By participating in this project you agree to abide by the Contributor Code of Conduct.)