Skip to main content

gluesql_cli/
lib.rs

1#![deny(clippy::str_to_string)]
2#![allow(deprecated)]
3
4mod cli;
5mod command;
6mod helper;
7mod print;
8mod upgrade;
9
10use {
11    crate::cli::Cli,
12    anyhow::Result,
13    clap::Parser,
14    gluesql_core::{
15        ast::{Expr, ToSql},
16        store::{GStore, GStoreMut, Planner, Store, Transaction},
17    },
18    gluesql_csv_storage::CsvStorage,
19    gluesql_file_storage::FileStorage,
20    gluesql_json_storage::JsonStorage,
21    gluesql_memory_storage::MemoryStorage,
22    gluesql_parquet_storage::ParquetStorage,
23    gluesql_redb_storage::RedbStorage,
24    gluesql_sled_storage::SledStorage,
25    std::{fmt::Debug, fs::File, io::Write, path::PathBuf},
26};
27
28const SLED_STORAGE_DEPRECATION_WARNING: &str = "[warning] sled-storage is deprecated and will be removed in v0.21.0; use redb-storage for new persistent-storage deployments";
29
30fn warn_sled_storage_deprecated() {
31    eprintln!("{SLED_STORAGE_DEPRECATION_WARNING}");
32}
33
34#[derive(Parser, Debug)]
35#[clap(name = "gluesql", about, version)]
36struct Args {
37    /// SQL file to execute
38    #[clap(short, long, value_parser)]
39    execute: Option<PathBuf>,
40
41    /// PATH to dump a Sled database as SQL; deprecated and removed in v0.21.0
42    #[clap(short, long, value_parser)]
43    dump: Option<PathBuf>,
44
45    /// Storage type to store data; defaults to memory.
46    /// sled is deprecated and will be removed in v0.21.0; use redb for new deployments.
47    #[clap(short, long, value_parser)]
48    storage: Option<Storage>,
49
50    /// Storage path to load
51    #[clap(short, long, value_parser)]
52    path: Option<PathBuf>,
53
54    /// Upgrade storage data format to the latest version
55    #[clap(
56        long,
57        requires_all = &["storage", "path"],
58        conflicts_with_all = &["execute", "dump"]
59    )]
60    upgrade: bool,
61}
62
63#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
64enum Storage {
65    Memory,
66    Sled,
67    Redb,
68    Json,
69    Csv,
70    Parquet,
71    File,
72}
73
74pub fn run() -> Result<()> {
75    fn run<T: GStore + GStoreMut + Planner>(storage: T, input: Option<PathBuf>) {
76        let output = std::io::stdout();
77        let mut cli = Cli::new(storage, output);
78
79        if let Some(path) = input
80            && let Err(e) = cli.load(path.as_path())
81        {
82            println!("[error] {e}\n");
83        }
84
85        if let Err(e) = cli.run() {
86            eprintln!("{e}");
87        }
88    }
89
90    let Args {
91        execute,
92        dump,
93        storage,
94        path,
95        upgrade,
96    } = Args::parse();
97
98    if upgrade {
99        return upgrade::run_upgrade(path.as_deref(), storage, execute.is_some(), dump.is_some());
100    }
101
102    let path = path.as_deref();
103
104    match (path, storage, dump) {
105        (Some(path), None, Some(dump_path)) => {
106            warn_sled_storage_deprecated();
107            let mut storage = SledStorage::new(path).expect("failed to load sled-storage");
108
109            dump_database(&mut storage, dump_path)?;
110        }
111        (None, None | Some(Storage::Memory), _) => {
112            println!("[memory-storage] initialized");
113
114            run(MemoryStorage::default(), execute);
115        }
116        (Some(_), Some(Storage::Memory), _) => {
117            panic!("failed to load memory-storage: it should be without path");
118        }
119        (Some(path), Some(Storage::Sled), _) => {
120            warn_sled_storage_deprecated();
121            println!("[sled-storage] connected to {}", path.display());
122
123            run(
124                SledStorage::new(path).expect("failed to load sled-storage"),
125                execute,
126            );
127        }
128        (Some(path), Some(Storage::Redb), _) => {
129            println!("[redb-storage] connected to {}", path.display());
130
131            run(
132                RedbStorage::new(path).expect("failed to load redb-storage"),
133                execute,
134            );
135        }
136        (Some(path), Some(Storage::Json), _) => {
137            println!("[json-storage] connected to {}", path.display());
138
139            run(
140                JsonStorage::new(path).expect("failed to load json-storage"),
141                execute,
142            );
143        }
144        (Some(path), Some(Storage::Csv), _) => {
145            println!("[csv-storage] connected to {}", path.display());
146
147            run(
148                CsvStorage::new(path).expect("failed to load csv-storage"),
149                execute,
150            );
151        }
152        (Some(path), Some(Storage::Parquet), _) => {
153            println!("[parquet-storage] connected to {}", path.display());
154
155            run(
156                ParquetStorage::new(path).expect("failed to load parquet-storage"),
157                execute,
158            );
159        }
160        (Some(path), Some(Storage::File), _) => {
161            println!("[file-storage] connected to {}", path.display());
162
163            run(
164                FileStorage::new(path).expect("failed to load file-storage"),
165                execute,
166            );
167        }
168        (None, Some(_), _) | (Some(_), None, None) => {
169            panic!("both path and storage should be specified");
170        }
171    }
172
173    Ok(())
174}
175
176pub fn dump_database(storage: &mut SledStorage, dump_path: PathBuf) -> Result<()> {
177    let file = File::create(dump_path)?;
178
179    storage.begin(true)?;
180    let schemas = storage.fetch_all_schemas()?;
181    for schema in schemas {
182        writeln!(&file, "{}", schema.to_ddl())?;
183
184        let mut rows = storage
185            .scan_data(&schema.table_name)?
186            .map(|result| result.map(|(_, row)| row));
187
188        loop {
189            let exprs_list = rows
190                .by_ref()
191                .take(100)
192                .map(|result| {
193                    result.map(|row| row.into_iter().map(Expr::Value).collect::<Vec<_>>())
194                })
195                .collect::<std::result::Result<Vec<_>, _>>()?;
196
197            if exprs_list.is_empty() {
198                break;
199            }
200
201            let values = exprs_list
202                .into_iter()
203                .map(|exprs| {
204                    let row = exprs
205                        .into_iter()
206                        .map(|expr| expr.to_sql())
207                        .collect::<Vec<_>>()
208                        .join(", ");
209                    format!("({row})")
210                })
211                .collect::<Vec<_>>()
212                .join(", ");
213
214            let insert_statement =
215                format!(r#"INSERT INTO "{}" VALUES {values};"#, schema.table_name);
216
217            writeln!(&file, "{insert_statement}")?;
218        }
219
220        writeln!(&file)?;
221    }
222
223    Ok(())
224}
225
226#[cfg(test)]
227mod tests {
228    use {super::Args, clap::Parser};
229
230    #[test]
231    fn parse_upgrade_requires_storage_and_path() {
232        let args = Args::try_parse_from(["gluesql", "--upgrade"]);
233        assert!(args.is_err());
234    }
235
236    #[test]
237    fn parse_upgrade_rejects_execute() {
238        let args = Args::try_parse_from([
239            "gluesql",
240            "--upgrade",
241            "--storage",
242            "sled",
243            "--path",
244            "./tmp",
245            "--execute",
246            "query.sql",
247        ]);
248        assert!(args.is_err());
249    }
250
251    #[test]
252    fn parse_upgrade_rejects_dump() {
253        let args = Args::try_parse_from([
254            "gluesql",
255            "--upgrade",
256            "--storage",
257            "file",
258            "--path",
259            "./tmp",
260            "--dump",
261            "dump.sql",
262        ]);
263        assert!(args.is_err());
264    }
265}