Utilities for SQL to execute CRUD-like operations, safely build dynamic queries,
and more. Based on the sqlx crate.
The ursid_sqlx crate aims to provide a database-vendor agnostic, safe, and convenient way to
build and execute SQL queries. It provides multiple layers of abstraction, so that you can
opt in to high-level API's to do simpler things, and lower-level API's when you need to get
closer to the SQL. It doesn't get in your way, and serves to compliment direct use of sqlx.
The name URSID comes from: Update, Replace, Select, Insert, Delete.
- Builders to safely (via parameterized queries) construct dynamic SQL statements &
clauses such as
SELECT,INSERT,WHERE,JOIN,ON. - Higher level builders to execute CRUD-like operations, including emulation of "replace", which inserts or updates a row as required.
- An expressions module full of
rustfunctions & traits that mirror SQL functions, such ascount(),coalesce(), etc. These can be used to compose arbitrary SQL expressions & grammar. - An
Sqltype, that all the API's produce, which safely manages the coupling of the text & values components of a parameterized query. - These
Sql"fragments" can also be concatenated together similar to Strings. - Functions such as
query(),query_as(), etc., just like insqlx, but work with theSqltype.
- All API's must generate SQL safely using parameterized queries. Although you can still create SQL directly from text/string, you are forced to opt-in to this in a clear and explicit way.
- The API surface should be database-vendor agnostic. You can swap out
your app's underlying
sqlxdatabase vendor/connection (e.g. frommysqltopostgres), without having to modify any SQL generation code. - Supported database-vendors, serializable types, etc. should generally be on par with
sqlxitself. There are however some cases where vendor-specific features are excluded as the focus of this crate is on cross-vendor compatibility. - Providing a builder interface for absolutely every feature of SQL is a non-goal.
Instead, you can use lower-level API's, and ultimately
sqlxitself, to achieve any obscure or database-vendor specific behaviour. ursidis not an ORM, nor is it a database library/driver such assqlxitself. It sits somewhere in between these layers, providing convenient API's to do SQL focused operations. Not as high-level as an ORM, and not as low-level as text-based SQL queries.
use ursid_sqlx::BuilderHelper;
let db: Pool<MySql> = PoolOptions::new().connect("...").await?;
db.build_crud()
.update_rows("some_table")
.update_column("some_column", 123)
.update_column("another_column", "some value")
.where_column_equal_to("id", 555)
.execute(&mut *db.acquire().await?)
.await?;insert_row(),delete_rows()work in a similar manner.- The method
with_where_clause()exists to specify more complex WHERE conditions.
use ursid_sqlx::BuilderHelper;
let db: Pool<MySql> = PoolOptions::new().connect("...").await?;
let mut transaction = db.begin().await?;
db.build_crud()
.replace_row("some_table")
.key_columns(["id"]) // names of columns used to uniquely identify the row
.replace_column("another_column", "some value") // will be inserted and/or updated
.insert_column("id", 123) // will only be inserted, never updated
.update_column("column3", 93845) // will only be updated, never inserted
.execute(&mut transaction)
.await?;
transaction.commit().await?;- The CRUD builder's
execute()method's will accept ansqlxconnection or transaction.
use ursid_sqlx::{query, BuilderHelper};
let db: Pool<MySql> = PoolOptions::new().connect("...").await?;
// build the statement
let mut sql = db
.build_sql()
.statement()
.select("some_table")
.select_columns(["id", "name"])
.where_column_equal_to("username", "some_user")
.finalize_and_freeze()?;
assert_eq!(
sql.query(),
"select `id`, `name` from `some_table` where `username`=?"
);
// use the statement to query the database
let _rows = query(&mut sql)?.fetch_all(&db).await?;- This SELECT includes a join in the FROM clause, and a more complex WHERE clause.
- Note that the
select_column()methods, etc. now require both a table and column reference, due to the use of a join.
use ursid_sqlx::sql_lang::clause::{SqlFrom, Where};
use ursid_sqlx::{query, BuilderHelper, IntoRawSql};
let db: Pool<MySql> = PoolOptions::new().connect("...").await?;
// build the statement
let mut sql = db
.build_sql()
.statement()
.select_with_join(
SqlFrom::build("user", "u")
.inner_join("employee", "e", ("e", "user_id", "u", "id").try_into()?)
.finalize()?,
)
.select_column("u", "id")
.select_column("u", "name")
.select_column_with_alias("e", "start_date", "employee_date")
.where_clause(|clause| {
clause
.column_equal_to("u", "username", "some_user")
.and_column_equal_to("e", "is_active", 1)
.and_column_in("e", "foo_id", [1, 2, 3])
})
.finalize_and_freeze()?;
assert_eq!(
sql.query(),
"\
select `u`.`id`, `u`.`name` \
from `user` `u` inner join `employee` `e` on `e`.`user_id` = `u`.`id` \
where \
`u`.`username`=? \
and `e`.`is_active`=? \
and `e`.`foo_id` in (?,?,?)\
"
);
// use the statement to query the database
let _rows = query(&mut sql)?.fetch_all(&db).await?;- Sometimes it can be cumbersome to use builders to create complex SQL expressions. The expressions module contains a series of rust fn's and traits to do this a different way.
- The two approaches can be combined as desired. The various builders have methods to integrate
arbitrary expressions, such as
SelectBuilder::select_expression()in the example below.
use ursid_sqlx::{query, BuilderHelper};
let db: Pool<MySql> = PoolOptions::new().connect("...").await?;
// build the statement
let mut sql = db
.build_sql()
.statement()
.select("user")
.select_expression(
{
use ursid_sqlx::sql_lang::expression::prelude::*;
concat(
"USER-",
coalesce(ColumnReference::new("start_date"), "2000-01-01"),
)
},
"badge",
)
.where_column_equal_to("username", "some_user")
.finalize_and_freeze()?;
assert_eq!(
sql.query(),
"\
select concat(?, coalesce(`start_date`, ?)) `badge`\
from `user` \
where `username`=?\
"
);
// use the statement to query the database
let _rows = query(&mut sql)?.fetch_all(&db).await?;- The
concat()andcoalesce()fn's in the example above, are just normal rust functions that mirror the corresponding SQL function. - They leverage the
IntoSqltrait to safely accept arbitrary values as arguments. - Simple argument values are safely integrated into the generated SQL as parameter bindings.
- The fn's return various "AST" types, which can be converted into the
Sqltype, just like the output of the various builders, etc.
Feature completion for this crate has two aspects; general SQL builder/API coverage, and database-vendor coverage for rendering those builders to the vendors dialect.
The former is easy to implement and is being done as needed. The latter is currently
focused on mysql & postgres coverage. Contributions are welcome on either!
The builders throughout this crate use rust const generics a lot. This enables compile-time
guarantees that you don't build a SELECT statement without any selected columns, or a
WHERE clause without any predicates, etc. They also provide some method variations, with a
goal of enforcing best-practices. For example, .select_column() requires two arguments
when building a SELECT that involves a JOIN, the column reference and the table reference.
A side-effect of this however, is that the builder types have many separate impl's, and
the generated rust documentation can be hard to navigate.