Implement raw SQL queries for fake SQL executor, remove sqlite deps by default
This commit is contained in:
parent
e0086b0dea
commit
7b92c340ee
15 changed files with 588 additions and 18 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" ]
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
438
interpreter/src/processing/sql/raw_emit.rs
Normal file
438
interpreter/src/processing/sql/raw_emit.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue