Skip to main content

polyglot_sql/
schema.rs

1//! Schema management for SQL queries
2//!
3//! This module provides functionality for:
4//! - Representing database schemas (tables, columns, types)
5//! - Looking up column types for query optimization
6//! - Normalizing identifiers per dialect
7//!
8//! Based on the Python implementation in `sqlglot/schema.py`.
9
10use crate::dialects::DialectType;
11use crate::expressions::DataType;
12use crate::trie::{Trie, TrieResult};
13use std::collections::{HashMap, HashSet};
14use thiserror::Error;
15
16/// Errors that can occur during schema operations
17#[derive(Debug, Error, Clone)]
18pub enum SchemaError {
19    #[error("Table not found: {0}")]
20    TableNotFound(String),
21
22    #[error("Ambiguous table: {table} matches multiple tables: {matches}")]
23    AmbiguousTable { table: String, matches: String },
24
25    #[error("Column not found: {column} in table {table}")]
26    ColumnNotFound { table: String, column: String },
27
28    #[error("Schema nesting depth mismatch: expected {expected}, got {actual}")]
29    DepthMismatch { expected: usize, actual: usize },
30
31    #[error("Invalid schema structure: {0}")]
32    InvalidStructure(String),
33}
34
35/// Result type for schema operations
36pub type SchemaResult<T> = Result<T, SchemaError>;
37
38/// Supported table argument names
39pub const TABLE_PARTS: &[&str] = &["this", "db", "catalog"];
40
41/// Abstract trait for database schemas
42pub trait Schema {
43    /// Get the dialect associated with this schema (if any)
44    fn dialect(&self) -> Option<DialectType>;
45
46    /// Add or update a table in the schema
47    fn add_table(
48        &mut self,
49        table: &str,
50        columns: &[(String, DataType)],
51        dialect: Option<DialectType>,
52    ) -> SchemaResult<()>;
53
54    /// Get column names for a table
55    fn column_names(&self, table: &str) -> SchemaResult<Vec<String>>;
56
57    /// Get the type of a column in a table
58    fn get_column_type(&self, table: &str, column: &str) -> SchemaResult<DataType>;
59
60    /// Check if a column exists in a table
61    fn has_column(&self, table: &str, column: &str) -> bool;
62
63    /// Get supported table argument levels
64    fn supported_table_args(&self) -> &[&str];
65
66    /// Check if the schema is empty
67    fn is_empty(&self) -> bool;
68
69    /// Get the nesting depth of the schema
70    fn depth(&self) -> usize;
71
72    /// Find which table(s) contain the given column name.
73    /// Returns table names that have the column. Used for correlated subquery resolution.
74    fn find_tables_for_column(&self, column: &str) -> Vec<String>;
75}
76
77/// A column with its type and visibility
78#[derive(Debug, Clone)]
79pub struct ColumnInfo {
80    pub data_type: DataType,
81    pub visible: bool,
82}
83
84impl ColumnInfo {
85    pub fn new(data_type: DataType) -> Self {
86        Self {
87            data_type,
88            visible: true,
89        }
90    }
91
92    pub fn with_visibility(data_type: DataType, visible: bool) -> Self {
93        Self { data_type, visible }
94    }
95}
96
97/// A mapping-based schema implementation
98///
99/// Supports nested schemas with different levels:
100/// - Level 1: `{table: {col: type}}`
101/// - Level 2: `{db: {table: {col: type}}}`
102/// - Level 3: `{catalog: {db: {table: {col: type}}}}`
103#[derive(Debug, Clone)]
104pub struct MappingSchema {
105    /// The actual schema data
106    mapping: HashMap<String, SchemaNode>,
107    /// Trie for efficient table lookup
108    mapping_trie: Trie<()>,
109    /// The dialect for this schema
110    dialect: Option<DialectType>,
111    /// Whether to normalize identifiers
112    normalize: bool,
113    /// Visible columns per table
114    visible: HashMap<String, HashSet<String>>,
115    /// Declared column order per normalized table path.
116    column_order: HashMap<String, Vec<String>>,
117    /// Cached depth
118    cached_depth: usize,
119}
120
121/// A node in the schema tree
122#[derive(Debug, Clone)]
123pub enum SchemaNode {
124    /// Intermediate node (database or catalog)
125    Namespace(HashMap<String, SchemaNode>),
126    /// Leaf node (table with columns)
127    Table(HashMap<String, ColumnInfo>),
128}
129
130impl Default for MappingSchema {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl MappingSchema {
137    /// Create a new empty schema
138    pub fn new() -> Self {
139        Self {
140            mapping: HashMap::new(),
141            mapping_trie: Trie::new(),
142            dialect: None,
143            normalize: true,
144            visible: HashMap::new(),
145            column_order: HashMap::new(),
146            cached_depth: 0,
147        }
148    }
149
150    /// Create a schema with a specific dialect
151    pub fn with_dialect(dialect: DialectType) -> Self {
152        Self {
153            dialect: Some(dialect),
154            ..Self::new()
155        }
156    }
157
158    /// Create a schema with normalization disabled
159    pub fn without_normalization(mut self) -> Self {
160        self.normalize = false;
161        self
162    }
163
164    /// Set visibility for columns in a table
165    pub fn set_visible_columns(&mut self, table: &str, columns: &[&str]) {
166        let key = self.normalize_name(table, true);
167        let cols: HashSet<String> = columns
168            .iter()
169            .map(|c| self.normalize_name(c, false))
170            .collect();
171        self.visible.insert(key, cols);
172    }
173
174    /// Normalize an identifier name based on dialect
175    fn normalize_name(&self, name: &str, is_table: bool) -> String {
176        if !self.normalize {
177            return name.to_string();
178        }
179
180        let identifier = crate::binding::schema_identifier(name);
181        if identifier.quoted {
182            return crate::optimizer::normalize_identifiers::normalize_identifier(
183                identifier,
184                crate::optimizer::normalize_identifiers::get_normalization_strategy(self.dialect),
185            )
186            .name;
187        }
188        // Default normalization: lowercase
189        // Different dialects may have different rules
190        match self.dialect {
191            Some(DialectType::BigQuery) if is_table => {
192                // BigQuery preserves case for tables
193                name.to_string()
194            }
195            Some(DialectType::Snowflake) => {
196                // Snowflake uppercases by default
197                name.to_uppercase()
198            }
199            _ => {
200                // Most dialects lowercase
201                name.to_lowercase()
202            }
203        }
204    }
205
206    /// Parse a qualified table name into parts
207    fn parse_table_parts(&self, table: &str) -> Vec<String> {
208        table
209            .split('.')
210            .map(|s| self.normalize_name(s.trim(), true))
211            .collect()
212    }
213
214    /// Get the column mapping for a table
215    fn find_table(&self, table: &str) -> SchemaResult<&HashMap<String, ColumnInfo>> {
216        let parts = self.parse_table_parts(table);
217
218        // Use trie to find table
219        let reversed_parts: Vec<_> = parts.iter().rev().map(|s| s.as_str()).collect();
220        let key: String = reversed_parts.join(".");
221
222        let (result, _) = self.mapping_trie.in_trie(&key);
223
224        match result {
225            TrieResult::Failed => Err(SchemaError::TableNotFound(table.to_string())),
226            TrieResult::Prefix => {
227                // Ambiguous - multiple tables match
228                Err(SchemaError::AmbiguousTable {
229                    table: table.to_string(),
230                    matches: "multiple matches".to_string(),
231                })
232            }
233            TrieResult::Exists => {
234                // Navigate to the table
235                self.navigate_to_table(&parts)
236            }
237        }
238    }
239
240    /// Navigate the schema tree to find a table's columns
241    fn navigate_to_table(&self, parts: &[String]) -> SchemaResult<&HashMap<String, ColumnInfo>> {
242        let mut current = &self.mapping;
243
244        for (i, part) in parts.iter().enumerate() {
245            match current.get(part) {
246                Some(SchemaNode::Namespace(inner)) => {
247                    current = inner;
248                }
249                Some(SchemaNode::Table(cols)) => {
250                    if i == parts.len() - 1 {
251                        return Ok(cols);
252                    } else {
253                        return Err(SchemaError::InvalidStructure(format!(
254                            "Found table at {} but expected more levels",
255                            parts[..=i].join(".")
256                        )));
257                    }
258                }
259                None => {
260                    return Err(SchemaError::TableNotFound(parts.join(".")));
261                }
262            }
263        }
264
265        // We've exhausted parts but didn't find a table
266        Err(SchemaError::TableNotFound(parts.join(".")))
267    }
268
269    /// Add a table to the schema
270    fn add_table_internal(
271        &mut self,
272        parts: &[String],
273        columns: HashMap<String, ColumnInfo>,
274    ) -> SchemaResult<()> {
275        if parts.is_empty() {
276            return Err(SchemaError::InvalidStructure(
277                "Table name cannot be empty".to_string(),
278            ));
279        }
280
281        // Build trie key (reversed parts)
282        let trie_key: String = parts.iter().rev().cloned().collect::<Vec<_>>().join(".");
283        self.mapping_trie.insert(&trie_key, ());
284
285        // Navigate/create path to table
286        let mut current = &mut self.mapping;
287
288        for (i, part) in parts.iter().enumerate() {
289            let is_last = i == parts.len() - 1;
290
291            if is_last {
292                // Insert table
293                current.insert(part.clone(), SchemaNode::Table(columns));
294                return Ok(());
295            } else {
296                // Navigate or create namespace
297                let entry = current
298                    .entry(part.clone())
299                    .or_insert_with(|| SchemaNode::Namespace(HashMap::new()));
300
301                match entry {
302                    SchemaNode::Namespace(inner) => {
303                        current = inner;
304                    }
305                    SchemaNode::Table(_) => {
306                        return Err(SchemaError::InvalidStructure(format!(
307                            "Expected namespace at {} but found table",
308                            parts[..=i].join(".")
309                        )));
310                    }
311                }
312            }
313        }
314
315        Ok(())
316    }
317
318    /// Update cached depth
319    fn update_depth(&mut self) {
320        self.cached_depth = self.calculate_depth(&self.mapping);
321    }
322
323    fn calculate_depth(&self, mapping: &HashMap<String, SchemaNode>) -> usize {
324        if mapping.is_empty() {
325            return 0;
326        }
327
328        let mut max_depth = 1;
329        for node in mapping.values() {
330            match node {
331                SchemaNode::Namespace(inner) => {
332                    let d = 1 + self.calculate_depth(inner);
333                    if d > max_depth {
334                        max_depth = d;
335                    }
336                }
337                SchemaNode::Table(_) => {
338                    // Tables don't add to depth beyond their level
339                }
340            }
341        }
342        max_depth
343    }
344}
345
346impl Schema for MappingSchema {
347    fn dialect(&self) -> Option<DialectType> {
348        self.dialect
349    }
350
351    fn add_table(
352        &mut self,
353        table: &str,
354        columns: &[(String, DataType)],
355        _dialect: Option<DialectType>,
356    ) -> SchemaResult<()> {
357        let parts = self.parse_table_parts(table);
358
359        let cols: HashMap<String, ColumnInfo> = columns
360            .iter()
361            .map(|(name, dtype)| {
362                let normalized_name = self.normalize_name(name, false);
363                (normalized_name, ColumnInfo::new(dtype.clone()))
364            })
365            .collect();
366        let column_order: Vec<String> = columns
367            .iter()
368            .map(|(name, _)| self.normalize_name(name, false))
369            .collect();
370
371        self.add_table_internal(&parts, cols)?;
372        self.column_order.insert(parts.join("."), column_order);
373        self.update_depth();
374        Ok(())
375    }
376
377    fn column_names(&self, table: &str) -> SchemaResult<Vec<String>> {
378        let cols = self.find_table(table)?;
379        let table_key = self.normalize_name(table, true);
380        let ordered_columns = self
381            .column_order
382            .get(&self.parse_table_parts(table).join("."));
383
384        // Check visibility
385        if let Some(visible_cols) = self.visible.get(&table_key) {
386            Ok(ordered_columns
387                .map(|columns| {
388                    columns
389                        .iter()
390                        .filter(|column| {
391                            cols.contains_key(*column) && visible_cols.contains(*column)
392                        })
393                        .cloned()
394                        .collect()
395                })
396                .unwrap_or_else(|| {
397                    cols.keys()
398                        .filter(|column| visible_cols.contains(*column))
399                        .cloned()
400                        .collect()
401                }))
402        } else if let Some(columns) = ordered_columns {
403            Ok(columns
404                .iter()
405                .filter(|column| cols.contains_key(*column))
406                .cloned()
407                .collect())
408        } else {
409            Ok(cols.keys().cloned().collect())
410        }
411    }
412
413    fn get_column_type(&self, table: &str, column: &str) -> SchemaResult<DataType> {
414        let cols = self.find_table(table)?;
415        let normalized_col = self.normalize_name(column, false);
416
417        cols.get(&normalized_col)
418            .map(|info| info.data_type.clone())
419            .ok_or_else(|| SchemaError::ColumnNotFound {
420                table: table.to_string(),
421                column: column.to_string(),
422            })
423    }
424
425    fn has_column(&self, table: &str, column: &str) -> bool {
426        self.get_column_type(table, column).is_ok()
427    }
428
429    fn supported_table_args(&self) -> &[&str] {
430        let depth = self.depth();
431        if depth == 0 {
432            &[]
433        } else if depth <= 3 {
434            &TABLE_PARTS[..depth]
435        } else {
436            TABLE_PARTS
437        }
438    }
439
440    fn is_empty(&self) -> bool {
441        self.mapping.is_empty()
442    }
443
444    fn depth(&self) -> usize {
445        self.cached_depth
446    }
447
448    fn find_tables_for_column(&self, column: &str) -> Vec<String> {
449        let normalized = normalize_name(column, self.dialect, false, self.normalize);
450        let mut result = Vec::new();
451        for table_name in self.mapping.keys() {
452            if self.has_column(table_name, &normalized) {
453                result.push(table_name.clone());
454            }
455        }
456        result
457    }
458}
459
460/// Normalize a table or column name according to dialect rules
461pub fn normalize_name(
462    name: &str,
463    dialect: Option<DialectType>,
464    is_table: bool,
465    normalize: bool,
466) -> String {
467    if !normalize {
468        return name.to_string();
469    }
470    let identifier = crate::binding::schema_identifier(name);
471    if identifier.quoted {
472        return crate::optimizer::normalize_identifiers::normalize_identifier(
473            identifier,
474            crate::optimizer::normalize_identifiers::get_normalization_strategy(dialect),
475        )
476        .name;
477    }
478
479    match dialect {
480        Some(DialectType::BigQuery) if is_table => name.to_string(),
481        Some(DialectType::Snowflake) => name.to_uppercase(),
482        _ => name.to_lowercase(),
483    }
484}
485
486/// Ensure we have a schema instance
487pub fn ensure_schema(schema: Option<MappingSchema>) -> MappingSchema {
488    schema.unwrap_or_default()
489}
490
491/// Helper to build a schema from a simple map
492///
493/// # Example
494///
495/// ```
496/// use polyglot_sql::schema::{MappingSchema, Schema, from_simple_map};
497/// use polyglot_sql::expressions::DataType;
498///
499/// let schema = from_simple_map(&[
500///     ("users", &[("id", DataType::Int { length: None, integer_spelling: false }), ("name", DataType::VarChar { length: Some(255), parenthesized_length: false })]),
501///     ("orders", &[("id", DataType::Int { length: None, integer_spelling: false }), ("user_id", DataType::Int { length: None, integer_spelling: false })]),
502/// ]);
503///
504/// assert_eq!(schema.column_names("users").unwrap().len(), 2);
505/// ```
506pub fn from_simple_map(tables: &[(&str, &[(&str, DataType)])]) -> MappingSchema {
507    let mut schema = MappingSchema::new();
508
509    for (table_name, columns) in tables {
510        let cols: Vec<(String, DataType)> = columns
511            .iter()
512            .map(|(name, dtype)| (name.to_string(), dtype.clone()))
513            .collect();
514
515        schema.add_table(table_name, &cols, None).ok();
516    }
517
518    schema
519}
520
521/// Flatten a nested schema to get all table paths
522pub fn flatten_schema_paths(schema: &MappingSchema) -> Vec<Vec<String>> {
523    let mut paths = Vec::new();
524    flatten_schema_paths_recursive(&schema.mapping, Vec::new(), &mut paths);
525    paths
526}
527
528fn flatten_schema_paths_recursive(
529    mapping: &HashMap<String, SchemaNode>,
530    prefix: Vec<String>,
531    paths: &mut Vec<Vec<String>>,
532) {
533    for (key, node) in mapping {
534        let mut path = prefix.clone();
535        path.push(key.clone());
536
537        match node {
538            SchemaNode::Namespace(inner) => {
539                flatten_schema_paths_recursive(inner, path, paths);
540            }
541            SchemaNode::Table(_) => {
542                paths.push(path);
543            }
544        }
545    }
546}
547
548/// Set a value in a nested dictionary-like structure
549pub fn nested_set<V: Clone>(
550    map: &mut HashMap<String, HashMap<String, V>>,
551    keys: &[String],
552    value: V,
553) {
554    if keys.is_empty() {
555        return;
556    }
557
558    if keys.len() == 1 {
559        // Can't set at single level - need at least 2 keys
560        return;
561    }
562
563    let outer_key = &keys[0];
564    let inner_key = &keys[1];
565
566    map.entry(outer_key.clone())
567        .or_insert_with(HashMap::new)
568        .insert(inner_key.clone(), value);
569}
570
571/// Get a value from a nested dictionary-like structure
572pub fn nested_get<'a, V>(
573    map: &'a HashMap<String, HashMap<String, V>>,
574    keys: &[String],
575) -> Option<&'a V> {
576    if keys.len() != 2 {
577        return None;
578    }
579
580    map.get(&keys[0])?.get(&keys[1])
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    #[test]
588    fn test_empty_schema() {
589        let schema = MappingSchema::new();
590        assert!(schema.is_empty());
591        assert_eq!(schema.depth(), 0);
592    }
593
594    #[test]
595    fn test_add_table() {
596        let mut schema = MappingSchema::new();
597        let columns = vec![
598            (
599                "id".to_string(),
600                DataType::Int {
601                    length: None,
602                    integer_spelling: false,
603                },
604            ),
605            (
606                "name".to_string(),
607                DataType::VarChar {
608                    length: Some(255),
609                    parenthesized_length: false,
610                },
611            ),
612        ];
613
614        schema.add_table("users", &columns, None).unwrap();
615
616        assert!(!schema.is_empty());
617        assert_eq!(schema.depth(), 1);
618        assert!(schema.has_column("users", "id"));
619        assert!(schema.has_column("users", "name"));
620        assert!(!schema.has_column("users", "email"));
621    }
622
623    #[test]
624    fn test_qualified_table_names() {
625        let mut schema = MappingSchema::new();
626        let columns = vec![(
627            "id".to_string(),
628            DataType::Int {
629                length: None,
630                integer_spelling: false,
631            },
632        )];
633
634        schema.add_table("mydb.users", &columns, None).unwrap();
635
636        assert!(schema.has_column("mydb.users", "id"));
637        assert_eq!(schema.depth(), 2);
638    }
639
640    #[test]
641    fn test_catalog_db_table() {
642        let mut schema = MappingSchema::new();
643        let columns = vec![(
644            "id".to_string(),
645            DataType::Int {
646                length: None,
647                integer_spelling: false,
648            },
649        )];
650
651        schema
652            .add_table("catalog.mydb.users", &columns, None)
653            .unwrap();
654
655        assert!(schema.has_column("catalog.mydb.users", "id"));
656        assert_eq!(schema.depth(), 3);
657    }
658
659    #[test]
660    fn test_get_column_type() {
661        let mut schema = MappingSchema::new();
662        let columns = vec![
663            (
664                "id".to_string(),
665                DataType::Int {
666                    length: None,
667                    integer_spelling: false,
668                },
669            ),
670            (
671                "name".to_string(),
672                DataType::VarChar {
673                    length: Some(255),
674                    parenthesized_length: false,
675                },
676            ),
677        ];
678
679        schema.add_table("users", &columns, None).unwrap();
680
681        let id_type = schema.get_column_type("users", "id").unwrap();
682        assert!(matches!(id_type, DataType::Int { .. }));
683
684        let name_type = schema.get_column_type("users", "name").unwrap();
685        assert!(matches!(
686            name_type,
687            DataType::VarChar {
688                length: Some(255),
689                parenthesized_length: false
690            }
691        ));
692    }
693
694    #[test]
695    fn test_column_names() {
696        let mut schema = MappingSchema::new();
697        let columns = vec![
698            (
699                "id".to_string(),
700                DataType::Int {
701                    length: None,
702                    integer_spelling: false,
703                },
704            ),
705            (
706                "name".to_string(),
707                DataType::VarChar {
708                    length: None,
709                    parenthesized_length: false,
710                },
711            ),
712        ];
713
714        schema.add_table("users", &columns, None).unwrap();
715
716        let names = schema.column_names("users").unwrap();
717        assert_eq!(names, vec!["id", "name"]);
718    }
719
720    #[test]
721    fn test_table_not_found() {
722        let schema = MappingSchema::new();
723        let result = schema.column_names("nonexistent");
724        assert!(matches!(result, Err(SchemaError::TableNotFound(_))));
725    }
726
727    #[test]
728    fn test_column_not_found() {
729        let mut schema = MappingSchema::new();
730        let columns = vec![(
731            "id".to_string(),
732            DataType::Int {
733                length: None,
734                integer_spelling: false,
735            },
736        )];
737        schema.add_table("users", &columns, None).unwrap();
738
739        let result = schema.get_column_type("users", "nonexistent");
740        assert!(matches!(result, Err(SchemaError::ColumnNotFound { .. })));
741    }
742
743    #[test]
744    fn test_normalize_name_default() {
745        let name = normalize_name("MyTable", None, true, true);
746        assert_eq!(name, "mytable");
747    }
748
749    #[test]
750    fn test_normalize_name_snowflake() {
751        let name = normalize_name("MyTable", Some(DialectType::Snowflake), true, true);
752        assert_eq!(name, "MYTABLE");
753    }
754
755    #[test]
756    fn test_normalize_disabled() {
757        let name = normalize_name("MyTable", None, true, false);
758        assert_eq!(name, "MyTable");
759    }
760
761    #[test]
762    fn test_from_simple_map() {
763        let schema = from_simple_map(&[
764            (
765                "users",
766                &[
767                    (
768                        "id",
769                        DataType::Int {
770                            length: None,
771                            integer_spelling: false,
772                        },
773                    ),
774                    (
775                        "name",
776                        DataType::VarChar {
777                            length: None,
778                            parenthesized_length: false,
779                        },
780                    ),
781                ],
782            ),
783            (
784                "orders",
785                &[
786                    (
787                        "id",
788                        DataType::Int {
789                            length: None,
790                            integer_spelling: false,
791                        },
792                    ),
793                    (
794                        "user_id",
795                        DataType::Int {
796                            length: None,
797                            integer_spelling: false,
798                        },
799                    ),
800                ],
801            ),
802        ]);
803
804        assert!(schema.has_column("users", "id"));
805        assert!(schema.has_column("users", "name"));
806        assert!(schema.has_column("orders", "id"));
807        assert!(schema.has_column("orders", "user_id"));
808    }
809
810    #[test]
811    fn test_flatten_schema_paths() {
812        let mut schema = MappingSchema::new();
813        schema
814            .add_table(
815                "db1.table1",
816                &[(
817                    "id".to_string(),
818                    DataType::Int {
819                        length: None,
820                        integer_spelling: false,
821                    },
822                )],
823                None,
824            )
825            .unwrap();
826        schema
827            .add_table(
828                "db1.table2",
829                &[(
830                    "id".to_string(),
831                    DataType::Int {
832                        length: None,
833                        integer_spelling: false,
834                    },
835                )],
836                None,
837            )
838            .unwrap();
839        schema
840            .add_table(
841                "db2.table1",
842                &[(
843                    "id".to_string(),
844                    DataType::Int {
845                        length: None,
846                        integer_spelling: false,
847                    },
848                )],
849                None,
850            )
851            .unwrap();
852
853        let paths = flatten_schema_paths(&schema);
854        assert_eq!(paths.len(), 3);
855    }
856
857    #[test]
858    fn test_visible_columns() {
859        let mut schema = MappingSchema::new();
860        let columns = vec![
861            (
862                "id".to_string(),
863                DataType::Int {
864                    length: None,
865                    integer_spelling: false,
866                },
867            ),
868            (
869                "name".to_string(),
870                DataType::VarChar {
871                    length: None,
872                    parenthesized_length: false,
873                },
874            ),
875            (
876                "password".to_string(),
877                DataType::VarChar {
878                    length: None,
879                    parenthesized_length: false,
880                },
881            ),
882        ];
883        schema.add_table("users", &columns, None).unwrap();
884        schema.set_visible_columns("users", &["id", "name"]);
885
886        let names = schema.column_names("users").unwrap();
887        assert_eq!(names.len(), 2);
888        assert!(names.contains(&"id".to_string()));
889        assert!(names.contains(&"name".to_string()));
890        assert!(!names.contains(&"password".to_string()));
891    }
892}