1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
//! Opininated parsing for SQLite virtual table constructor arguments.
//!
//! A "constructor" comes from the CREATE VIRTUAL TABLE statement
//! of a virtual table, like:
//! ```sql
//! CREATE VIRTUAL TABLE xxx USING custom_vtab(
//! mode="production",
//! state=null,
//! name TEXT,
//! age INTEGER,
//! progress REAL
//! )
//! ```
//!
//! sqlite_loadable passes down the arguments between `custom_vtab(...)`
//! as a vector of strings within `VTabArguments.arguments`, where each
//! comma-seperated argument is its own element in the vector.
//!
//! Virtual table statements are allowed to parse these arguments however
//! they want, and this module is one opinionated option, loosely based
//! on [FTS5 virtual tables](https://www.sqlite.org/fts5.html).
//!
use crate::api::ColumnAffinity;
/// A successfully parsed Argument from a virtual table constructor.
/// A single constructor can have multiple arguments, this struct
/// only represents a single one.
///
/// In this parser, the above constructor in `xxx` has 5 arguments -
/// 2 "configuration options" and "column declarations." The `mode`
/// argument is a configuration option with a key of `"mode"` and
/// a quoted string value of `"production"`. Similarly, `state` is
/// a configuration argument with key `"state"` and a bareword
/// value of `null`. On the other hand, `name`, `age`, and `progress`
/// are arguments that declare columns, with declared types `text`,
/// `integer`, and `real`, respectfully.
///
/// The virtual table implementations can do whatever they want with
/// the parsed arguments, including by not limited to erroring on
/// invalid options, creating new columns on the virtual table
/// based on the column definitions, requiring certain config options,
/// or anything else they want.
/// A single parsed argument from a virtual table constructor. Can
/// be a column declaration onf configuration option.
#[derive(Debug, PartialEq, Eq)]
pub enum Argument {
/// The argument declares a column - ex "name text" or "age integer".
/// Like SQLite, a column declartion can have 0 or any declared types,
/// and also tries to capture any "constraints".
Column(ColumnDeclaration),
/// The argument defines a configuration option - ex "mode=fast"
/// or "tokenize = 'porter ascii'". The key is always a string,
/// the value can be "rich" types like strings, booleans, numbers,
/// sqlite_parameters, or barewords.
Config(ConfigOption),
// TODO support wildcard column selection? '* EXCLUDE', '* REPLACE',
// maybe 'COLUMNS(/only_/)', etc.
}
/// A column declaration that defines a single column.
/// Example: `"name text"` would parse to a column with the name `"name"`
/// and declared type of `"text"`.
// TODO can this also support "aliased" or "computed/generated" columns,
// like "'/item/name' as name" (xml) or "FirstName as first_name" (csv)
// or "'$.name.first' as first_name" (json)?
#[derive(Debug, PartialEq, Eq)]
pub struct ColumnDeclaration {
/// Name of declared column
pub name: String,
// Declared type of the column
pub declared_type: Option<String>,
pub constraints: Option<String>,
}
impl ColumnDeclaration {
fn new(
name: &str,
declared_type: Option<&str>,
constraints: Option<&str>,
) -> ColumnDeclaration {
ColumnDeclaration {
name: name.to_owned(),
declared_type: declared_type.map(|d| d.to_owned()),
constraints: constraints.map(|d| d.to_owned()),
}
}
/// Determines the column declaration's "affinity", based on
/// the parsed declared type. Uses the same rules as
/// <https://www.sqlite.org/datatype3.html#determination_of_column_affinity>.
pub fn affinity(&self) -> ColumnAffinity {
match &self.declared_type {
Some(declared_type) => ColumnAffinity::from_declared_type(declared_type.as_str()),
None => crate::api::ColumnAffinity::Numeric,
}
}
/// Formats the column declaration into a way that a CREATE TABLE
/// statement expects ("escaping" the column name).
// TODO is this safe lol
pub fn vtab_declaration(&self) -> String {
format!(
"'{}' {}",
self.name,
self.declared_type.as_ref().map_or("", |d| d.as_str())
)
}
}
/// A parsed configuration option, that always contain a key/value
/// pair. These can be used as "table-options" to configure special
/// behavior or settings for the virtual table implementation.
/// Example: the `tokenize` and `prefix` config options on FTS5
/// virtual tables <https://www.sqlite.org/fts5.html#fts5_table_creation_and_initialization>
#[derive(Debug, PartialEq, Eq)]
pub struct ConfigOption {
pub key: String,
pub value: ConfigOptionValue,
}
/// Possible options for the "values" of configuration options.
///
#[derive(Debug, PartialEq, Eq)]
pub enum ConfigOptionValue {
///
Quoted(String),
///
SqliteParameter(String),
///
Bareword(String),
}
/// Given a raw argument, returns a parsed [`Argument`]. Should already by
/// comma (?) delimited, typically sourced from [`VTabArguments`](crate::table::VTabArguments).
pub fn parse_argument(argument: &str) -> std::result::Result<Argument, &str> {
match arg_is_config_option(argument) {
Ok(Some(config_option)) => return Ok(Argument::Config(config_option)),
Ok(None) => (),
Err(err) => return Err(err),
};
match arg_is_column_declaration(argument) {
Ok(Some(column_declaration)) => return Ok(Argument::Column(column_declaration)),
Ok(None) => (),
Err(err) => return Err(err),
};
Err("argument is niether a configuration option or column declaration.")
}
/// TODO renamed "parameter" to "named argument"
fn arg_is_config_option(arg: &str) -> Result<Option<ConfigOption>, &str> {
let mut split = arg.split('=');
let key = match split.next() {
Some(k) => k,
None => return Ok(None),
};
let value = match split.next() {
Some(k) => k,
None => return Ok(None),
};
Ok(Some(ConfigOption {
key: key.to_owned(),
value: parse_config_option_value(value)?,
}))
}
fn parse_config_option_value(value: &str) -> Result<ConfigOptionValue, &str> {
let value = value.trim();
match value.chars().next() {
Some('\'') | Some('"') => {
// TODO ensure last starts with quote
let mut chars = value.chars();
chars.next();
chars.next_back();
Ok(ConfigOptionValue::Quoted(chars.as_str().to_owned()))
}
Some(':') | Some('@') => {
// TODO ensure it's a proper sqlite_parameter
// (not start with digit?? or spaces??)
Ok(ConfigOptionValue::SqliteParameter(value.to_owned()))
}
Some(_) => {
// TODO ensure there's no quote words in bareword?
Ok(ConfigOptionValue::Bareword(value.to_owned()))
}
None => Err("asdf"),
}
}
pub fn arg_is_column_declaration(arg: &str) -> Result<Option<ColumnDeclaration>, &str> {
if arg.trim().is_empty() {
return Ok(None);
}
let mut split = arg.split(' ');
let name = split.next().ok_or("asdf")?;
let declared_type = split.next();
let constraints = None;
Ok(Some(ColumnDeclaration::new(
name,
declared_type,
constraints,
)))
}
#[cfg(test)]
mod tests {
use crate::vtab_argparse::*;
#[test]
fn test_parse_argument() {
assert_eq!(
parse_argument("name text"),
Ok(Argument::Column(ColumnDeclaration::new(
"name",
Some("text"),
None,
)))
);
assert_eq!(
parse_argument("option='quoted'"),
Ok(Argument::Config(ConfigOption {
key: "option".to_owned(),
value: ConfigOptionValue::Quoted("quoted".to_owned())
}))
);
assert_eq!(
parse_argument("option=:param"),
Ok(Argument::Config(ConfigOption {
key: "option".to_owned(),
value: ConfigOptionValue::SqliteParameter(":param".to_owned())
}))
);
assert_eq!(
parse_argument("option=bareword"),
Ok(Argument::Config(ConfigOption {
key: "option".to_owned(),
value: ConfigOptionValue::Bareword("bareword".to_owned())
}))
);
}
}