1use std::{collections::HashSet, sync::OnceLock};
2
3use anyhow::{Context, Result, anyhow, bail};
4use regex::Regex;
5use rusqlite::{
6 Connection, params_from_iter,
7 types::ValueRef,
8};
9
10use crate::input;
11use crate::value::to_sql_value;
12use crate::{
13 ExtractionOptions, InputNormalizationOptions, QueryParam, QueryResult, QueryValue,
14 TypeInferenceOptions, WorkbookInput,
15};
16
17const SQLITE_MAX_INSERT_VARIABLES: usize = 999;
18const SQLITE_MAX_INSERT_ROWS: usize = 250;
19
20pub fn run_query(
21 workbook_path: &std::path::Path,
22 sheet_name: Option<&str>,
23 query: &str,
24 has_headers: bool,
25) -> Result<QueryResult> {
26 run_query_with_params(workbook_path, sheet_name, query, &[], has_headers)
27}
28
29pub fn run_query_with_params(
30 workbook_path: &std::path::Path,
31 sheet_name: Option<&str>,
32 query: &str,
33 params: &[QueryParam],
34 has_headers: bool,
35) -> Result<QueryResult> {
36 run_query_with_params_multi(&[workbook_path], sheet_name, query, params, has_headers)
37}
38
39pub fn run_query_with_params_multi(
40 workbook_paths: &[&std::path::Path],
41 sheet_name: Option<&str>,
42 query: &str,
43 params: &[QueryParam],
44 has_headers: bool,
45) -> Result<QueryResult> {
46 let workbook_inputs = workbook_paths
47 .iter()
48 .map(|path| WorkbookInput {
49 path,
50 sheet_name,
51 table_name: None,
52 explicit_format: None,
53 })
54 .collect::<Vec<_>>();
55
56 run_query_with_params_multi_inputs(&workbook_inputs, query, params, has_headers)
57}
58
59pub fn run_query_with_params_multi_inputs(
60 workbook_inputs: &[WorkbookInput<'_>],
61 query: &str,
62 params: &[QueryParam],
63 has_headers: bool,
64) -> Result<QueryResult> {
65 run_query_with_params_multi_inputs_and_options(
66 workbook_inputs,
67 query,
68 params,
69 &TypeInferenceOptions::default(),
70 has_headers,
71 )
72}
73
74pub fn run_query_with_params_multi_inputs_and_options(
75 workbook_inputs: &[WorkbookInput<'_>],
76 query: &str,
77 params: &[QueryParam],
78 inference_options: &TypeInferenceOptions,
79 has_headers: bool,
80) -> Result<QueryResult> {
81 run_query_with_params_multi_inputs_and_options_and_normalization(
82 workbook_inputs,
83 query,
84 params,
85 inference_options,
86 &InputNormalizationOptions::default(),
87 &ExtractionOptions::default(),
88 has_headers,
89 )
90}
91
92pub fn run_query_with_params_multi_inputs_and_options_and_normalization(
93 workbook_inputs: &[WorkbookInput<'_>],
94 query: &str,
95 params: &[QueryParam],
96 inference_options: &TypeInferenceOptions,
97 normalization_options: &InputNormalizationOptions,
98 extraction_options: &ExtractionOptions,
99 has_headers: bool,
100) -> Result<QueryResult> {
101 if workbook_inputs.is_empty() {
102 bail!("at least one workbook input is required");
103 }
104
105 let connection =
106 Connection::open_in_memory().context("failed to create in-memory SQLite database")?;
107 let mut registered_sheet_views = HashSet::new();
108
109 for (index, workbook_input) in workbook_inputs.iter().enumerate() {
110 let mut sheet = input::load_input(
111 workbook_input.path,
112 workbook_input.sheet_name,
113 inference_options,
114 extraction_options,
115 has_headers,
116 workbook_input.explicit_format,
117 )?;
118 input::apply_input_normalization(&mut sheet, normalization_options);
119 let table_name = workbook_input
120 .table_name
121 .map(str::to_owned)
122 .unwrap_or_else(|| {
123 if index == 0 {
124 "table".to_owned()
125 } else {
126 format!("table{}", index + 1)
127 }
128 });
129
130 register_sheet(
131 &connection,
132 &sheet,
133 &table_name,
134 &mut registered_sheet_views,
135 )?;
136 }
137
138 execute_statements(&connection, query, params)
139}
140
141fn split_sql_statements(sql: &str) -> Vec<String> {
146 let mut statements = Vec::new();
147 let mut current = String::new();
148 let mut chars = sql.chars().peekable();
149
150 while let Some(ch) = chars.next() {
151 match ch {
152 '\'' => {
155 current.push(ch);
156 loop {
157 match chars.next() {
158 None => break,
159 Some('\'') => {
160 current.push('\'');
161 if chars.peek() == Some(&'\'') {
162 current.push(chars.next().unwrap());
163 } else {
164 break;
165 }
166 }
167 Some(c) => current.push(c),
168 }
169 }
170 }
171 '-' if chars.peek() == Some(&'-') => {
173 current.push(ch);
174 current.push(chars.next().unwrap()); for c in chars.by_ref() {
176 current.push(c);
177 if c == '\n' {
178 break;
179 }
180 }
181 }
182 '/' if chars.peek() == Some(&'*') => {
184 current.push(ch);
185 current.push(chars.next().unwrap()); let mut prev = '\0';
187 for c in chars.by_ref() {
188 current.push(c);
189 if prev == '*' && c == '/' {
190 break;
191 }
192 prev = c;
193 }
194 }
195 ';' => {
196 let trimmed = current.trim().to_owned();
197 if !trimmed.is_empty() {
198 statements.push(trimmed);
199 }
200 current.clear();
201 }
202 _ => current.push(ch),
203 }
204 }
205
206 let trimmed = current.trim().to_owned();
207 if !trimmed.is_empty() {
208 statements.push(trimmed);
209 }
210
211 statements
212}
213
214fn execute_statements(
218 connection: &Connection,
219 sql: &str,
220 params: &[QueryParam],
221) -> Result<QueryResult> {
222 let statements = split_sql_statements(sql);
223
224 match statements.as_slice() {
225 [] => bail!("no SQL statements provided"),
226 [single] => execute_query(connection, single, params),
227 _ => {
228 let last = statements.last().expect("non-empty slice always has a last element");
229 let init = &statements[..statements.len() - 1];
230 for stmt in init {
231 connection
232 .execute_batch(stmt)
233 .with_context(|| format!("failed to execute statement: {stmt}"))?;
234 }
235 execute_query(connection, last, params)
236 }
237 }
238}
239
240fn register_sheet(
241 connection: &Connection,
242 sheet: &input::SheetData,
243 table_name: &str,
244 registered_views: &mut HashSet<String>,
245) -> Result<()> {
246 let columns = sheet
247 .columns
248 .iter()
249 .map(|column| quote_identifier(column))
250 .collect::<Vec<_>>()
251 .join(", ");
252
253 connection
254 .execute(
255 &format!("CREATE TABLE {} ({columns})", quote_identifier(table_name)),
256 [],
257 )
258 .context("failed to create sheet table")?;
259
260 let table1_key = normalize_view_key("table1");
261 if table_name == "table" && !registered_views.contains(&table1_key) {
262 connection
263 .execute(
264 &format!(
265 "CREATE VIEW {} AS SELECT * FROM {}",
266 quote_identifier("table1"),
267 quote_identifier("table")
268 ),
269 [],
270 )
271 .context("failed to register alias view table1 for first input")?;
272 registered_views.insert(table1_key);
273 }
274
275 let sanitized_sheet_name = sanitize_table_name(&sheet.original_name);
276 let sanitized_sheet_key = normalize_view_key(&sanitized_sheet_name);
277 if sanitized_sheet_name != table_name && !registered_views.contains(&sanitized_sheet_key) {
278 connection
279 .execute(
280 &format!(
281 "CREATE VIEW {} AS SELECT * FROM {}",
282 quote_identifier(&sanitized_sheet_name),
283 quote_identifier(table_name)
284 ),
285 [],
286 )
287 .with_context(|| {
288 format!("failed to register view for sheet {}", sheet.original_name)
289 })?;
290 registered_views.insert(sanitized_sheet_key);
291 }
292
293 if sheet.rows.is_empty() {
294 return Ok(());
295 }
296
297 insert_sheet_rows(connection, table_name, sheet)
298}
299
300fn insert_sheet_rows(connection: &Connection, table_name: &str, sheet: &input::SheetData) -> Result<()> {
301 connection
302 .execute_batch("BEGIN IMMEDIATE TRANSACTION")
303 .context("failed to begin sheet insert transaction")?;
304
305 let insert_result = (|| {
306 let batch_size = calculate_insert_batch_size(sheet.columns.len());
307 for rows_chunk in sheet.rows.chunks(batch_size) {
308 let insert_sql =
309 build_batched_insert_sql(table_name, sheet.columns.len(), rows_chunk.len());
310 let mut values = Vec::with_capacity(rows_chunk.len() * sheet.columns.len());
311 for row in rows_chunk {
312 for index in 0..sheet.columns.len() {
313 let value = row.get(index).unwrap_or(&QueryValue::Null);
314 values.push(to_sql_value(value));
315 }
316 }
317
318 connection
319 .execute(&insert_sql, params_from_iter(values))
320 .context("failed to insert sheet rows")?;
321 }
322
323 Ok(())
324 })();
325
326 match insert_result {
327 Ok(()) => connection
328 .execute_batch("COMMIT")
329 .context("failed to commit sheet insert transaction"),
330 Err(error) => {
331 let _ = connection.execute_batch("ROLLBACK");
332 Err(error)
333 }
334 }
335}
336
337fn calculate_insert_batch_size(column_count: usize) -> usize {
338 let clamped_column_count = column_count.max(1);
339 (SQLITE_MAX_INSERT_VARIABLES / clamped_column_count)
340 .max(1)
341 .min(SQLITE_MAX_INSERT_ROWS)
342}
343
344fn build_batched_insert_sql(table_name: &str, column_count: usize, row_count: usize) -> String {
345 let row_placeholders = format!("({})", vec!["?"; column_count].join(", "));
346 let values = vec![row_placeholders; row_count].join(", ");
347 format!("INSERT INTO {} VALUES {values}", quote_identifier(table_name))
348}
349
350fn execute_query(
351 connection: &Connection,
352 query: &str,
353 params: &[QueryParam],
354) -> Result<QueryResult> {
355 let normalized_query = normalize_query_for_reserved_identifiers(query);
356 let mut statement = connection
357 .prepare(&normalized_query)
358 .map_err(|error| enrich_query_prepare_error(connection, query, error))?;
359
360 if statement.column_count() == 0 {
361 bail!("query must return rows");
362 }
363
364 let columns = statement
365 .column_names()
366 .into_iter()
367 .map(str::to_owned)
368 .collect::<Vec<_>>();
369
370 bind_query_params(&mut statement, params)?;
371
372 let column_count = statement.column_count();
373 let mut rows = statement.raw_query();
374 let mut result_rows = Vec::new();
375
376 while let Some(row) = rows.next().context("failed to fetch query row")? {
377 let mut values = Vec::with_capacity(column_count);
378
379 for index in 0..column_count {
380 values.push(match row.get_ref(index)? {
381 ValueRef::Null => QueryValue::Null,
382 ValueRef::Integer(value) => QueryValue::Integer(value),
383 ValueRef::Real(value) => QueryValue::Real(value),
384 ValueRef::Text(value) => {
385 QueryValue::Text(String::from_utf8_lossy(value).into_owned())
386 }
387 ValueRef::Blob(value) => QueryValue::Text(format_blob(value)),
388 });
389 }
390
391 result_rows.push(values);
392 }
393
394 Ok(QueryResult {
395 columns,
396 rows: result_rows,
397 })
398}
399
400fn enrich_query_prepare_error(
401 connection: &Connection,
402 query: &str,
403 error: rusqlite::Error,
404) -> anyhow::Error {
405 let raw_error = error.to_string();
406
407 if let Some(table) = raw_error.strip_prefix("no such table: ").map(str::trim) {
408 let table = simplify_sqlite_missing_identifier(table);
409 let available_tables = list_loaded_table_names(connection);
410 let available_suffix = if available_tables.is_empty() {
411 String::new()
412 } else {
413 format!(" Available tables/views: {}.", available_tables.join(", "))
414 };
415
416 return anyhow!(
417 "query references unknown table '{table}'.{available_suffix} Check your table names or run `qf tables --input ...` to inspect available tables.\nQuery: {query}"
418 );
419 }
420
421 if let Some(column) = raw_error.strip_prefix("no such column: ").map(str::trim) {
422 let column = simplify_sqlite_missing_identifier(column);
423 return anyhow!(
424 "query references unknown column '{column}'. Check your column names or run `qf schema --input ...` to inspect available columns.\nQuery: {query}"
425 );
426 }
427
428 anyhow!(error).context(format!("failed to prepare query: {query}"))
429}
430
431fn simplify_sqlite_missing_identifier(identifier: &str) -> &str {
432 identifier
433 .split_once(" in ")
434 .map(|(name, _)| name)
435 .unwrap_or(identifier)
436 .trim()
437}
438
439fn list_loaded_table_names(connection: &Connection) -> Vec<String> {
440 let mut statement = match connection.prepare(
441 "SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name",
442 ) {
443 Ok(statement) => statement,
444 Err(_) => return Vec::new(),
445 };
446
447 let names = match statement.query_map([], |row| row.get::<_, String>(0)) {
448 Ok(names) => names,
449 Err(_) => return Vec::new(),
450 };
451
452 names.filter_map(|name| name.ok()).collect()
453}
454
455fn normalize_query_for_reserved_identifiers(query: &str) -> String {
456 static RESERVED_TABLE_RE: OnceLock<Regex> = OnceLock::new();
457
458 let re = RESERVED_TABLE_RE.get_or_init(|| {
459 Regex::new(r"(?i)\b(from|join|update|into)\s+table\b")
460 .expect("reserved table regex should compile")
461 });
462
463 re.replace_all(query, |captures: ®ex::Captures<'_>| {
464 format!("{} \"table\"", &captures[1])
465 })
466 .into_owned()
467}
468
469fn bind_query_params(statement: &mut rusqlite::Statement<'_>, params: &[QueryParam]) -> Result<()> {
470 for param in params {
471 let mut bound = false;
472
473 for prefix in [":", "@", "$"] {
474 let parameter_name = format!("{prefix}{}", param.name);
475 if let Some(index) = statement
476 .parameter_index(¶meter_name)
477 .with_context(|| format!("failed to inspect parameter {parameter_name}"))?
478 {
479 statement
480 .raw_bind_parameter(index, to_sql_value(¶m.value))
481 .with_context(|| format!("failed to bind parameter {parameter_name}"))?;
482 bound = true;
483 break;
484 }
485 }
486
487 if !bound {
488 bail!("query does not contain parameter :{}", param.name);
489 }
490 }
491
492 Ok(())
493}
494
495fn format_blob(value: &[u8]) -> String {
496 use std::fmt::Write as _;
497
498 let mut formatted = String::from("0x");
499 for byte in value {
500 let _ = write!(&mut formatted, "{byte:02x}");
501 }
502
503 formatted
504}
505
506fn quote_identifier(identifier: &str) -> String {
507 format!("\"{}\"", identifier.replace('"', "\"\""))
508}
509
510fn sanitize_table_name(name: &str) -> String {
511 let sanitized = name
512 .chars()
513 .map(|character| {
514 if character.is_alphanumeric() || character == '_' {
515 character
516 } else {
517 '_'
518 }
519 })
520 .collect::<String>()
521 .trim_matches('_')
522 .to_string();
523
524 if sanitized.is_empty() {
525 "table_view".to_owned()
526 } else {
527 sanitized
528 }
529}
530
531fn normalize_view_key(name: &str) -> String {
532 name.to_ascii_lowercase()
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn calculates_safe_insert_batch_size() {
541 assert_eq!(calculate_insert_batch_size(1), SQLITE_MAX_INSERT_ROWS);
542 assert_eq!(calculate_insert_batch_size(3), SQLITE_MAX_INSERT_ROWS);
543 assert_eq!(calculate_insert_batch_size(400), 2);
544 assert_eq!(calculate_insert_batch_size(2_000), 1);
545 }
546
547 #[test]
548 fn builds_batched_insert_sql_for_multiple_rows() {
549 assert_eq!(
550 build_batched_insert_sql("table", 2, 3),
551 "INSERT INTO \"table\" VALUES (?, ?), (?, ?), (?, ?)"
552 );
553 }
554}