#table #parameters #traits #macro #sqlite #sql #macro-derive

nanosql_macros

SQLite(过程宏)的轻量级强类型数据映射器

4个版本 (2个重大更新)

0.3.1 2024年7月22日
0.3.0 2024年7月14日
0.2.0 2024年6月28日
0.1.0 2024年6月24日

#1057 in 过程宏

Download history 241/week @ 2024-06-22 34/week @ 2024-06-29 1/week @ 2024-07-06 115/week @ 2024-07-13 122/week @ 2024-07-20 44/week @ 2024-07-27

每月284次下载
用于 nanosql

MIT 许可证

70KB
1.5K SLoC

NanoSQL:SQLite的轻量级强类型数据映射器

NanoSQL是一个小型的数据映射库,它帮助您使用强类型参数和结果集执行SQL语句。它尝试对您的SQL代码进行类型检查。相反,它只确保参数和结果可以正确地序列化和反序列化。

概述

该库围绕预编译语句构建。首先,您创建查询的接口和实现描述。这通过包含输入(参数)和输出(结果)类型作为关联类型以及构建SQL文本函数的Query trait来实现。您可以手动实现此trait,或者使用define_query宏作为便捷的快捷方式。

接下来,您使用Connection::compile()Query编译成CompiledStatement。这包装了一个SQLite预编译语句,但限制了其参数和返回类型。

最后,您在编译后的语句上调用CompiledStatement::invoke()函数来实际查询数据库

  • 查询的输入可以是实现了 Param 特性的任何类型。这包括基本类型、基本类型的可选类型、基本类型或可选类型的元组以及具有基本类型或可选类型字段的 struct。Nanosql 可以与位置参数和命名参数一起工作,并支持 SQLite 接受的所有参数前缀(?:@$)。如果激活了 crate 的 derive 功能,则可以推导出此特性。
  • 查询的输出是实现了 ResultSet 特性的类型。这通常是一种类集合(例如,标准 Vec 类型),或者任何其他将 SQL 查询返回的行聚合到有意义的数据结构中的类型。值得注意的是,Option<T>Single<T> 可以用来期望最多一个或正好一个记录。这些类型在它们的包装值类型实现了 ResultRecord 时实现 ResultSet
  • ResultRecord 是一个可以由类似元组和类似 struct 的类型实现以反序列化单个行的特性。此特性也可以 #[derive]

通过 Connection 对象的特殊辅助/扩展方法,可以执行诸如创建表模式和插入记录等极其常见(基本上不可避免)的任务,这些方法通过 ConnectionExt 特性实现。这些方法进一步使用 Table 特性来准备和调用相应的 SQL 语句,并可以方便地调用。

示例

最基本的内容 - 创建一个表,将一些记录插入其中,然后检索它们

use std::fmt::{self, Formatter};
use nanosql::{
    Result, Connection, ConnectionExt, Query,
    ToSql, FromSql, AsSqlTy, Param, ResultRecord, Table
};


/// This type is going to represent our table.
///
/// We derive the `Param` trait for it so that it can be used for
/// binding parameters to the statement when inserting new records,
/// and the `ResultRecord` trait so that we can use it to retrieve
/// results, too.
///
/// We also derive the `Table` trait so that basic operations such as
/// creating the table in the schema and bulk insertion can be performed
/// using the appropriate convenience methods in [`ConnectionExt`].
///
/// The parameter prefix is '$' by default (if not specified via the
/// param_prefix attribute); it may also be one of ':', '@', or '?',
/// the last one being allowed only for tuples and scalar parameters.
///
/// `#[nanosql(rename)]` on a struct renames the table itself, while
/// `#[nanosql(rename_all)]` applies a casing convention to all columns.
#[derive(Clone, PartialEq, Eq, Hash, Debug, Param, ResultRecord, Table)]
#[nanosql(param_prefix = '$')] // optional
#[nanosql(rename = "MyLittlePet", rename_all = "lowerCamelCase")]
struct Pet {
    /// If you don't like the default `AsSqlTy` impl for your column's
    /// type, you can specify a different one. Here we add a non-zero
    /// constraint, but the `id` remains a plain `i64` for convenience.
    ///
    /// You can also add additional `CHECK` constraints, if necessary.
    #[nanosql(sql_ty = core::num::NonZeroI64, check = "id <= 999999")]
    id: i64,
    /// You can apply a `UNIQUE` constraint to any field.
    #[nanosql(unique)]
    nick_name: String,
    /// You can also rename fields/columns one by one
    #[nanosql(rename = "type")]
    kind: PetKind,
}

/// Collective and field-level casing/renaming also works with `enum`s
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, ToSql, FromSql, AsSqlTy)]
#[nanosql(rename_all = "UPPER_SNAKE_CASE")]
enum PetKind {
    Dog,
    #[nanosql(rename = "KITTEN")]
    Cat,
    Fish,
}

/// Our first custom query retrieves a pet by its unique ID.
///
/// If you don't want to spell out the impl by hand, you can
/// use the `define_query!{}` macro for a shorter incantation.
struct PetById;

impl Query for PetById {
    /// The type of the parameter(s). This can be a single scalar, a tuple,
    /// a tuple struct of scalars, or a struct with named fields of scalar types.
    type Input<'p> = i64;

    /// The return type of a query can be either a scalar, a single record (struct or
    /// tuple), or an optional of a scalar/record (when it returns either 0 or 1 rows),
    /// or a collection of arbitrarily many scalars/records. Here we choose an `Option`,
    /// because a given ID corresponds to at most one `Pet`.
    type Output = Option<Pet>;

    /// Finally, we create the actual SQL query.
    fn format_sql(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        formatter.write_str("SELECT id, nickName, type FROM MyLittlePet WHERE id = ?")
    }
}

fn main() -> Result<()> {
    // First, we open a database connection.
    let mut conn = Connection::connect_in_memory()?;
    
    // Then, we ensure that the table exists in the schema.
    conn.create_table::<Pet>()?;

    // Next, we insert a couple of records so we have test data to work on.
    conn.insert_batch([
        Pet {
            id: 1,
            nick_name: "Fluffy".into(),
            kind: PetKind::Dog,
        },
        Pet {
            id: 2,
            nick_name: "Hello Kitty".into(),
            kind: PetKind::Cat,
        },
        Pet {
            id: 3,
            nick_name: "Nemo".into(),
            kind: PetKind::Fish,
        },
    ])?;

    // We then compile the query into a prepared statement.
    let mut stmt = conn.compile(PetById)?;

    // Finally, we execute the compiled statement, passing parameters, and retrieve the results.
    let result = stmt.invoke(3)?;
    assert_eq!(result, Some(Pet { 
        id: 3,
        nick_name: "Nemo".into(),
        kind: PetKind::Fish,
    }));

    // We can re-use the statement and execute it multiple times
    let result = stmt.invoke(99)?;
    assert_eq!(result, None);

    drop(stmt);

    // Inserting a pet with id = 0 should fail due to the `#[nanosql(sql_ty = ...)]` attribute.
    let insert_id_0_result = conn.insert_batch([
        Pet {
            id: 0,
            nick_name: "Error".into(),
            kind: PetKind::Cat,
        }
    ]);
    assert!(insert_id_0_result.is_err(), "id = 0 violates NonZeroI64's CHECK constraint");

    // Inserting a pet with a high ID is expected to fail due to the CHECK constraint.
    let insert_id_high_result = conn.insert_batch([
        Pet {
            id: 1000000,
            nick_name: "this is unique".into(),
            kind: PetKind::Dog,
        }
    ]);
    assert!(insert_id_high_result.is_err(), "id = 1000000 violates extra CHECK constraint");

    // Inserting a pet with a duplicate name is not allowed due to `#[nanosql(unique)]`.
    let insert_dup_name_result = conn.insert_batch([
        Pet {
            id: 137731,
            nick_name: "Hello Kitty".into(),
            kind: PetKind::Dog,
        }
    ]);
    assert!(insert_dup_name_result.is_err(), "duplicate name violates uniqueness constraint");

    Ok(())
}

有关更高级和有趣的示例,请参阅 nanosql/examples 目录(特别是 realistic.rs)。

关于批量插入和事务的注意事项

方法 [ConnectionExt::insert_batch()] 将插入语句封装在事务中以提高性能。事务的排他性通过在类型级别上使用 rusqliteConnection 对象可变(唯一)借用来实现,该对象在事务期间被可变借用。这意味着 insert_batch() 也需要可变借用。然而,准备和调用查询需要不可变借用,并且预准备语句将 Connection 借用为其生命周期。因此,当混合批量插入与其他查询时,可能会出现“无法以可变方式借用 connection,因为它也被以不可变方式借用”的错误。解决这个问题有两个基本方法

  1. 在调用 [ConnectionExt::insert_batch()] 之前删除未完成的预准备语句;
  2. 或者如果您不能这样做,那么可以使用 [Connection::unchecked_transaction()] 获取一个不需要在编译时检查排他性的事务对象,然后在事务对象上调用不可变借用 [TransactionExt::insert_batch()] 方法。

Cargo 功能

  • derive:激活过程宏 - 主要为常用特性创建自定义的 #[derive]。默认启用。
  • expr-check:使用 sqlparser 包在编译时检查 derive 宏属性中原始 SQL 表达式的语法错误。这确保了生成的任何 SQL 都是有效的,并且用户提供的 SQL 代码中的语法错误将被清楚地指出,而不是在运行时导致神秘的语句准备错误。默认启用。
  • not-nan:为 ordered_float::NotNan 实现 ParamResultRecord。这允许在查询中获得更安全的类型接口:由于 SQLite 将 NaN 视为 SQL 的 NULL 值,因此当绑定或检索一个 f32::NANf64::NAN 以及相应的参数需要被 NOT NULL 时,或者源列 可以NULL,您可能会遇到意外的错误。
  • pretty-eqp:使用 ptree 包来美化打印 EXPLAIN QUERY PLAN 的结果。这将实现 Display 对于 [ConnectionExt::explain_query_plan()] 的返回类型 QueryPlan,以使用 ASCII 艺术渲染树,呈现为便于阅读的格式。

关于测试套件的说明

  1. 测试尝试全面测试库的所有功能。因此,它们依赖于在Cargo.toml中定义的大多数或所有Cargo功能。因此,为了成功编译和运行所有测试,您必须在Cargo中传递cargo test --all-features

  2. compiletest_rs包用于确保派生宏能够检测某些类型的错误,例如多个主键或引用不存在列的表级约束。

    由于compiletest_rs包的结构方式,测试可能会有些不可靠。如果您遇到E0464错误(例如,“找到rlib依赖nanosql的多个候选者”),则在运行cargo test compile_fail --all-features之前,先运行cargo clean

TL;DR: 运行测试的最佳“懒惰”方式是./runtests.sh脚本,它只是做

cargo clean
cargo test --workspace --all-features

依赖项

~2.5–3.5MB
~71K SLoC