Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 182 additions & 19 deletions Cargo.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub use self::{
Expression, FromItem, FromItemFunctionBuilder, FromItemJoinBuilder,
FromItemSubqueryBuilder, FromItemTableBuilder, Function, Identifier, JoinType,
PostgresType, SelectExpression, TableName, TableReference, UnaryExpression, UnaryOperator,
VariadicExpression, WhereExpression, WithExpression,
VariadicExpression, VariadicOperator, WhereExpression, WithExpression,
},
statement::{
Distinctness, InsertStatementBuilder, SelectStatement, Statement, WindowStatement,
Expand Down
2 changes: 2 additions & 0 deletions libs/@local/hashql/eval/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ insta = { workspace = true }
libtest-mimic = { workspace = true }
regex = { workspace = true }
similar-asserts = { workspace = true }
sqruff-lib = "0.37.3"
sqruff-lib-core = "0.37.3"
testcontainers = { workspace = true, features = ["reusable-containers"] }
testcontainers-modules = { workspace = true, features = ["postgres"] }

Expand Down
4 changes: 4 additions & 0 deletions libs/@local/hashql/eval/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
impl_trait_in_assoc_type,
try_blocks
)]
#![cfg_attr(test, feature(
// Library Features
iter_intersperse
))]

extern crate alloc;
pub mod context;
Expand Down
33 changes: 33 additions & 0 deletions libs/@local/hashql/eval/src/postgres/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const GRAPH_READ_TERMINATOR: TerminalDiagnosticCategory = TerminalDiagnosticCate
name: "Nested Graph Reads Not Supported in SQL",
};

const AMBIGUOUS_INTEGER_TYPE: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "ambiguous-integer-type",
name: "Cannot Determine Integer Type for SQL Operator Selection",
};

const MISSING_ISLAND_GRAPH: TerminalDiagnosticCategory = TerminalDiagnosticCategory {
id: "missing-island-graph",
name: "Missing Island Graph for Body",
Expand Down Expand Up @@ -93,6 +98,13 @@ pub enum PostgresDiagnosticCategory {
ProjectedAssignment,
/// A nested graph read terminator reached the SQL backend.
GraphReadTerminator,
/// The operand type could not be classified as boolean or integer for SQL operator
/// selection.
///
/// Boolean and integer operations share a single MIR operator (e.g. `BitNot` covers both
/// logical `NOT` and bitwise `~`), but PostgreSQL requires distinct SQL operators. When the
/// operand type cannot be resolved, the compiler cannot select the correct SQL form.
AmbiguousIntegerType,
/// Island analysis did not produce an island graph for a filter body.
MissingIslandGraph,
}
Expand All @@ -117,6 +129,7 @@ impl DiagnosticCategory for PostgresDiagnosticCategory {
Self::FunctionPointerConstant => Some(&FUNCTION_POINTER_CONSTANT),
Self::ProjectedAssignment => Some(&PROJECTED_ASSIGNMENT),
Self::GraphReadTerminator => Some(&GRAPH_READ_TERMINATOR),
Self::AmbiguousIntegerType => Some(&AMBIGUOUS_INTEGER_TYPE),
Self::MissingIslandGraph => Some(&MISSING_ISLAND_GRAPH),
}
}
Expand Down Expand Up @@ -290,6 +303,26 @@ pub(super) fn graph_read_terminator(span: SpanId) -> EvalDiagnostic {
diagnostic
}

#[coverage(off)]
pub(super) fn ambiguous_integer_type(span: SpanId, operator: &str) -> EvalDiagnostic {
let mut diagnostic = Diagnostic::new(
category(PostgresDiagnosticCategory::AmbiguousIntegerType),
Severity::Bug,
)
.primary(Label::new(
span,
format!("cannot determine operand type for `{operator}`"),
));

diagnostic.add_message(Message::note(format!(
"the `{operator}` operator compiles to different SQL depending on whether the operand is \
a boolean or an integer, but the type could not be resolved; this indicates a \
compiler/type-checking bug or an unanticipated type (e.g. a union produced by GVN)"
)));

diagnostic
}

#[coverage(off)]
pub(super) fn missing_island_graph(span: SpanId) -> EvalDiagnostic {
let mut diagnostic = Diagnostic::new(
Expand Down
145 changes: 101 additions & 44 deletions libs/@local/hashql/eval/src/postgres/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
use core::alloc::Allocator;

use hash_graph_postgres_store::store::postgres::query::{
self, BinaryExpression, BinaryOperator, Expression, PostgresType, UnaryExpression,
UnaryOperator,
self, BinaryExpression, BinaryOperator, Expression, Function, PostgresType, UnaryExpression,
UnaryOperator, VariadicExpression, VariadicOperator,
};
use hashql_core::{
graph::Predecessors as _,
Expand All @@ -36,7 +36,7 @@
span::SpanId,
};
use hashql_diagnostics::DiagnosticIssues;
use hashql_hir::node::operation::{InputOp, UnOp};
use hashql_hir::node::operation::InputOp;
use hashql_mir::{
body::{
Body,
Expand All @@ -45,7 +45,7 @@
local::{Local, LocalSnapshotVec},
operand::Operand,
place::{FieldIndex, Place, Projection, ProjectionKind},
rvalue::{Aggregate, AggregateKind, Apply, BinOp, Binary, Input, RValue, Unary},
rvalue::{Aggregate, AggregateKind, Apply, BinOp, Binary, Input, RValue, UnOp, Unary},
statement::{Assign, Statement, StatementKind},
terminator::{Goto, Return, SwitchInt, SwitchTargets, Target, TerminatorKind},
},
Expand All @@ -55,11 +55,12 @@
use super::{
DatabaseContext,
error::{
closure_aggregate, closure_application, entity_path_resolution, function_pointer_constant,
graph_read_terminator, invalid_env_access, invalid_env_projection, projected_assignment,
unsupported_vertex_type,
ambiguous_integer_type, closure_aggregate, closure_application, entity_path_resolution,
function_pointer_constant, graph_read_terminator, invalid_env_access,
invalid_env_projection, projected_assignment, unsupported_vertex_type,
},
traverse::eval_entity_path,
types::{IntegerType, integer_type},
};
use crate::{context::EvalContext, error::EvalDiagnosticIssues};

Expand Down Expand Up @@ -410,13 +411,20 @@
&mut self,
db: &mut DatabaseContext<'heap, A>,
span: SpanId,
Unary { op, operand }: &Unary<'heap>,
unary @ Unary { op, operand }: &Unary<'heap>,
) -> Expression {
let operand = self.compile_operand(db, span, operand);

let op = match *op {
UnOp::Not => UnaryOperator::Not,
UnOp::BitNot => UnaryOperator::BitwiseNot,
UnOp::BitNot => match integer_type(self.context.env, self.body, &unary.operand) {
Some(IntegerType::Boolean) => UnaryOperator::Not,
Some(IntegerType::Integer) => UnaryOperator::BitwiseNot,
None => {
self.diagnostics
.push(ambiguous_integer_type(span, UnOp::BitNot.as_str()));
return Expression::Constant(query::Constant::Null);
}
},
UnOp::Neg => UnaryOperator::Negate,
};

Expand All @@ -430,46 +438,95 @@
&mut self,
db: &mut DatabaseContext<'heap, A>,
span: SpanId,
Binary { op, left, right }: &Binary<'heap>,
binary @ Binary { op, left, right }: &Binary<'heap>,
) -> Expression {
let mut left = self.compile_operand(db, span, left);
let mut right = self.compile_operand(db, span, right);
struct Operands {
left: Expression,
right: Expression,
}

// Operands coming from jsonb extraction are untyped from Postgres' perspective.
// Arithmetic and bitwise operators need explicit casts; comparisons work on jsonb
// directly.
let (op, cast, function) = match *op {
BinOp::Add => (BinaryOperator::Add, Some(PostgresType::Numeric), None),
BinOp::Sub => (BinaryOperator::Subtract, Some(PostgresType::Numeric), None),
BinOp::BitAnd => (BinaryOperator::BitwiseAnd, Some(PostgresType::BigInt), None),
BinOp::BitOr => (BinaryOperator::BitwiseOr, Some(PostgresType::BigInt), None),
BinOp::Eq => (BinaryOperator::Equal, None, Some(query::Function::ToJson)),
BinOp::Ne => (
BinaryOperator::NotEqual,
None,
Some(query::Function::ToJson),
),
BinOp::Lt => (BinaryOperator::Less, None, None),
BinOp::Lte => (BinaryOperator::LessOrEqual, None, None),
BinOp::Gt => (BinaryOperator::Greater, None, None),
BinOp::Gte => (BinaryOperator::GreaterOrEqual, None, None),
};
impl Operands {
fn cast(self, r#type: PostgresType) -> Self {
Self {
left: self.left.grouped().cast(r#type.clone()),
right: self.right.grouped().cast(r#type),
}
}

if let Some(target) = cast {
left = left.grouped().cast(target.clone());
right = right.grouped().cast(target);
}
fn call(self, function: fn(Box<Expression>) -> Function) -> Self {
Self {
left: Expression::Function(function(Box::new(self.left))),
right: Expression::Function(function(Box::new(self.right))),
}
}

fn binary(self, op: BinaryOperator) -> Expression {
Expression::Binary(BinaryExpression {
op,
left: Box::new(self.left),
right: Box::new(self.right),
})
}

if let Some(function) = function {
left = Expression::Function(function(Box::new(left)));
right = Expression::Function(function(Box::new(right)));
fn variadic(self, op: VariadicOperator) -> Expression {
Expression::Variadic(VariadicExpression {
op,
exprs: vec![self.left, self.right],
})
}
}

Expression::Binary(BinaryExpression {
op,
left: Box::new(left),
right: Box::new(right),
})
let left = self.compile_operand(db, span, left);
let right = self.compile_operand(db, span, right);
let operands = Operands { left, right };

// Operands coming from jsonb extraction are untyped from Postgres' perspective.
// Arithmetic and bitwise operators need explicit casts; comparisons work on jsonb
// directly.
match *op {
BinOp::Add => operands
.cast(PostgresType::Numeric)
.binary(BinaryOperator::Add),
BinOp::Sub => operands
.cast(PostgresType::Numeric)
.binary(BinaryOperator::Subtract),
BinOp::BitAnd => match integer_type(self.context.env, self.body, &binary.left) {
Some(IntegerType::Integer) => operands
.cast(PostgresType::BigInt)
.binary(BinaryOperator::BitwiseAnd),
Some(IntegerType::Boolean) => operands
.cast(PostgresType::Boolean)
.variadic(VariadicOperator::And),
None => {
self.diagnostics
.push(ambiguous_integer_type(span, BinOp::BitAnd.as_str()));
return Expression::Constant(query::Constant::Null);

Check warning

Code scanning / clippy

unneeded return statement Warning

unneeded return statement
}
},
BinOp::BitOr => match integer_type(self.context.env, self.body, &binary.left) {
Some(IntegerType::Integer) => operands
.cast(PostgresType::BigInt)
.binary(BinaryOperator::BitwiseOr),
Some(IntegerType::Boolean) => operands
.cast(PostgresType::Boolean)
.variadic(VariadicOperator::Or),
None => {
self.diagnostics
.push(ambiguous_integer_type(span, BinOp::BitOr.as_str()));
return Expression::Constant(query::Constant::Null);

Check warning

Code scanning / clippy

unneeded return statement Warning

unneeded return statement
}
},
BinOp::Eq => operands
.call(query::Function::ToJson)
.binary(BinaryOperator::Equal),
BinOp::Ne => operands
.call(query::Function::ToJson)
.binary(BinaryOperator::NotEqual),
BinOp::Lt => operands.binary(BinaryOperator::Less),
BinOp::Lte => operands.binary(BinaryOperator::LessOrEqual),
BinOp::Gt => operands.binary(BinaryOperator::Greater),
BinOp::Gte => operands.binary(BinaryOperator::GreaterOrEqual),
}
}

fn compile_input(
Expand Down
Loading
Loading