7 releases

0.0.7 Jul 30, 2024
0.0.6 Jan 20, 2024
0.0.5 Dec 10, 2023
0.0.4 Nov 18, 2023
0.0.1 Apr 13, 2023

#1220 in Database interfaces

Download history 2/week @ 2024-09-13 17/week @ 2024-09-20 3/week @ 2024-09-27 1/week @ 2024-10-04 3/week @ 2024-10-11

316 downloads per month
Used in 2 crates (via cnsprcy)

GPL-3.0-or-later

59KB
1.5K SLoC

Experimental library for using SQLite with minimal SQL

Liter generates complete SQL schema definitions from ordinary Rust struct definitions at compile-time. Built on top of rusqlite, it leverages powerful user-implementable traits to generate well-constrained and type-aware table definitions.

SQL is not generated by the derive macros directly: they can only operate textually, so they invoke const functions instead. These then have access to the types and their trait implementations, which give you control over how the SQL is generated.

Basic Example

Here's a very simple example of a database with just one table.

 use liter::{database, Table};

 #[database]
 struct ShoppingList (
     Item
 );

 #[derive(Table, PartialEq, Debug)]
 struct Item {
     count: u32,
     name: String
 }

 let list = ShoppingList::create_in_memory()?;

 let oranges = Item {
     count: 3,
     name: "Orange".to_string(),
 };

 list.insert(&oranges)?;
 let items = list.get_all::<Item>()?;

 assert_eq!(oranges, items[0]);
 # Ok::<(), rusqlite::Error>(())

Liter generates the following SQL schema for you, as well as the SELECT & INSERT statements.

 BEGIN TRANSACTION;
 CREATE TABLE item (
     count INTEGER NOT NULL,
     name TEXT NOT NULL
 ) STRICT;
 END TRANSACTION;

Schema Generation Overview

There are several traits that make up a liter database, but it is actually quite straight-forward. We'll start at the top (or root, if you'll think of it as a tree), and work our way down.

A Schema defines a liter database by its constituent Tables, which is to say it defines the database by the tables it contains. This trait is implemented by the #[database] proc-macro on a tuple struct declaring the Table types that are part of the database.

The Table trait is implemented by using #[derive(Table)] on a regular struct. Each row of the generated SQL table (named after the struct) will store an instance of this struct. You might assume that each field of the struct represents a column in its table, and that is almost correct.

The Value trait is an intermediary layer which represents one or more Columns. As such, it is implemented for all types that implement Column -- which, as you may have guessed, represents a primitive data type that can be stored in a single SQL column -- but it is also implemented (and can be implemented by you) for types that require multiple SQL columns. In fact, a Value can be defined not only as a set of Columns, but as a set of Values.

Though it is admittedly rather generically named, the Value trait is an important abstraction that allows defining database tables with reusable & composable components rather than just column primitives. For instance, it enables easy foreign key references through the generic [Ref] struct, even to tables with composite primary keys.

Example with Primary & Foreign Keys

This slightly more complicated example showcases foreign key references & composite primary keys.

 use liter::{database, Table, Id, Ref};

 #[database]
 struct Dictionary (
     Language,
     Word
 );

 #[derive(Table)]
 struct Language {
     #[key]
     id: Id,
     name: String
 }

 #[derive(Table)]
 struct Word {
     #[key]
     language: Ref<Language>,
     #[key]
     word: String,
     definition: String
 }
 let dict = Dictionary::create_in_memory()?;

 let mut lang = Language {
     id: Id::NULL,
     name: "Latin".to_string()
 };
 dict.create(&mut lang)?; // assigns the newly created Id

 let word = Word {
     language: Ref::make_ref(&lang),
     word: "nunc".to_string(),
     definition:
         "now, at present, at this time, at this very moment".to_string()
 };
 dict.insert(&word)?;
 # Ok::<(), rusqlite::Error>(())

Note that this time, a few more SQL statements were generated as part of the HasKey impls for Language & Word.

And here's the generated schema:

 BEGIN TRANSACTION;
 CREATE TABLE language (
     id INTEGER NOT NULL,
     name TEXT NOT NULL,
     PRIMARY KEY ( id )
 ) STRICT;
 CREATE TABLE word (
     language INTEGER NOT NULL,
     word TEXT NOT NULL,
     definition TEXT NOT NULL,
     PRIMARY KEY ( language, word ),
     FOREIGN KEY (language) REFERENCES language
         ON UPDATE RESTRICT
         ON DELETE RESTRICT
         DEFERRABLE INITIALLY DEFERRED
 ) STRICT;
 END TRANSACTION;

Dependencies

~23MB
~435K SLoC