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