diff --git a/Cargo.lock b/Cargo.lock index 2cf20bd..feec813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1266,6 +1266,7 @@ dependencies = [ "regex 1.6.0", "rusqlite", "shellexpand", + "sqlparser", "symphonia 0.5.0", "unidecode", ] @@ -2105,6 +2106,15 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +[[package]] +name = "sqlparser" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beb13adabbdda01b63d595f38c8bfd19a361e697fd94ce0098a634077bc5b25" +dependencies = [ + "log", +] + [[package]] name = "stdweb" version = "0.1.3" diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml index f4c7d61..7352002 100644 --- a/interpreter/Cargo.toml +++ b/interpreter/Cargo.toml @@ -7,7 +7,8 @@ readme = "README.md" rust-version = "1.59" [dependencies] -rusqlite = { version = "0.26", features = ["bundled"] } +rusqlite = { version = "0.26", features = ["bundled"], optional = true } +sqlparser = { version = "0.23", optional = true } symphonia = { version = "0.5", optional = true, features = [ "aac", "alac", "flac", "mp3", "pcm", "vorbis", "isomp4", "ogg", "wav" ] } @@ -27,7 +28,10 @@ name = "file_parse" harness = false [features] -default = [ "music_library", "ergonomics", "advanced" ] +default = [ "music_library", "ergonomics", "advanced", "advanced-bliss", "fakesql" ] music_library = [ "symphonia", "mpd" ] # song metadata parsing and database auto-population ergonomics = ["shellexpand", "unidecode"] # niceties like ~ in paths and unicode string sanitisation -advanced = ["bliss-audio-symphonia"] # advanced language features like bliss playlist generation +advanced = [] # advanced language features like music analysis +advanced-bliss = ["bliss-audio-symphonia"] # bliss audio analysis +sql = [ "rusqlite" ] +fakesql = [ "sqlparser" ] diff --git a/interpreter/src/context.rs b/interpreter/src/context.rs index 1588e1d..99625f8 100644 --- a/interpreter/src/context.rs +++ b/interpreter/src/context.rs @@ -1,6 +1,8 @@ #[cfg(feature = "advanced")] use super::processing::advanced::{DefaultAnalyzer, MusicAnalyzer}; -use super::processing::database::{DatabaseQuerier, SQLiteTranspileExecutor}; +use super::processing::database::DatabaseQuerier; +#[cfg(feature = "fakesql")] +use super::processing::database::SQLiteTranspileExecutor; #[cfg(feature = "mpd")] use super::processing::database::{MpdExecutor, MpdQuerier}; use super::processing::general::{ @@ -22,7 +24,12 @@ pub struct Context { impl Default for Context { fn default() -> Self { Self { + #[cfg(feature = "fakesql")] database: Box::new(SQLiteTranspileExecutor::default()), + #[cfg(all(feature = "sql", not(feature = "fakesql")))] + database: Box::new(super::processing::database::SQLiteExecutor::default()), + #[cfg(all(not(feature = "sql"), not(feature = "fakesql")))] + database: Box::new(super::processing::database::SQLErrExecutor::default()), variables: Box::new(OpStorage::default()), filesystem: Box::new(FilesystemExecutor::default()), #[cfg(feature = "advanced")] diff --git a/interpreter/src/lang/db_items.rs b/interpreter/src/lang/db_items.rs index 4ae8633..6514650 100644 --- a/interpreter/src/lang/db_items.rs +++ b/interpreter/src/lang/db_items.rs @@ -1,15 +1,20 @@ +#[cfg(feature = "sql")] use std::path::Path; +#[cfg(feature = "sql")] pub const DEFAULT_SQLITE_FILEPATH: &str = "metadata.muss.sqlite"; pub trait DatabaseObj: Sized { + #[cfg(feature = "sql")] fn map_row(row: &rusqlite::Row) -> rusqlite::Result; + #[cfg(feature = "sql")] fn to_params(&self) -> Vec<&'_ dyn rusqlite::ToSql>; fn id(&self) -> u64; } +#[cfg(feature = "sql")] pub fn generate_default_db() -> rusqlite::Result { generate_db( super::utility::music_folder(), @@ -18,6 +23,7 @@ pub fn generate_default_db() -> rusqlite::Result { ) } +#[cfg(feature = "sql")] pub fn generate_db, P2: AsRef>( music_path: P1, sqlite_path: P2, @@ -172,6 +178,7 @@ pub struct DbMusicItem { } impl DatabaseObj for DbMusicItem { + #[cfg(feature = "sql")] fn map_row(row: &rusqlite::Row) -> rusqlite::Result { Ok(Self { song_id: row.get(0)?, @@ -184,6 +191,7 @@ impl DatabaseObj for DbMusicItem { }) } + #[cfg(feature = "sql")] fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { vec![ &self.song_id, @@ -212,6 +220,7 @@ pub struct DbMetaItem { } impl DatabaseObj for DbMetaItem { + #[cfg(feature = "sql")] fn map_row(row: &rusqlite::Row) -> rusqlite::Result { Ok(Self { meta_id: row.get(0)?, @@ -223,6 +232,7 @@ impl DatabaseObj for DbMetaItem { }) } + #[cfg(feature = "sql")] fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { vec![ &self.meta_id, @@ -247,6 +257,7 @@ pub struct DbArtistItem { } impl DatabaseObj for DbArtistItem { + #[cfg(feature = "sql")] fn map_row(row: &rusqlite::Row) -> rusqlite::Result { Ok(Self { artist_id: row.get(0)?, @@ -255,6 +266,7 @@ impl DatabaseObj for DbArtistItem { }) } + #[cfg(feature = "sql")] fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { vec![&self.artist_id, &self.name, &self.genre] } @@ -274,6 +286,7 @@ pub struct DbAlbumItem { } impl DatabaseObj for DbAlbumItem { + #[cfg(feature = "sql")] fn map_row(row: &rusqlite::Row) -> rusqlite::Result { Ok(Self { album_id: row.get(0)?, @@ -284,6 +297,7 @@ impl DatabaseObj for DbAlbumItem { }) } + #[cfg(feature = "sql")] fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { vec![ &self.album_id, @@ -306,6 +320,7 @@ pub struct DbGenreItem { } impl DatabaseObj for DbGenreItem { + #[cfg(feature = "sql")] fn map_row(row: &rusqlite::Row) -> rusqlite::Result { Ok(Self { genre_id: row.get(0)?, @@ -313,6 +328,7 @@ impl DatabaseObj for DbGenreItem { }) } + #[cfg(feature = "sql")] fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { vec![&self.genre_id, &self.title] } diff --git a/interpreter/src/lang/mod.rs b/interpreter/src/lang/mod.rs index 87175c6..2ca431c 100644 --- a/interpreter/src/lang/mod.rs +++ b/interpreter/src/lang/mod.rs @@ -1,5 +1,6 @@ #![allow(clippy::match_like_matches_macro)] #![allow(clippy::needless_range_loop)] + mod db_items; mod dictionary; mod error; @@ -39,13 +40,17 @@ pub mod vocabulary; pub mod db { pub use super::db_items::{ - generate_db, generate_default_db, DatabaseObj, DbAlbumItem, DbArtistItem, DbGenreItem, - DbMetaItem, DbMusicItem, DEFAULT_SQLITE_FILEPATH, + DbAlbumItem, DbArtistItem, DbGenreItem, DbMetaItem, DbMusicItem, DatabaseObj + }; + #[cfg(feature = "sql")] + pub use super::db_items::{ + generate_db, generate_default_db, DEFAULT_SQLITE_FILEPATH }; } #[cfg(test)] mod tests { + #[cfg(feature = "sql")] #[test] fn db_build_test() -> rusqlite::Result<()> { super::db::generate_default_db()?; diff --git a/interpreter/src/lang/type_primitives.rs b/interpreter/src/lang/type_primitives.rs index 80289b9..14cf2df 100644 --- a/interpreter/src/lang/type_primitives.rs +++ b/interpreter/src/lang/type_primitives.rs @@ -26,6 +26,14 @@ impl TypePrimitive { } } + #[inline] + pub fn for_compare(&self) -> Self { + match self { + Self::String(s) => Self::String(s.to_lowercase()), + x => x.clone(), + } + } + pub fn to_str(self) -> Option { match self { Self::String(s) => Some(s), diff --git a/interpreter/src/lang/vocabulary/mpd_query.rs b/interpreter/src/lang/vocabulary/mpd_query.rs index e47ec15..034558e 100644 --- a/interpreter/src/lang/vocabulary/mpd_query.rs +++ b/interpreter/src/lang/vocabulary/mpd_query.rs @@ -1,17 +1,30 @@ +#[cfg(feature = "mpd")] use std::collections::VecDeque; +#[cfg(feature = "mpd")] use std::fmt::{Debug, Display, Error, Formatter}; +#[cfg(feature = "mpd")] use std::iter::Iterator; +#[cfg(feature = "mpd")] use std::net::SocketAddr; +#[cfg(feature = "mpd")] use crate::tokens::Token; +#[cfg(feature = "mpd")] use crate::Context; +#[cfg(feature = "mpd")] use crate::lang::utility::{assert_token, assert_token_raw}; +#[cfg(feature = "mpd")] use crate::lang::TypePrimitive; +#[cfg(feature = "mpd")] use crate::lang::{repeated_tokens, LanguageDictionary, Lookup}; +#[cfg(feature = "mpd")] use crate::lang::{FunctionFactory, FunctionStatementFactory, IteratorItem, Op, PseudoOp}; +#[cfg(feature = "mpd")] use crate::lang::{RuntimeError, RuntimeOp, SyntaxError}; +#[cfg(feature = "mpd")] use crate::processing::general::Type; +#[cfg(feature = "mpd")] use crate::Item; #[cfg(feature = "mpd")] diff --git a/interpreter/src/music/build_library.rs b/interpreter/src/music/build_library.rs index 4a72794..a1ca931 100644 --- a/interpreter/src/music/build_library.rs +++ b/interpreter/src/music/build_library.rs @@ -1,6 +1,8 @@ use std::path::Path; use super::Library; + +#[cfg(feature = "sql")] use crate::lang::db::*; pub fn build_library_from_files>(path: P, lib: &mut Library) -> std::io::Result<()> { @@ -9,6 +11,7 @@ pub fn build_library_from_files>(path: P, lib: &mut Library) -> s Ok(()) } +#[cfg(feature = "sql")] pub fn build_library_from_sqlite( conn: &rusqlite::Connection, lib: &mut Library, diff --git a/interpreter/src/music/mod.rs b/interpreter/src/music/mod.rs index d1ac571..9a48368 100644 --- a/interpreter/src/music/mod.rs +++ b/interpreter/src/music/mod.rs @@ -2,5 +2,7 @@ mod build_library; mod library; mod tag; -pub use build_library::{build_library_from_files, build_library_from_sqlite}; +pub use build_library::build_library_from_files; +#[cfg(feature = "sql")] +pub use build_library::build_library_from_sqlite; pub use library::Library; diff --git a/interpreter/src/processing/mod.rs b/interpreter/src/processing/mod.rs index 7e5d86e..497de2a 100644 --- a/interpreter/src/processing/mod.rs +++ b/interpreter/src/processing/mod.rs @@ -11,7 +11,13 @@ mod variables; pub mod database { #[cfg(feature = "mpd")] pub use super::mpd::{MpdExecutor, MpdQuerier}; - pub use super::sql::{DatabaseQuerier, QueryResult, SQLiteExecutor, SQLiteTranspileExecutor}; + pub use super::sql::{DatabaseQuerier, QueryResult}; + #[cfg(feature = "sql")] + pub use super::sql::{SQLiteExecutor}; + #[cfg(feature = "fakesql")] + pub use super::sql::{SQLiteTranspileExecutor}; + #[cfg(all(not(feature = "fakesql"), not(feature = "sql")))] + pub use super::sql::{SQLErrExecutor}; } pub mod general { diff --git a/interpreter/src/processing/sql/executor.rs b/interpreter/src/processing/sql/executor.rs index 37105c9..2621823 100644 --- a/interpreter/src/processing/sql/executor.rs +++ b/interpreter/src/processing/sql/executor.rs @@ -1,10 +1,18 @@ use core::fmt::Debug; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; +#[cfg(feature = "sql")] +use std::collections::HashSet; + +#[cfg(feature = "sql")] use std::fmt::Write; +#[cfg(feature = "sql")] use crate::lang::db::*; use crate::lang::RuntimeMsg; -use crate::lang::{Op, VecOp}; +use crate::lang::Op; +#[cfg(feature = "sql")] +use crate::lang::VecOp; +#[cfg(feature = "sql")] use crate::Item; pub type QueryResult = Result, RuntimeMsg>; @@ -34,11 +42,13 @@ pub trait DatabaseQuerier: Debug { fn init_with_params(&mut self, params: &HashMap) -> Result<(), RuntimeMsg>; } +#[cfg(feature = "sql")] #[derive(Default, Debug)] pub struct SQLiteExecutor { sqlite_connection: Option, // initialized by first SQL statement } +#[cfg(feature = "sql")] impl SQLiteExecutor { #[inline] fn gen_db_maybe(&mut self) -> Result<(), RuntimeMsg> { @@ -67,6 +77,7 @@ impl SQLiteExecutor { } } +#[cfg(feature = "sql")] impl DatabaseQuerier for SQLiteExecutor { fn raw(&mut self, query: &str) -> QueryResult { self.gen_db_maybe()?; @@ -184,12 +195,14 @@ impl DatabaseQuerier for SQLiteExecutor { } } +#[cfg(feature = "sql")] struct SqliteSettings { music_path: Option, db_path: Option, auto_generate: bool, } +#[cfg(feature = "sql")] impl std::default::Default for SqliteSettings { fn default() -> Self { SqliteSettings { @@ -200,6 +213,7 @@ impl std::default::Default for SqliteSettings { } } +#[cfg(feature = "sql")] impl std::convert::TryInto for SqliteSettings { type Error = rusqlite::Error; @@ -215,6 +229,7 @@ impl std::convert::TryInto for SqliteSettings { } } +#[cfg(feature = "sql")] #[inline(always)] fn build_mps_item(conn: &mut rusqlite::Connection, item: DbMusicItem) -> rusqlite::Result { // query artist @@ -233,6 +248,7 @@ fn build_mps_item(conn: &mut rusqlite::Connection, item: DbMusicItem) -> rusqlit Ok(rows_to_item(item, artist, album, meta, genre)) } +#[cfg(feature = "sql")] #[inline] fn perform_query( conn: &mut rusqlite::Connection, @@ -255,6 +271,7 @@ fn perform_query( Ok(iter2.collect()) } +#[cfg(feature = "sql")] #[inline] fn perform_single_param_query( conn: &mut rusqlite::Connection, @@ -278,6 +295,7 @@ fn perform_single_param_query( Ok(iter2.collect()) } +#[cfg(feature = "sql")] fn rows_to_item( music: DbMusicItem, artist: DbArtistItem, @@ -302,13 +320,45 @@ fn rows_to_item( item } +#[cfg(all(not(feature = "fakesql"), not(feature = "sql")))] +#[derive(Default, Debug)] +pub struct SQLErrExecutor; + +#[cfg(all(not(feature = "fakesql"), not(feature = "sql")))] +impl DatabaseQuerier for SQLErrExecutor { + fn raw(&mut self, _query: &str) -> QueryResult { + Err(RuntimeMsg("No SQL executor available".to_owned())) + } + + fn artist_like(&mut self, _query: &str) -> QueryResult { + Err(RuntimeMsg("No SQL executor available".to_owned())) + } + + fn album_like(&mut self, _query: &str) -> QueryResult { + Err(RuntimeMsg("No SQL executor available".to_owned())) + } + + fn song_like(&mut self, _query: &str) -> QueryResult { + Err(RuntimeMsg("No SQL executor available".to_owned())) + } + + fn genre_like(&mut self, _query: &str) -> QueryResult { + Err(RuntimeMsg("No SQL executor available".to_owned())) + } + + fn init_with_params(&mut self, _params: &HashMap) -> Result<(), RuntimeMsg> { + Err(RuntimeMsg("No SQL executor available".to_owned())) + } +} + +#[cfg(feature = "fakesql")] #[derive(Default, Debug)] pub struct SQLiteTranspileExecutor; +#[cfg(feature = "fakesql")] impl DatabaseQuerier for SQLiteTranspileExecutor { - fn raw(&mut self, _query: &str) -> QueryResult { - // TODO - Err(RuntimeMsg("Unimplemented".to_owned())) + fn raw(&mut self, query: &str) -> QueryResult { + Ok(Box::new(super::RawSqlQuery::emit(query)?)) } fn artist_like(&mut self, query: &str) -> QueryResult { diff --git a/interpreter/src/processing/sql/mod.rs b/interpreter/src/processing/sql/mod.rs index ea6653a..4574020 100644 --- a/interpreter/src/processing/sql/mod.rs +++ b/interpreter/src/processing/sql/mod.rs @@ -1,5 +1,11 @@ mod executor; +#[cfg(feature = "fakesql")] +mod raw_emit; +#[cfg(feature = "fakesql")] mod simple_emit; pub use executor::*; +#[cfg(feature = "fakesql")] +pub use raw_emit::RawSqlQuery; +#[cfg(feature = "fakesql")] pub use simple_emit::SimpleSqlQuery; diff --git a/interpreter/src/processing/sql/raw_emit.rs b/interpreter/src/processing/sql/raw_emit.rs new file mode 100644 index 0000000..fe7de8a --- /dev/null +++ b/interpreter/src/processing/sql/raw_emit.rs @@ -0,0 +1,438 @@ +use std::fmt::{Debug, Display, Error, Formatter}; +use std::iter::Iterator; +use std::collections::VecDeque; + +use sqlparser::{parser::Parser, dialect::SQLiteDialect}; +use sqlparser::ast::{Statement, SetExpr, Expr, OrderByExpr, Value, BinaryOperator}; + +use crate::Context; + +use crate::lang::{IteratorItem, Op, PseudoOp}; +use crate::lang::{RuntimeError, RuntimeOp, RuntimeMsg, TypePrimitive}; +use crate::processing::general::FileIter; +use crate::Item; + +#[derive(Debug)] +pub struct RawSqlQuery { + context: Option, + file_iter: Option, + match_rule: Option, + sort_by: Option, + items_buffer: VecDeque, + raw_query: String, + has_tried: bool, +} + +impl RawSqlQuery { + pub fn emit(query_str: &str) -> Result { + let mut statements = Parser::parse_sql(&SQLiteDialect{}, query_str).map_err(|e| RuntimeMsg(format!("Could not parse SQL query: {}", e)))?; + if statements.len() == 1 { + if let Statement::Query(mut query) = statements.remove(0) { + let matching = if let SetExpr::Select(select) = *query.body { + if let Some(selection) = select.selection { + Some(MatchRule::from_parsed(selection)?) + } else { + None + } + } else { + return Err(RuntimeMsg("Unsupported SELECT syntax in SQL".to_owned())); + }; + let ordering = if !query.order_by.is_empty() { + Some(SortRule::from_parsed(query.order_by.remove(0))?) + } else { + None + }; + Ok(Self { + context: None, + file_iter: None, + match_rule: matching, + sort_by: ordering, + items_buffer: VecDeque::new(), + raw_query: query_str.to_owned(), + has_tried: false, + }) + } else { + Err(RuntimeMsg("Expected SQL SELECT statement".to_owned())) + } + } else { + Err(RuntimeMsg(format!("Expected exactly 1 SQL SELECT statement, got {} statements", statements.len()))) + } + } + + #[inline] + fn matches_filters(&self, item: &Item) -> bool { + if let Some(match_rule) = &self.match_rule { + match_rule.is_match(item) + } else { + true + } + } +} + +impl Display for RawSqlQuery { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "sql(`{}`)", self.raw_query) + } +} + +impl std::clone::Clone for RawSqlQuery { + fn clone(&self) -> Self { + Self { + context: None, + file_iter: None, + match_rule: self.match_rule.clone(), + sort_by: self.sort_by.clone(), + items_buffer: VecDeque::with_capacity(self.items_buffer.len()), + raw_query: self.raw_query.clone(), + has_tried: self.has_tried, + } + } +} + +impl Iterator for RawSqlQuery { + type Item = IteratorItem; + + fn next(&mut self) -> Option { + if self.file_iter.is_none() { + if self.has_tried { + return None; + } else { + self.has_tried = true; + } + let iter = self.context.as_mut().unwrap().filesystem.raw( + None, + None, + true, + ); + self.file_iter = Some(match iter { + Ok(x) => x, + Err(e) => return Some(Err(e.with(RuntimeOp(PseudoOp::from_printable(self))))), + }); + } + if let Some(sort_by) = &self.sort_by { + let old_len = self.items_buffer.len(); + while let Some(item) = self.file_iter.as_mut().unwrap().next() { + match item { + Ok(item) => { + // apply filter + if self.matches_filters(&item) { + self.items_buffer.push_back(Ok(item)); + } + }, + Err(e) => self.items_buffer.push_back(Err(RuntimeError { + line: 0, + op: PseudoOp::from_printable(self), + msg: e, + })) + } + } + let new_len = self.items_buffer.len(); + if old_len != new_len { + // file_iter was just completed, so buffer needs sorting + sort_by.sort_vecdeque(&mut self.items_buffer); + } + self.items_buffer.pop_front() + } else { + while let Some(item) = self.file_iter.as_mut().unwrap().next() { + match item { + Ok(item) => { + // apply filter + if self.matches_filters(&item) { + return Some(Ok(item)); + } + }, + Err(e) => return Some(Err(RuntimeError { + line: 0, + op: PseudoOp::from_printable(self), + msg: e, + })) + } + } + None + } + } + + fn size_hint(&self) -> (usize, Option) { + self.file_iter.as_ref().map(|x| x.size_hint()).unwrap_or_default() + } +} + +impl Op for RawSqlQuery { + fn enter(&mut self, ctx: Context) { + self.context = Some(ctx) + } + + fn escape(&mut self) -> Context { + self.context.take().unwrap() + } + + fn is_resetable(&self) -> bool { + true + } + + fn reset(&mut self) -> Result<(), RuntimeError> { + Ok(()) + } + + fn dup(&self) -> Box { + Box::new(self.clone()) + } +} + +#[derive(Debug, Clone)] +enum MatchRule { + Like { field: String, pattern: LikePattern, negated: bool }, + CompareVal { field: String, value: TypePrimitive, comparison: [i8; 2] }, + CompareFields { field_a: String, field_b: String, comparison: [i8; 2] }, + And { a: Box, b: Box }, + Or { a: Box, b: Box }, +} + +impl MatchRule { + #[inline] + fn is_match(&self, item: &Item) -> bool { + match self { + Self::Like { field, pattern, negated } => { + if let Some(TypePrimitive::String(val)) = item.field(field) { + pattern.is_match(val) != *negated + } else { + *negated + } + }, + Self::CompareVal { field, value, comparison } => { + if let Some(val) = item.field(field) { + match val.compare(value) { + Ok(cmp) => comparison[0] == cmp || comparison[1] == cmp, + Err(_) => comparison[0] != 0 && comparison[1] != 0, + } + } else { + match TypePrimitive::Empty.compare(value) { + Ok(cmp) => comparison[0] == cmp || comparison[1] == cmp, + Err(_) => comparison[0] != 0 && comparison[1] != 0, + } + } + }, + Self::CompareFields { field_a, field_b, comparison} => { + if let Some(val_a) = item.field(field_a) { + if let Some(val_b) = item.field(field_b) { + match val_a.compare(val_b) { + Ok(cmp) => comparison[0] == cmp || comparison[1] == cmp, + Err(_) => comparison[0] != 0 && comparison[1] != 0, + } + } else { + match val_a.compare(&TypePrimitive::Empty) { + Ok(cmp) => comparison[0] == cmp || comparison[1] == cmp, + Err(_) => comparison[0] != 0 && comparison[1] != 0, + } + } + } else { + if let Some(val_b) = item.field(field_b) { + match TypePrimitive::Empty.compare(val_b) { + Ok(cmp) => comparison[0] == cmp || comparison[1] == cmp, + Err(_) => comparison[0] != 0 && comparison[1] != 0, + } + } else { + match TypePrimitive::Empty.compare(&TypePrimitive::Empty) { + Ok(cmp) => comparison[0] == cmp || comparison[1] == cmp, + Err(_) => comparison[0] != 0 && comparison[1] != 0, + } + } + } + }, + Self::And { a, b } => { + a.is_match(item) && b.is_match(item) + }, + Self::Or { a, b } => { + a.is_match(item) || b.is_match(item) + }, + } + } + + #[inline] + fn from_parsed(expr: Expr) -> Result { + match expr { + Expr::IsFalse(x) => if let Expr::Identifier(id) = *x { + Ok(Self::CompareVal{ field: id.value, value:TypePrimitive::Bool(false), comparison: [0, 0] }) + } else { + Err(RuntimeMsg(format!("Unsupported SQL IS FALSE syntax: {}", x))) + }, + Expr::IsNotFalse(x) => if let Expr::Identifier(id) = *x { + Ok(Self::CompareVal{ field: id.value, value:TypePrimitive::Bool(false), comparison: [1, -1] }) + } else { + Err(RuntimeMsg(format!("Unsupported SQL IS NOT FALSE syntax: {}", x))) + }, + Expr::IsTrue(x) => if let Expr::Identifier(id) = *x { + Ok(Self::CompareVal{ field: id.value, value:TypePrimitive::Bool(true), comparison: [0, 0] }) + } else { + Err(RuntimeMsg(format!("Unsupported SQL IS TRUE syntax: {}", x))) + }, + Expr::IsNotTrue(x) => if let Expr::Identifier(id) = *x { + Ok(Self::CompareVal{ field: id.value, value:TypePrimitive::Bool(true), comparison: [1, -1] }) + } else { + Err(RuntimeMsg(format!("Unsupported SQL IS NOT TRUE syntax: {}", x))) + }, + Expr::IsNull(x) => if let Expr::Identifier(id) = *x { + Ok(Self::CompareVal{ field: id.value, value:TypePrimitive::Empty, comparison: [0, 0] }) + } else { + Err(RuntimeMsg(format!("Unsupported SQL IS NULL syntax: {}", x))) + }, + Expr::IsNotNull(x) => if let Expr::Identifier(id) = *x { + Ok(Self::CompareVal{ field: id.value, value:TypePrimitive::Empty, comparison: [1, -1] }) + } else { + Err(RuntimeMsg(format!("Unsupported SQL IS NOT NULL syntax: {}", x))) + }, + Expr::Like { negated, expr, pattern, .. } => match (*expr, *pattern) { + (Expr::Identifier(expr), Expr::Value(Value::SingleQuotedString(pattern))) => + Ok(Self::Like{ field: expr.value, negated: negated, pattern: LikePattern::from_string(pattern) }), + (x, y) => Err(RuntimeMsg(format!("Unsupported SQL LIKE syntax: {} LIKE {}", x, y))) + }, + Expr::ILike { negated, expr, pattern, .. } => match (*expr, *pattern) { + (Expr::Identifier(expr), Expr::Value(Value::SingleQuotedString(pattern))) => + Ok(Self::Like{ field: expr.value, negated: negated, pattern: LikePattern::from_string(pattern) }), + (x, y) => Err(RuntimeMsg(format!("Unsupported SQL ILIKE syntax: {} ILIKE {}", x, y))) + }, + Expr::Nested(x) => Self::from_parsed(*x), + Expr::BinaryOp { left, op, right } => { + if let BinaryOperator::And = op { + Ok(Self::And { a: Box::new(Self::from_parsed(*left)?), b: Box::new(Self::from_parsed(*right)?) }) + } else if let BinaryOperator::Or = op { + Ok(Self::Or { a: Box::new(Self::from_parsed(*left)?), b: Box::new(Self::from_parsed(*right)?) }) + } else { + match (*left, *right) { + (Expr::Identifier(left), Expr::Value(right)) => + Ok(Self::CompareVal { + field: left.value, + value: value_to_primitive(right)?, + comparison: binary_op_to_compare(op)?, + }), + (Expr::Identifier(left), Expr::Identifier(right)) => + Ok(Self::CompareFields { + field_a: left.value, + field_b: right.value, + comparison: binary_op_to_compare(op)?, + }), + (x, y) => Err(RuntimeMsg(format!("Unsupported SQL operator syntax: {} {} {}", x, op, y))) + } + } + }, + x => Err(RuntimeMsg(format!("Unsupported SQL WHERE syntax: {}", x))) + } + } +} + +#[inline] +fn binary_op_to_compare(op: BinaryOperator) -> Result<[i8; 2], RuntimeMsg> { + match op { + BinaryOperator::Gt => Ok([1, 1]), + BinaryOperator::Lt => Ok([-1, -1]), + BinaryOperator::GtEq => Ok([1, 0]), + BinaryOperator::LtEq => Ok([-1, 0]), + BinaryOperator::Eq => Ok([0, 0]), + BinaryOperator::NotEq => Ok([-1, 1]), + x => Err(RuntimeMsg(format!("Unsupported SQL operator syntax: {}", x))) + } +} + +#[inline] +fn value_to_primitive(val: Value) -> Result { + match val { + Value::Number(s, _) => Ok(TypePrimitive::parse(s)), + Value::SingleQuotedString(s) => Ok(TypePrimitive::String(s)), + Value::DoubleQuotedString(s) => Ok(TypePrimitive::String(s)), + Value::Boolean(b) => Ok(TypePrimitive::Bool(b)), + Value::Null => Ok(TypePrimitive::Empty), + x => Err(RuntimeMsg(format!("Unsupported SQL operator syntax: {}", x))) + } +} + +#[derive(Debug, Clone)] +enum LikePattern { + EndsWith(String), + StartWith(String), + Contains(String), + Is(String), +} + +impl LikePattern { + #[inline] + fn is_match(&self, text: &str) -> bool { + match self { + Self::EndsWith(p) => text.to_lowercase().ends_with(p), + Self::StartWith(p) => text.to_lowercase().starts_with(p), + Self::Contains(p) => text.to_lowercase().contains(p), + Self::Is(p) => &text.to_lowercase() == p, + } + } + + #[inline] + fn from_string(pattern: String) -> Self { + match (pattern.starts_with('%'), pattern.ends_with('%')) { + (false, true) => Self::EndsWith(pattern[..pattern.len()-1].to_owned()), + (true, false) => Self::StartWith(pattern[1..].to_owned()), + (true, true) => Self::Contains(pattern[1..pattern.len()-1].to_owned()), + (false, false) => Self::Is(pattern), + } + } +} + +#[derive(Debug, Clone)] +enum SortRule { + Ascending(String), + Descending(String), +} + +impl SortRule { + #[inline] + fn sort_vecdeque(&self, list: &mut VecDeque) { + let buffer = list.make_contiguous(); + match self { + Self::Ascending(field) => { + buffer.sort_by(|b, a| { + if let Ok(a) = a { + if let Some(a_field) = a.field(field) { + if let Ok(b) = b { + if let Some(b_field) = b.field(field) { + return a_field + .for_compare() + .partial_cmp(&b_field.for_compare()) + .unwrap_or(std::cmp::Ordering::Equal); + } + } + } + } + std::cmp::Ordering::Equal + }); + }, + Self::Descending(field) => { + buffer.sort_by(|a, b| { + if let Ok(a) = a { + if let Some(a_field) = a.field(field) { + if let Ok(b) = b { + if let Some(b_field) = b.field(field) { + return a_field + .for_compare() + .partial_cmp(&b_field.for_compare()) + .unwrap_or(std::cmp::Ordering::Equal); + } + } + } + } + std::cmp::Ordering::Equal + }); + } + } + } + + fn from_parsed(order: OrderByExpr) -> Result { + let field = if let Expr::Identifier(id) = order.expr { + id.value + } else { + return Err(RuntimeMsg(format!("Unsupported SQL syntax: ORDER BY value must be a field identifier"))); + }; + if order.asc.unwrap_or(true) { + Ok(Self::Ascending(field)) + } else { + Ok(Self::Descending(field)) + } + } +} diff --git a/interpreter/tests/single_line.rs b/interpreter/tests/single_line.rs index 1a3f50d..f69e115 100644 --- a/interpreter/tests/single_line.rs +++ b/interpreter/tests/single_line.rs @@ -110,7 +110,8 @@ fn execute_single_line( #[test] fn execute_sql_line() -> Result<(), InterpreterError> { - execute_single_line("sql(`SELECT * FROM songs ORDER BY artist;`)", false, true) + execute_single_line("sql(`SELECT * FROM songs WHERE artist IS NOT NULL ORDER BY artist;`)", false, true)?; + execute_single_line("sql(`SELECT * FROM songs WHERE artist IS NOT NULL AND format = 'flac' ORDER BY title DESC;`)", false, true) } #[test] diff --git a/src/repl.rs b/src/repl.rs index bce4236..c118d14 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -511,14 +511,15 @@ fn read_loop( } Key::Del => { if state.cursor_rightward_position != 0 { - let removed_char = state + let _removed_char = state .current_line .remove(state.current_line.len() - state.cursor_rightward_position); state .statement_buf .remove(state.statement_buf.len() - state.cursor_rightward_position); - // re-sync unclosed syntax tracking - match removed_char { + // don't re-sync unclosed syntax tracking + // (removing char in front of cursor, not under cursor) + /*match removed_char { '"' | '`' => { if let Some(c2) = state.in_literal { if removed_char == c2 { @@ -541,7 +542,7 @@ fn read_loop( } '}' => state.curly_depth += 1, _ => {} - } + }*/ // re-print end of line to remove character in middle for i in state.current_line.len() + 1 - state.cursor_rightward_position ..state.current_line.len()