rusqlite/src/vtab/csvtab.rs

397 lines
12 KiB
Rust
Raw Normal View History

//! CSV Virtual Table
2018-05-06 18:41:42 +08:00
//! Port of [csv](http://www.sqlite.org/cgi/src/finfo?name=ext/misc/csv.c) C extension.
extern crate csv;
use std::fs::File;
use std::os::raw::{c_char, c_int, c_void};
use std::path::Path;
use std::result;
2016-02-11 03:30:08 +08:00
use std::str;
use ffi;
2016-02-11 01:07:58 +08:00
use types::Null;
2018-05-06 23:21:36 +08:00
use vtab::{
declare_vtab, dequote, escape_double_quote, parse_boolean, Context, IndexInfo, VTab,
VTabCursor, Values,
};
use {Connection, Error, Result};
2016-02-12 04:47:47 +08:00
/// Register the "csv" module.
2018-05-06 18:41:42 +08:00
/// ```sql
/// CREATE VIRTUAL TABLE vtab USING csv(
/// filename=FILENAME -- Name of file containing CSV content
/// [, schema=SCHEMA] -- Alternative CSV schema. 'CREATE TABLE x(col1 TEXT NOT NULL, col2 INT, ...);'
/// [, header=YES|NO] -- First row of CSV defines the names of columns if "yes". Default "no".
/// [, columns=N] -- Assume the CSV file contains N columns.
/// [, delimiter=C] -- CSV delimiter. Default ','.
/// [, quote=C] -- CSV quote. Default '"'. 0 means no quote.
/// );
/// ```
pub fn load_module(conn: &Connection) -> Result<()> {
let aux: Option<()> = None;
conn.create_module("csv", &CSV_MODULE, aux)
}
2018-05-06 23:21:36 +08:00
init_module!(
CSV_MODULE,
CSVTab,
2018-05-07 00:05:02 +08:00
(),
2018-05-06 23:21:36 +08:00
CSVTabCursor,
csv_create,
csv_connect,
csv_best_index,
csv_disconnect,
csv_disconnect,
csv_open,
csv_close,
csv_filter,
csv_next,
csv_eof,
csv_column,
csv_rowid
);
2016-08-13 17:54:19 +08:00
/// An instance of the CSV virtual table
#[repr(C)]
struct CSVTab {
2016-08-13 17:54:19 +08:00
/// Base class. Must be first
base: ffi::sqlite3_vtab,
2016-08-13 17:54:19 +08:00
/// Name of the CSV file
filename: String,
has_headers: bool,
delimiter: u8,
quote: u8,
2016-08-13 17:54:19 +08:00
/// Offset to start of data
2018-05-13 18:21:58 +08:00
offset_first_row: csv::Position,
}
impl CSVTab {
fn reader(&self) -> result::Result<csv::Reader<File>, csv::Error> {
2018-05-13 18:21:58 +08:00
csv::ReaderBuilder::new()
.has_headers(self.has_headers)
.delimiter(self.delimiter)
.quote(self.quote)
.from_path(&self.filename)
}
2018-05-06 18:41:42 +08:00
fn parameter(c_slice: &[u8]) -> Result<(&str, &str)> {
let arg = try!(str::from_utf8(c_slice)).trim();
let mut split = arg.split('=');
if let Some(key) = split.next() {
if let Some(value) = split.next() {
let param = key.trim();
let value = dequote(value);
return Ok((param, value));
}
}
Err(Error::ModuleError(format!("illegal argument: '{}'", arg)))
}
fn parse_byte(arg: &str) -> Option<u8> {
if arg.len() == 1 {
arg.bytes().next()
} else {
None
}
}
}
impl VTab for CSVTab {
2018-05-07 00:05:02 +08:00
type Aux = ();
type Cursor = CSVTabCursor;
2018-05-07 00:05:02 +08:00
unsafe fn connect(db: *mut ffi::sqlite3, _aux: *mut (), args: &[&[u8]]) -> Result<CSVTab> {
2016-02-11 03:30:08 +08:00
if args.len() < 4 {
2016-04-02 23:16:17 +08:00
return Err(Error::ModuleError("no CSV file specified".to_owned()));
2016-02-11 01:07:58 +08:00
}
2018-05-06 18:41:42 +08:00
let mut vtab = CSVTab {
base: Default::default(),
2018-05-06 18:41:42 +08:00
filename: "".to_owned(),
has_headers: false,
delimiter: b',',
quote: b'"',
2018-05-13 18:21:58 +08:00
offset_first_row: csv::Position::new(),
};
2018-05-06 18:41:42 +08:00
let mut schema = None;
let mut n_col = None;
2016-02-11 03:30:08 +08:00
2018-05-06 18:41:42 +08:00
let args = &args[3..];
2016-08-13 17:54:19 +08:00
for c_slice in args {
2018-05-06 18:41:42 +08:00
let (param, value) = try!(CSVTab::parameter(c_slice));
match param {
"filename" => {
if !Path::new(value).exists() {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(format!(
"file '{}' does not exist",
value
)));
2018-05-06 18:41:42 +08:00
}
vtab.filename = value.to_owned();
2018-05-06 23:21:36 +08:00
}
2018-05-06 18:41:42 +08:00
"schema" => {
schema = Some(value.to_owned());
2018-05-06 23:21:36 +08:00
}
2018-05-06 18:41:42 +08:00
"columns" => {
if let Ok(n) = value.parse::<u16>() {
if n_col.is_some() {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(
"more than one 'columns' parameter".to_owned(),
));
2018-05-06 18:41:42 +08:00
} else if n == 0 {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(
"must have at least one column".to_owned(),
));
2018-05-06 18:41:42 +08:00
}
n_col = Some(n);
} else {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(format!(
"unrecognized argument to 'columns': {}",
value
)));
2018-05-06 18:41:42 +08:00
}
2018-05-06 23:21:36 +08:00
}
2018-05-06 18:41:42 +08:00
"header" => {
if let Some(b) = parse_boolean(value) {
vtab.has_headers = b;
} else {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(format!(
"unrecognized argument to 'header': {}",
value
)));
2018-05-06 18:41:42 +08:00
}
2018-05-06 23:21:36 +08:00
}
2018-05-06 18:41:42 +08:00
"delimiter" => {
if let Some(b) = CSVTab::parse_byte(value) {
vtab.delimiter = b;
} else {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(format!(
"unrecognized argument to 'delimiter': {}",
value
)));
2018-05-06 18:41:42 +08:00
}
2018-05-06 23:21:36 +08:00
}
2018-05-06 18:41:42 +08:00
"quote" => {
if let Some(b) = CSVTab::parse_byte(value) {
if b == b'0' {
vtab.quote = 0;
} else {
vtab.quote = b;
}
} else {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(format!(
"unrecognized argument to 'quote': {}",
value
)));
2018-05-06 18:41:42 +08:00
}
2018-05-06 23:21:36 +08:00
}
2018-05-06 18:41:42 +08:00
_ => {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(format!(
"unrecognized parameter '{}'",
param
)));
}
2016-02-11 03:30:08 +08:00
}
}
2018-05-06 18:41:42 +08:00
if vtab.filename.is_empty() {
return Err(Error::ModuleError("no CSV file specified".to_owned()));
}
let mut cols: Vec<String> = Vec::new();
if vtab.has_headers || (n_col.is_none() && schema.is_none()) {
let mut reader = try!(vtab.reader());
2018-05-06 18:41:42 +08:00
if vtab.has_headers {
2018-05-13 18:21:58 +08:00
{
let headers = try!(reader.headers());
// headers ignored if cols is not empty
if n_col.is_none() && schema.is_none() {
cols = headers
.into_iter()
.map(|header| escape_double_quote(&header).into_owned())
.collect();
}
2018-05-06 18:41:42 +08:00
}
2018-05-13 18:21:58 +08:00
vtab.offset_first_row = reader.position().clone();
2018-05-06 18:41:42 +08:00
} else {
2018-05-13 18:21:58 +08:00
let mut record = csv::ByteRecord::new();
if try!(reader.read_byte_record(&mut record)) {
2018-05-14 01:16:12 +08:00
for (i, _) in record.iter().enumerate() {
cols.push(format!("c{}", i));
2018-05-13 18:21:58 +08:00
}
2018-05-06 18:41:42 +08:00
}
2016-02-11 03:30:08 +08:00
}
2018-05-15 01:23:17 +08:00
} else if let Some(n_col) = n_col {
for i in 0..n_col {
cols.push(format!("c{}", i));
}
2016-02-11 03:30:08 +08:00
}
2018-05-06 18:41:42 +08:00
if cols.is_empty() && schema.is_none() {
return Err(Error::ModuleError("no column specified".to_owned()));
2016-02-11 03:30:08 +08:00
}
2018-05-06 18:41:42 +08:00
if schema.is_none() {
let mut sql = String::from("CREATE TABLE x(");
for (i, col) in cols.iter().enumerate() {
sql.push('"');
sql.push_str(col);
sql.push_str("\" TEXT");
if i == cols.len() - 1 {
sql.push_str(");");
} else {
sql.push_str(", ");
}
2016-02-11 03:30:08 +08:00
}
2018-05-06 18:41:42 +08:00
schema = Some(sql);
2016-02-11 03:30:08 +08:00
}
2018-05-06 18:41:42 +08:00
try!(declare_vtab(db, &schema.unwrap()));
Ok(vtab)
}
2018-05-06 18:41:42 +08:00
// Only a forward full table scan is supported.
fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
2018-05-14 01:16:12 +08:00
info.set_estimated_cost(1_000_000.);
2016-08-14 15:44:37 +08:00
Ok(())
}
fn open(&self) -> Result<CSVTabCursor> {
Ok(CSVTabCursor::new(try!(self.reader())))
}
}
2016-08-13 17:54:19 +08:00
/// A cursor for the CSV virtual table
#[repr(C)]
struct CSVTabCursor {
2016-08-13 17:54:19 +08:00
/// Base class. Must be first
base: ffi::sqlite3_vtab_cursor,
2016-08-13 17:54:19 +08:00
/// The CSV reader object
reader: csv::Reader<File>,
2018-05-06 18:41:42 +08:00
/// Current cursor position used as rowid
row_number: usize,
2018-05-06 18:41:42 +08:00
/// Values of the current row
2018-05-13 18:21:58 +08:00
cols: csv::StringRecord,
eof: bool,
}
impl CSVTabCursor {
fn new(reader: csv::Reader<File>) -> CSVTabCursor {
CSVTabCursor {
base: Default::default(),
2018-05-06 14:45:56 +08:00
reader,
row_number: 0,
2018-05-13 18:21:58 +08:00
cols: csv::StringRecord::new(),
eof: false,
}
}
2016-02-11 01:15:46 +08:00
}
impl VTabCursor for CSVTabCursor {
type Table = CSVTab;
2018-05-06 14:45:56 +08:00
fn vtab(&self) -> &CSVTab {
2018-05-06 23:21:36 +08:00
unsafe { &*(self.base.pVtab as *const CSVTab) }
}
2018-05-06 18:41:42 +08:00
// Only a full table scan is supported. So `filter` simply rewinds to
// the beginning.
2018-05-06 23:21:36 +08:00
fn filter(&mut self, _idx_num: c_int, _idx_str: Option<&str>, _args: &Values) -> Result<()> {
{
2018-05-13 18:21:58 +08:00
let offset_first_row = self.vtab().offset_first_row.clone();
try!(self.reader.seek(offset_first_row));
}
self.row_number = 0;
self.next()
}
fn next(&mut self) -> Result<()> {
2016-02-11 03:48:30 +08:00
{
2018-05-13 18:21:58 +08:00
self.eof = self.reader.is_done();
if self.eof {
return Ok(());
2016-02-11 03:48:30 +08:00
}
2018-05-13 18:21:58 +08:00
self.eof = !try!(self.reader.read_record(&mut self.cols));
}
2016-02-11 03:48:30 +08:00
2016-08-13 19:55:30 +08:00
self.row_number += 1;
Ok(())
}
fn eof(&self) -> bool {
self.eof
}
fn column(&self, ctx: &mut Context, col: c_int) -> Result<()> {
if col < 0 || col as usize >= self.cols.len() {
2018-05-06 23:21:36 +08:00
return Err(Error::ModuleError(format!(
"column index out of bounds: {}",
col
)));
2016-02-11 01:07:58 +08:00
}
if self.cols.is_empty() {
2016-08-13 23:46:49 +08:00
ctx.set_result(&Null);
2016-02-11 01:07:58 +08:00
return Ok(());
}
// TODO Affinity
2018-05-13 18:21:58 +08:00
ctx.set_result(&self.cols[col as usize].to_owned());
Ok(())
}
fn rowid(&self) -> Result<i64> {
Ok(self.row_number as i64)
}
}
impl From<csv::Error> for Error {
fn from(err: csv::Error) -> Error {
use std::error::Error as StdError;
Error::ModuleError(String::from(err.description()))
}
}
#[cfg(test)]
mod test {
use vtab::csvtab;
2018-05-06 23:21:36 +08:00
use {Connection, Result};
#[test]
fn test_csv_module() {
let db = Connection::open_in_memory().unwrap();
csvtab::load_module(&db).unwrap();
2018-05-06 23:21:36 +08:00
db.execute_batch("CREATE VIRTUAL TABLE vtab USING csv(filename='test.csv', header=yes)")
.unwrap();
{
let mut s = db.prepare("SELECT rowid, * FROM vtab").unwrap();
{
let headers = s.column_names();
assert_eq!(vec!["rowid", "colA", "colB", "colC"], headers);
}
2018-05-06 23:21:36 +08:00
let ids: Result<Vec<i32>> = s.query_map(&[], |row| row.get::<i32, i32>(0))
.unwrap()
.collect();
let sum = ids.unwrap().iter().fold(0, |acc, &id| acc + id);
assert_eq!(sum, 15);
}
db.execute_batch("DROP TABLE vtab").unwrap();
}
#[test]
fn test_csv_cursor() {
let db = Connection::open_in_memory().unwrap();
csvtab::load_module(&db).unwrap();
2018-05-06 23:21:36 +08:00
db.execute_batch("CREATE VIRTUAL TABLE vtab USING csv(filename='test.csv', header=yes)")
.unwrap();
{
2018-05-06 23:21:36 +08:00
let mut s = db.prepare(
"SELECT v1.rowid, v1.* FROM vtab v1 NATURAL JOIN vtab v2 WHERE \
v1.rowid < v2.rowid",
).unwrap();
let mut rows = s.query(&[]).unwrap();
let row = rows.next().unwrap().unwrap();
assert_eq!(row.get::<i32, i32>(0), 2);
}
db.execute_batch("DROP TABLE vtab").unwrap();
}
}