sqlite_loadable/
vtab_argparse.rs

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