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 #[clap(short, long, value_parser)]
39 execute: Option<PathBuf>,
40
41 #[clap(short, long, value_parser)]
43 dump: Option<PathBuf>,
44
45 #[clap(short, long, value_parser)]
48 storage: Option<Storage>,
49
50 #[clap(short, long, value_parser)]
52 path: Option<PathBuf>,
53
54 #[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}