From dd9d20be25094aeff536f6149eea03566618e6eb Mon Sep 17 00:00:00 2001 From: Kay Simmons Date: Mon, 28 Nov 2022 17:42:18 -0800 Subject: [PATCH] Added sql! proc macro which checks syntax errors on sql code and displays them with reasonable underline locations Co-Authored-By: Mikayla Maki --- Cargo.lock | 12 ++ Cargo.toml | 2 + crates/db/Cargo.toml | 1 + crates/db/src/db.rs | 163 +++++++++++++-------- crates/db/src/kvp.rs | 21 ++- crates/editor/src/persistence.rs | 21 +-- crates/gpui_macros/Cargo.toml | 1 + crates/sqlez/src/connection.rs | 63 +++++++- crates/sqlez/src/domain.rs | 6 + crates/sqlez/src/statement.rs | 73 --------- crates/sqlez/src/thread_safe_connection.rs | 2 +- crates/sqlez_macros/Cargo.toml | 16 ++ crates/sqlez_macros/src/sqlez_macros.rs | 78 ++++++++++ crates/terminal/src/persistence.rs | 28 ++-- crates/workspace/src/persistence.rs | 66 ++++----- 15 files changed, 342 insertions(+), 211 deletions(-) create mode 100644 crates/sqlez_macros/Cargo.toml create mode 100644 crates/sqlez_macros/src/sqlez_macros.rs diff --git a/Cargo.lock b/Cargo.lock index 150149c529..9e3181575f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1570,6 +1570,7 @@ dependencies = [ "parking_lot 0.11.2", "serde", "sqlez", + "sqlez_macros", "tempdir", "util", ] @@ -5598,6 +5599,17 @@ dependencies = [ "thread_local", ] +[[package]] +name = "sqlez_macros" +version = "0.1.0" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "sqlez", + "syn", +] + [[package]] name = "sqlformat" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index a97f272e47..c4f54d6a90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ members = [ "crates/search", "crates/settings", "crates/snippet", + "crates/sqlez", + "crates/sqlez_macros", "crates/sum_tree", "crates/terminal", "crates/text", diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml index 70721c310c..2d88d4ece5 100644 --- a/crates/db/Cargo.toml +++ b/crates/db/Cargo.toml @@ -14,6 +14,7 @@ test-support = [] collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } +sqlez_macros = { path = "../sqlez_macros" } util = { path = "../util" } anyhow = "1.0.57" indoc = "1.0.4" diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 1da51ef867..adf6f5c035 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -5,6 +5,7 @@ pub use anyhow; pub use indoc::indoc; pub use lazy_static; pub use sqlez; +pub use sqlez_macros; use sqlez::domain::Migrator; use sqlez::thread_safe_connection::ThreadSafeConnection; @@ -76,273 +77,315 @@ macro_rules! connection { #[macro_export] macro_rules! query { - ($vis:vis fn $id:ident() -> Result<()> { $sql:expr }) => { + ($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => { $vis fn $id(&self) -> $crate::anyhow::Result<()> { use $crate::anyhow::Context; - self.exec($sql)?().context(::std::format!( + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.exec(sql_stmt)?().context(::std::format!( "Error in {}, exec failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt, )) } }; - ($vis:vis async fn $id:ident() -> Result<()> { $sql:expr }) => { + ($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => { $vis async fn $id(&self) -> $crate::anyhow::Result<()> { use $crate::anyhow::Context; + self.write(|connection| { - connection.exec($sql)?().context(::std::format!( + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.exec(sql_stmt)?().context(::std::format!( "Error in {}, exec failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $sql:expr }) => { + ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => { $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> { use $crate::anyhow::Context; - self.exec_bound::<($($arg_type),+)>($sql)?(($($arg),+)) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, exec_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $sql:expr }) => { + ($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => { $vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> { use $crate::anyhow::Context; + self.write(move |connection| { - connection.exec_bound::<$arg_type>($sql)?($arg) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.exec_bound::<$arg_type>(sql_stmt)?($arg) .context(::std::format!( "Error in {}, exec_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $sql:expr }) => { + ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => { $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> { use $crate::anyhow::Context; self.write(move |connection| { - connection.exec_bound::<($($arg_type),+)>($sql)?(($($arg),+)) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, exec_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis fn $id:ident() -> Result> { $sql:expr }) => { + ($vis:vis fn $id:ident() -> Result> { $($sql:tt)+ }) => { $vis fn $id(&self) -> $crate::anyhow::Result> { use $crate::anyhow::Context; - self.select::<$return_type>($sql)?(()) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.select::<$return_type>(sql_stmt)?(()) .context(::std::format!( "Error in {}, select_row failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis async fn $id:ident() -> Result> { $sql:expr }) => { + ($vis:vis async fn $id:ident() -> Result> { $($sql:tt)+ }) => { pub async fn $id(&self) -> $crate::anyhow::Result> { use $crate::anyhow::Context; self.write(|connection| { - connection.select::<$return_type>($sql)?(()) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.select::<$return_type>(sql_stmt)?(()) .context(::std::format!( "Error in {}, select_row failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $sql:expr }) => { + ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $($sql:tt)+ }) => { $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { use $crate::anyhow::Context; - self.select_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, exec_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $sql:expr }) => { + ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $($sql:tt)+ }) => { $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { use $crate::anyhow::Context; self.write(|connection| { - connection.select_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, exec_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis fn $id:ident() -> Result> { $sql:expr }) => { + ($vis:vis fn $id:ident() -> Result> { $($sql:tt)+ }) => { $vis fn $id(&self) -> $crate::anyhow::Result> { use $crate::anyhow::Context; - self.select_row::<$return_type>($sql)?() + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.select_row::<$return_type>(sql_stmt)?() .context(::std::format!( "Error in {}, select_row failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis async fn $id:ident() -> Result> { $sql:expr }) => { + ($vis:vis async fn $id:ident() -> Result> { $($sql:tt)+ }) => { $vis async fn $id(&self) -> $crate::anyhow::Result> { use $crate::anyhow::Context; self.write(|connection| { - connection.select_row::<$return_type>($sql)?() + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.select_row::<$return_type>(sql_stmt)?() .context(::std::format!( "Error in {}, select_row failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result> { $sql:expr }) => { + ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result> { $($sql:tt)+ }) => { $vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result> { use $crate::anyhow::Context; - self.select_row_bound::<$arg_type, $return_type>($sql)?($arg) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $sql:expr }) => { + ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $($sql:tt)+ }) => { $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { use $crate::anyhow::Context; - self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $sql:expr }) => { + ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result> { $($sql:tt)+ }) => { $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { use $crate::anyhow::Context; + self.write(|connection| { + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+)) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis fn $id:ident() -> Result<$return_type:ty> { $sql:expr }) => { + ($vis:vis fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => { $vis fn $id(&self) -> $crate::anyhow::Result<$return_type> { use $crate::anyhow::Context; + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + self.select_row::<$return_type>(indoc! { $sql })?() .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt ))? .context(::std::format!( "Error in {}, select_row_bound expected single row result but found none for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $sql:expr }) => { + ($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => { $vis async fn $id(&self) -> $crate::anyhow::Result<$return_type> { use $crate::anyhow::Context; self.write(|connection| { - connection.select_row::<$return_type>($sql)?() + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.select_row::<$return_type>(sql_stmt)?() .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt ))? .context(::std::format!( "Error in {}, select_row_bound expected single row result but found none for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } }; - ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $sql:expr }) => { + ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $($sql:tt)+ }) => { pub fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<$return_type> { use $crate::anyhow::Context; - self.select_row_bound::<$arg_type, $return_type>($sql)?($arg) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt ))? .context(::std::format!( "Error in {}, select_row_bound expected single row result but found none for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $sql:expr }) => { + ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => { $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> { use $crate::anyhow::Context; - self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt ))? .context(::std::format!( "Error in {}, select_row_bound expected single row result but found none for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) } }; - ($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $sql:expr }) => { + ($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => { $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> { use $crate::anyhow::Context; + self.write(|connection| { - connection.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) + let sql_stmt = $crate::sqlez_macros::sql!($($sql)+); + + connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+)) .context(::std::format!( "Error in {}, select_row_bound failed to execute or parse for: {}", ::std::stringify!($id), - $sql, + sql_stmt ))? .context(::std::format!( "Error in {}, select_row_bound expected single row result but found none for: {}", ::std::stringify!($id), - $sql, + sql_stmt )) }).await } diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 1763ed964c..b3f2a716cb 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -1,6 +1,5 @@ -use indoc::indoc; - use sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}; +use sqlez_macros::sql; use crate::{open_file_db, open_memory_db, query}; @@ -28,31 +27,31 @@ impl Domain for KeyValueStore { } fn migrations() -> &'static [&'static str] { - &[indoc! {" - CREATE TABLE kv_store( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ) STRICT; - "}] + &[sql!( + CREATE TABLE kv_store( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) STRICT; + )] } } impl KeyValueStore { query! { pub fn read_kvp(key: &str) -> Result> { - "SELECT value FROM kv_store WHERE key = (?)" + SELECT value FROM kv_store WHERE key = (?) } } query! { pub async fn write_kvp(key: String, value: String) -> Result<()> { - "INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))" + INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?)) } } query! { pub async fn delete_kvp(key: String) -> Result<()> { - "DELETE FROM kv_store WHERE key = (?)" + DELETE FROM kv_store WHERE key = (?) } } } diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 548be88c80..22b0f158c1 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,12 +1,11 @@ use std::path::PathBuf; +use crate::Editor; +use db::sqlez_macros::sql; use db::{connection, query}; -use indoc::indoc; use sqlez::domain::Domain; use workspace::{ItemId, Workspace, WorkspaceId}; -use crate::Editor; - connection!(DB: EditorDb<(Workspace, Editor)>); impl Domain for Editor { @@ -15,7 +14,7 @@ impl Domain for Editor { } fn migrations() -> &'static [&'static str] { - &[indoc! {" + &[sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, workspace_id INTEGER NOT NULL, @@ -26,26 +25,22 @@ impl Domain for Editor { ON UPDATE CASCADE ) STRICT; - "}] + )] } } impl EditorDb { query! { pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result { - indoc!{" - SELECT path FROM editors - WHERE item_id = ? AND workspace_id = ? - "} + SELECT path FROM editors + WHERE item_id = ? AND workspace_id = ? } } query! { pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> { - indoc!{" - INSERT OR REPLACE INTO editors(item_id, workspace_id, path) - VALUES (?, ?, ?) - "} + INSERT OR REPLACE INTO editors(item_id, workspace_id, path) + VALUES (?, ?, ?) } } } diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index d8fc0521cc..e35e0b1d2b 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -12,3 +12,4 @@ doctest = false syn = "1.0" quote = "1.0" proc-macro2 = "1.0" + diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs index 4beddb4fed..6d859be23f 100644 --- a/crates/sqlez/src/connection.rs +++ b/crates/sqlez/src/connection.rs @@ -2,6 +2,7 @@ use std::{ ffi::{CStr, CString}, marker::PhantomData, path::Path, + ptr, }; use anyhow::{anyhow, Result}; @@ -85,6 +86,45 @@ impl Connection { self.backup_main(&destination) } + pub fn sql_has_syntax_error(&self, sql: &str) -> Option<(String, usize)> { + let sql = CString::new(sql).unwrap(); + let mut remaining_sql = sql.as_c_str(); + let sql_start = remaining_sql.as_ptr(); + + unsafe { + while { + let remaining_sql_str = remaining_sql.to_str().unwrap().trim(); + remaining_sql_str != ";" && !remaining_sql_str.is_empty() + } { + let mut raw_statement = 0 as *mut sqlite3_stmt; + let mut remaining_sql_ptr = ptr::null(); + sqlite3_prepare_v2( + self.sqlite3, + remaining_sql.as_ptr(), + -1, + &mut raw_statement, + &mut remaining_sql_ptr, + ); + + let res = sqlite3_errcode(self.sqlite3); + let offset = sqlite3_error_offset(self.sqlite3); + + if res == 1 && offset >= 0 { + let message = sqlite3_errmsg(self.sqlite3); + let err_msg = + String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes()) + .into_owned(); + let sub_statement_correction = + remaining_sql.as_ptr() as usize - sql_start as usize; + + return Some((err_msg, offset as usize + sub_statement_correction)); + } + remaining_sql = CStr::from_ptr(remaining_sql_ptr); + } + } + None + } + pub(crate) fn last_error(&self) -> Result<()> { unsafe { let code = sqlite3_errcode(self.sqlite3); @@ -259,10 +299,31 @@ mod test { assert_eq!( connection - .select_row::("SELECt * FROM test") + .select_row::("SELECT * FROM test") .unwrap()() .unwrap(), Some(2) ); } + + #[test] + fn test_sql_has_syntax_errors() { + let connection = Connection::open_memory(Some("test_sql_has_syntax_errors")); + let first_stmt = + "CREATE TABLE kv_store(key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT ;"; + let second_stmt = "SELECT FROM"; + + let second_offset = connection.sql_has_syntax_error(second_stmt).unwrap().1; + + let res = connection + .sql_has_syntax_error(&format!("{}\n{}", first_stmt, second_stmt)) + .map(|(_, offset)| offset); + + assert_eq!( + res, + Some(first_stmt.len() + second_offset + 1) // TODO: This value is wrong! + ); + + panic!("{:?}", res) + } } diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs index b7cfbaef88..3a477b2bc9 100644 --- a/crates/sqlez/src/domain.rs +++ b/crates/sqlez/src/domain.rs @@ -9,6 +9,12 @@ pub trait Migrator { fn migrate(connection: &Connection) -> anyhow::Result<()>; } +impl Migrator for () { + fn migrate(_connection: &Connection) -> anyhow::Result<()> { + Ok(()) // Do nothing + } +} + impl Migrator for D { fn migrate(connection: &Connection) -> anyhow::Result<()> { connection.migrate(Self::name(), Self::migrations()) diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index f3970827f8..86035f5d0a 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -489,76 +489,3 @@ mod test { ); } } - -mod syntax_check { - use std::{ - ffi::{CStr, CString}, - ptr, - }; - - use libsqlite3_sys::{ - sqlite3_close, sqlite3_errmsg, sqlite3_error_offset, sqlite3_extended_errcode, - sqlite3_extended_result_codes, sqlite3_finalize, sqlite3_open_v2, sqlite3_prepare_v2, - sqlite3_stmt, SQLITE_OPEN_CREATE, SQLITE_OPEN_NOMUTEX, SQLITE_OPEN_READWRITE, - }; - - fn syntax_errors(sql: &str) -> Option<(String, i32)> { - let mut sqlite3 = 0 as *mut _; - let mut raw_statement = 0 as *mut sqlite3_stmt; - - let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE; - unsafe { - let memory_str = CString::new(":memory:").unwrap(); - sqlite3_open_v2(memory_str.as_ptr(), &mut sqlite3, flags, 0 as *const _); - - let sql = CString::new(sql).unwrap(); - - // Turn on extended error codes - sqlite3_extended_result_codes(sqlite3, 1); - - sqlite3_prepare_v2( - sqlite3, - sql.as_c_str().as_ptr(), - -1, - &mut raw_statement, - &mut ptr::null(), - ); - - let res = sqlite3_extended_errcode(sqlite3); - let offset = sqlite3_error_offset(sqlite3); - - if res == 1 && offset != -1 { - let message = sqlite3_errmsg(sqlite3); - let err_msg = - String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes()) - .into_owned(); - - sqlite3_finalize(*&mut raw_statement); - sqlite3_close(sqlite3); - - return Some((err_msg, offset)); - } else { - sqlite3_finalize(*&mut raw_statement); - sqlite3_close(sqlite3); - - None - } - } - } - - #[cfg(test)] - mod test { - use super::syntax_errors; - - #[test] - fn test_check_syntax() { - assert!(syntax_errors("SELECT FROM").is_some()); - - assert!(syntax_errors("SELECT col FROM table_t;").is_none()); - - assert!(syntax_errors("CREATE TABLE t(col TEXT,) STRICT;").is_some()); - - assert!(syntax_errors("CREATE TABLE t(col TEXT) STRICT;").is_none()); - } - } -} diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 5402c6b5e1..88199ff0c8 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -17,7 +17,7 @@ lazy_static! { Default::default(); } -pub struct ThreadSafeConnection { +pub struct ThreadSafeConnection { uri: Arc, persistent: bool, initialize_query: Option<&'static str>, diff --git a/crates/sqlez_macros/Cargo.toml b/crates/sqlez_macros/Cargo.toml new file mode 100644 index 0000000000..413a3d30f5 --- /dev/null +++ b/crates/sqlez_macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sqlez_macros" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/sqlez_macros.rs" +proc-macro = true +doctest = false + +[dependencies] +syn = "1.0" +quote = "1.0" +proc-macro2 = "1.0" +lazy_static = "1.4" +sqlez = { path = "../sqlez" } \ No newline at end of file diff --git a/crates/sqlez_macros/src/sqlez_macros.rs b/crates/sqlez_macros/src/sqlez_macros.rs new file mode 100644 index 0000000000..25249b89b6 --- /dev/null +++ b/crates/sqlez_macros/src/sqlez_macros.rs @@ -0,0 +1,78 @@ +use proc_macro::{Delimiter, Span, TokenStream, TokenTree}; +use sqlez::thread_safe_connection::ThreadSafeConnection; +use syn::Error; + +lazy_static::lazy_static! { + static ref SQLITE: ThreadSafeConnection = ThreadSafeConnection::new(":memory:", false); +} + +#[proc_macro] +pub fn sql(tokens: TokenStream) -> TokenStream { + let mut sql_tokens = vec![]; + flatten_stream(tokens.clone(), &mut sql_tokens); + + // Lookup of spans by offset at the end of the token + let mut spans: Vec<(usize, Span)> = Vec::new(); + let mut sql = String::new(); + for (token_text, span) in sql_tokens { + sql.push_str(&token_text); + spans.push((sql.len(), span)); + } + + let error = SQLITE.sql_has_syntax_error(sql.trim()); + + if let Some((error, error_offset)) = error { + let error_span = spans + .into_iter() + .skip_while(|(offset, _)| offset <= &error_offset) + .map(|(_, span)| span) + .next() + .unwrap_or(Span::call_site()); + + let error_text = format!("Sql Error: {}\nFor Query: {}", error, sql); + TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error()) + } else { + format!("r#\"{}\"#", &sql).parse().unwrap() + } +} + +/// This method exists to normalize the representation of groups +/// to always include spaces between tokens. This is why we don't use the usual .to_string(). +/// This allows our token search in token_at_offset to resolve +/// ambiguity of '(tokens)' vs. '( token )', due to sqlite requiring byte offsets +fn flatten_stream(tokens: TokenStream, result: &mut Vec<(String, Span)>) { + for token_tree in tokens.into_iter() { + match token_tree { + TokenTree::Group(group) => { + // push open delimiter + result.push((open_delimiter(group.delimiter()), group.span())); + // recurse + flatten_stream(group.stream(), result); + // push close delimiter + result.push((close_delimiter(group.delimiter()), group.span())); + } + TokenTree::Ident(ident) => { + result.push((format!("{} ", ident.to_string()), ident.span())); + } + leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())), + } + } +} + +fn open_delimiter(delimiter: Delimiter) -> String { + match delimiter { + Delimiter::Parenthesis => "(".to_string(), + Delimiter::Brace => "[".to_string(), + Delimiter::Bracket => "{".to_string(), + Delimiter::None => "".to_string(), + } +} + +fn close_delimiter(delimiter: Delimiter) -> String { + match delimiter { + Delimiter::Parenthesis => ")".to_string(), + Delimiter::Brace => "]".to_string(), + Delimiter::Bracket => "}".to_string(), + Delimiter::None => "".to_string(), + } +} diff --git a/crates/terminal/src/persistence.rs b/crates/terminal/src/persistence.rs index 5fb7758bec..f9cfb6fc01 100644 --- a/crates/terminal/src/persistence.rs +++ b/crates/terminal/src/persistence.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use db::{connection, indoc, query, sqlez::domain::Domain}; +use db::{connection, query, sqlez::domain::Domain, sqlez_macros::sql}; use workspace::{ItemId, Workspace, WorkspaceId}; @@ -14,7 +14,7 @@ impl Domain for Terminal { } fn migrations() -> &'static [&'static str] { - &[indoc! {" + &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -23,7 +23,7 @@ impl Domain for Terminal { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - "}] + )] } } @@ -34,11 +34,9 @@ impl TerminalDb { old_id: WorkspaceId, item_id: ItemId ) -> Result<()> { - indoc!{" - UPDATE terminals - SET workspace_id = ? - WHERE workspace_id = ? AND item_id = ? - "} + UPDATE terminals + SET workspace_id = ? + WHERE workspace_id = ? AND item_id = ? } } @@ -48,20 +46,16 @@ impl TerminalDb { workspace_id: WorkspaceId, working_directory: PathBuf ) -> Result<()> { - indoc!{" - INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory) - VALUES (?1, ?2, ?3) - "} + INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory) + VALUES (?, ?, ?) } } query! { pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - indoc!{" - SELECT working_directory - FROM terminals - WHERE item_id = ? AND workspace_id = ? - "} + SELECT working_directory + FROM terminals + WHERE item_id = ? AND workspace_id = ? } } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 17b0aad13f..0d35c19d5d 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -5,7 +5,7 @@ pub mod model; use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; -use db::{connection, query, sqlez::connection::Connection}; +use db::{connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::Axis; use indoc::indoc; @@ -30,49 +30,49 @@ impl Domain for Workspace { } fn migrations() -> &'static [&'static str] { - &[indoc! {" + &[sql!( CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, workspace_location BLOB UNIQUE, - dock_visible INTEGER, -- Boolean - dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded' - dock_pane INTEGER, -- NULL indicates that we don't have a dock pane yet + dock_visible INTEGER, // Boolean + dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded' + dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) ) STRICT; - + CREATE TABLE pane_groups( group_id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, -- NULL indicates that this is a root node - position INTEGER, -- NULL indicates that this is a root node - axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE ) STRICT; - + CREATE TABLE panes( pane_id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, -- Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; - + CREATE TABLE center_panes( pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, -- NULL means that this is a root pane - position INTEGER, -- NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) ON DELETE CASCADE, FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE ) STRICT; - + CREATE TABLE items( - item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique workspace_id INTEGER NOT NULL, pane_id INTEGER NOT NULL, kind TEXT NOT NULL, @@ -84,7 +84,7 @@ impl Domain for Workspace { ON DELETE CASCADE, PRIMARY KEY(item_id, workspace_id) ) STRICT; - "}] + )] } } @@ -158,26 +158,22 @@ impl WorkspaceDb { .context("clearing out old locations")?; // Upsert - conn.exec_bound(indoc! {" + conn.exec_bound(sql!( INSERT INTO workspaces( - workspace_id, - workspace_location, - dock_visible, - dock_anchor, + workspace_id, + workspace_location, + dock_visible, + dock_anchor, timestamp - ) + ) VALUES (?1, ?2, ?3, ?4, CURRENT_TIMESTAMP) ON CONFLICT DO - UPDATE SET + UPDATE SET workspace_location = ?2, dock_visible = ?3, dock_anchor = ?4, timestamp = CURRENT_TIMESTAMP - "})?(( - workspace.id, - &workspace.location, - workspace.dock_position, - )) + ))?((workspace.id, &workspace.location, workspace.dock_position)) .context("Updating workspace")?; // Save center pane group and dock pane @@ -203,7 +199,7 @@ impl WorkspaceDb { query! { pub async fn next_id() -> Result { - "INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id" + INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id } }