Skip to main content

pg2sqlite_core/transform/
index.rs

1/// Index conversion with method filtering and expression handling.
2use crate::diagnostics::warning::{self, Severity, Warning};
3use crate::ir::{Index, IndexColumn, IndexMethod, SchemaModel};
4use crate::transform::expr_map;
5
6/// Transform indexes in the schema model.
7pub fn transform_indexes(model: &mut SchemaModel, warnings: &mut Vec<Warning>) {
8    let mut kept = Vec::new();
9
10    for index in &model.indexes {
11        if let Some(idx) = transform_index(index, warnings) {
12            kept.push(idx);
13        }
14    }
15
16    model.indexes = kept;
17}
18
19fn transform_index(index: &Index, warnings: &mut Vec<Warning>) -> Option<Index> {
20    let obj = index.name.normalized.clone();
21
22    // Warn about non-btree methods
23    if let Some(method) = &index.method
24        && *method != IndexMethod::Btree
25    {
26        warnings.push(
27            Warning::new(
28                warning::INDEX_METHOD_IGNORED,
29                Severity::Info,
30                format!("index method '{method}' ignored; SQLite only supports btree"),
31            )
32            .with_object(&obj),
33        );
34    }
35
36    // Transform WHERE clause
37    let where_clause = if let Some(where_expr) = &index.where_clause {
38        match expr_map::map_expr(where_expr, &obj, warnings) {
39            Some(mapped) => Some(mapped),
40            None => {
41                warnings.push(
42                    Warning::new(
43                        warning::PARTIAL_INDEX_UNSUPPORTED,
44                        Severity::Unsupported,
45                        "partial index WHERE clause uses unsupported PG features; index skipped",
46                    )
47                    .with_object(&obj),
48                );
49                return None;
50            }
51        }
52    } else {
53        None
54    };
55
56    // Transform expression columns
57    let mut columns = Vec::new();
58    for col in &index.columns {
59        match col {
60            IndexColumn::Column(ident) => {
61                columns.push(IndexColumn::Column(ident.clone()));
62            }
63            IndexColumn::Expression(expr) => match expr_map::map_expr(expr, &obj, warnings) {
64                Some(mapped) => {
65                    columns.push(IndexColumn::Expression(mapped));
66                }
67                None => {
68                    warnings.push(
69                        Warning::new(
70                            warning::EXPRESSION_INDEX_UNSUPPORTED,
71                            Severity::Unsupported,
72                            "expression index uses unsupported PG features; index skipped",
73                        )
74                        .with_object(&obj),
75                    );
76                    return None;
77                }
78            },
79        }
80    }
81
82    Some(Index {
83        name: index.name.clone(),
84        table: index.table.clone(),
85        columns,
86        unique: index.unique,
87        method: None, // Method always stripped for SQLite
88        where_clause,
89    })
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::ir::{Expr, Ident, QualifiedName};
96
97    fn make_index(name: &str, table: &str, cols: Vec<&str>) -> Index {
98        Index {
99            name: Ident::new(name),
100            table: QualifiedName::new(Ident::new(table)),
101            columns: cols
102                .into_iter()
103                .map(|c| IndexColumn::Column(Ident::new(c)))
104                .collect(),
105            unique: false,
106            method: None,
107            where_clause: None,
108        }
109    }
110
111    #[test]
112    fn test_simple_index_passthrough() {
113        let mut model = SchemaModel {
114            indexes: vec![make_index("idx_name", "users", vec!["name"])],
115            ..Default::default()
116        };
117        let mut w = Vec::new();
118        transform_indexes(&mut model, &mut w);
119        assert_eq!(model.indexes.len(), 1);
120        assert!(w.is_empty());
121    }
122
123    #[test]
124    fn test_gin_method_warned() {
125        let mut idx = make_index("idx_data", "t", vec!["data"]);
126        idx.method = Some(IndexMethod::Gin);
127        let mut model = SchemaModel {
128            indexes: vec![idx],
129            ..Default::default()
130        };
131        let mut w = Vec::new();
132        transform_indexes(&mut model, &mut w);
133        assert_eq!(model.indexes.len(), 1);
134        assert!(w.iter().any(|w| w.code == warning::INDEX_METHOD_IGNORED));
135        assert!(model.indexes[0].method.is_none());
136    }
137
138    #[test]
139    fn test_partial_index_with_compatible_where() {
140        let mut idx = make_index("idx_active", "users", vec!["email"]);
141        idx.where_clause = Some(Expr::IsNull {
142            expr: Box::new(Expr::ColumnRef("deleted_at".to_string())),
143            negated: false,
144        });
145        let mut model = SchemaModel {
146            indexes: vec![idx],
147            ..Default::default()
148        };
149        let mut w = Vec::new();
150        transform_indexes(&mut model, &mut w);
151        assert_eq!(model.indexes.len(), 1);
152        assert!(model.indexes[0].where_clause.is_some());
153    }
154}