1use std::{
4 path::{Path, PathBuf},
5 str::FromStr,
6 vec,
7};
8
9use crate::{
10 ast::ParsedSqlFile,
11 comments::{Comments, LeadingCommentCapture, MultiFlatten},
12 docs::{SqlFileDoc, TableDoc},
13 error::DocError,
14 files::SqlFiles,
15 source::SqlSource,
16};
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct SqlDoc {
21 tables: Vec<TableDoc>,
23}
24
25impl SqlDoc {
26 #[must_use]
28 pub fn new(mut tables: Vec<TableDoc>) -> Self {
29 tables.sort_by(|a, b| a.name().cmp(b.name()));
30 Self { tables }
31 }
32
33 pub fn from_dir<P: AsRef<Path> + ?Sized>(root: &P) -> SqlDocBuilder<'_> {
55 SqlDocBuilder {
56 source: SqlFileDocSource::Dir(root.as_ref().to_path_buf()),
57 deny: Vec::new(),
58 multiline_flat: MultiFlatten::default(),
59 leading_type: LeadingCommentCapture::default(),
60 }
61 }
62
63 pub fn from_path<P: AsRef<Path> + ?Sized>(path: &P) -> SqlDocBuilder<'_> {
83 SqlDocBuilder {
84 source: SqlFileDocSource::File(path.as_ref().to_path_buf()),
85 deny: Vec::new(),
86 multiline_flat: MultiFlatten::default(),
87 leading_type: LeadingCommentCapture::default(),
88 }
89 }
90
91 pub fn from_paths<P: AsRef<Path>>(paths: &[P]) -> SqlDocBuilder<'_> {
113 SqlDocBuilder {
114 source: SqlFileDocSource::Files(
115 paths.iter().map(|p| p.as_ref().to_path_buf()).collect(),
116 ),
117 deny: Vec::new(),
118 multiline_flat: MultiFlatten::default(),
119 leading_type: LeadingCommentCapture::default(),
120 }
121 }
122
123 #[must_use]
152 pub fn builder_from_str(content: &str) -> SqlDocBuilder<'_> {
153 SqlDocBuilder {
154 source: SqlFileDocSource::FromString(content),
155 deny: Vec::new(),
156 multiline_flat: MultiFlatten::default(),
157 leading_type: LeadingCommentCapture::default(),
158 }
159 }
160
161 #[must_use]
194 pub fn builder_from_strs_with_paths(
195 string_with_path: &[(String, PathBuf)],
196 ) -> SqlDocBuilder<'_> {
197 SqlDocBuilder {
198 source: SqlFileDocSource::FromStringsWithPaths(string_with_path),
199 deny: Vec::new(),
200 multiline_flat: MultiFlatten::default(),
201 leading_type: LeadingCommentCapture::default(),
202 }
203 }
204
205 pub fn table(&self, name: &str, schema: Option<&str>) -> Result<&TableDoc, DocError> {
216 let tables = self.tables();
217 let start = tables.partition_point(|n| n.name() < name);
218 if start == tables.len() || tables[start].name() != name {
219 return Err(DocError::TableNotFound { name: name.to_owned() });
220 }
221 let end = tables.partition_point(|t| t.name() <= name);
222 match &tables[start..end] {
223 [single] => Ok(single),
224 multiple => {
225 let mut schemas = multiple.iter().filter(|v| v.schema() == schema);
226 let first = schemas.next().ok_or_else(|| DocError::TableWithSchemaNotFound {
227 name: name.to_owned(),
228 schema: schema.map_or_else(
229 || "No schema provided".to_owned(),
230 std::borrow::ToOwned::to_owned,
231 ),
232 })?;
233 if schemas.next().is_some() {
234 return Err(DocError::DuplicateTablesFound {
235 tables: multiple
236 .iter()
237 .filter(|v| v.schema() == schema)
238 .map(std::borrow::ToOwned::to_owned)
239 .collect(),
240 });
241 }
242 Ok(first)
243 }
244 }
245 }
246
247 #[must_use]
249 pub fn tables(&self) -> &[TableDoc] {
250 &self.tables
251 }
252 #[must_use]
254 pub fn tables_mut(&mut self) -> &mut [TableDoc] {
255 &mut self.tables
256 }
257 #[must_use]
259 pub fn into_tables(self) -> Vec<TableDoc> {
260 self.tables
261 }
262
263 #[must_use]
265 pub fn number_of_tables(&self) -> usize {
266 self.tables().len()
267 }
268}
269
270impl FromStr for SqlDoc {
271 type Err = DocError;
272
273 fn from_str(s: &str) -> Result<Self, Self::Err> {
274 Self::builder_from_str(s).build()
275 }
276}
277
278#[derive(Debug, Eq, PartialEq)]
280pub struct SqlDocBuilder<'a> {
281 source: SqlFileDocSource<'a>,
283 deny: Vec<String>,
285 multiline_flat: MultiFlatten<'a>,
287 leading_type: LeadingCommentCapture,
289}
290
291#[derive(Debug, Eq, PartialEq)]
293enum SqlFileDocSource<'a> {
294 Dir(PathBuf),
295 File(PathBuf),
296 Files(Vec<PathBuf>),
297 FromString(&'a str),
298 FromStringsWithPaths(&'a [(String, PathBuf)]),
299}
300
301impl<'a> SqlDocBuilder<'a> {
302 #[must_use]
307 pub fn deny(mut self, deny_path: &str) -> Self {
308 self.deny.push(deny_path.into());
309 self
310 }
311
312 #[must_use]
314 pub const fn flatten_multiline(mut self) -> Self {
315 self.multiline_flat = MultiFlatten::FlattenWithNone;
316 self
317 }
318
319 #[must_use]
321 pub const fn flatten_multiline_with(mut self, suffix: &'a str) -> Self {
322 self.multiline_flat = MultiFlatten::Flatten(suffix);
323 self
324 }
325 #[must_use]
327 pub const fn preserve_multiline(mut self) -> Self {
328 self.multiline_flat = MultiFlatten::NoFlat;
329 self
330 }
331
332 #[must_use]
334 pub const fn collect_single_nearest(mut self) -> Self {
335 self.leading_type = LeadingCommentCapture::SingleNearest;
336 self
337 }
338
339 #[must_use]
341 pub const fn collect_all_leading(mut self) -> Self {
342 self.leading_type = LeadingCommentCapture::AllLeading;
343 self
344 }
345
346 #[must_use]
348 pub const fn collect_all_single_one_multi(mut self) -> Self {
349 self.leading_type = LeadingCommentCapture::AllSingleOneMulti;
350 self
351 }
352
353 pub fn build(self) -> Result<SqlDoc, DocError> {
361 let docs: Vec<SqlFileDoc> = match &self.source {
362 SqlFileDocSource::Dir(path) => {
363 generate_docs_from_dir(path, &self.deny, self.leading_type, self.multiline_flat)?
364 }
365 SqlFileDocSource::File(file) => {
366 let sql_doc =
367 generate_docs_from_file(file, self.leading_type, self.multiline_flat)?;
368 vec![sql_doc]
369 }
370 SqlFileDocSource::FromString(content) => {
371 let sql_docs =
372 generate_docs_str(content, None, self.leading_type, self.multiline_flat)?;
373 vec![sql_docs]
374 }
375 SqlFileDocSource::FromStringsWithPaths(strings_paths) => {
376 generate_docs_from_strs_with_paths(
377 strings_paths,
378 self.leading_type,
379 self.multiline_flat,
380 )?
381 }
382 SqlFileDocSource::Files(files) => {
383 generate_docs_from_files(files, self.leading_type, self.multiline_flat)?
384 }
385 };
386 let num_of_tables = docs.iter().map(super::docs::SqlFileDoc::number_of_tables).sum();
387 let mut tables = Vec::with_capacity(num_of_tables);
388 for sql_doc in docs {
389 tables.extend(sql_doc);
390 }
391 let sql_doc = SqlDoc::new(tables);
392 Ok(sql_doc)
393 }
394}
395
396fn generate_docs_from_dir<P: AsRef<Path>, S: AsRef<str>>(
397 source: P,
398 deny: &[S],
399 capture: LeadingCommentCapture,
400 flatten: MultiFlatten,
401) -> Result<Vec<SqlFileDoc>, DocError> {
402 let deny_list: Vec<String> = deny.iter().map(|file| file.as_ref().to_owned()).collect();
403 let file_set = SqlFiles::new(source, &deny_list)?;
404 let mut sql_docs = Vec::new();
405 for file in file_set.sql_files() {
406 let docs = generate_docs_from_file(file, capture, flatten)?;
407 sql_docs.push(docs);
408 }
409 Ok(sql_docs)
410}
411
412fn generate_docs_from_files(
413 files: &[PathBuf],
414 capture: LeadingCommentCapture,
415 flatten: MultiFlatten,
416) -> Result<Vec<SqlFileDoc>, DocError> {
417 let mut sql_docs = Vec::new();
418 for file in files {
419 let docs = generate_docs_from_file(file, capture, flatten)?;
420 sql_docs.push(docs);
421 }
422 Ok(sql_docs)
423}
424
425fn generate_docs_from_file<P: AsRef<Path>>(
426 source: P,
427 capture: LeadingCommentCapture,
428 flatten: MultiFlatten,
429) -> Result<SqlFileDoc, DocError> {
430 let file = SqlSource::from_path(source.as_ref())?;
431 let parsed_file = ParsedSqlFile::parse(file)?;
432 let comments = Comments::parse_all_comments_from_file(&parsed_file)?;
433 let docs = SqlFileDoc::from_parsed_file(&parsed_file, &comments, capture, flatten)?;
434 Ok(docs)
435}
436
437fn generate_docs_str(
438 content: &str,
439 path: Option<PathBuf>,
440 capture: LeadingCommentCapture,
441 flatten: MultiFlatten,
442) -> Result<SqlFileDoc, DocError> {
443 let dummy_file = SqlSource::from_str(content.to_owned(), path);
444 let parsed_sql = ParsedSqlFile::parse(dummy_file)?;
445 let comments = Comments::parse_all_comments_from_file(&parsed_sql)?;
446 let docs = SqlFileDoc::from_parsed_file(&parsed_sql, &comments, capture, flatten)?;
447 Ok(docs)
448}
449
450fn generate_docs_from_strs_with_paths(
451 strings_with_paths: &[(String, PathBuf)],
452 capture: LeadingCommentCapture,
453 flatten: MultiFlatten,
454) -> Result<Vec<SqlFileDoc>, DocError> {
455 let mut docs = Vec::new();
456 for (content, path) in strings_with_paths {
457 docs.push(generate_docs_str(content, Some(path.to_owned()), capture, flatten)?);
458 }
459
460 Ok(docs)
461}
462
463#[cfg(test)]
464mod tests {
465 use std::{
466 env, fs,
467 path::{Path, PathBuf},
468 vec,
469 };
470
471 use crate::{
472 SqlDoc,
473 comments::LeadingCommentCapture,
474 docs::{ColumnDoc, TableDoc},
475 error::DocError,
476 sql_doc::{MultiFlatten, SqlDocBuilder},
477 };
478
479 #[test]
480 fn build_sql_doc_from_file() -> Result<(), Box<dyn std::error::Error>> {
481 let base = env::temp_dir().join("build_sql_doc_from_file");
482 let _ = fs::remove_dir_all(&base);
483 fs::create_dir_all(&base)?;
484 let file = base.join("test_file.sql");
485 let sample = sample_sql();
486 let (contents, expected): (Vec<_>, Vec<_>) = sample.into_iter().unzip();
487 fs::write(&file, contents.join(""))?;
488 let sql_doc = SqlDoc::from_path(&file).build()?;
489 let mut expected_tables: Vec<TableDoc> =
490 expected.into_iter().flat_map(SqlDoc::into_tables).collect();
491 stamp_table_paths(&mut expected_tables, &file);
492 let expected_doc = SqlDoc::new(expected_tables);
493 assert_eq!(sql_doc, expected_doc);
494 let names: Vec<&str> =
495 sql_doc.tables().iter().map(super::super::docs::TableDoc::name).collect();
496 let mut sorted = names.clone();
497 sorted.sort_unstable();
498 assert_eq!(names, sorted, "tables should be in alphabetical order");
499 let _ = fs::remove_dir_all(&base);
500 Ok(())
501 }
502 #[test]
503 fn build_sql_doc_from_dir() -> Result<(), Box<dyn std::error::Error>> {
504 let base = env::temp_dir().join("build_sql_doc_from_dir");
505 let _ = fs::remove_dir_all(&base);
506 fs::create_dir_all(&base)?;
507 let mut expected: Vec<TableDoc> = Vec::new();
508 for (idx, (contents, doc)) in sample_sql().into_iter().enumerate() {
509 let path = base.join(format!("test_file{idx}.sql"));
510 fs::write(&path, contents)?;
511 let mut tables = doc.into_tables();
512 stamp_table_paths(&mut tables, &path);
513 expected.extend(tables);
514 }
515 let sql_doc = SqlDoc::from_dir(&base).build()?;
516 let mut actual: Vec<TableDoc> = sql_doc.into_tables();
517 assert_eq!(actual.len(), expected.len());
518 sort_tables(&mut actual);
519 sort_tables(&mut expected);
520 assert_eq!(actual, expected);
521 let _ = fs::remove_dir_all(&base);
522 Ok(())
523 }
524
525 #[test]
526 fn test_retrieve_table_and_schema() -> Result<(), Box<dyn std::error::Error>> {
527 let base = env::temp_dir().join("build_sql_doc_with_schema");
528 let _ = fs::remove_dir_all(&base);
529 fs::create_dir_all(&base)?;
530 let file = base.join("test_file.sql");
531 let sample = sample_sql();
532 let (contents, expected): (Vec<_>, Vec<_>) = sample.into_iter().unzip();
533 fs::write(&file, contents.join(""))?;
534 let sql_doc = SqlDoc::from_path(&file).build()?;
535 let mut expected_tables: Vec<TableDoc> =
536 expected.into_iter().flat_map(SqlDoc::into_tables).collect();
537 stamp_table_paths(&mut expected_tables, &file);
538 let expected_doc = SqlDoc::new(expected_tables);
539 assert_eq!(sql_doc, expected_doc);
540 let table = "users";
541 assert_eq!(sql_doc.table(table, None)?, expected_doc.table(table, None)?);
542 let schema = "analytics";
543 let schema_table = "events";
544 assert_eq!(
545 sql_doc.table(schema_table, Some(schema))?,
546 expected_doc.table(schema_table, Some(schema))?
547 );
548 let _ = fs::remove_dir_all(&base);
549 Ok(())
550 }
551
552 #[test]
553 fn test_table_err() {
554 let empty_set = SqlDoc::new(vec![]);
555 let empty_table_err = empty_set.table("name", None);
556 assert!(empty_table_err.is_err());
557 assert!(matches!(
558 empty_table_err,
559 Err(DocError::TableNotFound { name }) if name == "name"
560 ));
561 }
562
563 #[test]
564 fn test_schema_err() {
565 let empty_set = SqlDoc::new(vec![]);
566 let empty_table_err = empty_set.table("name", Some("schema"));
567 assert!(empty_table_err.is_err());
568 assert!(matches!(
569 empty_table_err,
570 Err(DocError::TableNotFound { name }) if name == "name"
571 ));
572 let duplicate_set = SqlDoc::new(vec![
573 TableDoc::new(Some("schema".to_owned()), "duplicate".to_owned(), None, vec![], None),
574 TableDoc::new(Some("schema".to_owned()), "duplicate".to_owned(), None, vec![], None),
575 ]);
576 let duplicate_tables_err = duplicate_set.table("duplicate", Some("schema"));
577 assert!(matches!(duplicate_tables_err, Err(DocError::DuplicateTablesFound { .. })));
578 }
579
580 fn sort_tables(tables: &mut [TableDoc]) {
581 tables.sort_by(|a, b| {
582 let a_key = (a.schema().unwrap_or(""), a.name());
583 let b_key = (b.schema().unwrap_or(""), b.name());
584 a_key.cmp(&b_key)
585 });
586 }
587
588 fn stamp_table_paths(tables: &mut [TableDoc], path: &Path) {
589 let pb = path.to_path_buf();
590 for t in tables {
591 t.set_path(Some(pb.clone()));
592 }
593 }
594
595 fn sample_sql() -> Vec<(&'static str, SqlDoc)> {
596 vec![
597 (
598 r"
599 -- Users table
600 CREATE TABLE users (
601 -- id
602 id INTEGER PRIMARY KEY,
603 -- login name
604 username TEXT NOT NULL
605 );
606 ",
607 SqlDoc::new(vec![TableDoc::new(
608 None,
609 "users".to_owned(),
610 Some("Users table".to_owned()),
611 vec![
612 ColumnDoc::new("id".to_owned(), Some("id".to_owned())),
613 ColumnDoc::new("username".to_owned(), Some("login name".to_owned())),
614 ],
615 None,
616 )]),
617 ),
618 (
619 r"
620 /* Posts table */
621 CREATE TABLE posts (
622 /* primary key */
623 id INTEGER PRIMARY KEY,
624 title TEXT NOT NULL
625 );
626 ",
627 SqlDoc::new(vec![TableDoc::new(
628 None,
629 "posts".to_owned(),
630 Some("Posts table".to_owned()),
631 vec![
632 ColumnDoc::new("id".to_owned(), Some("primary key".to_owned())),
633 ColumnDoc::new("title".to_owned(), None),
634 ],
635 None,
636 )]),
637 ),
638 (
639 r"
640 CREATE TABLE things (
641 id INTEGER PRIMARY KEY,
642 name TEXT,
643 value INTEGER
644 );
645 ",
646 SqlDoc::new(vec![TableDoc::new(
647 None,
648 "things".to_owned(),
649 None,
650 vec![
651 ColumnDoc::new("id".to_owned(), None),
652 ColumnDoc::new("name".to_owned(), None),
653 ColumnDoc::new("value".to_owned(), None),
654 ],
655 None,
656 )]),
657 ),
658 (
659 r"
660 -- Table with schema
661 CREATE TABLE analytics.events (
662 /* event id */
663 id INTEGER PRIMARY KEY,
664 /* event payload */
665 payload TEXT
666 );
667 ",
668 SqlDoc::new(vec![TableDoc::new(
669 Some("analytics".to_owned()),
670 "events".to_owned(),
671 Some("Table with schema".to_owned()),
672 vec![
673 ColumnDoc::new("id".to_owned(), Some("event id".to_owned())),
674 ColumnDoc::new("payload".to_owned(), Some("event payload".to_owned())),
675 ],
676 None,
677 )]),
678 ),
679 ]
680 }
681
682 #[test]
683 fn test_sql_doc_getters() {
684 let tables = vec![TableDoc::new(None, "name".to_owned(), None, vec![], None)];
685 let sql_doc = SqlDoc::new(vec![TableDoc::new(None, "name".to_owned(), None, vec![], None)]);
686 assert_eq!(sql_doc.number_of_tables(), tables.len());
687 assert_eq!(sql_doc.tables(), tables);
688 }
689
690 #[test]
691 fn test_sql_builder_deny_from_path() {
692 let actual_builder = SqlDoc::from_path("path").deny("path1").deny("path2");
693 let expected_builder = SqlDocBuilder {
694 source: crate::sql_doc::SqlFileDocSource::File(PathBuf::from("path")),
695 deny: vec!["path1".to_owned(), "path2".to_owned()],
696 multiline_flat: MultiFlatten::default(),
697 leading_type: LeadingCommentCapture::default(),
698 };
699 assert_eq!(actual_builder, expected_builder);
700 }
701
702 #[test]
703 fn test_sql_builder_to_sql_doc() -> Result<(), Box<dyn std::error::Error>> {
704 let base = env::temp_dir().join("sql_builder_to_sql_doc");
705 let _ = fs::remove_dir_all(&base);
706 fs::create_dir_all(&base)?;
707 let file = base.join("test_file.sql");
708 let sample = sample_sql();
709 let (contents, expected): (Vec<_>, Vec<_>) = sample.into_iter().unzip();
710 fs::write(&file, contents.join(""))?;
711 let sql_doc = SqlDoc::from_path(&file).build()?;
712 let deny_str =
713 file.to_str().unwrap_or_else(|| panic!("expected a file from PathBuf Found None"));
714 let sql_doc_deny = SqlDoc::from_dir(&base).deny(deny_str).build()?;
715 let mut expected_tables: Vec<TableDoc> =
716 expected.into_iter().flat_map(SqlDoc::into_tables).collect();
717 stamp_table_paths(&mut expected_tables, &file);
718 let expected_doc = SqlDoc::new(expected_tables);
719 assert_eq!(sql_doc, expected_doc);
720 assert_eq!(sql_doc_deny, SqlDoc::new(vec![]));
721 let _ = fs::remove_dir_all(&base);
722 Ok(())
723 }
724
725 #[test]
726 fn test_builder_multiflatten_variants() {
727 let b1 = SqlDoc::from_path("dummy.sql");
728 let b2 = SqlDoc::from_path("dummy.sql").flatten_multiline();
729 let b3 = SqlDoc::from_path("dummy.sql").flatten_multiline_with(" . ");
730 let b4 = SqlDoc::from_path("dummy.sql").flatten_multiline_with("--").preserve_multiline();
731 assert!(matches!(b1, SqlDocBuilder { multiline_flat: MultiFlatten::NoFlat, .. }));
732 assert!(matches!(b2, SqlDocBuilder { multiline_flat: MultiFlatten::FlattenWithNone, .. }));
733 assert!(
734 matches!(b3, SqlDocBuilder { multiline_flat: MultiFlatten::Flatten(s) , .. } if s == " . ")
735 );
736 assert!(matches!(b4, SqlDocBuilder { multiline_flat: MultiFlatten::NoFlat, .. }));
737 }
738
739 #[test]
740 fn test_preserve_multiline_keeps_newlines_in_docs() -> Result<(), Box<dyn std::error::Error>> {
741 let sql = r"
742 /* Table Doc line1
743 line2 */
744 CREATE TABLE things (
745 /* col1
746 doc */
747 id INTEGER
748 );
749 ";
750
751 let built = SqlDoc::builder_from_str(sql).preserve_multiline().build()?;
752
753 let t = built.table("things", None)?;
754 assert_eq!(t.doc(), Some("Table Doc line1\nline2"));
755 assert_eq!(t.columns()[0].doc(), Some("col1\ndoc"));
756 Ok(())
757 }
758
759 #[test]
760 fn test_flatten_multiline_no_separator_removes_newlines()
761 -> Result<(), Box<dyn std::error::Error>> {
762 let sql = r"
763 /* A
764 B
765 C */
766 CREATE TABLE t (
767 /* x
768 y */
769 c INTEGER
770 );
771 ";
772
773 let built = SqlDoc::builder_from_str(sql).flatten_multiline().build()?;
774
775 let t = built.table("t", None)?;
776 assert_eq!(t.doc(), Some("ABC"));
777 assert_eq!(t.columns()[0].doc(), Some("xy"));
778 Ok(())
779 }
780
781 #[test]
782 fn test_flatten_multiline_with_separator_inserts_separator()
783 -> Result<(), Box<dyn std::error::Error>> {
784 let sql = r"
785 /* hello
786 world */
787 CREATE TABLE t (
788 /* x
789 y
790 z */
791 c INTEGER
792 );
793 ";
794
795 let built = SqlDoc::builder_from_str(sql).flatten_multiline_with(" | ").build()?;
796 dbg!(&built);
797 let t = built.table("t", None)?;
798 assert_eq!(t.doc(), Some("hello | world"));
799 assert_eq!(t.columns()[0].doc(), Some("x | y | z"));
800 Ok(())
801 }
802
803 #[test]
804 fn test_tables_mut_allows_modification() {
805 let mut sql_doc =
806 SqlDoc::new(vec![TableDoc::new(None, "t".into(), Some("old".into()), vec![], None)]);
807 for t in sql_doc.tables_mut() {
808 t.set_doc("new");
809 }
810 assert_eq!(sql_doc.tables()[0].doc(), Some("new"));
811 }
812
813 #[test]
814 fn test_builder_build_with_flattening() -> Result<(), Box<dyn std::error::Error>> {
815 let sql = r"
816 /* Table Doc line1
817 line2 */
818 CREATE TABLE things (
819 /* col1
820 doc */
821 id INTEGER
822 );
823 ";
824
825 let built1 = SqlDoc::builder_from_str(sql).flatten_multiline_with(" • ").build()?;
826 let built2 = SqlDoc::builder_from_str(sql).flatten_multiline().build()?;
827
828 let t1 = built1.table("things", None)?;
829 let t2 = built2.table("things", None)?;
830
831 assert_eq!(t1.doc(), Some("Table Doc line1 • line2"));
832 assert_eq!(t1.columns()[0].doc(), Some("col1 • doc"));
833
834 assert_eq!(t2.doc(), Some("Table Doc line1line2"));
835 assert_eq!(t2.columns()[0].doc(), Some("col1doc"));
836
837 Ok(())
838 }
839
840 #[test]
841 fn test_sql_doc_from_str_builds_expected_builder() {
842 let content = "CREATE TABLE t(id INTEGER);";
843
844 let actual = SqlDoc::builder_from_str(content);
845
846 let expected = SqlDocBuilder {
847 source: crate::sql_doc::SqlFileDocSource::FromString(content),
848 deny: vec![],
849 multiline_flat: MultiFlatten::default(),
850 leading_type: LeadingCommentCapture::default(),
851 };
852
853 assert_eq!(actual, expected);
854 }
855
856 #[test]
857 fn test_from_str_parse_sql_doc() -> Result<(), Box<dyn std::error::Error>> {
858 let doc: SqlDoc = "CREATE TABLE t(id INTEGER);".parse()?;
859 assert_eq!(doc.tables().len(), 1);
860 Ok(())
861 }
862
863 #[test]
864 fn test_build_sql_doc_from_paths() -> Result<(), Box<dyn std::error::Error>> {
865 let base = env::temp_dir().join("build_sql_doc_from_paths");
866 let _ = fs::remove_dir_all(&base);
867 fs::create_dir_all(&base)?;
868 let sample = sample_sql();
869 let (sql1, doc1) = &sample[0];
870 let (sql2, doc2) = &sample[1];
871
872 let file1 = base.join("one.sql");
873 let file2 = base.join("two.sql");
874 fs::write(&file1, sql1)?;
875 fs::write(&file2, sql2)?;
876
877 let paths = vec![file1.clone(), file2.clone()];
878 let sql_doc = SqlDoc::from_paths(&paths).build()?;
879
880 let mut expected_tables: Vec<TableDoc> = Vec::new();
881
882 let mut t1 = doc1.clone().into_tables();
883 stamp_table_paths(&mut t1, &file1);
884 expected_tables.extend(t1);
885
886 let mut t2 = doc2.clone().into_tables();
887 stamp_table_paths(&mut t2, &file2);
888 expected_tables.extend(t2);
889
890 let mut actual_tables = sql_doc.into_tables();
891 assert_eq!(actual_tables.len(), expected_tables.len());
892
893 sort_tables(&mut actual_tables);
894 sort_tables(&mut expected_tables);
895
896 assert_eq!(actual_tables, expected_tables);
897
898 let _ = fs::remove_dir_all(&base);
899 Ok(())
900 }
901
902 #[test]
903 fn test_tables_binary_searchable_by_name() {
904 let sample = sample_sql();
905 let tables: Vec<TableDoc> =
906 sample.into_iter().flat_map(|(_, doc)| doc.into_tables()).collect();
907 let sql_doc = SqlDoc::new(tables);
908 let id = sql_doc
909 .tables()
910 .binary_search_by(|t| t.name().cmp("users"))
911 .unwrap_or_else(|_| panic!("expected to find table `users` via binary search"));
912 assert_eq!(sql_doc.tables()[id].name(), "users");
913 }
914
915 #[test]
916 fn test_table_with_schema_not_found_when_name_exists() {
917 let sql_doc = SqlDoc::new(vec![
918 TableDoc::new(Some("analytics".to_owned()), "events".to_owned(), None, vec![], None),
919 TableDoc::new(Some("public".to_owned()), "events".to_owned(), None, vec![], None),
920 ]);
921
922 match sql_doc.table("events", Some("missing")) {
923 Err(DocError::TableWithSchemaNotFound { name, schema })
924 if name == "events" && schema == "missing" => {}
925 Err(e) => panic!("expected TableWithSchemaNotFound(events, missing), got: {e:?}"),
926 Ok(_) => panic!("expected error, got Ok"),
927 }
928 }
929
930 #[test]
931 fn test_table_duplicate_tables_found_for_same_name_and_schema() {
932 let sql_doc = SqlDoc::new(vec![
933 TableDoc::new(Some("analytics".to_owned()), "events".to_owned(), None, vec![], None),
934 TableDoc::new(Some("analytics".to_owned()), "events".to_owned(), None, vec![], None),
935 ]);
936
937 match sql_doc.table("events", Some("analytics")) {
938 Err(DocError::DuplicateTablesFound { .. }) => {}
939 Err(e) => panic!("expected DuplicateTablesFound, got: {e:?}"),
940 Ok(_) => panic!("expected error, got Ok"),
941 }
942 }
943
944 #[test]
945 fn test_table_selects_correct_schema_when_multiple_exist()
946 -> Result<(), Box<dyn std::error::Error>> {
947 let sql_doc = SqlDoc::new(vec![
948 TableDoc::new(Some("analytics".to_owned()), "events".to_owned(), None, vec![], None),
949 TableDoc::new(Some("public".to_owned()), "events".to_owned(), None, vec![], None),
950 ]);
951
952 let t = sql_doc.table("events", Some("public"))?;
953 assert_eq!(t.schema(), Some("public"));
954 Ok(())
955 }
956 #[test]
957 fn test_generate_docs_from_strs_with_paths_builds_tables_and_stamps_paths()
958 -> Result<(), Box<dyn std::error::Error>> {
959 let sql1 = "
961 -- Users table
962 CREATE TABLE users (
963 -- id
964 id INTEGER PRIMARY KEY
965 );
966 ";
967
968 let sql2 = "
969 /* Posts table */
970 CREATE TABLE posts (
971 /* primary key */
972 id INTEGER PRIMARY KEY
973 );
974 ";
975
976 let p1 = PathBuf::from("a/one.sql");
977 let p2 = PathBuf::from("b/two.sql");
978
979 let inputs: Vec<(String, PathBuf)> =
981 vec![(sql1.to_owned(), p1.clone()), (sql2.to_owned(), p2.clone())];
982
983 let doc = SqlDoc::builder_from_strs_with_paths(&inputs).build()?;
985
986 assert_eq!(doc.tables().len(), 2);
988
989 let users = doc.table("users", None)?;
991 let posts = doc.table("posts", None)?;
992
993 assert_eq!(users.path(), Some(p1.as_path()));
995 assert_eq!(posts.path(), Some(p2.as_path()));
996
997 Ok(())
998 }
999
1000 #[test]
1001 fn test_builder_from_strs_with_paths_is_used_in_build_match_arm()
1002 -> Result<(), Box<dyn std::error::Error>> {
1003 let sql_a = "CREATE TABLE alpha (id INTEGER);";
1004 let sql_b = "CREATE TABLE beta (id INTEGER);";
1005 let path_a = PathBuf::from("alpha.sql");
1006 let path_b = PathBuf::from("beta.sql");
1007
1008 let inputs = vec![(sql_a.to_owned(), path_a.clone()), (sql_b.to_owned(), path_b.clone())];
1009
1010 let built = SqlDoc::builder_from_strs_with_paths(&inputs).build()?;
1011
1012 let names: Vec<&str> =
1013 built.tables().iter().map(super::super::docs::TableDoc::name).collect();
1014 assert_eq!(names, vec!["alpha", "beta"]);
1015
1016 assert_eq!(built.table("alpha", None)?.path(), Some(path_a.as_path()));
1017 assert_eq!(built.table("beta", None)?.path(), Some(path_b.as_path()));
1018
1019 Ok(())
1020 }
1021
1022 #[test]
1023 fn test_builder_from_str_no_path_has_none_path() -> Result<(), Box<dyn std::error::Error>> {
1024 let sql = "CREATE TABLE t (id INTEGER);";
1025 let built = SqlDoc::builder_from_str(sql).build()?;
1026
1027 let t = built.table("t", None)?;
1028 assert_eq!(t.path(), None);
1029
1030 Ok(())
1031 }
1032 #[test]
1033 fn test_table_with_schema_not_found_uses_no_schema_provided_message() {
1034 use crate::{SqlDoc, docs::TableDoc, error::DocError};
1035
1036 let sql_doc = SqlDoc::new(vec![
1037 TableDoc::new(Some("analytics".to_owned()), "events".to_owned(), None, vec![], None),
1038 TableDoc::new(Some("public".to_owned()), "events".to_owned(), None, vec![], None),
1039 ]);
1040
1041 match sql_doc.table("events", None) {
1042 Err(DocError::TableWithSchemaNotFound { name, schema }) => {
1043 assert_eq!(name, "events");
1044 assert_eq!(schema, "No schema provided");
1045 }
1046 Err(e) => {
1047 panic!("expected TableWithSchemaNotFound with 'No schema provided', got: {e:?}")
1048 }
1049 Ok(_) => panic!("expected error, got Ok"),
1050 }
1051 }
1052}