1use anyhow::{Result, bail};
2use itertools::Itertools;
3use rowan::Direction;
4use squawk_line_index::{LineEnding, UniversalNewlines, find_newline};
5use squawk_syntax::ast::{self, AstNode, LitKind};
6use squawk_syntax::quote::quote_column_alias;
7use squawk_syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
8use tiny_pretty::Doc;
9use tiny_pretty::{LineBreak, PrintOptions, print};
10
11fn build_source_file(source_file: &ast::SourceFile) -> Doc<'_> {
15 let mut doc = Doc::nil();
16 for el in source_file.syntax().children_with_tokens() {
17 match el {
18 rowan::NodeOrToken::Node(node) => {
19 if let Some(stmt) = ast::Stmt::cast(node) {
20 match stmt {
21 ast::Stmt::Select(select) => {
22 doc = doc.append(build_select_doc(&select));
23 }
24 ast::Stmt::CreateTable(create_table) => {
25 doc = doc.append(build_create_table(&create_table));
26 }
27 _ => (),
28 }
29 }
30 }
31 rowan::NodeOrToken::Token(token) => {
32 if token.kind() == SyntaxKind::COMMENT {
33 doc = doc.append(Doc::text(token.text().to_string()));
34 } else if token.kind() == SyntaxKind::WHITESPACE {
35 let lines = token.text().universal_newlines().count();
37 if lines >= 2 {
38 doc = doc.append(Doc::empty_line()).append(Doc::empty_line());
39 } else {
40 doc = doc.append(Doc::empty_line());
41 }
42 }
43 }
44 }
45 }
46 doc
47}
48
49fn build_create_table<'a>(create_table: &ast::CreateTable) -> Doc<'a> {
50 let mut doc = Doc::text("create")
51 .append(Doc::space())
52 .append(Doc::text("table"))
53 .append(Doc::space())
54 .append(Doc::text(
55 create_table.table_name().unwrap().syntax().to_string(),
56 ))
57 .append(Doc::text("("))
58 .append(
59 Doc::line_or_nil()
60 .append(Doc::list(
61 Itertools::intersperse(
62 create_table
63 .table_arg_list()
64 .unwrap()
65 .args()
66 .map(build_table_arg),
67 Doc::text(",").append(Doc::hard_line()),
68 )
69 .collect(),
70 ))
71 .nest(2)
72 .append(Doc::line_or_nil())
73 .group(),
74 )
75 .append(Doc::text(")"));
76
77 doc = doc.append(build_semicolon(create_table.semicolon_token()));
78
79 doc
80}
81
82fn build_table_arg<'a>(create_table: ast::TableArg) -> Doc<'a> {
83 match create_table {
84 ast::TableArg::Column(column) => build_column_name(column.name().unwrap())
85 .append(Doc::space())
86 .append(Doc::text(column.ty().unwrap().syntax().to_string())),
87 ast::TableArg::LikeClause(_like_clause) => todo!(),
88 ast::TableArg::TableConstraint(_table_constraint) => todo!(),
89 }
90}
91
92fn build_select_doc<'a>(select: &ast::Select) -> Doc<'a> {
93 let mut doc = Doc::text("select").append(Doc::line_or_space());
94
95 if let Some(select_clause) = select.select_clause() {
96 if let Some(distinct_clause) = select_clause.distinct_clause() {
97 doc = doc.append(leading_comments(distinct_clause.syntax()));
98 doc = doc.append(Doc::text("distinct")).append(Doc::space());
99 }
100 if let Some(all_token) = select_clause.all_token() {
101 doc = doc.append(leading_comments_token(&all_token));
102 doc = doc.append(Doc::text("all")).append(Doc::space());
103 }
104 if let Some(target_list) = select_clause.target_list() {
105 doc = doc.append(leading_comments(target_list.syntax()));
106 doc = doc
107 .append(Doc::list(
108 Itertools::intersperse(
109 target_list.targets().flat_map(build_target),
110 Doc::text(",").append(Doc::line_or_space()),
111 )
112 .collect(),
113 ))
114 .nest(2);
115 }
116 }
117
118 if let Some(from) = &select.from_clause() {
119 doc = doc.append(
120 Doc::line_or_space()
121 .append(Doc::text("from"))
122 .append(Doc::space())
123 .append(Doc::text(
124 from.from_items().next().unwrap().syntax().to_string(),
125 )),
126 );
127 }
128
129 if let Some(group) = &select.group_by_clause() {
130 let mut group_doc = Doc::line_or_space().append(leading_comments(group.syntax()));
131 group_doc = group_doc.append(Doc::text("group")).append(Doc::space());
132 if let Some(by_token) = group.by_token() {
133 group_doc = group_doc.append(leading_comments_token(&by_token));
134 }
135 group_doc = group_doc.append(Doc::text("by")).append(Doc::space());
136 if let Some(list) = group.group_by_list() {
137 group_doc = group_doc.append(build_group_by_list(list));
138 }
139 doc = doc.append(group_doc);
140 }
141
142 doc = doc.append(build_semicolon(select.semicolon_token()));
143
144 doc.group()
145}
146
147fn build_group_by_list<'a>(list: ast::GroupByList) -> Doc<'a> {
148 leading_comments(list.syntax()).append(Doc::text(list.syntax().to_string()))
149}
150
151fn build_semicolon<'a>(semi: Option<SyntaxToken>) -> Doc<'a> {
152 let Some(semi) = semi else {
153 return Doc::nil();
154 };
155 let mut doc = Doc::nil();
156 let mut comments: Vec<SyntaxToken> = vec![];
157 for next in semi.siblings_with_tokens(Direction::Prev).skip(1) {
158 match next {
159 rowan::NodeOrToken::Node(_) => break,
160 rowan::NodeOrToken::Token(token) => {
161 if token.kind() == SyntaxKind::COMMENT {
162 comments.push(token);
163 } else if token.kind() == SyntaxKind::WHITESPACE {
164 continue;
165 } else {
166 break;
167 }
168 }
169 }
170 }
171 for comment in comments.iter().rev() {
172 doc = doc.append(Doc::text(comment.text().to_string()));
173 }
174 doc.append(Doc::text(";"))
175}
176
177fn build_expr<'a>(expr: ast::Expr) -> Doc<'a> {
178 match expr {
179 ast::Expr::ArrayExpr(array_expr) => {
180 let mut doc = Doc::nil();
181
182 if array_expr.array_token().is_some() {
184 doc = doc.append(Doc::text("array"));
185 };
186
187 if let Some(select) = array_expr.select() {
188 doc = doc
189 .append(Doc::text("("))
190 .append(build_select_doc(&select))
191 .append(Doc::text(")"))
192 } else {
193 doc = doc
194 .append(Doc::text("["))
195 .append(Doc::list(
196 Itertools::intersperse(
197 array_expr.exprs().map(build_expr),
198 Doc::text(",").append(Doc::space()),
199 )
200 .collect(),
201 ))
202 .append(Doc::text("]"));
203 }
204
205 doc
206 }
207 ast::Expr::BetweenExpr(between_expr) => {
208 let mut doc = build_expr(between_expr.target().unwrap());
209 if between_expr.not_token().is_some() {
210 doc = doc.append(Doc::space()).append(Doc::text("not"));
211 }
212 doc = doc.append(Doc::space()).append(Doc::text("between"));
213 if between_expr.asymmetric_token().is_some() {
214 doc = doc.append(Doc::space()).append(Doc::text("asymmetric"));
215 } else if between_expr.symmetric_token().is_some() {
216 doc = doc.append(Doc::space()).append(Doc::text("symmetric"));
217 }
218 doc.append(Doc::space())
219 .append(build_expr(between_expr.start().unwrap()))
220 .append(Doc::space())
221 .append(Doc::text("and"))
222 .append(Doc::space())
223 .append(build_expr(between_expr.end().unwrap()))
224 }
225 ast::Expr::BinExpr(bin_expr) => build_expr(bin_expr.lhs().unwrap())
226 .append(Doc::space())
227 .append(build_op(bin_expr.op().unwrap()))
228 .append(Doc::space())
229 .append(build_expr(bin_expr.rhs().unwrap())),
230 ast::Expr::CastExpr(cast_expr) => {
233 let mut doc = Doc::nil();
234 if cast_expr.colon_colon().is_some() {
235 doc = doc
236 .append(build_expr(cast_expr.expr().unwrap()))
237 .append(Doc::text("::"))
238 .append(build_type(cast_expr.ty().unwrap()))
239 } else if cast_expr.as_token().is_some() {
240 if cast_expr.cast_token().is_some() {
241 doc = doc.append(Doc::text("cast"))
242 } else if cast_expr.treat_token().is_some() {
243 doc = doc.append(Doc::text("treat"))
244 }
245 doc = doc
246 .append(Doc::text("("))
247 .append(build_expr(cast_expr.expr().unwrap()))
248 .append(Doc::space())
249 .append(Doc::text("as"))
250 .append(Doc::space())
251 .append(build_type(cast_expr.ty().unwrap()))
252 .append(Doc::text(")"))
253 } else {
254 doc = doc
255 .append(build_type(cast_expr.ty().unwrap()))
256 .append(Doc::space())
257 .append(build_literal(cast_expr.literal().unwrap()))
258 }
259 doc
260 }
261 ast::Expr::Collate(collate) => build_expr(collate.expr().unwrap())
262 .append(Doc::space())
263 .append(Doc::text("collate"))
264 .append(Doc::space())
265 .append(Doc::text(
266 collate.collation_ref().unwrap().syntax().to_string(),
267 )),
268 ast::Expr::Literal(literal) => build_literal(literal),
271 ast::Expr::PostfixExpr(postfix_expr) => {
274 let expr = build_expr(postfix_expr.expr().unwrap());
275 let op = match postfix_expr.op().unwrap() {
276 ast::PostfixOp::AtLocal(_) => Doc::text("at local"),
277 ast::PostfixOp::IsNull(_) => Doc::text("isnull"),
278 ast::PostfixOp::NotNull(_) => Doc::text("notnull"),
279 ast::PostfixOp::IsJson(n) => {
280 let mut doc = Doc::text("is json");
281 if let Some(clause) = n.json_keys_unique_clause() {
282 doc = doc
283 .append(Doc::space())
284 .append(build_json_keys_unique_clause(clause));
285 }
286 doc
287 }
288 ast::PostfixOp::IsJsonArray(n) => {
289 let mut doc = Doc::text("is json array");
290 if let Some(clause) = n.json_keys_unique_clause() {
291 doc = doc
292 .append(Doc::space())
293 .append(build_json_keys_unique_clause(clause));
294 }
295 doc
296 }
297 ast::PostfixOp::IsJsonObject(n) => {
298 let mut doc = Doc::text("is json object");
299 if let Some(clause) = n.json_keys_unique_clause() {
300 doc = doc
301 .append(Doc::space())
302 .append(build_json_keys_unique_clause(clause));
303 }
304 doc
305 }
306 ast::PostfixOp::IsJsonScalar(n) => {
307 let mut doc = Doc::text("is json scalar");
308 if let Some(clause) = n.json_keys_unique_clause() {
309 doc = doc
310 .append(Doc::space())
311 .append(build_json_keys_unique_clause(clause));
312 }
313 doc
314 }
315 ast::PostfixOp::IsJsonValue(n) => {
316 let mut doc = Doc::text("is json value");
317 if let Some(clause) = n.json_keys_unique_clause() {
318 doc = doc
319 .append(Doc::space())
320 .append(build_json_keys_unique_clause(clause));
321 }
322 doc
323 }
324 ast::PostfixOp::IsNormalized(n) => {
325 let mut doc = Doc::text("is");
326 if let Some(form) = n.unicode_normal_form() {
327 doc = doc
328 .append(Doc::space())
329 .append(build_unicode_normal_form(form));
330 }
331 doc.append(Doc::space()).append(Doc::text("normalized"))
332 }
333 ast::PostfixOp::IsNotJson(n) => {
334 let mut doc = Doc::text("is not json");
335 if let Some(clause) = n.json_keys_unique_clause() {
336 doc = doc
337 .append(Doc::space())
338 .append(build_json_keys_unique_clause(clause));
339 }
340 doc
341 }
342 ast::PostfixOp::IsNotJsonArray(n) => {
343 let mut doc = Doc::text("is not json array");
344 if let Some(clause) = n.json_keys_unique_clause() {
345 doc = doc
346 .append(Doc::space())
347 .append(build_json_keys_unique_clause(clause));
348 }
349 doc
350 }
351 ast::PostfixOp::IsNotJsonObject(n) => {
352 let mut doc = Doc::text("is not json object");
353 if let Some(clause) = n.json_keys_unique_clause() {
354 doc = doc
355 .append(Doc::space())
356 .append(build_json_keys_unique_clause(clause));
357 }
358 doc
359 }
360 ast::PostfixOp::IsNotJsonScalar(n) => {
361 let mut doc = Doc::text("is not json scalar");
362 if let Some(clause) = n.json_keys_unique_clause() {
363 doc = doc
364 .append(Doc::space())
365 .append(build_json_keys_unique_clause(clause));
366 }
367 doc
368 }
369 ast::PostfixOp::IsNotJsonValue(n) => {
370 let mut doc = Doc::text("is not json value");
371 if let Some(clause) = n.json_keys_unique_clause() {
372 doc = doc
373 .append(Doc::space())
374 .append(build_json_keys_unique_clause(clause));
375 }
376 doc
377 }
378 ast::PostfixOp::IsNotNormalized(n) => {
379 let mut doc = Doc::text("is not");
380 if let Some(form) = n.unicode_normal_form() {
381 doc = doc
382 .append(Doc::space())
383 .append(build_unicode_normal_form(form));
384 }
385 doc.append(Doc::space()).append(Doc::text("normalized"))
386 }
387 };
388 expr.append(Doc::space()).append(op)
389 }
390 _ => Doc::text(expr.syntax().to_string()),
394 }
395}
396
397fn build_json_keys_unique_clause<'a>(clause: ast::JsonKeysUniqueClause) -> Doc<'a> {
398 let prefix = match clause {
399 ast::JsonKeysUniqueClause::JsonWithoutUniqueKeys(_) => "without",
400 ast::JsonKeysUniqueClause::JsonWithUniqueKeys(_) => "with",
401 };
402 Doc::text(prefix)
403 .append(Doc::space())
404 .append(Doc::text("unique"))
405 .append(Doc::space())
406 .append(Doc::text("keys"))
407}
408
409fn build_unicode_normal_form<'a>(form: ast::UnicodeNormalForm) -> Doc<'a> {
410 if form.nfc_token().is_some() {
411 Doc::text("nfc")
412 } else if form.nfd_token().is_some() {
413 Doc::text("nfd")
414 } else if form.nfkc_token().is_some() {
415 Doc::text("nfkc")
416 } else {
417 Doc::text("nfkd")
418 }
419}
420
421fn build_keyword_node<'a>(node: &SyntaxNode) -> Doc<'a> {
422 let mut docs: Vec<Doc<'a>> = vec![];
423 for el in node.children_with_tokens() {
424 match el {
425 rowan::NodeOrToken::Token(token) => match token.kind() {
426 SyntaxKind::WHITESPACE => continue,
427 SyntaxKind::COMMENT => {
428 if !docs.is_empty() {
429 docs.push(Doc::space());
430 }
431 docs.push(Doc::text(token.text().to_string()));
432 }
433 _ => {
434 if !docs.is_empty() {
435 docs.push(Doc::space());
436 }
437 docs.push(Doc::text(token.text().to_ascii_lowercase()));
438 }
439 },
440 rowan::NodeOrToken::Node(_) => (),
441 }
442 }
443 Doc::list(docs)
444}
445
446fn build_op<'a>(op: ast::BinOp) -> Doc<'a> {
447 match op {
448 ast::BinOp::And(_) => Doc::text("and"),
449 ast::BinOp::AtTimeZone(n) => build_keyword_node(n.syntax()),
450 ast::BinOp::Caret(_) => Doc::text("^"),
451 ast::BinOp::ColonColon(_) => Doc::text("::"),
452 ast::BinOp::ColonEq(_) => Doc::text(":="),
453 ast::BinOp::CustomOp(custom_op) => Doc::text(custom_op.syntax().to_string()),
454 ast::BinOp::Eq(_) => Doc::text("="),
455 ast::BinOp::Escape(_) => Doc::text("escape"),
456 ast::BinOp::FatArrow(_) => Doc::text("=>"),
457 ast::BinOp::Gteq(_) => Doc::text(">="),
458 ast::BinOp::Ilike(_) => Doc::text("ilike"),
459 ast::BinOp::In(_) => Doc::text("in"),
460 ast::BinOp::Is(_) => Doc::text("is"),
461 ast::BinOp::IsDistinctFrom(n) => build_keyword_node(n.syntax()),
462 ast::BinOp::IsNot(n) => build_keyword_node(n.syntax()),
463 ast::BinOp::IsNotDistinctFrom(n) => build_keyword_node(n.syntax()),
464 ast::BinOp::LAngle(_) => Doc::text("<"),
465 ast::BinOp::Like(_) => Doc::text("like"),
466 ast::BinOp::Lteq(_) => Doc::text("<="),
467 ast::BinOp::Minus(_) => Doc::text("-"),
468 ast::BinOp::Neq(_) => Doc::text("!="),
469 ast::BinOp::Neqb(_) => Doc::text("<>"),
470 ast::BinOp::NotIlike(n) => build_keyword_node(n.syntax()),
471 ast::BinOp::NotIn(n) => build_keyword_node(n.syntax()),
472 ast::BinOp::NotLike(n) => build_keyword_node(n.syntax()),
473 ast::BinOp::NotSimilarTo(n) => build_keyword_node(n.syntax()),
474 ast::BinOp::OperatorCall(op) => Doc::text(op.syntax().to_string()),
475 ast::BinOp::Or(_) => Doc::text("or"),
476 ast::BinOp::Overlaps(_) => Doc::text("overlaps"),
477 ast::BinOp::Percent(_) => Doc::text("%"),
478 ast::BinOp::Plus(_) => Doc::text("+"),
479 ast::BinOp::RAngle(_) => Doc::text(">"),
480 ast::BinOp::SimilarTo(n) => build_keyword_node(n.syntax()),
481 ast::BinOp::Slash(_) => Doc::text("/"),
482 ast::BinOp::Star(_) => Doc::text("*"),
483 }
484}
485
486fn build_literal<'a>(lit: ast::Literal) -> Doc<'a> {
487 let Some(kind) = lit.kind() else {
488 return Doc::nil();
489 };
490 match kind {
491 LitKind::Default(_) => Doc::text("default"),
492 LitKind::False(_) => Doc::text("false"),
493 LitKind::IntNumber(t) => Doc::text(t.text().to_string()),
494 LitKind::Null(_) => Doc::text("null"),
495 LitKind::NumericNumber(t) => Doc::text(t.text().to_string()),
496 LitKind::PositionalParam(t) => Doc::text(t.text().to_string()),
497 LitKind::True(_) => Doc::text("true"),
498 LitKind::BitString(_)
499 | LitKind::ByteString(_)
500 | LitKind::DollarQuotedString(_)
501 | LitKind::EscString(_)
502 | LitKind::NationalString(_)
503 | LitKind::String(_)
504 | LitKind::UnicodeEscString(_) => build_string_literal(&lit),
505 }
506}
507
508fn build_string_literal<'a>(lit: &ast::Literal) -> Doc<'a> {
509 let parts: Vec<Doc<'a>> = lit
510 .syntax()
511 .children_with_tokens()
512 .filter_map(|el| match el {
513 rowan::NodeOrToken::Token(t) if t.kind() != SyntaxKind::WHITESPACE => {
514 Some(Doc::text(format_string_token(&t)))
515 }
516 _ => None,
517 })
518 .collect();
519 Doc::list(Itertools::intersperse(parts.into_iter(), Doc::hard_line()).collect())
520}
521
522fn format_string_token(t: &SyntaxToken) -> String {
523 let text = t.text();
524 if matches!(
525 t.kind(),
526 SyntaxKind::STRING | SyntaxKind::DOLLAR_QUOTED_STRING
527 ) {
528 return text.to_string();
529 }
530 match text.find('\'') {
531 Some(idx) => {
532 let (prefix, rest) = text.split_at(idx);
533 let mut s = String::with_capacity(text.len());
534 s.push_str(&prefix.to_ascii_lowercase());
535 s.push_str(rest);
536 s
537 }
538 None => text.to_string(),
539 }
540}
541
542fn build_column_name<'a>(name: ast::ColumnName) -> Doc<'a> {
543 Doc::text(quote_column_alias(&name.text()))
544}
545
546fn build_type<'a>(ty: ast::Type) -> Doc<'a> {
547 Doc::text(ty.syntax().to_string())
548}
549
550fn leading_comments_token<'a>(node: &SyntaxToken) -> Doc<'a> {
551 let mut doc = Doc::nil();
552 for next in node.siblings_with_tokens(Direction::Prev).skip(1) {
553 match next {
554 rowan::NodeOrToken::Node(_node) => {
555 break;
556 }
557 rowan::NodeOrToken::Token(token) => {
558 if token.kind() == SyntaxKind::COMMENT {
559 doc = doc
560 .append(Doc::text(token.text().to_string()))
561 .append(Doc::space());
562 } else if token.kind() == SyntaxKind::WHITESPACE {
563 continue;
564 } else {
565 break;
566 }
567 }
568 }
569 }
570 doc
571}
572
573fn leading_comments<'a>(node: &SyntaxNode) -> Doc<'a> {
574 let mut doc = Doc::nil();
575 for next in node.siblings_with_tokens(Direction::Prev).skip(1) {
576 match next {
577 rowan::NodeOrToken::Node(_node) => {
578 break;
579 }
580 rowan::NodeOrToken::Token(token) => {
581 if token.kind() == SyntaxKind::COMMENT {
582 let is_block = token.text().starts_with("--");
583 doc = doc
584 .append(Doc::text(token.text().to_string()))
585 .append(if is_block {
586 Doc::hard_line()
587 } else {
588 Doc::space()
589 });
590 } else if token.kind() == SyntaxKind::WHITESPACE {
591 continue;
592 } else {
593 break;
594 }
595 }
596 }
597 }
598 doc
599}
600
601fn trailing_comments<'a>(node: &SyntaxNode) -> Doc<'a> {
602 let mut doc = Doc::nil();
603 for next in node.siblings_with_tokens(Direction::Next).skip(1) {
604 match next {
605 rowan::NodeOrToken::Node(_node) => {
606 break;
607 }
608 rowan::NodeOrToken::Token(token) => {
609 if token.kind() == SyntaxKind::COMMENT {
610 doc = doc
611 .append(Doc::space())
612 .append(Doc::text(token.text().to_string()));
613 } else if token.kind() == SyntaxKind::WHITESPACE {
614 continue;
615 } else {
616 break;
617 }
618 }
619 }
620 }
621 doc
622}
623
624fn build_target<'a>(target: ast::Target) -> Option<Doc<'a>> {
625 let mut doc = leading_comments(target.syntax());
626
627 if target.star_token().is_some() {
628 return Some(doc.append(Doc::text("*")));
629 }
630 let expr = target.expr()?;
631 doc = doc.append(build_expr(expr));
632
633 if let Some(as_name) = target.as_name() {
634 if as_name.as_token().is_some() {
635 doc = doc.append(Doc::space()).append(Doc::text("as"))
636 }
637
638 if let Some(column_name) = as_name.name() {
639 doc = doc
640 .append(Doc::space())
641 .append(build_column_name(column_name));
642 }
643 }
644
645 doc = doc.append(trailing_comments(target.syntax()));
646
647 Some(doc)
648}
649
650pub fn fmt(text: &str) -> Result<String> {
651 let line_ending = find_newline(text)
652 .map(|(_, ending)| ending)
653 .unwrap_or_default();
654
655 let line_break = match line_ending {
656 LineEnding::Cr => bail!("CR line endings aren't supported"),
658 LineEnding::CrLf => LineBreak::Crlf,
659 LineEnding::Lf => LineBreak::Lf,
660 };
661
662 let parse = ast::SourceFile::parse(text);
663 let file = parse.tree();
664 debug_assert_eq!(
665 parse.errors(),
666 vec![],
667 "should bail out when there's parse errors"
668 );
669 let doc = build_source_file(&file);
670
671 Ok(print(
672 &doc,
673 &PrintOptions {
674 line_break,
675 ..Default::default()
676 },
677 ))
678}