Implement raw SQL queries for fake SQL executor, remove sqlite deps by default

This commit is contained in:
NGnius (Graham) 2022-09-20 19:41:23 -04:00
parent e0086b0dea
commit 7b92c340ee
15 changed files with 588 additions and 18 deletions

10
Cargo.lock generated
View file

@ -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"

View file

@ -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" ]

View file

@ -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")]

View file

@ -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<Self>;
#[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<rusqlite::Connection> {
generate_db(
super::utility::music_folder(),
@ -18,6 +23,7 @@ pub fn generate_default_db() -> rusqlite::Result<rusqlite::Connection> {
)
}
#[cfg(feature = "sql")]
pub fn generate_db<P1: AsRef<Path>, P2: AsRef<Path>>(
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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]
}

View file

@ -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()?;

View file

@ -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<String> {
match self {
Self::String(s) => Some(s),

View file

@ -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")]

View file

@ -1,6 +1,8 @@
use std::path::Path;
use super::Library;
#[cfg(feature = "sql")]
use crate::lang::db::*;
pub fn build_library_from_files<P: AsRef<Path>>(path: P, lib: &mut Library) -> std::io::Result<()> {
@ -9,6 +11,7 @@ pub fn build_library_from_files<P: AsRef<Path>>(path: P, lib: &mut Library) -> s
Ok(())
}
#[cfg(feature = "sql")]
pub fn build_library_from_sqlite(
conn: &rusqlite::Connection,
lib: &mut Library,

View file

@ -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;

View file

@ -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 {

View file

@ -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<Box<dyn Op>, RuntimeMsg>;
@ -34,11 +42,13 @@ pub trait DatabaseQuerier: Debug {
fn init_with_params(&mut self, params: &HashMap<String, String>) -> Result<(), RuntimeMsg>;
}
#[cfg(feature = "sql")]
#[derive(Default, Debug)]
pub struct SQLiteExecutor {
sqlite_connection: Option<rusqlite::Connection>, // 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<String>,
db_path: Option<String>,
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<rusqlite::Connection> for SqliteSettings {
type Error = rusqlite::Error;
@ -215,6 +229,7 @@ impl std::convert::TryInto<rusqlite::Connection> for SqliteSettings {
}
}
#[cfg(feature = "sql")]
#[inline(always)]
fn build_mps_item(conn: &mut rusqlite::Connection, item: DbMusicItem) -> rusqlite::Result<Item> {
// 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<String, String>) -> 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 {

View file

@ -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;

View file

@ -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<Context>,
file_iter: Option<FileIter>,
match_rule: Option<MatchRule>,
sort_by: Option<SortRule>,
items_buffer: VecDeque<IteratorItem>,
raw_query: String,
has_tried: bool,
}
impl RawSqlQuery {
pub fn emit(query_str: &str) -> Result<Self, RuntimeMsg> {
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<Self::Item> {
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<usize>) {
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<dyn Op> {
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<MatchRule>, b: Box<MatchRule> },
Or { a: Box<MatchRule>, b: Box<MatchRule> },
}
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<Self, RuntimeMsg> {
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<TypePrimitive, RuntimeMsg> {
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<IteratorItem>) {
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<Self, RuntimeMsg> {
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))
}
}
}

View file

@ -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]

View file

@ -511,14 +511,15 @@ fn read_loop<F: FnMut(&mut ReplState, &CliArgs)>(
}
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<F: FnMut(&mut ReplState, &CliArgs)>(
}
'}' => 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()