6个版本
0.3.2 | 2024年7月22日 |
---|---|
0.3.1 | 2024年7月22日 |
0.2.0 | 2024年6月28日 |
0.1.1 | 2024年6月24日 |
#188 在 数据库接口
406 每月下载量
130KB
2.5K SLoC
NanoSQL:一个轻量级、强类型SQLite数据映射器
NanoSQL是一个小型数据映射库,帮助您使用类型参数和类型结果集执行SQL语句。它**不**尝试类型检查您的SQL代码。相反,它只确保参数和结果可以正确序列化和反序列化。
概述
该库围绕预编译语句构建。首先,您创建查询接口和实现的描述。这是通过包含输入(参数)和输出(结果)类型作为关联类型,以及构建SQL文本函数的`Query
` trait实现的。您可以手动实现此trait,或者使用`define_query
`宏作为方便的快捷方式。
接下来,您使用 [Connection::compile()
] 将`Query
`编译成`CompiledStatement
`。这包装了一个SQLite预编译语句,但限制了其参数和返回类型。
最后,您在编译好的语句上调用 [CompiledStatement::invoke()
] 函数来实际查询数据库
- 查询的输入可以是实现`
Param
` trait的任何类型。这包括原始类型、原始类型的可选、原始类型的元组或可选的元组,以及具有原始类型或可选类型字段的struct。Nanosql可以与位置参数和命名参数一起工作,并支持SQLite接受的全部参数前缀(`?
`,`:
`,`@
`,和`$
`)。如果激活了crate的`derive
`功能,则此trait可以派生。 - 查询的输出是一种实现了
ResultSet
特性的类型。这通常是某种集合(例如,标准的Vec
类型),或者任何其他将 SQL 查询返回的行聚合为有意义的数据结构的类型。值得注意的是,可以使用Option<T>
和Single
<T>
来分别期望最多一个或正好一个记录。这些类型在它们的包装值类型实现了ResultRecord
时实现了ResultSet
。 ResultRecord
是一个可以由类似于元组和结构体的类型实现的特性,用于反序列化单个行。这个特性也可以通过#[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()
] 方法将插入语句包装在事务中以提高性能。事务的排他性在 rusqlite
中通过在事务期间可变地(唯一地)借用 Connection
对象在类型级别上建模。这意味着 insert_batch()
也需要可变借用。然而,准备和调用查询需要一个不可变借用,而预定义的语句在其存在期间借用 Connection
。因此,在混合批量插入和其他查询时,您可能会遇到“不能可变借用 connection
因为它也被不可变借用”的错误。解决这个问题的有两个基本方法
- 在调用 [
ConnectionExt::insert_batch()
] 之前删除挂起的预定义语句; - 或者如果您不能这样做,那么使用 [
Connection::unchecked_transaction()
] 获取一个在编译时不会被检查排他性的事务对象,然后在该 事务对象 上调用不可变借用的 [TransactionExt::insert_batch()
] 方法。
Cargo 功能
derive
:激活过程宏 - 主要用于常用特性的自定义#[derive]
。默认启用。expr-check
:使用sqlparser
crate 在编译时检查 derive 宏属性中原始 SQL 表达式的语法错误。这确保生成的 SQL 将是有效的,并且在用户提供的 SQL 代码中存在的语法错误将被清楚地指出,而不是在运行时导致神秘的语句准备错误。默认启用。not-nan
:为ordered_float::NotNan
实现Param
和ResultRecord
。这允许在查询中具有更安全的类型接口:由于 SQLite 将NaN
视为 SQL 的NULL
值,当绑定或检索f32::NAN
或f64::NAN
以及相应的参数需要是NOT NULL
时,或者源列 可能 是NULL
,你可能会遇到意外的错误。pretty-eqp
:使用ptree
crate 来美化打印EXPLAIN QUERY PLAN
的结果。这将实现Display
对于QueryPlan
,这是 [ConnectionExt::explain_query_plan()
] 的返回类型,它使用 ASCII 艺术以美观、可读的格式呈现树。
关于测试套件的说明
-
测试试图广泛地测试库的所有功能。因此,它们依赖于在
Cargo.toml
中定义的大多数或所有 Cargo 功能。因此,为了成功编译和运行所有测试,您必须将cargo test --all-features
传递给 Cargo。 -
compiletest_rs
crate 用于确保 derive 宏检测某些类型的错误,例如多个主键或引用不存在列的表级约束。由于
compiletest_rs
crate 的结构方式,测试可能有些不可靠。如果您遇到E0464
错误(例如,“找到多个rlib
依赖nanosql
的候选”),则在运行cargo test compile_fail --all-features
之前运行cargo clean
。
TL;DR: 运行测试的最佳“懒惰”方式是 ./runtests.sh
脚本,该脚本只做
cargo clean
cargo test --workspace --all-features
依赖项
~23MB
~447K SLoC