vibesql_ast/
identifier.rs

1//! SQL Identifier types with proper case handling per SQL:1999.
2//!
3//! This module provides identifier types that correctly handle case sensitivity
4//! according to SQL:1999 standard:
5//!
6//! - **Unquoted identifiers**: Case-insensitive, folded to lowercase for canonical form
7//! - **Quoted (delimited) identifiers**: Case-sensitive, preserve exact case
8//!
9//! # Example
10//!
11//! ```
12//! use vibesql_ast::TableIdentifier;
13//!
14//! // Unquoted identifiers are case-insensitive
15//! let id1 = TableIdentifier::new("MyTable", false);
16//! let id2 = TableIdentifier::new("mytable", false);
17//! let id3 = TableIdentifier::new("MYTABLE", false);
18//! assert_eq!(id1, id2);
19//! assert_eq!(id2, id3);
20//!
21//! // Quoted identifiers are case-sensitive
22//! let quoted = TableIdentifier::new("MyTable", true);
23//! assert_ne!(id1, quoted); // Different canonical forms
24//! ```
25
26use std::fmt;
27use std::hash::{Hash, Hasher};
28
29/// A SQL identifier with proper case handling per SQL:1999.
30///
31/// This type separates three concerns:
32/// - **Canonical form**: Used for HashMap keys and equality comparison
33/// - **Display form**: Preserves user's original input for error messages
34/// - **Quoted flag**: Whether the identifier was delimited (quoted)
35///
36/// ## SQL:1999 Compliance
37///
38/// Per SQL:1999, identifier handling depends on whether the identifier was quoted:
39///
40/// | Input | Quoted | Canonical | Matches `mytable`? | Matches `"MyTable"`? |
41/// |-------|--------|-----------|--------------------|-----------------------|
42/// | `MyTable` | No | `mytable` | Yes | No |
43/// | `"MyTable"` | Yes | `MyTable` | No | Yes |
44/// | `MYTABLE` | No | `mytable` | Yes | No |
45/// | `"mytable"` | Yes | `mytable` | Yes (same canonical) | No |
46///
47/// ## Compound Identifiers
48///
49/// TableIdentifier supports schema-qualified table names where each component
50/// (schema and table) can be independently quoted or unquoted:
51///
52/// | SQL | Schema Part | Table Part | Canonical Form |
53/// |-----|-------------|------------|----------------|
54/// | `myApp.users` | unquoted | unquoted | `myapp.users` |
55/// | `"myApp".users` | quoted | unquoted | `myApp.users` |
56/// | `myapp."Users"` | unquoted | quoted | `myapp.Users` |
57/// | `"myApp"."Users"` | quoted | quoted | `myApp.Users` |
58#[derive(Debug, Clone)]
59pub struct TableIdentifier {
60    // Optional schema part
61    schema_canonical: Option<String>,
62    schema_display: Option<String>,
63    schema_quoted: bool,
64
65    // Table part (always present)
66    table_canonical: String,
67    table_display: String,
68    table_quoted: bool,
69
70    /// Canonical form for HashMap keys and comparison.
71    /// - If quoted: exact case preserved (case-sensitive)
72    /// - If unquoted: lowercase folded (case-insensitive)
73    /// - For compound identifiers: "schema.table" using canonical forms
74    canonical: String,
75
76    /// Display form preserving user's original input.
77    /// Used for error messages and user-facing output.
78    display: String,
79
80    /// Whether the identifier was quoted (delimited) in the original SQL.
81    /// Quoted identifiers use double quotes: `"MyTable"`
82    /// For compound identifiers, this is true if the table part was quoted.
83    quoted: bool,
84}
85
86impl TableIdentifier {
87    /// Create a new table identifier.
88    ///
89    /// # Arguments
90    ///
91    /// * `name` - The identifier name as written by the user
92    /// * `quoted` - Whether the identifier was quoted (delimited) in SQL
93    ///
94    /// # SQL:1999 Behavior
95    ///
96    /// - If `quoted` is `false`: The canonical form is lowercase-folded for
97    ///   case-insensitive comparison.
98    ///
99    /// - If `quoted` is `true`: The canonical form preserves exact case for
100    ///   case-sensitive comparison. This matches SQL:1999 behavior for
101    ///   delimited identifiers.
102    ///
103    /// # Example
104    ///
105    /// ```
106    /// use vibesql_ast::TableIdentifier;
107    ///
108    /// // Unquoted: case-insensitive
109    /// let unquoted = TableIdentifier::new("MyTable", false);
110    /// assert_eq!(unquoted.canonical(), "mytable");
111    /// assert_eq!(unquoted.display(), "MyTable");
112    ///
113    /// // Quoted: case-sensitive
114    /// let quoted = TableIdentifier::new("MyTable", true);
115    /// assert_eq!(quoted.canonical(), "MyTable");
116    /// assert_eq!(quoted.display(), "MyTable");
117    /// ```
118    pub fn new(name: &str, quoted: bool) -> Self {
119        let table_canonical = if quoted {
120            // Quoted identifiers preserve exact case (SQL:1999 delimited identifiers)
121            name.to_string()
122        } else {
123            // Unquoted identifiers fold to lowercase for case-insensitive comparison
124            name.to_ascii_lowercase()
125        };
126
127        Self {
128            schema_canonical: None,
129            schema_display: None,
130            schema_quoted: false,
131            table_canonical: table_canonical.clone(),
132            table_display: name.to_string(),
133            table_quoted: quoted,
134            canonical: table_canonical,
135            display: name.to_string(),
136            quoted,
137        }
138    }
139
140    /// Create an identifier from a canonical name (for internal use).
141    ///
142    /// This is used when loading from persistence where we only have the
143    /// canonical form. The display form is set to match canonical.
144    pub fn from_canonical(canonical: String, quoted: bool) -> Self {
145        Self {
146            schema_canonical: None,
147            schema_display: None,
148            schema_quoted: false,
149            table_canonical: canonical.clone(),
150            table_display: canonical.clone(),
151            table_quoted: quoted,
152            canonical: canonical.clone(),
153            display: canonical,
154            quoted,
155        }
156    }
157
158    /// Create a qualified (schema.table) identifier.
159    ///
160    /// Each component (schema and table) has independent quoted/unquoted semantics.
161    ///
162    /// # Arguments
163    ///
164    /// * `schema_name` - The schema name as written by the user
165    /// * `schema_quoted` - Whether the schema was quoted in SQL
166    /// * `table_name` - The table name as written by the user
167    /// * `table_quoted` - Whether the table was quoted in SQL
168    ///
169    /// # SQL:1999 Behavior
170    ///
171    /// Each identifier component is independent:
172    /// - Unquoted: canonical form is lowercase (case-insensitive)
173    /// - Quoted: canonical form preserves case (case-sensitive)
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// use vibesql_ast::TableIdentifier;
179    ///
180    /// // "myApp".users → myApp.users (schema case-sensitive, table case-insensitive)
181    /// let id = TableIdentifier::qualified("myApp", true, "users", false);
182    /// assert_eq!(id.canonical(), "myApp.users");
183    ///
184    /// // myapp."Users" → myapp.Users (schema case-insensitive, table case-sensitive)
185    /// let id = TableIdentifier::qualified("myapp", false, "Users", true);
186    /// assert_eq!(id.canonical(), "myapp.Users");
187    /// ```
188    pub fn qualified(
189        schema_name: &str,
190        schema_quoted: bool,
191        table_name: &str,
192        table_quoted: bool,
193    ) -> Self {
194        let schema_canonical =
195            if schema_quoted { schema_name.to_string() } else { schema_name.to_ascii_lowercase() };
196
197        let table_canonical =
198            if table_quoted { table_name.to_string() } else { table_name.to_ascii_lowercase() };
199
200        let canonical = format!("{}.{}", schema_canonical, table_canonical);
201        let display = format!("{}.{}", schema_name, table_name);
202
203        Self {
204            schema_canonical: Some(schema_canonical),
205            schema_display: Some(schema_name.to_string()),
206            schema_quoted,
207            table_canonical,
208            table_display: table_name.to_string(),
209            table_quoted,
210            canonical,
211            display,
212            quoted: table_quoted,
213        }
214    }
215
216    /// Get the canonical form of the identifier.
217    ///
218    /// This is used for HashMap keys and equality comparison.
219    /// - Unquoted identifiers: lowercase
220    /// - Quoted identifiers: exact case preserved
221    #[inline]
222    pub fn canonical(&self) -> &str {
223        &self.canonical
224    }
225
226    /// Get the display form of the identifier.
227    ///
228    /// This preserves the user's original input and should be used
229    /// in error messages and user-facing output.
230    #[inline]
231    pub fn display(&self) -> &str {
232        &self.display
233    }
234
235    /// Check if this identifier was quoted (delimited) in the original SQL.
236    #[inline]
237    pub fn is_quoted(&self) -> bool {
238        self.quoted
239    }
240
241    /// Get the canonical form as an owned String.
242    ///
243    /// Useful for HashMap operations that need owned keys.
244    #[inline]
245    pub fn into_canonical(self) -> String {
246        self.canonical
247    }
248
249    /// Check if this is a qualified (schema.table) identifier.
250    #[inline]
251    pub fn is_qualified(&self) -> bool {
252        self.schema_canonical.is_some()
253    }
254
255    /// Get the schema part canonical form (if this is a qualified identifier).
256    #[inline]
257    pub fn schema_canonical(&self) -> Option<&str> {
258        self.schema_canonical.as_deref()
259    }
260
261    /// Get the schema part display form (if this is a qualified identifier).
262    #[inline]
263    pub fn schema_display(&self) -> Option<&str> {
264        self.schema_display.as_deref()
265    }
266
267    /// Check if the schema part was quoted (if this is a qualified identifier).
268    #[inline]
269    pub fn is_schema_quoted(&self) -> bool {
270        self.schema_quoted
271    }
272
273    /// Get the table part canonical form.
274    ///
275    /// For simple identifiers, this is the same as `canonical()`.
276    /// For qualified identifiers, this is just the table name portion.
277    #[inline]
278    pub fn table_canonical(&self) -> &str {
279        &self.table_canonical
280    }
281
282    /// Get the table part display form.
283    ///
284    /// For simple identifiers, this is the same as `display()`.
285    /// For qualified identifiers, this is just the table name portion.
286    #[inline]
287    pub fn table_display(&self) -> &str {
288        &self.table_display
289    }
290
291    /// Check if the table part was quoted.
292    ///
293    /// For simple identifiers, this is the same as `is_quoted()`.
294    /// For qualified identifiers, this indicates the quoting of the table part only.
295    #[inline]
296    pub fn is_table_quoted(&self) -> bool {
297        self.table_quoted
298    }
299
300    /// Create an identifier that matches any case variation.
301    ///
302    /// This creates an unquoted identifier from the given name,
303    /// which will match any case variation of that name.
304    pub fn unquoted(name: &str) -> Self {
305        Self::new(name, false)
306    }
307
308    /// Create an identifier that matches only the exact case.
309    ///
310    /// This creates a quoted identifier from the given name,
311    /// which will only match the exact same case.
312    pub fn quoted(name: &str) -> Self {
313        Self::new(name, true)
314    }
315}
316
317impl PartialEq for TableIdentifier {
318    /// Two identifiers are equal if their canonical forms match.
319    fn eq(&self, other: &Self) -> bool {
320        self.canonical == other.canonical
321    }
322}
323
324impl Eq for TableIdentifier {}
325
326impl Hash for TableIdentifier {
327    /// Hash based on canonical form for consistent HashMap behavior.
328    fn hash<H: Hasher>(&self, state: &mut H) {
329        self.canonical.hash(state);
330    }
331}
332
333impl fmt::Display for TableIdentifier {
334    /// Display uses the original user input form.
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        write!(f, "{}", self.display)
337    }
338}
339
340impl From<&str> for TableIdentifier {
341    /// Convert from a string, assuming unquoted (case-insensitive).
342    fn from(s: &str) -> Self {
343        Self::new(s, false)
344    }
345}
346
347impl From<String> for TableIdentifier {
348    /// Convert from a String, assuming unquoted (case-insensitive).
349    fn from(s: String) -> Self {
350        Self::new(&s, false)
351    }
352}
353
354/// A SQL column identifier with proper case handling per SQL:1999.
355///
356/// This type handles column references with optional table and schema qualifiers.
357/// Each component (schema, table, column) has independent quoted/unquoted semantics.
358///
359/// ## Supported Forms
360///
361/// | SQL Form | Description |
362/// |----------|-------------|
363/// | `id` | Unqualified column reference |
364/// | `users.id` | Table-qualified column reference |
365/// | `myschema.users.id` | Fully-qualified column reference |
366///
367/// ## SQL:1999 Compliance
368///
369/// Per SQL:1999, identifier handling depends on whether each component was quoted:
370///
371/// | Input | Quoted | Canonical | Matches `mycolumn`? |
372/// |-------|--------|-----------|---------------------|
373/// | `MyColumn` | No | `mycolumn` | Yes |
374/// | `"MyColumn"` | Yes | `MyColumn` | No |
375/// | `MYCOLUMN` | No | `mycolumn` | Yes |
376///
377/// ## Example
378///
379/// ```
380/// use vibesql_ast::ColumnIdentifier;
381///
382/// // Unquoted identifiers are case-insensitive
383/// let c1 = ColumnIdentifier::simple("MyColumn", false);
384/// let c2 = ColumnIdentifier::simple("mycolumn", false);
385/// assert_eq!(c1, c2);
386///
387/// // Quoted identifiers are case-sensitive
388/// let quoted = ColumnIdentifier::simple("MyColumn", true);
389/// assert_ne!(c1, quoted);
390///
391/// // Table-qualified column
392/// let qualified = ColumnIdentifier::qualified("users", false, "id", false);
393/// assert_eq!(qualified.canonical(), "users.id");
394/// ```
395#[derive(Debug, Clone)]
396pub struct ColumnIdentifier {
397    // Optional schema part
398    schema_canonical: Option<String>,
399    schema_display: Option<String>,
400    schema_quoted: bool,
401
402    // Optional table part
403    table_canonical: Option<String>,
404    table_display: Option<String>,
405    table_quoted: bool,
406
407    // Column part (always present)
408    column_canonical: String,
409    column_display: String,
410    column_quoted: bool,
411
412    /// Canonical form for HashMap keys and comparison.
413    /// Format: "schema.table.column" or "table.column" or "column"
414    canonical: String,
415
416    /// Display form preserving user's original input.
417    /// Used for error messages and user-facing output.
418    display: String,
419}
420
421impl ColumnIdentifier {
422    /// Create a simple (unqualified) column identifier.
423    ///
424    /// # Arguments
425    ///
426    /// * `column` - The column name as written by the user
427    /// * `quoted` - Whether the identifier was quoted (delimited) in SQL
428    ///
429    /// # Example
430    ///
431    /// ```
432    /// use vibesql_ast::ColumnIdentifier;
433    ///
434    /// let c = ColumnIdentifier::simple("MyColumn", false);
435    /// assert_eq!(c.canonical(), "mycolumn");
436    /// assert_eq!(c.display(), "MyColumn");
437    /// ```
438    pub fn simple(column: &str, quoted: bool) -> Self {
439        let column_canonical =
440            if quoted { column.to_string() } else { column.to_ascii_lowercase() };
441
442        Self {
443            schema_canonical: None,
444            schema_display: None,
445            schema_quoted: false,
446            table_canonical: None,
447            table_display: None,
448            table_quoted: false,
449            column_canonical: column_canonical.clone(),
450            column_display: column.to_string(),
451            column_quoted: quoted,
452            canonical: column_canonical,
453            display: column.to_string(),
454        }
455    }
456
457    /// Create a table-qualified column identifier.
458    ///
459    /// # Arguments
460    ///
461    /// * `table` - The table name as written by the user
462    /// * `table_quoted` - Whether the table was quoted in SQL
463    /// * `column` - The column name as written by the user
464    /// * `column_quoted` - Whether the column was quoted in SQL
465    ///
466    /// # Example
467    ///
468    /// ```
469    /// use vibesql_ast::ColumnIdentifier;
470    ///
471    /// let c = ColumnIdentifier::qualified("Users", false, "ID", false);
472    /// assert_eq!(c.canonical(), "users.id");
473    /// assert_eq!(c.table_canonical(), Some("users"));
474    /// assert_eq!(c.column_canonical(), "id");
475    /// ```
476    pub fn qualified(table: &str, table_quoted: bool, column: &str, column_quoted: bool) -> Self {
477        let table_canonical =
478            if table_quoted { table.to_string() } else { table.to_ascii_lowercase() };
479
480        let column_canonical =
481            if column_quoted { column.to_string() } else { column.to_ascii_lowercase() };
482
483        let canonical = format!("{}.{}", table_canonical, column_canonical);
484        let display = format!("{}.{}", table, column);
485
486        Self {
487            schema_canonical: None,
488            schema_display: None,
489            schema_quoted: false,
490            table_canonical: Some(table_canonical),
491            table_display: Some(table.to_string()),
492            table_quoted,
493            column_canonical,
494            column_display: column.to_string(),
495            column_quoted,
496            canonical,
497            display,
498        }
499    }
500
501    /// Create a fully-qualified (schema.table.column) identifier.
502    ///
503    /// # Arguments
504    ///
505    /// * `schema` - The schema name as written by the user
506    /// * `schema_quoted` - Whether the schema was quoted in SQL
507    /// * `table` - The table name as written by the user
508    /// * `table_quoted` - Whether the table was quoted in SQL
509    /// * `column` - The column name as written by the user
510    /// * `column_quoted` - Whether the column was quoted in SQL
511    ///
512    /// # Example
513    ///
514    /// ```
515    /// use vibesql_ast::ColumnIdentifier;
516    ///
517    /// let c = ColumnIdentifier::fully_qualified(
518    ///     "myApp", true,   // quoted schema
519    ///     "users", false,  // unquoted table
520    ///     "ID", false      // unquoted column
521    /// );
522    /// assert_eq!(c.canonical(), "myApp.users.id");
523    /// ```
524    pub fn fully_qualified(
525        schema: &str,
526        schema_quoted: bool,
527        table: &str,
528        table_quoted: bool,
529        column: &str,
530        column_quoted: bool,
531    ) -> Self {
532        let schema_canonical =
533            if schema_quoted { schema.to_string() } else { schema.to_ascii_lowercase() };
534
535        let table_canonical =
536            if table_quoted { table.to_string() } else { table.to_ascii_lowercase() };
537
538        let column_canonical =
539            if column_quoted { column.to_string() } else { column.to_ascii_lowercase() };
540
541        let canonical = format!("{}.{}.{}", schema_canonical, table_canonical, column_canonical);
542        let display = format!("{}.{}.{}", schema, table, column);
543
544        Self {
545            schema_canonical: Some(schema_canonical),
546            schema_display: Some(schema.to_string()),
547            schema_quoted,
548            table_canonical: Some(table_canonical),
549            table_display: Some(table.to_string()),
550            table_quoted,
551            column_canonical,
552            column_display: column.to_string(),
553            column_quoted,
554            canonical,
555            display,
556        }
557    }
558
559    /// Create an unquoted column identifier (convenience constructor).
560    ///
561    /// This creates a simple, case-insensitive column reference.
562    ///
563    /// # Example
564    ///
565    /// ```
566    /// use vibesql_ast::ColumnIdentifier;
567    ///
568    /// let c = ColumnIdentifier::unquoted("MyColumn");
569    /// assert_eq!(c.canonical(), "mycolumn");
570    /// ```
571    pub fn unquoted(column: &str) -> Self {
572        Self::simple(column, false)
573    }
574
575    /// Create a quoted column identifier (convenience constructor).
576    ///
577    /// This creates a simple, case-sensitive column reference.
578    ///
579    /// # Example
580    ///
581    /// ```
582    /// use vibesql_ast::ColumnIdentifier;
583    ///
584    /// let c = ColumnIdentifier::quoted("MyColumn");
585    /// assert_eq!(c.canonical(), "MyColumn");
586    /// ```
587    pub fn quoted(column: &str) -> Self {
588        Self::simple(column, true)
589    }
590
591    /// Create a table.column reference with unquoted identifiers.
592    ///
593    /// # Example
594    ///
595    /// ```
596    /// use vibesql_ast::ColumnIdentifier;
597    ///
598    /// let c = ColumnIdentifier::table_column("users", "id");
599    /// assert_eq!(c.canonical(), "users.id");
600    /// ```
601    pub fn table_column(table: &str, column: &str) -> Self {
602        Self::qualified(table, false, column, false)
603    }
604
605    /// Create an identifier from a canonical name (for internal use).
606    ///
607    /// This is used when loading from persistence where we only have the
608    /// canonical form. The display form is set to match canonical.
609    pub fn from_canonical(canonical: String, quoted: bool) -> Self {
610        Self {
611            schema_canonical: None,
612            schema_display: None,
613            schema_quoted: false,
614            table_canonical: None,
615            table_display: None,
616            table_quoted: false,
617            column_canonical: canonical.clone(),
618            column_display: canonical.clone(),
619            column_quoted: quoted,
620            canonical: canonical.clone(),
621            display: canonical,
622        }
623    }
624
625    /// Get the canonical form of the identifier.
626    ///
627    /// This is used for HashMap keys and equality comparison.
628    /// Format: "schema.table.column" or "table.column" or "column"
629    #[inline]
630    pub fn canonical(&self) -> &str {
631        &self.canonical
632    }
633
634    /// Get the display form of the identifier.
635    ///
636    /// This preserves the user's original input and should be used
637    /// in error messages and user-facing output.
638    #[inline]
639    pub fn display(&self) -> &str {
640        &self.display
641    }
642
643    /// Get the column name in canonical form.
644    #[inline]
645    pub fn column_canonical(&self) -> &str {
646        &self.column_canonical
647    }
648
649    /// Get the column name in display form.
650    #[inline]
651    pub fn column_display(&self) -> &str {
652        &self.column_display
653    }
654
655    /// Check if the column was quoted.
656    #[inline]
657    pub fn is_column_quoted(&self) -> bool {
658        self.column_quoted
659    }
660
661    /// Get the table name in canonical form (if qualified).
662    #[inline]
663    pub fn table_canonical(&self) -> Option<&str> {
664        self.table_canonical.as_deref()
665    }
666
667    /// Get the table name in display form (if qualified).
668    #[inline]
669    pub fn table_display(&self) -> Option<&str> {
670        self.table_display.as_deref()
671    }
672
673    /// Check if the table was quoted (if qualified).
674    #[inline]
675    pub fn is_table_quoted(&self) -> bool {
676        self.table_quoted
677    }
678
679    /// Get the schema name in canonical form (if fully qualified).
680    #[inline]
681    pub fn schema_canonical(&self) -> Option<&str> {
682        self.schema_canonical.as_deref()
683    }
684
685    /// Get the schema name in display form (if fully qualified).
686    #[inline]
687    pub fn schema_display(&self) -> Option<&str> {
688        self.schema_display.as_deref()
689    }
690
691    /// Check if the schema was quoted (if fully qualified).
692    #[inline]
693    pub fn is_schema_quoted(&self) -> bool {
694        self.schema_quoted
695    }
696
697    /// Check if this is a table-qualified column (has table but not schema).
698    #[inline]
699    pub fn is_qualified(&self) -> bool {
700        self.table_canonical.is_some()
701    }
702
703    /// Check if this is a fully-qualified column (has schema.table.column).
704    #[inline]
705    pub fn is_fully_qualified(&self) -> bool {
706        self.schema_canonical.is_some()
707    }
708
709    /// Check if this column reference is ambiguous (no table qualifier).
710    #[inline]
711    pub fn is_ambiguous(&self) -> bool {
712        self.table_canonical.is_none()
713    }
714
715    /// Get the canonical form as an owned String.
716    #[inline]
717    pub fn into_canonical(self) -> String {
718        self.canonical
719    }
720
721    /// Check if this column matches another by canonical column name only.
722    ///
723    /// This ignores table and schema qualifiers, useful for finding columns
724    /// by name across different tables.
725    pub fn matches_column_name(&self, name: &str, case_sensitive: bool) -> bool {
726        if case_sensitive {
727            self.column_canonical == name
728        } else {
729            self.column_canonical == name.to_ascii_lowercase()
730        }
731    }
732
733    /// Resolve an ambiguous column against a table.
734    ///
735    /// If this column is unqualified, creates a new column identifier
736    /// qualified with the given table. If already qualified, returns self.
737    pub fn resolve_against(&self, table: &TableIdentifier) -> Self {
738        if self.is_qualified() {
739            return self.clone();
740        }
741
742        if table.is_qualified() {
743            // Table has schema qualification
744            Self::fully_qualified(
745                table.schema_display().unwrap_or_default(),
746                table.is_schema_quoted(),
747                table.table_display(),
748                table.is_table_quoted(),
749                &self.column_display,
750                self.column_quoted,
751            )
752        } else {
753            // Simple table name
754            Self::qualified(
755                table.table_display(),
756                table.is_table_quoted(),
757                &self.column_display,
758                self.column_quoted,
759            )
760        }
761    }
762
763    /// Check if this column reference matches another.
764    ///
765    /// Matching rules:
766    /// - An unqualified column matches if the column names match
767    /// - A qualified column matches only if both table and column match
768    /// - A fully-qualified column matches only if schema, table, and column match
769    pub fn matches(&self, other: &ColumnIdentifier) -> bool {
770        // Both must have same qualification level for exact match
771        if self.is_fully_qualified() != other.is_fully_qualified() {
772            // But an unqualified can match a qualified if column name matches
773            if self.is_ambiguous() {
774                return self.column_canonical == other.column_canonical;
775            }
776            if other.is_ambiguous() {
777                return self.column_canonical == other.column_canonical;
778            }
779            return false;
780        }
781
782        if self.is_qualified() != other.is_qualified() {
783            // Unqualified matches qualified if column name matches
784            if self.is_ambiguous() {
785                return self.column_canonical == other.column_canonical;
786            }
787            if other.is_ambiguous() {
788                return self.column_canonical == other.column_canonical;
789            }
790            return false;
791        }
792
793        // Same qualification level - compare canonical forms
794        self.canonical == other.canonical
795    }
796
797    /// Create a column identifier by stripping qualifiers.
798    ///
799    /// Returns a new unqualified column identifier with just the column name.
800    pub fn unqualify(&self) -> Self {
801        Self::simple(&self.column_display, self.column_quoted)
802    }
803
804    /// Create a column identifier with a different table qualifier.
805    ///
806    /// Useful when remapping columns during query planning.
807    pub fn with_table(&self, table: &str, table_quoted: bool) -> Self {
808        if let Some(schema_display) = &self.schema_display {
809            Self::fully_qualified(
810                schema_display,
811                self.schema_quoted,
812                table,
813                table_quoted,
814                &self.column_display,
815                self.column_quoted,
816            )
817        } else {
818            Self::qualified(table, table_quoted, &self.column_display, self.column_quoted)
819        }
820    }
821}
822
823impl PartialEq for ColumnIdentifier {
824    /// Two column identifiers are equal if their canonical forms match.
825    fn eq(&self, other: &Self) -> bool {
826        self.canonical == other.canonical
827    }
828}
829
830impl Eq for ColumnIdentifier {}
831
832impl Hash for ColumnIdentifier {
833    /// Hash based on canonical form for consistent HashMap behavior.
834    fn hash<H: Hasher>(&self, state: &mut H) {
835        self.canonical.hash(state);
836    }
837}
838
839impl fmt::Display for ColumnIdentifier {
840    /// Display uses the original user input form.
841    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
842        write!(f, "{}", self.display)
843    }
844}
845
846impl From<&str> for ColumnIdentifier {
847    /// Convert from a string, assuming unquoted (case-insensitive).
848    fn from(s: &str) -> Self {
849        Self::simple(s, false)
850    }
851}
852
853impl From<String> for ColumnIdentifier {
854    /// Convert from a String, assuming unquoted (case-insensitive).
855    fn from(s: String) -> Self {
856        Self::simple(&s, false)
857    }
858}
859
860/// A general SQL identifier (for indexes, views, etc.)
861///
862/// This is a type alias for now, but can be specialized later if needed.
863pub type Identifier = TableIdentifier;
864
865/// A SQL function identifier with proper case handling.
866///
867/// Similar to TableIdentifier and ColumnIdentifier, this type separates:
868/// - **Canonical form**: Lowercase for comparison and lookup
869/// - **Display form**: Preserves user's original case for error messages
870///
871/// ## Example
872///
873/// ```
874/// use vibesql_ast::FunctionIdentifier;
875///
876/// // User wrote SUBSTR in their query
877/// let func = FunctionIdentifier::new("SUBSTR");
878/// assert_eq!(func.canonical(), "substr");  // Lowercase for comparison
879/// assert_eq!(func.display(), "SUBSTR");    // Original case for errors
880/// ```
881#[derive(Debug, Clone)]
882pub struct FunctionIdentifier {
883    /// Canonical form (lowercase) for comparison and lookup
884    canonical: String,
885    /// Display form preserving user's original input
886    display: String,
887}
888
889impl FunctionIdentifier {
890    /// Create a new function identifier preserving original case.
891    ///
892    /// The canonical form is lowercased for case-insensitive comparison,
893    /// while the display form preserves the original case for error messages.
894    pub fn new(name: &str) -> Self {
895        Self { canonical: name.to_lowercase(), display: name.to_string() }
896    }
897
898    /// Get the canonical (lowercase) form for comparison.
899    pub fn canonical(&self) -> &str {
900        &self.canonical
901    }
902
903    /// Get the display form (original case) for error messages.
904    pub fn display(&self) -> &str {
905        &self.display
906    }
907
908    /// Check if this function matches the given name (case-insensitive).
909    pub fn matches(&self, name: &str) -> bool {
910        self.canonical == name.to_lowercase()
911    }
912
913    /// Get the canonical form as str (alias for canonical()).
914    pub fn as_str(&self) -> &str {
915        &self.canonical
916    }
917
918    /// Return the canonical (lowercase) form as a new String.
919    /// Provides compatibility with code expecting String methods.
920    pub fn to_lowercase(&self) -> String {
921        self.canonical.clone()
922    }
923
924    /// Return the canonical form uppercased.
925    /// Provides compatibility with code expecting String methods.
926    pub fn to_uppercase(&self) -> String {
927        self.canonical.to_uppercase()
928    }
929
930    /// Case-insensitive comparison with a string slice.
931    /// Provides compatibility with code expecting String methods.
932    pub fn eq_ignore_ascii_case(&self, other: &str) -> bool {
933        self.canonical == other.to_ascii_lowercase()
934    }
935}
936
937impl PartialEq for FunctionIdentifier {
938    fn eq(&self, other: &Self) -> bool {
939        self.canonical == other.canonical
940    }
941}
942
943impl PartialEq<str> for FunctionIdentifier {
944    fn eq(&self, other: &str) -> bool {
945        self.canonical == other.to_lowercase()
946    }
947}
948
949impl PartialEq<&str> for FunctionIdentifier {
950    fn eq(&self, other: &&str) -> bool {
951        self.canonical == other.to_lowercase()
952    }
953}
954
955impl Eq for FunctionIdentifier {}
956
957impl Hash for FunctionIdentifier {
958    fn hash<H: Hasher>(&self, state: &mut H) {
959        self.canonical.hash(state);
960    }
961}
962
963impl fmt::Display for FunctionIdentifier {
964    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
965        write!(f, "{}", self.display)
966    }
967}
968
969impl From<&str> for FunctionIdentifier {
970    fn from(s: &str) -> Self {
971        Self::new(s)
972    }
973}
974
975impl From<String> for FunctionIdentifier {
976    fn from(s: String) -> Self {
977        Self::new(&s)
978    }
979}
980
981#[cfg(test)]
982mod tests {
983    use super::*;
984    use std::collections::HashMap;
985
986    #[test]
987    fn test_unquoted_case_insensitive() {
988        let id1 = TableIdentifier::new("MyTable", false);
989        let id2 = TableIdentifier::new("mytable", false);
990        let id3 = TableIdentifier::new("MYTABLE", false);
991        let id4 = TableIdentifier::new("myTABLE", false);
992
993        // All unquoted variations should be equal
994        assert_eq!(id1, id2);
995        assert_eq!(id2, id3);
996        assert_eq!(id3, id4);
997
998        // Canonical should be lowercase
999        assert_eq!(id1.canonical(), "mytable");
1000        assert_eq!(id2.canonical(), "mytable");
1001        assert_eq!(id3.canonical(), "mytable");
1002
1003        // Display preserves original
1004        assert_eq!(id1.display(), "MyTable");
1005        assert_eq!(id2.display(), "mytable");
1006        assert_eq!(id3.display(), "MYTABLE");
1007    }
1008
1009    #[test]
1010    fn test_quoted_case_sensitive() {
1011        let id1 = TableIdentifier::new("MyTable", true);
1012        let id2 = TableIdentifier::new("mytable", true);
1013        let id3 = TableIdentifier::new("MYTABLE", true);
1014
1015        // Quoted identifiers with different cases should NOT be equal
1016        assert_ne!(id1, id2);
1017        assert_ne!(id2, id3);
1018        assert_ne!(id1, id3);
1019
1020        // Canonical preserves exact case
1021        assert_eq!(id1.canonical(), "MyTable");
1022        assert_eq!(id2.canonical(), "mytable");
1023        assert_eq!(id3.canonical(), "MYTABLE");
1024    }
1025
1026    #[test]
1027    fn test_quoted_vs_unquoted() {
1028        // Unquoted "MyTable" should NOT equal quoted "MyTable"
1029        // because they have different canonical forms
1030        let unquoted = TableIdentifier::new("MyTable", false);
1031        let quoted = TableIdentifier::new("MyTable", true);
1032
1033        assert_ne!(unquoted, quoted);
1034        assert_eq!(unquoted.canonical(), "mytable");
1035        assert_eq!(quoted.canonical(), "MyTable");
1036    }
1037
1038    #[test]
1039    fn test_quoted_lowercase_matches_unquoted() {
1040        // Special case: quoted "mytable" SHOULD equal unquoted "MyTable"
1041        // because they have the same canonical form
1042        let unquoted = TableIdentifier::new("MyTable", false);
1043        let quoted_lower = TableIdentifier::new("mytable", true);
1044
1045        assert_eq!(unquoted, quoted_lower);
1046        assert_eq!(unquoted.canonical(), "mytable");
1047        assert_eq!(quoted_lower.canonical(), "mytable");
1048    }
1049
1050    #[test]
1051    fn test_hashmap_lookup() {
1052        let mut map: HashMap<TableIdentifier, i32> = HashMap::new();
1053
1054        // Insert with unquoted identifier
1055        let key = TableIdentifier::new("users", false);
1056        map.insert(key, 42);
1057
1058        // Should find with different case (unquoted)
1059        let lookup1 = TableIdentifier::new("USERS", false);
1060        let lookup2 = TableIdentifier::new("Users", false);
1061        let lookup3 = TableIdentifier::new("users", false);
1062
1063        assert_eq!(map.get(&lookup1), Some(&42));
1064        assert_eq!(map.get(&lookup2), Some(&42));
1065        assert_eq!(map.get(&lookup3), Some(&42));
1066
1067        // Quoted "users" (lowercase) SHOULD find the value because it has the same canonical form
1068        // (unquoted "users" → "users", quoted "users" → "users")
1069        let quoted_lower = TableIdentifier::new("users", true);
1070        assert_eq!(map.get(&quoted_lower), Some(&42));
1071
1072        // But quoted "USERS" (uppercase) should NOT find it
1073        let quoted_upper = TableIdentifier::new("USERS", true);
1074        assert_eq!(map.get(&quoted_upper), None);
1075    }
1076
1077    #[test]
1078    fn test_hashmap_quoted_keys() {
1079        let mut map: HashMap<TableIdentifier, i32> = HashMap::new();
1080
1081        // Insert with quoted identifier "MyTable" (preserves case)
1082        let key = TableIdentifier::new("MyTable", true);
1083        map.insert(key, 42);
1084
1085        // Should only find with exact case (quoted)
1086        let exact = TableIdentifier::new("MyTable", true);
1087        let wrong_case = TableIdentifier::new("mytable", true);
1088        let unquoted = TableIdentifier::new("MyTable", false); // becomes "mytable"
1089
1090        assert_eq!(map.get(&exact), Some(&42));
1091        assert_eq!(map.get(&wrong_case), None); // "mytable" != "MyTable"
1092        assert_eq!(map.get(&unquoted), None); // "mytable" != "MyTable"
1093    }
1094
1095    #[test]
1096    fn test_display_trait() {
1097        let id = TableIdentifier::new("MyTable", false);
1098        assert_eq!(format!("{}", id), "MyTable");
1099
1100        let quoted = TableIdentifier::new("MyTable", true);
1101        assert_eq!(format!("{}", quoted), "MyTable");
1102    }
1103
1104    #[test]
1105    fn test_from_traits() {
1106        let id1: TableIdentifier = "MyTable".into();
1107        let id2: TableIdentifier = String::from("MyTable").into();
1108
1109        assert_eq!(id1, id2);
1110        assert_eq!(id1.canonical(), "mytable"); // From assumes unquoted
1111    }
1112
1113    #[test]
1114    fn test_helper_constructors() {
1115        let unquoted = TableIdentifier::unquoted("MyTable");
1116        let quoted = TableIdentifier::quoted("MyTable");
1117
1118        assert!(!unquoted.is_quoted());
1119        assert!(quoted.is_quoted());
1120
1121        assert_eq!(unquoted.canonical(), "mytable");
1122        assert_eq!(quoted.canonical(), "MyTable");
1123    }
1124
1125    #[test]
1126    fn test_from_canonical() {
1127        let id = TableIdentifier::from_canonical("mytable".to_string(), false);
1128        assert_eq!(id.canonical(), "mytable");
1129        assert_eq!(id.display(), "mytable");
1130        assert!(!id.is_quoted());
1131
1132        let quoted_id = TableIdentifier::from_canonical("MyTable".to_string(), true);
1133        assert_eq!(quoted_id.canonical(), "MyTable");
1134        assert_eq!(quoted_id.display(), "MyTable");
1135        assert!(quoted_id.is_quoted());
1136    }
1137
1138    #[test]
1139    fn test_into_canonical() {
1140        let id = TableIdentifier::new("MyTable", false);
1141        let canonical: String = id.into_canonical();
1142        assert_eq!(canonical, "mytable");
1143    }
1144
1145    #[test]
1146    fn test_sql_examples_from_issue() {
1147        // Examples from the issue description
1148
1149        // CREATE TABLE MyTable (id INT);
1150        // INSERT INTO mytable VALUES (1);  -- Should work
1151        // INSERT INTO MYTABLE VALUES (2);  -- Should work
1152        let created = TableIdentifier::new("MyTable", false);
1153        let lookup1 = TableIdentifier::new("mytable", false);
1154        let lookup2 = TableIdentifier::new("MYTABLE", false);
1155
1156        assert_eq!(created, lookup1);
1157        assert_eq!(created, lookup2);
1158
1159        // CREATE TABLE "MyTable" (id INT);  -- Different table!
1160        let quoted_created = TableIdentifier::new("MyTable", true);
1161        assert_ne!(created, quoted_created);
1162
1163        // SELECT * FROM "MyTable";  -- Only finds quoted table
1164        let quoted_lookup = TableIdentifier::new("MyTable", true);
1165        assert_eq!(quoted_created, quoted_lookup);
1166        assert_ne!(created, quoted_lookup);
1167
1168        // SELECT * FROM MyTable;  -- Only finds unquoted table
1169        let unquoted_lookup = TableIdentifier::new("MyTable", false);
1170        assert_eq!(created, unquoted_lookup);
1171        assert_ne!(quoted_created, unquoted_lookup);
1172    }
1173
1174    #[test]
1175    fn test_create_duplicate_detection() {
1176        // CREATE TABLE test (id INT);
1177        // CREATE TABLE TEST (id INT);  -- Should ERROR: table already exists
1178        let first = TableIdentifier::new("test", false);
1179        let second = TableIdentifier::new("TEST", false);
1180        assert_eq!(first, second); // Same table, should conflict
1181        assert_eq!(first.canonical(), "test"); // Both normalize to lowercase
1182
1183        // CREATE TABLE "TEST" (id INT);  -- Different table from unquoted!
1184        let quoted = TableIdentifier::new("TEST", true);
1185        assert_ne!(first, quoted); // Different table, no conflict (quoted "TEST" != "test")
1186
1187        // CREATE TABLE "test" (id INT);  -- Same canonical as unquoted test
1188        let quoted_lower = TableIdentifier::new("test", true);
1189        assert_eq!(first, quoted_lower); // Same canonical form
1190    }
1191
1192    #[test]
1193    fn test_qualified_identifier_unquoted_both() {
1194        // myApp.users → myapp.users
1195        let id = TableIdentifier::qualified("myApp", false, "users", false);
1196
1197        assert!(id.is_qualified());
1198        assert_eq!(id.canonical(), "myapp.users");
1199        assert_eq!(id.display(), "myApp.users");
1200
1201        assert_eq!(id.schema_canonical(), Some("myapp"));
1202        assert_eq!(id.schema_display(), Some("myApp"));
1203        assert!(!id.is_schema_quoted());
1204
1205        assert_eq!(id.table_canonical(), "users");
1206        assert_eq!(id.table_display(), "users");
1207        assert!(!id.is_table_quoted());
1208    }
1209
1210    #[test]
1211    fn test_qualified_identifier_quoted_schema() {
1212        // "myApp".users → myApp.users
1213        let id = TableIdentifier::qualified("myApp", true, "users", false);
1214
1215        assert!(id.is_qualified());
1216        assert_eq!(id.canonical(), "myApp.users");
1217        assert_eq!(id.display(), "myApp.users");
1218
1219        assert_eq!(id.schema_canonical(), Some("myApp"));
1220        assert_eq!(id.schema_display(), Some("myApp"));
1221        assert!(id.is_schema_quoted());
1222
1223        assert_eq!(id.table_canonical(), "users");
1224        assert_eq!(id.table_display(), "users");
1225        assert!(!id.is_table_quoted());
1226    }
1227
1228    #[test]
1229    fn test_qualified_identifier_quoted_table() {
1230        // myapp."Users" → myapp.Users
1231        let id = TableIdentifier::qualified("myapp", false, "Users", true);
1232
1233        assert!(id.is_qualified());
1234        assert_eq!(id.canonical(), "myapp.Users");
1235        assert_eq!(id.display(), "myapp.Users");
1236
1237        assert_eq!(id.schema_canonical(), Some("myapp"));
1238        assert_eq!(id.schema_display(), Some("myapp"));
1239        assert!(!id.is_schema_quoted());
1240
1241        assert_eq!(id.table_canonical(), "Users");
1242        assert_eq!(id.table_display(), "Users");
1243        assert!(id.is_table_quoted());
1244    }
1245
1246    #[test]
1247    fn test_qualified_identifier_quoted_both() {
1248        // "myApp"."Users" → myApp.Users
1249        let id = TableIdentifier::qualified("myApp", true, "Users", true);
1250
1251        assert!(id.is_qualified());
1252        assert_eq!(id.canonical(), "myApp.Users");
1253        assert_eq!(id.display(), "myApp.Users");
1254
1255        assert_eq!(id.schema_canonical(), Some("myApp"));
1256        assert_eq!(id.schema_display(), Some("myApp"));
1257        assert!(id.is_schema_quoted());
1258
1259        assert_eq!(id.table_canonical(), "Users");
1260        assert_eq!(id.table_display(), "Users");
1261        assert!(id.is_table_quoted());
1262    }
1263
1264    #[test]
1265    fn test_qualified_identifier_equality() {
1266        // Schema case differs, but both unquoted → should match
1267        let id1 = TableIdentifier::qualified("myApp", false, "users", false);
1268        let id2 = TableIdentifier::qualified("MYAPP", false, "USERS", false);
1269        assert_eq!(id1, id2);
1270        assert_eq!(id1.canonical(), "myapp.users");
1271        assert_eq!(id2.canonical(), "myapp.users");
1272
1273        // Schema quoted with different case → should NOT match
1274        let id3 = TableIdentifier::qualified("myApp", true, "users", false);
1275        let id4 = TableIdentifier::qualified("MYAPP", true, "users", false);
1276        assert_ne!(id3, id4);
1277        assert_eq!(id3.canonical(), "myApp.users");
1278        assert_eq!(id4.canonical(), "MYAPP.users");
1279
1280        // Table quoted with different case → should NOT match
1281        let id5 = TableIdentifier::qualified("myapp", false, "Users", true);
1282        let id6 = TableIdentifier::qualified("myapp", false, "USERS", true);
1283        assert_ne!(id5, id6);
1284        assert_eq!(id5.canonical(), "myapp.Users");
1285        assert_eq!(id6.canonical(), "myapp.USERS");
1286    }
1287
1288    #[test]
1289    fn test_qualified_vs_simple_identifier() {
1290        // Qualified and simple identifiers with same table name should NOT be equal
1291        let simple = TableIdentifier::new("users", false);
1292        let qualified = TableIdentifier::qualified("myapp", false, "users", false);
1293
1294        assert_ne!(simple, qualified);
1295        assert_eq!(simple.canonical(), "users");
1296        assert_eq!(qualified.canonical(), "myapp.users");
1297
1298        assert!(!simple.is_qualified());
1299        assert!(qualified.is_qualified());
1300    }
1301
1302    #[test]
1303    fn test_qualified_identifier_hashmap() {
1304        let mut map: HashMap<TableIdentifier, i32> = HashMap::new();
1305
1306        // Insert with quoted schema, unquoted table
1307        let key = TableIdentifier::qualified("myApp", true, "users", false);
1308        map.insert(key, 42);
1309
1310        // Should find with exact schema case (case-sensitive), any table case (case-insensitive)
1311        let lookup1 = TableIdentifier::qualified("myApp", true, "USERS", false);
1312        assert_eq!(map.get(&lookup1), Some(&42));
1313
1314        // Should NOT find with different schema case
1315        let lookup2 = TableIdentifier::qualified("MYAPP", true, "users", false);
1316        assert_eq!(map.get(&lookup2), None);
1317
1318        // Should NOT find with unquoted schema (even if lowercase matches)
1319        let lookup3 = TableIdentifier::qualified("myApp", false, "users", false);
1320        assert_eq!(map.get(&lookup3), None);
1321    }
1322
1323    // ==================== ColumnIdentifier Tests ====================
1324
1325    #[test]
1326    fn test_column_unquoted_case_insensitive() {
1327        let c1 = ColumnIdentifier::simple("MyColumn", false);
1328        let c2 = ColumnIdentifier::simple("mycolumn", false);
1329        let c3 = ColumnIdentifier::simple("MYCOLUMN", false);
1330
1331        // All unquoted variations should be equal
1332        assert_eq!(c1, c2);
1333        assert_eq!(c2, c3);
1334
1335        // Canonical should be lowercase
1336        assert_eq!(c1.canonical(), "mycolumn");
1337        assert_eq!(c2.canonical(), "mycolumn");
1338        assert_eq!(c3.canonical(), "mycolumn");
1339
1340        // Display preserves original
1341        assert_eq!(c1.display(), "MyColumn");
1342        assert_eq!(c2.display(), "mycolumn");
1343        assert_eq!(c3.display(), "MYCOLUMN");
1344    }
1345
1346    #[test]
1347    fn test_column_quoted_case_sensitive() {
1348        let c1 = ColumnIdentifier::simple("MyColumn", true);
1349        let c2 = ColumnIdentifier::simple("mycolumn", true);
1350        let c3 = ColumnIdentifier::simple("MYCOLUMN", true);
1351
1352        // Quoted identifiers with different cases should NOT be equal
1353        assert_ne!(c1, c2);
1354        assert_ne!(c2, c3);
1355        assert_ne!(c1, c3);
1356
1357        // Canonical preserves exact case
1358        assert_eq!(c1.canonical(), "MyColumn");
1359        assert_eq!(c2.canonical(), "mycolumn");
1360        assert_eq!(c3.canonical(), "MYCOLUMN");
1361    }
1362
1363    #[test]
1364    fn test_column_qualified() {
1365        let c = ColumnIdentifier::qualified("Users", false, "ID", false);
1366        assert_eq!(c.canonical(), "users.id");
1367        assert_eq!(c.table_canonical(), Some("users"));
1368        assert_eq!(c.column_canonical(), "id");
1369        assert!(c.is_qualified());
1370        assert!(!c.is_fully_qualified());
1371        assert!(!c.is_ambiguous());
1372    }
1373
1374    #[test]
1375    fn test_column_fully_qualified() {
1376        let c = ColumnIdentifier::fully_qualified(
1377            "myApp", true, // quoted schema
1378            "users", false, // unquoted table
1379            "ID", false, // unquoted column
1380        );
1381        assert_eq!(c.canonical(), "myApp.users.id");
1382        assert_eq!(c.schema_canonical(), Some("myApp"));
1383        assert_eq!(c.table_canonical(), Some("users"));
1384        assert_eq!(c.column_canonical(), "id");
1385        assert!(c.is_qualified());
1386        assert!(c.is_fully_qualified());
1387        assert!(!c.is_ambiguous());
1388    }
1389
1390    #[test]
1391    fn test_column_convenience_constructors() {
1392        let unquoted = ColumnIdentifier::unquoted("MyColumn");
1393        assert_eq!(unquoted.canonical(), "mycolumn");
1394        assert!(!unquoted.is_column_quoted());
1395
1396        let quoted = ColumnIdentifier::quoted("MyColumn");
1397        assert_eq!(quoted.canonical(), "MyColumn");
1398        assert!(quoted.is_column_quoted());
1399
1400        let table_col = ColumnIdentifier::table_column("users", "id");
1401        assert_eq!(table_col.canonical(), "users.id");
1402        assert!(table_col.is_qualified());
1403    }
1404
1405    #[test]
1406    fn test_column_hashmap_lookup() {
1407        let mut map: HashMap<ColumnIdentifier, i32> = HashMap::new();
1408
1409        // Insert with unquoted identifier
1410        let key = ColumnIdentifier::simple("userId", false);
1411        map.insert(key, 42);
1412
1413        // Should find with different case (unquoted)
1414        let lookup1 = ColumnIdentifier::simple("USERID", false);
1415        let lookup2 = ColumnIdentifier::simple("UserId", false);
1416        let lookup3 = ColumnIdentifier::simple("userid", false);
1417
1418        assert_eq!(map.get(&lookup1), Some(&42));
1419        assert_eq!(map.get(&lookup2), Some(&42));
1420        assert_eq!(map.get(&lookup3), Some(&42));
1421
1422        // Quoted "userid" (lowercase) SHOULD find the value
1423        let quoted_lower = ColumnIdentifier::simple("userid", true);
1424        assert_eq!(map.get(&quoted_lower), Some(&42));
1425
1426        // But quoted "USERID" (uppercase) should NOT find it
1427        let quoted_upper = ColumnIdentifier::simple("USERID", true);
1428        assert_eq!(map.get(&quoted_upper), None);
1429    }
1430
1431    #[test]
1432    fn test_column_matches_column_name() {
1433        let c = ColumnIdentifier::qualified("users", false, "id", false);
1434
1435        // Case-insensitive match
1436        assert!(c.matches_column_name("id", false));
1437        assert!(c.matches_column_name("ID", false));
1438        assert!(c.matches_column_name("Id", false));
1439
1440        // Case-sensitive match
1441        assert!(c.matches_column_name("id", true));
1442        assert!(!c.matches_column_name("ID", true));
1443        assert!(!c.matches_column_name("Id", true));
1444    }
1445
1446    #[test]
1447    fn test_column_matches() {
1448        // Same qualified columns
1449        let c1 = ColumnIdentifier::qualified("users", false, "id", false);
1450        let c2 = ColumnIdentifier::qualified("USERS", false, "ID", false);
1451        assert!(c1.matches(&c2));
1452
1453        // Unqualified matches qualified by column name
1454        let unqualified = ColumnIdentifier::simple("id", false);
1455        assert!(unqualified.matches(&c1));
1456        assert!(c1.matches(&unqualified));
1457
1458        // Different tables should not match when both qualified
1459        let c3 = ColumnIdentifier::qualified("orders", false, "id", false);
1460        assert!(!c1.matches(&c3));
1461
1462        // But unqualified still matches
1463        assert!(unqualified.matches(&c3));
1464    }
1465
1466    #[test]
1467    fn test_column_resolve_against() {
1468        let col = ColumnIdentifier::simple("id", false);
1469        let table = TableIdentifier::new("users", false);
1470
1471        let resolved = col.resolve_against(&table);
1472        assert_eq!(resolved.canonical(), "users.id");
1473        assert!(resolved.is_qualified());
1474
1475        // Already qualified column should not change
1476        let qualified = ColumnIdentifier::qualified("orders", false, "id", false);
1477        let resolved2 = qualified.resolve_against(&table);
1478        assert_eq!(resolved2.canonical(), "orders.id");
1479    }
1480
1481    #[test]
1482    fn test_column_resolve_against_qualified_table() {
1483        let col = ColumnIdentifier::simple("id", false);
1484        let table = TableIdentifier::qualified("myapp", false, "users", false);
1485
1486        let resolved = col.resolve_against(&table);
1487        assert_eq!(resolved.canonical(), "myapp.users.id");
1488        assert!(resolved.is_fully_qualified());
1489    }
1490
1491    #[test]
1492    fn test_column_unqualify() {
1493        let qualified = ColumnIdentifier::qualified("users", false, "ID", true);
1494        let unqualified = qualified.unqualify();
1495
1496        assert!(!unqualified.is_qualified());
1497        assert_eq!(unqualified.column_canonical(), "ID"); // Quoted preserved
1498        assert!(unqualified.is_column_quoted());
1499    }
1500
1501    #[test]
1502    fn test_column_with_table() {
1503        let col = ColumnIdentifier::simple("id", false);
1504        let with_table = col.with_table("users", false);
1505
1506        assert_eq!(with_table.canonical(), "users.id");
1507        assert_eq!(with_table.table_canonical(), Some("users"));
1508    }
1509
1510    #[test]
1511    fn test_column_display_trait() {
1512        let c = ColumnIdentifier::qualified("Users", false, "ID", false);
1513        assert_eq!(format!("{}", c), "Users.ID");
1514
1515        let simple = ColumnIdentifier::simple("MyColumn", false);
1516        assert_eq!(format!("{}", simple), "MyColumn");
1517    }
1518
1519    #[test]
1520    fn test_column_from_traits() {
1521        let c1: ColumnIdentifier = "MyColumn".into();
1522        let c2: ColumnIdentifier = String::from("MyColumn").into();
1523
1524        assert_eq!(c1, c2);
1525        assert_eq!(c1.canonical(), "mycolumn"); // From assumes unquoted
1526    }
1527
1528    #[test]
1529    fn test_column_from_canonical() {
1530        let c = ColumnIdentifier::from_canonical("mycolumn".to_string(), false);
1531        assert_eq!(c.canonical(), "mycolumn");
1532        assert_eq!(c.display(), "mycolumn");
1533        assert!(!c.is_column_quoted());
1534
1535        let quoted = ColumnIdentifier::from_canonical("MyColumn".to_string(), true);
1536        assert_eq!(quoted.canonical(), "MyColumn");
1537        assert_eq!(quoted.display(), "MyColumn");
1538        assert!(quoted.is_column_quoted());
1539    }
1540
1541    #[test]
1542    fn test_column_into_canonical() {
1543        let c = ColumnIdentifier::simple("MyColumn", false);
1544        let canonical: String = c.into_canonical();
1545        assert_eq!(canonical, "mycolumn");
1546    }
1547
1548    #[test]
1549    fn test_column_qualified_equality() {
1550        // Table case differs, but both unquoted → should match
1551        let c1 = ColumnIdentifier::qualified("Users", false, "id", false);
1552        let c2 = ColumnIdentifier::qualified("USERS", false, "ID", false);
1553        assert_eq!(c1, c2);
1554        assert_eq!(c1.canonical(), "users.id");
1555
1556        // Table quoted with different case → should NOT match
1557        let c3 = ColumnIdentifier::qualified("Users", true, "id", false);
1558        let c4 = ColumnIdentifier::qualified("USERS", true, "id", false);
1559        assert_ne!(c3, c4);
1560
1561        // Column quoted with different case → should NOT match
1562        let c5 = ColumnIdentifier::qualified("users", false, "Id", true);
1563        let c6 = ColumnIdentifier::qualified("users", false, "ID", true);
1564        assert_ne!(c5, c6);
1565    }
1566
1567    #[test]
1568    fn test_column_qualified_hashmap() {
1569        let mut map: HashMap<ColumnIdentifier, i32> = HashMap::new();
1570
1571        // Insert with quoted table, unquoted column
1572        let key = ColumnIdentifier::qualified("Users", true, "id", false);
1573        map.insert(key, 42);
1574
1575        // Should find with exact table case, any column case
1576        let lookup1 = ColumnIdentifier::qualified("Users", true, "ID", false);
1577        assert_eq!(map.get(&lookup1), Some(&42));
1578
1579        // Should NOT find with different table case
1580        let lookup2 = ColumnIdentifier::qualified("USERS", true, "id", false);
1581        assert_eq!(map.get(&lookup2), None);
1582
1583        // Should NOT find with unquoted table
1584        let lookup3 = ColumnIdentifier::qualified("Users", false, "id", false);
1585        assert_eq!(map.get(&lookup3), None);
1586    }
1587
1588    #[test]
1589    fn test_column_issue_examples() {
1590        // Examples from issue #4527
1591
1592        // Unquoted case-insensitive
1593        let c1 = ColumnIdentifier::simple("MyColumn", false);
1594        let c2 = ColumnIdentifier::simple("mycolumn", false);
1595        let c3 = ColumnIdentifier::simple("MYCOLUMN", false);
1596        assert_eq!(c1, c2);
1597        assert_eq!(c2, c3);
1598        assert_eq!(c1.canonical(), "mycolumn");
1599
1600        // Quoted case-sensitive
1601        let q1 = ColumnIdentifier::simple("MyColumn", true);
1602        let q2 = ColumnIdentifier::simple("mycolumn", true);
1603        assert_ne!(q1, q2);
1604        assert_eq!(q1.canonical(), "MyColumn");
1605
1606        // Qualified column
1607        let qc = ColumnIdentifier::qualified("Users", false, "ID", false);
1608        assert_eq!(qc.canonical(), "users.id");
1609        assert_eq!(qc.table_canonical(), Some("users"));
1610        assert_eq!(qc.column_canonical(), "id");
1611
1612        // Fully qualified
1613        let fq = ColumnIdentifier::fully_qualified(
1614            "myApp", true, // quoted schema
1615            "users", false, // unquoted table
1616            "ID", false, // unquoted column
1617        );
1618        assert_eq!(fq.canonical(), "myApp.users.id");
1619    }
1620
1621    #[test]
1622    fn test_column_ambiguous_predicates() {
1623        let simple = ColumnIdentifier::simple("id", false);
1624        assert!(simple.is_ambiguous());
1625        assert!(!simple.is_qualified());
1626        assert!(!simple.is_fully_qualified());
1627
1628        let qualified = ColumnIdentifier::qualified("users", false, "id", false);
1629        assert!(!qualified.is_ambiguous());
1630        assert!(qualified.is_qualified());
1631        assert!(!qualified.is_fully_qualified());
1632
1633        let fully = ColumnIdentifier::fully_qualified("myapp", false, "users", false, "id", false);
1634        assert!(!fully.is_ambiguous());
1635        assert!(fully.is_qualified());
1636        assert!(fully.is_fully_qualified());
1637    }
1638}