use crate::db::schema::PersistedFieldKind;
const SQL_CAPABILITY_SELECTABLE: u8 = 1 << 0;
const SQL_CAPABILITY_COMPARABLE: u8 = 1 << 1;
const SQL_CAPABILITY_ORDERABLE: u8 = 1 << 2;
const SQL_CAPABILITY_GROUPABLE: u8 = 1 << 3;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct SqlAggregateInputCapabilities {
count: bool,
numeric: bool,
extrema: bool,
}
impl SqlAggregateInputCapabilities {
#[must_use]
const fn new(count: bool, numeric: bool, extrema: bool) -> Self {
Self {
count,
numeric,
extrema,
}
}
#[must_use]
pub(in crate::db) const fn count(self) -> bool {
self.count
}
#[must_use]
pub(in crate::db) const fn numeric(self) -> bool {
self.numeric
}
#[must_use]
pub(in crate::db) const fn extrema(self) -> bool {
self.extrema
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct SqlCapabilities {
flags: u8,
aggregate_input: SqlAggregateInputCapabilities,
}
impl SqlCapabilities {
#[must_use]
const fn new(flags: u8, aggregate_input: SqlAggregateInputCapabilities) -> Self {
Self {
flags,
aggregate_input,
}
}
#[must_use]
pub(in crate::db) const fn selectable(self) -> bool {
self.flags & SQL_CAPABILITY_SELECTABLE != 0
}
#[must_use]
pub(in crate::db) const fn comparable(self) -> bool {
self.flags & SQL_CAPABILITY_COMPARABLE != 0
}
#[must_use]
pub(in crate::db) const fn orderable(self) -> bool {
self.flags & SQL_CAPABILITY_ORDERABLE != 0
}
#[must_use]
pub(in crate::db) const fn groupable(self) -> bool {
self.flags & SQL_CAPABILITY_GROUPABLE != 0
}
#[must_use]
pub(in crate::db) const fn aggregate_input(self) -> SqlAggregateInputCapabilities {
self.aggregate_input
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PersistedSqlScalarFamily {
Boolean,
Numeric { arithmetic: bool },
Text,
OrderedOpaque,
Opaque,
Unit,
}
impl PersistedSqlScalarFamily {
const fn comparable(self) -> bool {
!matches!(self, Self::Unit)
}
const fn orderable(self) -> bool {
matches!(
self,
Self::Boolean | Self::Numeric { .. } | Self::Text | Self::OrderedOpaque
)
}
const fn groupable(self) -> bool {
!matches!(self, Self::Unit)
}
const fn supports_numeric_aggregate(self) -> bool {
matches!(self, Self::Numeric { arithmetic: true })
}
}
#[must_use]
pub(in crate::db) fn sql_capabilities(kind: &PersistedFieldKind) -> SqlCapabilities {
match persisted_sql_scalar_family(kind) {
Some(family) => sql_capabilities_for_scalar_family(family),
None => sql_capabilities_for_non_scalar(kind),
}
}
const fn sql_capabilities_for_scalar_family(family: PersistedSqlScalarFamily) -> SqlCapabilities {
let comparable = family.comparable();
let orderable = family.orderable();
let groupable = family.groupable();
let numeric = family.supports_numeric_aggregate();
let mut flags = SQL_CAPABILITY_SELECTABLE;
if comparable {
flags |= SQL_CAPABILITY_COMPARABLE;
}
if orderable {
flags |= SQL_CAPABILITY_ORDERABLE;
}
if groupable {
flags |= SQL_CAPABILITY_GROUPABLE;
}
SqlCapabilities::new(
flags,
SqlAggregateInputCapabilities::new(comparable, numeric, orderable),
)
}
fn sql_capabilities_for_non_scalar(kind: &PersistedFieldKind) -> SqlCapabilities {
match kind {
PersistedFieldKind::List(_)
| PersistedFieldKind::Set(_)
| PersistedFieldKind::Map { .. } => SqlCapabilities::new(
SQL_CAPABILITY_SELECTABLE,
SqlAggregateInputCapabilities::new(false, false, false),
),
PersistedFieldKind::Structured { queryable } => SqlCapabilities::new(
if *queryable {
SQL_CAPABILITY_SELECTABLE
} else {
0
},
SqlAggregateInputCapabilities::new(false, false, false),
),
PersistedFieldKind::Account
| PersistedFieldKind::Blob
| PersistedFieldKind::Bool
| PersistedFieldKind::Date
| PersistedFieldKind::Decimal { .. }
| PersistedFieldKind::Duration
| PersistedFieldKind::Enum { .. }
| PersistedFieldKind::Float32
| PersistedFieldKind::Float64
| PersistedFieldKind::Int
| PersistedFieldKind::Int128
| PersistedFieldKind::IntBig
| PersistedFieldKind::Principal
| PersistedFieldKind::Subaccount
| PersistedFieldKind::Text { .. }
| PersistedFieldKind::Timestamp
| PersistedFieldKind::Uint
| PersistedFieldKind::Uint128
| PersistedFieldKind::UintBig
| PersistedFieldKind::Ulid
| PersistedFieldKind::Unit
| PersistedFieldKind::Relation { .. } => {
unreachable!(
"scalar persisted field kind should be handled by scalar-family projection"
)
}
}
}
fn persisted_sql_scalar_family(kind: &PersistedFieldKind) -> Option<PersistedSqlScalarFamily> {
match kind {
PersistedFieldKind::Account
| PersistedFieldKind::Date
| PersistedFieldKind::Principal
| PersistedFieldKind::Subaccount
| PersistedFieldKind::Ulid => Some(PersistedSqlScalarFamily::OrderedOpaque),
PersistedFieldKind::Blob => Some(PersistedSqlScalarFamily::Opaque),
PersistedFieldKind::Bool => Some(PersistedSqlScalarFamily::Boolean),
PersistedFieldKind::Decimal { .. }
| PersistedFieldKind::Float32
| PersistedFieldKind::Float64
| PersistedFieldKind::Int
| PersistedFieldKind::Int128
| PersistedFieldKind::IntBig
| PersistedFieldKind::Uint
| PersistedFieldKind::Uint128
| PersistedFieldKind::UintBig => {
Some(PersistedSqlScalarFamily::Numeric { arithmetic: true })
}
PersistedFieldKind::Duration | PersistedFieldKind::Timestamp => {
Some(PersistedSqlScalarFamily::Numeric { arithmetic: false })
}
PersistedFieldKind::Enum { .. } | PersistedFieldKind::Text { .. } => {
Some(PersistedSqlScalarFamily::Text)
}
PersistedFieldKind::Unit => Some(PersistedSqlScalarFamily::Unit),
PersistedFieldKind::Relation { key_kind, .. } => persisted_sql_scalar_family(key_kind),
PersistedFieldKind::List(_)
| PersistedFieldKind::Set(_)
| PersistedFieldKind::Map { .. }
| PersistedFieldKind::Structured { .. } => None,
}
}
#[cfg(test)]
mod tests {
use crate::{
db::schema::{PersistedFieldKind, PersistedRelationStrength},
types::EntityTag,
};
use crate::db::schema::capabilities::sql_capabilities;
fn relation_to_key(key_kind: PersistedFieldKind) -> PersistedFieldKind {
PersistedFieldKind::Relation {
target_path: "target::Entity".into(),
target_entity_name: "Target".into(),
target_entity_tag: EntityTag::new(77),
target_store_path: "target::Store".into(),
key_kind: Box::new(key_kind),
strength: PersistedRelationStrength::Weak,
}
}
#[test]
fn sql_capabilities_keep_blob_selectable_and_comparable_but_not_orderable() {
let capabilities = sql_capabilities(&PersistedFieldKind::Blob);
assert!(capabilities.selectable());
assert!(capabilities.comparable());
assert!(!capabilities.orderable());
assert!(capabilities.groupable());
assert!(capabilities.aggregate_input().count());
assert!(!capabilities.aggregate_input().numeric());
assert!(!capabilities.aggregate_input().extrema());
}
#[test]
fn sql_capabilities_keep_numeric_arithmetic_and_extrema_distinct() {
let amount = sql_capabilities(&PersistedFieldKind::Decimal { scale: 3 });
let timestamp = sql_capabilities(&PersistedFieldKind::Timestamp);
assert!(amount.aggregate_input().numeric());
assert!(amount.aggregate_input().extrema());
assert!(!timestamp.aggregate_input().numeric());
assert!(timestamp.aggregate_input().extrema());
}
#[test]
fn sql_capabilities_reject_collection_and_structured_predicates() {
let list = sql_capabilities(&PersistedFieldKind::List(Box::new(
PersistedFieldKind::Text { max_len: None },
)));
let structured = sql_capabilities(&PersistedFieldKind::Structured { queryable: false });
assert!(list.selectable());
assert!(!list.comparable());
assert!(!list.orderable());
assert!(!list.groupable());
assert!(!structured.selectable());
assert!(!structured.comparable());
}
#[test]
fn sql_capabilities_relation_inherits_key_capabilities() {
let relation = sql_capabilities(&relation_to_key(PersistedFieldKind::Uint));
assert!(relation.selectable());
assert!(relation.comparable());
assert!(relation.orderable());
assert!(relation.aggregate_input().numeric());
}
}