Skip to main content

dibs_query_schema/
lib.rs

1//! Facet types for the dibs query DSL schema.
2//!
3//! These types define the structure of `.styx` query files and can be:
4//! - Deserialized from styx using facet-styx
5//! - Used to generate a styx schema via facet-styx's schema generation
6//! - Used by the LSP extension for diagnostics, hover, go-to-definition
7
8use dibs_sql::{ColumnName, ParamName, TableName};
9use facet::Facet;
10pub use facet_reflect::Span;
11use indexmap::IndexMap;
12use std::{borrow::Borrow, hash::Hash, ops::Deref};
13
14/// Generate a parseable Styx schema for [`QueryFile`].
15pub fn query_file_schema() -> String {
16    normalize_schema_tag_payload_spacing(&facet_styx::schema_from_type::<QueryFile>())
17}
18
19/// Normalize schema strings rendered by facet-styx so tag payloads stay attached.
20///
21/// Styx tag payloads are syntactically significant: `@map(...)` is a tagged
22/// sequence payload, while `@map (...)` is a unit tag followed by a separate
23/// sequence. facet-styx 3.0.x can currently render the latter for generated
24/// schema types, so Dibs normalizes that source boundary before embedding or
25/// round-tripping the schema.
26pub fn normalize_schema_tag_payload_spacing(schema: &str) -> String {
27    let mut normalized = String::with_capacity(schema.len());
28    let mut chars = schema.chars().peekable();
29
30    while let Some(ch) = chars.next() {
31        normalized.push(ch);
32
33        if ch != '@' || !matches!(chars.peek(), Some(c) if is_schema_tag_char(*c)) {
34            continue;
35        }
36
37        while let Some(c) = chars.peek().copied() {
38            if is_schema_tag_char(c) {
39                normalized.push(c);
40                chars.next();
41            } else {
42                break;
43            }
44        }
45
46        let mut whitespace = String::new();
47        while let Some(c) = chars.peek().copied() {
48            if c.is_whitespace() {
49                whitespace.push(c);
50                chars.next();
51            } else {
52                break;
53            }
54        }
55
56        if matches!(chars.peek(), Some('(')) {
57            continue;
58        }
59
60        normalized.push_str(&whitespace);
61    }
62
63    normalized
64}
65
66fn is_schema_tag_char(ch: char) -> bool {
67    ch.is_ascii_alphanumeric() || ch == '-' || ch == '_'
68}
69
70/// A value with source span and documentation.
71///
72/// This struct wraps a value along with:
73/// - Source location (for diagnostics, go-to-definition)
74/// - Doc comments (for hover info)
75#[derive(Debug, Clone, Facet)]
76#[facet(metadata_container)]
77pub struct Meta<T> {
78    /// The wrapped value.
79    pub value: T,
80
81    /// The tag associated to this value if any
82    #[facet(metadata = "tag")]
83    pub tag: Option<String>,
84
85    /// The source span (offset and length).
86    #[facet(metadata = "span")]
87    pub span: Span,
88
89    /// Documentation lines (each line is a separate string).
90    #[facet(metadata = "doc")]
91    pub doc: Option<Vec<String>>,
92}
93
94impl<T: Hash> Hash for Meta<T> {
95    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
96        self.value.hash(state);
97    }
98}
99
100impl<T: PartialEq> PartialEq for Meta<T> {
101    fn eq(&self, other: &Self) -> bool {
102        self.value == other.value
103    }
104}
105
106impl<T: Eq> Eq for Meta<T> {}
107
108impl Borrow<str> for Meta<String> {
109    fn borrow(&self) -> &str {
110        &self.value
111    }
112}
113
114impl PartialEq<&str> for Meta<String> {
115    fn eq(&self, other: &&str) -> bool {
116        self.value == *other
117    }
118}
119
120impl PartialEq<str> for Meta<String> {
121    fn eq(&self, other: &str) -> bool {
122        self.value == other
123    }
124}
125
126impl<T> Meta<T> {
127    /// Create a new spanned value with span information.
128    pub fn with_span(value: T, span: Span) -> Self {
129        Self {
130            value,
131            span,
132            doc: None,
133            tag: None,
134        }
135    }
136
137    /// Get the documentation as a single joined string.
138    pub fn doc_string(&self) -> Option<String> {
139        self.doc.as_ref().map(|lines| lines.join("\n"))
140    }
141}
142
143impl Meta<String> {
144    /// Get the value as a string slice.
145    pub fn as_str(&self) -> &str {
146        &self.value
147    }
148}
149
150impl<'a> Meta<std::borrow::Cow<'a, str>> {
151    /// Get the value as a string slice.
152    pub fn as_str(&self) -> &str {
153        &self.value
154    }
155}
156
157impl<T: Copy> Meta<T> {
158    /// Get the inner value (for Copy types like bool).
159    pub fn get(&self) -> T {
160        self.value
161    }
162}
163
164/// Extension trait for `Option<Meta<T>>` to make access more ergonomic.
165pub trait OptionMetaExt<T> {
166    /// Get the inner value by reference if present.
167    fn inner(&self) -> Option<&T>;
168    /// Get the span if the Meta is present.
169    fn meta_span(&self) -> Option<Span>;
170}
171
172impl<T> OptionMetaExt<T> for Option<Meta<T>> {
173    fn inner(&self) -> Option<&T> {
174        self.as_ref().map(|m| &m.value)
175    }
176
177    fn meta_span(&self) -> Option<Span> {
178        self.as_ref().map(|m| m.span)
179    }
180}
181
182/// Extension trait for `Option<Meta<T>>` where T is Copy (like bool).
183pub trait OptionMetaCopyExt<T: Copy> {
184    /// Get the inner value if present.
185    fn value(&self) -> Option<T>;
186}
187
188impl<T: Copy> OptionMetaCopyExt<T> for Option<Meta<T>> {
189    fn value(&self) -> Option<T> {
190        self.as_ref().map(|m| m.value)
191    }
192}
193
194/// Extension trait for `Option<Meta<T>>` to get references to the inner value.
195pub trait OptionMetaDerefExt<T> {
196    /// Get the inner value by reference if present.
197    fn value_as_ref(&self) -> Option<&T>;
198    /// Get the inner value as its Deref target if present.
199    fn value_as_deref(&self) -> Option<&<T as Deref>::Target>
200    where
201        T: Deref;
202}
203
204impl<T> OptionMetaDerefExt<T> for Option<Meta<T>> {
205    fn value_as_ref(&self) -> Option<&T> {
206        self.as_ref().map(|m| &m.value)
207    }
208
209    fn value_as_deref(&self) -> Option<&<T as Deref>::Target>
210    where
211        T: Deref,
212    {
213        self.as_ref().map(|m| m.value.deref())
214    }
215}
216
217impl<T> OptionMetaDerefExt<T> for Option<&Meta<T>> {
218    fn value_as_ref(&self) -> Option<&T> {
219        self.map(|m| &m.value)
220    }
221
222    fn value_as_deref(&self) -> Option<&<T as Deref>::Target>
223    where
224        T: Deref,
225    {
226        self.map(|m| m.value.deref())
227    }
228}
229
230impl<T> Deref for Meta<T> {
231    type Target = T;
232    fn deref(&self) -> &Self::Target {
233        &self.value
234    }
235}
236
237/// A query file - top level is a map of declaration names to declarations.
238/// Uses `Meta<String>` as keys to capture doc comments from the styx file.
239#[derive(Debug, Facet)]
240#[facet(transparent)]
241pub struct QueryFile(pub IndexMap<Meta<String>, Decl>);
242
243/// A declaration in a query file.
244#[derive(Debug, Facet)]
245#[facet(rename_all = "kebab-case")]
246#[repr(u8)]
247#[allow(clippy::large_enum_variant)]
248pub enum Decl {
249    /// A SELECT query declaration.
250    Select(Select),
251    /// An INSERT declaration.
252    Insert(Insert),
253    /// A bulk INSERT declaration (insert multiple rows).
254    InsertMany(InsertMany),
255    /// An UPSERT declaration.
256    Upsert(Upsert),
257    /// A bulk UPSERT declaration (upsert multiple rows).
258    UpsertMany(UpsertMany),
259    /// An UPDATE declaration.
260    Update(Update),
261    /// A DELETE declaration.
262    Delete(Delete),
263}
264
265/// A SELECT query definition.
266///
267/// Can be either a structured query (with `from` and `select`) or a raw SQL query
268/// (with `sql` and `returns`).
269#[derive(Debug, Facet)]
270#[facet(rename_all = "kebab-case")]
271pub struct Select {
272    /// Query parameters.
273    pub params: Option<Params>,
274
275    /// Source table to query from (for structured queries).
276    pub from: Option<Meta<TableName>>,
277
278    /// Filter conditions.
279    #[facet(rename = "where")]
280    pub where_clause: Option<Where>,
281
282    /// Return only the first result.
283    pub first: Option<Meta<bool>>,
284
285    /// Use DISTINCT to return only unique rows.
286    pub distinct: Option<Meta<bool>>,
287
288    /// DISTINCT ON clause (PostgreSQL-specific) - return first row of each group.
289    pub distinct_on: Option<DistinctOn>,
290
291    /// Order by clause.
292    pub order_by: Option<OrderBy>,
293
294    /// Limit clause (number or param reference like $limit).
295    pub limit: Option<Meta<String>>,
296
297    /// Offset clause (number or param reference like $offset).
298    pub offset: Option<Meta<String>>,
299
300    /// Fields to select (for structured queries).
301    pub fields: Option<SelectFields>,
302
303    /// Raw SQL query string (for raw SQL queries).
304    pub sql: Option<Meta<String>>,
305
306    /// Return type specification (for raw SQL queries).
307    pub returns: Option<Returns>,
308}
309
310/// Return type specification for raw SQL queries.
311#[derive(Debug, Facet)]
312pub struct Returns {
313    #[facet(flatten)]
314    pub fields: IndexMap<Meta<ColumnName>, ParamType>,
315}
316
317/// DISTINCT ON clause (PostgreSQL-specific) - a sequence of column names.
318#[derive(Debug, Facet)]
319#[facet(transparent)]
320pub struct DistinctOn(pub Vec<Meta<ColumnName>>);
321
322/// ORDER BY clause.
323#[derive(Debug, Facet)]
324pub struct OrderBy {
325    /// Column name -> direction ("asc" or "desc", None means asc)
326    #[facet(flatten)]
327    pub columns: IndexMap<Meta<ColumnName>, Option<Meta<String>>>,
328}
329
330/// WHERE clause - filter conditions.
331#[derive(Debug, Clone, Facet)]
332pub struct Where {
333    #[facet(flatten)]
334    pub filters: IndexMap<Meta<ColumnName>, FilterValue>,
335}
336
337/// A filter value - tagged operators or bare scalars for where clauses.
338///
339/// Tagged operators:
340/// - `@null` for IS NULL
341/// - `@not_null` for IS NOT NULL
342/// - `@ilike($param)` or `@ilike("pattern")` for case-insensitive LIKE
343/// - `@like`, `@gt`, `@lt`, `@gte`, `@lte`, `@ne` for comparison operators
344/// - `@in($param)` for `= ANY($1)` (array containment)
345/// - `@json-get($param)` for JSONB `->` operator (get JSON object)
346/// - `@json-get-text($param)` for JSONB `->>` operator (get JSON value as text)
347/// - `@contains($param)` for `@>` operator (contains, typically JSONB)
348/// - `@key-exists($param)` for `?` operator (key exists, typically JSONB)
349///
350/// Bare scalars (like `$handle`) are treated as equality filters via `#[facet(other)]`.
351#[derive(Debug, Clone, Facet)]
352#[facet(rename_all = "kebab-case")]
353#[repr(u8)]
354pub enum FilterValue {
355    /// NULL check (@null)
356    Null,
357    /// NOT NULL check (@not-null)
358    #[facet(rename = "not-null")]
359    NotNull,
360    /// ILIKE pattern matching (@ilike($param) or @ilike("pattern"))
361    Ilike(Vec<Meta<String>>),
362    /// LIKE pattern matching (@like($param) or @like("pattern"))
363    Like(Vec<Meta<String>>),
364    /// Greater than (@gt($param) or @gt(value))
365    Gt(Vec<Meta<String>>),
366    /// Less than (@lt($param) or @lt(value))
367    Lt(Vec<Meta<String>>),
368    /// Greater than or equal (@gte($param) or @gte(value))
369    Gte(Vec<Meta<String>>),
370    /// Less than or equal (@lte($param) or @lte(value))
371    Lte(Vec<Meta<String>>),
372    /// Not equal (@ne($param) or @ne(value))
373    Ne(Vec<Meta<String>>),
374    /// IN array check (@in($param)) - param should be an array type
375    In(Vec<Meta<String>>),
376    /// JSONB get object operator (@json_get($param)) -> `column -> $param`
377    JsonGet(Vec<Meta<String>>),
378    /// JSONB get text operator (@json_get_text($param)) -> `column ->> $param`
379    JsonGetText(Vec<Meta<String>>),
380    /// Contains operator (@contains($param)) -> `column @> $param`
381    Contains(Vec<Meta<String>>),
382    /// Key exists operator (@key_exists($param)) -> `column ? $param`
383    KeyExists(Vec<Meta<String>>),
384    /// Explicit equality (@eq($param) or @eq(value))
385    Eq(Vec<Meta<String>>),
386    /// Equality - bare scalar fallback (e.g., `$handle` or `"value"`)
387    #[facet(other)]
388    EqBare(Option<Meta<String>>),
389}
390
391/// Query parameters.
392#[derive(Debug, Clone, Facet)]
393pub struct Params {
394    #[facet(flatten)]
395    pub params: IndexMap<Meta<ParamName>, ParamType>,
396}
397
398/// Parameter type.
399#[derive(Debug, Clone, Facet)]
400#[facet(rename_all = "lowercase")]
401#[repr(u8)]
402pub enum ParamType {
403    String,
404    Int,
405    /// `f64` / `DOUBLE PRECISION`.
406    Float,
407    Bool,
408    Uuid,
409    Decimal,
410    Timestamp,
411    Bytes,
412    /// JSONB column. At the wire level the param is a JSON-encoded
413    /// `String`; sqlgen casts it to `jsonb` at the binding site
414    /// (`$N::jsonb`) so PG validates the body on insert.
415    Jsonb,
416    /// Optional type: @optional(@string) -> Optional(vec![String])
417    Optional(Vec<ParamType>),
418}
419
420/// SELECT clause.
421#[derive(Debug, Facet)]
422#[facet(metadata_container)]
423pub struct SelectFields {
424    /// Source span of the select block.
425    #[facet(metadata = "span")]
426    pub span: Span,
427
428    #[facet(flatten)]
429    pub fields: IndexMap<Meta<ColumnName>, Option<FieldDef>>,
430}
431
432/// A field definition - tagged values in select.
433#[derive(Debug, Facet)]
434#[facet(rename_all = "lowercase")]
435#[repr(u8)]
436#[allow(clippy::large_enum_variant)]
437pub enum FieldDef {
438    /// A relation field (`@rel{...}`).
439    Rel(Relation),
440    /// A count aggregation (`@count(table_name)`).
441    Count(Vec<Meta<TableName>>),
442}
443
444/// A relation definition (nested query on related table).
445#[derive(Debug, Facet)]
446#[facet(rename_all = "kebab-case")]
447pub struct Relation {
448    /// Optional explicit table name.
449    pub from: Option<Meta<TableName>>,
450
451    /// Filter conditions.
452    #[facet(rename = "where")]
453    pub where_clause: Option<Where>,
454
455    /// Order by clause.
456    pub order_by: Option<OrderBy>,
457
458    /// Return only the first result.
459    pub first: Option<Meta<bool>>,
460
461    /// Fields to select from the relation.
462    pub fields: Option<SelectFields>,
463}
464
465/// An INSERT declaration.
466#[derive(Debug, Clone, Facet)]
467pub struct Insert {
468    /// Query parameters.
469    pub params: Option<Params>,
470    /// Target table.
471    pub into: Meta<TableName>,
472    /// Values to insert (column -> value expression).
473    pub values: Values,
474    /// Columns to return.
475    pub returning: Option<Returning>,
476}
477
478/// An UPSERT declaration (INSERT ... ON CONFLICT ... DO UPDATE).
479#[derive(Debug, Clone, Facet)]
480pub struct Upsert {
481    /// Query parameters.
482    pub params: Option<Params>,
483    /// Target table.
484    pub into: Meta<TableName>,
485    /// ON CONFLICT clause.
486    #[facet(rename = "on-conflict")]
487    pub on_conflict: OnConflict,
488    /// Values to insert (column -> value expression).
489    pub values: Values,
490    /// Columns to return.
491    pub returning: Option<Returning>,
492}
493
494/// A bulk INSERT declaration (insert multiple rows with a single query).
495///
496/// Uses PostgreSQL's UNNEST to insert multiple rows efficiently with constant SQL.
497///
498/// Example:
499/// ```styx
500/// BulkCreateProducts @insert-many{
501///   params {handle @string, status @string}
502///   into products
503///   values {handle, status, created_at @now}
504///   returning {id, handle, status}
505/// }
506/// ```
507#[derive(Debug, Clone, Facet)]
508pub struct InsertMany {
509    /// Query parameters - each becomes an array parameter.
510    pub params: Option<Params>,
511    /// Target table.
512    pub into: Meta<TableName>,
513    /// Values to insert (column -> value expression).
514    /// Params become UNNEST columns, other expressions are applied to each row.
515    pub values: Values,
516    /// Columns to return.
517    pub returning: Option<Returning>,
518}
519
520/// A bulk UPSERT declaration (upsert multiple rows with a single query).
521///
522/// Uses PostgreSQL's UNNEST with ON CONFLICT for efficient bulk upserts.
523///
524/// Example:
525/// ```styx
526/// BulkUpsertProducts @upsert-many{
527///   params {handle @string, status @string}
528///   into products
529///   on-conflict {
530///     target {handle}
531///     update {status, updated_at @now}
532///   }
533///   values {handle, status, created_at @now}
534///   returning {id, handle, status}
535/// }
536/// ```
537#[derive(Debug, Clone, Facet)]
538pub struct UpsertMany {
539    /// Query parameters - each becomes an array parameter.
540    pub params: Option<Params>,
541    /// Target table.
542    pub into: Meta<TableName>,
543    /// ON CONFLICT clause.
544    #[facet(rename = "on-conflict")]
545    pub on_conflict: OnConflict,
546    /// Values to insert (column -> value expression).
547    pub values: Values,
548    /// Columns to return.
549    pub returning: Option<Returning>,
550}
551
552/// An UPDATE declaration.
553#[derive(Debug, Clone, Facet)]
554pub struct Update {
555    /// Query parameters.
556    pub params: Option<Params>,
557    /// Target table.
558    pub table: Meta<TableName>,
559    /// Values to set (column -> value expression).
560    pub set: Values,
561    /// Filter conditions.
562    #[facet(rename = "where")]
563    pub where_clause: Option<Where>,
564    /// Columns to return.
565    pub returning: Option<Returning>,
566}
567
568/// A DELETE declaration.
569#[derive(Debug, Clone, Facet)]
570pub struct Delete {
571    /// Query parameters.
572    pub params: Option<Params>,
573    /// Target table.
574    pub from: Meta<TableName>,
575    /// Filter conditions.
576    #[facet(rename = "where")]
577    pub where_clause: Option<Where>,
578    /// Columns to return.
579    pub returning: Option<Returning>,
580}
581
582/// Values clause for INSERT/UPDATE.
583#[derive(Debug, Clone, Facet)]
584pub struct Values {
585    /// Column name -> value expression. None means use param with same name ($column_name).
586    #[facet(flatten)]
587    pub columns: IndexMap<Meta<ColumnName>, Option<ValueExpr>>,
588}
589
590/// Payload of a value expression - can be scalar or sequence.
591#[derive(Debug, Clone, Facet)]
592#[facet(untagged)]
593#[repr(u8)]
594pub enum Payload {
595    /// Scalar payload (for bare values like $name)
596    Scalar(Meta<String>),
597    /// Sequence payload (for functions with args like @coalesce($a $b))
598    Seq(Vec<ValueExpr>),
599}
600
601/// A value expression in INSERT/UPDATE.
602///
603/// Special cases:
604/// - `@default` - the DEFAULT keyword
605/// - `@funcname` or `@funcname(args...)` - SQL function calls like NOW(), COALESCE(), etc.
606/// - Bare scalars - parameter references ($name) or literals
607#[derive(Debug, Clone, Facet)]
608#[facet(rename_all = "lowercase")]
609#[repr(u8)]
610pub enum ValueExpr {
611    /// Default value (@default).
612    Default,
613    /// Everything else: functions and bare scalars.
614    /// - Bare scalars: tag=None, content=Some(Scalar(...))
615    /// - Nullary functions: tag=Some("name"), content=None
616    /// - Functions with args: tag=Some("name"), content=Some(Seq(...))
617    #[facet(other)]
618    Other {
619        #[facet(tag)]
620        tag: Option<String>,
621        #[facet(content)]
622        content: Option<Payload>,
623    },
624}
625
626/// ON CONFLICT clause for UPSERT.
627#[derive(Debug, Clone, Facet)]
628pub struct OnConflict {
629    /// Target columns for conflict detection.
630    pub target: ConflictTarget,
631    /// Columns to update on conflict.
632    pub update: ConflictUpdate,
633}
634
635/// Conflict target columns.
636#[derive(Debug, Clone, Facet)]
637pub struct ConflictTarget {
638    #[facet(flatten)]
639    pub columns: IndexMap<Meta<ColumnName>, ()>,
640}
641
642/// Columns to update on conflict.
643#[derive(Debug, Clone, Facet)]
644pub struct ConflictUpdate {
645    #[facet(flatten)]
646    pub columns: IndexMap<Meta<ColumnName>, Option<UpdateValue>>,
647}
648
649/// Value for an update column - mirrors `ValueExpr`.
650#[derive(Debug, Clone, Facet)]
651#[facet(rename_all = "lowercase")]
652#[repr(u8)]
653pub enum UpdateValue {
654    /// Default value (@default).
655    Default,
656    /// Everything else: functions and bare scalars.
657    #[facet(other)]
658    Other {
659        #[facet(tag)]
660        tag: Option<String>,
661        #[facet(content)]
662        content: Option<Payload>,
663    },
664}
665
666/// RETURNING clause.
667#[derive(Debug, Clone, Facet)]
668pub struct Returning {
669    #[facet(flatten)]
670    pub columns: IndexMap<Meta<ColumnName>, ()>,
671}
672
673// ============================================================================
674// CONVENIENCE METHODS FOR SCHEMA TYPES
675// ============================================================================
676
677impl Select {
678    /// Check if this query returns only the first result.
679    pub fn is_first(&self) -> bool {
680        self.first.is_some()
681    }
682
683    /// Check if this query has any relations in its select clause.
684    pub fn has_relations(&self) -> bool {
685        self.fields
686            .as_ref()
687            .map(|select| select.has_relations())
688            .unwrap_or(false)
689    }
690
691    /// Check if this query has any Vec (has-many) relations.
692    pub fn has_vec_relations(&self) -> bool {
693        self.fields
694            .as_ref()
695            .map(|select| select.has_vec_relations())
696            .unwrap_or(false)
697    }
698
699    /// Check if this query has nested Vec relations (Vec containing Vec).
700    pub fn has_nested_vec_relations(&self) -> bool {
701        self.fields
702            .as_ref()
703            .map(|select| select.has_nested_vec_relations())
704            .unwrap_or(false)
705    }
706}
707
708impl SelectFields {
709    /// Check if this select has any relations.
710    pub fn has_relations(&self) -> bool {
711        self.fields
712            .values()
713            .any(|field_def| matches!(field_def, Some(FieldDef::Rel(_))))
714    }
715
716    /// Check if this select has any Vec (has-many) relations.
717    pub fn has_vec_relations(&self) -> bool {
718        self.fields.values().any(|field_def| {
719            if let Some(FieldDef::Rel(rel)) = field_def {
720                rel.first.is_none()
721            } else {
722                false
723            }
724        })
725    }
726
727    /// Check if this select has nested Vec relations.
728    pub fn has_nested_vec_relations(&self) -> bool {
729        for field_def in self.fields.values() {
730            if let Some(FieldDef::Rel(rel)) = field_def
731                && rel.first.is_none()
732            {
733                // This is a Vec relation
734                if let Some(rel_select) = &rel.fields
735                    && (rel_select.has_vec_relations() || rel_select.has_nested_vec_relations())
736                {
737                    return true;
738                }
739            }
740        }
741        false
742    }
743
744    /// Check if this select has any count aggregations.
745    pub fn has_count(&self) -> bool {
746        self.fields
747            .values()
748            .any(|field_def| matches!(field_def, Some(FieldDef::Count(_))))
749    }
750
751    /// Iterate over simple columns (fields with None FieldDef).
752    pub fn columns(&self) -> impl Iterator<Item = (&Meta<ColumnName>, &Option<FieldDef>)> {
753        self.fields
754            .iter()
755            .filter(|(_, field_def)| field_def.is_none())
756    }
757
758    /// Iterate over relations (fields with Some(FieldDef::Rel(_))).
759    pub fn relations(&self) -> impl Iterator<Item = (&Meta<ColumnName>, &Relation)> {
760        self.fields.iter().filter_map(|(name, field_def)| {
761            if let Some(FieldDef::Rel(rel)) = field_def {
762                Some((name, rel))
763            } else {
764                None
765            }
766        })
767    }
768
769    /// Iterate over count aggregations (fields with Some(FieldDef::Count(_))).
770    pub fn counts(&self) -> impl Iterator<Item = (&Meta<ColumnName>, &Vec<Meta<TableName>>)> {
771        self.fields.iter().filter_map(|(name, field_def)| {
772            if let Some(FieldDef::Count(tables)) = field_def {
773                Some((name, tables))
774            } else {
775                None
776            }
777        })
778    }
779
780    /// Get the first column name (first simple column, not a relation).
781    /// Returns None if there are no simple columns.
782    pub fn first_column(&self) -> Option<&ColumnName> {
783        self.fields
784            .iter()
785            .find(|(_, field_def)| field_def.is_none())
786            .map(|(name, _)| &name.value)
787    }
788
789    /// Get the ID column name (column named "id", or first column as fallback).
790    /// Returns None if there are no simple columns.
791    pub fn id_column(&self) -> Option<&ColumnName> {
792        // First try to find a column named "id"
793        self.fields
794            .iter()
795            .find(|(name, field_def)| field_def.is_none() && name.value.as_str() == "id")
796            .map(|(name, _)| &name.value)
797            .or_else(|| self.first_column())
798    }
799}
800
801impl Relation {
802    /// Get the table name for this relation.
803    /// Returns the explicit `from` table if set, otherwise returns None
804    /// (caller should use the relation field name as fallback).
805    pub fn table_name(&self) -> Option<&str> {
806        self.from.as_ref().map(|m| m.value.as_str())
807    }
808
809    /// Check if this relation is a single result (first).
810    pub fn is_first(&self) -> bool {
811        self.first.is_some()
812    }
813
814    /// Check if this relation has any nested relations.
815    pub fn has_relations(&self) -> bool {
816        self.fields
817            .as_ref()
818            .map(|select| select.has_relations())
819            .unwrap_or(false)
820    }
821
822    /// Check if this relation has any Vec (has-many) nested relations.
823    pub fn has_vec_relations(&self) -> bool {
824        self.fields
825            .as_ref()
826            .map(|select| select.has_vec_relations())
827            .unwrap_or(false)
828    }
829}
830
831impl Params {
832    /// Iterate over parameters by name and type.
833    pub fn iter(&self) -> impl Iterator<Item = (&Meta<ParamName>, &ParamType)> {
834        self.params.iter()
835    }
836}
837
838#[cfg(test)]
839mod tests;