1use std::collections::HashSet;
6use std::fs;
7use std::io::{self, Read, Write};
8
9use alopex_embedded::Database;
10
11use crate::batch::BatchMode;
12use crate::cli::{OutputFormat, SqlCommand};
13use crate::client::http::{ClientError, HttpClient};
14use crate::error::{CliError, Result};
15use crate::models::{Column, DataType, Row, Value};
16use crate::output::formatter::{create_formatter, Formatter};
17use crate::streaming::timeout::parse_deadline;
18use crate::streaming::{CancelSignal, Deadline, StreamingWriter, WriteStatus};
19use crate::tui::{is_tty, TuiApp};
20use crate::ui::mode::UiMode;
21use futures_util::StreamExt;
22use reqwest::Response;
23
24#[doc(hidden)]
25pub struct SqlExecutionOptions<'a> {
26 pub limit: Option<usize>,
27 pub quiet: bool,
28 pub cancel: &'a CancelSignal,
29 pub deadline: &'a Deadline,
30 pub admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
31}
32
33enum SqlOutput<'a> {
38 Format(OutputFormat),
41 Custom(&'a mut dyn FnMut() -> Box<dyn Formatter>),
44}
45
46impl SqlOutput<'_> {
47 fn create_block_formatter(&mut self) -> Box<dyn Formatter> {
49 match self {
50 SqlOutput::Format(format) => create_formatter(*format),
51 SqlOutput::Custom(factory) => factory(),
52 }
53 }
54
55 fn statement_array(&self) -> bool {
57 matches!(self, SqlOutput::Format(OutputFormat::Json))
58 }
59
60 fn supports_streaming(&mut self) -> bool {
62 match self {
63 SqlOutput::Format(format) => format.supports_streaming(),
64 SqlOutput::Custom(factory) => factory().supports_streaming(),
65 }
66 }
67}
68
69struct CountingWriter<W> {
74 inner: W,
75 bytes: u64,
76}
77
78impl<W: Write> CountingWriter<W> {
79 fn new(inner: W) -> Self {
80 Self { inner, bytes: 0 }
81 }
82
83 fn bytes_written(&self) -> u64 {
84 self.bytes
85 }
86}
87
88impl<W: Write> Write for CountingWriter<W> {
89 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
90 let written = self.inner.write(buf)?;
91 self.bytes += written as u64;
92 Ok(written)
93 }
94
95 fn flush(&mut self) -> io::Result<()> {
96 self.inner.flush()
97 }
98}
99
100#[allow(clippy::too_many_arguments)]
122pub fn execute_with_formatter<'a, W: Write>(
123 db: &Database,
124 cmd: SqlCommand,
125 batch_mode: &BatchMode,
126 ui_mode: UiMode,
127 writer: &mut W,
128 output_format: OutputFormat,
129 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
130 limit: Option<usize>,
131 quiet: bool,
132) -> Result<()> {
133 let deadline = Deadline::new(parse_deadline(cmd.deadline.as_deref())?);
134 let cancel = CancelSignal::new();
135
136 execute_with_formatter_control(
137 db,
138 cmd,
139 batch_mode,
140 ui_mode,
141 writer,
142 output_format,
143 SqlExecutionOptions {
144 limit,
145 quiet,
146 cancel: &cancel,
147 deadline: &deadline,
148 admin_launcher,
149 },
150 )
151}
152
153#[allow(clippy::too_many_arguments)]
160pub fn execute_with_formatter_factory<'a, W: Write>(
161 db: &Database,
162 cmd: SqlCommand,
163 batch_mode: &BatchMode,
164 ui_mode: UiMode,
165 writer: &mut W,
166 make_formatter: &mut dyn FnMut() -> Box<dyn Formatter>,
167 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
168 limit: Option<usize>,
169 quiet: bool,
170) -> Result<()> {
171 let deadline = Deadline::new(parse_deadline(cmd.deadline.as_deref())?);
172 let cancel = CancelSignal::new();
173 let mut output = SqlOutput::Custom(make_formatter);
174
175 execute_with_output_control(
176 db,
177 cmd,
178 batch_mode,
179 ui_mode,
180 writer,
181 &mut output,
182 SqlExecutionOptions {
183 limit,
184 quiet,
185 cancel: &cancel,
186 deadline: &deadline,
187 admin_launcher,
188 },
189 )
190}
191
192#[doc(hidden)]
193pub fn execute_with_formatter_control<W: Write>(
194 db: &Database,
195 cmd: SqlCommand,
196 batch_mode: &BatchMode,
197 ui_mode: UiMode,
198 writer: &mut W,
199 output_format: OutputFormat,
200 options: SqlExecutionOptions<'_>,
201) -> Result<()> {
202 let mut output = SqlOutput::Format(output_format);
203 execute_with_output_control(db, cmd, batch_mode, ui_mode, writer, &mut output, options)
204}
205
206fn execute_with_output_control<W: Write>(
207 db: &Database,
208 cmd: SqlCommand,
209 batch_mode: &BatchMode,
210 ui_mode: UiMode,
211 writer: &mut W,
212 output: &mut SqlOutput<'_>,
213 mut options: SqlExecutionOptions<'_>,
214) -> Result<()> {
215 let sql = cmd.resolve_query(batch_mode)?;
216 let effective_limit = merge_limit(options.limit, cmd.max_rows);
217 options.limit = effective_limit;
218
219 if ui_mode == UiMode::Tui {
220 return execute_tui_local_or_fallback(db, &sql, writer, output, options);
221 }
222
223 execute_sql_with_formatter(db, &sql, writer, output, &options)
224}
225
226fn is_select_query(sql: &str) -> Result<bool> {
227 use alopex_sql::{AlopexDialect, Parser, StatementKind};
228
229 let dialect = AlopexDialect;
230 let stmts = Parser::parse_sql(&dialect, sql).map_err(|e| CliError::Parse(format!("{}", e)))?;
231 Ok(stmts.len() == 1
232 && matches!(
233 stmts.first().map(|s| &s.kind),
234 Some(StatementKind::Select(_))
235 ))
236}
237
238#[allow(clippy::too_many_arguments)]
244pub async fn execute_remote_with_formatter<'a, W: Write>(
245 client: &HttpClient,
246 cmd: &SqlCommand,
247 batch_mode: &BatchMode,
248 ui_mode: UiMode,
249 writer: &mut W,
250 output_format: OutputFormat,
251 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
252 limit: Option<usize>,
253 quiet: bool,
254) -> Result<()> {
255 let effective_limit = merge_limit(limit, cmd.max_rows);
256 let deadline = Deadline::new(parse_deadline(cmd.deadline.as_deref())?);
257 let cancel = CancelSignal::new();
258 let options = SqlExecutionOptions {
259 limit: effective_limit,
260 quiet,
261 cancel: &cancel,
262 deadline: &deadline,
263 admin_launcher,
264 };
265
266 execute_remote_with_formatter_control(
267 client,
268 cmd,
269 batch_mode,
270 ui_mode,
271 writer,
272 output_format,
273 options,
274 )
275 .await
276}
277
278fn sql_context_message(sql: &str) -> String {
279 let condensed = sql.split_whitespace().collect::<Vec<_>>().join(" ");
280 let max_len = 200;
281 if condensed.chars().count() > max_len {
282 let truncated: String = condensed.chars().take(max_len).collect();
283 format!("SQL: {truncated}...")
284 } else {
285 format!("SQL: {condensed}")
286 }
287}
288
289#[allow(clippy::too_many_arguments)]
293pub async fn execute_remote_with_formatter_factory<'a, W: Write>(
294 client: &HttpClient,
295 cmd: &SqlCommand,
296 batch_mode: &BatchMode,
297 ui_mode: UiMode,
298 writer: &mut W,
299 make_formatter: &mut dyn FnMut() -> Box<dyn Formatter>,
300 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
301 limit: Option<usize>,
302 quiet: bool,
303) -> Result<()> {
304 let effective_limit = merge_limit(limit, cmd.max_rows);
305 let deadline = Deadline::new(parse_deadline(cmd.deadline.as_deref())?);
306 let cancel = CancelSignal::new();
307 let options = SqlExecutionOptions {
308 limit: effective_limit,
309 quiet,
310 cancel: &cancel,
311 deadline: &deadline,
312 admin_launcher,
313 };
314 let mut output = SqlOutput::Custom(make_formatter);
315 execute_remote_with_output_control(
316 client,
317 cmd,
318 batch_mode,
319 ui_mode,
320 writer,
321 &mut output,
322 options,
323 )
324 .await
325}
326
327#[doc(hidden)]
328pub async fn execute_remote_with_formatter_control<W: Write>(
329 client: &HttpClient,
330 cmd: &SqlCommand,
331 batch_mode: &BatchMode,
332 ui_mode: UiMode,
333 writer: &mut W,
334 output_format: OutputFormat,
335 options: SqlExecutionOptions<'_>,
336) -> Result<()> {
337 let mut output = SqlOutput::Format(output_format);
338 execute_remote_with_output_control(
339 client,
340 cmd,
341 batch_mode,
342 ui_mode,
343 writer,
344 &mut output,
345 options,
346 )
347 .await
348}
349
350async fn execute_remote_with_output_control<W: Write>(
351 client: &HttpClient,
352 cmd: &SqlCommand,
353 batch_mode: &BatchMode,
354 ui_mode: UiMode,
355 writer: &mut W,
356 output: &mut SqlOutput<'_>,
357 options: SqlExecutionOptions<'_>,
358) -> Result<()> {
359 let sql = cmd.resolve_query(batch_mode)?;
360 if ui_mode == UiMode::Tui {
361 return execute_tui_remote_or_fallback(client, &sql, cmd, writer, output, options).await;
362 }
363 execute_remote_with_formatter_impl(client, &sql, cmd, writer, output, &options).await
364}
365
366async fn execute_remote_with_formatter_impl<W: Write>(
367 client: &HttpClient,
368 sql: &str,
369 cmd: &SqlCommand,
370 writer: &mut W,
371 output: &mut SqlOutput<'_>,
372 options: &SqlExecutionOptions<'_>,
373) -> Result<()> {
374 if is_select_query(sql)? && output.supports_streaming() {
375 return execute_remote_streaming(
376 client,
377 sql,
378 writer,
379 output,
380 options,
381 cmd.fetch_size,
382 cmd.max_rows,
383 )
384 .await;
385 }
386
387 let request = RemoteSqlRequest {
388 sql: sql.to_string(),
389 streaming: false,
390 fetch_size: cmd.fetch_size,
391 max_rows: cmd.max_rows,
392 };
393 let response: RemoteSqlResponse = tokio::select! {
394 result = tokio::time::timeout(options.deadline.remaining(), client.post_json("api/sql/query", &request)) => {
395 match result {
396 Ok(value) => value.map_err(map_client_error)?,
397 Err(_) => {
398 let _ = send_cancel_request(client).await;
399 return Err(CliError::Timeout(format!(
400 "deadline exceeded after {}",
401 humantime::format_duration(options.deadline.duration())
402 )));
403 }
404 }
405 }
406 _ = options.cancel.wait() => {
407 let _ = send_cancel_request(client).await;
408 return Err(CliError::Cancelled);
409 }
410 };
411
412 let json_array = output.statement_array();
416
417 if response.columns.is_empty() {
418 if options.quiet {
419 if json_array {
420 writeln!(writer, "[")?;
421 writeln!(writer, "]")?;
422 }
423 return Ok(());
424 }
425 let message = match response.affected_rows {
426 Some(count) => format!("{count} row(s) affected"),
427 None => "Operation completed successfully".to_string(),
428 };
429 if json_array {
430 writeln!(writer, "[")?;
431 }
432 let columns = sql_status_columns();
433 {
434 let mut streaming_writer = StreamingWriter::new(
435 &mut *writer,
436 output.create_block_formatter(),
437 columns,
438 options.limit,
439 )
440 .with_quiet(options.quiet);
441 streaming_writer.prepare(Some(1))?;
442 let row = Row::new(vec![Value::Text("OK".to_string()), Value::Text(message)]);
443 streaming_writer.write_row(row)?;
444 streaming_writer.finish()?;
445 }
446 if json_array {
447 writeln!(writer, "]")?;
448 }
449 return Ok(());
450 }
451
452 let columns: Vec<Column> = response
453 .columns
454 .iter()
455 .map(|col| Column::new(&col.name, data_type_from_string(&col.data_type)))
456 .collect();
457 if json_array {
458 writeln!(writer, "[")?;
459 }
460 {
461 let mut streaming_writer = StreamingWriter::new(
462 &mut *writer,
463 output.create_block_formatter(),
464 columns,
465 options.limit,
466 )
467 .with_quiet(options.quiet);
468 streaming_writer.prepare(Some(response.rows.len()))?;
469 for row in response.rows {
470 if options.cancel.is_cancelled() {
471 let _ = send_cancel_request(client).await;
472 return Err(CliError::Cancelled);
473 }
474 options.deadline.check()?;
475 let values = row.into_iter().map(remote_value_to_value).collect();
476 match streaming_writer.write_row(Row::new(values))? {
477 WriteStatus::LimitReached => break,
478 WriteStatus::Continue => {}
479 }
480 }
481 streaming_writer.finish()?;
482 }
483 if json_array {
484 writeln!(writer, "]")?;
485 }
486 Ok(())
487}
488
489fn execute_tui_local_or_fallback<'a, W: Write>(
490 db: &Database,
491 sql: &str,
492 writer: &mut W,
493 output: &mut SqlOutput<'_>,
494 mut options: SqlExecutionOptions<'a>,
495) -> Result<()> {
496 if !is_tty() {
497 if !options.quiet {
498 eprintln!("Warning: --tui requires a TTY, falling back to batch output.");
499 }
500 return execute_sql_with_formatter(db, sql, writer, output, &options);
501 }
502
503 let admin_launcher = options.admin_launcher.take();
504 match execute_tui_local(db, sql, &options, admin_launcher) {
505 Ok(()) => Ok(()),
506 Err(err) => {
507 if !options.quiet {
508 eprintln!("Warning: TUI failed ({err}); falling back to batch output.");
509 }
510 execute_sql_with_formatter(db, sql, writer, output, &options)
511 }
512 }
513}
514
515fn execute_tui_local<'a>(
516 db: &Database,
517 sql: &str,
518 options: &SqlExecutionOptions<'a>,
519 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
520) -> Result<()> {
521 use alopex_sql::ExecutionResult;
522
523 options.deadline.check()?;
524 let result = db.execute_sql(sql)?;
525 options.deadline.check()?;
526
527 let (columns, rows) = match result {
528 ExecutionResult::Success => {
529 let columns = sql_status_columns();
530 let row = Row::new(vec![
531 Value::Text("OK".to_string()),
532 Value::Text("Operation completed successfully".to_string()),
533 ]);
534 (columns, vec![row])
535 }
536 ExecutionResult::RowsAffected(count) => {
537 let columns = sql_status_columns();
538 let row = Row::new(vec![
539 Value::Text("OK".to_string()),
540 Value::Text(format!("{count} row(s) affected")),
541 ]);
542 (columns, vec![row])
543 }
544 ExecutionResult::Query(query_result) => {
545 let columns = query_result
546 .columns
547 .iter()
548 .map(|col| Column::new(&col.name, DataType::Text))
549 .collect::<Vec<_>>();
550 let mut rows = Vec::with_capacity(query_result.rows.len());
551 for sql_row in query_result.rows {
552 let values = sql_row.into_iter().map(sql_value_to_value).collect();
553 rows.push(Row::new(values));
554 }
555 if let Some(limit) = options.limit {
556 rows.truncate(limit);
557 }
558 (columns, rows)
559 }
560 };
561
562 let app = TuiApp::new(columns, rows, "local", false)
563 .with_context_message(Some(sql_context_message(sql)))
564 .with_admin_launcher(admin_launcher);
565 app.run()
566}
567
568async fn execute_tui_remote_or_fallback<'a, W: Write>(
569 client: &HttpClient,
570 sql: &str,
571 cmd: &SqlCommand,
572 writer: &mut W,
573 output: &mut SqlOutput<'_>,
574 mut options: SqlExecutionOptions<'a>,
575) -> Result<()> {
576 if !is_tty() {
577 if !options.quiet {
578 eprintln!("Warning: --tui requires a TTY, falling back to batch output.");
579 }
580 return execute_remote_with_formatter_impl(client, sql, cmd, writer, output, &options)
581 .await;
582 }
583
584 let admin_launcher = options.admin_launcher.take();
585 match execute_tui_remote(client, sql, cmd, &options, admin_launcher).await {
586 Ok(()) => Ok(()),
587 Err(err) => {
588 if !options.quiet {
589 eprintln!("Warning: TUI failed ({err}); falling back to batch output.");
590 }
591 execute_remote_with_formatter_impl(client, sql, cmd, writer, output, &options).await
592 }
593 }
594}
595
596async fn execute_tui_remote<'a>(
597 client: &HttpClient,
598 sql: &str,
599 cmd: &SqlCommand,
600 options: &SqlExecutionOptions<'a>,
601 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
602) -> Result<()> {
603 if is_select_query(sql)? {
604 let (columns, rows) =
605 collect_remote_streaming_rows(client, sql, options, cmd.fetch_size, cmd.max_rows)
606 .await?;
607 let app = TuiApp::new(columns, rows, "server", false)
608 .with_context_message(Some(sql_context_message(sql)))
609 .with_admin_launcher(admin_launcher);
610 return app.run();
611 }
612
613 let request = RemoteSqlRequest {
614 sql: sql.to_string(),
615 streaming: false,
616 fetch_size: cmd.fetch_size,
617 max_rows: cmd.max_rows,
618 };
619
620 let response: RemoteSqlResponse = tokio::select! {
621 result = tokio::time::timeout(options.deadline.remaining(), client.post_json("api/sql/query", &request)) => {
622 match result {
623 Ok(value) => value.map_err(map_client_error)?,
624 Err(_) => {
625 let _ = send_cancel_request(client).await;
626 return Err(CliError::Timeout(format!(
627 "deadline exceeded after {}",
628 humantime::format_duration(options.deadline.duration())
629 )));
630 }
631 }
632 }
633 _ = options.cancel.wait() => {
634 let _ = send_cancel_request(client).await;
635 return Err(CliError::Cancelled);
636 }
637 };
638
639 let (columns, rows) = if response.columns.is_empty() {
640 let columns = sql_status_columns();
641 let message = match response.affected_rows {
642 Some(count) => format!("{count} row(s) affected"),
643 None => "Operation completed successfully".to_string(),
644 };
645 let row = Row::new(vec![Value::Text("OK".to_string()), Value::Text(message)]);
646 (columns, vec![row])
647 } else {
648 let columns: Vec<Column> = response
649 .columns
650 .iter()
651 .map(|col| Column::new(&col.name, data_type_from_string(&col.data_type)))
652 .collect();
653 let mut rows = response
654 .rows
655 .into_iter()
656 .map(|row| Row::new(row.into_iter().map(remote_value_to_value).collect()))
657 .collect::<Vec<_>>();
658 if let Some(limit) = options.limit {
659 rows.truncate(limit);
660 }
661 (columns, rows)
662 };
663
664 let app = TuiApp::new(columns, rows, "server", false)
665 .with_context_message(Some(sql_context_message(sql)))
666 .with_admin_launcher(admin_launcher);
667 app.run()
668}
669
670async fn collect_remote_streaming_rows(
671 client: &HttpClient,
672 sql: &str,
673 options: &SqlExecutionOptions<'_>,
674 fetch_size: Option<usize>,
675 max_rows: Option<usize>,
676) -> Result<(Vec<Column>, Vec<Row>)> {
677 let request = RemoteSqlRequest {
678 sql: sql.to_string(),
679 streaming: true,
680 fetch_size,
681 max_rows,
682 };
683
684 let response = tokio::select! {
685 result = tokio::time::timeout(options.deadline.remaining(), client.post_json_stream("api/sql/query", &request)) => {
686 match result {
687 Ok(value) => value.map_err(map_client_error)?,
688 Err(_) => {
689 let _ = send_cancel_request(client).await;
690 return Err(CliError::Timeout(format!(
691 "deadline exceeded after {}",
692 humantime::format_duration(options.deadline.duration())
693 )));
694 }
695 }
696 }
697 _ = options.cancel.wait() => {
698 let _ = send_cancel_request(client).await;
699 return Err(CliError::Cancelled);
700 }
701 };
702
703 if let Some(content_type) = response
704 .headers()
705 .get(reqwest::header::CONTENT_TYPE)
706 .and_then(|value| value.to_str().ok())
707 {
708 if content_type.starts_with("application/jsonl") {
709 return collect_remote_jsonl_rows(client, response, options).await;
710 }
711 }
712
713 let mut stream = response.bytes_stream();
714 let mut buffer: Vec<u8> = Vec::new();
715 let mut pos: usize = 0;
716 let mut done = false;
717 let mut saw_array_start = false;
718 let mut columns: Option<Vec<String>> = None;
719 let mut column_set: Option<HashSet<String>> = None;
720 let mut rows: Vec<Row> = Vec::new();
721
722 while !done {
723 if options.cancel.is_cancelled() {
724 let _ = send_cancel_request(client).await;
725 return Err(CliError::Cancelled);
726 }
727 if let Err(err) = options.deadline.check() {
728 let _ = send_cancel_request(client).await;
729 return Err(err);
730 }
731
732 let next = tokio::select! {
733 _ = options.cancel.wait() => {
734 let _ = send_cancel_request(client).await;
735 return Err(CliError::Cancelled);
736 }
737 result = tokio::time::timeout(options.deadline.remaining(), stream.next()) => {
738 match result {
739 Ok(value) => value,
740 Err(_) => {
741 let _ = send_cancel_request(client).await;
742 return Err(CliError::Timeout(format!(
743 "deadline exceeded after {}",
744 humantime::format_duration(options.deadline.duration())
745 )));
746 }
747 }
748 }
749 };
750
751 let chunk = match next {
752 Some(chunk) => chunk,
753 None => break,
754 };
755
756 let bytes = match chunk {
757 Ok(bytes) => bytes,
758 Err(err) => return Err(CliError::ServerConnection(format!("request failed: {err}"))),
759 };
760
761 buffer.extend_from_slice(&bytes);
762
763 loop {
764 skip_whitespace(&buffer, &mut pos);
765 if pos >= buffer.len() {
766 break;
767 }
768
769 if !saw_array_start {
770 if buffer[pos] != b'[' {
771 return Err(CliError::InvalidArgument(
772 "Invalid streaming response: expected JSON array".into(),
773 ));
774 }
775 pos += 1;
776 saw_array_start = true;
777 continue;
778 }
779
780 skip_whitespace(&buffer, &mut pos);
781 if pos >= buffer.len() {
782 break;
783 }
784
785 if buffer[pos] == b']' {
786 pos += 1;
787 done = true;
788 break;
789 }
790
791 let slice = &buffer[pos..];
792 let mut stream =
793 serde_json::Deserializer::from_slice(slice).into_iter::<serde_json::Value>();
794 let value = match stream.next() {
795 Some(Ok(value)) => value,
796 Some(Err(err)) if err.is_eof() => break,
797 Some(Err(err)) => return Err(CliError::Json(err)),
798 None => break,
799 };
800 pos = pos.saturating_add(stream.byte_offset());
801
802 let object = value.as_object().ok_or_else(|| {
803 CliError::InvalidArgument("Invalid streaming row: expected JSON object".into())
804 })?;
805
806 if columns.is_none() {
807 let names: Vec<String> = object.keys().cloned().collect();
808 let set: HashSet<String> = names.iter().cloned().collect();
809 if names.is_empty() {
810 return Err(CliError::InvalidArgument(
811 "Invalid streaming row: empty object".into(),
812 ));
813 }
814 columns = Some(names);
815 column_set = Some(set);
816 }
817
818 let names = columns
819 .as_ref()
820 .ok_or_else(|| CliError::InvalidArgument("Missing columns".into()))?;
821 let set = column_set
822 .as_ref()
823 .ok_or_else(|| CliError::InvalidArgument("Missing column set".into()))?;
824
825 if object.len() != names.len() || !object.keys().all(|key| set.contains(key)) {
826 return Err(CliError::InvalidArgument(
827 "Invalid streaming row: column mismatch".into(),
828 ));
829 }
830
831 let values = names
832 .iter()
833 .map(|name| {
834 object.get(name).ok_or_else(|| {
835 CliError::InvalidArgument(format!(
836 "Invalid streaming row: missing column '{name}'"
837 ))
838 })
839 })
840 .map(|value| value.and_then(json_value_to_value))
841 .collect::<Result<Vec<_>>>()?;
842 rows.push(Row::new(values));
843
844 if let Some(limit) = options.limit {
845 if rows.len() >= limit {
846 let _ = send_cancel_request(client).await;
847 done = true;
848 break;
849 }
850 }
851
852 skip_whitespace(&buffer, &mut pos);
853 if pos >= buffer.len() {
854 break;
855 }
856 match buffer[pos] {
857 b',' => {
858 pos += 1;
859 }
860 b']' => {
861 pos += 1;
862 done = true;
863 break;
864 }
865 _ => {
866 return Err(CliError::InvalidArgument(
867 "Invalid streaming response: expected ',' or ']'".into(),
868 ))
869 }
870 }
871 }
872
873 if pos > 0 {
874 buffer.drain(..pos);
875 pos = 0;
876 }
877 }
878
879 if done {
880 if has_non_whitespace(&buffer) {
881 return Err(CliError::InvalidArgument(
882 "Invalid streaming response: unexpected trailing data".into(),
883 ));
884 }
885 buffer.clear();
886 loop {
887 let next = tokio::select! {
888 _ = options.cancel.wait() => {
889 let _ = send_cancel_request(client).await;
890 return Err(CliError::Cancelled);
891 }
892 result = tokio::time::timeout(options.deadline.remaining(), stream.next()) => {
893 match result {
894 Ok(value) => value,
895 Err(_) => {
896 let _ = send_cancel_request(client).await;
897 return Err(CliError::Timeout(format!(
898 "deadline exceeded after {}",
899 humantime::format_duration(options.deadline.duration())
900 )));
901 }
902 }
903 }
904 };
905
906 let chunk = match next {
907 Some(chunk) => chunk,
908 None => break,
909 };
910
911 let bytes = match chunk {
912 Ok(bytes) => bytes,
913 Err(err) => {
914 return Err(CliError::ServerConnection(format!("request failed: {err}")))
915 }
916 };
917
918 if has_non_whitespace(&bytes) {
919 return Err(CliError::InvalidArgument(
920 "Invalid streaming response: unexpected trailing data".into(),
921 ));
922 }
923 }
924 } else {
925 skip_whitespace(&buffer, &mut pos);
926 if pos < buffer.len() {
927 return Err(CliError::InvalidArgument(
928 "Invalid streaming response: unexpected trailing data".into(),
929 ));
930 }
931 return Err(CliError::InvalidArgument(
932 "Invalid streaming response: unexpected end of stream".into(),
933 ));
934 }
935
936 let columns = columns
937 .unwrap_or_default()
938 .into_iter()
939 .map(|name| Column::new(name, DataType::Text))
940 .collect();
941 Ok((columns, rows))
942}
943
944#[allow(dead_code)]
945async fn collect_remote_non_streaming_rows(
946 client: &HttpClient,
947 sql: &str,
948 options: &SqlExecutionOptions<'_>,
949 fetch_size: Option<usize>,
950 max_rows: Option<usize>,
951) -> Result<(Vec<Column>, Vec<Row>)> {
952 let request = RemoteSqlRequest {
953 sql: sql.to_string(),
954 streaming: false,
955 fetch_size,
956 max_rows,
957 };
958 let response: RemoteSqlResponse = tokio::select! {
959 result = tokio::time::timeout(options.deadline.remaining(), client.post_json("api/sql/query", &request)) => {
960 match result {
961 Ok(value) => value.map_err(map_client_error)?,
962 Err(_) => {
963 let _ = send_cancel_request(client).await;
964 return Err(CliError::Timeout(format!(
965 "deadline exceeded after {}",
966 humantime::format_duration(options.deadline.duration())
967 )));
968 }
969 }
970 }
971 _ = options.cancel.wait() => {
972 let _ = send_cancel_request(client).await;
973 return Err(CliError::Cancelled);
974 }
975 };
976
977 if response.columns.is_empty() {
978 let message = match response.affected_rows {
979 Some(count) => format!("{count} row(s) affected"),
980 None => "Operation completed successfully".to_string(),
981 };
982 let columns = sql_status_columns();
983 let row = Row::new(vec![Value::Text("OK".to_string()), Value::Text(message)]);
984 return Ok((columns, vec![row]));
985 }
986
987 let columns = response
988 .columns
989 .iter()
990 .map(|col| Column::new(&col.name, data_type_from_string(&col.data_type)))
991 .collect::<Vec<_>>();
992 let mut rows = response
993 .rows
994 .into_iter()
995 .map(|row| Row::new(row.into_iter().map(remote_value_to_value).collect()))
996 .collect::<Vec<_>>();
997 if let Some(limit) = options.limit {
998 rows.truncate(limit);
999 }
1000 Ok((columns, rows))
1001}
1002
1003async fn execute_remote_streaming<W: Write>(
1004 client: &HttpClient,
1005 sql: &str,
1006 writer: &mut W,
1007 output: &mut SqlOutput<'_>,
1008 options: &SqlExecutionOptions<'_>,
1009 fetch_size: Option<usize>,
1010 max_rows: Option<usize>,
1011) -> Result<()> {
1012 let request = RemoteSqlRequest {
1013 sql: sql.to_string(),
1014 streaming: true,
1015 fetch_size,
1016 max_rows,
1017 };
1018
1019 let response = tokio::select! {
1020 result = tokio::time::timeout(options.deadline.remaining(), client.post_json_stream("api/sql/query", &request)) => {
1021 match result {
1022 Ok(value) => value.map_err(map_client_error)?,
1023 Err(_) => {
1024 let _ = send_cancel_request(client).await;
1025 return Err(CliError::Timeout(format!(
1026 "deadline exceeded after {}",
1027 humantime::format_duration(options.deadline.duration())
1028 )));
1029 }
1030 }
1031 }
1032 _ = options.cancel.wait() => {
1033 let _ = send_cancel_request(client).await;
1034 return Err(CliError::Cancelled);
1035 }
1036 };
1037
1038 if !output.statement_array() {
1042 return stream_remote_result_set(
1043 client,
1044 response,
1045 writer,
1046 output.create_block_formatter(),
1047 options,
1048 )
1049 .await;
1050 }
1051
1052 writeln!(writer, "[")?;
1053 let mut result_set_writer = CountingWriter::new(&mut *writer);
1054 let result = stream_remote_result_set(
1055 client,
1056 response,
1057 &mut result_set_writer,
1058 output.create_block_formatter(),
1059 options,
1060 )
1061 .await;
1062 let result_set_started = result_set_writer.bytes_written() > 0;
1063 match result {
1064 Ok(()) => {
1065 writeln!(writer, "]")?;
1066 Ok(())
1067 }
1068 Err(err) => {
1069 if result_set_started {
1074 let _ = writeln!(writer, "]");
1075 }
1076 let _ = writeln!(writer, "]");
1077 Err(err)
1078 }
1079 }
1080}
1081
1082async fn stream_remote_result_set<W: Write>(
1084 client: &HttpClient,
1085 response: Response,
1086 writer: &mut W,
1087 formatter: Box<dyn Formatter>,
1088 options: &SqlExecutionOptions<'_>,
1089) -> Result<()> {
1090 if let Some(content_type) = response
1091 .headers()
1092 .get(reqwest::header::CONTENT_TYPE)
1093 .and_then(|value| value.to_str().ok())
1094 {
1095 if content_type.starts_with("application/jsonl") {
1096 return execute_remote_jsonl_streaming(client, response, writer, formatter, options)
1097 .await;
1098 }
1099 }
1100
1101 let mut stream = response.bytes_stream();
1102 let mut buffer: Vec<u8> = Vec::new();
1103 let mut pos: usize = 0;
1104 let mut streaming_writer: Option<StreamingWriter<&mut W>> = None;
1105 let mut formatter = Some(formatter);
1106 let mut columns: Option<Vec<String>> = None;
1107 let mut column_set: Option<HashSet<String>> = None;
1108 let mut done = false;
1109 let mut saw_array_start = false;
1110
1111 while !done {
1112 if options.cancel.is_cancelled() {
1113 let _ = send_cancel_request(client).await;
1114 return Err(CliError::Cancelled);
1115 }
1116 if let Err(err) = options.deadline.check() {
1117 let _ = send_cancel_request(client).await;
1118 return Err(err);
1119 }
1120
1121 let next = tokio::select! {
1122 _ = options.cancel.wait() => {
1123 let _ = send_cancel_request(client).await;
1124 return Err(CliError::Cancelled);
1125 }
1126 result = tokio::time::timeout(options.deadline.remaining(), stream.next()) => {
1127 match result {
1128 Ok(value) => value,
1129 Err(_) => {
1130 let _ = send_cancel_request(client).await;
1131 return Err(CliError::Timeout(format!(
1132 "deadline exceeded after {}",
1133 humantime::format_duration(options.deadline.duration())
1134 )));
1135 }
1136 }
1137 }
1138 };
1139
1140 let chunk = match next {
1141 Some(chunk) => chunk,
1142 None => break,
1143 };
1144
1145 let bytes = match chunk {
1146 Ok(bytes) => bytes,
1147 Err(err) => return Err(CliError::ServerConnection(format!("request failed: {err}"))),
1148 };
1149
1150 buffer.extend_from_slice(&bytes);
1151
1152 loop {
1153 skip_whitespace(&buffer, &mut pos);
1154 if pos >= buffer.len() {
1155 break;
1156 }
1157
1158 if !saw_array_start {
1159 if buffer[pos] != b'[' {
1160 return Err(CliError::InvalidArgument(
1161 "Invalid streaming response: expected JSON array".into(),
1162 ));
1163 }
1164 pos += 1;
1165 saw_array_start = true;
1166 continue;
1167 }
1168
1169 skip_whitespace(&buffer, &mut pos);
1170 if pos >= buffer.len() {
1171 break;
1172 }
1173
1174 if buffer[pos] == b']' {
1175 pos += 1;
1176 done = true;
1177 break;
1178 }
1179
1180 let slice = &buffer[pos..];
1181 let mut stream =
1182 serde_json::Deserializer::from_slice(slice).into_iter::<serde_json::Value>();
1183 let value = match stream.next() {
1184 Some(Ok(value)) => value,
1185 Some(Err(err)) if err.is_eof() => break,
1186 Some(Err(err)) => return Err(CliError::Json(err)),
1187 None => break,
1188 };
1189 pos = pos.saturating_add(stream.byte_offset());
1190
1191 let object = value.as_object().ok_or_else(|| {
1192 CliError::InvalidArgument("Invalid streaming row: expected JSON object".into())
1193 })?;
1194
1195 if columns.is_none() {
1196 let names: Vec<String> = object.keys().cloned().collect();
1197 let set: HashSet<String> = names.iter().cloned().collect();
1198 if names.is_empty() {
1199 return Err(CliError::InvalidArgument(
1200 "Invalid streaming row: empty object".into(),
1201 ));
1202 }
1203 let cols = names
1204 .iter()
1205 .map(|name| Column::new(name, DataType::Text))
1206 .collect::<Vec<_>>();
1207 let formatter = formatter
1208 .take()
1209 .ok_or_else(|| CliError::InvalidArgument("Missing formatter".into()))?;
1210 let mut writer = StreamingWriter::new(&mut *writer, formatter, cols, options.limit)
1211 .with_quiet(options.quiet);
1212 writer.prepare(None)?;
1213 streaming_writer = Some(writer);
1214 columns = Some(names);
1215 column_set = Some(set);
1216 }
1217
1218 let names = columns
1219 .as_ref()
1220 .ok_or_else(|| CliError::InvalidArgument("Missing columns".into()))?;
1221 let set = column_set
1222 .as_ref()
1223 .ok_or_else(|| CliError::InvalidArgument("Missing column set".into()))?;
1224
1225 if object.len() != names.len() || !object.keys().all(|key| set.contains(key)) {
1226 return Err(CliError::InvalidArgument(
1227 "Invalid streaming row: column mismatch".into(),
1228 ));
1229 }
1230
1231 let values = names
1232 .iter()
1233 .map(|name| {
1234 object.get(name).ok_or_else(|| {
1235 CliError::InvalidArgument(format!(
1236 "Invalid streaming row: missing column '{name}'"
1237 ))
1238 })
1239 })
1240 .map(|value| value.and_then(json_value_to_value))
1241 .collect::<Result<Vec<_>>>()?;
1242
1243 if let Some(writer) = streaming_writer.as_mut() {
1244 match writer.write_row(Row::new(values))? {
1245 WriteStatus::LimitReached => {
1246 let _ = send_cancel_request(client).await;
1247 return writer.finish();
1248 }
1249 WriteStatus::Continue => {}
1250 }
1251 }
1252
1253 skip_whitespace(&buffer, &mut pos);
1254 if pos >= buffer.len() {
1255 break;
1256 }
1257 match buffer[pos] {
1258 b',' => {
1259 pos += 1;
1260 }
1261 b']' => {
1262 pos += 1;
1263 done = true;
1264 break;
1265 }
1266 _ => {
1267 return Err(CliError::InvalidArgument(
1268 "Invalid streaming response: expected ',' or ']'".into(),
1269 ))
1270 }
1271 }
1272 }
1273
1274 if pos > 0 {
1275 buffer.drain(..pos);
1276 pos = 0;
1277 }
1278 }
1279
1280 if done {
1281 if has_non_whitespace(&buffer) {
1282 return Err(CliError::InvalidArgument(
1283 "Invalid streaming response: unexpected trailing data".into(),
1284 ));
1285 }
1286 buffer.clear();
1287 loop {
1288 let next = tokio::select! {
1289 _ = options.cancel.wait() => {
1290 let _ = send_cancel_request(client).await;
1291 return Err(CliError::Cancelled);
1292 }
1293 result = tokio::time::timeout(options.deadline.remaining(), stream.next()) => {
1294 match result {
1295 Ok(value) => value,
1296 Err(_) => {
1297 let _ = send_cancel_request(client).await;
1298 return Err(CliError::Timeout(format!(
1299 "deadline exceeded after {}",
1300 humantime::format_duration(options.deadline.duration())
1301 )));
1302 }
1303 }
1304 }
1305 };
1306
1307 let chunk = match next {
1308 Some(chunk) => chunk,
1309 None => break,
1310 };
1311
1312 let bytes = match chunk {
1313 Ok(bytes) => bytes,
1314 Err(err) => {
1315 return Err(CliError::ServerConnection(format!("request failed: {err}")))
1316 }
1317 };
1318
1319 if has_non_whitespace(&bytes) {
1320 return Err(CliError::InvalidArgument(
1321 "Invalid streaming response: unexpected trailing data".into(),
1322 ));
1323 }
1324 }
1325 } else {
1326 skip_whitespace(&buffer, &mut pos);
1327 if pos < buffer.len() {
1328 return Err(CliError::InvalidArgument(
1329 "Invalid streaming response: unexpected trailing data".into(),
1330 ));
1331 }
1332 return Err(CliError::InvalidArgument(
1333 "Invalid streaming response: unexpected end of stream".into(),
1334 ));
1335 }
1336
1337 if let Some(mut writer) = streaming_writer {
1338 return writer.finish();
1339 }
1340
1341 if done && saw_array_start {
1342 if let Some(formatter) = formatter.take() {
1343 let mut writer =
1344 StreamingWriter::new(&mut *writer, formatter, Vec::new(), options.limit)
1345 .with_quiet(options.quiet);
1346 writer.prepare(None)?;
1347 return writer.finish();
1348 }
1349 }
1350
1351 Ok(())
1352}
1353
1354async fn collect_remote_jsonl_rows(
1355 client: &HttpClient,
1356 response: Response,
1357 options: &SqlExecutionOptions<'_>,
1358) -> Result<(Vec<Column>, Vec<Row>)> {
1359 let mut stream = response.bytes_stream();
1360 let mut buffer: Vec<u8> = Vec::new();
1361 let mut columns: Option<Vec<Column>> = None;
1362 let mut rows: Vec<Row> = Vec::new();
1363 let mut done = false;
1364
1365 while !done {
1366 if options.cancel.is_cancelled() {
1367 let _ = send_cancel_request(client).await;
1368 return Err(CliError::Cancelled);
1369 }
1370 if let Err(err) = options.deadline.check() {
1371 let _ = send_cancel_request(client).await;
1372 return Err(err);
1373 }
1374
1375 let next = tokio::select! {
1376 _ = options.cancel.wait() => {
1377 let _ = send_cancel_request(client).await;
1378 return Err(CliError::Cancelled);
1379 }
1380 result = tokio::time::timeout(options.deadline.remaining(), stream.next()) => {
1381 match result {
1382 Ok(value) => value,
1383 Err(_) => {
1384 let _ = send_cancel_request(client).await;
1385 return Err(CliError::Timeout(format!(
1386 "deadline exceeded after {}",
1387 humantime::format_duration(options.deadline.duration())
1388 )));
1389 }
1390 }
1391 }
1392 };
1393
1394 let chunk = match next {
1395 Some(chunk) => chunk,
1396 None => break,
1397 };
1398
1399 let bytes = match chunk {
1400 Ok(bytes) => bytes,
1401 Err(err) => return Err(CliError::ServerConnection(format!("request failed: {err}"))),
1402 };
1403
1404 buffer.extend_from_slice(&bytes);
1405
1406 while let Some(newline) = buffer.iter().position(|&b| b == b'\n') {
1407 let line = buffer.drain(..=newline).collect::<Vec<u8>>();
1408 if let Some(item) = parse_jsonl_line(&line)? {
1409 if let Some(error) = item.error {
1410 return Err(CliError::InvalidArgument(format!(
1411 "Server error: {}",
1412 error.message
1413 )));
1414 }
1415 if item.done {
1416 done = true;
1417 break;
1418 }
1419 let row = item.row.ok_or_else(|| {
1420 CliError::InvalidArgument("Invalid streaming response: missing row".into())
1421 })?;
1422 if columns.is_none() {
1423 columns = Some(default_stream_columns(row.len()));
1424 }
1425 rows.push(Row::new(
1426 row.into_iter().map(remote_value_to_value).collect(),
1427 ));
1428 if let Some(limit) = options.limit {
1429 if rows.len() >= limit {
1430 let _ = send_cancel_request(client).await;
1431 done = true;
1432 break;
1433 }
1434 }
1435 }
1436 }
1437 }
1438
1439 if !done {
1440 if let Some(item) = parse_jsonl_line(&buffer)? {
1441 if let Some(error) = item.error {
1442 return Err(CliError::InvalidArgument(format!(
1443 "Server error: {}",
1444 error.message
1445 )));
1446 }
1447 if item.done {
1448 done = true;
1449 } else if let Some(row) = item.row {
1450 if columns.is_none() {
1451 columns = Some(default_stream_columns(row.len()));
1452 }
1453 rows.push(Row::new(
1454 row.into_iter().map(remote_value_to_value).collect(),
1455 ));
1456 if let Some(limit) = options.limit {
1457 if rows.len() >= limit {
1458 let _ = send_cancel_request(client).await;
1459 done = true;
1460 }
1461 }
1462 } else {
1463 return Err(CliError::InvalidArgument(
1464 "Invalid streaming response: missing row".into(),
1465 ));
1466 }
1467 }
1468 }
1469
1470 if !done {
1471 return Err(CliError::InvalidArgument(
1472 "Invalid streaming response: unexpected end of stream".into(),
1473 ));
1474 }
1475
1476 Ok((columns.unwrap_or_default(), rows))
1477}
1478
1479async fn execute_remote_jsonl_streaming<W: Write>(
1480 client: &HttpClient,
1481 response: Response,
1482 writer: &mut W,
1483 formatter: Box<dyn Formatter>,
1484 options: &SqlExecutionOptions<'_>,
1485) -> Result<()> {
1486 let mut stream = response.bytes_stream();
1487 let mut buffer: Vec<u8> = Vec::new();
1488 let mut streaming_writer: Option<StreamingWriter<&mut W>> = None;
1489 let mut formatter = Some(formatter);
1490 let mut done = false;
1491
1492 while !done {
1493 if options.cancel.is_cancelled() {
1494 let _ = send_cancel_request(client).await;
1495 return Err(CliError::Cancelled);
1496 }
1497 if let Err(err) = options.deadline.check() {
1498 let _ = send_cancel_request(client).await;
1499 return Err(err);
1500 }
1501
1502 let next = tokio::select! {
1503 _ = options.cancel.wait() => {
1504 let _ = send_cancel_request(client).await;
1505 return Err(CliError::Cancelled);
1506 }
1507 result = tokio::time::timeout(options.deadline.remaining(), stream.next()) => {
1508 match result {
1509 Ok(value) => value,
1510 Err(_) => {
1511 let _ = send_cancel_request(client).await;
1512 return Err(CliError::Timeout(format!(
1513 "deadline exceeded after {}",
1514 humantime::format_duration(options.deadline.duration())
1515 )));
1516 }
1517 }
1518 }
1519 };
1520
1521 let chunk = match next {
1522 Some(chunk) => chunk,
1523 None => break,
1524 };
1525
1526 let bytes = match chunk {
1527 Ok(bytes) => bytes,
1528 Err(err) => return Err(CliError::ServerConnection(format!("request failed: {err}"))),
1529 };
1530
1531 buffer.extend_from_slice(&bytes);
1532
1533 while let Some(newline) = buffer.iter().position(|&b| b == b'\n') {
1534 let line = buffer.drain(..=newline).collect::<Vec<u8>>();
1535 if let Some(item) = parse_jsonl_line(&line)? {
1536 if let Some(error) = item.error {
1537 return Err(CliError::InvalidArgument(format!(
1538 "Server error: {}",
1539 error.message
1540 )));
1541 }
1542 if item.done {
1543 done = true;
1544 break;
1545 }
1546 let row = item.row.ok_or_else(|| {
1547 CliError::InvalidArgument("Invalid streaming response: missing row".into())
1548 })?;
1549 if streaming_writer.is_none() {
1550 let columns = default_stream_columns(row.len());
1551 let formatter = formatter
1552 .take()
1553 .ok_or_else(|| CliError::InvalidArgument("Missing formatter".into()))?;
1554 let mut writer =
1555 StreamingWriter::new(&mut *writer, formatter, columns, options.limit)
1556 .with_quiet(options.quiet);
1557 writer.prepare(None)?;
1558 streaming_writer = Some(writer);
1559 }
1560 let values = row.into_iter().map(remote_value_to_value).collect();
1561 if let Some(writer) = streaming_writer.as_mut() {
1562 match writer.write_row(Row::new(values))? {
1563 WriteStatus::LimitReached => {
1564 let _ = send_cancel_request(client).await;
1565 done = true;
1566 break;
1567 }
1568 WriteStatus::Continue => {}
1569 }
1570 }
1571 }
1572 }
1573 }
1574
1575 if !done {
1576 if let Some(item) = parse_jsonl_line(&buffer)? {
1577 if let Some(error) = item.error {
1578 return Err(CliError::InvalidArgument(format!(
1579 "Server error: {}",
1580 error.message
1581 )));
1582 }
1583 if item.done {
1584 done = true;
1585 } else if let Some(row) = item.row {
1586 if streaming_writer.is_none() {
1587 let columns = default_stream_columns(row.len());
1588 let formatter = formatter
1589 .take()
1590 .ok_or_else(|| CliError::InvalidArgument("Missing formatter".into()))?;
1591 let mut writer =
1592 StreamingWriter::new(&mut *writer, formatter, columns, options.limit)
1593 .with_quiet(options.quiet);
1594 writer.prepare(None)?;
1595 streaming_writer = Some(writer);
1596 }
1597 let values = row.into_iter().map(remote_value_to_value).collect();
1598 if let Some(writer) = streaming_writer.as_mut() {
1599 match writer.write_row(Row::new(values))? {
1600 WriteStatus::LimitReached => {
1601 let _ = send_cancel_request(client).await;
1602 done = true;
1603 }
1604 WriteStatus::Continue => {}
1605 }
1606 }
1607 } else {
1608 return Err(CliError::InvalidArgument(
1609 "Invalid streaming response: missing row".into(),
1610 ));
1611 }
1612 }
1613 }
1614
1615 if !done {
1616 return Err(CliError::InvalidArgument(
1617 "Invalid streaming response: unexpected end of stream".into(),
1618 ));
1619 }
1620
1621 if let Some(mut writer) = streaming_writer {
1622 return writer.finish();
1623 }
1624
1625 if let Some(formatter) = formatter.take() {
1626 let mut writer = StreamingWriter::new(&mut *writer, formatter, Vec::new(), options.limit)
1627 .with_quiet(options.quiet);
1628 writer.prepare(None)?;
1629 return writer.finish();
1630 }
1631
1632 Ok(())
1633}
1634
1635fn default_stream_columns(count: usize) -> Vec<Column> {
1636 (0..count)
1637 .map(|idx| Column::new(format!("col{}", idx + 1), DataType::Text))
1638 .collect()
1639}
1640
1641fn parse_jsonl_line(line: &[u8]) -> Result<Option<RemoteStreamItem>> {
1642 if line
1643 .iter()
1644 .all(|b| matches!(b, b' ' | b'\n' | b'\r' | b'\t'))
1645 {
1646 return Ok(None);
1647 }
1648 let text = std::str::from_utf8(line)
1649 .map_err(|err| CliError::InvalidArgument(format!("Invalid UTF-8: {err}")))?;
1650 let trimmed = text.trim();
1651 if trimmed.is_empty() {
1652 return Ok(None);
1653 }
1654 let item = serde_json::from_str::<RemoteStreamItem>(trimmed)?;
1655 Ok(Some(item))
1656}
1657
1658fn skip_whitespace(buffer: &[u8], pos: &mut usize) {
1659 while *pos < buffer.len() {
1660 match buffer[*pos] {
1661 b' ' | b'\n' | b'\r' | b'\t' => *pos += 1,
1662 _ => break,
1663 }
1664 }
1665}
1666
1667fn has_non_whitespace(buffer: &[u8]) -> bool {
1668 buffer
1669 .iter()
1670 .any(|byte| !matches!(byte, b' ' | b'\n' | b'\r' | b'\t'))
1671}
1672
1673fn json_value_to_value(value: &serde_json::Value) -> Result<Value> {
1674 match value {
1675 serde_json::Value::Null => Ok(Value::Null),
1676 serde_json::Value::Bool(value) => Ok(Value::Bool(*value)),
1677 serde_json::Value::Number(value) => {
1678 if let Some(value) = value.as_i64() {
1679 Ok(Value::Int(value))
1680 } else if let Some(value) = value.as_f64() {
1681 Ok(Value::Float(value))
1682 } else {
1683 Err(CliError::InvalidArgument(
1684 "Invalid numeric value in streaming row".into(),
1685 ))
1686 }
1687 }
1688 serde_json::Value::String(value) => Ok(Value::Text(value.clone())),
1689 serde_json::Value::Array(values) => {
1690 let mut vector = Vec::with_capacity(values.len());
1691 for entry in values {
1692 let number = entry.as_f64().ok_or_else(|| {
1693 CliError::InvalidArgument("Invalid vector value in streaming row".into())
1694 })?;
1695 vector.push(number as f32);
1696 }
1697 Ok(Value::Vector(vector))
1698 }
1699 serde_json::Value::Object(_) => Err(CliError::InvalidArgument(
1700 "Invalid streaming row: nested objects are not supported".into(),
1701 )),
1702 }
1703}
1704
1705#[allow(dead_code)]
1707pub fn execute<W: Write>(
1708 db: &Database,
1709 cmd: SqlCommand,
1710 batch_mode: &BatchMode,
1711 writer: &mut StreamingWriter<W>,
1712) -> Result<()> {
1713 let sql = cmd.resolve_query(batch_mode)?;
1714
1715 execute_sql(db, &sql, writer)
1716}
1717
1718impl SqlCommand {
1719 pub fn resolve_query(&self, batch_mode: &BatchMode) -> Result<String> {
1721 match (&self.query, &self.file) {
1722 (Some(query), None) => Ok(query.clone()),
1723 (None, Some(file)) => fs::read_to_string(file).map_err(|e| {
1724 CliError::InvalidArgument(format!("Failed to read SQL file '{}': {}", file, e))
1725 }),
1726 (None, None) if !batch_mode.is_tty => {
1727 let mut buf = String::new();
1728 io::stdin().read_to_string(&mut buf)?;
1729 Ok(buf)
1730 }
1731 (None, None) => Err(CliError::NoQueryProvided),
1732 (Some(_), Some(_)) => Err(CliError::InvalidArgument(
1733 "Cannot specify both query and file".to_string(),
1734 )),
1735 }
1736 }
1737}
1738
1739fn execute_sql<W: Write>(db: &Database, sql: &str, writer: &mut StreamingWriter<W>) -> Result<()> {
1741 use alopex_embedded::SqlResult;
1742
1743 let result = db.execute_sql(sql)?;
1744
1745 match result {
1746 SqlResult::Success => {
1747 writer.prepare(Some(1))?;
1749 let row = Row::new(vec![
1750 Value::Text("OK".to_string()),
1751 Value::Text("Operation completed successfully".to_string()),
1752 ]);
1753 writer.write_row(row)?;
1754 writer.finish()?;
1755 }
1756 SqlResult::RowsAffected(count) => {
1757 writer.prepare(Some(1))?;
1759 let row = Row::new(vec![
1760 Value::Text("OK".to_string()),
1761 Value::Text(format!("{} row(s) affected", count)),
1762 ]);
1763 writer.write_row(row)?;
1764 writer.finish()?;
1765 }
1766 SqlResult::Query(query_result) => {
1767 let row_count = query_result.rows.len();
1769 writer.prepare(Some(row_count))?;
1770
1771 for sql_row in query_result.rows {
1772 let values: Vec<Value> = sql_row.into_iter().map(sql_value_to_value).collect();
1773 let row = Row::new(values);
1774
1775 match writer.write_row(row)? {
1776 WriteStatus::LimitReached => break,
1777 WriteStatus::Continue => {}
1778 }
1779 }
1780
1781 writer.finish()?;
1782 }
1783 }
1784
1785 Ok(())
1786}
1787
1788fn execute_sql_with_formatter<W: Write>(
1803 db: &Database,
1804 sql: &str,
1805 writer: &mut W,
1806 output: &mut SqlOutput<'_>,
1807 options: &SqlExecutionOptions<'_>,
1808) -> Result<()> {
1809 use alopex_sql::{AlopexDialect, Parser, StatementKind};
1810
1811 let dialect = AlopexDialect;
1814 let stmts = Parser::parse_sql(&dialect, sql).map_err(|e| CliError::Parse(format!("{}", e)))?;
1815
1816 let is_single_select = stmts.len() == 1
1817 && matches!(
1818 stmts.first().map(|s| &s.kind),
1819 Some(StatementKind::Select(_))
1820 );
1821
1822 if is_single_select {
1823 let formatter = output.create_block_formatter();
1825 if !output.statement_array() {
1826 return execute_sql_select_streaming(db, sql, writer, formatter, options, false);
1827 }
1828 let mut result_set_writer = CountingWriter::new(&mut *writer);
1829 let result =
1830 execute_sql_select_streaming(db, sql, &mut result_set_writer, formatter, options, true);
1831 let output_started = result_set_writer.bytes_written() > 0;
1832 match result {
1833 Ok(()) => {
1834 writeln!(writer, "]")?;
1836 Ok(())
1837 }
1838 Err(err) => {
1839 if output_started {
1846 let _ = writeln!(writer, "]");
1847 let _ = writeln!(writer, "]");
1848 }
1849 Err(err)
1850 }
1851 }
1852 } else {
1853 execute_sql_statements(db, sql, writer, output, options)
1855 }
1856}
1857
1858fn execute_sql_select_streaming<W: Write>(
1868 db: &Database,
1869 sql: &str,
1870 writer: &mut W,
1871 formatter: Box<dyn Formatter>,
1872 options: &SqlExecutionOptions<'_>,
1873 open_statement_array: bool,
1874) -> Result<()> {
1875 use alopex_embedded::StreamingQueryResult;
1876 use std::sync::atomic::{AtomicBool, Ordering};
1877 use std::sync::Arc;
1878
1879 fn cli_err_to_embedded(e: crate::error::CliError) -> alopex_embedded::Error {
1881 alopex_embedded::Error::Sql(alopex_sql::SqlError::Execution {
1882 message: e.to_string(),
1883 code: "ALOPEX-C001",
1884 })
1885 }
1886
1887 let cancelled = Arc::new(AtomicBool::new(false));
1888 let timed_out = Arc::new(AtomicBool::new(false));
1889 let cancel_flag = cancelled.clone();
1890 let timeout_flag = timed_out.clone();
1891
1892 let result = db.execute_sql_with_rows(sql, |mut rows| {
1893 let columns = columns_from_streaming_rows(&rows);
1895 if open_statement_array {
1898 writeln!(writer, "[").map_err(|e| cli_err_to_embedded(CliError::Io(e)))?;
1899 }
1900 let mut streaming_writer = StreamingWriter::new(writer, formatter, columns, options.limit)
1901 .with_quiet(options.quiet);
1902
1903 streaming_writer
1905 .prepare(None)
1906 .map_err(cli_err_to_embedded)?;
1907
1908 if let Err(err) = options.deadline.check() {
1909 timeout_flag.store(true, Ordering::SeqCst);
1910 return Err(cli_err_to_embedded(err));
1911 }
1912
1913 while let Some(sql_row) = rows.next_row()? {
1917 if options.cancel.is_cancelled() {
1918 cancel_flag.store(true, Ordering::SeqCst);
1919 return Err(cli_err_to_embedded(CliError::Cancelled));
1920 }
1921 if let Err(err) = options.deadline.check() {
1922 timeout_flag.store(true, Ordering::SeqCst);
1923 return Err(cli_err_to_embedded(err));
1924 }
1925 let values: Vec<Value> = sql_row.into_iter().map(sql_value_to_value).collect();
1926 let row = Row::new(values);
1927
1928 match streaming_writer
1929 .write_row(row)
1930 .map_err(cli_err_to_embedded)?
1931 {
1932 WriteStatus::LimitReached => break,
1933 WriteStatus::Continue => {}
1934 }
1935 }
1936
1937 streaming_writer.finish().map_err(cli_err_to_embedded)?;
1938 Ok(())
1939 });
1940
1941 let result = match result {
1942 Ok(value) => value,
1943 Err(err) => {
1944 if cancelled.load(Ordering::SeqCst) {
1945 return Err(CliError::Cancelled);
1946 }
1947 if timed_out.load(Ordering::SeqCst) {
1948 return Err(CliError::Timeout(format!(
1949 "deadline exceeded after {}",
1950 humantime::format_duration(options.deadline.duration())
1951 )));
1952 }
1953 return Err(CliError::Database(err));
1954 }
1955 };
1956
1957 match result {
1958 StreamingQueryResult::QueryProcessed(()) => Ok(()),
1959 StreamingQueryResult::Success | StreamingQueryResult::RowsAffected(_) => {
1960 Ok(())
1962 }
1963 }
1964}
1965
1966#[derive(serde::Serialize)]
1967struct RemoteSqlRequest {
1968 sql: String,
1969 #[serde(default)]
1970 streaming: bool,
1971 #[serde(skip_serializing_if = "Option::is_none")]
1972 fetch_size: Option<usize>,
1973 #[serde(skip_serializing_if = "Option::is_none")]
1974 max_rows: Option<usize>,
1975}
1976
1977#[derive(serde::Deserialize)]
1978struct RemoteColumnInfo {
1979 name: String,
1980 data_type: String,
1981}
1982
1983#[derive(serde::Deserialize)]
1984struct RemoteSqlResponse {
1985 columns: Vec<RemoteColumnInfo>,
1986 rows: Vec<Vec<alopex_sql::storage::SqlValue>>,
1987 affected_rows: Option<u64>,
1988}
1989
1990#[derive(serde::Deserialize)]
1991struct RemoteStreamItem {
1992 row: Option<Vec<alopex_sql::storage::SqlValue>>,
1993 error: Option<RemoteStreamError>,
1994 #[serde(default)]
1995 done: bool,
1996}
1997
1998#[derive(serde::Deserialize)]
1999struct RemoteStreamError {
2000 message: String,
2001}
2002
2003fn map_client_error(err: ClientError) -> CliError {
2004 match err {
2005 ClientError::Request { source, .. } => {
2006 CliError::ServerConnection(format!("request failed: {source}"))
2007 }
2008 ClientError::InvalidUrl(message) => CliError::InvalidArgument(message),
2009 ClientError::Build(message) => CliError::InvalidArgument(message),
2010 ClientError::Auth(err) => CliError::InvalidArgument(err.to_string()),
2011 ClientError::HttpStatus { status, body } => {
2012 CliError::InvalidArgument(format!("Server error: HTTP {} - {}", status.as_u16(), body))
2013 }
2014 }
2015}
2016
2017async fn send_cancel_request(client: &HttpClient) -> Result<()> {
2018 #[derive(serde::Serialize)]
2019 struct CancelRequest {}
2020
2021 let request = CancelRequest {};
2022 let _: serde_json::Value = client
2023 .post_json("api/sql/cancel", &request)
2024 .await
2025 .map_err(map_client_error)?;
2026 Ok(())
2027}
2028
2029fn merge_limit(limit: Option<usize>, max_rows: Option<usize>) -> Option<usize> {
2030 match (limit, max_rows) {
2031 (Some(a), Some(b)) => Some(a.min(b)),
2032 (Some(value), None) | (None, Some(value)) => Some(value),
2033 (None, None) => None,
2034 }
2035}
2036
2037fn execute_sql_statements<W: Write>(
2050 db: &Database,
2051 sql: &str,
2052 writer: &mut W,
2053 output: &mut SqlOutput<'_>,
2054 options: &SqlExecutionOptions<'_>,
2055) -> Result<()> {
2056 use alopex_sql::ExecutionResult;
2057
2058 options.deadline.check()?;
2059 let results = db.execute_sql_multi(sql)?;
2060 options.deadline.check()?;
2061
2062 let json_array = output.statement_array();
2063 if json_array {
2064 writeln!(writer, "[")?;
2065 }
2066 let mut first = true;
2067 for result in results {
2068 let is_status = !matches!(result, ExecutionResult::Query(_));
2069 if is_status && options.quiet {
2070 continue;
2071 }
2072 if json_array && !first {
2073 writeln!(writer, ",")?;
2074 }
2075 first = false;
2076 emit_execution_result(result, writer, output, options)?;
2077 }
2078 if json_array {
2079 writeln!(writer, "]")?;
2080 }
2081 Ok(())
2082}
2083
2084fn emit_execution_result<W: Write>(
2086 result: alopex_sql::ExecutionResult,
2087 writer: &mut W,
2088 output: &mut SqlOutput<'_>,
2089 options: &SqlExecutionOptions<'_>,
2090) -> Result<()> {
2091 use alopex_sql::ExecutionResult;
2092
2093 match result {
2094 ExecutionResult::Success => emit_status_result(
2095 writer,
2096 output,
2097 options,
2098 "Operation completed successfully".to_string(),
2099 ),
2100 ExecutionResult::RowsAffected(count) => {
2101 emit_status_result(writer, output, options, format!("{count} row(s) affected"))
2102 }
2103 ExecutionResult::Query(query_result) => {
2104 let columns = columns_from_query_result(&query_result);
2105 let mut streaming_writer = StreamingWriter::new(
2106 &mut *writer,
2107 output.create_block_formatter(),
2108 columns,
2109 options.limit,
2110 )
2111 .with_quiet(options.quiet);
2112 streaming_writer.prepare(Some(query_result.rows.len()))?;
2113 for sql_row in query_result.rows {
2114 let values: Vec<Value> = sql_row.into_iter().map(sql_value_to_value).collect();
2115 match streaming_writer.write_row(Row::new(values))? {
2116 WriteStatus::LimitReached => break,
2117 WriteStatus::Continue => {}
2118 }
2119 }
2120 streaming_writer.finish()
2121 }
2122 }
2123}
2124
2125fn emit_status_result<W: Write>(
2127 writer: &mut W,
2128 output: &mut SqlOutput<'_>,
2129 options: &SqlExecutionOptions<'_>,
2130 message: String,
2131) -> Result<()> {
2132 let columns = sql_status_columns();
2133 let mut streaming_writer = StreamingWriter::new(
2134 &mut *writer,
2135 output.create_block_formatter(),
2136 columns,
2137 options.limit,
2138 )
2139 .with_quiet(options.quiet);
2140 streaming_writer.prepare(Some(1))?;
2141 streaming_writer.write_row(Row::new(vec![
2142 Value::Text("OK".to_string()),
2143 Value::Text(message),
2144 ]))?;
2145 streaming_writer.finish()
2146}
2147
2148fn sql_value_to_value(sql_value: alopex_sql::SqlValue) -> Value {
2150 use alopex_sql::SqlValue;
2151
2152 match sql_value {
2153 SqlValue::Null => Value::Null,
2154 SqlValue::Integer(i) => Value::Int(i as i64),
2155 SqlValue::BigInt(i) => Value::Int(i),
2156 SqlValue::Float(f) => Value::Float(f as f64),
2157 SqlValue::Double(f) => Value::Float(f),
2158 SqlValue::Text(s) => Value::Text(s),
2159 SqlValue::Blob(b) => Value::Bytes(b),
2160 SqlValue::Boolean(b) => Value::Bool(b),
2161 SqlValue::Timestamp(ts) => {
2162 Value::Text(format!("{}", ts))
2164 }
2165 SqlValue::Vector(v) => Value::Vector(v),
2166 }
2167}
2168
2169fn remote_value_to_value(sql_value: alopex_sql::storage::SqlValue) -> Value {
2170 use alopex_sql::storage::SqlValue;
2171
2172 match sql_value {
2173 SqlValue::Null => Value::Null,
2174 SqlValue::Integer(i) => Value::Int(i as i64),
2175 SqlValue::BigInt(i) => Value::Int(i),
2176 SqlValue::Float(f) => Value::Float(f as f64),
2177 SqlValue::Double(f) => Value::Float(f),
2178 SqlValue::Text(s) => Value::Text(s),
2179 SqlValue::Blob(b) => Value::Bytes(b),
2180 SqlValue::Boolean(b) => Value::Bool(b),
2181 SqlValue::Timestamp(ts) => Value::Text(ts.to_string()),
2182 SqlValue::Vector(v) => Value::Vector(v),
2183 }
2184}
2185
2186fn data_type_from_string(value: &str) -> DataType {
2187 let upper = value.to_ascii_uppercase();
2188 if upper.starts_with("INT") || upper.starts_with("BIGINT") {
2189 DataType::Int
2190 } else if upper.starts_with("FLOAT") || upper.starts_with("DOUBLE") {
2191 DataType::Float
2192 } else if upper.starts_with("BLOB") {
2193 DataType::Bytes
2194 } else if upper.starts_with("BOOLEAN") {
2195 DataType::Bool
2196 } else if upper.starts_with("VECTOR") {
2197 DataType::Vector
2198 } else {
2199 DataType::Text
2200 }
2201}
2202
2203fn sql_column_to_column(col: &alopex_sql::executor::ColumnInfo) -> Column {
2205 use alopex_sql::planner::ResolvedType;
2206
2207 let data_type = match &col.data_type {
2208 ResolvedType::Integer | ResolvedType::BigInt => DataType::Int,
2209 ResolvedType::Float | ResolvedType::Double => DataType::Float,
2210 ResolvedType::Text => DataType::Text,
2211 ResolvedType::Blob => DataType::Bytes,
2212 ResolvedType::Boolean => DataType::Bool,
2213 ResolvedType::Timestamp => DataType::Text, ResolvedType::Vector { .. } => DataType::Vector,
2215 ResolvedType::Null => DataType::Text, };
2217
2218 Column::new(&col.name, data_type)
2219}
2220
2221fn columns_from_query_result(query_result: &alopex_sql::executor::QueryResult) -> Vec<Column> {
2223 query_result
2224 .columns
2225 .iter()
2226 .map(sql_column_to_column)
2227 .collect()
2228}
2229
2230#[allow(dead_code)] fn columns_from_streaming_result(
2233 query_iter: &alopex_embedded::QueryRowIterator<'_>,
2234) -> Vec<Column> {
2235 query_iter
2236 .columns()
2237 .iter()
2238 .map(sql_column_to_column)
2239 .collect()
2240}
2241
2242fn columns_from_streaming_rows(rows: &alopex_embedded::StreamingRows<'_>) -> Vec<Column> {
2244 rows.columns().iter().map(sql_column_to_column).collect()
2245}
2246
2247pub fn sql_status_columns() -> Vec<Column> {
2249 vec![
2250 Column::new("status", DataType::Text),
2251 Column::new("message", DataType::Text),
2252 ]
2253}
2254
2255#[cfg(test)]
2256mod tests {
2257 use super::*;
2258 use crate::batch::BatchModeSource;
2259 use crate::output::jsonl::JsonlFormatter;
2260
2261 fn create_test_db() -> Database {
2262 Database::open_in_memory().unwrap()
2263 }
2264
2265 fn create_status_writer(output: &mut Vec<u8>) -> StreamingWriter<&mut Vec<u8>> {
2266 let formatter = Box::new(JsonlFormatter::new());
2267 let columns = sql_status_columns();
2268 StreamingWriter::new(output, formatter, columns, None)
2269 }
2270
2271 fn create_query_writer(
2272 output: &mut Vec<u8>,
2273 columns: Vec<Column>,
2274 ) -> StreamingWriter<&mut Vec<u8>> {
2275 let formatter = Box::new(JsonlFormatter::new());
2276 StreamingWriter::new(output, formatter, columns, None)
2277 }
2278
2279 fn default_batch_mode() -> BatchMode {
2280 BatchMode {
2281 is_batch: false,
2282 is_tty: true,
2283 source: BatchModeSource::Default,
2284 }
2285 }
2286
2287 #[test]
2288 fn test_create_table() {
2289 let db = create_test_db();
2290
2291 let mut output = Vec::new();
2292 {
2293 let mut writer = create_status_writer(&mut output);
2294 execute_sql(
2295 &db,
2296 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);",
2297 &mut writer,
2298 )
2299 .unwrap();
2300 }
2301
2302 let result = String::from_utf8(output).unwrap();
2303 assert!(result.contains("OK"));
2304 }
2305
2306 #[test]
2307 fn test_insert_and_select() {
2308 let db = create_test_db();
2309
2310 {
2312 let mut output = Vec::new();
2313 let mut writer = create_status_writer(&mut output);
2314 execute_sql(
2315 &db,
2316 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);",
2317 &mut writer,
2318 )
2319 .unwrap();
2320 }
2321
2322 {
2324 let mut output = Vec::new();
2325 let mut writer = create_status_writer(&mut output);
2326 execute_sql(
2327 &db,
2328 "INSERT INTO users (id, name) VALUES (1, 'Alice');",
2329 &mut writer,
2330 )
2331 .unwrap();
2332 let result = String::from_utf8(output).unwrap();
2333 assert!(result.contains("row(s) affected"));
2334 }
2335
2336 {
2338 let mut output = Vec::new();
2339 let columns = vec![
2340 Column::new("id", DataType::Int),
2341 Column::new("name", DataType::Text),
2342 ];
2343 let mut writer = create_query_writer(&mut output, columns);
2344 execute_sql(&db, "SELECT id, name FROM users;", &mut writer).unwrap();
2345 let result = String::from_utf8(output).unwrap();
2346 assert!(result.contains("Alice"));
2347 }
2348 }
2349
2350 #[test]
2351 fn test_syntax_error() {
2352 let db = create_test_db();
2353
2354 let mut output = Vec::new();
2355 let mut writer = create_status_writer(&mut output);
2356 let result = execute_sql(&db, "CREATE TABEL invalid_syntax;", &mut writer);
2357 assert!(result.is_err());
2358 }
2359
2360 #[test]
2361 fn test_multiple_statements() {
2362 let db = create_test_db();
2363
2364 let mut output = Vec::new();
2365 {
2366 let mut writer = create_status_writer(&mut output);
2367 execute_sql(
2368 &db,
2369 "CREATE TABLE t (id INTEGER PRIMARY KEY); INSERT INTO t (id) VALUES (1);",
2370 &mut writer,
2371 )
2372 .unwrap();
2373 }
2374
2375 {
2377 let mut output = Vec::new();
2378 let columns = vec![Column::new("id", DataType::Int)];
2379 let mut writer = create_query_writer(&mut output, columns);
2380 execute_sql(&db, "SELECT id FROM t;", &mut writer).unwrap();
2381 let result = String::from_utf8(output).unwrap();
2382 assert!(result.contains("1"));
2383 }
2384 }
2385
2386 #[test]
2391 fn streaming_select_row_error_propagates() {
2392 let db = create_test_db();
2393 db.execute_sql("CREATE TABLE t (id INTEGER PRIMARY KEY);")
2394 .unwrap();
2395 db.execute_sql("INSERT INTO t (id) VALUES (1);").unwrap();
2396
2397 let deadline = Deadline::new(parse_deadline(None).unwrap());
2398 let cancel = CancelSignal::new();
2399 let options = SqlExecutionOptions {
2400 limit: None,
2401 quiet: false,
2402 cancel: &cancel,
2403 deadline: &deadline,
2404 admin_launcher: None,
2405 };
2406
2407 let mut output = Vec::new();
2408 let formatter = Box::new(JsonlFormatter::new());
2409 let result = execute_sql_select_streaming(
2412 &db,
2413 "SELECT id / 0 AS x FROM t;",
2414 &mut output,
2415 formatter,
2416 &options,
2417 false,
2418 );
2419 assert!(
2420 result.is_err(),
2421 "row evaluation errors must propagate instead of yielding an empty result"
2422 );
2423 }
2424
2425 #[test]
2426 fn test_sql_value_conversion() {
2427 use alopex_sql::SqlValue;
2428
2429 assert!(matches!(sql_value_to_value(SqlValue::Null), Value::Null));
2430 assert!(matches!(
2431 sql_value_to_value(SqlValue::Integer(42)),
2432 Value::Int(42)
2433 ));
2434 assert!(matches!(
2435 sql_value_to_value(SqlValue::BigInt(100)),
2436 Value::Int(100)
2437 ));
2438 assert!(matches!(
2439 sql_value_to_value(SqlValue::Boolean(true)),
2440 Value::Bool(true)
2441 ));
2442 assert!(
2443 matches!(sql_value_to_value(SqlValue::Text("hello".to_string())), Value::Text(s) if s == "hello")
2444 );
2445 }
2446
2447 #[test]
2448 fn resolve_query_from_argument() {
2449 let cmd = SqlCommand {
2450 query: Some("SELECT 1".to_string()),
2451 file: None,
2452 fetch_size: None,
2453 max_rows: None,
2454 deadline: None,
2455 tui: false,
2456 };
2457
2458 let sql = cmd.resolve_query(&default_batch_mode()).unwrap();
2459 assert_eq!(sql, "SELECT 1");
2460 }
2461
2462 #[test]
2463 fn resolve_query_from_file() {
2464 let mut file = tempfile::NamedTempFile::new().unwrap();
2465 writeln!(file, "SELECT * FROM users").unwrap();
2466
2467 let cmd = SqlCommand {
2468 query: None,
2469 file: Some(file.path().display().to_string()),
2470 fetch_size: None,
2471 max_rows: None,
2472 deadline: None,
2473 tui: false,
2474 };
2475
2476 let sql = cmd.resolve_query(&default_batch_mode()).unwrap();
2477 assert_eq!(sql, "SELECT * FROM users\n");
2478 }
2479
2480 #[test]
2481 fn resolve_query_returns_no_query_error() {
2482 let cmd = SqlCommand {
2483 query: None,
2484 file: None,
2485 fetch_size: None,
2486 max_rows: None,
2487 deadline: None,
2488 tui: false,
2489 };
2490
2491 let err = cmd.resolve_query(&default_batch_mode()).unwrap_err();
2492 assert!(matches!(err, CliError::NoQueryProvided));
2493 }
2494
2495 #[test]
2496 fn resolve_query_rejects_query_and_file() {
2497 let cmd = SqlCommand {
2498 query: Some("SELECT 1".to_string()),
2499 file: Some("query.sql".into()),
2500 fetch_size: None,
2501 max_rows: None,
2502 deadline: None,
2503 tui: false,
2504 };
2505
2506 let err = cmd.resolve_query(&default_batch_mode()).unwrap_err();
2507 assert!(matches!(
2508 err,
2509 CliError::InvalidArgument(msg) if msg == "Cannot specify both query and file"
2510 ));
2511 }
2512}