use crate::schema::object::TableObjectType;
use crate::schema::relationship::RelationshipField;
use crate::types::GraphQLType;
use async_graphql::dynamic::{InputObject, InputValue, TypeRef};
use std::collections::{HashMap, HashSet};
const UNIVERSAL: &[&str] = &["_eq", "_neq", "_gt", "_gte", "_lt", "_lte"];
const LIST_VALUED: &[&str] = &["_in", "_nin"];
const TEXT: &[&str] = &[
"_like",
"_nlike",
"_ilike",
"_nilike",
"_similar",
"_nsimilar",
"_regex",
"_iregex",
"_nregex",
"_niregex",
];
const JSON: &[&str] = &["_contains", "_contained_in"];
const LTREE: &[(&str, &str)] = &[
("_ancestor", "ltree"),
("_ancestor_any", "ltree"),
("_descendant", "ltree"),
("_descendant_any", "ltree"),
("_matches", "lquery"),
("_matches_any", "lquery"),
("_matches_fulltext", "ltxtquery"),
];
pub fn ltree_operator(comparison: &str) -> Option<(&'static str, &'static str)> {
Some(match comparison {
"_ancestor" => ("@>", "ltree"),
"_ancestor_any" => ("@>", "ltree[]"),
"_descendant" => ("<@", "ltree"),
"_descendant_any" => ("<@", "ltree[]"),
"_matches" => ("~", "lquery"),
"_matches_any" => ("?", "lquery[]"),
"_matches_fulltext" => ("@", "ltxtquery"),
_ => return None,
})
}
const GEOMETRY: &[&str] = &[
"_st_contains",
"_st_crosses",
"_st_equals",
"_st_intersects",
"_st_3d_intersects",
"_st_overlaps",
"_st_touches",
"_st_within",
];
pub fn postgis_function(operator: &str) -> Option<&'static str> {
Some(match operator {
"_st_contains" => "ST_Contains",
"_st_crosses" => "ST_Crosses",
"_st_equals" => "ST_Equals",
"_st_intersects" => "ST_Intersects",
"_st_3d_intersects" => "ST_3DIntersects",
"_st_overlaps" => "ST_Overlaps",
"_st_touches" => "ST_Touches",
"_st_within" => "ST_Within",
"_st_d_within" => "ST_DWithin",
"_st_3d_d_within" => "ST_3DDWithin",
"_st_intersects_rast" => "ST_Intersects",
"_st_intersects_geom_nband" => "ST_Intersects",
"_st_intersects_nband_geom" => "ST_Intersects",
_ => return None,
})
}
pub fn comparison_type_name(scalar: &str) -> String {
format!("{}_comparison_exp", scalar)
}
fn uncallable_from_a_filter(relationship: &RelationshipField) -> bool {
relationship
.arguments
.iter()
.any(|(_, _, required)| *required)
}
pub fn bool_exp_type_name(base_name: &str) -> String {
format!("{}_bool_exp", base_name)
}
pub fn aggregate_bool_exp_type_name(base_name: &str) -> String {
format!("{}_aggregate_bool_exp", base_name)
}
pub fn aggregate_bool_exp_function(base_name: &str, function: &str) -> String {
format!("{}_aggregate_bool_exp_{}", base_name, function)
}
pub fn aggregate_bool_exp_columns(base_name: &str, function: &str) -> String {
format!(
"{}_select_column_{}_aggregate_bool_exp_{}_arguments_columns",
base_name, base_name, function
)
}
pub const AGGREGATE_PREDICATES: [&str; 3] = ["bool_and", "bool_or", "count"];
fn comparison_input(scalar: &str) -> InputObject {
let mut input = InputObject::new(comparison_type_name(scalar)).description(format!(
"Comparisons against a {} column. All fields are combined with AND.",
scalar
));
for op in UNIVERSAL {
input = input.field(InputValue::new(*op, TypeRef::named(scalar)));
}
for op in LIST_VALUED {
input = input.field(InputValue::new(*op, TypeRef::named_nn_list(scalar)));
}
input = input.field(InputValue::new("_is_null", TypeRef::named("Boolean")));
if scalar == "String" || scalar == "citext" {
for op in TEXT {
input = input.field(InputValue::new(*op, TypeRef::named(scalar)));
}
}
if scalar == "geometry" || scalar == "geography" {
if scalar == "geometry" {
for op in GEOMETRY {
input = input.field(InputValue::new(*op, TypeRef::named("geometry")));
}
} else {
input = input.field(InputValue::new(
"_st_intersects",
TypeRef::named("geography"),
));
}
input = input.field(InputValue::new(
"_st_d_within",
TypeRef::named(format!("st_d_within_{}_input", scalar)),
));
input = input.field(InputValue::new(
"_cast",
TypeRef::named(format!("{}_cast_exp", scalar)),
));
if scalar == "geometry" {
input = input.field(InputValue::new(
"_st_3d_d_within",
TypeRef::named("st_d_within_geometry_input"),
));
}
}
if scalar == "raster" {
input = input
.field(InputValue::new(
"_st_intersects_rast",
TypeRef::named("raster"),
))
.field(InputValue::new(
"_st_intersects_geom_nband",
TypeRef::named("st_intersects_geom_nband_input"),
))
.field(InputValue::new(
"_st_intersects_nband_geom",
TypeRef::named("st_intersects_nband_geom_input"),
));
}
if scalar == "ltree" {
for (comparison, operand) in LTREE {
input = input.field(InputValue::new(
*comparison,
match comparison.ends_with("_any") {
true => TypeRef::named_list(*operand),
false => TypeRef::named(*operand),
},
));
}
}
if scalar == "jsonb" || scalar == "json" {
for op in JSON {
input = input.field(InputValue::new(*op, TypeRef::named(scalar)));
}
input = input.field(InputValue::new("_has_key", TypeRef::named("String")));
input = input.field(InputValue::new(
"_has_keys_any",
TypeRef::named_list("String"),
));
input = input.field(InputValue::new(
"_has_keys_all",
TypeRef::named_list("String"),
));
input = input.field(InputValue::new(
"_cast",
TypeRef::named(format!("{}_cast_exp", scalar)),
));
}
if scalar == "jsonb" {
input = input
.field(InputValue::new(
"_jsonb_path_exists",
TypeRef::named("String"),
))
.field(InputValue::new(
"_jsonb_path_match",
TypeRef::named("String"),
));
}
input
}
fn scalar_for(graphql_type: &GraphQLType) -> String {
match graphql_type {
GraphQLType::List(inner) => scalar_for(inner),
other => other.to_string(),
}
}
pub fn build_inputs(
object_types: &HashMap<String, TableObjectType>,
relationship_fields: &HashMap<String, Vec<RelationshipField>>,
) -> (Vec<InputObject>, HashSet<String>) {
let mut scalars: HashSet<String> = HashSet::new();
let mut inputs = Vec::new();
for (type_name, object) in object_types {
let local_type_name = object.local_name.as_str();
let bool_exp = bool_exp_type_name(local_type_name);
let mut input = InputObject::new(&bool_exp).description(format!(
"Filter rows of {}. Fields are combined with AND unless _or says otherwise.",
local_type_name
));
input = input
.field(InputValue::new("_and", TypeRef::named_nn_list(&bool_exp)))
.field(InputValue::new("_or", TypeRef::named_nn_list(&bool_exp)))
.field(InputValue::new("_not", TypeRef::named(&bool_exp)));
let mut taken: HashSet<String> = HashSet::new();
for field in &object.fields {
if !taken.insert(field.name.clone()) {
continue;
}
let scalar = scalar_for(&field.graphql_type);
input = input.field(InputValue::new(
&field.name,
TypeRef::named(comparison_type_name(&scalar)),
));
scalars.insert(scalar);
}
for relationship in relationship_fields.get(type_name).into_iter().flatten() {
if uncallable_from_a_filter(relationship) {
continue;
}
if !taken.insert(relationship.name.clone()) {
continue;
}
input = input.field(InputValue::new(
&relationship.name,
TypeRef::named(bool_exp_type_name(&relationship.target_local_name)),
));
}
for relationship in relationship_fields.get(type_name).into_iter().flatten() {
if !relationship.is_list || uncallable_from_a_filter(relationship) {
continue;
}
let name = format!("{}_aggregate", relationship.name);
if object.fields.iter().any(|field| field.name == name) {
continue;
}
if !taken.insert(name.clone()) {
continue;
}
input = input.field(InputValue::new(
&name,
TypeRef::named(aggregate_bool_exp_type_name(
&relationship.target_local_name,
)),
));
scalars.insert("Int".to_string());
}
inputs.push(input);
}
if scalars.contains("geometry") {
scalars.insert("geography".to_string());
}
if scalars.contains("geography") {
scalars.insert("geometry".to_string());
}
let mut named: HashSet<String> = scalars.clone();
if scalars.contains("raster") {
named.insert("geometry".to_string());
}
if scalars.contains("ltree") {
named.insert("lquery".to_string());
named.insert("ltxtquery".to_string());
}
if scalars.contains("jsonb") || scalars.contains("json") {
scalars.insert("String".to_string());
}
let mut comparisons: Vec<InputObject> = scalars.iter().map(|s| comparison_input(s)).collect();
if scalars.contains("geometry") || scalars.contains("geography") {
for shape in ["geometry", "geography"] {
if !scalars.contains(shape) {
continue;
}
let mut within = InputObject::new(format!("st_d_within_{}_input", shape))
.description("Within a distance of another shape.")
.field(InputValue::new(
"distance",
TypeRef::named_nn(TypeRef::FLOAT),
))
.field(InputValue::new("from", TypeRef::named_nn(shape)));
if shape == "geography" {
within = within.field(
InputValue::new("use_spheroid", TypeRef::named(TypeRef::BOOLEAN))
.default_value(true),
);
}
comparisons.push(within);
}
}
if scalars.contains("geometry") {
comparisons.push(
InputObject::new("geometry_cast_exp")
.description("Compare a geometry column as another type.")
.field(InputValue::new(
"geography",
TypeRef::named(comparison_type_name("geography")),
)),
);
}
if scalars.contains("geography") {
comparisons.push(
InputObject::new("geography_cast_exp")
.description("Compare a geography column as another type.")
.field(InputValue::new(
"geometry",
TypeRef::named(comparison_type_name("geometry")),
)),
);
}
for document in ["jsonb", "json"] {
if scalars.contains(document) {
comparisons.push(
InputObject::new(format!("{}_cast_exp", document))
.description(format!("Compare a {} column as another type.", document))
.field(InputValue::new(
"String",
TypeRef::named(comparison_type_name("String")),
)),
);
}
}
if scalars.contains("raster") {
comparisons.push(
InputObject::new("st_intersects_geom_nband_input")
.description("Intersecting a shape, optionally in one band.")
.field(InputValue::new("geommin", TypeRef::named_nn("geometry")))
.field(InputValue::new("nband", TypeRef::named(TypeRef::INT))),
);
comparisons.push(
InputObject::new("st_intersects_nband_geom_input")
.description("Intersecting a shape in a given band.")
.field(InputValue::new("nband", TypeRef::named_nn(TypeRef::INT)))
.field(InputValue::new("geommin", TypeRef::named_nn("geometry"))),
);
}
comparisons.extend(inputs);
(comparisons, named)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn a_table_expression_is_named_after_the_table() {
assert_eq!(bool_exp_type_name("author"), "author_bool_exp");
}
#[test]
fn a_comparison_is_named_after_the_scalar() {
assert_eq!(comparison_type_name("String"), "String_comparison_exp");
}
#[test]
fn an_array_column_is_compared_against_its_element_type() {
let list = GraphQLType::List(Box::new(GraphQLType::String));
assert_eq!(scalar_for(&list), "String");
}
#[test]
fn a_type_with_its_own_name_compares_as_itself() {
let custom = GraphQLType::Custom("geography".to_string());
assert_eq!(scalar_for(&custom), "geography");
}
}