Skip to content
This repository was archived by the owner on Aug 18, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# IDEs
.idea

# Rust
/target
56 changes: 54 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ heck = "0.4.0"
indexmap = { version = "1.8.2" }
postgres = { version = "0.19.3", optional = true }
postgres-types = { version = "0.2.3", features = ["derive"] }
sea-query = { version = "0.26.3", default-features = false, features = ["backend-postgres", "derive", "with-chrono"], optional = true }
thiserror = "1.0.31"
time = { version = "0.3.9", features = ["parsing"] }

Expand All @@ -23,4 +24,6 @@ postgres = { version = "0.19.3", features = ["with-chrono-0_4", ] }
tempfile = "^3.3.0"

[features]
default = ["postgres", ]
default = ["postgres", "sql"]
# Enables SQL query builder functionality.
sql = ["dep:sea-query"]
90 changes: 79 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
# instant-models

Run tests:
## Generate Rust code with the CLI

```shell
cargo test -- --nocapture
cargo run --bin cli --features="postgres clap sql" -- -t "accounts" > accounts.rs
```

## Generate Rust code with the cli


```shell
cargo run --bin cli --features="postgres clap" -- -t "accounts" > accounts.rs
```


```shell
$ cargo run --bin cli --features="postgres clap" -- --help
$ cargo run --bin cli --features="postgres clap sql" -- --help
instant-models 0.1.0
Generate Rust code from postgres table

Expand All @@ -32,3 +24,79 @@ OPTIONS:
-t, --table-name <TABLE_NAME> Name of the table to generate
-V, --version Print version information
```

## Query Builder

When the `sql` feature is enabled, generated tables include code to compose simple SQL queries.

E.g. Consider the following generated table structure.

```rust
pub struct Accounts {
pub user_id: i32,
pub username: String,
pub password: String,
pub email: String,
pub created_on: chrono::naive::NaiveDateTime,
pub last_login: Option<chrono::naive::NaiveDateTime>,
}
```

We can construct a query to retrieve the username and email address of all users:
```rust
// SELECT "username", "email" FROM "accounts"
let select: String = Accounts::query()
.select(|a| [a.username, a.email])
.to_string();
```

Conditionals can be specified using `filter` and combined using bitwise operators:
```rust
// SELECT "username", "email" FROM "accounts"
// WHERE "last_login" IS NOT NULL AND ("user_id" = 1 OR "username" != "admin")
let select: String = Accounts::query()
.select(|a| [a.username, a.email])
.filter(|a| Sql::is_not_null(a.last_login) & (Sql::eq(a.user_id, 1) | Sql::ne(a.username, "admin")))
.to_string();
```

### Fetching Queries

With the `postgres` feature enabled, the query can be excuted directly using the [postgres](https://crates.io/crates/postgres) crate:

```rust
use postgres::{Config, NoTls, Row};

// Connect to a Postgres database.
let client = &mut Config::new()
.user("postgres")
.password("postgres")
.host("127.0.0.1")
.port(5432)
.dbname("postgres")
.connect(NoTls)
.unwrap();

// Fetch all rows.
let rows: Vec<Row> = Accounts::query()
.select(|a| [a.username, a.email])
.fetch(client, &[])
.unwrap();
```

## Development


### Tests

Start a local, ephemeral Postgres instance:

```shell
docker run -it -p 127.0.0.1:5432:5432 --rm -e POSTGRES_PASSWORD=postgres postgres
```

Run all tests:

```shell
cargo test -- --nocapture
```
2 changes: 2 additions & 0 deletions src/bin/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ fn main() {
println!("{}", struct_bldr.build_type());
println!("\n{}", struct_bldr.build_new_type());
println!("\n{}", struct_bldr.build_type_methods());
#[cfg(feature = "sql")]
println!("\n{}", struct_bldr.build_field_identifiers());
}
11 changes: 7 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
mod struct_builder;
pub use column::*;
#[cfg(feature = "sql")]
pub use sql::*;
pub use struct_builder::*;
pub use types::*;

mod column;
pub use column::*;

#[cfg(feature = "sql")]
mod sql;
mod struct_builder;
mod types;
pub use types::*;
101 changes: 101 additions & 0 deletions src/sql/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use std::borrow::Cow;
use std::marker::PhantomData;

use crate::Table;

/// SQL column definition with the Rust type.
// TODO: add table type back-reference?
pub struct Field<Type, Table: crate::Table> {
pub name: &'static str,
// TODO: replace sea_query.
pub iden: Table::IdenType,
pub typ: PhantomData<Type>,
pub table: PhantomData<Table>,
}

impl<Type, Table: crate::Table> Field<Type, Table> {
pub const fn new(name: &'static str, iden: Table::IdenType) -> Self {
Self {
name,
iden,
typ: PhantomData::<Type>,
table: PhantomData::<Table>,
}
}

pub fn table() -> Table::IdenType {
Table::table()
}
}

// TODO: replace sea_query.
impl<Type, Table: crate::Table + 'static> sea_query::IntoIden for Field<Type, Table> {
fn into_iden(self) -> sea_query::DynIden {
self.iden.into_iden()
}
}

/// Helper trait for converting tuples of fields into an iterator.
pub trait FieldList {
type IntoIter: Iterator<Item = sea_query::DynIden>;

fn into_iter(self) -> Self::IntoIter;
}

macro_rules! impl_field_list {
( $( $name:ident.$idx:tt )+ ) => {
impl<$($name),+> FieldList for ($($name,)+)
where $($name: sea_query::IntoIden,)+
{
type IntoIter = std::vec::IntoIter<sea_query::DynIden>;

fn into_iter(self) -> Self::IntoIter {
vec![$(self.$idx.into_iden(),)+].into_iter()
}
}
};
}

// If you need to select more than 12 fields in a single query, open an issue.
impl_field_list!(A.0);
impl_field_list!(A.0 B.1);
impl_field_list!(A.0 B.1 C.2);
impl_field_list!(A.0 B.1 C.2 D.3);
impl_field_list!(A.0 B.1 C.2 D.3 E.4);
impl_field_list!(A.0 B.1 C.2 D.3 E.4 F.5);
impl_field_list!(A.0 B.1 C.2 D.3 E.4 F.5 G.6);
impl_field_list!(A.0 B.1 C.2 D.3 E.4 F.5 G.6 H.7);
impl_field_list!(A.0 B.1 C.2 D.3 E.4 F.5 G.6 H.7 I.8);
impl_field_list!(A.0 B.1 C.2 D.3 E.4 F.5 G.6 H.7 I.8 J.9);
impl_field_list!(A.0 B.1 C.2 D.3 E.4 F.5 G.6 H.7 I.8 J.9 K.10);
impl_field_list!(A.0 B.1 C.2 D.3 E.4 F.5 G.6 H.7 I.8 J.9 K.10 L.11);

/// Marker trait to indicate which types and fields can be compared.
pub trait Compatible<Type> {}

impl<Type, T1: Table, T2: Table> Compatible<Field<Type, T1>> for Field<Type, T2> {}

impl<Type, T1: Table, T2: Table> Compatible<Field<Type, T1>> for Field<Option<Type>, T2> {}

impl<Type, T1: Table, T2: Table> Compatible<Field<Option<Type>, T1>> for Field<Type, T2> {}

impl<Type, T: Table> Compatible<Field<Type, T>> for Type {}

impl<Type, T: Table> Compatible<Field<Type, T>> for Option<Type> {}

impl<Type, T: Table> Compatible<Field<Option<Type>, T>> for Type {}

macro_rules! impl_compatible {
( $t:ty | $( $s:ty ),+ ) => ($(
impl<T: Table> Compatible<Field<$t, T>> for $s {}
impl<T: Table> Compatible<Field<$t, T>> for Option<$s> {}
impl<T: Table> Compatible<Field<Option<$t>, T>> for $s {}
impl<T: Table> Compatible<Field<Option<$t>, T>> for Option<$s> {}

impl<T1: Table, T2: Table> Compatible<Field<$t, T1>> for Field<$s, T2> {}
impl<T1: Table, T2: Table> Compatible<Field<$t, T1>> for Field<Option<$s>, T2> {}
impl<T1: Table, T2: Table> Compatible<Field<Option<$t>, T1>> for Field<$s, T2> {}
)*)
}

impl_compatible!(String | &str, Cow<'static, str>);
7 changes: 7 additions & 0 deletions src/sql/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod field;
mod query;
mod table;

pub use field::*;
pub use query::*;
pub use table::*;
Loading