use dibs_sql::{ColumnName, ParamName, TableName};
use facet::Facet;
pub use facet_reflect::Span;
use indexmap::IndexMap;
use std::{borrow::Borrow, hash::Hash, ops::Deref};
pub fn query_file_schema() -> String {
normalize_schema_tag_payload_spacing(&facet_styx::schema_from_type::<QueryFile>())
}
pub fn normalize_schema_tag_payload_spacing(schema: &str) -> String {
let mut normalized = String::with_capacity(schema.len());
let mut chars = schema.chars().peekable();
while let Some(ch) = chars.next() {
normalized.push(ch);
if ch != '@' || !matches!(chars.peek(), Some(c) if is_schema_tag_char(*c)) {
continue;
}
while let Some(c) = chars.peek().copied() {
if is_schema_tag_char(c) {
normalized.push(c);
chars.next();
} else {
break;
}
}
let mut whitespace = String::new();
while let Some(c) = chars.peek().copied() {
if c.is_whitespace() {
whitespace.push(c);
chars.next();
} else {
break;
}
}
if matches!(chars.peek(), Some('(')) {
continue;
}
normalized.push_str(&whitespace);
}
normalized
}
fn is_schema_tag_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '-' || ch == '_'
}
#[derive(Debug, Clone, Facet)]
#[facet(metadata_container)]
pub struct Meta<T> {
pub value: T,
#[facet(metadata = "tag")]
pub tag: Option<String>,
#[facet(metadata = "span")]
pub span: Span,
#[facet(metadata = "doc")]
pub doc: Option<Vec<String>>,
}
impl<T: Hash> Hash for Meta<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}
impl<T: PartialEq> PartialEq for Meta<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl<T: Eq> Eq for Meta<T> {}
impl Borrow<str> for Meta<String> {
fn borrow(&self) -> &str {
&self.value
}
}
impl PartialEq<&str> for Meta<String> {
fn eq(&self, other: &&str) -> bool {
self.value == *other
}
}
impl PartialEq<str> for Meta<String> {
fn eq(&self, other: &str) -> bool {
self.value == other
}
}
impl<T> Meta<T> {
pub fn with_span(value: T, span: Span) -> Self {
Self {
value,
span,
doc: None,
tag: None,
}
}
pub fn doc_string(&self) -> Option<String> {
self.doc.as_ref().map(|lines| lines.join("\n"))
}
}
impl Meta<String> {
pub fn as_str(&self) -> &str {
&self.value
}
}
impl<'a> Meta<std::borrow::Cow<'a, str>> {
pub fn as_str(&self) -> &str {
&self.value
}
}
impl<T: Copy> Meta<T> {
pub fn get(&self) -> T {
self.value
}
}
pub trait OptionMetaExt<T> {
fn inner(&self) -> Option<&T>;
fn meta_span(&self) -> Option<Span>;
}
impl<T> OptionMetaExt<T> for Option<Meta<T>> {
fn inner(&self) -> Option<&T> {
self.as_ref().map(|m| &m.value)
}
fn meta_span(&self) -> Option<Span> {
self.as_ref().map(|m| m.span)
}
}
pub trait OptionMetaCopyExt<T: Copy> {
fn value(&self) -> Option<T>;
}
impl<T: Copy> OptionMetaCopyExt<T> for Option<Meta<T>> {
fn value(&self) -> Option<T> {
self.as_ref().map(|m| m.value)
}
}
pub trait OptionMetaDerefExt<T> {
fn value_as_ref(&self) -> Option<&T>;
fn value_as_deref(&self) -> Option<&<T as Deref>::Target>
where
T: Deref;
}
impl<T> OptionMetaDerefExt<T> for Option<Meta<T>> {
fn value_as_ref(&self) -> Option<&T> {
self.as_ref().map(|m| &m.value)
}
fn value_as_deref(&self) -> Option<&<T as Deref>::Target>
where
T: Deref,
{
self.as_ref().map(|m| m.value.deref())
}
}
impl<T> OptionMetaDerefExt<T> for Option<&Meta<T>> {
fn value_as_ref(&self) -> Option<&T> {
self.map(|m| &m.value)
}
fn value_as_deref(&self) -> Option<&<T as Deref>::Target>
where
T: Deref,
{
self.map(|m| m.value.deref())
}
}
impl<T> Deref for Meta<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
#[derive(Debug, Facet)]
#[facet(transparent)]
pub struct QueryFile(pub IndexMap<Meta<String>, Decl>);
#[derive(Debug, Facet)]
#[facet(rename_all = "kebab-case")]
#[repr(u8)]
#[allow(clippy::large_enum_variant)]
pub enum Decl {
Select(Select),
Insert(Insert),
InsertMany(InsertMany),
Upsert(Upsert),
UpsertMany(UpsertMany),
Update(Update),
Delete(Delete),
}
#[derive(Debug, Facet)]
#[facet(rename_all = "kebab-case")]
pub struct Select {
pub params: Option<Params>,
pub from: Option<Meta<TableName>>,
#[facet(rename = "where")]
pub where_clause: Option<Where>,
pub first: Option<Meta<bool>>,
pub distinct: Option<Meta<bool>>,
pub distinct_on: Option<DistinctOn>,
pub order_by: Option<OrderBy>,
pub limit: Option<Meta<String>>,
pub offset: Option<Meta<String>>,
pub fields: Option<SelectFields>,
pub sql: Option<Meta<String>>,
pub returns: Option<Returns>,
}
#[derive(Debug, Facet)]
pub struct Returns {
#[facet(flatten)]
pub fields: IndexMap<Meta<ColumnName>, ParamType>,
}
#[derive(Debug, Facet)]
#[facet(transparent)]
pub struct DistinctOn(pub Vec<Meta<ColumnName>>);
#[derive(Debug, Facet)]
pub struct OrderBy {
#[facet(flatten)]
pub columns: IndexMap<Meta<ColumnName>, Option<Meta<String>>>,
}
#[derive(Debug, Clone, Facet)]
pub struct Where {
#[facet(flatten)]
pub filters: IndexMap<Meta<ColumnName>, FilterValue>,
}
#[derive(Debug, Clone, Facet)]
#[facet(rename_all = "kebab-case")]
#[repr(u8)]
pub enum FilterValue {
Null,
#[facet(rename = "not-null")]
NotNull,
Ilike(Vec<Meta<String>>),
Like(Vec<Meta<String>>),
Gt(Vec<Meta<String>>),
Lt(Vec<Meta<String>>),
Gte(Vec<Meta<String>>),
Lte(Vec<Meta<String>>),
Ne(Vec<Meta<String>>),
In(Vec<Meta<String>>),
JsonGet(Vec<Meta<String>>),
JsonGetText(Vec<Meta<String>>),
Contains(Vec<Meta<String>>),
KeyExists(Vec<Meta<String>>),
Eq(Vec<Meta<String>>),
#[facet(other)]
EqBare(Option<Meta<String>>),
}
#[derive(Debug, Clone, Facet)]
pub struct Params {
#[facet(flatten)]
pub params: IndexMap<Meta<ParamName>, ParamType>,
}
#[derive(Debug, Clone, Facet)]
#[facet(rename_all = "lowercase")]
#[repr(u8)]
pub enum ParamType {
String,
Int,
Float,
Bool,
Uuid,
Decimal,
Timestamp,
Bytes,
Jsonb,
Optional(Vec<ParamType>),
}
#[derive(Debug, Facet)]
#[facet(metadata_container)]
pub struct SelectFields {
#[facet(metadata = "span")]
pub span: Span,
#[facet(flatten)]
pub fields: IndexMap<Meta<ColumnName>, Option<FieldDef>>,
}
#[derive(Debug, Facet)]
#[facet(rename_all = "lowercase")]
#[repr(u8)]
#[allow(clippy::large_enum_variant)]
pub enum FieldDef {
Rel(Relation),
Count(Vec<Meta<TableName>>),
}
#[derive(Debug, Facet)]
#[facet(rename_all = "kebab-case")]
pub struct Relation {
pub from: Option<Meta<TableName>>,
#[facet(rename = "where")]
pub where_clause: Option<Where>,
pub order_by: Option<OrderBy>,
pub first: Option<Meta<bool>>,
pub fields: Option<SelectFields>,
}
#[derive(Debug, Clone, Facet)]
pub struct Insert {
pub params: Option<Params>,
pub into: Meta<TableName>,
pub values: Values,
pub returning: Option<Returning>,
}
#[derive(Debug, Clone, Facet)]
pub struct Upsert {
pub params: Option<Params>,
pub into: Meta<TableName>,
#[facet(rename = "on-conflict")]
pub on_conflict: OnConflict,
pub values: Values,
pub returning: Option<Returning>,
}
#[derive(Debug, Clone, Facet)]
pub struct InsertMany {
pub params: Option<Params>,
pub into: Meta<TableName>,
pub values: Values,
pub returning: Option<Returning>,
}
#[derive(Debug, Clone, Facet)]
pub struct UpsertMany {
pub params: Option<Params>,
pub into: Meta<TableName>,
#[facet(rename = "on-conflict")]
pub on_conflict: OnConflict,
pub values: Values,
pub returning: Option<Returning>,
}
#[derive(Debug, Clone, Facet)]
pub struct Update {
pub params: Option<Params>,
pub table: Meta<TableName>,
pub set: Values,
#[facet(rename = "where")]
pub where_clause: Option<Where>,
pub returning: Option<Returning>,
}
#[derive(Debug, Clone, Facet)]
pub struct Delete {
pub params: Option<Params>,
pub from: Meta<TableName>,
#[facet(rename = "where")]
pub where_clause: Option<Where>,
pub returning: Option<Returning>,
}
#[derive(Debug, Clone, Facet)]
pub struct Values {
#[facet(flatten)]
pub columns: IndexMap<Meta<ColumnName>, Option<ValueExpr>>,
}
#[derive(Debug, Clone, Facet)]
#[facet(untagged)]
#[repr(u8)]
pub enum Payload {
Scalar(Meta<String>),
Seq(Vec<ValueExpr>),
}
#[derive(Debug, Clone, Facet)]
#[facet(rename_all = "lowercase")]
#[repr(u8)]
pub enum ValueExpr {
Default,
#[facet(other)]
Other {
#[facet(tag)]
tag: Option<String>,
#[facet(content)]
content: Option<Payload>,
},
}
#[derive(Debug, Clone, Facet)]
pub struct OnConflict {
pub target: ConflictTarget,
pub update: ConflictUpdate,
}
#[derive(Debug, Clone, Facet)]
pub struct ConflictTarget {
#[facet(flatten)]
pub columns: IndexMap<Meta<ColumnName>, ()>,
}
#[derive(Debug, Clone, Facet)]
pub struct ConflictUpdate {
#[facet(flatten)]
pub columns: IndexMap<Meta<ColumnName>, Option<UpdateValue>>,
}
#[derive(Debug, Clone, Facet)]
#[facet(rename_all = "lowercase")]
#[repr(u8)]
pub enum UpdateValue {
Default,
#[facet(other)]
Other {
#[facet(tag)]
tag: Option<String>,
#[facet(content)]
content: Option<Payload>,
},
}
#[derive(Debug, Clone, Facet)]
pub struct Returning {
#[facet(flatten)]
pub columns: IndexMap<Meta<ColumnName>, ()>,
}
impl Select {
pub fn is_first(&self) -> bool {
self.first.is_some()
}
pub fn has_relations(&self) -> bool {
self.fields
.as_ref()
.map(|select| select.has_relations())
.unwrap_or(false)
}
pub fn has_vec_relations(&self) -> bool {
self.fields
.as_ref()
.map(|select| select.has_vec_relations())
.unwrap_or(false)
}
pub fn has_nested_vec_relations(&self) -> bool {
self.fields
.as_ref()
.map(|select| select.has_nested_vec_relations())
.unwrap_or(false)
}
}
impl SelectFields {
pub fn has_relations(&self) -> bool {
self.fields
.values()
.any(|field_def| matches!(field_def, Some(FieldDef::Rel(_))))
}
pub fn has_vec_relations(&self) -> bool {
self.fields.values().any(|field_def| {
if let Some(FieldDef::Rel(rel)) = field_def {
rel.first.is_none()
} else {
false
}
})
}
pub fn has_nested_vec_relations(&self) -> bool {
for field_def in self.fields.values() {
if let Some(FieldDef::Rel(rel)) = field_def
&& rel.first.is_none()
{
if let Some(rel_select) = &rel.fields
&& (rel_select.has_vec_relations() || rel_select.has_nested_vec_relations())
{
return true;
}
}
}
false
}
pub fn has_count(&self) -> bool {
self.fields
.values()
.any(|field_def| matches!(field_def, Some(FieldDef::Count(_))))
}
pub fn columns(&self) -> impl Iterator<Item = (&Meta<ColumnName>, &Option<FieldDef>)> {
self.fields
.iter()
.filter(|(_, field_def)| field_def.is_none())
}
pub fn relations(&self) -> impl Iterator<Item = (&Meta<ColumnName>, &Relation)> {
self.fields.iter().filter_map(|(name, field_def)| {
if let Some(FieldDef::Rel(rel)) = field_def {
Some((name, rel))
} else {
None
}
})
}
pub fn counts(&self) -> impl Iterator<Item = (&Meta<ColumnName>, &Vec<Meta<TableName>>)> {
self.fields.iter().filter_map(|(name, field_def)| {
if let Some(FieldDef::Count(tables)) = field_def {
Some((name, tables))
} else {
None
}
})
}
pub fn first_column(&self) -> Option<&ColumnName> {
self.fields
.iter()
.find(|(_, field_def)| field_def.is_none())
.map(|(name, _)| &name.value)
}
pub fn id_column(&self) -> Option<&ColumnName> {
self.fields
.iter()
.find(|(name, field_def)| field_def.is_none() && name.value.as_str() == "id")
.map(|(name, _)| &name.value)
.or_else(|| self.first_column())
}
}
impl Relation {
pub fn table_name(&self) -> Option<&str> {
self.from.as_ref().map(|m| m.value.as_str())
}
pub fn is_first(&self) -> bool {
self.first.is_some()
}
pub fn has_relations(&self) -> bool {
self.fields
.as_ref()
.map(|select| select.has_relations())
.unwrap_or(false)
}
pub fn has_vec_relations(&self) -> bool {
self.fields
.as_ref()
.map(|select| select.has_vec_relations())
.unwrap_or(false)
}
}
impl Params {
pub fn iter(&self) -> impl Iterator<Item = (&Meta<ParamName>, &ParamType)> {
self.params.iter()
}
}
#[cfg(test)]
mod tests;