Skip to content
Open
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
142 changes: 142 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ resolver = "2"
static_sqlite_macros = { path = "static_sqlite_macros", version = "0.1.0" }
static_sqlite_core = { path = "static_sqlite_core", version = "0.1.0" }
static_sqlite_async = { path = "static_sqlite_async", version = "0.1.0" }

futures = { version = "0.3" }
anyhow = "1.0.97"
[dev-dependencies]
tokio = { version = "1", features = ["rt", "sync", "macros"] }
trybuild = "1.0"
Expand Down
176 changes: 176 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,182 @@ async fn main() -> Result<()> {
cargo add --git https://github.com/swlkr/static_sqlite
```


# Example for Transactions

Use the methods begin_transaction, commit_transaction and rollback_transaction to manage Sqlite transactions.


```rust

// migration and sql-fn definition goes here

let db = static_sqlite::open(":memory:").await?;

migrate(&db).await?;

db.begin_transaction()?;
insert_row(&db, "test1").await?.first_row()?;
insert_row(&db, "test2").await?.first_row()?;
db.commit_transaction()?;
```

# Example for First

If the name of your statement ends with "_first", the created fn return an Option<T> with the first value instead of a Vec<T>.

I the query returns more than one rows, it throws an error.

```rust
sql! {
let migrate = r#"
create table Row (
id integer primary key autoincrement,
txt text NOT NULL
)
"#;

let insert_row = r#"
insert into Row (txt) values (:txt) returning *
"#;

let select_row = r#"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting, I actually had a feature in a previous version where you could infer a single row from limit 1 want to bring that back? instead of a _first suffix?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think that would be better than a suffix.

select * from Row where id = :id
"#;
}

let db = static_sqlite::open(":memory:").await?;
migrate(&db).await?;

insert_row(&db, "test1").await?.first_row()?;
insert_row(&db, "test2").await?.first_row()?;

match select_row_first(&db, 1).await? {
Some(row) => assert_eq!(row.txt, "test1"),
None => panic!("Row 1 not found"),
}
```

# Example for Streams

If the name of your statement ends with "_stream", the created fn return an async Stream<T> instead of a Vec<T>.

This way you can iterate over large result sets.

```rust
sql! {
let migrate = r#"
create table Row (
txt text
)
"#;

let insert_row = r#"
insert into Row (txt) values (:txt) returning *
"#;

let select_rows_stream = r#"
select * from Row
"#;
}

let db = static_sqlite::open(":memory:").await?;
migrate(&db).await?;

insert_row(&db, Some("test1")).await?.first_row()?;
insert_row(&db, Some("test2")).await?.first_row()?;
insert_row(&db, Some("test3")).await?.first_row()?;
insert_row(&db, Some("test4")).await?.first_row()?;

let f = select_rows_stream(&db).await?;

pin_mut!(f);

assert_eq!(f.next().await.unwrap().unwrap().txt, Some("test1".into()));
assert_eq!(f.next().await.unwrap().unwrap().txt, Some("test2".into()));
assert_eq!(f.next().await.unwrap().unwrap().txt, Some("test3".into()));
assert_eq!(f.next().await.unwrap().unwrap().txt, Some("test4".into()));
}

```

# Example with aliased columns and type-hints

Sometimes the type of either a bound parameter or a returned column can not be inferred by
sqlite / static_sqlite (see [sqlite3 docs](https://www.sqlite.org/c3ref/column_decltype.html))

In this case you can use type-hints to help the static_sqlite to use the correct type.

To use type-hints your parameter or column name needs to follow the following format:

```
<name>__<INTEGER|REAL|TEXT|BLOB>
```

or

```
<name>__<INTEGER|REAL|TEXT|BLOB>__<NULLABLE|NOT_NULL>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of want to keep it limited to valid sql only, even if it does sort of limit this lib

Copy link
Author

@toolbar23 toolbar23 Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 - I also like to still be able to copy/paste the SQL from/to other tools. the proposed attributes would still be legal though.

I have no real idea about what a macro does/how exactly the tokenization etc works, but if the macro didn't just expect a string here

   let get_friendship = r#"
       select
           u1.name as friend1_name__TEXT,
           u2.name as friend2_name__TEXT
       from Friendship, User as u1, User as u2
       where Friendship.user_id = u1.id
             and Friendship.friend_id = u2.id
             and Friendship.id = :friendship_id
   "#;

but accepted additional "params" like

      let get_friendship = first(r#"
           SELECT
               u1.name as friend1_name,
               u2.name as friend2_name
           FROM Friendship, User as u1, User as u2
           WHERE Friendship.user_id = u1.id
                 AND Friendship.friend_id = u2.id
                 AND Friendship.id = :friendship_id
       "#,
       TypeHint::IsInteger("friendship_id"),
       TypeHint::IsText("friend2_name"),
       TypeHint::IsText("friend1_name"))

this would keep the SQL clearer.

```

If not explicitly specified, the parameter or column is assumed to be NOT NULL.

```rust
sql! {
let migrate = r#"
create table User (
id integer primary key,
name text unique not null
);
create table Friendship (
id integer primary key,
user_id integer not null references User(id),
friend_id integer not null references User(id)
);
"#;

let insert_user = r#"
insert into User (name)
values (:name)
returning *
"#;
let create_friendship = r#"
insert into Friendship (user_id, friend_id)
values (:user_id, :friend_id)
returning *
"#;
let get_friendship = r#"
select
u1.name as friend1_name__TEXT,
u2.name as friend2_name__TEXT
from Friendship, User as u1, User as u2
where Friendship.user_id = u1.id
and Friendship.friend_id = u2.id
and Friendship.id = :friendship_id__INTEGER
"#;
}


#[tokio::main]
async fn main() -> Result<()> {
let db = static_sqlite::open(":memory:").await?;
let _ = migrate(&db).await?;
insert_user(&db, "swlkr").await?;
insert_user(&db, "toolbar23").await?;
create_friendship(&db, 1, 2).await?;

let friends = get_friendship(&db, 1).await?;

assert_eq!(friends.len(), 1);
assert_eq!(friends.first().unwrap().friend1_name, "swlkr");
assert_eq!(friends.first().unwrap().friend2_name, "toolbar23");

Ok(())
}
```



# Treesitter

```
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
extern crate self as static_sqlite;
pub use static_sqlite_async::{
execute, execute_all, open, query, rows, Error, FromRow, Result, Savepoint, Sqlite, Value,
execute, execute_all, open, query, query_first, rows, stream, Error, FromRow, Result,
Savepoint, Sqlite, Value,
};
pub use static_sqlite_core::FirstRow;
pub use static_sqlite_macros::sql;
2 changes: 2 additions & 0 deletions static_sqlite_async/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ edition = "2021"
static_sqlite_core = { path = "../static_sqlite_core", version = "0.1.0" }
tokio = { version = "1", features = ["sync"] }
crossbeam-channel = { version = "0.5" }
futures = { version = "0.3" }
async-stream = "0.3"
Loading