Detailed changes
@@ -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"
@@ -45,6 +45,8 @@ members = [
"crates/search",
"crates/settings",
"crates/snippet",
+ "crates/sqlez",
+ "crates/sqlez_macros",
"crates/sum_tree",
"crates/terminal",
"crates/text",
@@ -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"
@@ -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<Vec<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
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<Vec<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
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<Vec<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
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<Vec<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
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<Option<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
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<Option<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis async fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis async fn $id(&self) -> $crate::anyhow::Result<Option<$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 failed to execute or parse for: {}",
::std::stringify!($id),
- $sql,
+ sql_stmt
))
}).await
}
};
- ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$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
))
}
};
- ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$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
))
}
};
- ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $sql:expr }) => {
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
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
}
@@ -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<Option<String>> {
- "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 = (?)
}
}
}
@@ -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<PathBuf> {
- 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 (?, ?, ?)
}
}
}
@@ -12,3 +12,4 @@ doctest = false
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
+
@@ -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::<usize>("SELECt * FROM test")
+ .select_row::<usize>("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)
+ }
}
@@ -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<D: Domain> Migrator for D {
fn migrate(connection: &Connection) -> anyhow::Result<()> {
connection.migrate(Self::name(), Self::migrations())
@@ -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());
- }
- }
-}
@@ -17,7 +17,7 @@ lazy_static! {
Default::default();
}
-pub struct ThreadSafeConnection<M: Migrator> {
+pub struct ThreadSafeConnection<M: Migrator = ()> {
uri: Arc<str>,
persistent: bool,
initialize_query: Option<&'static str>,
@@ -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" }
@@ -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(),
+ }
+}
@@ -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<Option<PathBuf>> {
- indoc!{"
- SELECT working_directory
- FROM terminals
- WHERE item_id = ? AND workspace_id = ?
- "}
+ SELECT working_directory
+ FROM terminals
+ WHERE item_id = ? AND workspace_id = ?
}
}
}
@@ -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<WorkspaceId> {
- "INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id"
+ INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
}
}