halo_space/
structs.rs

1//! Struct: lightweight ORM-style helpers for table structs.
2//!
3//! Without runtime reflection (and avoiding extra proc-macro crates), this uses `macro_rules!`
4//! to generate field metadata and getters, providing an experience close to reflective builders.
5
6use crate::delete::DeleteBuilder;
7use crate::escape_all;
8use crate::field_mapper::{FieldMapperFunc, default_field_mapper};
9use crate::flavor::Flavor;
10use crate::insert::InsertBuilder;
11use crate::select::SelectBuilder;
12use crate::select_cols;
13use crate::update::UpdateBuilder;
14use std::any::Any;
15use std::collections::HashSet;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FieldOpt {
19    WithQuote,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct FieldMeta {
24    /// Rust field name (used to generate value accessors).
25    pub rust: &'static str,
26    /// Original field name used by the FieldMapper (macro-generated; defaults to `rust` but can override CamelCase).
27    pub orig: &'static str,
28    /// SQL column name (after db tag/mapper).
29    pub db: &'static str,
30    /// Optional alias (AS).
31    pub as_: Option<&'static str>,
32    /// Tags.
33    pub tags: &'static [&'static str],
34    /// Omitempty tags (include "" for default).
35    pub omitempty_tags: &'static [&'static str],
36    pub with_quote: bool,
37}
38
39impl FieldMeta {
40    pub fn name_for_select(&self, flavor: Flavor, alias: &str) -> String {
41        let base = if self.with_quote {
42            flavor.quote(alias)
43        } else {
44            alias.to_string()
45        };
46        if let Some(as_) = self.as_ {
47            format!("{base} AS {as_}")
48        } else {
49            base
50        }
51    }
52}
53
54fn is_ignored(fm: &FieldMeta) -> bool {
55    // Honor `db:"-"`: skip this field.
56    fm.db == "-"
57}
58
59/// Trait implemented by the macro for your structs: exposes metadata, values, and emptiness checks.
60pub trait SqlStruct: Sized {
61    const FIELDS: &'static [FieldMeta];
62
63    /// Extract field values for INSERT/UPDATE (in FIELDS order).
64    fn values(&self) -> Vec<crate::modifiers::Arg>;
65
66    /// Check whether a field is "empty" (used for omitempty).
67    fn is_empty_field(&self, rust_field: &'static str) -> bool;
68
69    /// Return writable scan targets for `Struct::addr*`.
70    ///
71    /// Note: builds all `ScanCell`s at once (internally holds raw pointers) to avoid borrow-checker conflicts.
72    fn addr_cells<'a>(
73        &'a mut self,
74        rust_fields: &[&'static str],
75    ) -> Option<Vec<crate::scan::ScanCell<'a>>>;
76}
77
78/// Trait to determine "empty" values (implements omitempty semantics).
79pub trait IsEmpty {
80    fn is_empty_value(&self) -> bool;
81}
82
83impl IsEmpty for String {
84    fn is_empty_value(&self) -> bool {
85        self.is_empty()
86    }
87}
88
89impl IsEmpty for &str {
90    fn is_empty_value(&self) -> bool {
91        self.is_empty()
92    }
93}
94
95impl IsEmpty for bool {
96    fn is_empty_value(&self) -> bool {
97        !*self
98    }
99}
100
101macro_rules! empty_num {
102    ($($t:ty),+ $(,)?) => {
103        $(impl IsEmpty for $t {
104            fn is_empty_value(&self) -> bool {
105                *self == 0 as $t
106            }
107        })+
108    };
109}
110
111empty_num!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
112
113impl IsEmpty for f64 {
114    fn is_empty_value(&self) -> bool {
115        // Use bits to check zero to avoid -0.0 edge cases.
116        self.to_bits() == 0
117    }
118}
119
120impl<T: IsEmpty> IsEmpty for Option<T> {
121    fn is_empty_value(&self) -> bool {
122        match self {
123            None => true,
124            Some(v) => v.is_empty_value(),
125        }
126    }
127}
128
129impl<T> IsEmpty for Vec<T> {
130    fn is_empty_value(&self) -> bool {
131        self.is_empty()
132    }
133}
134
135impl IsEmpty for Box<dyn crate::valuer::SqlValuer> {
136    fn is_empty_value(&self) -> bool {
137        // Mirror pointer semantics: non-null pointer is not empty.
138        false
139    }
140}
141
142pub struct Struct<T: SqlStruct> {
143    pub flavor: Flavor,
144    mapper: FieldMapperFunc,
145    with_tags: Vec<&'static str>,
146    without_tags: Vec<&'static str>,
147    _phantom: std::marker::PhantomData<T>,
148}
149
150impl<T: SqlStruct> Clone for Struct<T> {
151    fn clone(&self) -> Self {
152        Self {
153            flavor: self.flavor,
154            mapper: self.mapper.clone(),
155            with_tags: self.with_tags.clone(),
156            without_tags: self.without_tags.clone(),
157            _phantom: std::marker::PhantomData,
158        }
159    }
160}
161
162impl<T: SqlStruct> std::fmt::Debug for Struct<T> {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        // Mapper cannot derive Debug; print key fields only.
165        f.debug_struct("Struct")
166            .field("flavor", &self.flavor)
167            .field("with_tags", &self.with_tags)
168            .field("without_tags", &self.without_tags)
169            .finish()
170    }
171}
172
173impl<T: SqlStruct> Default for Struct<T> {
174    fn default() -> Self {
175        Self {
176            flavor: crate::default_flavor(),
177            mapper: default_field_mapper(),
178            with_tags: Vec::new(),
179            without_tags: Vec::new(),
180            _phantom: std::marker::PhantomData,
181        }
182    }
183}
184
185impl<T: SqlStruct> Struct<T> {
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// WithFieldMapper: return a shadow copy with a different mapper.
191    ///
192    /// - Passing `identity_mapper()` matches the effect of a nil mapper.
193    pub fn with_field_mapper(&self, mapper: FieldMapperFunc) -> Self {
194        let mut c = self.clone();
195        c.mapper = mapper;
196        c
197    }
198
199    fn has_defined_tag(tag: &str) -> bool {
200        if tag.is_empty() {
201            return false;
202        }
203        T::FIELDS
204            .iter()
205            .any(|f| !is_ignored(f) && f.tags.contains(&tag))
206    }
207
208    /// ForFlavor: return a shadow copy with a different flavor.
209    pub fn for_flavor(&self, flavor: Flavor) -> Self {
210        let mut c = self.clone();
211        c.flavor = flavor;
212        c
213    }
214
215    /// WithTag: return a shadow copy with additional tags.
216    pub fn with_tag(&self, tags: impl IntoIterator<Item = &'static str>) -> Self {
217        let mut c = self.clone();
218        for t in tags {
219            if t.is_empty() {
220                continue;
221            }
222            if !c.with_tags.contains(&t) {
223                c.with_tags.push(t);
224            }
225        }
226        c.with_tags.sort_unstable();
227        c.with_tags.dedup();
228        c
229    }
230
231    /// WithoutTag: return a shadow copy excluding specific tags.
232    pub fn without_tag(&self, tags: impl IntoIterator<Item = &'static str>) -> Self {
233        let mut c = self.clone();
234        for t in tags {
235            if t.is_empty() {
236                continue;
237            }
238            if !c.without_tags.contains(&t) {
239                c.without_tags.push(t);
240            }
241        }
242        c.without_tags.sort_unstable();
243        c.without_tags.dedup();
244        // Filter out excluded with_tags entries.
245        c.with_tags.retain(|t| !c.without_tags.contains(t));
246        c
247    }
248
249    fn should_omit_empty(&self, fm: &FieldMeta) -> bool {
250        // Omit-empty rules:
251        // - default tag ""
252        // - then any with_tags
253        let omit = fm.omitempty_tags;
254        if omit.is_empty() {
255            return false;
256        }
257        if omit.contains(&"") {
258            return true;
259        }
260        self.with_tags.iter().any(|t| omit.contains(t))
261    }
262
263    fn excluded_by_without(&self, fm: &FieldMeta) -> bool {
264        self.without_tags.iter().any(|t| fm.tags.contains(t))
265    }
266
267    fn alias_of(&self, fm: &FieldMeta) -> String {
268        if is_ignored(fm) {
269            return String::new();
270        }
271
272        if !fm.db.is_empty() {
273            return fm.db.to_string();
274        }
275
276        let mapped = (self.mapper)(fm.orig);
277        if mapped.is_empty() {
278            fm.orig.to_string()
279        } else {
280            mapped
281        }
282    }
283
284    fn read_key_of(&self, fm: &FieldMeta) -> String {
285        // Key preference: AS alias, otherwise mapped alias, otherwise rust name.
286        if let Some(as_) = fm.as_ {
287            return as_.to_string();
288        }
289        let a = self.alias_of(fm);
290        if a.is_empty() { fm.rust.to_string() } else { a }
291    }
292
293    fn write_key_of(&self, fm: &FieldMeta) -> String {
294        // For writes: deduplicate by alias.
295        let a = self.alias_of(fm);
296        if a.is_empty() { fm.rust.to_string() } else { a }
297    }
298
299    fn fields_for_read(&self) -> Vec<&'static FieldMeta> {
300        self.fields_filtered(true)
301    }
302
303    fn fields_for_write(&self) -> Vec<&'static FieldMeta> {
304        self.fields_filtered(false)
305    }
306
307    fn fields_filtered(&self, for_read: bool) -> Vec<&'static FieldMeta> {
308        let mut out = Vec::new();
309        let mut seen = HashSet::<String>::new();
310
311        let push_field = |out: &mut Vec<&'static FieldMeta>,
312                          seen: &mut HashSet<String>,
313                          fm: &'static FieldMeta,
314                          for_read: bool| {
315            if is_ignored(fm) {
316                return;
317            }
318            if self.excluded_by_without(fm) {
319                return;
320            }
321            let key = if for_read {
322                self.read_key_of(fm)
323            } else {
324                self.write_key_of(fm)
325            };
326            if seen.insert(key) {
327                out.push(fm);
328            }
329        };
330
331        if self.with_tags.is_empty() {
332            for fm in T::FIELDS {
333                push_field(&mut out, &mut seen, fm, for_read);
334            }
335            return out;
336        }
337
338        // Filter by with_tags in order (deduplicated).
339        for tag in &self.with_tags {
340            for fm in T::FIELDS {
341                if fm.tags.contains(tag) {
342                    push_field(&mut out, &mut seen, fm, for_read);
343                }
344            }
345        }
346
347        out
348    }
349
350    fn parse_table_alias(table: &str) -> &str {
351        // Match Go behavior: take token after the last space.
352        table.rsplit_once(' ').map(|(_, a)| a).unwrap_or(table)
353    }
354
355    /// Columns: return unquoted column names for write.
356    pub fn columns(&self) -> Vec<String> {
357        self.fields_for_write()
358            .into_iter()
359            .map(|f| self.alias_of(f))
360            .collect()
361    }
362
363    /// ColumnsForTag: return columns for a specific tag; None if tag not defined.
364    pub fn columns_for_tag(&self, tag: &str) -> Option<Vec<String>> {
365        if !Self::has_defined_tag(tag) {
366            return None;
367        }
368        // API constraint: requires &'static str; we leak for convenience. Could switch to Cow later.
369        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
370        Some(self.with_tag([tag]).columns())
371    }
372
373    /// Values: return values for write in the same order as `columns()`.
374    pub fn values(&self, v: &T) -> Vec<crate::modifiers::Arg> {
375        let all = v.values();
376        let mut out = Vec::new();
377        for (fm, arg) in T::FIELDS.iter().zip(all) {
378            if is_ignored(fm) || self.excluded_by_without(fm) {
379                continue;
380            }
381            if self.with_tags.is_empty() || self.with_tags.iter().any(|t| fm.tags.contains(t)) {
382                out.push(arg);
383            }
384        }
385        // Note: initial order follows declaration, not tag grouping; reorder with fields_for_write to dedupe by tag order.
386        let mut map = std::collections::HashMap::<&'static str, crate::modifiers::Arg>::new();
387        for (fm, arg) in T::FIELDS.iter().zip(v.values()) {
388            map.insert(fm.rust, arg);
389        }
390        self.fields_for_write()
391            .into_iter()
392            .filter_map(|fm| map.get(fm.rust).cloned())
393            .collect()
394    }
395
396    /// ValuesForTag: values restricted to a tag; None if tag not defined.
397    pub fn values_for_tag(&self, tag: &str, v: &T) -> Option<Vec<crate::modifiers::Arg>> {
398        if !Self::has_defined_tag(tag) {
399            return None;
400        }
401        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
402        Some(self.with_tag([tag]).values(v))
403    }
404
405    /// ForeachRead: iterate readable fields.
406    ///
407    /// - `dbtag`: db tag (may be empty)
408    /// - `is_quoted`: whether the column needs quoting
409    /// - `field_meta`: Rust-side metadata instead of reflect.StructField
410    pub fn foreach_read(&self, mut trans: impl FnMut(&str, bool, &FieldMeta)) {
411        for fm in self.fields_for_read() {
412            trans(fm.db, fm.with_quote, fm);
413        }
414    }
415
416    /// ForeachWrite: iterate writable fields.
417    pub fn foreach_write(&self, mut trans: impl FnMut(&str, bool, &FieldMeta)) {
418        for fm in self.fields_for_write() {
419            trans(fm.db, fm.with_quote, fm);
420        }
421    }
422
423    /// Addr: return scan targets for readable fields.
424    pub fn addr<'a>(&self, st: &'a mut T) -> Vec<crate::scan::ScanCell<'a>> {
425        let rust_fields: Vec<&'static str> = self
426            .fields_for_read()
427            .into_iter()
428            .map(|fm| fm.rust)
429            .collect();
430        st.addr_cells(&rust_fields).unwrap_or_default()
431    }
432
433    /// AddrForTag: scan targets filtered by tag; None if tag not defined.
434    pub fn addr_for_tag<'a>(
435        &self,
436        tag: &str,
437        st: &'a mut T,
438    ) -> Option<Vec<crate::scan::ScanCell<'a>>> {
439        if !Self::has_defined_tag(tag) {
440            return None;
441        }
442        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
443        Some(self.with_tag([tag]).addr(st))
444    }
445
446    /// AddrWithCols: scan targets for specific columns; None if any column is missing.
447    pub fn addr_with_cols<'a>(
448        &self,
449        cols: &[&str],
450        st: &'a mut T,
451    ) -> Option<Vec<crate::scan::ScanCell<'a>>> {
452        let fields = self.fields_for_read();
453        let mut map = std::collections::HashMap::<String, &'static str>::new();
454        for fm in fields {
455            let key = self.read_key_of(fm);
456            map.insert(key, fm.rust);
457        }
458
459        let mut rust_fields = Vec::with_capacity(cols.len());
460        for &c in cols {
461            rust_fields.push(*map.get(c)?);
462        }
463        st.addr_cells(&rust_fields)
464    }
465
466    pub fn select_from(&self, table: &str) -> SelectBuilder {
467        let mut sb = SelectBuilder::new();
468        sb.set_flavor(self.flavor);
469        sb.from([table.to_string()]);
470
471        let alias = Self::parse_table_alias(table);
472        let cols: Vec<String> = self
473            .fields_for_read()
474            .into_iter()
475            .map(|f| {
476                let field_alias = self.alias_of(f);
477                let mut c = String::new();
478                // Follow alias rule: only check if alias contains '.' when adding table prefix.
479                if self.flavor != Flavor::CQL && !field_alias.contains('.') {
480                    c.push_str(alias);
481                    c.push('.');
482                }
483                c.push_str(&f.name_for_select(self.flavor, &field_alias));
484                c
485            })
486            .collect();
487
488        if cols.is_empty() {
489            select_cols!(sb, "*");
490        } else {
491            sb.select(cols);
492        }
493        sb
494    }
495
496    /// SelectFromForTag: build SELECT for a tag (deprecated).
497    pub fn select_from_for_tag(&self, table: &str, tag: &str) -> SelectBuilder {
498        // If tag is missing: behaves like SELECT * (with_tag yields empty cols => select "*").
499        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
500        self.with_tag([tag]).select_from(table)
501    }
502
503    pub fn update(&self, table: &str, value: &T) -> UpdateBuilder {
504        let mut ub = UpdateBuilder::new();
505        ub.set_flavor(self.flavor);
506        ub.update([table.to_string()]);
507
508        let mut assigns = Vec::new();
509
510        let mut map = std::collections::HashMap::<&'static str, crate::modifiers::Arg>::new();
511        for (fm, arg) in T::FIELDS.iter().zip(value.values()) {
512            map.insert(fm.rust, arg);
513        }
514
515        for fm in self.fields_for_write() {
516            if self.should_omit_empty(fm) && value.is_empty_field(fm.rust) {
517                continue;
518            }
519            // If with_quote, keep quoting when writing column names.
520            let field_alias = self.alias_of(fm);
521            let col = if fm.with_quote {
522                self.flavor.quote(&field_alias)
523            } else {
524                field_alias
525            };
526            if let Some(v) = map.get(fm.rust).cloned() {
527                assigns.push(ub.assign(&col, v));
528            }
529        }
530
531        ub.set(assigns);
532        ub
533    }
534
535    /// UpdateForTag: build UPDATE for a tag (deprecated).
536    pub fn update_for_tag(&self, table: &str, tag: &str, value: &T) -> UpdateBuilder {
537        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
538        self.with_tag([tag]).update(table, value)
539    }
540
541    pub fn delete_from(&self, table: &str) -> DeleteBuilder {
542        let mut db = DeleteBuilder::new();
543        db.set_flavor(self.flavor);
544        db.delete_from([table.to_string()]);
545        db
546    }
547
548    pub fn insert_into<'a>(
549        &self,
550        table: &str,
551        rows: impl IntoIterator<Item = &'a T>,
552    ) -> InsertBuilder
553    where
554        T: 'a,
555    {
556        self.insert_internal(table, rows, InsertVerb::Insert)
557    }
558
559    /// InsertIntoForTag: build INSERT for a tag (deprecated).
560    pub fn insert_into_for_tag<'a>(
561        &self,
562        table: &str,
563        tag: &str,
564        rows: impl IntoIterator<Item = &'a T>,
565    ) -> InsertBuilder
566    where
567        T: 'a,
568    {
569        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
570        self.with_tag([tag]).insert_into(table, rows)
571    }
572
573    pub fn insert_ignore_into_for_tag<'a>(
574        &self,
575        table: &str,
576        tag: &str,
577        rows: impl IntoIterator<Item = &'a T>,
578    ) -> InsertBuilder
579    where
580        T: 'a,
581    {
582        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
583        self.with_tag([tag]).insert_ignore_into(table, rows)
584    }
585
586    pub fn replace_into_for_tag<'a>(
587        &self,
588        table: &str,
589        tag: &str,
590        rows: impl IntoIterator<Item = &'a T>,
591    ) -> InsertBuilder
592    where
593        T: 'a,
594    {
595        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
596        self.with_tag([tag]).replace_into(table, rows)
597    }
598
599    fn filter_rows_any<'a>(values: impl IntoIterator<Item = &'a dyn Any>) -> Vec<&'a T>
600    where
601        T: 'static,
602    {
603        values
604            .into_iter()
605            .filter_map(|v| v.downcast_ref::<T>())
606            .collect()
607    }
608
609    /// InsertIntoAny: ignore values that are not of type T (like Go's permissive interface slice).
610    pub fn insert_into_any<'a>(
611        &self,
612        table: &str,
613        values: impl IntoIterator<Item = &'a dyn Any>,
614    ) -> InsertBuilder
615    where
616        T: 'static,
617    {
618        let rows = Self::filter_rows_any(values);
619        self.insert_into(table, rows)
620    }
621
622    pub fn insert_ignore_into_any<'a>(
623        &self,
624        table: &str,
625        values: impl IntoIterator<Item = &'a dyn Any>,
626    ) -> InsertBuilder
627    where
628        T: 'static,
629    {
630        let rows = Self::filter_rows_any(values);
631        self.insert_ignore_into(table, rows)
632    }
633
634    pub fn replace_into_any<'a>(
635        &self,
636        table: &str,
637        values: impl IntoIterator<Item = &'a dyn Any>,
638    ) -> InsertBuilder
639    where
640        T: 'static,
641    {
642        let rows = Self::filter_rows_any(values);
643        self.replace_into(table, rows)
644    }
645
646    pub fn insert_into_for_tag_any<'a>(
647        &self,
648        table: &str,
649        tag: &str,
650        values: impl IntoIterator<Item = &'a dyn Any>,
651    ) -> InsertBuilder
652    where
653        T: 'static,
654    {
655        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
656        let rows = Self::filter_rows_any(values);
657        self.with_tag([tag]).insert_into(table, rows)
658    }
659
660    pub fn insert_ignore_into_for_tag_any<'a>(
661        &self,
662        table: &str,
663        tag: &str,
664        values: impl IntoIterator<Item = &'a dyn Any>,
665    ) -> InsertBuilder
666    where
667        T: 'static,
668    {
669        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
670        let rows = Self::filter_rows_any(values);
671        self.with_tag([tag]).insert_ignore_into(table, rows)
672    }
673
674    pub fn replace_into_for_tag_any<'a>(
675        &self,
676        table: &str,
677        tag: &str,
678        values: impl IntoIterator<Item = &'a dyn Any>,
679    ) -> InsertBuilder
680    where
681        T: 'static,
682    {
683        let tag: &'static str = Box::leak(tag.to_string().into_boxed_str());
684        let rows = Self::filter_rows_any(values);
685        self.with_tag([tag]).replace_into(table, rows)
686    }
687
688    pub fn insert_ignore_into<'a>(
689        &self,
690        table: &str,
691        rows: impl IntoIterator<Item = &'a T>,
692    ) -> InsertBuilder
693    where
694        T: 'a,
695    {
696        self.insert_internal(table, rows, InsertVerb::InsertIgnore)
697    }
698
699    pub fn replace_into<'a>(
700        &self,
701        table: &str,
702        rows: impl IntoIterator<Item = &'a T>,
703    ) -> InsertBuilder
704    where
705        T: 'a,
706    {
707        self.insert_internal(table, rows, InsertVerb::Replace)
708    }
709
710    fn insert_internal<'a>(
711        &self,
712        table: &str,
713        rows: impl IntoIterator<Item = &'a T>,
714        verb: InsertVerb,
715    ) -> InsertBuilder
716    where
717        T: 'a,
718    {
719        let mut ib = InsertBuilder::new();
720        ib.set_flavor(self.flavor);
721        match verb {
722            InsertVerb::Insert => {
723                ib.insert_into(table);
724            }
725            InsertVerb::InsertIgnore => {
726                ib.insert_ignore_into(table);
727            }
728            InsertVerb::Replace => {
729                ib.replace_into(table);
730            }
731        }
732
733        let rows: Vec<&T> = rows.into_iter().collect();
734        if rows.is_empty() {
735            // Empty rows: do not emit cols/values.
736            return ib;
737        }
738
739        let fields = self.fields_for_write();
740
741        // Decide if a column should be filtered entirely (omitempty and all rows empty).
742        let mut nil_cnt = vec![0_usize; fields.len()];
743        for (fi, fm) in fields.iter().enumerate() {
744            let should_omit = self.should_omit_empty(fm);
745            if !should_omit {
746                continue;
747            }
748            for r in &rows {
749                if r.is_empty_field(fm.rust) {
750                    nil_cnt[fi] += 1;
751                }
752            }
753        }
754
755        let mut kept = Vec::<usize>::new();
756        for (i, cnt) in nil_cnt.into_iter().enumerate() {
757            if cnt == rows.len() {
758                continue;
759            }
760            kept.push(i);
761        }
762
763        let cols: Vec<String> = kept
764            .iter()
765            .map(|&i| {
766                let fm = fields[i];
767                let field_alias = self.alias_of(fm);
768                if fm.with_quote {
769                    self.flavor.quote(&field_alias)
770                } else {
771                    field_alias
772                }
773            })
774            .collect();
775        ib.cols(escape_all(cols));
776
777        for r in rows {
778            let mut map = std::collections::HashMap::<&'static str, crate::modifiers::Arg>::new();
779            for (fm, arg) in T::FIELDS.iter().zip(r.values()) {
780                map.insert(fm.rust, arg);
781            }
782            let mut row_args = Vec::new();
783            for &i in &kept {
784                let fm = fields[i];
785                row_args.push(
786                    map.get(fm.rust)
787                        .cloned()
788                        .unwrap_or_else(|| crate::SqlValue::Null.into()),
789                );
790            }
791            ib.values(row_args);
792        }
793
794        ib
795    }
796}
797
798#[derive(Debug, Clone, Copy)]
799enum InsertVerb {
800    Insert,
801    InsertIgnore,
802    Replace,
803}
804
805/// Declare metadata and value accessors for a business struct usable by `Struct<T>`.
806///
807/// Example:
808///
809/// ```ignore
810/// #[derive(Default)]
811/// struct User { id: i64, name: String }
812///
813/// halo_space::sqlbuilder::sql_struct! {
814///   impl User {
815///     id:  { db: "id", tags: ["pk"], omitempty: [], quote: false, as: None },
816///     name:{ db: "name", tags: [],     omitempty: [""], quote: true,  as: None },
817///   }
818/// }
819/// ```
820#[macro_export]
821macro_rules! sql_struct {
822    (
823        impl $ty:ty {
824            $(
825                $field:ident : { db: $db:literal, $(orig: $orig:literal,)? tags: [ $($tag:literal),* $(,)? ], omitempty: [ $($omit:literal),* $(,)? ], quote: $quote:literal, as: $as:expr }
826            ),* $(,)?
827        }
828    ) => {
829        impl $crate::structs::SqlStruct for $ty {
830            const FIELDS: &'static [$crate::structs::FieldMeta] = &[
831                $(
832                    $crate::structs::FieldMeta{
833                        rust: stringify!($field),
834                        orig: $crate::__sql_struct_orig!(stringify!($field) $(, $orig)?),
835                        db: $db,
836                        as_: $as,
837                        tags: &[ $($tag),* ],
838                        omitempty_tags: &[ $($omit),* ],
839                        with_quote: $quote,
840                    }
841                ),*
842            ];
843
844            fn values(&self) -> Vec<$crate::modifiers::Arg> {
845                vec![
846                    $(
847                        $crate::modifiers::Arg::from(self.$field.clone())
848                    ),*
849                ]
850            }
851
852            fn is_empty_field(&self, rust_field: &'static str) -> bool {
853                match rust_field {
854                    $(
855                        stringify!($field) => $crate::structs::IsEmpty::is_empty_value(&self.$field),
856                    )*
857                    _ => false,
858                }
859            }
860
861            fn addr_cells<'a>(
862                &'a mut self,
863                rust_fields: &[&'static str],
864            ) -> Option<Vec<$crate::scan::ScanCell<'a>>> {
865                let mut out = Vec::with_capacity(rust_fields.len());
866                for &rf in rust_fields {
867                    match rf {
868                        $(
869                            stringify!($field) => {
870                                out.push($crate::scan::ScanCell::from_ptr(std::ptr::addr_of_mut!(self.$field)));
871                            }
872                        )*
873                        _ => return None,
874                    }
875                }
876                Some(out)
877            }
878        }
879    };
880}
881
882/// Macro helper: support optional `orig:` parameter.
883#[doc(hidden)]
884#[macro_export]
885macro_rules! __sql_struct_orig {
886    ($default:expr) => {
887        $default
888    };
889    ($default:expr, $custom:expr) => {
890        $custom
891    };
892}