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