Skip to main content

hawdb_sql/pgq/
binder.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt;
17
18use hawdb_sql_syntax::{
19    BinaryOperatorSyntax, ExpressionKindSyntax, ExpressionSyntax, GraphElementPatternSyntax,
20    GraphPathPrimarySyntax, GraphPathSyntax, GraphTable, Identifier, LiteralSyntax,
21    PostgresFromItemSyntax, PostgresSelectSyntax, QualifiedName, Span, TableAlias,
22};
23
24use super::{
25    BoundPgqColumn, BoundPgqExpression, BoundPgqExpressionKind, BoundPgqGraphPattern,
26    BoundPgqGraphTable, BoundPgqLiteral, BoundPgqPath, BoundPgqPathFactor, BoundPgqPathPrimary,
27    BoundPgqVariable, PgqBindingContext, PgqCatalog, PgqDataType, PgqElementKind, PgqSlotId,
28    PropertyGraphElementSchema, PropertyGraphSchema,
29};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum PgqBindErrorCode {
33    UnknownGraph,
34    UnknownLabel,
35    DuplicateVariable,
36    ConflictingVariable,
37    InvalidPath,
38    UnknownReference,
39    UnknownProperty,
40    MissingColumnName,
41    DuplicateColumn,
42    ColumnAliasCountMismatch,
43    UnsupportedExpression,
44    InvalidLiteral,
45    TypeMismatch,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct PgqBindError {
50    pub code: PgqBindErrorCode,
51    pub span: Span,
52    pub message: String,
53}
54
55impl PgqBindError {
56    fn new(code: PgqBindErrorCode, span: Span, message: impl Into<String>) -> Self {
57        Self {
58            code,
59            span,
60            message: message.into(),
61        }
62    }
63}
64
65impl fmt::Display for PgqBindError {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(
68            formatter,
69            "SQL/PGQ binding error {:?} at bytes {}..{}: {}",
70            self.code, self.span.start, self.span.end, self.message
71        )
72    }
73}
74
75impl std::error::Error for PgqBindError {}
76
77pub fn bind_postgres_graph_tables(
78    input: &str,
79    select: &PostgresSelectSyntax,
80    catalog: &dyn PgqCatalog,
81    context: &PgqBindingContext,
82) -> Result<Vec<BoundPgqGraphTable>, PgqBindError> {
83    let mut bound = Vec::new();
84    for source in &select.from {
85        bind_from_item(input, &source.relation, catalog, context, &mut bound)?;
86        for join in &source.joins {
87            bind_from_item(input, &join.relation, catalog, context, &mut bound)?;
88        }
89    }
90    Ok(bound)
91}
92
93fn bind_from_item(
94    input: &str,
95    item: &PostgresFromItemSyntax,
96    catalog: &dyn PgqCatalog,
97    context: &PgqBindingContext,
98    bound: &mut Vec<BoundPgqGraphTable>,
99) -> Result<(), PgqBindError> {
100    let PostgresFromItemSyntax::GraphTable(table) = item else {
101        return Ok(());
102    };
103    let graph_name = normalize_qualified_name(input, &table.graph);
104    let graph = catalog.property_graph(&graph_name).ok_or_else(|| {
105        PgqBindError::new(
106            PgqBindErrorCode::UnknownGraph,
107            table.graph.span,
108            format!("property graph {} does not exist", graph_name.join(".")),
109        )
110    })?;
111    bound.push(GraphTableBinder::new(input, graph, context).bind(table, graph_name)?);
112    Ok(())
113}
114
115struct GraphTableBinder<'a> {
116    input: &'a str,
117    graph: &'a PropertyGraphSchema,
118    context: &'a PgqBindingContext,
119    variables: Vec<BoundPgqVariable>,
120    named_slots: BTreeMap<String, PgqSlotId>,
121    slots_by_span: BTreeMap<(usize, usize), PgqSlotId>,
122}
123
124impl<'a> GraphTableBinder<'a> {
125    fn new(input: &'a str, graph: &'a PropertyGraphSchema, context: &'a PgqBindingContext) -> Self {
126        Self {
127            input,
128            graph,
129            context,
130            variables: Vec::new(),
131            named_slots: BTreeMap::new(),
132            slots_by_span: BTreeMap::new(),
133        }
134    }
135
136    fn bind(
137        mut self,
138        table: &GraphTable,
139        graph_name: Vec<String>,
140    ) -> Result<BoundPgqGraphTable, PgqBindError> {
141        if table.pattern.paths.len() != 1 {
142            return Err(PgqBindError::new(
143                PgqBindErrorCode::InvalidPath,
144                table.pattern.span,
145                "the qualified GRAPH_TABLE slice requires exactly one graph path",
146            ));
147        }
148        for path in &table.pattern.paths {
149            self.validate_path_shape(path)?;
150            self.register_path(path)?;
151        }
152
153        let paths = table
154            .pattern
155            .paths
156            .iter()
157            .map(|path| self.bind_path(path))
158            .collect::<Result<Vec<_>, _>>()?;
159        let predicate = table
160            .pattern
161            .predicate
162            .as_ref()
163            .map(|predicate| self.bind_predicate(predicate))
164            .transpose()?;
165        let mut columns = self.bind_columns(&table.columns)?;
166        if let Some(alias) = &table.alias {
167            self.apply_table_column_aliases(alias, &mut columns)?;
168        }
169        let alias = table
170            .alias
171            .as_ref()
172            .map(|alias| normalize_table_alias(self.input, alias));
173
174        Ok(BoundPgqGraphTable {
175            graph_name,
176            variables: self.variables,
177            pattern: BoundPgqGraphPattern { paths, predicate },
178            columns,
179            alias,
180            span: table.span,
181        })
182    }
183
184    fn validate_path_shape(&self, path: &GraphPathSyntax) -> Result<(), PgqBindError> {
185        if path.factors.len() == 1
186            && matches!(
187                path.factors[0].primary,
188                GraphPathPrimarySyntax::Parenthesized { .. }
189            )
190        {
191            return Ok(());
192        }
193        let mut expect_vertex = true;
194        for factor in &path.factors {
195            let is_vertex = matches!(factor.primary, GraphPathPrimarySyntax::Vertex(_));
196            let is_edge = matches!(factor.primary, GraphPathPrimarySyntax::Edge(_));
197            if !is_vertex && !is_edge {
198                return Err(PgqBindError::new(
199                    PgqBindErrorCode::InvalidPath,
200                    factor.span,
201                    "a parenthesized path must be a standalone path factor in this slice",
202                ));
203            }
204            if is_vertex != expect_vertex {
205                return Err(PgqBindError::new(
206                    PgqBindErrorCode::InvalidPath,
207                    factor.span,
208                    "graph paths must alternate vertex and edge patterns and end at a vertex",
209                ));
210            }
211            if factor.quantifier.is_some() && is_vertex {
212                return Err(PgqBindError::new(
213                    PgqBindErrorCode::InvalidPath,
214                    factor.span,
215                    "vertex pattern quantifiers are not supported",
216                ));
217            }
218            if let Some(quantifier) = &factor.quantifier
219                && quantifier.min > quantifier.max
220            {
221                return Err(PgqBindError::new(
222                    PgqBindErrorCode::InvalidPath,
223                    quantifier.span,
224                    "graph quantifier lower bound exceeds upper bound",
225                ));
226            }
227            expect_vertex = !expect_vertex;
228        }
229        if expect_vertex {
230            return Err(PgqBindError::new(
231                PgqBindErrorCode::InvalidPath,
232                path.span,
233                "graph paths must end at a vertex pattern",
234            ));
235        }
236        Ok(())
237    }
238
239    fn register_path(&mut self, path: &GraphPathSyntax) -> Result<(), PgqBindError> {
240        for factor in &path.factors {
241            match &factor.primary {
242                GraphPathPrimarySyntax::Vertex(vertex) => {
243                    self.register_element(vertex, PgqElementKind::Vertex)?;
244                }
245                GraphPathPrimarySyntax::Edge(edge) => {
246                    self.register_element_fields(
247                        edge.variable.as_ref(),
248                        &edge.labels,
249                        PgqElementKind::Edge,
250                        edge.span,
251                    )?;
252                }
253                GraphPathPrimarySyntax::Parenthesized { path, .. } => {
254                    self.validate_path_shape(path)?;
255                    self.register_path(path)?;
256                }
257            }
258        }
259        Ok(())
260    }
261
262    fn register_element(
263        &mut self,
264        element: &GraphElementPatternSyntax,
265        kind: PgqElementKind,
266    ) -> Result<PgqSlotId, PgqBindError> {
267        self.register_element_fields(
268            element.variable.as_ref(),
269            &element.labels,
270            kind,
271            element.span,
272        )
273    }
274
275    fn register_element_fields(
276        &mut self,
277        variable: Option<&Identifier>,
278        labels: &[Identifier],
279        kind: PgqElementKind,
280        span: Span,
281    ) -> Result<PgqSlotId, PgqBindError> {
282        let labels = labels
283            .iter()
284            .map(|label| normalize_identifier(self.input, label))
285            .collect::<Vec<_>>();
286        self.validate_labels(kind, &labels, span)?;
287        let name = variable.map(|variable| normalize_identifier(self.input, variable));
288        let slot = if let Some(name) = &name {
289            if let Some(slot) = self.named_slots.get(name).copied() {
290                let existing = &mut self.variables[slot.0 as usize];
291                if existing.kind != kind {
292                    return Err(PgqBindError::new(
293                        PgqBindErrorCode::ConflictingVariable,
294                        span,
295                        format!("graph variable {name} is used as both vertex and edge"),
296                    ));
297                }
298                if !labels.is_empty() {
299                    if existing.labels.is_empty() {
300                        existing.labels.clone_from(&labels);
301                    } else if existing.labels != labels {
302                        return Err(PgqBindError::new(
303                            PgqBindErrorCode::ConflictingVariable,
304                            span,
305                            format!("graph variable {name} has conflicting label predicates"),
306                        ));
307                    }
308                }
309                slot
310            } else {
311                let slot = self.next_slot(span)?;
312                self.named_slots.insert(name.clone(), slot);
313                self.variables.push(BoundPgqVariable {
314                    slot,
315                    name: Some(name.clone()),
316                    kind,
317                    labels,
318                    span,
319                });
320                slot
321            }
322        } else {
323            let slot = self.next_slot(span)?;
324            self.variables.push(BoundPgqVariable {
325                slot,
326                name: None,
327                kind,
328                labels,
329                span,
330            });
331            slot
332        };
333        self.slots_by_span.insert((span.start, span.end), slot);
334        Ok(slot)
335    }
336
337    fn next_slot(&self, span: Span) -> Result<PgqSlotId, PgqBindError> {
338        u32::try_from(self.variables.len())
339            .map(PgqSlotId)
340            .map_err(|_| {
341                PgqBindError::new(
342                    PgqBindErrorCode::DuplicateVariable,
343                    span,
344                    "too many graph variables",
345                )
346            })
347    }
348
349    fn validate_labels(
350        &self,
351        kind: PgqElementKind,
352        labels: &[String],
353        span: Span,
354    ) -> Result<(), PgqBindError> {
355        let schemas = self.element_schemas(kind);
356        for label in labels {
357            if !schemas.contains_key(label) {
358                return Err(PgqBindError::new(
359                    PgqBindErrorCode::UnknownLabel,
360                    span,
361                    format!("unknown {} label {label}", element_kind_name(kind)),
362                ));
363            }
364        }
365        Ok(())
366    }
367
368    fn bind_path(&self, path: &GraphPathSyntax) -> Result<BoundPgqPath, PgqBindError> {
369        let factors = path
370            .factors
371            .iter()
372            .map(|factor| {
373                let primary = match &factor.primary {
374                    GraphPathPrimarySyntax::Vertex(vertex) => BoundPgqPathPrimary::Vertex {
375                        slot: self.slot_for_span(vertex.span)?,
376                        predicate: vertex
377                            .predicate
378                            .as_ref()
379                            .map(|predicate| self.bind_predicate(predicate))
380                            .transpose()?,
381                    },
382                    GraphPathPrimarySyntax::Edge(edge) => BoundPgqPathPrimary::Edge {
383                        slot: self.slot_for_span(edge.span)?,
384                        direction: edge.direction,
385                        predicate: edge
386                            .predicate
387                            .as_ref()
388                            .map(|predicate| self.bind_predicate(predicate))
389                            .transpose()?,
390                    },
391                    GraphPathPrimarySyntax::Parenthesized {
392                        path, predicate, ..
393                    } => BoundPgqPathPrimary::Parenthesized {
394                        path: Box::new(self.bind_path(path)?),
395                        predicate: predicate
396                            .as_ref()
397                            .map(|predicate| self.bind_predicate(predicate))
398                            .transpose()?,
399                    },
400                };
401                Ok(BoundPgqPathFactor {
402                    primary,
403                    quantifier: factor.quantifier.clone(),
404                })
405            })
406            .collect::<Result<Vec<_>, PgqBindError>>()?;
407        Ok(BoundPgqPath { factors })
408    }
409
410    fn slot_for_span(&self, span: Span) -> Result<PgqSlotId, PgqBindError> {
411        self.slots_by_span
412            .get(&(span.start, span.end))
413            .copied()
414            .ok_or_else(|| {
415                PgqBindError::new(
416                    PgqBindErrorCode::UnknownReference,
417                    span,
418                    "graph element has no registered slot",
419                )
420            })
421    }
422
423    fn bind_columns(
424        &self,
425        columns: &[hawdb_sql_syntax::GraphTableColumn],
426    ) -> Result<Vec<BoundPgqColumn>, PgqBindError> {
427        let mut names = BTreeSet::new();
428        columns
429            .iter()
430            .map(|column| {
431                let expression = self.bind_expression(&column.expression)?;
432                let name = if let Some(alias) = &column.alias {
433                    normalize_identifier(self.input, alias)
434                } else {
435                    derive_column_name(self.input, &column.expression).ok_or_else(|| {
436                        PgqBindError::new(
437                            PgqBindErrorCode::MissingColumnName,
438                            column.span,
439                            "complex GRAPH_TABLE output expressions require an explicit alias",
440                        )
441                    })?
442                };
443                if !names.insert(name.clone()) {
444                    return Err(PgqBindError::new(
445                        PgqBindErrorCode::DuplicateColumn,
446                        column.span,
447                        format!("duplicate GRAPH_TABLE output column {name}"),
448                    ));
449                }
450                Ok(BoundPgqColumn {
451                    name,
452                    data_type: expression.data_type,
453                    expression,
454                })
455            })
456            .collect()
457    }
458
459    fn apply_table_column_aliases(
460        &self,
461        alias: &TableAlias,
462        columns: &mut [BoundPgqColumn],
463    ) -> Result<(), PgqBindError> {
464        if alias.columns.len() > columns.len() {
465            return Err(PgqBindError::new(
466                PgqBindErrorCode::ColumnAliasCountMismatch,
467                alias.span,
468                "GRAPH_TABLE alias has more column names than its output schema",
469            ));
470        }
471        for (column, alias) in columns.iter_mut().zip(&alias.columns) {
472            column.name = normalize_identifier(self.input, alias);
473        }
474        let mut names = BTreeSet::new();
475        for column in columns {
476            if !names.insert(column.name.clone()) {
477                return Err(PgqBindError::new(
478                    PgqBindErrorCode::DuplicateColumn,
479                    alias.span,
480                    format!("duplicate GRAPH_TABLE output column {}", column.name),
481                ));
482            }
483        }
484        Ok(())
485    }
486
487    fn bind_predicate(
488        &self,
489        expression: &ExpressionSyntax,
490    ) -> Result<BoundPgqExpression, PgqBindError> {
491        let bound = self.bind_expression(expression)?;
492        if !matches!(bound.data_type, PgqDataType::Boolean | PgqDataType::Unknown) {
493            return Err(PgqBindError::new(
494                PgqBindErrorCode::TypeMismatch,
495                expression.span,
496                "graph predicates must have boolean type",
497            ));
498        }
499        Ok(bound)
500    }
501
502    fn bind_expression(
503        &self,
504        expression: &ExpressionSyntax,
505    ) -> Result<BoundPgqExpression, PgqBindError> {
506        let (kind, data_type) = match &expression.kind {
507            ExpressionKindSyntax::Like { .. } | ExpressionKindSyntax::Filtered { .. } => {
508                return Err(PgqBindError::new(
509                    PgqBindErrorCode::UnsupportedExpression,
510                    expression.span,
511                    "relational LIKE and FILTER are not qualified GRAPH_TABLE expressions",
512                ));
513            }
514            ExpressionKindSyntax::Column(name) => self.bind_column_reference(name)?,
515            ExpressionKindSyntax::Wildcard(_) => {
516                return Err(PgqBindError::new(
517                    PgqBindErrorCode::UnsupportedExpression,
518                    expression.span,
519                    "wildcards are not scalar GRAPH_TABLE expressions",
520                ));
521            }
522            ExpressionKindSyntax::Parameter(position) => (
523                BoundPgqExpressionKind::Parameter(*position),
524                PgqDataType::Unknown,
525            ),
526            ExpressionKindSyntax::Literal(literal) => {
527                let (literal, data_type) = bind_literal(self.input, literal)?;
528                (BoundPgqExpressionKind::Literal(literal), data_type)
529            }
530            ExpressionKindSyntax::TypedString {
531                time_zone: Some(_), ..
532            } => {
533                return Err(PgqBindError::new(
534                    PgqBindErrorCode::UnsupportedExpression,
535                    expression.span,
536                    "TIMESTAMP literals are not supported in PGQ",
537                ));
538            }
539            ExpressionKindSyntax::TypedString {
540                data_type,
541                value,
542                time_zone: None,
543            } => {
544                let data_type_name = normalize_identifier(self.input, data_type);
545                let value = decode_string(self.input, *value).ok_or_else(|| {
546                    PgqBindError::new(
547                        PgqBindErrorCode::InvalidLiteral,
548                        *value,
549                        "invalid PostgreSQL string literal",
550                    )
551                })?;
552                (
553                    BoundPgqExpressionKind::Literal(BoundPgqLiteral::TypedString {
554                        data_type: data_type_name,
555                        value,
556                    }),
557                    PgqDataType::String,
558                )
559            }
560            ExpressionKindSyntax::Unary {
561                operator,
562                expression: inner,
563            } => {
564                let inner = self.bind_expression(inner)?;
565                let data_type = match operator {
566                    hawdb_sql_syntax::UnaryOperatorSyntax::Not => {
567                        require_boolean(inner.data_type, expression.span)?;
568                        PgqDataType::Boolean
569                    }
570                    hawdb_sql_syntax::UnaryOperatorSyntax::Plus
571                    | hawdb_sql_syntax::UnaryOperatorSyntax::Minus => {
572                        require_numeric(inner.data_type, expression.span)?;
573                        inner.data_type
574                    }
575                };
576                (
577                    BoundPgqExpressionKind::Unary {
578                        operator: *operator,
579                        expression: Box::new(inner),
580                    },
581                    data_type,
582                )
583            }
584            ExpressionKindSyntax::Binary {
585                left,
586                operator,
587                right,
588            } => {
589                let left = self.bind_expression(left)?;
590                let right = self.bind_expression(right)?;
591                validate_binary_operands(
592                    *operator,
593                    left.data_type,
594                    right.data_type,
595                    expression.span,
596                )?;
597                let data_type = binary_result_type(*operator, left.data_type, right.data_type);
598                (
599                    BoundPgqExpressionKind::Binary {
600                        left: Box::new(left),
601                        operator: *operator,
602                        right: Box::new(right),
603                    },
604                    data_type,
605                )
606            }
607            ExpressionKindSyntax::IsNull {
608                expression: inner,
609                negated,
610            } => (
611                BoundPgqExpressionKind::IsNull {
612                    expression: Box::new(self.bind_expression(inner)?),
613                    negated: *negated,
614                },
615                PgqDataType::Boolean,
616            ),
617            ExpressionKindSyntax::InList {
618                expression: inner,
619                values,
620                negated,
621            } => {
622                let inner = self.bind_expression(inner)?;
623                let values = values
624                    .iter()
625                    .map(|value| self.bind_expression(value))
626                    .collect::<Result<Vec<_>, _>>()?;
627                for value in &values {
628                    require_comparable(inner.data_type, value.data_type, expression.span)?;
629                }
630                (
631                    BoundPgqExpressionKind::InList {
632                        expression: Box::new(inner),
633                        values,
634                        negated: *negated,
635                    },
636                    PgqDataType::Boolean,
637                )
638            }
639            ExpressionKindSyntax::Between {
640                expression: inner,
641                low,
642                high,
643                negated,
644            } => {
645                let inner = self.bind_expression(inner)?;
646                let low = self.bind_expression(low)?;
647                let high = self.bind_expression(high)?;
648                require_comparable(inner.data_type, low.data_type, expression.span)?;
649                require_comparable(inner.data_type, high.data_type, expression.span)?;
650                (
651                    BoundPgqExpressionKind::Between {
652                        expression: Box::new(inner),
653                        low: Box::new(low),
654                        high: Box::new(high),
655                        negated: *negated,
656                    },
657                    PgqDataType::Boolean,
658                )
659            }
660            ExpressionKindSyntax::Function {
661                name,
662                arguments,
663                distinct,
664            } => {
665                let name = normalize_qualified_name(self.input, name);
666                let function = name.last().map(String::as_str).unwrap_or_default();
667                if is_disallowed_graph_table_function(function) {
668                    return Err(PgqBindError::new(
669                        PgqBindErrorCode::UnsupportedExpression,
670                        expression.span,
671                        format!("function {function} is not allowed in GRAPH_TABLE expressions"),
672                    ));
673                }
674                let arguments = arguments
675                    .iter()
676                    .map(|argument| self.bind_expression(argument))
677                    .collect::<Result<Vec<_>, _>>()?;
678                let data_type = function_result_type(function, &arguments);
679                (
680                    BoundPgqExpressionKind::Function {
681                        name,
682                        arguments,
683                        distinct: *distinct,
684                    },
685                    data_type,
686                )
687            }
688            ExpressionKindSyntax::Cast {
689                expression: inner,
690                data_type,
691                type_parameters,
692                time_zone,
693            } => {
694                if !type_parameters.is_empty() || time_zone.is_some() {
695                    return Err(PgqBindError::new(
696                        PgqBindErrorCode::UnsupportedExpression,
697                        expression.span,
698                        "parameterized scalar casts are not supported in PGQ",
699                    ));
700                }
701                let inner = self.bind_expression(inner)?;
702                let data_type_name = normalize_qualified_name(self.input, data_type);
703                let result_type = cast_result_type(&data_type_name);
704                (
705                    BoundPgqExpressionKind::Cast {
706                        expression: Box::new(inner),
707                        data_type: data_type_name,
708                    },
709                    result_type,
710                )
711            }
712            ExpressionKindSyntax::Collate {
713                expression: inner,
714                collation,
715            } => {
716                let inner = self.bind_expression(inner)?;
717                require_string(inner.data_type, expression.span)?;
718                let data_type = inner.data_type;
719                (
720                    BoundPgqExpressionKind::Collate {
721                        expression: Box::new(inner),
722                        collation: normalize_qualified_name(self.input, collation),
723                    },
724                    data_type,
725                )
726            }
727            ExpressionKindSyntax::Parenthesized(inner) => {
728                let inner = self.bind_expression(inner)?;
729                return Ok(BoundPgqExpression {
730                    span: expression.span,
731                    ..inner
732                });
733            }
734        };
735        Ok(BoundPgqExpression {
736            kind,
737            data_type,
738            span: expression.span,
739        })
740    }
741
742    fn bind_column_reference(
743        &self,
744        name: &QualifiedName,
745    ) -> Result<(BoundPgqExpressionKind, PgqDataType), PgqBindError> {
746        let parts = normalize_qualified_name(self.input, name);
747        if let Some(variable) = parts
748            .first()
749            .and_then(|variable| self.named_slots.get(variable))
750            .and_then(|slot| self.variables.get(slot.0 as usize))
751        {
752            return match parts.as_slice() {
753                [_] => Ok((
754                    BoundPgqExpressionKind::Variable(variable.slot),
755                    PgqDataType::GraphElement(variable.kind),
756                )),
757                [_, property] => Ok((
758                    BoundPgqExpressionKind::Property {
759                        slot: variable.slot,
760                        property: property.clone(),
761                    },
762                    self.property_type(variable, property, name.span)?,
763                )),
764                _ => Err(PgqBindError::new(
765                    PgqBindErrorCode::UnsupportedExpression,
766                    name.span,
767                    "nested graph property paths are not supported",
768                )),
769            };
770        }
771        if let Some(data_type) = self.context.outer_columns.get(&parts).copied() {
772            return Ok((BoundPgqExpressionKind::OuterColumn(parts), data_type));
773        }
774        Err(PgqBindError::new(
775            PgqBindErrorCode::UnknownReference,
776            name.span,
777            format!(
778                "unknown GRAPH_TABLE expression reference {}",
779                parts.join(".")
780            ),
781        ))
782    }
783
784    fn property_type(
785        &self,
786        variable: &BoundPgqVariable,
787        property: &str,
788        span: Span,
789    ) -> Result<PgqDataType, PgqBindError> {
790        let schemas = self.element_schemas(variable.kind);
791        let candidates = if variable.labels.is_empty() {
792            schemas.values().collect::<Vec<_>>()
793        } else {
794            variable
795                .labels
796                .iter()
797                .filter_map(|label| schemas.get(label))
798                .collect::<Vec<_>>()
799        };
800        let mut types = candidates
801            .into_iter()
802            .filter_map(|schema| schema.properties.get(property).copied());
803        let Some(first) = types.next() else {
804            return Err(PgqBindError::new(
805                PgqBindErrorCode::UnknownProperty,
806                span,
807                format!("unknown property {property} for graph variable"),
808            ));
809        };
810        Ok(if types.all(|data_type| data_type == first) {
811            first
812        } else {
813            PgqDataType::Unknown
814        })
815    }
816
817    fn element_schemas(
818        &self,
819        kind: PgqElementKind,
820    ) -> &BTreeMap<String, PropertyGraphElementSchema> {
821        match kind {
822            PgqElementKind::Vertex => &self.graph.vertex_labels,
823            PgqElementKind::Edge => &self.graph.edge_labels,
824        }
825    }
826}
827
828fn normalize_table_alias(input: &str, alias: &TableAlias) -> String {
829    normalize_identifier(input, &alias.name)
830}
831
832fn normalize_qualified_name(input: &str, name: &QualifiedName) -> Vec<String> {
833    name.parts
834        .iter()
835        .map(|part| normalize_identifier(input, part))
836        .collect()
837}
838
839fn normalize_identifier(input: &str, identifier: &Identifier) -> String {
840    let text = &input[identifier.span.start..identifier.span.end];
841    if identifier.quoted {
842        text[1..text.len() - 1].replace("\"\"", "\"")
843    } else {
844        text.to_ascii_lowercase()
845    }
846}
847
848fn derive_column_name(input: &str, expression: &ExpressionSyntax) -> Option<String> {
849    let ExpressionKindSyntax::Column(name) = &expression.kind else {
850        return None;
851    };
852    name.parts
853        .last()
854        .map(|identifier| normalize_identifier(input, identifier))
855}
856
857fn bind_literal(
858    input: &str,
859    literal: &LiteralSyntax,
860) -> Result<(BoundPgqLiteral, PgqDataType), PgqBindError> {
861    match literal {
862        LiteralSyntax::Null => Ok((BoundPgqLiteral::Null, PgqDataType::Null)),
863        LiteralSyntax::Boolean(value) => {
864            Ok((BoundPgqLiteral::Boolean(*value), PgqDataType::Boolean))
865        }
866        LiteralSyntax::Number(span) => {
867            let text = &input[span.start..span.end];
868            if let Ok(value) = text.parse::<i64>() {
869                Ok((BoundPgqLiteral::Int64(value), PgqDataType::Int64))
870            } else if let Ok(value) = text.parse::<f64>() {
871                Ok((BoundPgqLiteral::Float64(value), PgqDataType::Float64))
872            } else {
873                Err(PgqBindError::new(
874                    PgqBindErrorCode::InvalidLiteral,
875                    *span,
876                    "numeric literal is outside the supported range",
877                ))
878            }
879        }
880        LiteralSyntax::String(span) => decode_string(input, *span)
881            .map(|value| (BoundPgqLiteral::String(value), PgqDataType::String))
882            .ok_or_else(|| {
883                PgqBindError::new(
884                    PgqBindErrorCode::InvalidLiteral,
885                    *span,
886                    "invalid PostgreSQL string literal",
887                )
888            }),
889    }
890}
891
892fn decode_string(input: &str, span: Span) -> Option<String> {
893    let text = input.get(span.start..span.end)?;
894    if text.starts_with('\'') && text.ends_with('\'') && text.len() >= 2 {
895        return Some(text[1..text.len() - 1].replace("''", "'"));
896    }
897    if let Some(without_prefix) = text.strip_prefix('$') {
898        let delimiter_end = without_prefix.find('$')? + 1;
899        let delimiter = &text[..=delimiter_end];
900        if text.ends_with(delimiter) && text.len() >= delimiter.len() * 2 {
901            return Some(text[delimiter.len()..text.len() - delimiter.len()].to_string());
902        }
903    }
904    None
905}
906
907fn binary_result_type(
908    operator: BinaryOperatorSyntax,
909    left: PgqDataType,
910    right: PgqDataType,
911) -> PgqDataType {
912    match operator {
913        BinaryOperatorSyntax::Or
914        | BinaryOperatorSyntax::And
915        | BinaryOperatorSyntax::Equal
916        | BinaryOperatorSyntax::NotEqual
917        | BinaryOperatorSyntax::Less
918        | BinaryOperatorSyntax::LessOrEqual
919        | BinaryOperatorSyntax::Greater
920        | BinaryOperatorSyntax::GreaterOrEqual => PgqDataType::Boolean,
921        BinaryOperatorSyntax::Concat => PgqDataType::String,
922        BinaryOperatorSyntax::Add
923        | BinaryOperatorSyntax::Subtract
924        | BinaryOperatorSyntax::Multiply
925        | BinaryOperatorSyntax::Divide
926        | BinaryOperatorSyntax::Modulo => numeric_common_type(left, right),
927    }
928}
929
930fn validate_binary_operands(
931    operator: BinaryOperatorSyntax,
932    left: PgqDataType,
933    right: PgqDataType,
934    span: Span,
935) -> Result<(), PgqBindError> {
936    match operator {
937        BinaryOperatorSyntax::Or | BinaryOperatorSyntax::And => {
938            require_boolean(left, span)?;
939            require_boolean(right, span)
940        }
941        BinaryOperatorSyntax::Concat => {
942            require_string(left, span)?;
943            require_string(right, span)
944        }
945        BinaryOperatorSyntax::Add
946        | BinaryOperatorSyntax::Subtract
947        | BinaryOperatorSyntax::Multiply
948        | BinaryOperatorSyntax::Divide
949        | BinaryOperatorSyntax::Modulo => {
950            require_numeric(left, span)?;
951            require_numeric(right, span)
952        }
953        BinaryOperatorSyntax::Equal
954        | BinaryOperatorSyntax::NotEqual
955        | BinaryOperatorSyntax::Less
956        | BinaryOperatorSyntax::LessOrEqual
957        | BinaryOperatorSyntax::Greater
958        | BinaryOperatorSyntax::GreaterOrEqual => require_comparable(left, right, span),
959    }
960}
961
962fn require_comparable(
963    left: PgqDataType,
964    right: PgqDataType,
965    span: Span,
966) -> Result<(), PgqBindError> {
967    if matches!(left, PgqDataType::Unknown | PgqDataType::Null)
968        || matches!(right, PgqDataType::Unknown | PgqDataType::Null)
969        || left == right
970        || matches!(
971            (left, right),
972            (PgqDataType::Int64, PgqDataType::Float64) | (PgqDataType::Float64, PgqDataType::Int64)
973        )
974    {
975        return Ok(());
976    }
977    Err(PgqBindError::new(
978        PgqBindErrorCode::TypeMismatch,
979        span,
980        format!("cannot compare {left:?} with {right:?}"),
981    ))
982}
983
984fn require_boolean(data_type: PgqDataType, span: Span) -> Result<(), PgqBindError> {
985    require_type(
986        data_type,
987        &[PgqDataType::Boolean],
988        span,
989        "boolean expression",
990    )
991}
992
993fn require_numeric(data_type: PgqDataType, span: Span) -> Result<(), PgqBindError> {
994    require_type(
995        data_type,
996        &[PgqDataType::Int64, PgqDataType::Float64],
997        span,
998        "numeric expression",
999    )
1000}
1001
1002fn require_string(data_type: PgqDataType, span: Span) -> Result<(), PgqBindError> {
1003    require_type(data_type, &[PgqDataType::String], span, "string expression")
1004}
1005
1006fn require_type(
1007    data_type: PgqDataType,
1008    accepted: &[PgqDataType],
1009    span: Span,
1010    expected: &'static str,
1011) -> Result<(), PgqBindError> {
1012    if matches!(data_type, PgqDataType::Unknown | PgqDataType::Null)
1013        || accepted.contains(&data_type)
1014    {
1015        return Ok(());
1016    }
1017    Err(PgqBindError::new(
1018        PgqBindErrorCode::TypeMismatch,
1019        span,
1020        format!("expected {expected}, found {data_type:?}"),
1021    ))
1022}
1023
1024fn numeric_common_type(left: PgqDataType, right: PgqDataType) -> PgqDataType {
1025    match (left, right) {
1026        (PgqDataType::Float64, PgqDataType::Int64 | PgqDataType::Float64)
1027        | (PgqDataType::Int64, PgqDataType::Float64) => PgqDataType::Float64,
1028        (PgqDataType::Int64, PgqDataType::Int64) => PgqDataType::Int64,
1029        _ => PgqDataType::Unknown,
1030    }
1031}
1032
1033fn function_result_type(function: &str, arguments: &[BoundPgqExpression]) -> PgqDataType {
1034    if ["lower", "upper", "pg_typeof"]
1035        .into_iter()
1036        .any(|candidate| function.eq_ignore_ascii_case(candidate))
1037    {
1038        PgqDataType::String
1039    } else {
1040        arguments
1041            .first()
1042            .map_or(PgqDataType::Unknown, |argument| argument.data_type)
1043    }
1044}
1045
1046fn cast_result_type(name: &[String]) -> PgqDataType {
1047    match name.last().map(String::as_str) {
1048        Some("bool" | "boolean") => PgqDataType::Boolean,
1049        Some("bigint" | "int8" | "integer" | "int4") => PgqDataType::Int64,
1050        Some("float8" | "double" | "real" | "float4") => PgqDataType::Float64,
1051        Some("text" | "varchar" | "character") => PgqDataType::String,
1052        _ => PgqDataType::Unknown,
1053    }
1054}
1055
1056fn is_disallowed_graph_table_function(function: &str) -> bool {
1057    [
1058        "avg",
1059        "count",
1060        "generate_series",
1061        "max",
1062        "min",
1063        "row_number",
1064        "sum",
1065    ]
1066    .into_iter()
1067    .any(|candidate| function.eq_ignore_ascii_case(candidate))
1068}
1069
1070fn element_kind_name(kind: PgqElementKind) -> &'static str {
1071    match kind {
1072        PgqElementKind::Vertex => "vertex",
1073        PgqElementKind::Edge => "edge",
1074    }
1075}