1use 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}