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}