Skip to main content

squawk_syntax/ast/
node_ext.rs

1// via https://github.com/rust-lang/rust-analyzer/blob/d8887c0758bbd2d5f752d5bd405d4491e90e7ed6/crates/syntax/src/ast/node_ext.rs
2//
3// Permission is hereby granted, free of charge, to any
4// person obtaining a copy of this software and associated
5// documentation files (the "Software"), to deal in the
6// Software without restriction, including without
7// limitation the rights to use, copy, modify, merge,
8// publish, distribute, sublicense, and/or sell copies of
9// the Software, and to permit persons to whom the Software
10// is furnished to do so, subject to the following
11// conditions:
12//
13// The above copyright notice and this permission notice
14// shall be included in all copies or substantial portions
15// of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25// DEALINGS IN THE SOFTWARE.
26
27use std::borrow::Cow;
28
29#[cfg(test)]
30use insta::assert_snapshot;
31use rowan::{GreenNodeData, GreenTokenData, NodeOrToken};
32
33#[cfg(test)]
34use crate::SourceFile;
35use rowan::Direction;
36
37use crate::ast;
38use crate::ast::AstNode;
39use crate::unescape::{escape_unicode_esc_str, uescape_char};
40use crate::{SyntaxKind, SyntaxNode, SyntaxToken, TokenText};
41
42use super::support;
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum LitKind {
46    BitString(SyntaxToken),
47    ByteString(SyntaxToken),
48    Default(SyntaxToken),
49    DollarQuotedString(SyntaxToken),
50    EscString(SyntaxToken),
51    False(SyntaxToken),
52    IntNumber(SyntaxToken),
53    NationalString(SyntaxToken),
54    Null(SyntaxToken),
55    NumericNumber(SyntaxToken),
56    PositionalParam(SyntaxToken),
57    String(SyntaxToken),
58    True(SyntaxToken),
59    UnicodeEscString(SyntaxToken),
60}
61
62impl ast::Literal {
63    pub fn kind(&self) -> Option<LitKind> {
64        let token = self.syntax().first_child_or_token()?.into_token()?;
65        let kind = match token.kind() {
66            SyntaxKind::BIT_STRING => LitKind::BitString(token),
67            SyntaxKind::BYTE_STRING => LitKind::ByteString(token),
68            SyntaxKind::DEFAULT_KW => LitKind::Default(token),
69            SyntaxKind::DOLLAR_QUOTED_STRING => LitKind::DollarQuotedString(token),
70            SyntaxKind::ESC_STRING => LitKind::EscString(token),
71            SyntaxKind::FALSE_KW => LitKind::False(token),
72            SyntaxKind::INT_NUMBER => LitKind::IntNumber(token),
73            SyntaxKind::NATIONAL_STRING => LitKind::NationalString(token),
74            SyntaxKind::NULL_KW => LitKind::Null(token),
75            SyntaxKind::NUMERIC_NUMBER => LitKind::NumericNumber(token),
76            SyntaxKind::POSITIONAL_PARAM => LitKind::PositionalParam(token),
77            SyntaxKind::STRING => LitKind::String(token),
78            SyntaxKind::TRUE_KW => LitKind::True(token),
79            SyntaxKind::UNICODE_ESC_STRING => LitKind::UnicodeEscString(token),
80            _ => return None,
81        };
82        Some(kind)
83    }
84}
85
86impl ast::Constraint {
87    #[inline]
88    pub fn constraint_name(&self) -> Option<ast::ConstraintName> {
89        support::child::<ast::ConstraintNameClause>(self.syntax())
90            .and_then(|clause| clause.constraint_name())
91    }
92}
93
94impl ast::CreateSchema {
95    pub fn schema_name(&self) -> Option<SyntaxNode> {
96        match self.create_schema_target()? {
97            ast::CreateSchemaTarget::AuthorizationSchema(auth) => {
98                Some(auth.role()?.syntax().clone())
99            }
100            ast::CreateSchemaTarget::NamedSchema(named) => Some(named.schema()?.syntax().clone()),
101        }
102    }
103}
104
105impl ast::FromItem {
106    pub fn alias(&self) -> Option<ast::FromAlias> {
107        match self {
108            ast::FromItem::ExprFromItem(it) => it.alias(),
109            ast::FromItem::FunctionFromItem(it) => it.alias(),
110            ast::FromItem::GraphTableFromItem(it) => it.alias(),
111            ast::FromItem::JsonTableFromItem(it) => it.alias(),
112            ast::FromItem::ParenFromItem(it) => it.alias(),
113            ast::FromItem::RelationFromItem(it) => it.alias(),
114            ast::FromItem::RowsFromItem(it) => it.alias(),
115            ast::FromItem::XmlTableFromItem(it) => it.alias(),
116        }
117    }
118
119    pub fn ordinality_token(&self) -> Option<SyntaxToken> {
120        match self {
121            ast::FromItem::FunctionFromItem(it) => it.ordinality_token(),
122            ast::FromItem::RowsFromItem(it) => it.ordinality_token(),
123            _ => None,
124        }
125    }
126}
127
128impl ast::RelationFromItem {
129    pub fn path_ref(&self) -> Option<ast::PathRef> {
130        self.relation_name_ref()?.path_ref()
131    }
132
133    pub fn name_ref(&self) -> Option<ast::NameRef> {
134        self.path_ref()?.segment()?.name_ref()
135    }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub enum BinOp {
140    And(SyntaxToken),
141    AtTimeZone(ast::AtTimeZone),
142    Caret(SyntaxToken),
143    ColonColon(ast::ColonColon),
144    ColonEq(SyntaxToken),
145    CustomOp(ast::CustomOp),
146    Eq(SyntaxToken),
147    Escape(SyntaxToken),
148    FatArrow(SyntaxToken),
149    Gteq(SyntaxToken),
150    Ilike(SyntaxToken),
151    In(SyntaxToken),
152    Is(SyntaxToken),
153    IsDistinctFrom(ast::IsDistinctFrom),
154    IsNot(ast::IsNot),
155    IsNotDistinctFrom(ast::IsNotDistinctFrom),
156    LAngle(SyntaxToken),
157    Like(SyntaxToken),
158    Lteq(SyntaxToken),
159    Minus(SyntaxToken),
160    Neq(SyntaxToken),
161    Neqb(SyntaxToken),
162    NotIlike(ast::NotIlike),
163    NotIn(ast::NotIn),
164    NotLike(ast::NotLike),
165    NotSimilarTo(ast::NotSimilarTo),
166    OperatorCall(ast::OperatorCall),
167    Or(SyntaxToken),
168    Overlaps(SyntaxToken),
169    Percent(SyntaxToken),
170    Plus(SyntaxToken),
171    RAngle(SyntaxToken),
172    SimilarTo(ast::SimilarTo),
173    Slash(SyntaxToken),
174    Star(SyntaxToken),
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub enum PostfixOp {
179    AtLocal(ast::AtLocal),
180    IsJson(ast::IsJson),
181    IsJsonArray(ast::IsJsonArray),
182    IsJsonObject(ast::IsJsonObject),
183    IsJsonScalar(ast::IsJsonScalar),
184    IsJsonValue(ast::IsJsonValue),
185    IsNormalized(ast::IsNormalized),
186    IsNotJson(ast::IsNotJson),
187    IsNotJsonArray(ast::IsNotJsonArray),
188    IsNotJsonObject(ast::IsNotJsonObject),
189    IsNotJsonScalar(ast::IsNotJsonScalar),
190    IsNotJsonValue(ast::IsNotJsonValue),
191    IsNotNormalized(ast::IsNotNormalized),
192    IsNull(SyntaxToken),
193    NotNull(SyntaxToken),
194}
195
196impl ast::BinExpr {
197    #[inline]
198    pub fn lhs(&self) -> Option<ast::Expr> {
199        support::children(self.syntax()).next()
200    }
201
202    #[inline]
203    pub fn rhs(&self) -> Option<ast::Expr> {
204        support::children(self.syntax()).nth(1)
205    }
206
207    pub fn op(&self) -> Option<BinOp> {
208        let lhs = self.lhs()?;
209        for child in lhs.syntax().siblings_with_tokens(Direction::Next).skip(1) {
210            match child {
211                NodeOrToken::Token(token) => {
212                    let op = match token.kind() {
213                        SyntaxKind::AND_KW => BinOp::And(token),
214                        SyntaxKind::CARET => BinOp::Caret(token),
215                        SyntaxKind::COLON_EQ => BinOp::ColonEq(token),
216                        SyntaxKind::EQ => BinOp::Eq(token),
217                        SyntaxKind::ESCAPE_KW => BinOp::Escape(token),
218                        SyntaxKind::FAT_ARROW => BinOp::FatArrow(token),
219                        SyntaxKind::GTEQ => BinOp::Gteq(token),
220                        SyntaxKind::ILIKE_KW => BinOp::Ilike(token),
221                        SyntaxKind::IN_KW => BinOp::In(token),
222                        SyntaxKind::IS_KW => BinOp::Is(token),
223                        SyntaxKind::L_ANGLE => BinOp::LAngle(token),
224                        SyntaxKind::LIKE_KW => BinOp::Like(token),
225                        SyntaxKind::LTEQ => BinOp::Lteq(token),
226                        SyntaxKind::MINUS => BinOp::Minus(token),
227                        SyntaxKind::NEQ => BinOp::Neq(token),
228                        SyntaxKind::NEQB => BinOp::Neqb(token),
229                        SyntaxKind::OR_KW => BinOp::Or(token),
230                        SyntaxKind::OVERLAPS_KW => BinOp::Overlaps(token),
231                        SyntaxKind::PERCENT => BinOp::Percent(token),
232                        SyntaxKind::PLUS => BinOp::Plus(token),
233                        SyntaxKind::R_ANGLE => BinOp::RAngle(token),
234                        SyntaxKind::SLASH => BinOp::Slash(token),
235                        SyntaxKind::STAR => BinOp::Star(token),
236                        _ => continue,
237                    };
238                    return Some(op);
239                }
240                NodeOrToken::Node(node) => {
241                    let op = match node.kind() {
242                        SyntaxKind::AT_TIME_ZONE => {
243                            BinOp::AtTimeZone(ast::AtTimeZone { syntax: node })
244                        }
245                        SyntaxKind::COLON_COLON => {
246                            BinOp::ColonColon(ast::ColonColon { syntax: node })
247                        }
248                        SyntaxKind::CUSTOM_OP => BinOp::CustomOp(ast::CustomOp { syntax: node }),
249                        SyntaxKind::IS_DISTINCT_FROM => {
250                            BinOp::IsDistinctFrom(ast::IsDistinctFrom { syntax: node })
251                        }
252                        SyntaxKind::IS_NOT => BinOp::IsNot(ast::IsNot { syntax: node }),
253                        SyntaxKind::IS_NOT_DISTINCT_FROM => {
254                            BinOp::IsNotDistinctFrom(ast::IsNotDistinctFrom { syntax: node })
255                        }
256                        SyntaxKind::NOT_ILIKE => BinOp::NotIlike(ast::NotIlike { syntax: node }),
257                        SyntaxKind::NOT_IN => BinOp::NotIn(ast::NotIn { syntax: node }),
258                        SyntaxKind::NOT_LIKE => BinOp::NotLike(ast::NotLike { syntax: node }),
259                        SyntaxKind::NOT_SIMILAR_TO => {
260                            BinOp::NotSimilarTo(ast::NotSimilarTo { syntax: node })
261                        }
262                        SyntaxKind::OPERATOR_CALL => {
263                            BinOp::OperatorCall(ast::OperatorCall { syntax: node })
264                        }
265                        SyntaxKind::SIMILAR_TO => BinOp::SimilarTo(ast::SimilarTo { syntax: node }),
266                        _ => continue,
267                    };
268                    return Some(op);
269                }
270            }
271        }
272        None
273    }
274}
275
276impl ast::PostfixExpr {
277    pub fn op(&self) -> Option<PostfixOp> {
278        let lhs = self.expr()?;
279
280        let siblings = lhs.syntax().siblings_with_tokens(Direction::Next).skip(1);
281        for child in siblings {
282            match child {
283                NodeOrToken::Token(token) => {
284                    let op = match token.kind() {
285                        SyntaxKind::ISNULL_KW => PostfixOp::IsNull(token),
286                        SyntaxKind::NOTNULL_KW => PostfixOp::NotNull(token),
287                        _ => continue,
288                    };
289                    return Some(op);
290                }
291                NodeOrToken::Node(node) => {
292                    let op = match node.kind() {
293                        SyntaxKind::AT_LOCAL => PostfixOp::AtLocal(ast::AtLocal { syntax: node }),
294                        SyntaxKind::IS_JSON => PostfixOp::IsJson(ast::IsJson { syntax: node }),
295                        SyntaxKind::IS_JSON_ARRAY => {
296                            PostfixOp::IsJsonArray(ast::IsJsonArray { syntax: node })
297                        }
298                        SyntaxKind::IS_JSON_OBJECT => {
299                            PostfixOp::IsJsonObject(ast::IsJsonObject { syntax: node })
300                        }
301                        SyntaxKind::IS_JSON_SCALAR => {
302                            PostfixOp::IsJsonScalar(ast::IsJsonScalar { syntax: node })
303                        }
304                        SyntaxKind::IS_JSON_VALUE => {
305                            PostfixOp::IsJsonValue(ast::IsJsonValue { syntax: node })
306                        }
307                        SyntaxKind::IS_NORMALIZED => {
308                            PostfixOp::IsNormalized(ast::IsNormalized { syntax: node })
309                        }
310                        SyntaxKind::IS_NOT_JSON => {
311                            PostfixOp::IsNotJson(ast::IsNotJson { syntax: node })
312                        }
313                        SyntaxKind::IS_NOT_JSON_ARRAY => {
314                            PostfixOp::IsNotJsonArray(ast::IsNotJsonArray { syntax: node })
315                        }
316                        SyntaxKind::IS_NOT_JSON_OBJECT => {
317                            PostfixOp::IsNotJsonObject(ast::IsNotJsonObject { syntax: node })
318                        }
319                        SyntaxKind::IS_NOT_JSON_SCALAR => {
320                            PostfixOp::IsNotJsonScalar(ast::IsNotJsonScalar { syntax: node })
321                        }
322                        SyntaxKind::IS_NOT_JSON_VALUE => {
323                            PostfixOp::IsNotJsonValue(ast::IsNotJsonValue { syntax: node })
324                        }
325                        SyntaxKind::IS_NOT_NORMALIZED => {
326                            PostfixOp::IsNotNormalized(ast::IsNotNormalized { syntax: node })
327                        }
328                        _ => continue,
329                    };
330                    return Some(op);
331                }
332            }
333        }
334
335        None
336    }
337}
338
339impl ast::FieldExpr {
340    // We have NameRef as a variant of Expr which complicates things (and it
341    // might not be worth it).
342    // Rust analyzer doesn't do this so it doesn't have to special case this.
343    #[inline]
344    pub fn base(&self) -> Option<ast::Expr> {
345        support::children(self.syntax()).next()
346    }
347    #[inline]
348    pub fn field(&self) -> Option<ast::NameRef> {
349        support::children(self.syntax()).last()
350    }
351}
352
353impl ast::IndexExpr {
354    #[inline]
355    pub fn base(&self) -> Option<ast::Expr> {
356        support::children(&self.syntax).next()
357    }
358    #[inline]
359    pub fn index(&self) -> Option<ast::Expr> {
360        support::children(&self.syntax).nth(1)
361    }
362}
363
364impl ast::SliceExpr {
365    #[inline]
366    pub fn base(&self) -> Option<ast::Expr> {
367        support::children(&self.syntax).next()
368    }
369
370    #[inline]
371    pub fn start(&self) -> Option<ast::Expr> {
372        // With `select x[1:]`, we have two exprs, `x` and `1`.
373        // We skip over the first one, and then we want the second one, but we
374        // want to make sure we don't choose the end expr if instead we had:
375        // `select x[:1]`
376        let colon = self.colon_token()?;
377        support::children(&self.syntax)
378            .skip(1)
379            .find(|expr: &ast::Expr| expr.syntax().text_range().end() <= colon.text_range().start())
380    }
381
382    #[inline]
383    pub fn end(&self) -> Option<ast::Expr> {
384        // We want to make sure we get the last expr after the `:` which is the
385        // end of the slice, i.e., `2` in: `select x[:2]`
386        let colon = self.colon_token()?;
387        support::children(&self.syntax)
388            .find(|expr: &ast::Expr| expr.syntax().text_range().start() >= colon.text_range().end())
389    }
390}
391
392impl ast::RenameValue {
393    #[inline]
394    pub fn from(&self) -> Option<ast::Literal> {
395        support::children(&self.syntax).nth(0)
396    }
397    #[inline]
398    pub fn to(&self) -> Option<ast::Literal> {
399        support::children(&self.syntax).nth(1)
400    }
401}
402
403impl ast::ForeignKeyConstraint {
404    #[inline]
405    pub fn from_columns(&self) -> Option<ast::ColumnRefList> {
406        support::children(&self.syntax).nth(0)
407    }
408    #[inline]
409    pub fn to_columns(&self) -> Option<ast::ColumnRefList> {
410        support::children(&self.syntax).nth(1)
411    }
412}
413
414impl ast::XmlPiFn {
415    #[inline]
416    pub fn target(&self) -> Option<ast::XmlPiTarget> {
417        support::child(&self.syntax)
418    }
419}
420
421impl ast::BetweenExpr {
422    #[inline]
423    pub fn target(&self) -> Option<ast::Expr> {
424        support::children(&self.syntax).nth(0)
425    }
426    #[inline]
427    pub fn start(&self) -> Option<ast::Expr> {
428        support::children(&self.syntax).nth(1)
429    }
430    #[inline]
431    pub fn end(&self) -> Option<ast::Expr> {
432        support::children(&self.syntax).nth(2)
433    }
434}
435
436impl ast::FrameBetween {
437    #[inline]
438    pub fn start(&self) -> Option<ast::FrameBound> {
439        support::children(&self.syntax).nth(0)
440    }
441    #[inline]
442    pub fn end(&self) -> Option<ast::FrameBound> {
443        support::children(&self.syntax).nth(1)
444    }
445}
446
447impl ast::WhenClause {
448    #[inline]
449    pub fn condition(&self) -> Option<ast::Expr> {
450        support::children(&self.syntax).next()
451    }
452    #[inline]
453    pub fn then(&self) -> Option<ast::Expr> {
454        support::children(&self.syntax).nth(1)
455    }
456}
457
458impl ast::CompoundSelect {
459    #[inline]
460    pub fn lhs(&self) -> Option<ast::SelectVariant> {
461        support::children(&self.syntax).next()
462    }
463    #[inline]
464    pub fn rhs(&self) -> Option<ast::SelectVariant> {
465        support::children(&self.syntax).nth(1)
466    }
467}
468
469impl ast::NameRef {
470    #[inline]
471    pub fn text(&self) -> String {
472        normalize_name_node(self.syntax())
473    }
474
475    #[inline]
476    pub fn is_quoted(&self) -> bool {
477        is_quoted(self.syntax())
478    }
479}
480
481impl ast::ColumnName {
482    #[inline]
483    pub fn text(&self) -> String {
484        normalize_name_node(self.syntax())
485    }
486
487    #[inline]
488    pub fn is_quoted(&self) -> bool {
489        is_quoted(self.syntax())
490    }
491}
492
493impl ast::Name {
494    #[inline]
495    pub fn text(&self) -> String {
496        normalize_name_node(self.syntax())
497    }
498
499    #[inline]
500    pub fn is_quoted(&self) -> bool {
501        is_quoted(self.syntax())
502    }
503}
504
505fn is_quoted(node: &SyntaxNode) -> bool {
506    let text = node.text();
507    let first = text.char_at(0.into());
508    let second = text.char_at(1.into());
509    matches!(
510        (first, second),
511        (Some('u' | 'U'), Some('"')) | (Some('"'), Some(_))
512    )
513}
514
515// TODO: return a NewType wrapper around String?
516fn normalize_name_node(node: &SyntaxNode) -> String {
517    let mut tokens = node
518        .children_with_tokens()
519        .filter_map(|el| el.into_token())
520        .filter(|t| !t.kind().is_trivia());
521
522    let Some(mut ident_token) = tokens.next() else {
523        return String::new();
524    };
525    // Support some deprecated syntax where you can plop a `group` keyword
526    // before a role name.
527    if matches!(node.kind(), SyntaxKind::ROLE | SyntaxKind::ROLE_REF)
528        && ident_token.kind() == SyntaxKind::GROUP_KW
529    {
530        let Some(role_name) = tokens.next() else {
531            return String::new();
532        };
533        ident_token = role_name;
534    }
535    let raw = ident_token.text();
536
537    let unicode_inner = raw
538        .strip_prefix(['u', 'U'])
539        .and_then(|s| s.strip_prefix("&\""))
540        .and_then(|s| s.strip_suffix('"'));
541
542    if let Some(inner) = unicode_inner {
543        let mut escape_char = '\\';
544        if let Some(uesc) = tokens.next()
545            && uesc.kind() == SyntaxKind::UESCAPE_KW
546            && let Some(token) = tokens.next()
547            && let Some(ch) = uescape_char(token.text())
548        {
549            escape_char = ch;
550        }
551
552        let inner = inner.replace(r#""""#, "\"");
553        let mut result = String::with_capacity(inner.len());
554        escape_unicode_esc_str(&inner, escape_char, |_range, r| {
555            if let Ok(ch) = r {
556                result.push(ch);
557            }
558        });
559        return result;
560    }
561
562    raw.strip_prefix('"')
563        .and_then(|t| t.strip_suffix('"'))
564        .map(|x| x.replace(r#""""#, "\""))
565        .unwrap_or_else(|| raw.to_ascii_lowercase())
566}
567
568impl ast::CharType {
569    #[inline]
570    pub fn text(&self) -> TokenText<'_> {
571        text_of_first_token(self.syntax())
572    }
573}
574
575fn string_literal_contents(token: &SyntaxToken) -> Option<&str> {
576    match token.kind() {
577        SyntaxKind::STRING => token.text().strip_prefix('\'')?.strip_suffix('\''),
578        SyntaxKind::ESC_STRING | SyntaxKind::NATIONAL_STRING => {
579            token.text().get(2..)?.strip_suffix('\'')
580        }
581        SyntaxKind::UNICODE_ESC_STRING => token.text().get(3..)?.strip_suffix('\''),
582        SyntaxKind::DOLLAR_QUOTED_STRING => {
583            let text = token.text();
584            let rest = text.strip_prefix('$')?;
585            let tag_len = rest.find('$')?;
586            let delimiter = text.get(..=tag_len + 1)?;
587            text.get(delimiter.len()..)?.strip_suffix(delimiter)
588        }
589        _ => None,
590    }
591}
592
593fn is_falsey_token(token: &SyntaxToken) -> bool {
594    match token.kind() {
595        SyntaxKind::FALSE_KW | SyntaxKind::NO_KW | SyntaxKind::OFF_KW => true,
596        SyntaxKind::INT_NUMBER => token.text() == "0",
597        SyntaxKind::STRING
598        | SyntaxKind::ESC_STRING
599        | SyntaxKind::NATIONAL_STRING
600        | SyntaxKind::UNICODE_ESC_STRING
601        | SyntaxKind::DOLLAR_QUOTED_STRING => string_literal_contents(token)
602            .is_some_and(|text| matches!(text.to_ascii_lowercase().as_str(), "false" | "off")),
603        _ => false,
604    }
605}
606
607fn is_falsey_vacuum_option_value(value: &ast::VacuumOptionValue) -> bool {
608    value
609        .syntax()
610        .first_token()
611        .is_some_and(|token| is_falsey_token(&token))
612}
613
614impl ast::Reindex {
615    pub fn is_concurrently(&self) -> bool {
616        self.concurrently_token().is_some()
617            || self.reindex_option_list().is_some_and(|options| {
618                options.reindex_options().any(|option| {
619                    option.concurrently_token().is_some()
620                        && !option.literal().is_some_and(|literal| {
621                            literal
622                                .syntax()
623                                .first_token()
624                                .is_some_and(|token| is_falsey_token(&token))
625                        })
626                })
627            })
628    }
629}
630
631impl ast::Vacuum {
632    pub fn is_full(&self) -> bool {
633        self.full_token().is_some()
634            // TODO: we need a better way of handling option lists
635            || self.vacuum_option_list().is_some_and(|opt_list| {
636                opt_list.vacuum_options().any(|opt| {
637                    opt.vacuum_option_name().is_some_and(|name| {
638                        name.syntax()
639                            .first_token()
640                            .is_some_and(|token| token.text().eq_ignore_ascii_case("full"))
641                    }) && opt
642                        .vacuum_option_value()
643                        .is_none_or(|value| !is_falsey_vacuum_option_value(&value))
644                })
645            })
646    }
647}
648
649impl ast::OpSig {
650    #[inline]
651    pub fn lhs(&self) -> Option<ast::Type> {
652        support::children(self.syntax()).next()
653    }
654
655    #[inline]
656    pub fn rhs(&self) -> Option<ast::Type> {
657        support::children(self.syntax()).nth(1)
658    }
659}
660
661impl ast::CastSig {
662    #[inline]
663    pub fn lhs(&self) -> Option<ast::Type> {
664        support::children(self.syntax()).next()
665    }
666
667    #[inline]
668    pub fn rhs(&self) -> Option<ast::Type> {
669        support::children(self.syntax()).nth(1)
670    }
671}
672
673impl ast::ColumnConstraint {
674    #[inline]
675    pub fn constraint_name(&self) -> Option<ast::ConstraintName> {
676        match self {
677            ast::ColumnConstraint::CheckConstraint(check_constraint) => check_constraint
678                .constraint_name_clause()
679                .and_then(|clause| clause.constraint_name()),
680            ast::ColumnConstraint::DefaultConstraint(default_constraint) => default_constraint
681                .constraint_name_clause()
682                .and_then(|clause| clause.constraint_name()),
683            ast::ColumnConstraint::ExcludeConstraint(exclude_constraint) => exclude_constraint
684                .constraint_name_clause()
685                .and_then(|clause| clause.constraint_name()),
686            ast::ColumnConstraint::GeneratedConstraint(generated_constraint) => {
687                generated_constraint
688                    .constraint_name_clause()
689                    .and_then(|clause| clause.constraint_name())
690            }
691            ast::ColumnConstraint::NotNullConstraint(not_null_constraint) => not_null_constraint
692                .constraint_name_clause()
693                .and_then(|clause| clause.constraint_name()),
694            ast::ColumnConstraint::NullConstraint(null_constraint) => null_constraint
695                .constraint_name_clause()
696                .and_then(|clause| clause.constraint_name()),
697            ast::ColumnConstraint::PrimaryKeyConstraint(primary_key_constraint) => {
698                primary_key_constraint
699                    .constraint_name_clause()
700                    .and_then(|clause| clause.constraint_name())
701            }
702            ast::ColumnConstraint::ReferencesConstraint(references_constraint) => {
703                references_constraint
704                    .constraint_name_clause()
705                    .and_then(|clause| clause.constraint_name())
706            }
707            ast::ColumnConstraint::UniqueConstraint(unique_constraint) => unique_constraint
708                .constraint_name_clause()
709                .and_then(|clause| clause.constraint_name()),
710        }
711    }
712}
713
714impl ast::TableConstraint {
715    #[inline]
716    pub fn constraint_name(&self) -> Option<ast::ConstraintName> {
717        match self {
718            ast::TableConstraint::CheckConstraint(check_constraint) => check_constraint
719                .constraint_name_clause()
720                .and_then(|clause| clause.constraint_name()),
721            ast::TableConstraint::ExcludeConstraint(exclude_constraint) => exclude_constraint
722                .constraint_name_clause()
723                .and_then(|clause| clause.constraint_name()),
724            ast::TableConstraint::ForeignKeyConstraint(foreign_key_constraint) => {
725                foreign_key_constraint
726                    .constraint_name_clause()
727                    .and_then(|clause| clause.constraint_name())
728            }
729            ast::TableConstraint::PrimaryKeyConstraint(primary_key_constraint) => {
730                primary_key_constraint
731                    .constraint_name_clause()
732                    .and_then(|clause| clause.constraint_name())
733            }
734            ast::TableConstraint::UniqueConstraint(unique_constraint) => unique_constraint
735                .constraint_name_clause()
736                .and_then(|clause| clause.constraint_name()),
737        }
738    }
739}
740
741pub(crate) fn text_of_first_token(node: &SyntaxNode) -> TokenText<'_> {
742    fn first_token(green_ref: &GreenNodeData) -> &GreenTokenData {
743        green_ref
744            .children()
745            .next()
746            .and_then(NodeOrToken::into_token)
747            .unwrap()
748    }
749
750    match node.green() {
751        Cow::Borrowed(green_ref) => TokenText::borrowed(first_token(green_ref).text()),
752        Cow::Owned(green) => TokenText::owned(first_token(&green).to_owned()),
753    }
754}
755
756impl ast::WithQuery {
757    #[inline]
758    pub fn with_clause(&self) -> Option<ast::WithClause> {
759        support::child(self.syntax())
760    }
761}
762
763impl ast::CreateTableAsQuery {
764    #[inline]
765    pub fn select_variant(&self) -> Option<ast::SelectVariant> {
766        match self {
767            ast::CreateTableAsQuery::Execute(_) => None,
768            ast::CreateTableAsQuery::SelectVariant(select_variant) => Some(select_variant.clone()),
769        }
770    }
771}
772
773impl ast::SelectVariant {
774    #[inline]
775    pub fn target_list(&self) -> Option<ast::TargetList> {
776        match self {
777            ast::SelectVariant::Select(select) => {
778                return select.select_clause()?.target_list();
779            }
780            ast::SelectVariant::SelectInto(select_into) => {
781                return select_into.select_clause()?.target_list();
782            }
783            ast::SelectVariant::ParenSelect(paren_select) => {
784                return paren_select.select()?.target_list();
785            }
786            _ => return None,
787        }
788    }
789}
790
791impl ast::HasParamList {
792    #[inline]
793    pub fn param_list(&self) -> Option<ast::ParamList> {
794        support::child(self.syntax())
795    }
796    #[inline]
797    pub fn path(&self) -> Option<ast::Path> {
798        match self {
799            ast::HasParamList::CreateFunction(function) => function.name()?.path(),
800            ast::HasParamList::CreateProcedure(procedure) => procedure.name()?.path(),
801            _ => support::child(self.syntax()),
802        }
803    }
804    #[inline]
805    pub fn path_ref(&self) -> Option<ast::PathRef> {
806        match self {
807            ast::HasParamList::FunctionSig(signature) => signature.function_name_ref()?.path_ref(),
808            ast::HasParamList::ProcedureSig(signature) => {
809                signature.procedure_name_ref()?.path_ref()
810            }
811            ast::HasParamList::RoutineSig(signature) => signature.routine_name_ref()?.path_ref(),
812            _ => support::child(self.syntax()),
813        }
814    }
815}
816
817impl<T> ast::NameLike for T
818where
819    T: AstNode,
820    ast::AnyName: From<T>,
821{
822    #[inline]
823    fn text(&self) -> String {
824        normalize_name_node(self.syntax())
825    }
826
827    #[inline]
828    fn is_quoted(&self) -> bool {
829        is_quoted(self.syntax())
830    }
831}
832
833impl ast::HasWithClause for ast::Select {}
834impl ast::HasWithClause for ast::SelectInto {}
835impl ast::HasWithClause for ast::Insert {}
836impl ast::HasWithClause for ast::Update {}
837impl ast::HasWithClause for ast::Delete {}
838
839impl ast::HasCreateTable for ast::CreateTable {}
840impl ast::HasCreateTable for ast::CreateForeignTable {}
841impl ast::HasCreateTable for ast::CreateTableLike {}
842
843#[test]
844fn name() {
845    assert_snapshot!(extract_name("select 1 foo"), @"foo");
846    assert_snapshot!(extract_name("select 1 FOO"), @"foo");
847    assert_snapshot!(extract_name(r#"select 1 "foo""#), @"foo");
848    assert_snapshot!(extract_name(r#"select 1 "Foo""#), @"Foo");
849    assert_snapshot!(extract_name(r#"select 1 "FOO""#), @"FOO");
850    assert_snapshot!(extract_name(r#"select 1 U&"\0066\006f\006f""#), @"foo");
851    assert_snapshot!(extract_name(r#"select 1 U&"@0066@006f@006f" uescape '@'"#), @"foo");
852
853    fn extract_name(source_code: &str) -> String {
854        let parse = SourceFile::parse(source_code);
855        assert!(parse.errors().is_empty());
856        let stmt = parse.tree().stmts().next().unwrap();
857        let ast::Stmt::Select(select) = stmt else {
858            unreachable!()
859        };
860        let name = select
861            .select_clause()
862            .unwrap()
863            .target_list()
864            .unwrap()
865            .targets()
866            .next()
867            .unwrap()
868            .as_name()
869            .unwrap()
870            .name()
871            .unwrap();
872        name.text().to_string()
873    }
874}
875
876#[test]
877fn name_ref() {
878    assert_snapshot!(extract_name_ref("select foo"), @"foo");
879    assert_snapshot!(extract_name_ref("select FOO"), @"foo");
880    assert_snapshot!(extract_name_ref(r#"select "foo""#), @"foo");
881    assert_snapshot!(extract_name_ref(r#"select "Foo""#), @"Foo");
882    assert_snapshot!(extract_name_ref(r#"select "FOO""#), @"FOO");
883    assert_snapshot!(extract_name_ref(r#"select U&"\0066\006f\006f""#), @"foo");
884    assert_snapshot!(extract_name_ref(r#"select U&"@0066@006f@006f" uescape '@'"#), @"foo");
885
886    fn extract_name_ref(source_code: &str) -> String {
887        let parse = SourceFile::parse(source_code);
888        assert!(parse.errors().is_empty());
889        let stmt = parse.tree().stmts().next().unwrap();
890        let ast::Stmt::Select(select) = stmt else {
891            unreachable!()
892        };
893        let select_clause = select.select_clause().unwrap();
894        let target = select_clause
895            .target_list()
896            .unwrap()
897            .targets()
898            .next()
899            .unwrap();
900        let ast::Expr::NameRef(name_ref) = target.expr().unwrap() else {
901            unreachable!()
902        };
903        name_ref.text().to_string()
904    }
905}
906
907#[test]
908fn unicode_quoted_name_keeps_doubled_single_quotes() {
909    let parse = SourceFile::parse(r#"select 1 U&"a''b""#);
910    assert!(parse.errors().is_empty());
911    let stmt = parse.tree().stmts().next().unwrap();
912    let ast::Stmt::Select(select) = stmt else {
913        unreachable!()
914    };
915    let name = select
916        .select_clause()
917        .unwrap()
918        .target_list()
919        .unwrap()
920        .targets()
921        .next()
922        .unwrap()
923        .as_name()
924        .unwrap()
925        .name()
926        .unwrap();
927
928    assert_snapshot!(name.text().to_string(), @"a''b");
929}
930
931#[test]
932fn index_expr() {
933    let source_code = "
934        select foo[bar];
935    ";
936    let parse = SourceFile::parse(source_code);
937    assert!(parse.errors().is_empty());
938    let stmt = parse.tree().stmts().next().unwrap();
939    let ast::Stmt::Select(select) = stmt else {
940        unreachable!()
941    };
942    let select_clause = select.select_clause().unwrap();
943    let target = select_clause
944        .target_list()
945        .unwrap()
946        .targets()
947        .next()
948        .unwrap();
949    let ast::Expr::IndexExpr(index_expr) = target.expr().unwrap() else {
950        unreachable!()
951    };
952    let base = index_expr.base().unwrap();
953    let index = index_expr.index().unwrap();
954    assert_eq!(base.syntax().text(), "foo");
955    assert_eq!(index.syntax().text(), "bar");
956}
957
958#[test]
959fn slice_expr() {
960    use insta::assert_snapshot;
961    let source_code = "
962        select x[1:2], x[2:], x[:3], x[:];
963    ";
964    let parse = SourceFile::parse(source_code);
965    assert!(parse.errors().is_empty());
966    let stmt = parse.tree().stmts().next().unwrap();
967    let ast::Stmt::Select(select) = stmt else {
968        unreachable!()
969    };
970    let select_clause = select.select_clause().unwrap();
971    let mut targets = select_clause.target_list().unwrap().targets();
972
973    let ast::Expr::SliceExpr(slice) = targets.next().unwrap().expr().unwrap() else {
974        unreachable!()
975    };
976    assert_snapshot!(slice.syntax(), @"x[1:2]");
977    assert_eq!(slice.base().unwrap().syntax().text(), "x");
978    assert_eq!(slice.start().unwrap().syntax().text(), "1");
979    assert_eq!(slice.end().unwrap().syntax().text(), "2");
980
981    let ast::Expr::SliceExpr(slice) = targets.next().unwrap().expr().unwrap() else {
982        unreachable!()
983    };
984    assert_snapshot!(slice.syntax(), @"x[2:]");
985    assert_eq!(slice.base().unwrap().syntax().text(), "x");
986    assert_eq!(slice.start().unwrap().syntax().text(), "2");
987    assert!(slice.end().is_none());
988
989    let ast::Expr::SliceExpr(slice) = targets.next().unwrap().expr().unwrap() else {
990        unreachable!()
991    };
992    assert_snapshot!(slice.syntax(), @"x[:3]");
993    assert_eq!(slice.base().unwrap().syntax().text(), "x");
994    assert!(slice.start().is_none());
995    assert_eq!(slice.end().unwrap().syntax().text(), "3");
996
997    let ast::Expr::SliceExpr(slice) = targets.next().unwrap().expr().unwrap() else {
998        unreachable!()
999    };
1000    assert_snapshot!(slice.syntax(), @"x[:]");
1001    assert_eq!(slice.base().unwrap().syntax().text(), "x");
1002    assert!(slice.start().is_none());
1003    assert!(slice.end().is_none());
1004}
1005
1006#[test]
1007fn field_expr() {
1008    let source_code = "
1009        select foo.bar;
1010    ";
1011    let parse = SourceFile::parse(source_code);
1012    assert!(parse.errors().is_empty());
1013    let stmt = parse.tree().stmts().next().unwrap();
1014    let ast::Stmt::Select(select) = stmt else {
1015        unreachable!()
1016    };
1017    let select_clause = select.select_clause().unwrap();
1018    let target = select_clause
1019        .target_list()
1020        .unwrap()
1021        .targets()
1022        .next()
1023        .unwrap();
1024    let ast::Expr::FieldExpr(field_expr) = target.expr().unwrap() else {
1025        unreachable!()
1026    };
1027    let base = field_expr.base().unwrap();
1028    let field = field_expr.field().unwrap();
1029    assert_eq!(base.syntax().text(), "foo");
1030    assert_eq!(field.syntax().text(), "bar");
1031}
1032
1033#[test]
1034fn between_expr() {
1035    let source_code = "
1036        select 2 between 1 and 3;
1037    ";
1038    let parse = SourceFile::parse(source_code);
1039    assert!(parse.errors().is_empty());
1040    let stmt = parse.tree().stmts().next().unwrap();
1041    let ast::Stmt::Select(select) = stmt else {
1042        unreachable!()
1043    };
1044    let select_clause = select.select_clause().unwrap();
1045    let target = select_clause
1046        .target_list()
1047        .unwrap()
1048        .targets()
1049        .next()
1050        .unwrap();
1051    let ast::Expr::BetweenExpr(between_expr) = target.expr().unwrap() else {
1052        unreachable!()
1053    };
1054    let target = between_expr.target().unwrap();
1055    let start = between_expr.start().unwrap();
1056    let end = between_expr.end().unwrap();
1057    assert_eq!(target.syntax().text(), "2");
1058    assert_eq!(start.syntax().text(), "1");
1059    assert_eq!(end.syntax().text(), "3");
1060}
1061
1062#[test]
1063fn cast_expr() {
1064    use insta::assert_snapshot;
1065
1066    let cast = extract_expr("select cast('123' as int)");
1067    assert!(cast.expr().is_some());
1068    assert_snapshot!(cast.expr().unwrap().syntax(), @"'123'");
1069    assert!(cast.ty().is_some());
1070    assert_snapshot!(cast.ty().unwrap().syntax(), @"int");
1071
1072    let cast = extract_expr("select cast('123' as pg_catalog.int4)");
1073    assert!(cast.expr().is_some());
1074    assert_snapshot!(cast.expr().unwrap().syntax(), @"'123'");
1075    assert!(cast.ty().is_some());
1076    assert_snapshot!(cast.ty().unwrap().syntax(), @"pg_catalog.int4");
1077
1078    let cast = extract_expr("select int '123'");
1079    assert!(cast.expr().is_some());
1080    assert_snapshot!(cast.expr().unwrap().syntax(), @"'123'");
1081    assert!(cast.ty().is_some());
1082    assert_snapshot!(cast.ty().unwrap().syntax(), @"int");
1083
1084    let cast = extract_expr("select pg_catalog.int4 '123'");
1085    assert!(cast.expr().is_some());
1086    assert_snapshot!(cast.expr().unwrap().syntax(), @"'123'");
1087    assert!(cast.ty().is_some());
1088    assert_snapshot!(cast.ty().unwrap().syntax(), @"pg_catalog.int4");
1089
1090    let cast = extract_expr("select '123'::int");
1091    assert!(cast.expr().is_some());
1092    assert_snapshot!(cast.expr().unwrap().syntax(), @"'123'");
1093    assert!(cast.ty().is_some());
1094    assert_snapshot!(cast.ty().unwrap().syntax(), @"int");
1095
1096    let cast = extract_expr("select '123'::int4");
1097    assert!(cast.expr().is_some());
1098    assert_snapshot!(cast.expr().unwrap().syntax(), @"'123'");
1099    assert!(cast.ty().is_some());
1100    assert_snapshot!(cast.ty().unwrap().syntax(), @"int4");
1101
1102    let cast = extract_expr("select '123'::pg_catalog.int4");
1103    assert!(cast.expr().is_some());
1104    assert_snapshot!(cast.expr().unwrap().syntax(), @"'123'");
1105    assert!(cast.ty().is_some());
1106    assert_snapshot!(cast.ty().unwrap().syntax(), @"pg_catalog.int4");
1107
1108    let cast = extract_expr("select '{123}'::pg_catalog.varchar(10)[]");
1109    assert!(cast.expr().is_some());
1110    assert_snapshot!(cast.expr().unwrap().syntax(), @"'{123}'");
1111    assert!(cast.ty().is_some());
1112    assert_snapshot!(cast.ty().unwrap().syntax(), @"pg_catalog.varchar(10)[]");
1113
1114    let cast = extract_expr("select cast('{123}' as pg_catalog.varchar(10)[])");
1115    assert!(cast.expr().is_some());
1116    assert_snapshot!(cast.expr().unwrap().syntax(), @"'{123}'");
1117    assert!(cast.ty().is_some());
1118    assert_snapshot!(cast.ty().unwrap().syntax(), @"pg_catalog.varchar(10)[]");
1119
1120    let cast = extract_expr("select pg_catalog.varchar(10) '{123}'");
1121    assert!(cast.expr().is_some());
1122    assert_snapshot!(cast.expr().unwrap().syntax(), @"'{123}'");
1123    assert!(cast.ty().is_some());
1124    assert_snapshot!(cast.ty().unwrap().syntax(), @"pg_catalog.varchar(10)");
1125
1126    let cast = extract_expr("select interval '1' month");
1127    assert!(cast.expr().is_some());
1128    assert_snapshot!(cast.expr().unwrap().syntax(), @"'1'");
1129    assert!(cast.ty().is_some());
1130    assert_snapshot!(cast.ty().unwrap().syntax(), @"interval");
1131
1132    fn extract_expr(sql: &str) -> ast::CastExpr {
1133        let parse = SourceFile::parse(sql);
1134        assert!(parse.errors().is_empty());
1135        let node = parse
1136            .tree()
1137            .stmts()
1138            .map(|x| match x {
1139                ast::Stmt::Select(select) => select
1140                    .select_clause()
1141                    .unwrap()
1142                    .target_list()
1143                    .unwrap()
1144                    .targets()
1145                    .next()
1146                    .unwrap()
1147                    .expr()
1148                    .unwrap()
1149                    .clone(),
1150                _ => unreachable!(),
1151            })
1152            .next()
1153            .unwrap();
1154        match node {
1155            ast::Expr::CastExpr(cast) => cast,
1156            _ => unreachable!(),
1157        }
1158    }
1159}
1160
1161#[test]
1162fn op_sig() {
1163    let source_code = "
1164      alter operator p.+ (int4, int8) 
1165        owner to u;
1166    ";
1167    let parse = SourceFile::parse(source_code);
1168    assert!(parse.errors().is_empty());
1169    let stmt = parse.tree().stmts().next().unwrap();
1170    let ast::Stmt::AlterOperator(alter_op) = stmt else {
1171        unreachable!()
1172    };
1173    let op_sig = alter_op.op_sig().unwrap();
1174    let lhs = op_sig.lhs().unwrap();
1175    let rhs = op_sig.rhs().unwrap();
1176    assert_snapshot!(lhs.syntax().text(), @"int4");
1177    assert_snapshot!(rhs.syntax().text(), @"int8");
1178}
1179
1180#[test]
1181fn cast_sig() {
1182    let source_code = "
1183      drop cast (text as int);
1184    ";
1185    let parse = SourceFile::parse(source_code);
1186    assert!(parse.errors().is_empty());
1187    let stmt = parse.tree().stmts().next().unwrap();
1188    let ast::Stmt::DropCast(alter_op) = stmt else {
1189        unreachable!()
1190    };
1191    let cast_sig = alter_op.cast_sig().unwrap();
1192    let lhs = cast_sig.lhs().unwrap();
1193    let rhs = cast_sig.rhs().unwrap();
1194    assert_snapshot!(lhs.syntax().text(), @"text");
1195    assert_snapshot!(rhs.syntax().text(), @"int");
1196}
1197
1198#[cfg(test)]
1199fn extract_vacuum(sql: &str) -> ast::Vacuum {
1200    let parse = SourceFile::parse(sql);
1201    assert!(parse.errors().is_empty());
1202    let stmt = parse.tree().stmts().next().unwrap();
1203    let ast::Stmt::Vacuum(vacuum) = stmt else {
1204        unreachable!()
1205    };
1206    vacuum
1207}
1208
1209#[test]
1210fn vacuum_full_is_full() {
1211    assert!(extract_vacuum("VACUUM FULL foo;").is_full());
1212}
1213
1214#[test]
1215fn vacuum_option_list_full_is_full() {
1216    assert!(extract_vacuum("VACUUM (FULL) foo;").is_full());
1217}
1218
1219#[test]
1220fn vacuum_full_true_is_full() {
1221    assert!(extract_vacuum("VACUUM (FULL TRUE) foo;").is_full());
1222}
1223
1224#[test]
1225fn vacuum_full_on_is_full() {
1226    assert!(extract_vacuum("VACUUM (FULL ON) foo;").is_full());
1227}
1228
1229#[test]
1230fn vacuum_full_1_is_full() {
1231    assert!(extract_vacuum("VACUUM (FULL 1) foo;").is_full());
1232}
1233
1234#[test]
1235fn vacuum_no_full_is_not_full() {
1236    assert!(!extract_vacuum("VACUUM foo;").is_full());
1237}
1238
1239#[test]
1240fn vacuum_other_option_is_not_full() {
1241    assert!(!extract_vacuum("VACUUM (FREEZE) foo;").is_full());
1242}
1243
1244#[test]
1245fn vacuum_full_false_is_not_full() {
1246    assert!(!extract_vacuum("VACUUM (FULL FALSE) foo;").is_full());
1247}
1248
1249#[test]
1250fn vacuum_full_off_is_not_full() {
1251    assert!(!extract_vacuum("VACUUM (FULL OFF) foo;").is_full());
1252}
1253
1254#[test]
1255fn vacuum_full_no_is_not_full() {
1256    assert!(!extract_vacuum("VACUUM (FULL NO) foo;").is_full());
1257}
1258
1259#[test]
1260fn vacuum_full_quoted_off_is_not_full() {
1261    assert!(!extract_vacuum("VACUUM (FULL 'off') foo;").is_full());
1262}
1263
1264#[test]
1265fn vacuum_full_escaped_string_off_is_not_full() {
1266    assert!(!extract_vacuum("VACUUM (FULL E'off') foo;").is_full());
1267}
1268
1269#[test]
1270fn vacuum_full_unicode_escaped_string_off_is_not_full() {
1271    assert!(!extract_vacuum("VACUUM (FULL U&'off') foo;").is_full());
1272}
1273
1274#[test]
1275fn vacuum_full_dollar_quoted_off_is_not_full() {
1276    assert!(!extract_vacuum("VACUUM (FULL $$off$$) t;").is_full());
1277}
1278
1279#[test]
1280fn vacuum_full_0_is_not_full() {
1281    assert!(!extract_vacuum("VACUUM (FULL 0) foo;").is_full());
1282}