├── .gitignore ├── src ├── lib.rs ├── loader.rs └── main.rs ├── .github ├── dependabot.yml └── workflows │ ├── binaries.yml │ └── main.yml ├── Cargo.toml ├── LICENSE ├── README.rst └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod loader; 2 | 3 | pub use crate::loader::{CsvLoader, ExactSizeIterable, Loader}; 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | allow: 8 | # Also update indirect dependencies 9 | - dependency-type: all 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "csvsql" 3 | version = "0.1.0" 4 | authors = ["Alex Gaynor "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | csv = "1.4" 9 | indicatif = "0.18" 10 | comfy-table = "7.2" 11 | regex = "1.12" 12 | rusqlite = { version = "0.37", features = ["functions"] } 13 | rustyline = "17" 14 | clap = { version = "4.5.51", features = ["derive"] } 15 | dirs = "6.0" 16 | anyhow = "1.0.100" 17 | -------------------------------------------------------------------------------- /.github/workflows/binaries.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | push: 4 | branches: master 5 | 6 | name: Build Binaries 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: 13 | - name: macos-latest 14 | binary: csvsql 15 | - name: macos-13 16 | binary: csvsql 17 | - name: ubuntu-latest 18 | binary: csvsql 19 | - name: windows-latest 20 | binary: csvsql.exe 21 | runs-on: ${{ matrix.os.name }} 22 | name: "${{ matrix.os.name }}" 23 | 24 | steps: 25 | - uses: actions/checkout@v1 26 | 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | override: true 32 | 33 | - uses: actions-rs/cargo@v1 34 | with: 35 | command: build 36 | args: --release --features rusqlite/bundled 37 | 38 | - uses: actions/upload-artifact@v4 39 | with: 40 | name: csvsql-${{ matrix.os.name }} 41 | path: target/release/${{ matrix.os.binary }} 42 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | push: 4 | branches: master 5 | 6 | name: Continuous integration 7 | 8 | jobs: 9 | ci: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust: 14 | - stable 15 | - beta 16 | - nightly 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | profile: minimal 24 | toolchain: ${{ matrix.rust }} 25 | override: true 26 | components: rustfmt, clippy 27 | 28 | - run: sudo apt update && sudo apt install libsqlite3-dev 29 | 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: build 33 | 34 | - uses: actions-rs/cargo@v1 35 | with: 36 | command: test 37 | 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | command: fmt 41 | args: --all -- --check 42 | 43 | - uses: actions-rs/cargo@v1 44 | with: 45 | command: clippy 46 | args: -- -D warnings 47 | 48 | - uses: actions-rs/audit-check@v1 49 | with: 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Alex Gaynor and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of csv-sql nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | CSV SQL 2 | ======= 3 | 4 | Take a CSV file, query it with SQL. Magic! 5 | 6 | .. code-block:: console 7 | 8 | $ cargo run file.csv 9 | Loaded 3162 rows into t(domain, base_domain, agency, sslv2) 10 | > SELECT COUNT(*) FROM t 11 | +----------+ 12 | | 3162 | 13 | +----------+ 14 | 15 | All your rows go into a table named ``t``. It's great! 16 | 17 | You can also specify multiple files: 18 | 19 | .. code-block:: console 20 | 21 | $ cargo run file1.csv file2.csv 22 | Loaded 12 rows into t1(some, schema) 23 | Loaded 74 rows into t2(some, other, schema) 24 | > 25 | 26 | If you'd like to export the results of a query to a CSV file: 27 | 28 | .. code-block:: console 29 | 30 | $ cargo run file.csv 31 | Loaded 3162 rows into t(domain, base_domain, agency, sslv2) 32 | > .export(results.csv) SELECT COUNT(*) from t; 33 | 34 | If you have tab-, pipe-, or semicolon-delimited files you can specify `--tab`, 35 | `--pipe`, or `--semicolon` respectively). 36 | 37 | You can change the output style to be "vertical" instead of "table" with: 38 | 39 | .. code-block:: console 40 | 41 | $ cargo run file.csv 42 | Loaded 3162 rows into t(domain, base_domain, agency, sslv2) 43 | > .style(table) 44 | > -- Or 45 | > .style(vertical) 46 | 47 | UDFs 48 | ---- 49 | 50 | ``csv-sql`` contains additional UDFs (User Defined Functions) to enable easier 51 | data analysis. They are: 52 | 53 | `regexp_extract(pattern, value, replacement)` 54 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 55 | 56 | Example: ``regexp_extract("abc-(\d+)", "abc-12345", "lol $1")`` returns ``"lol 12345"`` 57 | 58 | Binaries 59 | -------- 60 | 61 | Binaries for macOS and Windows are automatically built in CI and can be 62 | downloaded from Github Actions. 63 | -------------------------------------------------------------------------------- /src/loader.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::iter; 3 | 4 | pub trait ExactSizeIterable { 5 | fn iter(&self) -> impl iter::ExactSizeIterator; 6 | } 7 | 8 | impl ExactSizeIterable for csv::ByteRecord { 9 | fn iter(&self) -> impl iter::ExactSizeIterator { 10 | self.into_iter() 11 | } 12 | } 13 | 14 | pub trait Loader { 15 | type RecordType: ExactSizeIterable; 16 | 17 | /// Name of the resource we're loading from (e.g., a file path). 18 | fn name(&self) -> &str; 19 | 20 | /// Returns the size of the data of the loader, in unspecified units. 21 | /// Should be used for showing progress bars and similar. 22 | fn progress_size(&self) -> u64; 23 | 24 | /// Returns the current position of the loader relative to `progress_size`, 25 | /// in unspecified units. 26 | /// Should be used for showing progress bars and similar. 27 | fn progress_position(&self) -> u64; 28 | 29 | /// Returns the names of fields, as they exist in the underlying data. 30 | fn raw_fields(&mut self) -> anyhow::Result>; 31 | 32 | fn next_record(&mut self) -> Option>; 33 | } 34 | 35 | pub struct CsvLoader<'a> { 36 | path: &'a str, 37 | records: csv::ByteRecordsIntoIter, 38 | } 39 | 40 | impl<'a> CsvLoader<'a> { 41 | pub fn new(path: &'a str, delimiter: u8) -> anyhow::Result { 42 | let f = File::open(path)?; 43 | 44 | let reader = csv::ReaderBuilder::new() 45 | .flexible(true) 46 | .delimiter(delimiter) 47 | .from_reader(f); 48 | 49 | Ok(CsvLoader { 50 | path, 51 | records: reader.into_byte_records(), 52 | }) 53 | } 54 | } 55 | 56 | impl Loader for CsvLoader<'_> { 57 | type RecordType = csv::ByteRecord; 58 | 59 | fn name(&self) -> &str { 60 | self.path 61 | } 62 | 63 | fn progress_size(&self) -> u64 { 64 | self.records.reader().get_ref().metadata().unwrap().len() 65 | } 66 | 67 | fn progress_position(&self) -> u64 { 68 | self.records.reader().position().byte() 69 | } 70 | 71 | fn raw_fields(&mut self) -> anyhow::Result> { 72 | Ok(self.records.reader_mut().headers()?.iter()) 73 | } 74 | 75 | fn next_record(&mut self) -> Option> { 76 | match self.records.next() { 77 | Some(Ok(v)) => Some(Ok(v)), 78 | Some(Err(e)) => Some(Err(e.into())), 79 | None => None, 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::sync::LazyLock; 3 | 4 | use csvsql::ExactSizeIterable; 5 | 6 | fn normalize_col(col: &str) -> String { 7 | static RE: LazyLock = LazyLock::new(|| regex::Regex::new(r"\(.*?\)$").unwrap()); 8 | 9 | let mut col = RE 10 | .replace_all(col, "") 11 | .to_lowercase() 12 | .trim() 13 | .replace(['(', ')'], "") 14 | .replace([' ', '.', '-', '/'], "_") 15 | .replace('?', "") 16 | .replace([',', '&'], "_") 17 | .replace([':', '#'], ""); 18 | if !col.chars().next().map(char::is_alphabetic).unwrap_or(true) { 19 | col = format!("c_{col}") 20 | } 21 | col 22 | } 23 | 24 | fn _create_table(db: &rusqlite::Connection, table_name: &str, cols: &[String]) { 25 | let create_columns = cols 26 | .iter() 27 | .map(|c| format!("\"{c}\" varchar")) 28 | .collect::>() 29 | .join(", "); 30 | db.execute( 31 | &format!("CREATE TABLE {table_name} ({create_columns})"), 32 | &[] as &[&dyn rusqlite::types::ToSql], 33 | ) 34 | .unwrap(); 35 | } 36 | 37 | fn _load_table_from_path( 38 | db: &mut rusqlite::Connection, 39 | table_name: &str, 40 | path: &str, 41 | delimiter: u8, 42 | ) -> anyhow::Result> { 43 | let loader = csvsql::CsvLoader::new(path, delimiter)?; 44 | 45 | _load_table_from_loader(db, table_name, loader) 46 | } 47 | 48 | fn _load_table_from_loader( 49 | db: &mut rusqlite::Connection, 50 | table_name: &str, 51 | mut loader: impl csvsql::Loader, 52 | ) -> anyhow::Result> { 53 | let mut num_rows = 0; 54 | let progress_size = loader.progress_size(); 55 | 56 | let normalized_cols = 57 | loader 58 | .raw_fields()? 59 | .map(normalize_col) 60 | .fold(vec![], |mut v, orig_col| { 61 | let mut col = orig_col.clone(); 62 | let mut i = 1; 63 | while v.contains(&col) { 64 | col = format!("{orig_col}_{i}"); 65 | i += 1 66 | } 67 | v.push(col); 68 | v 69 | }); 70 | _create_table(db, table_name, &normalized_cols); 71 | 72 | let insert_query = format!( 73 | "INSERT INTO {} VALUES ({})", 74 | table_name, 75 | normalized_cols 76 | .iter() 77 | .map(|_| "?") 78 | .collect::>() 79 | .join(", ") 80 | ); 81 | let pb = indicatif::ProgressBar::new(progress_size); 82 | pb.set_style( 83 | indicatif::ProgressStyle::default_bar() 84 | .template("[{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")? 85 | .progress_chars("#>-"), 86 | ); 87 | let tx = db.transaction().unwrap(); 88 | { 89 | let mut stmt = tx.prepare(&insert_query).expect("tx.prepare() failed"); 90 | while let Some(record) = loader.next_record() { 91 | let record = record?; 92 | let row = record.iter(); 93 | let row_len = row.len(); 94 | if row_len > normalized_cols.len() { 95 | anyhow::bail!( 96 | "Too many fields on row {}, fields: {:?}", 97 | num_rows + 1, 98 | row.collect::>() 99 | ); 100 | } 101 | 102 | stmt.execute(rusqlite::params_from_iter( 103 | row.chain(std::iter::repeat_n( 104 | &b""[..], 105 | normalized_cols.len() - row_len, 106 | )) 107 | .map(String::from_utf8_lossy), 108 | )) 109 | .unwrap(); 110 | 111 | num_rows += 1; 112 | if num_rows % 10000 == 0 { 113 | pb.set_position(loader.progress_position()); 114 | } 115 | } 116 | } 117 | tx.commit().unwrap(); 118 | pb.finish(); 119 | 120 | println!( 121 | "Loaded {} rows into {}({}) from {:?}", 122 | num_rows, 123 | table_name, 124 | normalized_cols.join(", "), 125 | loader.name(), 126 | ); 127 | Ok(normalized_cols) 128 | } 129 | 130 | struct FromAnySqlType { 131 | value: String, 132 | } 133 | 134 | impl rusqlite::types::FromSql for FromAnySqlType { 135 | fn column_result( 136 | value: rusqlite::types::ValueRef<'_>, 137 | ) -> Result { 138 | let result = match value { 139 | rusqlite::types::ValueRef::Null => "null".to_string(), 140 | rusqlite::types::ValueRef::Integer(v) => v.to_string(), 141 | rusqlite::types::ValueRef::Real(v) => v.to_string(), 142 | rusqlite::types::ValueRef::Blob(v) | rusqlite::types::ValueRef::Text(v) => { 143 | String::from_utf8(v.to_vec()).unwrap() 144 | } 145 | }; 146 | Ok(FromAnySqlType { value: result }) 147 | } 148 | } 149 | 150 | fn _prepare_query<'a>( 151 | conn: &'a rusqlite::Connection, 152 | query: &str, 153 | ) -> anyhow::Result> { 154 | Ok(conn.prepare(query)?) 155 | } 156 | 157 | fn _handle_query( 158 | conn: &rusqlite::Connection, 159 | line: &str, 160 | style: &OutputStyle, 161 | ) -> anyhow::Result<()> { 162 | let mut stmt = _prepare_query(conn, line)?; 163 | let col_count = stmt.column_count(); 164 | let col_names = stmt 165 | .column_names() 166 | .into_iter() 167 | .map(|s| s.to_owned()) 168 | .collect::>(); 169 | let mut results = stmt.query(&[] as &[&dyn rusqlite::types::ToSql]).unwrap(); 170 | 171 | match style { 172 | OutputStyle::Table => { 173 | let mut table = comfy_table::Table::new(); 174 | table.load_preset("││──╞═╪╡┆ ┬┴┌┐└┘"); 175 | table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic); 176 | let mut title_row = comfy_table::Row::new(); 177 | for col in col_names { 178 | title_row.add_cell(comfy_table::Cell::new(col)); 179 | } 180 | table.set_header(title_row); 181 | 182 | while let Ok(Some(r)) = results.next() { 183 | let mut row = comfy_table::Row::new(); 184 | for i in 0..col_count { 185 | let cell: FromAnySqlType = r.get(i).unwrap(); 186 | row.add_cell(comfy_table::Cell::new(cell.value)); 187 | } 188 | table.add_row(row); 189 | } 190 | println!("{table}"); 191 | } 192 | OutputStyle::Vertical => { 193 | let max_col_length = col_names.iter().map(|c| c.len()).max().unwrap(); 194 | let mut record_number = 1; 195 | while let Ok(Some(r)) = results.next() { 196 | println!("------ [ RECORD {record_number} ] ------"); 197 | for (i, name) in col_names.iter().enumerate() { 198 | let cell: FromAnySqlType = r.get(i).unwrap(); 199 | println!( 200 | "{field:width$} | {value}", 201 | field = name, 202 | width = max_col_length, 203 | value = cell.value 204 | ); 205 | } 206 | record_number += 1; 207 | } 208 | } 209 | } 210 | 211 | Ok(()) 212 | } 213 | 214 | fn _handle_export(conn: &rusqlite::Connection, line: &str) -> anyhow::Result<()> { 215 | static RE: LazyLock = 216 | LazyLock::new(|| regex::Regex::new(r"^\.export\(([\w_\-\./]+)\) (.*)").unwrap()); 217 | 218 | let caps = RE 219 | .captures(line) 220 | .ok_or_else(|| anyhow::anyhow!("Must match `.export(file-name) SQL`"))?; 221 | let destination_path = &caps[1]; 222 | let query = &caps[2]; 223 | 224 | let mut stmt = _prepare_query(conn, query)?; 225 | let col_count = stmt.column_count(); 226 | 227 | let mut writer = csv::Writer::from_path(destination_path)?; 228 | writer.write_record(stmt.column_names()).unwrap(); 229 | 230 | let mut results = stmt.query(&[] as &[&dyn rusqlite::types::ToSql]).unwrap(); 231 | while let Ok(Some(r)) = results.next() { 232 | writer 233 | .write_record((0..col_count).map(|i| { 234 | let cell: FromAnySqlType = r.get(i).unwrap(); 235 | cell.value 236 | })) 237 | .unwrap(); 238 | } 239 | 240 | Ok(()) 241 | } 242 | 243 | fn _process_query(conn: &rusqlite::Connection, line: &str, style: &mut OutputStyle) { 244 | let result = if line.starts_with(".export") { 245 | _handle_export(conn, line) 246 | } else if line.starts_with(".schema") { 247 | _handle_query( 248 | conn, 249 | "SELECT sql AS schema FROM sqlite_master WHERE name like 't%'", 250 | style, 251 | ) 252 | } else if line.starts_with(".style") { 253 | static RE: LazyLock = 254 | LazyLock::new(|| regex::Regex::new(r"^\.style\((table|vertical)\)").unwrap()); 255 | 256 | match RE.captures(line).as_ref().map(|caps| &caps[1]) { 257 | Some("table") => { 258 | *style = OutputStyle::Table; 259 | Ok(()) 260 | } 261 | Some("vertical") => { 262 | *style = OutputStyle::Vertical; 263 | Ok(()) 264 | } 265 | _ => Err(anyhow::anyhow!("Must match `.style(table|vertical)`")), 266 | } 267 | } else { 268 | _handle_query(conn, line, style) 269 | }; 270 | if let Err(e) = result { 271 | println!("{e:?}"); 272 | } 273 | } 274 | 275 | fn install_udfs(c: &rusqlite::Connection) -> anyhow::Result<()> { 276 | c.create_scalar_function( 277 | "regexp_extract", 278 | 3, 279 | rusqlite::functions::FunctionFlags::SQLITE_UTF8 280 | | rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC, 281 | |ctx| { 282 | let re = ctx.get_or_create_aux( 283 | 0, 284 | |vr| -> Result<_, Box> { 285 | Ok(regex::Regex::new(vr.as_str()?)?) 286 | }, 287 | )?; 288 | let value = ctx.get::>(1)?; 289 | let replacement = ctx.get::>(2)?; 290 | 291 | let caps = match re.captures(&value) { 292 | Some(caps) => caps, 293 | None => return Ok("".to_string()), 294 | }; 295 | let mut dest = String::new(); 296 | caps.expand(&replacement, &mut dest); 297 | Ok(dest) 298 | }, 299 | )?; 300 | c.create_scalar_function( 301 | "regexp", 302 | 2, 303 | rusqlite::functions::FunctionFlags::SQLITE_UTF8 304 | | rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC, 305 | |ctx| { 306 | let re = ctx.get_or_create_aux( 307 | 0, 308 | |vr| -> Result<_, Box> { 309 | Ok(regex::Regex::new(vr.as_str()?)?) 310 | }, 311 | )?; 312 | let value = ctx.get::>(1)?; 313 | Ok(re.is_match(&value)) 314 | }, 315 | )?; 316 | 317 | Ok(()) 318 | } 319 | 320 | struct SimpleWordCompleter { 321 | words: Vec, 322 | } 323 | 324 | static BREAK_CHARS: [char; 5] = [' ', '(', ')', ',', '.']; 325 | impl SimpleWordCompleter { 326 | fn new(words: Vec) -> SimpleWordCompleter { 327 | SimpleWordCompleter { words } 328 | } 329 | } 330 | 331 | impl rustyline::Helper for SimpleWordCompleter {} 332 | 333 | impl rustyline::hint::Hinter for SimpleWordCompleter { 334 | type Hint = String; 335 | 336 | fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { 337 | None 338 | } 339 | } 340 | 341 | impl rustyline::highlight::Highlighter for SimpleWordCompleter {} 342 | 343 | impl rustyline::validate::Validator for SimpleWordCompleter {} 344 | 345 | impl rustyline::completion::Completer for SimpleWordCompleter { 346 | type Candidate = String; 347 | 348 | fn complete( 349 | &self, 350 | line: &str, 351 | pos: usize, 352 | _ctx: &rustyline::Context<'_>, 353 | ) -> rustyline::Result<(usize, Vec)> { 354 | let (start, word) = 355 | rustyline::completion::extract_word(line, pos, None, |c| BREAK_CHARS.contains(&c)); 356 | 357 | let matches = self 358 | .words 359 | .iter() 360 | .filter(|w| w.starts_with(word)) 361 | .cloned() 362 | .collect(); 363 | Ok((start, matches)) 364 | } 365 | } 366 | 367 | #[derive(clap::Parser)] 368 | struct Opts { 369 | #[clap(long, help = "Use ',' as the delimiter for the CSV")] 370 | comma: bool, 371 | #[clap(long, help = "Use '|' as the delimiter for the CSV")] 372 | pipe: bool, 373 | #[clap(long, help = "Use '\\t' as the delimiter for the CSV")] 374 | tab: bool, 375 | #[clap(long, help = "Use ';' as the delimiter for the CSV")] 376 | semicolon: bool, 377 | 378 | #[clap()] 379 | paths: Vec, 380 | } 381 | 382 | enum OutputStyle { 383 | Table, 384 | Vertical, 385 | } 386 | 387 | fn main() -> anyhow::Result<()> { 388 | let opts = Opts::parse(); 389 | 390 | let delim = match (opts.comma, opts.pipe, opts.tab, opts.semicolon) { 391 | (true, false, false, false) | (false, false, false, false) => b',', 392 | (false, true, false, false) => b'|', 393 | (false, false, true, false) => b'\t', 394 | (false, false, false, true) => b';', 395 | _ => { 396 | eprintln!("Can't pass more than one of --comma, --pipe, and --tab"); 397 | std::process::exit(1); 398 | } 399 | }; 400 | 401 | let mut conn = rusqlite::Connection::open_in_memory().unwrap(); 402 | 403 | let mut base_words = [ 404 | // keywords 405 | "distinct", 406 | "select", 407 | "from", 408 | "group", 409 | "by", 410 | "order", 411 | "where", 412 | "count", 413 | "limit", 414 | "offset", 415 | // functions 416 | "length", 417 | "coalesce", 418 | "regexp_extract", 419 | "group_concat", 420 | // csv-sql commands 421 | "export", 422 | "schema", 423 | "style", 424 | "table", 425 | "vertical", 426 | ] 427 | .iter() 428 | .map(|&s| s.to_string()) 429 | .collect::>(); 430 | 431 | if opts.paths.len() == 1 { 432 | let mut col_names = _load_table_from_path(&mut conn, "t", &opts.paths[0], delim)?; 433 | base_words.append(&mut col_names); 434 | } else { 435 | for (idx, path) in opts.paths.iter().enumerate() { 436 | let mut col_names = 437 | _load_table_from_path(&mut conn, &format!("t{}", idx + 1), path, delim)?; 438 | base_words.append(&mut col_names); 439 | } 440 | } 441 | 442 | install_udfs(&conn)?; 443 | 444 | let mut style = OutputStyle::Table; 445 | let completer = SimpleWordCompleter::new(base_words); 446 | let mut rl = rustyline::Editor::new()?; 447 | rl.set_helper(Some(completer)); 448 | let history_path = dirs::home_dir().unwrap().join(".csv-sql-history"); 449 | let _ = rl.load_history(&history_path); 450 | loop { 451 | match rl.readline("> ") { 452 | Ok(line) => { 453 | if line.trim().is_empty() { 454 | continue; 455 | } 456 | _process_query(&conn, &line, &mut style); 457 | let _ = rl.add_history_entry(line); 458 | } 459 | Err(rustyline::error::ReadlineError::Interrupted) => { 460 | println!("Interrupted"); 461 | continue; 462 | } 463 | Err(rustyline::error::ReadlineError::Eof) => { 464 | break; 465 | } 466 | Err(err) => { 467 | println!("Error: {err}"); 468 | break; 469 | } 470 | } 471 | } 472 | rl.save_history(&history_path).unwrap(); 473 | 474 | Ok(()) 475 | } 476 | 477 | #[cfg(test)] 478 | mod test { 479 | use super::normalize_col; 480 | 481 | #[test] 482 | fn test_normalize_col() { 483 | for (value, expected) in &[ 484 | ("", ""), 485 | ("abc", "abc"), 486 | ("(S)AO", "sao"), 487 | ("abc (123)", "abc"), 488 | ("2/6/2000", "c_2_6_2000"), 489 | ("COMBO#", "combo"), 490 | ] { 491 | assert_eq!(&&normalize_col(value), expected); 492 | } 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.21" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.13" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.7" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.4" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 49 | dependencies = [ 50 | "windows-sys 0.60.2", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.10" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys 0.60.2", 62 | ] 63 | 64 | [[package]] 65 | name = "anyhow" 66 | version = "1.0.100" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 69 | 70 | [[package]] 71 | name = "bitflags" 72 | version = "2.10.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 75 | 76 | [[package]] 77 | name = "bumpalo" 78 | version = "3.19.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 81 | 82 | [[package]] 83 | name = "cfg-if" 84 | version = "1.0.4" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 87 | 88 | [[package]] 89 | name = "cfg_aliases" 90 | version = "0.2.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 93 | 94 | [[package]] 95 | name = "clap" 96 | version = "4.5.51" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" 99 | dependencies = [ 100 | "clap_builder", 101 | "clap_derive", 102 | ] 103 | 104 | [[package]] 105 | name = "clap_builder" 106 | version = "4.5.51" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" 109 | dependencies = [ 110 | "anstream", 111 | "anstyle", 112 | "clap_lex", 113 | "strsim", 114 | ] 115 | 116 | [[package]] 117 | name = "clap_derive" 118 | version = "4.5.49" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" 121 | dependencies = [ 122 | "heck", 123 | "proc-macro2", 124 | "quote", 125 | "syn", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_lex" 130 | version = "0.7.6" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 133 | 134 | [[package]] 135 | name = "clipboard-win" 136 | version = "5.4.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" 139 | dependencies = [ 140 | "error-code", 141 | ] 142 | 143 | [[package]] 144 | name = "colorchoice" 145 | version = "1.0.4" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 148 | 149 | [[package]] 150 | name = "comfy-table" 151 | version = "7.2.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" 154 | dependencies = [ 155 | "crossterm", 156 | "unicode-segmentation", 157 | "unicode-width", 158 | ] 159 | 160 | [[package]] 161 | name = "console" 162 | version = "0.16.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" 165 | dependencies = [ 166 | "encode_unicode", 167 | "libc", 168 | "once_cell", 169 | "unicode-width", 170 | "windows-sys 0.61.0", 171 | ] 172 | 173 | [[package]] 174 | name = "crossterm" 175 | version = "0.29.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 178 | dependencies = [ 179 | "bitflags", 180 | "crossterm_winapi", 181 | "document-features", 182 | "parking_lot", 183 | "rustix", 184 | "winapi", 185 | ] 186 | 187 | [[package]] 188 | name = "crossterm_winapi" 189 | version = "0.9.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 192 | dependencies = [ 193 | "winapi", 194 | ] 195 | 196 | [[package]] 197 | name = "csv" 198 | version = "1.4.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" 201 | dependencies = [ 202 | "csv-core", 203 | "itoa", 204 | "ryu", 205 | "serde_core", 206 | ] 207 | 208 | [[package]] 209 | name = "csv-core" 210 | version = "0.1.13" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" 213 | dependencies = [ 214 | "memchr", 215 | ] 216 | 217 | [[package]] 218 | name = "csvsql" 219 | version = "0.1.0" 220 | dependencies = [ 221 | "anyhow", 222 | "clap", 223 | "comfy-table", 224 | "csv", 225 | "dirs", 226 | "indicatif", 227 | "regex", 228 | "rusqlite", 229 | "rustyline", 230 | ] 231 | 232 | [[package]] 233 | name = "dirs" 234 | version = "6.0.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 237 | dependencies = [ 238 | "dirs-sys", 239 | ] 240 | 241 | [[package]] 242 | name = "dirs-sys" 243 | version = "0.5.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 246 | dependencies = [ 247 | "libc", 248 | "option-ext", 249 | "redox_users", 250 | "windows-sys 0.61.0", 251 | ] 252 | 253 | [[package]] 254 | name = "document-features" 255 | version = "0.2.12" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" 258 | dependencies = [ 259 | "litrs", 260 | ] 261 | 262 | [[package]] 263 | name = "encode_unicode" 264 | version = "1.0.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 267 | 268 | [[package]] 269 | name = "endian-type" 270 | version = "0.1.2" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 273 | 274 | [[package]] 275 | name = "errno" 276 | version = "0.3.14" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 279 | dependencies = [ 280 | "libc", 281 | "windows-sys 0.61.0", 282 | ] 283 | 284 | [[package]] 285 | name = "error-code" 286 | version = "3.3.2" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" 289 | 290 | [[package]] 291 | name = "fallible-iterator" 292 | version = "0.3.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 295 | 296 | [[package]] 297 | name = "fallible-streaming-iterator" 298 | version = "0.1.9" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 301 | 302 | [[package]] 303 | name = "fd-lock" 304 | version = "4.0.4" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" 307 | dependencies = [ 308 | "cfg-if", 309 | "rustix", 310 | "windows-sys 0.59.0", 311 | ] 312 | 313 | [[package]] 314 | name = "foldhash" 315 | version = "0.1.5" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 318 | 319 | [[package]] 320 | name = "getrandom" 321 | version = "0.2.16" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 324 | dependencies = [ 325 | "cfg-if", 326 | "libc", 327 | "wasi", 328 | ] 329 | 330 | [[package]] 331 | name = "hashbrown" 332 | version = "0.15.5" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 335 | dependencies = [ 336 | "foldhash", 337 | ] 338 | 339 | [[package]] 340 | name = "hashlink" 341 | version = "0.10.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 344 | dependencies = [ 345 | "hashbrown", 346 | ] 347 | 348 | [[package]] 349 | name = "heck" 350 | version = "0.5.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 353 | 354 | [[package]] 355 | name = "home" 356 | version = "0.5.12" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" 359 | dependencies = [ 360 | "windows-sys 0.61.0", 361 | ] 362 | 363 | [[package]] 364 | name = "indicatif" 365 | version = "0.18.2" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" 368 | dependencies = [ 369 | "console", 370 | "portable-atomic", 371 | "unicode-width", 372 | "unit-prefix", 373 | "web-time", 374 | ] 375 | 376 | [[package]] 377 | name = "is_terminal_polyfill" 378 | version = "1.70.2" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 381 | 382 | [[package]] 383 | name = "itoa" 384 | version = "1.0.15" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 387 | 388 | [[package]] 389 | name = "js-sys" 390 | version = "0.3.74" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" 393 | dependencies = [ 394 | "once_cell", 395 | "wasm-bindgen", 396 | ] 397 | 398 | [[package]] 399 | name = "libc" 400 | version = "0.2.177" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 403 | 404 | [[package]] 405 | name = "libredox" 406 | version = "0.1.10" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" 409 | dependencies = [ 410 | "bitflags", 411 | "libc", 412 | ] 413 | 414 | [[package]] 415 | name = "libsqlite3-sys" 416 | version = "0.35.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" 419 | dependencies = [ 420 | "pkg-config", 421 | "vcpkg", 422 | ] 423 | 424 | [[package]] 425 | name = "linux-raw-sys" 426 | version = "0.11.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 429 | 430 | [[package]] 431 | name = "litrs" 432 | version = "1.0.0" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" 435 | 436 | [[package]] 437 | name = "lock_api" 438 | version = "0.4.14" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 441 | dependencies = [ 442 | "scopeguard", 443 | ] 444 | 445 | [[package]] 446 | name = "log" 447 | version = "0.4.28" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 450 | 451 | [[package]] 452 | name = "memchr" 453 | version = "2.7.6" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 456 | 457 | [[package]] 458 | name = "nibble_vec" 459 | version = "0.1.0" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 462 | dependencies = [ 463 | "smallvec", 464 | ] 465 | 466 | [[package]] 467 | name = "nix" 468 | version = "0.30.1" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 471 | dependencies = [ 472 | "bitflags", 473 | "cfg-if", 474 | "cfg_aliases", 475 | "libc", 476 | ] 477 | 478 | [[package]] 479 | name = "once_cell" 480 | version = "1.21.3" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 483 | 484 | [[package]] 485 | name = "once_cell_polyfill" 486 | version = "1.70.2" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 489 | 490 | [[package]] 491 | name = "option-ext" 492 | version = "0.2.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 495 | 496 | [[package]] 497 | name = "parking_lot" 498 | version = "0.12.5" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 501 | dependencies = [ 502 | "lock_api", 503 | "parking_lot_core", 504 | ] 505 | 506 | [[package]] 507 | name = "parking_lot_core" 508 | version = "0.9.12" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 511 | dependencies = [ 512 | "cfg-if", 513 | "libc", 514 | "redox_syscall", 515 | "smallvec", 516 | "windows-link", 517 | ] 518 | 519 | [[package]] 520 | name = "pkg-config" 521 | version = "0.3.32" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 524 | 525 | [[package]] 526 | name = "portable-atomic" 527 | version = "1.11.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 530 | 531 | [[package]] 532 | name = "proc-macro2" 533 | version = "1.0.103" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 536 | dependencies = [ 537 | "unicode-ident", 538 | ] 539 | 540 | [[package]] 541 | name = "quote" 542 | version = "1.0.42" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 545 | dependencies = [ 546 | "proc-macro2", 547 | ] 548 | 549 | [[package]] 550 | name = "radix_trie" 551 | version = "0.2.1" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 554 | dependencies = [ 555 | "endian-type", 556 | "nibble_vec", 557 | ] 558 | 559 | [[package]] 560 | name = "redox_syscall" 561 | version = "0.5.18" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 564 | dependencies = [ 565 | "bitflags", 566 | ] 567 | 568 | [[package]] 569 | name = "redox_users" 570 | version = "0.5.2" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" 573 | dependencies = [ 574 | "getrandom", 575 | "libredox", 576 | "thiserror", 577 | ] 578 | 579 | [[package]] 580 | name = "regex" 581 | version = "1.12.2" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 584 | dependencies = [ 585 | "aho-corasick", 586 | "memchr", 587 | "regex-automata", 588 | "regex-syntax", 589 | ] 590 | 591 | [[package]] 592 | name = "regex-automata" 593 | version = "0.4.13" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 596 | dependencies = [ 597 | "aho-corasick", 598 | "memchr", 599 | "regex-syntax", 600 | ] 601 | 602 | [[package]] 603 | name = "regex-syntax" 604 | version = "0.8.8" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 607 | 608 | [[package]] 609 | name = "rusqlite" 610 | version = "0.37.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" 613 | dependencies = [ 614 | "bitflags", 615 | "fallible-iterator", 616 | "fallible-streaming-iterator", 617 | "hashlink", 618 | "libsqlite3-sys", 619 | "smallvec", 620 | ] 621 | 622 | [[package]] 623 | name = "rustix" 624 | version = "1.1.2" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 627 | dependencies = [ 628 | "bitflags", 629 | "errno", 630 | "libc", 631 | "linux-raw-sys", 632 | "windows-sys 0.61.0", 633 | ] 634 | 635 | [[package]] 636 | name = "rustyline" 637 | version = "17.0.2" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" 640 | dependencies = [ 641 | "bitflags", 642 | "cfg-if", 643 | "clipboard-win", 644 | "fd-lock", 645 | "home", 646 | "libc", 647 | "log", 648 | "memchr", 649 | "nix", 650 | "radix_trie", 651 | "unicode-segmentation", 652 | "unicode-width", 653 | "utf8parse", 654 | "windows-sys 0.60.2", 655 | ] 656 | 657 | [[package]] 658 | name = "ryu" 659 | version = "1.0.20" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 662 | 663 | [[package]] 664 | name = "scopeguard" 665 | version = "1.2.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 668 | 669 | [[package]] 670 | name = "serde_core" 671 | version = "1.0.228" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 674 | dependencies = [ 675 | "serde_derive", 676 | ] 677 | 678 | [[package]] 679 | name = "serde_derive" 680 | version = "1.0.228" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 683 | dependencies = [ 684 | "proc-macro2", 685 | "quote", 686 | "syn", 687 | ] 688 | 689 | [[package]] 690 | name = "smallvec" 691 | version = "1.15.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 694 | 695 | [[package]] 696 | name = "strsim" 697 | version = "0.11.1" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 700 | 701 | [[package]] 702 | name = "syn" 703 | version = "2.0.110" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" 706 | dependencies = [ 707 | "proc-macro2", 708 | "quote", 709 | "unicode-ident", 710 | ] 711 | 712 | [[package]] 713 | name = "thiserror" 714 | version = "2.0.17" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 717 | dependencies = [ 718 | "thiserror-impl", 719 | ] 720 | 721 | [[package]] 722 | name = "thiserror-impl" 723 | version = "2.0.17" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 726 | dependencies = [ 727 | "proc-macro2", 728 | "quote", 729 | "syn", 730 | ] 731 | 732 | [[package]] 733 | name = "unicode-ident" 734 | version = "1.0.22" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 737 | 738 | [[package]] 739 | name = "unicode-segmentation" 740 | version = "1.12.0" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 743 | 744 | [[package]] 745 | name = "unicode-width" 746 | version = "0.2.2" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" 749 | 750 | [[package]] 751 | name = "unit-prefix" 752 | version = "0.5.1" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" 755 | 756 | [[package]] 757 | name = "utf8parse" 758 | version = "0.2.2" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 761 | 762 | [[package]] 763 | name = "vcpkg" 764 | version = "0.2.15" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 767 | 768 | [[package]] 769 | name = "wasi" 770 | version = "0.11.1+wasi-snapshot-preview1" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 773 | 774 | [[package]] 775 | name = "wasm-bindgen" 776 | version = "0.2.97" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" 779 | dependencies = [ 780 | "cfg-if", 781 | "once_cell", 782 | "wasm-bindgen-macro", 783 | ] 784 | 785 | [[package]] 786 | name = "wasm-bindgen-backend" 787 | version = "0.2.97" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" 790 | dependencies = [ 791 | "bumpalo", 792 | "log", 793 | "once_cell", 794 | "proc-macro2", 795 | "quote", 796 | "syn", 797 | "wasm-bindgen-shared", 798 | ] 799 | 800 | [[package]] 801 | name = "wasm-bindgen-macro" 802 | version = "0.2.97" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" 805 | dependencies = [ 806 | "quote", 807 | "wasm-bindgen-macro-support", 808 | ] 809 | 810 | [[package]] 811 | name = "wasm-bindgen-macro-support" 812 | version = "0.2.97" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" 815 | dependencies = [ 816 | "proc-macro2", 817 | "quote", 818 | "syn", 819 | "wasm-bindgen-backend", 820 | "wasm-bindgen-shared", 821 | ] 822 | 823 | [[package]] 824 | name = "wasm-bindgen-shared" 825 | version = "0.2.97" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" 828 | 829 | [[package]] 830 | name = "web-time" 831 | version = "1.1.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 834 | dependencies = [ 835 | "js-sys", 836 | "wasm-bindgen", 837 | ] 838 | 839 | [[package]] 840 | name = "winapi" 841 | version = "0.3.9" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 844 | dependencies = [ 845 | "winapi-i686-pc-windows-gnu", 846 | "winapi-x86_64-pc-windows-gnu", 847 | ] 848 | 849 | [[package]] 850 | name = "winapi-i686-pc-windows-gnu" 851 | version = "0.4.0" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 854 | 855 | [[package]] 856 | name = "winapi-x86_64-pc-windows-gnu" 857 | version = "0.4.0" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 860 | 861 | [[package]] 862 | name = "windows-link" 863 | version = "0.2.1" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 866 | 867 | [[package]] 868 | name = "windows-sys" 869 | version = "0.59.0" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 872 | dependencies = [ 873 | "windows-targets 0.52.6", 874 | ] 875 | 876 | [[package]] 877 | name = "windows-sys" 878 | version = "0.60.2" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 881 | dependencies = [ 882 | "windows-targets 0.53.2", 883 | ] 884 | 885 | [[package]] 886 | name = "windows-sys" 887 | version = "0.61.0" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" 890 | dependencies = [ 891 | "windows-link", 892 | ] 893 | 894 | [[package]] 895 | name = "windows-targets" 896 | version = "0.52.6" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 899 | dependencies = [ 900 | "windows_aarch64_gnullvm 0.52.6", 901 | "windows_aarch64_msvc 0.52.6", 902 | "windows_i686_gnu 0.52.6", 903 | "windows_i686_gnullvm 0.52.6", 904 | "windows_i686_msvc 0.52.6", 905 | "windows_x86_64_gnu 0.52.6", 906 | "windows_x86_64_gnullvm 0.52.6", 907 | "windows_x86_64_msvc 0.52.6", 908 | ] 909 | 910 | [[package]] 911 | name = "windows-targets" 912 | version = "0.53.2" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 915 | dependencies = [ 916 | "windows_aarch64_gnullvm 0.53.0", 917 | "windows_aarch64_msvc 0.53.0", 918 | "windows_i686_gnu 0.53.0", 919 | "windows_i686_gnullvm 0.53.0", 920 | "windows_i686_msvc 0.53.0", 921 | "windows_x86_64_gnu 0.53.0", 922 | "windows_x86_64_gnullvm 0.53.0", 923 | "windows_x86_64_msvc 0.53.0", 924 | ] 925 | 926 | [[package]] 927 | name = "windows_aarch64_gnullvm" 928 | version = "0.52.6" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 931 | 932 | [[package]] 933 | name = "windows_aarch64_gnullvm" 934 | version = "0.53.0" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 937 | 938 | [[package]] 939 | name = "windows_aarch64_msvc" 940 | version = "0.52.6" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 943 | 944 | [[package]] 945 | name = "windows_aarch64_msvc" 946 | version = "0.53.0" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 949 | 950 | [[package]] 951 | name = "windows_i686_gnu" 952 | version = "0.52.6" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 955 | 956 | [[package]] 957 | name = "windows_i686_gnu" 958 | version = "0.53.0" 959 | source = "registry+https://github.com/rust-lang/crates.io-index" 960 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 961 | 962 | [[package]] 963 | name = "windows_i686_gnullvm" 964 | version = "0.52.6" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 967 | 968 | [[package]] 969 | name = "windows_i686_gnullvm" 970 | version = "0.53.0" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 973 | 974 | [[package]] 975 | name = "windows_i686_msvc" 976 | version = "0.52.6" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 979 | 980 | [[package]] 981 | name = "windows_i686_msvc" 982 | version = "0.53.0" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 985 | 986 | [[package]] 987 | name = "windows_x86_64_gnu" 988 | version = "0.52.6" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 991 | 992 | [[package]] 993 | name = "windows_x86_64_gnu" 994 | version = "0.53.0" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 997 | 998 | [[package]] 999 | name = "windows_x86_64_gnullvm" 1000 | version = "0.52.6" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1003 | 1004 | [[package]] 1005 | name = "windows_x86_64_gnullvm" 1006 | version = "0.53.0" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1009 | 1010 | [[package]] 1011 | name = "windows_x86_64_msvc" 1012 | version = "0.52.6" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1015 | 1016 | [[package]] 1017 | name = "windows_x86_64_msvc" 1018 | version = "0.53.0" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1021 | --------------------------------------------------------------------------------