Skip to main content

qusql_parse/
create_function.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5// http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12use crate::{
13    DataType, Expression, Identifier, SString, Span, Spanned, Statement,
14    create_option::CreateOption,
15    data_type::{DataTypeContext, parse_data_type},
16    expression::{PRIORITY_MAX, parse_expression_unreserved},
17    keywords::Keyword,
18    lexer::Token,
19    parser::{ParseError, Parser},
20    statement::parse_statement,
21};
22use alloc::vec::Vec;
23
24/// Language of a function
25#[derive(Clone, Debug)]
26pub enum FunctionLanguage<'a> {
27    Sql(Span),
28    Plpgsql(Span),
29    Other(Identifier<'a>),
30}
31
32impl<'a> Spanned for FunctionLanguage<'a> {
33    fn span(&self) -> Span {
34        match &self {
35            FunctionLanguage::Sql(v) => v.span(),
36            FunctionLanguage::Plpgsql(v) => v.span(),
37            FunctionLanguage::Other(v) => v.span(),
38        }
39    }
40}
41
42/// Parallel safety level of a function
43#[derive(Clone, Debug)]
44pub enum FunctionParallel {
45    Safe(Span),
46    Unsafe(Span),
47    Restricted(Span),
48}
49
50impl Spanned for FunctionParallel {
51    fn span(&self) -> Span {
52        match self {
53            FunctionParallel::Safe(s) => s.clone(),
54            FunctionParallel::Unsafe(s) => s.clone(),
55            FunctionParallel::Restricted(s) => s.clone(),
56        }
57    }
58}
59
60/// Characteristic of a function
61#[derive(Clone, Debug)]
62pub enum FunctionCharacteristic<'a> {
63    Language(Span, FunctionLanguage<'a>),
64    Immutable(Span),
65    Stable(Span),
66    Volatile(Span),
67    Strict(Span),
68    CalledOnNullInput(Span),
69    ReturnsNullOnNullInput(Span),
70    Parallel(Span, FunctionParallel),
71    NotDeterministic(Span),
72    Deterministic(Span),
73    ContainsSql(Span),
74    NoSql(Span),
75    ReadsSqlData(Span),
76    ModifiesSqlData(Span),
77    SqlSecurityDefiner(Span),
78    SqlSecurityUser(Span),
79    Comment(SString<'a>),
80}
81
82impl<'a> Spanned for FunctionCharacteristic<'a> {
83    fn span(&self) -> Span {
84        match &self {
85            FunctionCharacteristic::Language(s, v) => s.join_span(v),
86            FunctionCharacteristic::NotDeterministic(v) => v.span(),
87            FunctionCharacteristic::Deterministic(v) => v.span(),
88            FunctionCharacteristic::ContainsSql(v) => v.span(),
89            FunctionCharacteristic::NoSql(v) => v.span(),
90            FunctionCharacteristic::ReadsSqlData(v) => v.span(),
91            FunctionCharacteristic::ModifiesSqlData(v) => v.span(),
92            FunctionCharacteristic::SqlSecurityDefiner(v) => v.span(),
93            FunctionCharacteristic::SqlSecurityUser(v) => v.span(),
94            FunctionCharacteristic::Comment(v) => v.span(),
95            FunctionCharacteristic::Immutable(v) => v.span(),
96            FunctionCharacteristic::Stable(v) => v.span(),
97            FunctionCharacteristic::Volatile(v) => v.span(),
98            FunctionCharacteristic::Strict(v) => v.span(),
99            FunctionCharacteristic::CalledOnNullInput(v) => v.span(),
100            FunctionCharacteristic::ReturnsNullOnNullInput(v) => v.span(),
101            FunctionCharacteristic::Parallel(s, v) => s.join_span(v),
102        }
103    }
104}
105
106/// Direction of a function argument
107#[derive(Clone, Debug)]
108pub enum FunctionParamDirection {
109    In(Span),
110    Out(Span),
111    InOut(Span),
112}
113
114impl Spanned for FunctionParamDirection {
115    fn span(&self) -> Span {
116        match &self {
117            FunctionParamDirection::In(v) => v.span(),
118            FunctionParamDirection::Out(v) => v.span(),
119            FunctionParamDirection::InOut(v) => v.span(),
120        }
121    }
122}
123
124/// A single function parameter
125#[derive(Clone, Debug)]
126pub struct FunctionParam<'a> {
127    /// Optional direction modifier (IN, OUT, INOUT)
128    pub direction: Option<FunctionParamDirection>,
129    /// Optional parameter name
130    pub name: Option<Identifier<'a>>,
131    /// Parameter type
132    pub type_: DataType<'a>,
133    /// Optional default value: (= or DEFAULT span, expression)
134    pub default: Option<(Span, Expression<'a>)>,
135}
136
137impl<'a> Spanned for FunctionParam<'a> {
138    fn span(&self) -> Span {
139        self.type_
140            .join_span(&self.direction)
141            .join_span(&self.name)
142            .join_span(&self.default.as_ref().map(|(s, e)| s.join_span(e)))
143    }
144}
145
146/// Body of a CREATE FUNCTION AS clause
147#[derive(Clone, Debug)]
148pub struct FunctionBody<'a> {
149    /// Span of the AS keyword
150    pub as_span: Span,
151    /// The body string(s) — typically one dollar-quoted string, or two strings for C functions
152    pub strings: Vec<SString<'a>>,
153}
154
155impl<'a> Spanned for FunctionBody<'a> {
156    fn span(&self) -> Span {
157        self.as_span.join_span(&self.strings)
158    }
159}
160
161/// Representation of Create Function Statement
162///
163/// This is not fully implemented yet
164///
165/// ```ignore
166/// # use qusql_parse::{SQLDialect, SQLArguments, ParseOptions, parse_statements, CreateFunction, Statement, Issues};
167/// # let options = ParseOptions::new().dialect(SQLDialect::MariaDB);
168/// #
169/// let sql = "DELIMITER $$
170/// CREATE FUNCTION add_func3(IN a INT, IN b INT, OUT c INT) RETURNS INT
171/// BEGIN
172///     SET c = 100;
173///     RETURN a + b;
174/// END;
175/// $$
176/// DELIMITER ;";
177/// let mut issues = Issues::new(sql);
178/// let mut stmts = parse_statements(sql, &mut issues, &options);
179///
180/// assert!(issues.is_empty());
181/// #
182/// let create: CreateFunction = match stmts.pop() {
183///     Some(Statement::CreateFunction(c)) => c,
184///     _ => panic!("We should get an create function statement")
185/// };
186///
187/// assert!(create.name.as_str() == "add_func3");
188/// println!("{:#?}", create.return_)
189/// ```
190#[derive(Clone, Debug)]
191pub struct CreateFunction<'a> {
192    /// Span of "CREATE"
193    pub create_span: Span,
194    /// Options after "CREATE"
195    pub create_options: Vec<CreateOption<'a>>,
196    /// Span of "FUNCTION"
197    pub function_span: Span,
198    /// Span of "IF NOT EXISTS" if specified
199    pub if_not_exists: Option<Span>,
200    /// Name o created function
201    pub name: Identifier<'a>,
202    /// Names and types of function arguments
203    pub params: Vec<FunctionParam<'a>>,
204    /// Span of "RETURNS"
205    pub returns_span: Span,
206    /// Type of return value
207    pub return_type: DataType<'a>,
208    /// Characteristics of created function
209    pub characteristics: Vec<FunctionCharacteristic<'a>>,
210    /// Optional AS body (PostgreSQL)
211    pub body: Option<FunctionBody<'a>>,
212    /// Statement computing return value
213    pub return_: Option<Statement<'a>>,
214}
215
216impl<'a> Spanned for CreateFunction<'a> {
217    fn span(&self) -> Span {
218        self.create_span
219            .join_span(&self.create_options)
220            .join_span(&self.function_span)
221            .join_span(&self.if_not_exists)
222            .join_span(&self.name)
223            .join_span(&self.params)
224            .join_span(&self.returns_span)
225            .join_span(&self.return_type)
226            .join_span(&self.characteristics)
227            .join_span(&self.body)
228            .join_span(&self.return_)
229    }
230}
231
232pub(crate) fn parse_create_function<'a>(
233    parser: &mut Parser<'a, '_>,
234    create_span: Span,
235    create_options: Vec<CreateOption<'a>>,
236) -> Result<CreateFunction<'a>, ParseError> {
237    let function_span = parser.consume_keyword(Keyword::FUNCTION)?;
238
239    let if_not_exists = if let Some(if_) = parser.skip_keyword(Keyword::IF) {
240        Some(
241            parser
242                .consume_keywords(&[Keyword::NOT, Keyword::EXISTS])?
243                .join_span(&if_),
244        )
245    } else {
246        None
247    };
248
249    let name = parser.consume_plain_identifier_unreserved()?;
250    let mut params = Vec::new();
251    parser.consume_token(Token::LParen)?;
252    parser.recovered("')'", &|t| t == &Token::RParen, |parser| {
253        loop {
254            let direction = match &parser.token {
255                Token::Ident(_, Keyword::IN) => {
256                    let in_ = parser.consume_keyword(Keyword::IN)?;
257                    if let Some(out) = parser.skip_keyword(Keyword::OUT) {
258                        Some(FunctionParamDirection::InOut(in_.join_span(&out)))
259                    } else {
260                        Some(FunctionParamDirection::In(in_))
261                    }
262                }
263                Token::Ident(_, Keyword::OUT) => Some(FunctionParamDirection::Out(
264                    parser.consume_keyword(Keyword::OUT)?,
265                )),
266                Token::Ident(_, Keyword::INOUT) => Some(FunctionParamDirection::InOut(
267                    parser.consume_keyword(Keyword::INOUT)?,
268                )),
269                _ => None,
270            };
271
272            let name = if parser.options.dialect.is_postgresql() {
273                // In PostgreSQL, params can be unnamed (type only).
274                // Peek at the next token to decide: if it's a boundary/separator,
275                // the current token is the type start (unnamed param).
276                let is_unnamed = matches!(
277                    parser.peek(),
278                    Token::Comma
279                        | Token::RParen
280                        | Token::LParen
281                        | Token::Eq
282                        | Token::Ident(_, Keyword::DEFAULT)
283                        | Token::LBracket
284                );
285                if is_unnamed {
286                    None
287                } else {
288                    Some(parser.consume_plain_identifier_unreserved()?)
289                }
290            } else {
291                Some(parser.consume_plain_identifier_unreserved()?)
292            };
293            let type_ = parse_data_type(parser, DataTypeContext::FunctionParam)?;
294            // Optional default value: '= expr' or 'DEFAULT expr'
295            let default = if let Some(eq_span) = parser.skip_token(Token::Eq) {
296                Some((eq_span, parse_expression_unreserved(parser, PRIORITY_MAX)?))
297            } else if let Some(default_span) = parser.skip_keyword(Keyword::DEFAULT) {
298                Some((
299                    default_span,
300                    parse_expression_unreserved(parser, PRIORITY_MAX)?,
301                ))
302            } else {
303                None
304            };
305            params.push(FunctionParam {
306                direction,
307                name,
308                type_,
309                default,
310            });
311            if parser.skip_token(Token::Comma).is_none() {
312                break;
313            }
314        }
315        Ok(())
316    })?;
317    parser.consume_token(Token::RParen)?;
318    let returns_span = parser.consume_keyword(Keyword::RETURNS)?;
319    let return_type = parse_data_type(parser, DataTypeContext::FunctionReturn)?;
320    let mut body: Option<FunctionBody<'_>> = None;
321    let mut characteristics = Vec::new();
322    loop {
323        let f = match &parser.token {
324            Token::Ident(_, Keyword::LANGUAGE) => {
325                let lg = parser.consume();
326                match &parser.token {
327                    Token::Ident(_, Keyword::SQL) => FunctionCharacteristic::Language(
328                        lg,
329                        FunctionLanguage::Sql(parser.consume()),
330                    ),
331                    Token::Ident(_, Keyword::PLPGSQL) => FunctionCharacteristic::Language(
332                        lg,
333                        FunctionLanguage::Plpgsql(parser.consume()),
334                    ),
335                    Token::Ident(_, _) if parser.options.dialect.is_postgresql() => {
336                        FunctionCharacteristic::Language(
337                            lg,
338                            FunctionLanguage::Other(parser.consume_plain_identifier_unreserved()?),
339                        )
340                    }
341                    _ => parser.expected_failure("language name")?,
342                }
343            }
344            Token::Ident(_, Keyword::NOT) => FunctionCharacteristic::NotDeterministic(
345                parser.consume_keywords(&[Keyword::NOT, Keyword::DETERMINISTIC])?,
346            ),
347            Token::Ident(_, Keyword::DETERMINISTIC) => FunctionCharacteristic::Deterministic(
348                parser.consume_keyword(Keyword::DETERMINISTIC)?,
349            ),
350            Token::Ident(_, Keyword::CONTAINS) => FunctionCharacteristic::ContainsSql(
351                parser.consume_keywords(&[Keyword::CONTAINS, Keyword::SQL])?,
352            ),
353            Token::Ident(_, Keyword::NO) => FunctionCharacteristic::NoSql(
354                parser.consume_keywords(&[Keyword::NO, Keyword::SQL])?,
355            ),
356            Token::Ident(_, Keyword::READS) => {
357                FunctionCharacteristic::ReadsSqlData(parser.consume_keywords(&[
358                    Keyword::READS,
359                    Keyword::SQL,
360                    Keyword::DATA,
361                ])?)
362            }
363            Token::Ident(_, Keyword::MODIFIES) => {
364                FunctionCharacteristic::ModifiesSqlData(parser.consume_keywords(&[
365                    Keyword::MODIFIES,
366                    Keyword::SQL,
367                    Keyword::DATA,
368                ])?)
369            }
370            Token::Ident(_, Keyword::COMMENT) => {
371                parser.consume_keyword(Keyword::COMMENT)?;
372                FunctionCharacteristic::Comment(parser.consume_string()?)
373            }
374            Token::Ident(_, Keyword::SQL) => {
375                let span = parser.consume_keywords(&[Keyword::SQL, Keyword::SECURITY])?;
376                match &parser.token {
377                    Token::Ident(_, Keyword::DEFINER) => {
378                        FunctionCharacteristic::SqlSecurityDefiner(
379                            parser.consume_keyword(Keyword::DEFINER)?.join_span(&span),
380                        )
381                    }
382                    Token::Ident(_, Keyword::USER) => FunctionCharacteristic::SqlSecurityUser(
383                        parser.consume_keyword(Keyword::USER)?.join_span(&span),
384                    ),
385                    _ => parser.expected_failure("'DEFINER' or 'USER'")?,
386                }
387            }
388            Token::Ident(_, Keyword::IMMUTABLE) => {
389                FunctionCharacteristic::Immutable(parser.consume_keyword(Keyword::IMMUTABLE)?)
390            }
391            Token::Ident(_, Keyword::STABLE) => {
392                FunctionCharacteristic::Stable(parser.consume_keyword(Keyword::STABLE)?)
393            }
394            Token::Ident(_, Keyword::VOLATILE) => {
395                FunctionCharacteristic::Volatile(parser.consume_keyword(Keyword::VOLATILE)?)
396            }
397            Token::Ident(_, Keyword::STRICT) => {
398                FunctionCharacteristic::Strict(parser.consume_keyword(Keyword::STRICT)?)
399            }
400            Token::Ident(_, Keyword::CALLED) if parser.options.dialect.is_postgresql() => {
401                FunctionCharacteristic::CalledOnNullInput(parser.consume_keywords(&[
402                    Keyword::CALLED,
403                    Keyword::ON,
404                    Keyword::NULL,
405                    Keyword::INPUT,
406                ])?)
407            }
408            Token::Ident(_, Keyword::RETURNS) if parser.options.dialect.is_postgresql() => {
409                FunctionCharacteristic::ReturnsNullOnNullInput(parser.consume_keywords(&[
410                    Keyword::RETURNS,
411                    Keyword::NULL,
412                    Keyword::ON,
413                    Keyword::NULL,
414                    Keyword::INPUT,
415                ])?)
416            }
417            Token::Ident(_, Keyword::PARALLEL) => {
418                let parallel_span = parser.consume_keyword(Keyword::PARALLEL)?;
419                let level = match parser.consume_plain_identifier_unreserved()?.value {
420                    v if v.eq_ignore_ascii_case("safe") => {
421                        FunctionParallel::Safe(parallel_span.clone())
422                    }
423                    v if v.eq_ignore_ascii_case("unsafe") => {
424                        FunctionParallel::Unsafe(parallel_span.clone())
425                    }
426                    v if v.eq_ignore_ascii_case("restricted") => {
427                        FunctionParallel::Restricted(parallel_span.clone())
428                    }
429                    _ => {
430                        parser.expected_error("SAFE, UNSAFE, or RESTRICTED");
431                        FunctionParallel::Unsafe(parallel_span.clone())
432                    }
433                };
434                FunctionCharacteristic::Parallel(parallel_span, level)
435            }
436            Token::Ident(_, Keyword::AS) if parser.options.dialect.is_postgresql() => {
437                let as_span = parser.consume_keyword(Keyword::AS)?;
438                let mut strings = Vec::new();
439                match &parser.token {
440                    Token::String(_, _) => {
441                        strings.push(parser.consume_string()?);
442                        // Handle comma-separated strings (e.g. C functions: 'MODULE_PATHNAME', 'func_name')
443                        while parser.skip_token(Token::Comma).is_some() {
444                            if matches!(&parser.token, Token::String(_, _)) {
445                                strings.push(parser.consume_string()?);
446                            } else {
447                                break;
448                            }
449                        }
450                    }
451                    _ => {
452                        parser.expected_error("'$$' or string");
453                    }
454                }
455                body = Some(FunctionBody { as_span, strings });
456                break;
457            }
458            _ => break,
459        };
460        characteristics.push(f);
461    }
462
463    if parser.options.dialect.is_postgresql()
464        && !characteristics
465            .iter()
466            .any(|c| matches!(c, FunctionCharacteristic::Language(_, _)))
467    {
468        parser.expected_failure("LANGUAGE")?;
469    }
470
471    let return_ = if parser.options.dialect.is_maria() {
472        let old = core::mem::replace(&mut parser.permit_compound_statements, true);
473        let r = match parse_statement(parser)? {
474            Some(v) => Some(v),
475            None => parser.expected_failure("statement")?,
476        };
477        parser.permit_compound_statements = old;
478        r
479    } else if matches!(&parser.token, Token::Ident(_, Keyword::RETURN)) {
480        // PostgreSQL SQL/PSM inline function body: `RETURN <expr>`
481        match parse_statement(parser)? {
482            Some(v) => Some(v),
483            None => parser.expected_failure("statement after RETURN")?,
484        }
485    } else {
486        None
487    };
488
489    Ok(CreateFunction {
490        create_span,
491        create_options,
492        function_span,
493        if_not_exists,
494        name,
495        params,
496        return_type,
497        characteristics,
498        body,
499        return_,
500        returns_span,
501    })
502}
503
504/// Representation of a CREATE PROCEDURE statement
505///
506/// Like functions but without a RETURNS clause.
507#[derive(Clone, Debug)]
508pub struct CreateProcedure<'a> {
509    /// Span of "CREATE"
510    pub create_span: Span,
511    /// Options after "CREATE" (e.g. DEFINER=)
512    pub create_options: Vec<CreateOption<'a>>,
513    /// Span of "PROCEDURE"
514    pub procedure_span: Span,
515    /// Span of "IF NOT EXISTS" if specified
516    pub if_not_exists: Option<Span>,
517    /// Name of created procedure
518    pub name: Identifier<'a>,
519    /// Names and types of procedure parameters
520    pub params: Vec<FunctionParam<'a>>,
521    /// Characteristics (DETERMINISTIC, NO SQL, etc.)
522    pub characteristics: Vec<FunctionCharacteristic<'a>>,
523    /// Body statement (typically a BEGIN...END block)
524    pub body: Option<Statement<'a>>,
525}
526
527impl<'a> Spanned for CreateProcedure<'a> {
528    fn span(&self) -> Span {
529        self.create_span
530            .join_span(&self.create_options)
531            .join_span(&self.procedure_span)
532            .join_span(&self.if_not_exists)
533            .join_span(&self.name)
534            .join_span(&self.params)
535            .join_span(&self.characteristics)
536            .join_span(&self.body)
537    }
538}
539
540pub(crate) fn parse_create_procedure<'a>(
541    parser: &mut Parser<'a, '_>,
542    create_span: Span,
543    create_options: Vec<CreateOption<'a>>,
544) -> Result<CreateProcedure<'a>, ParseError> {
545    let procedure_span = parser.consume_keyword(Keyword::PROCEDURE)?;
546
547    let if_not_exists = if let Some(if_) = parser.skip_keyword(Keyword::IF) {
548        Some(
549            parser
550                .consume_keywords(&[Keyword::NOT, Keyword::EXISTS])?
551                .join_span(&if_),
552        )
553    } else {
554        None
555    };
556
557    let name = parser.consume_plain_identifier_unreserved()?;
558    let mut params = Vec::new();
559    parser.consume_token(Token::LParen)?;
560    parser.recovered("')'", &|t| t == &Token::RParen, |parser| {
561        loop {
562            if matches!(parser.token, Token::RParen) {
563                break;
564            }
565            let direction = match &parser.token {
566                Token::Ident(_, Keyword::IN) => {
567                    let in_ = parser.consume_keyword(Keyword::IN)?;
568                    if let Some(out) = parser.skip_keyword(Keyword::OUT) {
569                        Some(FunctionParamDirection::InOut(in_.join_span(&out)))
570                    } else {
571                        Some(FunctionParamDirection::In(in_))
572                    }
573                }
574                Token::Ident(_, Keyword::OUT) => Some(FunctionParamDirection::Out(
575                    parser.consume_keyword(Keyword::OUT)?,
576                )),
577                Token::Ident(_, Keyword::INOUT) => Some(FunctionParamDirection::InOut(
578                    parser.consume_keyword(Keyword::INOUT)?,
579                )),
580                _ => None,
581            };
582            let name = Some(parser.consume_plain_identifier_unreserved()?);
583            let type_ = parse_data_type(parser, DataTypeContext::FunctionParam)?;
584            params.push(FunctionParam {
585                direction,
586                name,
587                type_,
588                default: None,
589            });
590            if parser.skip_token(Token::Comma).is_none() {
591                break;
592            }
593        }
594        Ok(())
595    })?;
596    parser.consume_token(Token::RParen)?;
597
598    let mut characteristics = Vec::new();
599    loop {
600        let f = match &parser.token {
601            Token::Ident(_, Keyword::NOT) => FunctionCharacteristic::NotDeterministic(
602                parser.consume_keywords(&[Keyword::NOT, Keyword::DETERMINISTIC])?,
603            ),
604            Token::Ident(_, Keyword::DETERMINISTIC) => FunctionCharacteristic::Deterministic(
605                parser.consume_keyword(Keyword::DETERMINISTIC)?,
606            ),
607            Token::Ident(_, Keyword::CONTAINS) => FunctionCharacteristic::ContainsSql(
608                parser.consume_keywords(&[Keyword::CONTAINS, Keyword::SQL])?,
609            ),
610            Token::Ident(_, Keyword::NO) => FunctionCharacteristic::NoSql(
611                parser.consume_keywords(&[Keyword::NO, Keyword::SQL])?,
612            ),
613            Token::Ident(_, Keyword::READS) => {
614                FunctionCharacteristic::ReadsSqlData(parser.consume_keywords(&[
615                    Keyword::READS,
616                    Keyword::SQL,
617                    Keyword::DATA,
618                ])?)
619            }
620            Token::Ident(_, Keyword::MODIFIES) => {
621                FunctionCharacteristic::ModifiesSqlData(parser.consume_keywords(&[
622                    Keyword::MODIFIES,
623                    Keyword::SQL,
624                    Keyword::DATA,
625                ])?)
626            }
627            Token::Ident(_, Keyword::COMMENT) => {
628                parser.consume_keyword(Keyword::COMMENT)?;
629                FunctionCharacteristic::Comment(parser.consume_string()?)
630            }
631            Token::Ident(_, Keyword::SQL) => {
632                let span = parser.consume_keywords(&[Keyword::SQL, Keyword::SECURITY])?;
633                match &parser.token {
634                    Token::Ident(_, Keyword::DEFINER) => {
635                        FunctionCharacteristic::SqlSecurityDefiner(
636                            parser.consume_keyword(Keyword::DEFINER)?.join_span(&span),
637                        )
638                    }
639                    Token::Ident(_, Keyword::USER) => FunctionCharacteristic::SqlSecurityUser(
640                        parser.consume_keyword(Keyword::USER)?.join_span(&span),
641                    ),
642                    _ => parser.expected_failure("'DEFINER' or 'USER'")?,
643                }
644            }
645            _ => break,
646        };
647        characteristics.push(f);
648    }
649
650    let old = core::mem::replace(&mut parser.permit_compound_statements, true);
651    let body = parse_statement(parser)?;
652    parser.permit_compound_statements = old;
653
654    Ok(CreateProcedure {
655        create_span,
656        create_options,
657        procedure_span,
658        if_not_exists,
659        name,
660        params,
661        characteristics,
662        body,
663    })
664}