1use {
2 crate::command::{SetOption, ShowOption},
3 gluesql_core::prelude::{Payload, PayloadVariable},
4 std::{
5 collections::{BTreeMap, HashSet},
6 fmt::Display,
7 fs::File,
8 io::{Result as IOResult, Write},
9 path::Path,
10 },
11 strum_macros::Display,
12 tabled::{Style, Table, builder::Builder},
13};
14
15pub struct Print<W: Write> {
16 pub output: W,
17 spool_file: Option<File>,
18 pub option: PrintOption,
19}
20
21pub struct PrintOption {
22 pub tabular: bool,
23 colsep: String,
24 colwrap: String,
25 heading: bool,
26}
27
28impl PrintOption {
29 pub fn tabular(&mut self, tabular: bool) {
30 if tabular {
31 self.tabular = tabular;
32 self.colsep("|".into());
33 self.colwrap(String::new());
34 self.heading(true);
35 } else {
36 self.tabular = tabular;
37 }
38 }
39
40 fn colsep(&mut self, colsep: String) {
41 self.colsep = colsep;
42 }
43
44 fn colwrap(&mut self, colwrap: String) {
45 self.colwrap = colwrap;
46 }
47
48 fn heading(&mut self, heading: bool) {
49 self.heading = heading;
50 }
51
52 fn format(&self, option: ShowOption) -> String {
53 fn string_from(value: bool) -> String {
54 if value { "ON".into() } else { "OFF".into() }
55 }
56 match option {
57 ShowOption::Tabular => format!("tabular {}", string_from(self.tabular)),
58 ShowOption::Colsep => format!("colsep \"{}\"", self.colsep),
59 ShowOption::Colwrap => format!("colwrap \"{}\"", self.colwrap),
60 ShowOption::Heading => format!("heading {}", string_from(self.heading)),
61 ShowOption::All => format!(
62 "{}\n{}\n{}\n{}",
63 self.format(ShowOption::Tabular),
64 self.format(ShowOption::Colsep),
65 self.format(ShowOption::Colwrap),
66 self.format(ShowOption::Heading),
67 ),
68 }
69 }
70}
71
72impl Default for PrintOption {
73 fn default() -> Self {
74 Self {
75 tabular: true,
76 colsep: "|".into(),
77 colwrap: String::new(),
78 heading: true,
79 }
80 }
81}
82
83impl<'a, W: Write> Print<W> {
84 pub fn new(output: W, spool_file: Option<File>, option: PrintOption) -> Self {
85 Print {
86 output,
87 spool_file,
88 option,
89 }
90 }
91
92 pub fn payloads(&mut self, payloads: &[Payload]) -> IOResult<()> {
93 payloads.iter().try_for_each(|p| self.payload(p))
94 }
95
96 pub fn payload(&mut self, payload: &Payload) -> IOResult<()> {
97 #[derive(Display)]
98 #[strum(serialize_all = "snake_case")]
99 enum Target {
100 Table,
101 Row,
102 }
103 use Target::*;
104
105 let mut affected = |n: usize, target: Target, msg: &str| -> IOResult<()> {
106 let payload = format!("{n} {target}{} {msg}", if n > 1 { "s" } else { "" });
107 self.writeln(payload)
108 };
109 match payload {
110 Payload::Create => self.writeln("Table created")?,
111 Payload::DropTable(n) => affected(*n, Table, "dropped")?,
112 Payload::DropFunction => self.writeln("Function dropped")?,
113 Payload::AlterTable => self.writeln("Table altered")?,
114 Payload::CreateIndex => self.writeln("Index created")?,
115 Payload::DropIndex => self.writeln("Index dropped")?,
116 Payload::Commit => self.writeln("Commit completed")?,
117 Payload::Rollback => self.writeln("Rollback completed")?,
118 Payload::StartTransaction => self.writeln("Transaction started")?,
119 Payload::Insert(n) => affected(*n, Row, "inserted")?,
120 Payload::Delete(n) => affected(*n, Row, "deleted")?,
121 Payload::Update(n) => affected(*n, Row, "updated")?,
122 Payload::ShowVariable(PayloadVariable::Version(v)) => self.writeln(format!("v{v}"))?,
123 Payload::ShowVariable(PayloadVariable::Tables(names)) => {
124 let mut table = Self::get_table(["tables"]);
125 for name in names {
126 table.add_record([name]);
127 }
128 let table = Self::build_table(table);
129 self.writeln(table)?;
130 }
131 Payload::ShowVariable(PayloadVariable::Functions(names)) => {
132 let mut table = Self::get_table(["functions"]);
133 for name in names {
134 table.add_record([name]);
135 }
136 let table = Self::build_table(table);
137 self.writeln(table)?;
138 }
139 Payload::ShowColumns(columns) => {
140 let mut table = Self::get_table(vec!["Field", "Type"]);
141 for (field, field_type) in columns {
142 table.add_record([field, &field_type.to_string()]);
143 }
144 let table = Self::build_table(table);
145 self.writeln(table)?;
146 }
147 Payload::Select { labels, rows } => match &self.option.tabular {
148 true => {
149 let labels = labels.iter().map(AsRef::as_ref);
150 let mut table = Self::get_table(labels);
151 for row in rows {
152 let row: Vec<String> = row.iter().map(Into::into).collect();
153
154 table.add_record(row);
155 }
156 let table = Self::build_table(table);
157 self.writeln(table)?;
158 }
159 false => {
160 self.write_header(labels.iter().map(String::as_str))?;
161 let rows = rows.iter().map(|row| row.iter().map(String::from));
162 self.write_rows(rows)?;
163 }
164 },
165 Payload::SelectMap(rows) => {
166 let mut labels = rows
167 .iter()
168 .flat_map(BTreeMap::keys)
169 .map(AsRef::as_ref)
170 .collect::<HashSet<&str>>()
171 .into_iter()
172 .collect::<Vec<_>>();
173 labels.sort_unstable();
174
175 match &self.option.tabular {
176 true => {
177 let mut table = Self::get_table(labels.clone());
178 for row in rows {
179 let row = labels
180 .iter()
181 .map(|label| row.get(*label).map(Into::into).unwrap_or_default())
182 .collect::<Vec<String>>();
183
184 table.add_record(row);
185 }
186 let table = Self::build_table(table);
187 self.writeln(table)?;
188 }
189 false => {
190 self.write_header(labels.iter().map(AsRef::as_ref))?;
191
192 let rows = rows.iter().map(|row| {
193 labels
194 .iter()
195 .map(|label| row.get(*label).map(String::from).unwrap_or_default())
196 });
197 self.write_rows(rows)?;
198 }
199 }
200 }
201 }
202
203 Ok(())
204 }
205
206 fn write_rows(
207 &mut self,
208 rows: impl Iterator<Item = impl Iterator<Item = String>>,
209 ) -> IOResult<()> {
210 for row in rows {
211 let row = row
212 .map(|v| format!("{c}{v}{c}", c = self.option.colwrap))
213 .collect::<Vec<_>>()
214 .join(self.option.colsep.as_str());
215
216 self.write(row)?;
217 }
218
219 Ok(())
220 }
221
222 fn write_lf(&mut self, payload: impl Display, lf: &str) -> IOResult<()> {
223 if let Some(file) = &self.spool_file {
224 writeln!(file.to_owned(), "{payload}{lf}")?;
225 }
226
227 writeln!(self.output, "{payload}{lf}")
228 }
229
230 fn write(&mut self, payload: impl Display) -> IOResult<()> {
231 self.write_lf(payload, "")
232 }
233
234 fn writeln(&mut self, payload: impl Display) -> IOResult<()> {
235 self.write_lf(payload, "\n")
236 }
237
238 fn write_header<'b>(&mut self, labels: impl Iterator<Item = &'b str>) -> IOResult<()> {
239 let PrintOption {
240 heading,
241 colsep,
242 colwrap,
243 ..
244 } = &self.option;
245
246 if !heading {
247 return Ok(());
248 }
249
250 let labels = labels
251 .map(|v| format!("{colwrap}{v}{colwrap}"))
252 .collect::<Vec<_>>()
253 .join(colsep.as_str());
254
255 self.write(labels)
256 }
257
258 pub fn help(&mut self) -> IOResult<()> {
259 const HEADER: [&str; 2] = ["command", "description"];
260 const CONTENT: [[&str; 2]; 12] = [
261 [".help", "show help"],
262 [".quit", "quit program"],
263 [".tables", "show table names"],
264 [".functions", "show function names"],
265 [".columns TABLE", "show columns from TABLE"],
266 [".version", "show version"],
267 [".execute PATH", "execute SQL from PATH"],
268 [".spool PATH|off", "spool to PATH or off"],
269 [".show OPTION", "show print option eg).show all"],
270 [".set OPTION", "set print option eg).set tabular off"],
271 [".edit [PATH]", "open editor with last command or PATH"],
272 [".run ", "execute last command"],
273 ];
274
275 let mut table = Self::get_table(HEADER);
276 for row in CONTENT {
277 table.add_record(row);
278 }
279 let table = Self::build_table(table);
280
281 writeln!(self.output, "{table}\n")
282 }
283
284 pub fn spool_on<P: AsRef<Path>>(&mut self, filename: P) -> IOResult<()> {
285 let file = File::create(filename)?;
286 self.spool_file = Some(file);
287
288 Ok(())
289 }
290
291 pub fn spool_off(&mut self) {
292 self.spool_file = None;
293 }
294
295 fn get_table<T: IntoIterator<Item = &'a str>>(headers: T) -> Builder {
296 let mut table = Builder::default();
297 table.set_columns(headers);
298
299 table
300 }
301
302 fn build_table(builder: Builder) -> Table {
303 builder.build().with(Style::markdown())
304 }
305
306 pub fn set_option(&mut self, option: SetOption) {
307 match option {
308 SetOption::Tabular(value) => self.option.tabular(value),
309 SetOption::Colsep(value) => self.option.colsep(value),
310 SetOption::Colwrap(value) => self.option.colwrap(value),
311 SetOption::Heading(value) => self.option.heading(value),
312 }
313 }
314
315 pub fn show_option(&mut self, option: ShowOption) -> IOResult<()> {
316 let payload = self.option.format(option);
317 self.writeln(payload)?;
318
319 Ok(())
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use {
326 super::{Print, PrintOption},
327 crate::command::{SetOption, ShowOption},
328 std::path::PathBuf,
329 };
330
331 #[test]
332 fn print_help() {
333 let mut print = Print::new(Vec::new(), None, PrintOption::default());
334
335 let actual = {
336 print.help().unwrap();
337
338 String::from_utf8(print.output).unwrap()
339 };
340 let expected = "
341| command | description |
342|-----------------|---------------------------------------|
343| .help | show help |
344| .quit | quit program |
345| .tables | show table names |
346| .functions | show function names |
347| .columns TABLE | show columns from TABLE |
348| .version | show version |
349| .execute PATH | execute SQL from PATH |
350| .spool PATH|off | spool to PATH or off |
351| .show OPTION | show print option eg).show all |
352| .set OPTION | set print option eg).set tabular off |
353| .edit [PATH] | open editor with last command or PATH |
354| .run | execute last command |";
355
356 assert_eq!(
357 actual.as_str().trim_matches('\n'),
358 expected.trim_matches('\n')
359 );
360 }
361
362 #[test]
363 fn print_payload() {
364 use gluesql_core::{
365 ast::DataType,
366 prelude::{Payload, PayloadVariable, Value},
367 };
368
369 let mut print = Print::new(Vec::new(), None, PrintOption::default());
370
371 macro_rules! test {
372 ($payload: expr, $expected: literal ) => {
373 print.payloads(&[$payload]).unwrap();
374
375 assert_eq!(
376 String::from_utf8(print.output.clone())
377 .unwrap()
378 .as_str()
379 .trim_matches('\n'),
380 $expected.trim_matches('\n')
381 );
382
383 print.output.clear();
384 };
385 }
386
387 test!(Payload::Create, "Table created");
388 test!(Payload::DropTable(1), "1 table dropped");
389 test!(Payload::AlterTable, "Table altered");
390 test!(Payload::CreateIndex, "Index created");
391 test!(Payload::DropIndex, "Index dropped");
392 test!(Payload::DropFunction, "Function dropped");
393 test!(Payload::Commit, "Commit completed");
394 test!(Payload::Rollback, "Rollback completed");
395 test!(Payload::StartTransaction, "Transaction started");
396 test!(Payload::Insert(0), "0 row inserted");
397 test!(Payload::Insert(1), "1 row inserted");
398 test!(Payload::Insert(7), "7 rows inserted");
399 test!(Payload::Delete(300), "300 rows deleted");
400 test!(Payload::Update(123), "123 rows updated");
401 test!(
402 Payload::ShowVariable(PayloadVariable::Version("11.6.1989".to_owned())),
403 "v11.6.1989"
404 );
405 test!(
406 Payload::ShowVariable(PayloadVariable::Tables(Vec::new())),
407 "
408| tables |"
409 );
410 test!(
411 Payload::ShowVariable(PayloadVariable::Functions(Vec::new())),
412 "
413| functions |"
414 );
415 test!(
416 Payload::ShowVariable(PayloadVariable::Tables(
417 [
418 "Allocator",
419 "ExtendFromWithin",
420 "IntoRawParts",
421 "Reserve",
422 "Splice",
423 ]
424 .into_iter()
425 .map(ToOwned::to_owned)
426 .collect()
427 )),
428 "
429| tables |
430|------------------|
431| Allocator |
432| ExtendFromWithin |
433| IntoRawParts |
434| Reserve |
435| Splice |"
436 );
437 test!(
438 Payload::ShowVariable(PayloadVariable::Functions(
439 [
440 "Allocator",
441 "ExtendFromWithin",
442 "IntoRawParts",
443 "Reserve",
444 "Splice",
445 ]
446 .into_iter()
447 .map(ToOwned::to_owned)
448 .collect()
449 )),
450 "
451| functions |
452|------------------|
453| Allocator |
454| ExtendFromWithin |
455| IntoRawParts |
456| Reserve |
457| Splice |"
458 );
459 test!(
460 Payload::Select {
461 labels: vec!["id".to_owned()],
462 rows: [101, 202, 301, 505, 1001]
463 .into_iter()
464 .map(Value::I64)
465 .map(|v| vec![v])
466 .collect::<Vec<Vec<Value>>>(),
467 },
468 "
469| id |
470|------|
471| 101 |
472| 202 |
473| 301 |
474| 505 |
475| 1001 |"
476 );
477 test!(
478 Payload::Select {
479 labels: ["id", "title", "valid"]
480 .into_iter()
481 .map(ToOwned::to_owned)
482 .collect(),
483 rows: vec![
484 vec![
485 Value::I64(1),
486 Value::Str("foo".to_owned()),
487 Value::Bool(true)
488 ],
489 vec![
490 Value::I64(2),
491 Value::Str("bar".to_owned()),
492 Value::Bool(false)
493 ],
494 vec![
495 Value::I64(3),
496 Value::Str("bas".to_owned()),
497 Value::Bool(false)
498 ],
499 vec![
500 Value::I64(4),
501 Value::Str("lim".to_owned()),
502 Value::Bool(true)
503 ],
504 vec![
505 Value::I64(5),
506 Value::Str("kim".to_owned()),
507 Value::Bool(true)
508 ],
509 ],
510 },
511 "
512| id | title | valid |
513|----|-------|-------|
514| 1 | foo | TRUE |
515| 2 | bar | FALSE |
516| 3 | bas | FALSE |
517| 4 | lim | TRUE |
518| 5 | kim | TRUE |"
519 );
520
521 test!(
522 Payload::SelectMap(vec![
523 [
524 ("id".to_owned(), Value::I64(1)),
525 ("title".to_owned(), Value::Str("foo".to_owned()))
526 ]
527 .into_iter()
528 .collect(),
529 [("id".to_owned(), Value::I64(2))].into_iter().collect(),
530 [("title".to_owned(), Value::Str("bar".to_owned()))]
531 .into_iter()
532 .collect(),
533 ]),
534 "
535| id | title |
536|----|-------|
537| 1 | foo |
538| 2 | |
539| | bar |"
540 );
541
542 test!(
543 Payload::ShowColumns(vec![
544 ("id".to_owned(), DataType::Int),
545 ("name".to_owned(), DataType::Text),
546 ("isabear".to_owned(), DataType::Boolean),
547 ],),
548 "
549| Field | Type |
550|---------|---------|
551| id | INT |
552| name | TEXT |
553| isabear | BOOLEAN |"
554 );
555
556 test!(
557 Payload::ShowColumns(vec![
558 ("id".to_owned(), DataType::Int8),
559 ("calc1".to_owned(), DataType::Float),
560 ("cost".to_owned(), DataType::Decimal),
561 ("DOB".to_owned(), DataType::Date),
562 ("clock".to_owned(), DataType::Time),
563 ("tstamp".to_owned(), DataType::Timestamp),
564 ("ival".to_owned(), DataType::Interval),
565 ("uuid".to_owned(), DataType::Uuid),
566 ("hash".to_owned(), DataType::Map),
567 ("mylist".to_owned(), DataType::List),
568 ],),
569 "
570| Field | Type |
571|--------|-----------|
572| id | INT8 |
573| calc1 | FLOAT |
574| cost | DECIMAL |
575| DOB | DATE |
576| clock | TIME |
577| tstamp | TIMESTAMP |
578| ival | INTERVAL |
579| uuid | UUID |
580| hash | MAP |
581| mylist | LIST |"
582 );
583
584 print.set_option(SetOption::Tabular(false));
586 test!(
587 Payload::Select {
588 labels: ["id", "title", "valid"]
589 .into_iter()
590 .map(ToOwned::to_owned)
591 .collect(),
592 rows: vec![
593 vec![
594 Value::I64(1),
595 Value::Str("foo".to_owned()),
596 Value::Bool(true)
597 ],
598 vec![
599 Value::I64(2),
600 Value::Str("bar".to_owned()),
601 Value::Bool(false)
602 ],
603 ]
604 },
605 "
606id|title|valid
6071|foo|TRUE
6082|bar|FALSE"
609 );
610
611 test!(
612 Payload::SelectMap(vec![
613 [
614 ("id".to_owned(), Value::I64(1)),
615 ("title".to_owned(), Value::Str("foo".to_owned()))
616 ]
617 .into_iter()
618 .collect(),
619 [("id".to_owned(), Value::I64(2))].into_iter().collect(),
620 [("title".to_owned(), Value::Str("bar".to_owned()))]
621 .into_iter()
622 .collect(),
623 ]),
624 "
625id|title
6261|foo
6272|
628|bar"
629 );
630
631 print.set_option(SetOption::Colsep(",".into()));
633 assert_eq!(print.option.format(ShowOption::Colsep), r#"colsep ",""#);
634
635 test!(
636 Payload::Select {
637 labels: ["id", "title", "valid"]
638 .into_iter()
639 .map(ToOwned::to_owned)
640 .collect(),
641 rows: vec![
642 vec![
643 Value::I64(1),
644 Value::Str("foo".to_owned()),
645 Value::Bool(true)
646 ],
647 vec![
648 Value::I64(2),
649 Value::Str("bar".to_owned()),
650 Value::Bool(false)
651 ],
652 ],
653 },
654 "
655id,title,valid
6561,foo,TRUE
6572,bar,FALSE"
658 );
659
660 print.set_option(SetOption::Colwrap("'".into()));
662 assert_eq!(print.option.format(ShowOption::Colwrap), r#"colwrap "'""#);
663 test!(
664 Payload::Select {
665 labels: ["id", "title", "valid"]
666 .into_iter()
667 .map(ToOwned::to_owned)
668 .collect(),
669 rows: vec![
670 vec![
671 Value::I64(1),
672 Value::Str("foo".to_owned()),
673 Value::Bool(true)
674 ],
675 vec![
676 Value::I64(2),
677 Value::Str("bar".to_owned()),
678 Value::Bool(false)
679 ],
680 ],
681 },
682 "
683'id','title','valid'
684'1','foo','TRUE'
685'2','bar','FALSE'"
686 );
687
688 print.set_option(SetOption::Heading(false));
690 test!(
691 Payload::Select {
692 labels: ["id", "title", "valid"]
693 .into_iter()
694 .map(ToOwned::to_owned)
695 .collect(),
696 rows: vec![
697 vec![
698 Value::I64(1),
699 Value::Str("foo".to_owned()),
700 Value::Bool(true)
701 ],
702 vec![
703 Value::I64(2),
704 Value::Str("bar".to_owned()),
705 Value::Bool(false)
706 ],
707 ],
708 },
709 "
710'1','foo','TRUE'
711'2','bar','FALSE'"
712 );
713
714 print.set_option(SetOption::Heading(true));
716 print.set_option(SetOption::Tabular(false));
717 test!(
718 Payload::Select {
719 labels: ["id", "title", "valid"]
720 .into_iter()
721 .map(ToOwned::to_owned)
722 .collect(),
723 rows: vec![
724 vec![
725 Value::I64(1),
726 Value::Str("foo".to_owned()),
727 Value::Bool(true)
728 ],
729 vec![
730 Value::I64(2),
731 Value::Str("bar".to_owned()),
732 Value::Bool(false)
733 ],
734 ],
735 },
736 "
737'id','title','valid'
738'1','foo','TRUE'
739'2','bar','FALSE'"
740 );
741
742 print.set_option(SetOption::Heading(false));
743 print.set_option(SetOption::Tabular(false));
744 test!(
745 Payload::Select {
746 labels: ["id"].into_iter().map(ToOwned::to_owned).collect(),
747 rows: vec![vec![Value::I64(1),], vec![Value::I64(2),],],
748 },
749 "'1'\n'2'"
750 );
751
752 print.set_option(SetOption::Tabular(true));
754 assert_eq!(print.option.format(ShowOption::Tabular), "tabular ON");
755 assert_eq!(print.option.format(ShowOption::Colsep), r#"colsep "|""#);
756 assert_eq!(print.option.format(ShowOption::Colwrap), r#"colwrap """#);
757 assert_eq!(print.option.format(ShowOption::Heading), "heading ON");
758 assert_eq!(
759 print.option.format(ShowOption::All),
760 "
761tabular ON
762colsep \"|\"
763colwrap \"\"
764heading ON"
765 .trim_matches('\n')
766 );
767 }
768
769 #[test]
770 fn print_spool() {
771 use std::fs;
772
773 let mut print = Print::new(Vec::new(), None, PrintOption::default());
774
775 fs::create_dir_all("tmp").unwrap();
777 assert!(print.spool_on(PathBuf::from("tmp/spool.txt")).is_ok());
778 assert!(print.writeln("Test").is_ok());
779 assert!(print.show_option(ShowOption::All).is_ok());
780 print.spool_off();
781 assert!(print.writeln("Test").is_ok());
782 }
783}