1use core::fmt;
4use std::path::{Path, PathBuf};
5
6use sqlparser::ast::{Ident, ObjectName, ObjectNamePart, Spanned, Statement};
7
8use crate::{
9 ast::ParsedSqlFile,
10 comments::{Comments, LeadingCommentCapture, MultiFlatten},
11 error::DocError,
12};
13
14#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct ColumnDoc {
18 name: String,
19 doc: Option<String>,
20}
21impl ColumnDoc {
22 #[must_use]
28 pub const fn new(name: String, doc: Option<String>) -> Self {
29 Self { name, doc }
30 }
31
32 #[must_use]
34 pub fn name(&self) -> &str {
35 &self.name
36 }
37
38 #[must_use]
40 pub fn doc(&self) -> Option<&str> {
41 self.doc.as_deref()
42 }
43
44 pub fn set_doc(&mut self, doc: impl Into<String>) {
46 self.doc = Some(doc.into());
47 }
48}
49
50impl fmt::Display for ColumnDoc {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 writeln!(f, "Column Name: {}", self.name())?;
53 if let Some(c) = self.doc() {
54 writeln!(f, "Column Doc: {c}")?;
55 } else {
56 writeln!(f, "No Column Doc Found")?;
57 }
58 Ok(())
59 }
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct TableDoc {
67 schema: Option<String>,
68 name: String,
69 doc: Option<String>,
70 columns: Vec<ColumnDoc>,
71 path: Option<PathBuf>,
72}
73
74impl TableDoc {
75 #[must_use]
82 #[allow(clippy::missing_const_for_fn)]
83 pub fn new(
84 schema: Option<String>,
85 name: String,
86 doc: Option<String>,
87 mut columns: Vec<ColumnDoc>,
88 path: Option<PathBuf>,
89 ) -> Self {
90 columns.sort_by(|a, b| a.name().cmp(b.name()));
91 Self { schema, name, doc, columns, path }
92 }
93
94 #[must_use]
96 pub fn schema(&self) -> Option<&str> {
97 self.schema.as_deref()
98 }
99
100 #[must_use]
102 pub fn name(&self) -> &str {
103 &self.name
104 }
105
106 #[must_use]
108 pub fn doc(&self) -> Option<&str> {
109 self.doc.as_deref()
110 }
111
112 pub fn set_doc(&mut self, doc: impl Into<String>) {
114 self.doc = Some(doc.into());
115 }
116
117 pub fn set_path(&mut self, path: Option<impl Into<PathBuf>>) {
119 self.path = path.map(Into::into);
120 }
121
122 #[must_use]
124 pub fn columns(&self) -> &[ColumnDoc] {
125 &self.columns
126 }
127
128 pub fn columns_mut(&mut self) -> &mut [ColumnDoc] {
130 &mut self.columns
131 }
132
133 pub fn column(&self, name: &str) -> Result<&ColumnDoc, DocError> {
142 let columns = self.columns();
143 let start = columns.partition_point(|n| n.name() < name);
144 if start == columns.len() || columns[start].name() != name {
145 return Err(DocError::ColumnNotFound { name: name.to_owned() });
146 }
147 let end = columns.partition_point(|n| n.name() <= name);
148 match &columns[start..end] {
149 [single] => Ok(single),
150 multiple => Err(DocError::DuplicateColumnsFound { columns: multiple.to_vec() }),
151 }
152 }
153
154 #[must_use]
156 pub fn path(&self) -> Option<&Path> {
157 self.path.as_deref()
158 }
159
160 #[must_use]
162 pub fn number_of_columns(&self) -> usize {
163 self.columns().len()
164 }
165}
166
167impl fmt::Display for TableDoc {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 if let Some(s) = self.schema() {
170 writeln!(f, "Table Schema: {s}")?;
171 } else {
172 writeln!(f, "No Table Schema")?;
173 }
174
175 writeln!(f, "Table Name: {}", self.name())?;
176
177 if let Some(d) = self.doc() {
178 writeln!(f, "Table Doc: {d}")?;
179 } else {
180 writeln!(f, "No Table Doc")?;
181 }
182
183 writeln!(f, "Table Column Docs: ")?;
184 for col in self.columns() {
185 write!(f, " {col}")?;
186 }
187 Ok(())
188 }
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
194pub struct SqlFileDoc {
195 tables: Vec<TableDoc>,
196}
197
198impl SqlFileDoc {
199 #[must_use]
204 pub const fn new(tables: Vec<TableDoc>) -> Self {
205 Self { tables }
206 }
207
208 pub fn from_parsed_file(
222 file: &ParsedSqlFile,
223 comments: &Comments,
224 capture: LeadingCommentCapture,
225 flatten: MultiFlatten,
226 ) -> Result<Self, DocError> {
227 let mut tables = Vec::new();
228 for statement in file.statements() {
229 #[allow(clippy::single_match)]
230 match statement {
231 Statement::CreateTable(table) => {
232 let table_start = table.span().start.line;
233 let mut column_docs = Vec::new();
234 for column in &table.columns {
235 let column_start = column.span().start.line;
236 let column_leading = comments
237 .leading_comments(column_start, capture)
238 .collapse_comments(flatten);
239 let column_name = column.name.value.clone();
240 let column_doc = match column_leading {
241 Some(col_comment) => {
242 ColumnDoc::new(column_name, Some(col_comment.text().to_owned()))
243 }
244 None => ColumnDoc::new(column_name, None),
245 };
246 column_docs.push(column_doc);
247 }
248 let table_leading =
249 comments.leading_comments(table_start, capture).collapse_comments(flatten);
250 let (schema, name) = schema_and_table(&table.name)?;
251 let table_doc = TableDoc::new(
252 schema,
253 name,
254 table_leading.as_ref().map(|c| c.text().to_owned()),
255 column_docs,
256 file.path_into_path_buf(),
257 );
258 tables.push(table_doc);
259 }
260 _ => {}
262 }
263 }
264
265 Ok(Self { tables })
266 }
267
268 #[must_use]
270 pub fn tables(&self) -> &[TableDoc] {
271 &self.tables
272 }
273
274 pub fn tables_mut(&mut self) -> &mut [TableDoc] {
276 &mut self.tables
277 }
278
279 #[must_use]
281 pub fn number_of_tables(&self) -> usize {
282 self.tables().len()
283 }
284}
285
286impl From<SqlFileDoc> for Vec<TableDoc> {
288 fn from(value: SqlFileDoc) -> Self {
289 value.tables
290 }
291}
292
293impl IntoIterator for SqlFileDoc {
294 type Item = TableDoc;
295 type IntoIter = <Vec<TableDoc> as IntoIterator>::IntoIter;
296 fn into_iter(self) -> Self::IntoIter {
297 self.tables.into_iter()
298 }
299}
300
301fn schema_and_table(name: &ObjectName) -> Result<(Option<String>, String), DocError> {
310 let idents: Vec<&Ident> = name
311 .0
312 .iter()
313 .filter_map(|part| match part {
314 ObjectNamePart::Identifier(ident) => Some(ident),
315 ObjectNamePart::Function(_func) => None,
316 })
317 .collect();
318
319 match idents.as_slice() {
320 [] => {
321 let span = name.span();
322 Err(DocError::InvalidObjectName {
323 message: "ObjectName had no identifier parts".to_owned(),
324 line: span.start.line,
325 column: span.start.column,
326 })
327 }
328 [only] => Ok((None, only.value.clone())),
329 [.., schema, table] => Ok((Some(schema.value.clone()), table.value.clone())),
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use core::fmt;
336 use std::{env, fs, path::PathBuf};
337
338 use sqlparser::{
339 ast::{Ident, ObjectName, ObjectNamePart, ObjectNamePartFunction},
340 tokenizer::Span,
341 };
342
343 use crate::{
344 docs::{ColumnDoc, SqlFileDoc, TableDoc, schema_and_table},
345 error::DocError,
346 };
347
348 #[test]
349 fn test_sql_docs_struct() {
350 let column_doc = ColumnDoc::new("id".to_owned(), Some("The ID for the table".to_owned()));
351 let columns = vec![column_doc];
352 let table_doc = TableDoc::new(
353 None,
354 "user".to_owned(),
355 Some("The table for users".to_owned()),
356 columns,
357 None,
358 );
359 let tables = vec![table_doc];
360 let sql_doc = SqlFileDoc::new(tables);
361 let sql_doc_val =
362 sql_doc.tables().first().unwrap_or_else(|| panic!("unable to find table"));
363 assert_eq!(sql_doc_val.name(), "user");
364 let sql_doc_val_column =
365 sql_doc_val.columns().first().unwrap_or_else(|| panic!("unable to find columns"));
366 assert_eq!(sql_doc_val_column.name(), "id");
367 }
368
369 fn single_line_comments_sql() -> &'static str {
370 "-- Users table stores user account information
371CREATE TABLE users (
372 -- Primary key
373 id INTEGER PRIMARY KEY,
374 -- Username for login
375 username VARCHAR(255) NOT NULL,
376 -- Email address
377 email VARCHAR(255) UNIQUE NOT NULL,
378 -- When the user registered
379 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
380);
381
382-- Posts table stores blog posts
383CREATE TABLE posts (
384 -- Primary key
385 id INTEGER PRIMARY KEY,
386 -- Post title
387 title VARCHAR(255) NOT NULL,
388 -- Foreign key linking to users
389 user_id INTEGER NOT NULL,
390 -- Main body text
391 body TEXT NOT NULL,
392 -- When the post was created
393 published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
394);"
395 }
396
397 fn multiline_comments_sql() -> &'static str {
398 r"/* Users table stores user account information
399multiline */
400CREATE TABLE users (
401 /* Primary key
402 multiline */
403 id INTEGER PRIMARY KEY,
404 /* Username for login
405 multiline */
406 username VARCHAR(255) NOT NULL,
407 /* Email address
408 multiline */
409 email VARCHAR(255) UNIQUE NOT NULL,
410 /* When the user registered
411 multiline */
412 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
413);
414
415/* Posts table stores blog posts
416multiline */
417CREATE TABLE posts (
418 /* Primary key
419 multiline */
420 id INTEGER PRIMARY KEY,
421 /* Post title
422 multiline */
423 title VARCHAR(255) NOT NULL,
424 /* Foreign key linking to users
425 multiline */
426 user_id INTEGER NOT NULL,
427 /* Main body text
428 multiline */
429 body TEXT NOT NULL,
430 /* When the post was created
431 multiline */
432 published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
433);"
434 }
435
436 fn no_comments_sql() -> &'static str {
437 "CREATE TABLE users (
438 id INTEGER PRIMARY KEY,
439 username VARCHAR(255) NOT NULL,
440 email VARCHAR(255) UNIQUE NOT NULL,
441 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
442);
443
444CREATE TABLE posts (
445 id INTEGER PRIMARY KEY,
446 title VARCHAR(255) NOT NULL,
447 user_id INTEGER NOT NULL,
448 body TEXT NOT NULL,
449 published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
450);"
451 }
452
453 fn mixed_comments_sql() -> &'static str {
454 "-- interstitial Comment above statements (should be ignored)
455
456/* Users table stores user account information */
457CREATE TABLE users ( /* users interstitial comment
458(should be ignored) */
459 -- Primary key
460 id INTEGER PRIMARY KEY, -- Id comment that is interstitial (should be ignored)
461 /* Username for login */
462 username VARCHAR(255) NOT NULL,
463 -- Email address
464 email VARCHAR(255) UNIQUE NOT NULL,
465 /* When the user registered */
466 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
467);
468
469/* Posts table stores blog posts */
470CREATE TABLE posts (
471 -- Primary key
472 id INTEGER PRIMARY KEY,
473 /* Post title */
474 title VARCHAR(255) NOT NULL,
475 -- Foreign key linking to users
476 user_id INTEGER NOT NULL,
477 /* Main body text */
478 body TEXT NOT NULL,
479 -- When the post was created
480 published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
481);
482"
483 }
484
485 #[test]
486 fn generate_docs_files() -> Result<(), Box<dyn std::error::Error>> {
487 use crate::{ast::ParsedSqlFileSet, comments::Comments, source::SqlSource};
488 let base = env::temp_dir().join("all_sql_files2");
489 let _ = fs::remove_dir_all(&base);
490 fs::create_dir_all(&base)?;
491 let file1 = base.join("with_single_line_comments.sql");
492 fs::File::create(&file1)?;
493 fs::write(&file1, single_line_comments_sql())?;
494 let file2 = base.join("with_multiline_comments.sql");
495 fs::File::create(&file2)?;
496 fs::write(&file2, multiline_comments_sql())?;
497 let file3 = base.join("with_mixed_comments.sql");
498 fs::File::create(&file3)?;
499 fs::write(&file3, mixed_comments_sql())?;
500 let file4 = base.join("without_comments.sql");
501 fs::File::create(&file4)?;
502 fs::write(&file4, no_comments_sql())?;
503 let set = SqlSource::sql_sources(&base, &[])?;
504 let parsed_set = ParsedSqlFileSet::parse_all(set)?;
505 let expected_values = expect_values();
506 let capture = crate::comments::LeadingCommentCapture::default();
507
508 for file in parsed_set.files() {
509 let comments = Comments::parse_all_comments_from_file(file)?;
510 let docs = SqlFileDoc::from_parsed_file(
511 file,
512 &comments,
513 capture,
514 crate::comments::MultiFlatten::NoFlat,
515 );
516 let filename = file
517 .file()
518 .path()
519 .and_then(|p| p.file_name())
520 .and_then(|s| s.to_str())
521 .ok_or("unable to parse file")?;
522
523 let got = docs?;
524 let file_path = file.file().path().ok_or("missing path")?;
525
526 match filename {
527 "with_single_line_comments.sql" | "with_mixed_comments.sql" => {
528 let expected = with_path(expected_values[0].clone(), file_path);
529 assert_eq!(&got, &expected);
530 }
531 "with_multiline_comments.sql" => {
532 let expected = with_path(expected_values[1].clone(), file_path);
533 assert_eq!(&got, &expected);
534 }
535 "without_comments.sql" => {
536 let expected = with_path(expected_without_comments_docs(), file_path);
537 assert_eq!(&got, &expected);
538 }
539 _ => unreachable!(),
540 }
541 }
542 let _ = fs::remove_dir_all(&base);
543 Ok(())
544 }
545
546 fn with_path(mut doc: SqlFileDoc, path: &std::path::Path) -> SqlFileDoc {
547 let pb = path.to_path_buf();
548
549 for table in doc.tables_mut() {
550 table.set_path(Some(pb.clone()));
551 }
552
553 doc
554 }
555
556 fn expected_without_comments_docs() -> SqlFileDoc {
557 SqlFileDoc::new(vec![
558 TableDoc::new(
559 None,
560 "users".to_owned(),
561 None,
562 vec![
563 ColumnDoc::new("id".to_owned(), None),
564 ColumnDoc::new("username".to_owned(), None),
565 ColumnDoc::new("email".to_owned(), None),
566 ColumnDoc::new("created_at".to_owned(), None),
567 ],
568 None,
569 ),
570 TableDoc::new(
571 None,
572 "posts".to_owned(),
573 None,
574 vec![
575 ColumnDoc::new("id".to_owned(), None),
576 ColumnDoc::new("title".to_owned(), None),
577 ColumnDoc::new("user_id".to_owned(), None),
578 ColumnDoc::new("body".to_owned(), None),
579 ColumnDoc::new("published_at".to_owned(), None),
580 ],
581 None,
582 ),
583 ])
584 }
585
586 fn expect_values() -> Vec<SqlFileDoc> {
587 let mut docs = Vec::new();
588
589 let first_docs = SqlFileDoc::new(vec![
590 TableDoc::new(
591 None,
592 "users".to_owned(),
593 Some("Users table stores user account information".to_owned()),
594 vec![
595 ColumnDoc::new("id".to_owned(), Some("Primary key".to_owned())),
596 ColumnDoc::new("username".to_owned(), Some("Username for login".to_owned())),
597 ColumnDoc::new("email".to_owned(), Some("Email address".to_owned())),
598 ColumnDoc::new(
599 "created_at".to_owned(),
600 Some("When the user registered".to_owned()),
601 ),
602 ],
603 None,
604 ),
605 TableDoc::new(
606 None,
607 "posts".to_owned(),
608 Some("Posts table stores blog posts".to_owned()),
609 vec![
610 ColumnDoc::new("id".to_owned(), Some("Primary key".to_owned())),
611 ColumnDoc::new("title".to_owned(), Some("Post title".to_owned())),
612 ColumnDoc::new(
613 "user_id".to_owned(),
614 Some("Foreign key linking to users".to_owned()),
615 ),
616 ColumnDoc::new("body".to_owned(), Some("Main body text".to_owned())),
617 ColumnDoc::new(
618 "published_at".to_owned(),
619 Some("When the post was created".to_owned()),
620 ),
621 ],
622 None,
623 ),
624 ]);
625 docs.push(first_docs);
626
627 let second_docs = SqlFileDoc::new(vec![
628 TableDoc::new(
629 None,
630 "users".to_owned(),
631 Some("Users table stores user account information\nmultiline".to_owned()),
632 vec![
633 ColumnDoc::new("id".to_owned(), Some("Primary key\nmultiline".to_owned())),
634 ColumnDoc::new(
635 "username".to_owned(),
636 Some("Username for login\nmultiline".to_owned()),
637 ),
638 ColumnDoc::new("email".to_owned(), Some("Email address\nmultiline".to_owned())),
639 ColumnDoc::new(
640 "created_at".to_owned(),
641 Some("When the user registered\nmultiline".to_owned()),
642 ),
643 ],
644 None,
645 ),
646 TableDoc::new(
647 None,
648 "posts".to_owned(),
649 Some("Posts table stores blog posts\nmultiline".to_owned()),
650 vec![
651 ColumnDoc::new("id".to_owned(), Some("Primary key\nmultiline".to_owned())),
652 ColumnDoc::new("title".to_owned(), Some("Post title\nmultiline".to_owned())),
653 ColumnDoc::new(
654 "user_id".to_owned(),
655 Some("Foreign key linking to users\nmultiline".to_owned()),
656 ),
657 ColumnDoc::new("body".to_owned(), Some("Main body text\nmultiline".to_owned())),
658 ColumnDoc::new(
659 "published_at".to_owned(),
660 Some("When the post was created\nmultiline".to_owned()),
661 ),
662 ],
663 None,
664 ),
665 ]);
666 docs.push(second_docs);
667
668 docs
669 }
670
671 #[test]
672 fn test_doc() {
673 let col_doc = ColumnDoc::new("test".to_owned(), Some("comment".to_owned()));
674 assert_eq!(&col_doc.to_string(), &"Column Name: test\nColumn Doc: comment\n".to_owned());
675 let col_doc_no_doc = ColumnDoc::new("id".to_owned(), None);
676 assert_eq!(
677 &col_doc_no_doc.to_string(),
678 &"Column Name: id\nNo Column Doc Found\n".to_owned()
679 );
680 assert_eq!(col_doc.doc(), Some("comment"));
681 assert_eq!(col_doc.name(), "test");
682 assert_eq!(col_doc_no_doc.doc(), None);
683 assert_eq!(col_doc_no_doc.name(), "id");
684 let table_doc = TableDoc::new(
685 Some("schema".to_owned()),
686 "table".to_owned(),
687 Some("table doc".to_owned()),
688 vec![col_doc.clone(), col_doc_no_doc.clone()],
689 None,
690 );
691 assert_eq!(table_doc.number_of_columns(), 2);
692 let last_col = ColumnDoc::new("zed".to_owned(), Some("the last column".to_owned()));
693 let table_doc_no_doc = TableDoc::new(
694 None,
695 "table".to_owned(),
696 None,
697 vec![last_col, col_doc, col_doc_no_doc],
698 None,
699 );
700 assert_eq!(table_doc_no_doc.number_of_columns(), 3);
701 assert_eq!(table_doc.name(), "table");
702 assert_eq!(table_doc.schema(), Some("schema"));
703 assert_eq!(
704 table_doc.to_string(),
705 "Table Schema: schema\nTable Name: table\nTable Doc: table doc\nTable Column Docs: \n Column Name: id\nNo Column Doc Found\n Column Name: test\nColumn Doc: comment\n"
706 );
707 assert_eq!(table_doc_no_doc.schema(), None);
708 assert_eq!(table_doc_no_doc.name(), "table");
709 assert_eq!(
710 table_doc_no_doc.to_string(),
711 "No Table Schema\nTable Name: table\nNo Table Doc\nTable Column Docs: \n Column Name: id\nNo Column Doc Found\n Column Name: test\nColumn Doc: comment\n Column Name: zed\nColumn Doc: the last column\n"
712 );
713 }
714
715 fn ident(v: &str) -> Ident {
716 Ident { value: v.to_owned(), quote_style: None, span: Span::empty() }
717 }
718
719 fn func_part(name: &str) -> ObjectNamePart {
720 ObjectNamePart::Function(ObjectNamePartFunction { name: ident(name), args: vec![] })
721 }
722
723 #[test]
724 fn schema_and_table_errors_when_no_identifier_parts() {
725 let name = ObjectName(vec![func_part("now")]);
726
727 let err = match schema_and_table(&name) {
728 Ok(v) => panic!("expected Err(DocError::InvalidObjectName), got Ok({v:?})"),
729 Err(e) => e,
730 };
731
732 match err {
733 DocError::InvalidObjectName { message, .. } => {
734 assert_eq!(message, "ObjectName had no identifier parts");
735 }
736 other => panic!("unexpected error: {other:?}"),
737 }
738 }
739
740 #[test]
741 fn schema_and_table_single_identifier() {
742 let name = ObjectName(vec![ObjectNamePart::Identifier(ident("users"))]);
743
744 let (schema, table) = match schema_and_table(&name) {
745 Ok(v) => v,
746 Err(e) => panic!("unexpected error: {e:?}"),
747 };
748
749 assert_eq!(schema, None);
750 assert_eq!(table, "users");
751 }
752
753 #[test]
754 fn schema_and_table_schema_and_table_with_function_ignored()
755 -> Result<(), Box<dyn std::error::Error>> {
756 let name = ObjectName(vec![
757 ObjectNamePart::Identifier(ident("catalog")),
758 ObjectNamePart::Identifier(ident("public")),
759 func_part("some_func"),
760 ObjectNamePart::Identifier(ident("orders")),
761 ]);
762
763 let (schema, table) = schema_and_table(&name)?;
764 assert_eq!(schema, Some("public".to_owned()));
765 assert_eq!(table, "orders");
766 Ok(())
767 }
768
769 struct FailOnNthWrite {
770 fail_at: usize,
771 writes: usize,
772 sink: String,
773 }
774
775 impl FailOnNthWrite {
776 fn new(fail_at: usize) -> Self {
777 Self { fail_at, writes: 0, sink: String::new() }
778 }
779 }
780
781 impl fmt::Write for FailOnNthWrite {
782 fn write_str(&mut self, s: &str) -> fmt::Result {
783 self.writes += 1;
784 if self.writes == self.fail_at {
785 return Err(fmt::Error);
786 }
787 self.sink.push_str(s);
788 Ok(())
789 }
790 }
791
792 fn run_fail_at<T: fmt::Display>(v: &T, fail_at: usize) -> Result<(), fmt::Error> {
793 let mut w = FailOnNthWrite::new(fail_at);
794 fmt::write(&mut w, format_args!("{v}"))
795 }
796
797 fn count_writes<T: fmt::Display>(v: &T) -> usize {
798 let mut w = FailOnNthWrite { fail_at: usize::MAX, writes: 0, sink: String::new() };
799 let _ = fmt::write(&mut w, format_args!("{v}"));
800 w.writes
801 }
802
803 #[test]
804 fn test_display_propagates_every_question_mark_path_for_column_and_table() {
805 let col_with_doc = ColumnDoc::new("col_a".into(), Some("doc".into()));
806 let col_without_doc = ColumnDoc::new("col_b".into(), None);
807
808 let table = TableDoc::new(
809 Some("public".into()),
810 "users".into(),
811 Some("table doc".into()),
812 vec![col_with_doc.clone(), col_without_doc],
813 None,
814 );
815
816 let col_writes = count_writes(&col_with_doc);
817 let table_writes = count_writes(&table);
818
819 for i in 1..=col_writes {
820 assert!(
821 run_fail_at(&col_with_doc, i).is_err(),
822 "ColumnDoc should error when failing at write #{i} (total writes {col_writes})"
823 );
824 }
825
826 for i in 1..=table_writes {
827 assert!(
828 run_fail_at(&table, i).is_err(),
829 "TableDoc should error when failing at write #{i} (total writes {table_writes})"
830 );
831 }
832 }
833
834 #[test]
835 fn column_doc_set_doc_updates_doc() {
836 let mut col = ColumnDoc::new("id".to_owned(), None);
837 assert_eq!(col.name(), "id");
838 assert_eq!(col.doc(), None);
839 col.set_doc("primary key");
840 assert_eq!(col.doc(), Some("primary key"));
841 let new_doc = String::from("primary key for users table");
842 col.set_doc(new_doc);
843 assert_eq!(col.doc(), Some("primary key for users table"));
844 }
845
846 #[test]
847 fn table_doc_set_doc_updates_doc() {
848 let mut table = TableDoc::new(None, "users".to_owned(), None, Vec::new(), None);
849 assert_eq!(table.name(), "users");
850 assert_eq!(table.schema(), None);
851 assert_eq!(table.doc(), None);
852 table.set_doc("users table docs");
853 assert_eq!(table.doc(), Some("users table docs"));
854 table.set_doc(String::from("updated users table docs"));
855 assert_eq!(table.doc(), Some("updated users table docs"));
856 }
857
858 #[test]
859 fn columns_mut_allows_mutating_column_docs() {
860 let mut table = TableDoc::new(
861 None,
862 "users".to_owned(),
863 None,
864 vec![
865 ColumnDoc::new("id".to_owned(), None),
866 ColumnDoc::new("username".to_owned(), None),
867 ],
868 None,
869 );
870
871 {
872 let cols_mut = table.columns_mut();
873 assert_eq!(cols_mut.len(), 2);
874 cols_mut[0].set_doc("primary key");
875 cols_mut[1].set_doc("login name");
876 }
877
878 let cols = table.columns();
879 assert_eq!(cols[0].name(), "id");
880 assert_eq!(cols[0].doc(), Some("primary key"));
881 assert_eq!(cols[1].name(), "username");
882 assert_eq!(cols[1].doc(), Some("login name"));
883 }
884 #[test]
885 fn test_from_sql_file_doc_into_vec_table_doc_preserves_contents_and_order() {
886 let t1 = TableDoc::new(
887 None,
888 "users".to_owned(),
889 Some("users doc".to_owned()),
890 vec![ColumnDoc::new("id".to_owned(), Some("pk".to_owned()))],
891 None,
892 );
893 let t2 = TableDoc::new(
894 Some("analytics".to_owned()),
895 "events".to_owned(),
896 None,
897 vec![ColumnDoc::new("payload".to_owned(), None)],
898 None,
899 );
900
901 let sql_file_doc = SqlFileDoc::new(vec![t1.clone(), t2.clone()]);
902 let got: Vec<TableDoc> = Vec::from(sql_file_doc);
903
904 let expected = vec![t1, t2];
905 assert_eq!(got, expected);
906 }
907 #[test]
908 fn test_table_doc_path_getter_returns_expected_value() {
909 let mut table = TableDoc::new(None, "users".to_owned(), None, Vec::new(), None);
910 assert_eq!(table.path(), None);
911 let pb = PathBuf::from("some/dir/file.sql");
912 table.set_path(Some(pb.clone()));
913 assert_eq!(table.path(), Some(pb.as_path()));
914 let no_path: Option<PathBuf> = None;
915 table.set_path(no_path);
916 assert_eq!(table.path(), None);
917 }
918
919 #[test]
920 fn test_table_doc_column() {
921 let table = TableDoc::new(
922 None,
923 "users".to_owned(),
924 None,
925 vec![
926 ColumnDoc::new("id".to_owned(), None),
927 ColumnDoc::new("username".to_owned(), None),
928 ],
929 None,
930 );
931
932 let col = table.column("id").unwrap_or_else(|_| panic!("Column 'id' should exist"));
933 assert_eq!(col.name(), "id");
934
935 let missing = table.column("nope");
936 assert!(missing.is_err());
937 assert!(matches!(missing, Err(DocError::ColumnNotFound { name }) if name == "nope"));
938 }
939}