mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 02:46:43 +00:00
Add typed statements
This commit is contained in:
parent
64ac84fdf4
commit
4a00f0b062
14 changed files with 388 additions and 394 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -10,7 +10,6 @@
|
||||||
/assets/themes/Internal/*.json
|
/assets/themes/Internal/*.json
|
||||||
/assets/themes/Experiments/*.json
|
/assets/themes/Experiments/*.json
|
||||||
**/venv
|
**/venv
|
||||||
<<<<<<< HEAD
|
|
||||||
.build
|
.build
|
||||||
Packages
|
Packages
|
||||||
*.xcodeproj
|
*.xcodeproj
|
||||||
|
@ -19,6 +18,4 @@ DerivedData/
|
||||||
.swiftpm/config/registries.json
|
.swiftpm/config/registries.json
|
||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
.netrc
|
.netrc
|
||||||
=======
|
|
||||||
crates/db/test-db.db
|
crates/db/test-db.db
|
||||||
>>>>>>> 9d9ad38ce (Successfully detecting workplace IDs :D)
|
|
||||||
|
|
|
@ -15,24 +15,19 @@ pub(crate) const KVP_MIGRATION: Migration = Migration::new(
|
||||||
|
|
||||||
impl Db {
|
impl Db {
|
||||||
pub fn read_kvp(&self, key: &str) -> Result<Option<String>> {
|
pub fn read_kvp(&self, key: &str) -> Result<Option<String>> {
|
||||||
self.0
|
self.select_row_bound("SELECT value FROM kv_store WHERE key = (?)")?(key)
|
||||||
.prepare("SELECT value FROM kv_store WHERE key = (?)")?
|
|
||||||
.with_bindings(key)?
|
|
||||||
.maybe_row()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_kvp(&self, key: &str, value: &str) -> Result<()> {
|
pub fn write_kvp(&self, key: &str, value: &str) -> Result<()> {
|
||||||
self.0
|
self.exec_bound("INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))")?((
|
||||||
.prepare("INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))")?
|
key, value,
|
||||||
.with_bindings((key, value))?
|
))?;
|
||||||
.exec()
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_kvp(&self, key: &str) -> Result<()> {
|
pub fn delete_kvp(&self, key: &str) -> Result<()> {
|
||||||
self.0
|
self.exec_bound("DELETE FROM kv_store WHERE key = (?)")?(key)
|
||||||
.prepare("DELETE FROM kv_store WHERE key = (?)")?
|
|
||||||
.with_bindings(key)?
|
|
||||||
.exec()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
|
||||||
"}],
|
"}],
|
||||||
);
|
);
|
||||||
|
|
||||||
use self::model::{SerializedWorkspace, WorkspaceId, WorkspaceRow};
|
use self::model::{SerializedWorkspace, WorkspaceId};
|
||||||
|
|
||||||
use super::Db;
|
use super::Db;
|
||||||
|
|
||||||
|
@ -40,21 +40,19 @@ impl Db {
|
||||||
// and we've grabbed the most recent workspace
|
// and we've grabbed the most recent workspace
|
||||||
let (workspace_id, dock_anchor, dock_visible) = iife!({
|
let (workspace_id, dock_anchor, dock_visible) = iife!({
|
||||||
if worktree_roots.len() == 0 {
|
if worktree_roots.len() == 0 {
|
||||||
self.prepare(indoc! {"
|
self.select_row(indoc! {"
|
||||||
SELECT workspace_id, dock_anchor, dock_visible
|
SELECT workspace_id, dock_anchor, dock_visible
|
||||||
FROM workspaces
|
FROM workspaces
|
||||||
ORDER BY timestamp DESC LIMIT 1"})?
|
ORDER BY timestamp DESC LIMIT 1"})?()?
|
||||||
.maybe_row::<WorkspaceRow>()
|
|
||||||
} else {
|
} else {
|
||||||
self.prepare(indoc! {"
|
self.select_row_bound(indoc! {"
|
||||||
SELECT workspace_id, dock_anchor, dock_visible
|
SELECT workspace_id, dock_anchor, dock_visible
|
||||||
FROM workspaces
|
FROM workspaces
|
||||||
WHERE workspace_id = ?"})?
|
WHERE workspace_id = ?"})?(&workspace_id)?
|
||||||
.with_bindings(&workspace_id)?
|
|
||||||
.maybe_row::<WorkspaceRow>()
|
|
||||||
}
|
}
|
||||||
|
.context("No workspaces found")
|
||||||
})
|
})
|
||||||
.log_err()
|
.warn_on_err()
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
|
|
||||||
Some(SerializedWorkspace {
|
Some(SerializedWorkspace {
|
||||||
|
@ -85,23 +83,17 @@ impl Db {
|
||||||
if let Some(old_roots) = old_roots {
|
if let Some(old_roots) = old_roots {
|
||||||
let old_id: WorkspaceId = old_roots.into();
|
let old_id: WorkspaceId = old_roots.into();
|
||||||
|
|
||||||
self.prepare("DELETE FROM WORKSPACES WHERE workspace_id = ?")?
|
self.exec_bound("DELETE FROM WORKSPACES WHERE workspace_id = ?")?(&old_id)?;
|
||||||
.with_bindings(&old_id)?
|
|
||||||
.exec()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete any previous workspaces with the same roots. This cascades to all
|
// Delete any previous workspaces with the same roots. This cascades to all
|
||||||
// other tables that are based on the same roots set.
|
// other tables that are based on the same roots set.
|
||||||
// Insert new workspace into workspaces table if none were found
|
// Insert new workspace into workspaces table if none were found
|
||||||
self.prepare("DELETE FROM workspaces WHERE workspace_id = ?;")?
|
self.exec_bound("DELETE FROM workspaces WHERE workspace_id = ?;")?(&workspace_id)?;
|
||||||
.with_bindings(&workspace_id)?
|
|
||||||
.exec()?;
|
|
||||||
|
|
||||||
self.prepare(
|
self.exec_bound(
|
||||||
"INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)",
|
"INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)",
|
||||||
)?
|
)?((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?;
|
||||||
.with_bindings((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?
|
|
||||||
.exec()?;
|
|
||||||
|
|
||||||
// Save center pane group and dock pane
|
// Save center pane group and dock pane
|
||||||
self.save_pane_group(&workspace_id, &workspace.center_group, None)?;
|
self.save_pane_group(&workspace_id, &workspace.center_group, None)?;
|
||||||
|
@ -126,11 +118,9 @@ impl Db {
|
||||||
iife!({
|
iife!({
|
||||||
// TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
|
// TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
|
||||||
Ok::<_, anyhow::Error>(
|
Ok::<_, anyhow::Error>(
|
||||||
self.prepare(
|
self.select_bound::<usize, WorkspaceId>(
|
||||||
"SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
|
"SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
|
||||||
)?
|
)?(limit)?
|
||||||
.with_bindings(limit)?
|
|
||||||
.rows::<WorkspaceId>()?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| id.paths())
|
.map(|id| id.paths())
|
||||||
.collect::<Vec<Vec<PathBuf>>>(),
|
.collect::<Vec<Vec<PathBuf>>>(),
|
||||||
|
|
|
@ -3,7 +3,7 @@ use indoc::indoc;
|
||||||
use sqlez::migrations::Migration;
|
use sqlez::migrations::Migration;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{ItemId, PaneId, SerializedItem, SerializedItemKind, WorkspaceId},
|
model::{PaneId, SerializedItem, SerializedItemKind, WorkspaceId},
|
||||||
Db,
|
Db,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,19 +29,16 @@ pub(crate) const ITEM_MIGRATIONS: Migration = Migration::new(
|
||||||
|
|
||||||
impl Db {
|
impl Db {
|
||||||
pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
|
pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
|
||||||
Ok(self
|
Ok(self.select_bound(indoc! {"
|
||||||
.prepare(indoc! {"
|
|
||||||
SELECT item_id, kind FROM items
|
SELECT item_id, kind FROM items
|
||||||
WHERE pane_id = ?
|
WHERE pane_id = ?
|
||||||
ORDER BY position"})?
|
ORDER BY position"})?(pane_id)?
|
||||||
.with_bindings(pane_id)?
|
.into_iter()
|
||||||
.rows::<(ItemId, SerializedItemKind)>()?
|
.map(|(item_id, kind)| match kind {
|
||||||
.into_iter()
|
SerializedItemKind::Terminal => SerializedItem::Terminal { item_id },
|
||||||
.map(|(item_id, kind)| match kind {
|
_ => unimplemented!(),
|
||||||
SerializedItemKind::Terminal => SerializedItem::Terminal { item_id },
|
})
|
||||||
_ => unimplemented!(),
|
.collect())
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn save_items(
|
pub(crate) fn save_items(
|
||||||
|
@ -51,19 +48,14 @@ impl Db {
|
||||||
items: &[SerializedItem],
|
items: &[SerializedItem],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut delete_old = self
|
let mut delete_old = self
|
||||||
.prepare("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?")
|
.exec_bound("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?")
|
||||||
.context("Preparing deletion")?;
|
.context("Preparing deletion")?;
|
||||||
let mut insert_new = self.prepare(
|
let mut insert_new = self.exec_bound(
|
||||||
"INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)",
|
||||||
).context("Preparing insertion")?;
|
).context("Preparing insertion")?;
|
||||||
for (position, item) in items.iter().enumerate() {
|
for (position, item) in items.iter().enumerate() {
|
||||||
delete_old
|
delete_old((workspace_id, pane_id, item.item_id()))?;
|
||||||
.with_bindings((workspace_id, pane_id, item.item_id()))?
|
insert_new((item.item_id(), workspace_id, pane_id, item.kind(), position))?;
|
||||||
.exec()?;
|
|
||||||
|
|
||||||
insert_new
|
|
||||||
.with_bindings((item.item_id(), workspace_id, pane_id, item.kind(), position))?
|
|
||||||
.exec()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -80,8 +80,6 @@ impl Column for DockAnchor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type WorkspaceRow = (WorkspaceId, DockAnchor, bool);
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct SerializedWorkspace {
|
pub struct SerializedWorkspace {
|
||||||
pub dock_anchor: DockAnchor,
|
pub dock_anchor: DockAnchor,
|
||||||
|
@ -240,23 +238,20 @@ mod tests {
|
||||||
workspace_id BLOB,
|
workspace_id BLOB,
|
||||||
dock_anchor TEXT
|
dock_anchor TEXT
|
||||||
);"})
|
);"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let workspace_id: WorkspaceId = WorkspaceId::from(&["\test2", "\test1"]);
|
let workspace_id: WorkspaceId = WorkspaceId::from(&["\test2", "\test1"]);
|
||||||
|
|
||||||
db.prepare("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
|
db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
|
||||||
.unwrap()
|
.unwrap()((&workspace_id, DockAnchor::Bottom))
|
||||||
.with_bindings((&workspace_id, DockAnchor::Bottom))
|
.unwrap();
|
||||||
.unwrap()
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.prepare("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
|
db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.row::<(WorkspaceId, DockAnchor)>()
|
.unwrap(),
|
||||||
.unwrap(),
|
Some((WorkspaceId::from(&["\test1", "\test2"]), DockAnchor::Bottom))
|
||||||
(WorkspaceId::from(&["\test1", "\test2"]), DockAnchor::Bottom)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use sqlez::{migrations::Migration, statement::Statement};
|
use sqlez::migrations::Migration;
|
||||||
use util::unzip_option;
|
use util::unzip_option;
|
||||||
|
|
||||||
use crate::model::{Axis, GroupId, PaneId, SerializedPane};
|
use crate::model::{Axis, GroupId, PaneId, SerializedPane};
|
||||||
|
@ -39,38 +39,29 @@ impl Db {
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &WorkspaceId,
|
workspace_id: &WorkspaceId,
|
||||||
) -> Result<SerializedPaneGroup> {
|
) -> Result<SerializedPaneGroup> {
|
||||||
let mut query = self.prepare(indoc! {"
|
self.get_pane_group_children(workspace_id, None)?
|
||||||
SELECT group_id, axis, pane_id
|
|
||||||
FROM (SELECT group_id, axis, NULL as pane_id, position, parent_group_id, workspace_id
|
|
||||||
FROM pane_groups
|
|
||||||
UNION
|
|
||||||
SELECT NULL, NULL, pane_id, position, parent_group_id, workspace_id
|
|
||||||
FROM panes
|
|
||||||
-- Remove the dock panes from the union
|
|
||||||
WHERE parent_group_id IS NOT NULL and position IS NOT NULL)
|
|
||||||
WHERE parent_group_id IS ? AND workspace_id = ?
|
|
||||||
ORDER BY position
|
|
||||||
"})?;
|
|
||||||
|
|
||||||
self.get_pane_group_children(workspace_id, None, &mut query)?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.context("No center pane group")
|
.context("No center pane group")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pane_group_children(
|
fn get_pane_group_children<'a>(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &WorkspaceId,
|
workspace_id: &WorkspaceId,
|
||||||
group_id: Option<GroupId>,
|
group_id: Option<GroupId>,
|
||||||
query: &mut Statement,
|
|
||||||
) -> Result<Vec<SerializedPaneGroup>> {
|
) -> Result<Vec<SerializedPaneGroup>> {
|
||||||
let children = query.with_bindings((group_id, workspace_id))?.rows::<(
|
self.select_bound::<(Option<GroupId>, &WorkspaceId), (Option<GroupId>, Option<Axis>, Option<PaneId>)>(indoc! {"
|
||||||
Option<GroupId>,
|
SELECT group_id, axis, pane_id
|
||||||
Option<Axis>,
|
FROM (SELECT group_id, axis, NULL as pane_id, position, parent_group_id, workspace_id
|
||||||
Option<PaneId>,
|
FROM pane_groups
|
||||||
)>()?;
|
UNION
|
||||||
|
SELECT NULL, NULL, pane_id, position, parent_group_id, workspace_id
|
||||||
children
|
FROM panes
|
||||||
|
-- Remove the dock panes from the union
|
||||||
|
WHERE parent_group_id IS NOT NULL and position IS NOT NULL)
|
||||||
|
WHERE parent_group_id IS ? AND workspace_id = ?
|
||||||
|
ORDER BY position
|
||||||
|
"})?((group_id, workspace_id))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(group_id, axis, pane_id)| {
|
.map(|(group_id, axis, pane_id)| {
|
||||||
if let Some((group_id, axis)) = group_id.zip(axis) {
|
if let Some((group_id, axis)) = group_id.zip(axis) {
|
||||||
|
@ -79,7 +70,6 @@ impl Db {
|
||||||
children: self.get_pane_group_children(
|
children: self.get_pane_group_children(
|
||||||
workspace_id,
|
workspace_id,
|
||||||
Some(group_id),
|
Some(group_id),
|
||||||
query,
|
|
||||||
)?,
|
)?,
|
||||||
})
|
})
|
||||||
} else if let Some(pane_id) = pane_id {
|
} else if let Some(pane_id) = pane_id {
|
||||||
|
@ -107,9 +97,8 @@ impl Db {
|
||||||
|
|
||||||
match pane_group {
|
match pane_group {
|
||||||
SerializedPaneGroup::Group { axis, children } => {
|
SerializedPaneGroup::Group { axis, children } => {
|
||||||
let parent_id = self.prepare("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")?
|
let parent_id = self.insert_bound("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")?
|
||||||
.with_bindings((workspace_id, parent_id, position, *axis))?
|
((workspace_id, parent_id, position, *axis))?;
|
||||||
.insert()? as GroupId;
|
|
||||||
|
|
||||||
for (position, group) in children.iter().enumerate() {
|
for (position, group) in children.iter().enumerate() {
|
||||||
self.save_pane_group(workspace_id, group, Some((parent_id, position)))?
|
self.save_pane_group(workspace_id, group, Some((parent_id, position)))?
|
||||||
|
@ -121,12 +110,12 @@ impl Db {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
|
pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
|
||||||
let pane_id = self
|
let pane_id = self.select_row_bound(indoc! {"
|
||||||
.prepare(indoc! {"
|
|
||||||
SELECT pane_id FROM panes
|
SELECT pane_id FROM panes
|
||||||
WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?
|
WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?(
|
||||||
.with_bindings(workspace_id)?
|
workspace_id,
|
||||||
.row::<PaneId>()?;
|
)?
|
||||||
|
.context("No dock pane for workspace")?;
|
||||||
|
|
||||||
Ok(SerializedPane::new(
|
Ok(SerializedPane::new(
|
||||||
self.get_items(pane_id).context("Reading items")?,
|
self.get_items(pane_id).context("Reading items")?,
|
||||||
|
@ -141,10 +130,9 @@ impl Db {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (parent_id, order) = unzip_option(parent);
|
let (parent_id, order) = unzip_option(parent);
|
||||||
|
|
||||||
let pane_id = self
|
let pane_id = self.insert_bound(
|
||||||
.prepare("INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)")?
|
"INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)",
|
||||||
.with_bindings((workspace_id, parent_id, order))?
|
)?((workspace_id, parent_id, order))?;
|
||||||
.insert()? as PaneId;
|
|
||||||
|
|
||||||
self.save_items(workspace_id, pane_id, &pane.children)
|
self.save_items(workspace_id, pane_id, &pane.children)
|
||||||
.context("Saving items")
|
.context("Saving items")
|
||||||
|
|
Binary file not shown.
|
@ -6,8 +6,6 @@ use std::{
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use libsqlite3_sys::*;
|
use libsqlite3_sys::*;
|
||||||
|
|
||||||
use crate::statement::Statement;
|
|
||||||
|
|
||||||
pub struct Connection {
|
pub struct Connection {
|
||||||
pub(crate) sqlite3: *mut sqlite3,
|
pub(crate) sqlite3: *mut sqlite3,
|
||||||
persistent: bool,
|
persistent: bool,
|
||||||
|
@ -60,30 +58,6 @@ impl Connection {
|
||||||
unsafe { sqlite3_last_insert_rowid(self.sqlite3) }
|
unsafe { sqlite3_last_insert_rowid(self.sqlite3) }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&self, query: impl AsRef<str>) -> Result<i64> {
|
|
||||||
self.exec(query)?;
|
|
||||||
Ok(self.last_insert_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exec(&self, query: impl AsRef<str>) -> Result<()> {
|
|
||||||
unsafe {
|
|
||||||
sqlite3_exec(
|
|
||||||
self.sqlite3,
|
|
||||||
CString::new(query.as_ref())?.as_ptr(),
|
|
||||||
None,
|
|
||||||
0 as *mut _,
|
|
||||||
0 as *mut _,
|
|
||||||
);
|
|
||||||
sqlite3_errcode(self.sqlite3);
|
|
||||||
self.last_error()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prepare<T: AsRef<str>>(&self, query: T) -> Result<Statement> {
|
|
||||||
Statement::prepare(&self, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn backup_main(&self, destination: &Connection) -> Result<()> {
|
pub fn backup_main(&self, destination: &Connection) -> Result<()> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let backup = sqlite3_backup_init(
|
let backup = sqlite3_backup_init(
|
||||||
|
@ -136,7 +110,7 @@ mod test {
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::{connection::Connection, migrations::Migration};
|
use crate::connection::Connection;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn string_round_trips() -> Result<()> {
|
fn string_round_trips() -> Result<()> {
|
||||||
|
@ -146,25 +120,19 @@ mod test {
|
||||||
CREATE TABLE text (
|
CREATE TABLE text (
|
||||||
text TEXT
|
text TEXT
|
||||||
);"})
|
);"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let text = "Some test text";
|
let text = "Some test text";
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.prepare("INSERT INTO text (text) VALUES (?);")
|
.insert_bound("INSERT INTO text (text) VALUES (?);")
|
||||||
.unwrap()
|
.unwrap()(text)
|
||||||
.with_bindings(text)
|
.unwrap();
|
||||||
.unwrap()
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&connection
|
connection.select_row("SELECT text FROM text;").unwrap()().unwrap(),
|
||||||
.prepare("SELECT text FROM text;")
|
Some(text.to_string())
|
||||||
.unwrap()
|
|
||||||
.row::<String>()
|
|
||||||
.unwrap(),
|
|
||||||
text
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -180,32 +148,26 @@ mod test {
|
||||||
integer INTEGER,
|
integer INTEGER,
|
||||||
blob BLOB
|
blob BLOB
|
||||||
);"})
|
);"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let tuple1 = ("test".to_string(), 64, vec![0, 1, 2, 4, 8, 16, 32, 64]);
|
let tuple1 = ("test".to_string(), 64, vec![0, 1, 2, 4, 8, 16, 32, 64]);
|
||||||
let tuple2 = ("test2".to_string(), 32, vec![64, 32, 16, 8, 4, 2, 1, 0]);
|
let tuple2 = ("test2".to_string(), 32, vec![64, 32, 16, 8, 4, 2, 1, 0]);
|
||||||
|
|
||||||
let mut insert = connection
|
let mut insert = connection
|
||||||
.prepare("INSERT INTO test (text, integer, blob) VALUES (?, ?, ?)")
|
.insert_bound::<(String, usize, Vec<u8>)>(
|
||||||
|
"INSERT INTO test (text, integer, blob) VALUES (?, ?, ?)",
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
insert
|
insert(tuple1.clone()).unwrap();
|
||||||
.with_bindings(tuple1.clone())
|
insert(tuple2.clone()).unwrap();
|
||||||
.unwrap()
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
insert
|
|
||||||
.with_bindings(tuple2.clone())
|
|
||||||
.unwrap()
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection
|
||||||
.prepare("SELECT * FROM test")
|
.select::<(String, usize, Vec<u8>)>("SELECT * FROM test")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.rows::<(String, usize, Vec<u8>)>()
|
.unwrap(),
|
||||||
.unwrap(),
|
|
||||||
vec![tuple1, tuple2]
|
vec![tuple1, tuple2]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -219,23 +181,20 @@ mod test {
|
||||||
t INTEGER,
|
t INTEGER,
|
||||||
f INTEGER
|
f INTEGER
|
||||||
);"})
|
);"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.prepare("INSERT INTO bools(t, f) VALUES (?, ?);")
|
.insert_bound("INSERT INTO bools(t, f) VALUES (?, ?);")
|
||||||
.unwrap()
|
.unwrap()((true, false))
|
||||||
.with_bindings((true, false))
|
.unwrap();
|
||||||
.unwrap()
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&connection
|
connection
|
||||||
.prepare("SELECT * FROM bools;")
|
.select_row::<(bool, bool)>("SELECT * FROM bools;")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.row::<(bool, bool)>()
|
.unwrap(),
|
||||||
.unwrap(),
|
Some((true, false))
|
||||||
&(true, false)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,13 +206,13 @@ mod test {
|
||||||
CREATE TABLE blobs (
|
CREATE TABLE blobs (
|
||||||
data BLOB
|
data BLOB
|
||||||
);"})
|
);"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
let blob = &[0, 1, 2, 4, 8, 16, 32, 64];
|
.unwrap();
|
||||||
let mut write = connection1
|
let blob = vec![0, 1, 2, 4, 8, 16, 32, 64];
|
||||||
.prepare("INSERT INTO blobs (data) VALUES (?);")
|
connection1
|
||||||
.unwrap();
|
.insert_bound::<Vec<u8>>("INSERT INTO blobs (data) VALUES (?);")
|
||||||
write.bind_blob(1, blob).unwrap();
|
.unwrap()(blob.clone())
|
||||||
write.exec().unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Backup connection1 to connection2
|
// Backup connection1 to connection2
|
||||||
let connection2 = Connection::open_memory("backup_works_other");
|
let connection2 = Connection::open_memory("backup_works_other");
|
||||||
|
@ -261,40 +220,36 @@ mod test {
|
||||||
|
|
||||||
// Delete the added blob and verify its deleted on the other side
|
// Delete the added blob and verify its deleted on the other side
|
||||||
let read_blobs = connection1
|
let read_blobs = connection1
|
||||||
.prepare("SELECT * FROM blobs;")
|
.select::<Vec<u8>>("SELECT * FROM blobs;")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.rows::<Vec<u8>>()
|
.unwrap();
|
||||||
.unwrap();
|
|
||||||
assert_eq!(read_blobs, vec![blob]);
|
assert_eq!(read_blobs, vec![blob]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kv_store() -> anyhow::Result<()> {
|
fn multi_step_statement_works() {
|
||||||
let connection = Connection::open_memory("kv_store");
|
let connection = Connection::open_memory("multi_step_statement_works");
|
||||||
|
|
||||||
Migration::new(
|
connection
|
||||||
"kv",
|
.exec(indoc! {"
|
||||||
&["CREATE TABLE kv_store(
|
CREATE TABLE test (
|
||||||
key TEXT PRIMARY KEY,
|
col INTEGER
|
||||||
value TEXT NOT NULL
|
)"})
|
||||||
) STRICT;"],
|
.unwrap()()
|
||||||
)
|
|
||||||
.run(&connection)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut stmt = connection.prepare("INSERT INTO kv_store(key, value) VALUES(?, ?)")?;
|
connection
|
||||||
stmt.bind_text(1, "a").unwrap();
|
.exec(indoc! {"
|
||||||
stmt.bind_text(2, "b").unwrap();
|
INSERT INTO test(col) VALUES (2)"})
|
||||||
stmt.exec().unwrap();
|
.unwrap()()
|
||||||
let id = connection.last_insert_id();
|
.unwrap();
|
||||||
|
|
||||||
let res = connection
|
assert_eq!(
|
||||||
.prepare("SELECT key, value FROM kv_store WHERE rowid = ?")?
|
connection
|
||||||
.with_bindings(id)?
|
.select_row::<usize>("SELECt * FROM test")
|
||||||
.row::<(String, String)>()?;
|
.unwrap()()
|
||||||
|
.unwrap(),
|
||||||
assert_eq!(res, ("a".to_string(), "b".to_string()));
|
Some(2)
|
||||||
|
);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@ pub mod migrations;
|
||||||
pub mod savepoint;
|
pub mod savepoint;
|
||||||
pub mod statement;
|
pub mod statement;
|
||||||
pub mod thread_safe_connection;
|
pub mod thread_safe_connection;
|
||||||
|
pub mod typed_statements;
|
||||||
|
|
|
@ -18,7 +18,7 @@ const MIGRATIONS_MIGRATION: Migration = Migration::new(
|
||||||
domain TEXT,
|
domain TEXT,
|
||||||
step INTEGER,
|
step INTEGER,
|
||||||
migration TEXT
|
migration TEXT
|
||||||
);
|
)
|
||||||
"}],
|
"}],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -34,24 +34,26 @@ impl Migration {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_unchecked(&self, connection: &Connection) -> Result<()> {
|
fn run_unchecked(&self, connection: &Connection) -> Result<()> {
|
||||||
connection.exec(self.migrations.join(";\n"))
|
for migration in self.migrations {
|
||||||
|
connection.exec(migration)?()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self, connection: &Connection) -> Result<()> {
|
pub fn run(&self, connection: &Connection) -> Result<()> {
|
||||||
// Setup the migrations table unconditionally
|
// Setup the migrations table unconditionally
|
||||||
MIGRATIONS_MIGRATION.run_unchecked(connection)?;
|
MIGRATIONS_MIGRATION.run_unchecked(connection)?;
|
||||||
|
|
||||||
let completed_migrations = connection
|
let completed_migrations =
|
||||||
.prepare(indoc! {"
|
connection.select_bound::<&str, (String, usize, String)>(indoc! {"
|
||||||
SELECT domain, step, migration FROM migrations
|
SELECT domain, step, migration FROM migrations
|
||||||
WHERE domain = ?
|
WHERE domain = ?
|
||||||
ORDER BY step
|
ORDER BY step
|
||||||
"})?
|
"})?(self.domain)?;
|
||||||
.with_bindings(self.domain)?
|
|
||||||
.rows::<(String, usize, String)>()?;
|
|
||||||
|
|
||||||
let mut store_completed_migration = connection
|
let mut store_completed_migration = connection
|
||||||
.prepare("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
|
.insert_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
|
||||||
|
|
||||||
for (index, migration) in self.migrations.iter().enumerate() {
|
for (index, migration) in self.migrations.iter().enumerate() {
|
||||||
if let Some((_, _, completed_migration)) = completed_migrations.get(index) {
|
if let Some((_, _, completed_migration)) = completed_migrations.get(index) {
|
||||||
|
@ -70,10 +72,8 @@ impl Migration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.exec(migration)?;
|
connection.exec(migration)?()?;
|
||||||
store_completed_migration
|
store_completed_migration((self.domain, index, *migration))?;
|
||||||
.with_bindings((self.domain, index, *migration))?
|
|
||||||
.exec()?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -97,17 +97,16 @@ mod test {
|
||||||
CREATE TABLE test1 (
|
CREATE TABLE test1 (
|
||||||
a TEXT,
|
a TEXT,
|
||||||
b TEXT
|
b TEXT
|
||||||
);"}],
|
)"}],
|
||||||
);
|
);
|
||||||
migration.run(&connection).unwrap();
|
migration.run(&connection).unwrap();
|
||||||
|
|
||||||
// Verify it got added to the migrations table
|
// Verify it got added to the migrations table
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&connection
|
&connection
|
||||||
.prepare("SELECT (migration) FROM migrations")
|
.select::<String>("SELECT (migration) FROM migrations")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.rows::<String>()
|
.unwrap()[..],
|
||||||
.unwrap()[..],
|
|
||||||
migration.migrations
|
migration.migrations
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -117,22 +116,21 @@ mod test {
|
||||||
CREATE TABLE test1 (
|
CREATE TABLE test1 (
|
||||||
a TEXT,
|
a TEXT,
|
||||||
b TEXT
|
b TEXT
|
||||||
);"},
|
)"},
|
||||||
indoc! {"
|
indoc! {"
|
||||||
CREATE TABLE test2 (
|
CREATE TABLE test2 (
|
||||||
c TEXT,
|
c TEXT,
|
||||||
d TEXT
|
d TEXT
|
||||||
);"},
|
)"},
|
||||||
];
|
];
|
||||||
migration.run(&connection).unwrap();
|
migration.run(&connection).unwrap();
|
||||||
|
|
||||||
// Verify it is also added to the migrations table
|
// Verify it is also added to the migrations table
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&connection
|
&connection
|
||||||
.prepare("SELECT (migration) FROM migrations")
|
.select::<String>("SELECT (migration) FROM migrations")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.rows::<String>()
|
.unwrap()[..],
|
||||||
.unwrap()[..],
|
|
||||||
migration.migrations
|
migration.migrations
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -142,15 +140,17 @@ mod test {
|
||||||
let connection = Connection::open_memory("migration_setup_works");
|
let connection = Connection::open_memory("migration_setup_works");
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.exec(indoc! {"CREATE TABLE IF NOT EXISTS migrations (
|
.exec(indoc! {"
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
domain TEXT,
|
domain TEXT,
|
||||||
step INTEGER,
|
step INTEGER,
|
||||||
migration TEXT
|
migration TEXT
|
||||||
);"})
|
);"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut store_completed_migration = connection
|
let mut store_completed_migration = connection
|
||||||
.prepare(indoc! {"
|
.insert_bound::<(&str, usize, String)>(indoc! {"
|
||||||
INSERT INTO migrations (domain, step, migration)
|
INSERT INTO migrations (domain, step, migration)
|
||||||
VALUES (?, ?, ?)"})
|
VALUES (?, ?, ?)"})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -159,14 +159,11 @@ mod test {
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
// Create a table forcing a schema change
|
// Create a table forcing a schema change
|
||||||
connection
|
connection
|
||||||
.exec(format!("CREATE TABLE table{} ( test TEXT );", i))
|
.exec(&format!("CREATE TABLE table{} ( test TEXT );", i))
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
store_completed_migration
|
store_completed_migration((domain, i, i.to_string())).unwrap();
|
||||||
.with_bindings((domain, i, i.to_string()))
|
|
||||||
.unwrap()
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,46 +177,49 @@ mod test {
|
||||||
// Manually create the table for that migration with a row
|
// Manually create the table for that migration with a row
|
||||||
connection
|
connection
|
||||||
.exec(indoc! {"
|
.exec(indoc! {"
|
||||||
CREATE TABLE test_table (
|
CREATE TABLE test_table (
|
||||||
test_column INTEGER
|
test_column INTEGER
|
||||||
);
|
);"})
|
||||||
INSERT INTO test_table (test_column) VALUES (1)"})
|
.unwrap()()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
connection
|
||||||
|
.exec(indoc! {"
|
||||||
|
INSERT INTO test_table (test_column) VALUES (1);"})
|
||||||
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection
|
||||||
.prepare("SELECT * FROM test_table")
|
.select_row::<usize>("SELECT * FROM test_table")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.row::<usize>()
|
.unwrap(),
|
||||||
.unwrap(),
|
Some(1)
|
||||||
1
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run the migration verifying that the row got dropped
|
// Run the migration verifying that the row got dropped
|
||||||
migration.run(&connection).unwrap();
|
migration.run(&connection).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection
|
||||||
.prepare("SELECT * FROM test_table")
|
.select_row::<usize>("SELECT * FROM test_table")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.rows::<usize>()
|
.unwrap(),
|
||||||
.unwrap(),
|
None
|
||||||
Vec::new()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Recreate the dropped row
|
// Recreate the dropped row
|
||||||
connection
|
connection
|
||||||
.exec("INSERT INTO test_table (test_column) VALUES (2)")
|
.exec("INSERT INTO test_table (test_column) VALUES (2)")
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Run the same migration again and verify that the table was left unchanged
|
// Run the same migration again and verify that the table was left unchanged
|
||||||
migration.run(&connection).unwrap();
|
migration.run(&connection).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection
|
||||||
.prepare("SELECT * FROM test_table")
|
.select_row::<usize>("SELECT * FROM test_table")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.row::<usize>()
|
.unwrap(),
|
||||||
.unwrap(),
|
Some(2)
|
||||||
2
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use indoc::{formatdoc, indoc};
|
||||||
|
|
||||||
use crate::connection::Connection;
|
use crate::connection::Connection;
|
||||||
|
|
||||||
|
@ -10,16 +11,17 @@ impl Connection {
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Result<R>,
|
F: FnOnce() -> Result<R>,
|
||||||
{
|
{
|
||||||
let name = name.as_ref().to_owned();
|
let name = name.as_ref();
|
||||||
self.exec(format!("SAVEPOINT {}", &name))?;
|
self.exec(&format!("SAVEPOINT {name}"))?()?;
|
||||||
let result = f();
|
let result = f();
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.exec(format!("RELEASE {}", name))?;
|
self.exec(&format!("RELEASE {name}"))?()?;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
self.exec(format!("ROLLBACK TO {}", name))?;
|
self.exec(&formatdoc! {"
|
||||||
self.exec(format!("RELEASE {}", name))?;
|
ROLLBACK TO {name};
|
||||||
|
RELEASE {name}"})?()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
|
@ -32,16 +34,17 @@ impl Connection {
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Result<Option<R>>,
|
F: FnOnce() -> Result<Option<R>>,
|
||||||
{
|
{
|
||||||
let name = name.as_ref().to_owned();
|
let name = name.as_ref();
|
||||||
self.exec(format!("SAVEPOINT {}", &name))?;
|
self.exec(&format!("SAVEPOINT {name}"))?()?;
|
||||||
let result = f();
|
let result = f();
|
||||||
match result {
|
match result {
|
||||||
Ok(Some(_)) => {
|
Ok(Some(_)) => {
|
||||||
self.exec(format!("RELEASE {}", name))?;
|
self.exec(&format!("RELEASE {name}"))?()?;
|
||||||
}
|
}
|
||||||
Ok(None) | Err(_) => {
|
Ok(None) | Err(_) => {
|
||||||
self.exec(format!("ROLLBACK TO {}", name))?;
|
self.exec(&formatdoc! {"
|
||||||
self.exec(format!("RELEASE {}", name))?;
|
ROLLBACK TO {name};
|
||||||
|
RELEASE {name}"})?()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
|
@ -64,28 +67,25 @@ mod tests {
|
||||||
text TEXT,
|
text TEXT,
|
||||||
idx INTEGER
|
idx INTEGER
|
||||||
);"})
|
);"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let save1_text = "test save1";
|
let save1_text = "test save1";
|
||||||
let save2_text = "test save2";
|
let save2_text = "test save2";
|
||||||
|
|
||||||
connection.with_savepoint("first", || {
|
connection.with_savepoint("first", || {
|
||||||
connection
|
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((save1_text, 1))?;
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
|
||||||
.with_bindings((save1_text, 1))?
|
|
||||||
.exec()?;
|
|
||||||
|
|
||||||
assert!(connection
|
assert!(connection
|
||||||
.with_savepoint("second", || -> Result<Option<()>, anyhow::Error> {
|
.with_savepoint("second", || -> Result<Option<()>, anyhow::Error> {
|
||||||
connection
|
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
save2_text, 2,
|
||||||
.with_bindings((save2_text, 2))?
|
))?;
|
||||||
.exec()?;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?(
|
||||||
.rows::<String>()?,
|
)?,
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -95,22 +95,17 @@ mod tests {
|
||||||
.is_some());
|
.is_some());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
|
||||||
.rows::<String>()?,
|
|
||||||
vec![save1_text],
|
vec![save1_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
connection.with_savepoint_rollback::<(), _>("second", || {
|
connection.with_savepoint_rollback::<(), _>("second", || {
|
||||||
connection
|
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
save2_text, 2,
|
||||||
.with_bindings((save2_text, 2))?
|
))?;
|
||||||
.exec()?;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
|
||||||
.rows::<String>()?,
|
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -118,22 +113,17 @@ mod tests {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
|
||||||
.rows::<String>()?,
|
|
||||||
vec![save1_text],
|
vec![save1_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
connection.with_savepoint_rollback("second", || {
|
connection.with_savepoint_rollback("second", || {
|
||||||
connection
|
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
|
||||||
.prepare("INSERT INTO text(text, idx) VALUES (?, ?)")?
|
save2_text, 2,
|
||||||
.with_bindings((save2_text, 2))?
|
))?;
|
||||||
.exec()?;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
|
||||||
.rows::<String>()?,
|
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -141,9 +131,7 @@ mod tests {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
|
||||||
.rows::<String>()?,
|
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -151,9 +139,7 @@ mod tests {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
|
||||||
.prepare("SELECT text FROM text ORDER BY text.idx ASC")?
|
|
||||||
.rows::<String>()?,
|
|
||||||
vec![save1_text, save2_text],
|
vec![save1_text, save2_text],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::ffi::{c_int, CString};
|
use std::ffi::{c_int, CStr, CString};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::{slice, str};
|
use std::{ptr, slice, str};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use libsqlite3_sys::*;
|
use libsqlite3_sys::*;
|
||||||
|
@ -9,7 +9,8 @@ use crate::bindable::{Bind, Column};
|
||||||
use crate::connection::Connection;
|
use crate::connection::Connection;
|
||||||
|
|
||||||
pub struct Statement<'a> {
|
pub struct Statement<'a> {
|
||||||
raw_statement: *mut sqlite3_stmt,
|
raw_statements: Vec<*mut sqlite3_stmt>,
|
||||||
|
current_statement: usize,
|
||||||
connection: &'a Connection,
|
connection: &'a Connection,
|
||||||
phantom: PhantomData<sqlite3_stmt>,
|
phantom: PhantomData<sqlite3_stmt>,
|
||||||
}
|
}
|
||||||
|
@ -34,19 +35,31 @@ pub enum SqlType {
|
||||||
impl<'a> Statement<'a> {
|
impl<'a> Statement<'a> {
|
||||||
pub fn prepare<T: AsRef<str>>(connection: &'a Connection, query: T) -> Result<Self> {
|
pub fn prepare<T: AsRef<str>>(connection: &'a Connection, query: T) -> Result<Self> {
|
||||||
let mut statement = Self {
|
let mut statement = Self {
|
||||||
raw_statement: 0 as *mut _,
|
raw_statements: Default::default(),
|
||||||
|
current_statement: 0,
|
||||||
connection,
|
connection,
|
||||||
phantom: PhantomData,
|
phantom: PhantomData,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_prepare_v2(
|
let sql = CString::new(query.as_ref())?;
|
||||||
connection.sqlite3,
|
let mut remaining_sql = sql.as_c_str();
|
||||||
CString::new(query.as_ref())?.as_ptr(),
|
while {
|
||||||
-1,
|
let remaining_sql_str = remaining_sql.to_str()?;
|
||||||
&mut statement.raw_statement,
|
remaining_sql_str.trim() != ";" && !remaining_sql_str.is_empty()
|
||||||
0 as *mut _,
|
} {
|
||||||
);
|
let mut raw_statement = 0 as *mut sqlite3_stmt;
|
||||||
|
let mut remaining_sql_ptr = ptr::null();
|
||||||
|
sqlite3_prepare_v2(
|
||||||
|
connection.sqlite3,
|
||||||
|
remaining_sql.as_ptr(),
|
||||||
|
-1,
|
||||||
|
&mut raw_statement,
|
||||||
|
&mut remaining_sql_ptr,
|
||||||
|
);
|
||||||
|
remaining_sql = CStr::from_ptr(remaining_sql_ptr);
|
||||||
|
statement.raw_statements.push(raw_statement);
|
||||||
|
}
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.last_error()
|
.last_error()
|
||||||
|
@ -56,131 +69,138 @@ impl<'a> Statement<'a> {
|
||||||
Ok(statement)
|
Ok(statement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_statement(&self) -> *mut sqlite3_stmt {
|
||||||
|
*self.raw_statements.get(self.current_statement).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_reset(self.raw_statement);
|
for raw_statement in self.raw_statements.iter() {
|
||||||
|
sqlite3_reset(*raw_statement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.current_statement = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parameter_count(&self) -> i32 {
|
pub fn parameter_count(&self) -> i32 {
|
||||||
unsafe { sqlite3_bind_parameter_count(self.raw_statement) }
|
unsafe {
|
||||||
|
self.raw_statements
|
||||||
|
.iter()
|
||||||
|
.map(|raw_statement| sqlite3_bind_parameter_count(*raw_statement))
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_blob(&self, index: i32, blob: &[u8]) -> Result<()> {
|
pub fn bind_blob(&self, index: i32, blob: &[u8]) -> Result<()> {
|
||||||
// dbg!("bind blob", index);
|
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let blob_pointer = blob.as_ptr() as *const _;
|
let blob_pointer = blob.as_ptr() as *const _;
|
||||||
let len = blob.len() as c_int;
|
let len = blob.len() as c_int;
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_bind_blob(
|
for raw_statement in self.raw_statements.iter() {
|
||||||
self.raw_statement,
|
sqlite3_bind_blob(*raw_statement, index, blob_pointer, len, SQLITE_TRANSIENT());
|
||||||
index,
|
}
|
||||||
blob_pointer,
|
|
||||||
len,
|
|
||||||
SQLITE_TRANSIENT(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
self.connection.last_error()
|
self.connection.last_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_blob<'b>(&'b mut self, index: i32) -> Result<&'b [u8]> {
|
pub fn column_blob<'b>(&'b mut self, index: i32) -> Result<&'b [u8]> {
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let pointer = unsafe { sqlite3_column_blob(self.raw_statement, index) };
|
let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) };
|
||||||
|
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
if pointer.is_null() {
|
if pointer.is_null() {
|
||||||
return Ok(&[]);
|
return Ok(&[]);
|
||||||
}
|
}
|
||||||
let len = unsafe { sqlite3_column_bytes(self.raw_statement, index) as usize };
|
let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize };
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
unsafe { Ok(slice::from_raw_parts(pointer as *const u8, len)) }
|
unsafe { Ok(slice::from_raw_parts(pointer as *const u8, len)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_double(&self, index: i32, double: f64) -> Result<()> {
|
pub fn bind_double(&self, index: i32, double: f64) -> Result<()> {
|
||||||
// dbg!("bind double", index);
|
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_bind_double(self.raw_statement, index, double);
|
for raw_statement in self.raw_statements.iter() {
|
||||||
|
sqlite3_bind_double(*raw_statement, index, double);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.connection.last_error()
|
self.connection.last_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_double(&self, index: i32) -> Result<f64> {
|
pub fn column_double(&self, index: i32) -> Result<f64> {
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let result = unsafe { sqlite3_column_double(self.raw_statement, index) };
|
let result = unsafe { sqlite3_column_double(self.current_statement(), index) };
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_int(&self, index: i32, int: i32) -> Result<()> {
|
pub fn bind_int(&self, index: i32, int: i32) -> Result<()> {
|
||||||
// dbg!("bind int", index);
|
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_bind_int(self.raw_statement, index, int);
|
for raw_statement in self.raw_statements.iter() {
|
||||||
|
sqlite3_bind_int(*raw_statement, index, int);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
self.connection.last_error()
|
self.connection.last_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_int(&self, index: i32) -> Result<i32> {
|
pub fn column_int(&self, index: i32) -> Result<i32> {
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let result = unsafe { sqlite3_column_int(self.raw_statement, index) };
|
let result = unsafe { sqlite3_column_int(self.current_statement(), index) };
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_int64(&self, index: i32, int: i64) -> Result<()> {
|
pub fn bind_int64(&self, index: i32, int: i64) -> Result<()> {
|
||||||
// dbg!("bind int64", index);
|
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_bind_int64(self.raw_statement, index, int);
|
for raw_statement in self.raw_statements.iter() {
|
||||||
|
sqlite3_bind_int64(*raw_statement, index, int);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.connection.last_error()
|
self.connection.last_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_int64(&self, index: i32) -> Result<i64> {
|
pub fn column_int64(&self, index: i32) -> Result<i64> {
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let result = unsafe { sqlite3_column_int64(self.raw_statement, index) };
|
let result = unsafe { sqlite3_column_int64(self.current_statement(), index) };
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_null(&self, index: i32) -> Result<()> {
|
pub fn bind_null(&self, index: i32) -> Result<()> {
|
||||||
// dbg!("bind null", index);
|
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_bind_null(self.raw_statement, index);
|
for raw_statement in self.raw_statements.iter() {
|
||||||
|
sqlite3_bind_null(*raw_statement, index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.connection.last_error()
|
self.connection.last_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_text(&self, index: i32, text: &str) -> Result<()> {
|
pub fn bind_text(&self, index: i32, text: &str) -> Result<()> {
|
||||||
// dbg!("bind text", index, text);
|
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let text_pointer = text.as_ptr() as *const _;
|
let text_pointer = text.as_ptr() as *const _;
|
||||||
let len = text.len() as c_int;
|
let len = text.len() as c_int;
|
||||||
unsafe {
|
unsafe {
|
||||||
sqlite3_bind_text(
|
for raw_statement in self.raw_statements.iter() {
|
||||||
self.raw_statement,
|
sqlite3_bind_text(*raw_statement, index, text_pointer, len, SQLITE_TRANSIENT());
|
||||||
index,
|
}
|
||||||
text_pointer,
|
|
||||||
len,
|
|
||||||
SQLITE_TRANSIENT(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
self.connection.last_error()
|
self.connection.last_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_text<'b>(&'b mut self, index: i32) -> Result<&'b str> {
|
pub fn column_text<'b>(&'b mut self, index: i32) -> Result<&'b str> {
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let pointer = unsafe { sqlite3_column_text(self.raw_statement, index) };
|
let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) };
|
||||||
|
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
if pointer.is_null() {
|
if pointer.is_null() {
|
||||||
return Ok("");
|
return Ok("");
|
||||||
}
|
}
|
||||||
let len = unsafe { sqlite3_column_bytes(self.raw_statement, index) as usize };
|
let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize };
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
|
|
||||||
let slice = unsafe { slice::from_raw_parts(pointer as *const u8, len) };
|
let slice = unsafe { slice::from_raw_parts(pointer as *const u8, len) };
|
||||||
|
@ -198,7 +218,7 @@ impl<'a> Statement<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_type(&mut self, index: i32) -> Result<SqlType> {
|
pub fn column_type(&mut self, index: i32) -> Result<SqlType> {
|
||||||
let result = unsafe { sqlite3_column_type(self.raw_statement, index) }; // SELECT <FRIEND> FROM TABLE
|
let result = unsafe { sqlite3_column_type(self.current_statement(), index) };
|
||||||
self.connection.last_error()?;
|
self.connection.last_error()?;
|
||||||
match result {
|
match result {
|
||||||
SQLITE_INTEGER => Ok(SqlType::Integer),
|
SQLITE_INTEGER => Ok(SqlType::Integer),
|
||||||
|
@ -217,9 +237,16 @@ impl<'a> Statement<'a> {
|
||||||
|
|
||||||
fn step(&mut self) -> Result<StepResult> {
|
fn step(&mut self) -> Result<StepResult> {
|
||||||
unsafe {
|
unsafe {
|
||||||
match sqlite3_step(self.raw_statement) {
|
match sqlite3_step(self.current_statement()) {
|
||||||
SQLITE_ROW => Ok(StepResult::Row),
|
SQLITE_ROW => Ok(StepResult::Row),
|
||||||
SQLITE_DONE => Ok(StepResult::Done),
|
SQLITE_DONE => {
|
||||||
|
if self.current_statement >= self.raw_statements.len() - 1 {
|
||||||
|
Ok(StepResult::Done)
|
||||||
|
} else {
|
||||||
|
self.current_statement += 1;
|
||||||
|
self.step()
|
||||||
|
}
|
||||||
|
}
|
||||||
SQLITE_MISUSE => Ok(StepResult::Misuse),
|
SQLITE_MISUSE => Ok(StepResult::Misuse),
|
||||||
other => self
|
other => self
|
||||||
.connection
|
.connection
|
||||||
|
@ -311,7 +338,11 @@ impl<'a> Statement<'a> {
|
||||||
|
|
||||||
impl<'a> Drop for Statement<'a> {
|
impl<'a> Drop for Statement<'a> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe { sqlite3_finalize(self.raw_statement) };
|
unsafe {
|
||||||
|
for raw_statement in self.raw_statements.iter() {
|
||||||
|
sqlite3_finalize(*raw_statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,7 +350,10 @@ impl<'a> Drop for Statement<'a> {
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::{connection::Connection, statement::StepResult};
|
use crate::{
|
||||||
|
connection::Connection,
|
||||||
|
statement::{Statement, StepResult},
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blob_round_trips() {
|
fn blob_round_trips() {
|
||||||
|
@ -327,28 +361,28 @@ mod test {
|
||||||
connection1
|
connection1
|
||||||
.exec(indoc! {"
|
.exec(indoc! {"
|
||||||
CREATE TABLE blobs (
|
CREATE TABLE blobs (
|
||||||
data BLOB
|
data BLOB
|
||||||
);"})
|
)"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let blob = &[0, 1, 2, 4, 8, 16, 32, 64];
|
let blob = &[0, 1, 2, 4, 8, 16, 32, 64];
|
||||||
|
|
||||||
let mut write = connection1
|
let mut write =
|
||||||
.prepare("INSERT INTO blobs (data) VALUES (?);")
|
Statement::prepare(&connection1, "INSERT INTO blobs (data) VALUES (?)").unwrap();
|
||||||
.unwrap();
|
|
||||||
write.bind_blob(1, blob).unwrap();
|
write.bind_blob(1, blob).unwrap();
|
||||||
assert_eq!(write.step().unwrap(), StepResult::Done);
|
assert_eq!(write.step().unwrap(), StepResult::Done);
|
||||||
|
|
||||||
// Read the blob from the
|
// Read the blob from the
|
||||||
let connection2 = Connection::open_memory("blob_round_trips");
|
let connection2 = Connection::open_memory("blob_round_trips");
|
||||||
let mut read = connection2.prepare("SELECT * FROM blobs;").unwrap();
|
let mut read = Statement::prepare(&connection2, "SELECT * FROM blobs").unwrap();
|
||||||
assert_eq!(read.step().unwrap(), StepResult::Row);
|
assert_eq!(read.step().unwrap(), StepResult::Row);
|
||||||
assert_eq!(read.column_blob(0).unwrap(), blob);
|
assert_eq!(read.column_blob(0).unwrap(), blob);
|
||||||
assert_eq!(read.step().unwrap(), StepResult::Done);
|
assert_eq!(read.step().unwrap(), StepResult::Done);
|
||||||
|
|
||||||
// Delete the added blob and verify its deleted on the other side
|
// Delete the added blob and verify its deleted on the other side
|
||||||
connection2.exec("DELETE FROM blobs;").unwrap();
|
connection2.exec("DELETE FROM blobs").unwrap()().unwrap();
|
||||||
let mut read = connection1.prepare("SELECT * FROM blobs;").unwrap();
|
let mut read = Statement::prepare(&connection1, "SELECT * FROM blobs").unwrap();
|
||||||
assert_eq!(read.step().unwrap(), StepResult::Done);
|
assert_eq!(read.step().unwrap(), StepResult::Done);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,32 +393,25 @@ mod test {
|
||||||
.exec(indoc! {"
|
.exec(indoc! {"
|
||||||
CREATE TABLE texts (
|
CREATE TABLE texts (
|
||||||
text TEXT
|
text TEXT
|
||||||
);"})
|
)"})
|
||||||
.unwrap();
|
.unwrap()()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(connection
|
assert!(connection
|
||||||
.prepare("SELECT text FROM texts")
|
.select_row::<String>("SELECT text FROM texts")
|
||||||
.unwrap()
|
.unwrap()()
|
||||||
.maybe_row::<String>()
|
.unwrap()
|
||||||
.unwrap()
|
.is_none());
|
||||||
.is_none());
|
|
||||||
|
|
||||||
let text_to_insert = "This is a test";
|
let text_to_insert = "This is a test";
|
||||||
|
|
||||||
connection
|
connection
|
||||||
.prepare("INSERT INTO texts VALUES (?)")
|
.exec_bound("INSERT INTO texts VALUES (?)")
|
||||||
.unwrap()
|
.unwrap()(text_to_insert)
|
||||||
.with_bindings(text_to_insert)
|
.unwrap();
|
||||||
.unwrap()
|
|
||||||
.exec()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
connection
|
connection.select_row("SELECT text FROM texts").unwrap()().unwrap(),
|
||||||
.prepare("SELECT text FROM texts")
|
|
||||||
.unwrap()
|
|
||||||
.maybe_row::<String>()
|
|
||||||
.unwrap(),
|
|
||||||
Some(text_to_insert.to_string())
|
Some(text_to_insert.to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,8 @@ impl Deref for ThreadSafeConnection {
|
||||||
connection.exec(initialize_query).expect(&format!(
|
connection.exec(initialize_query).expect(&format!(
|
||||||
"Initialize query failed to execute: {}",
|
"Initialize query failed to execute: {}",
|
||||||
initialize_query
|
initialize_query
|
||||||
));
|
))()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(migrations) = self.migrations {
|
if let Some(migrations) = self.migrations {
|
||||||
|
|
67
crates/sqlez/src/typed_statements.rs
Normal file
67
crates/sqlez/src/typed_statements.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bindable::{Bind, Column},
|
||||||
|
connection::Connection,
|
||||||
|
statement::Statement,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
pub fn exec<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<()>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move || statement.exec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec_bound<'a, B: Bind>(
|
||||||
|
&'a self,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move |bindings| statement.with_bindings(bindings)?.exec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<i64>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move || statement.insert())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_bound<'a, B: Bind>(
|
||||||
|
&'a self,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<impl 'a + FnMut(B) -> Result<i64>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move |bindings| statement.with_bindings(bindings)?.insert())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select<'a, C: Column>(
|
||||||
|
&'a self,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<impl 'a + FnMut() -> Result<Vec<C>>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move || statement.rows::<C>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_bound<'a, B: Bind, C: Column>(
|
||||||
|
&'a self,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_row<'a, C: Column>(
|
||||||
|
&'a self,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<impl 'a + FnMut() -> Result<Option<C>>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move || statement.maybe_row::<C>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_row_bound<'a, B: Bind, C: Column>(
|
||||||
|
&'a self,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<impl 'a + FnMut(B) -> Result<Option<C>>> {
|
||||||
|
let mut statement = Statement::prepare(&self, query)?;
|
||||||
|
Ok(move |bindings| statement.with_bindings(bindings)?.maybe_row::<C>())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue