Kosame (小雨, Japanese for "light rain" or "drizzle") is a Rust ORM inspired by Prisma and Drizzle.
Some TypeScript ORMs like Prisma can infer the result type of a database query based solely on the database schema and the query itself. Conversely, most Rust ORMs require developers to manually define a struct for the query's results, even though this type is tightly coupled to the query itself. Kosame was born out of a desire to have this level of developer ergonomics in Rust, using macro magic. Kosame also offers relational queries, allowing you to fetch multiple nested 1:N relationships in a single statement.
Kosame requires no active database connection during development and has no build step. Despite this, Kosame offers strong typing and rust-analyzer auto-completion.
Kosame is currently a prototype and not recommended for production use.
- Showcase
- Planned features
- Declaring the schema
- Queries
- Statements
- Kosame CLI
- Can Kosame handle all use cases well?
Showcase
use *;
// Declare your database schema. You may split the schema into multiple Rust modules.
async
// This function connects to a database using tokio-postgres.
async
Planned features
Kosame is an early prototype. There are many features and performance optimizations left to implement, including but not limited to:
- Support for other database management systems. Currently, only PostgreSQL (using
tokio_postgres) is supported. - CLI for generating database migrations based on changes in the Kosame schema.
- CLI for generating a Kosame schema by introspecting a database.
- Support for more SQL expression syntax.
- Alternative query runners, similar to the
relationLoadStrategythat Prisma offers. - Type inference for bind parameters.
Declaring the schema
Before you can write queries with Kosame, you must declare your database schema. Instead of inventing a new syntax, Kosame tries to follow the existing CREATE TABLE syntax closely.
pg_table!
This means declaring your schema may be as simple as copying a pg_dump into the Kosame macro. However, to enforce consistency, all SQL keywords must be lowercase. Kosame has a basic SQL expression parser, which allows you to define the default expression of a column.
Column renaming and type overrides
Kosame converts database identifiers to snake_case by default. If you want to refer to a database column by a different name in Rust, you can rename it:
pg_table!
Kosame attempts to guess the Rust type of a column based on its database type. For example, a PostgreSQL column of type text will be represented by a Rust String. If you want to use a different type, or if the database type is unknown to Kosame (e.g., for PostgreSQL custom types), you can specify a type override:
use smol_str;
pg_table!
Note that the specified type must either be declared or used in the scope of the kosame::pg_table! call or be a fully qualified path (e.g., crate::MyType or ::std::string::String).
Relations
Diverging from regular SQL syntax, you can declare relation fields. Relations tell Kosame how different tables can be queried together.
pg_table!
In this example, we have a posts_table and a comments_table. For each row in posts_table, we expect there to be zero or more comments. Conversely, each row in the comments_table has exactly one post associated with it, as defined by the post_id column.
The relation field declaration
comments: (id) <= my_module::comments_table (post_id)
describes a relation called comments. It specifies that the post_id column in my_module::comments_table "points to" the id column of posts_table. Although a Kosame relation does not have to map to a database foreign key, you can think of the <= as pointing in the direction of the foreign key "pointer". With this relation field, we can query all comments associated with a given post:
pg_query!
In the comments table, we have the inverse relation:
post: (post_id) => super::posts_table (id),
This states that post is a row in super::posts_table, and it is linked by matching the comments_table's post_id column with the posts_table's id column. Note that the arrow (=>) points in the other direction here. In this case, Kosame expects there to be at most one post per comment.
pg_query!
Queries
Columns and relations
A basic Kosame query starts by defining the root table you want to read from. This can be a relative or absolute path to your table's declaration.
pg_query!
// or
pg_query!
In the query, you can list the column and relation fields you want to read. Relations can be nested as often as desired.
pg_query!
Instead of listing each column manually, you can also use * to select all columns of a table.
pg_query!
Aliases and type overrides
You can rename column or relation fields for each query using as .... You can also change the Rust type of a column using : ....
pg_query!
The row structs generated by Kosame will use the new aliases and data types.
Attributes
Kosame allows you to annotate your query and its fields with Rust attributes. Attributes assigned to the top-level table will be applied to all generated row structs, including those representing nested relations. Attributes above column or relation fields will be assigned only to the row struct field they correspond to. It is also possible to document your query with documentation comments. Enable the serde feature for automatic serde derives on all row structs.
pg_query!
Serializing the result of the query above using serde_json returns the following JSON string:
Expressions
Kosame can parse basic SQL expressions. Expressions can be used in various places, one of which is an expression field in your query:
pg_query!
Like in the table definition, SQL keywords must be lowercase. Expression fields in a query must be aliased and given a type override. Kosame makes no attempt to deduce the name or type of an expression automatically.
The main difference between the syntax of Kosame expressions and SQL expressions is the handling of string literals and identifiers. Unlike in PostgreSQL, you do not need to use double-quotes to make your identifiers case-sensitive. Strings are written using double-quoted Rust strings, as opposed to single quotes:
pg_query!
Bind parameters
Kosame uses the :param_name syntax for using bind parameters in expressions:
pg_query!
Kosame generates a Params struct containing a borrowed field for each parameter referenced in your query. When executing the query, the bind parameters are converted to the respective database management system's parameter syntax (e.g., $1, $2, etc., for PostgreSQL).
where, order by, limit, and offset
Kosame uses the familiar syntax for where, order by, limit, and offset. You can use expressions for each of these:
pg_query!
where, order by, limit, and offset must be specified in this order. They must come at the end of a block in a query. Make sure your last query field has a trailing comma.
Named vs. anonymous queries
Kosame supports both named and anonymous queries. Anonymous queries are defined inline and act as a Rust expression that can be executed immediately. They also allow capturing variables from the surrounding scope as bind parameters for the query (:id in this example):
let id = 5;
let rows = pg_query!
.exec
.await?;
While they are concise, anonymous queries have the drawback that the row types generated by Kosame cannot be named. This makes it difficult to specify concrete return types. We can only resort to the impl Trait syntax.
async
Named queries solve this problem by declaring the query upfront. To do this, give your query an alias that will be used as the module name generated by Kosame:
pg_query!
You can now refer to all generated types by name:
async
Statements
Kosame also supports an SQL-like syntax for SELECT, INSERT, UPDATE, and DELETE queries which make database mutations possible and allow for greater oversight and flexibility over what exactly your database does.
select
A simple select statement works without a from clause.
let rows = pg_statement!
.query_one
.await?;
You can also buid more complex queries with where, group by, having, order by, limit, and offset.
let rows = pg_statement!
.query_vec
.await?;
Common table expressions and (lateral) subqueries are also supported:
let rows = pg_statement!
.query_vec
.await?;
Kosame also supports set operations for combining multiple select statements:
let rows = pg_statement!
.query_vec
.await?;
The following set operations are supported:
union- Combines results from multiple queries, removing duplicatesunion all- Combines results from multiple queries, keeping duplicatesintersect- Returns only rows that appear in both queriesintersect all- Returns rows that appear in both queries, keeping duplicatesexcept- Returns rows from the first query that don't appear in the secondexcept all- Returns rows from the first query that don't appear in the second, keeping duplicates
The Rust type is inferred from the first set of select fields in the chain of operations.
insert
let new_post_ids = pg_statement!
// With the `RETURNING` clause we can now use `query` instead of `exec` and retrieve data.
.query_vec
.await?;
update
let new_upvotes = pg_statement!
.query_one
.await?;
delete
pg_statement!
.exec
.await?;
Kosame CLI
Kosame provides a command-line tool for code formatting. In the future, it will also be used for database migrations and introspection. Install the CLI tool using:
And make sure your PATH environment variable is configured correctly (see https://rust-lang.org/tools/install/).
Commands can be run either through the kosame binary or using cargo kosame ....
Formatting Kosame macros
The CLI includes a formatter that automatically reformats only the contents of pg_table!, pg_query!, and pg_statement! macros (and their non-pg_ variants) with proper indentation and structure.
# Format a single file
# Format multiple files
# Format all Rust files in a directory recursively
# Use glob patterns
# Read from stdin and write to stdout (useful for editor integrations)
Editor integration
Neovim with conform.nvim
Create a Kosame.toml file at the root of your Rust project. Then add the following conform.nvim setup to your Neovim configuration:
require.
Can Kosame handle all use cases well?
No. Writing raw SQL directly will always give you more flexibility and control over what your database does, which may also allow you to optimize performance beyond what the Kosame supports. But that's okay! You can combine Kosame with another method to access the database. Use Kosame for situations in which you benefit from the relational query syntax and type inference. In more demanding situations, consider using a crate like sqlx.