Skip to main content

polyglot_sql/dialects/
fabric.rs

1//! Microsoft Fabric Data Warehouse Dialect
2//!
3//! Fabric-specific SQL dialect based on sqlglot patterns.
4//! Fabric inherits from T-SQL with specific differences.
5//!
6//! References:
7//! - Data Types: https://learn.microsoft.com/en-us/fabric/data-warehouse/data-types
8//! - T-SQL Surface Area: https://learn.microsoft.com/en-us/fabric/data-warehouse/tsql-surface-area
9//!
10//! Key differences from T-SQL:
11//! - Case-sensitive identifiers (unlike T-SQL which is case-insensitive)
12//! - Limited data type support with mappings to supported alternatives
13//! - Temporal types (DATETIME2, DATETIMEOFFSET, TIME) limited to 6 digits precision
14//! - Certain legacy types (MONEY, SMALLMONEY, etc.) are not supported
15//! - Unicode types (NCHAR, NVARCHAR) are mapped to non-unicode equivalents
16
17use super::{DialectImpl, DialectType, TSQLDialect};
18use crate::error::Result;
19use crate::expressions::{BinaryOp, Cast, DataType, Expression, Function, Identifier, Literal};
20#[cfg(feature = "generate")]
21use crate::generator::GeneratorConfig;
22use crate::tokens::TokenizerConfig;
23
24/// Microsoft Fabric Data Warehouse dialect (based on T-SQL)
25pub struct FabricDialect;
26
27impl DialectImpl for FabricDialect {
28    fn dialect_type(&self) -> DialectType {
29        DialectType::Fabric
30    }
31
32    fn tokenizer_config(&self) -> TokenizerConfig {
33        // Inherit from T-SQL
34        let tsql = TSQLDialect;
35        tsql.tokenizer_config()
36    }
37
38    #[cfg(feature = "generate")]
39
40    fn generator_config(&self) -> GeneratorConfig {
41        use crate::generator::IdentifierQuoteStyle;
42        // Inherit from T-SQL with Fabric dialect type
43        GeneratorConfig {
44            // Use square brackets like T-SQL
45            identifier_quote: '[',
46            identifier_quote_style: IdentifierQuoteStyle::BRACKET,
47            dialect: Some(DialectType::Fabric),
48            null_ordering_supported: false,
49            aggregate_filter_supported: false,
50            cte_recursive_keyword_required: false,
51            ensure_bools: true,
52            except_intersect_support_all_clause: false,
53            ..Default::default()
54        }
55    }
56
57    #[cfg(feature = "transpile")]
58
59    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
60        // Handle CreateTable specially - add default precision of 1 to VARCHAR/CHAR without length
61        // Reference: Python sqlglot Fabric dialect parser._parse_create adds default precision
62        if let Expression::CreateTable(mut ct) = expr {
63            for col in &mut ct.columns {
64                match &col.data_type {
65                    DataType::VarChar { length: None, .. } => {
66                        col.data_type = DataType::VarChar {
67                            length: Some(1),
68                            parenthesized_length: false,
69                        };
70                    }
71                    DataType::Char { length: None } => {
72                        col.data_type = DataType::Char { length: Some(1) };
73                    }
74                    _ => {}
75                }
76                // Also transform column data types through Fabric's type mappings.
77                // Apply TSQL normalisation first (e.g. BPCHAR → Char), then Fabric-specific.
78                let tsql = TSQLDialect;
79                if let Ok(Expression::DataType(tsql_dt)) =
80                    tsql.transform_data_type(col.data_type.clone())
81                {
82                    col.data_type = tsql_dt;
83                }
84                if let Expression::DataType(new_dt) =
85                    self.transform_fabric_data_type(col.data_type.clone())?
86                {
87                    col.data_type = new_dt;
88                }
89            }
90            return Ok(Expression::CreateTable(ct));
91        }
92
93        // Handle DataType::Timestamp specially BEFORE T-SQL transform
94        // because TSQL loses precision info when converting Timestamp to DATETIME2
95        if let Expression::DataType(DataType::Timestamp { precision, .. }) = &expr {
96            let p = FabricDialect::cap_precision(*precision, 6);
97            return Ok(Expression::DataType(DataType::Custom {
98                name: format!("DATETIME2({})", p),
99            }));
100        }
101
102        // Handle DataType::Time specially BEFORE T-SQL transform
103        // to ensure we get default precision of 6
104        if let Expression::DataType(DataType::Time { precision, .. }) = &expr {
105            let p = FabricDialect::cap_precision(*precision, 6);
106            return Ok(Expression::DataType(DataType::Custom {
107                name: format!("TIME({})", p),
108            }));
109        }
110
111        // Handle DataType::Decimal specially BEFORE T-SQL transform
112        // because TSQL converts DECIMAL to NUMERIC, but Fabric wants DECIMAL
113        if let Expression::DataType(DataType::Decimal { precision, scale }) = &expr {
114            let name = Self::decimal_type_name(*precision, *scale);
115            return Ok(Expression::DataType(DataType::Custom { name }));
116        }
117
118        // Handle AT TIME ZONE with TIMESTAMPTZ cast
119        // Reference: Python sqlglot Fabric dialect cast_sql and attimezone_sql methods
120        // Input: CAST(x AS TIMESTAMPTZ) AT TIME ZONE 'Pacific Standard Time'
121        // Output: CAST(CAST(x AS DATETIMEOFFSET(6)) AT TIME ZONE 'Pacific Standard Time' AS DATETIME2(6))
122        if let Expression::AtTimeZone(ref at_tz) = expr {
123            // Check if this contains a TIMESTAMPTZ cast
124            if let Expression::Cast(ref inner_cast) = at_tz.this {
125                if let DataType::Timestamp {
126                    timezone: true,
127                    precision,
128                } = &inner_cast.to
129                {
130                    // Get precision, default 6, cap at 6
131                    let capped_precision = FabricDialect::cap_precision(*precision, 6);
132
133                    // Create inner DATETIMEOFFSET cast
134                    let datetimeoffset_cast = Expression::Cast(Box::new(Cast {
135                        this: inner_cast.this.clone(),
136                        to: DataType::Custom {
137                            name: format!("DATETIMEOFFSET({})", capped_precision),
138                        },
139                        trailing_comments: inner_cast.trailing_comments.clone(),
140                        double_colon_syntax: false,
141                        format: None,
142                        default: None,
143                        inferred_type: None,
144                    }));
145
146                    // Create new AT TIME ZONE with DATETIMEOFFSET
147                    let new_at_tz =
148                        Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
149                            this: datetimeoffset_cast,
150                            zone: at_tz.zone.clone(),
151                        }));
152
153                    // Wrap in outer DATETIME2 cast
154                    return Ok(Expression::Cast(Box::new(Cast {
155                        this: new_at_tz,
156                        to: DataType::Custom {
157                            name: format!("DATETIME2({})", capped_precision),
158                        },
159                        trailing_comments: Vec::new(),
160                        double_colon_syntax: false,
161                        format: None,
162                        default: None,
163                        inferred_type: None,
164                    })));
165                }
166            }
167        }
168
169        // Handle UnixToTime -> DATEADD(MICROSECONDS, CAST(ROUND(column * 1e6, 0) AS BIGINT), CAST('1970-01-01' AS DATETIME2(6)))
170        // Reference: Python sqlglot Fabric dialect unixtotime_sql
171        if let Expression::UnixToTime(ref f) = expr {
172            // Build: column * 1e6
173            let column_times_1e6 = Expression::Mul(Box::new(BinaryOp {
174                left: (*f.this).clone(),
175                right: Expression::Literal(Box::new(Literal::Number("1e6".to_string()))),
176                left_comments: Vec::new(),
177                operator_comments: Vec::new(),
178                trailing_comments: Vec::new(),
179                inferred_type: None,
180            }));
181
182            // Build: ROUND(column * 1e6, 0)
183            let round_expr = Expression::Function(Box::new(Function::new(
184                "ROUND".to_string(),
185                vec![
186                    column_times_1e6,
187                    Expression::Literal(Box::new(Literal::Number("0".to_string()))),
188                ],
189            )));
190
191            // Build: CAST(ROUND(...) AS BIGINT)
192            let cast_to_bigint = Expression::Cast(Box::new(Cast {
193                this: round_expr,
194                to: DataType::BigInt { length: None },
195                trailing_comments: Vec::new(),
196                double_colon_syntax: false,
197                format: None,
198                default: None,
199                inferred_type: None,
200            }));
201
202            // Build: CAST('1970-01-01' AS DATETIME2(6))
203            let epoch_start = Expression::Cast(Box::new(Cast {
204                this: Expression::Literal(Box::new(Literal::String("1970-01-01".to_string()))),
205                to: DataType::Custom {
206                    name: "DATETIME2(6)".to_string(),
207                },
208                trailing_comments: Vec::new(),
209                double_colon_syntax: false,
210                format: None,
211                default: None,
212                inferred_type: None,
213            }));
214
215            // Build: DATEADD(MICROSECONDS, cast_to_bigint, epoch_start)
216            let dateadd = Expression::Function(Box::new(Function::new(
217                "DATEADD".to_string(),
218                vec![
219                    Expression::Identifier(Identifier::new("MICROSECONDS")),
220                    cast_to_bigint,
221                    epoch_start,
222                ],
223            )));
224
225            return Ok(dateadd);
226        }
227
228        // Handle Function named UNIX_TO_TIME (parsed as generic function, not UnixToTime expression)
229        // Reference: Python sqlglot Fabric dialect unixtotime_sql
230        if let Expression::Function(ref f) = expr {
231            if f.name.eq_ignore_ascii_case("UNIX_TO_TIME") && !f.args.is_empty() {
232                let timestamp_input = f.args[0].clone();
233
234                // Build: column * 1e6
235                let column_times_1e6 = Expression::Mul(Box::new(BinaryOp {
236                    left: timestamp_input,
237                    right: Expression::Literal(Box::new(Literal::Number("1e6".to_string()))),
238                    left_comments: Vec::new(),
239                    operator_comments: Vec::new(),
240                    trailing_comments: Vec::new(),
241                    inferred_type: None,
242                }));
243
244                // Build: ROUND(column * 1e6, 0)
245                let round_expr = Expression::Function(Box::new(Function::new(
246                    "ROUND".to_string(),
247                    vec![
248                        column_times_1e6,
249                        Expression::Literal(Box::new(Literal::Number("0".to_string()))),
250                    ],
251                )));
252
253                // Build: CAST(ROUND(...) AS BIGINT)
254                let cast_to_bigint = Expression::Cast(Box::new(Cast {
255                    this: round_expr,
256                    to: DataType::BigInt { length: None },
257                    trailing_comments: Vec::new(),
258                    double_colon_syntax: false,
259                    format: None,
260                    default: None,
261                    inferred_type: None,
262                }));
263
264                // Build: CAST('1970-01-01' AS DATETIME2(6))
265                let epoch_start = Expression::Cast(Box::new(Cast {
266                    this: Expression::Literal(Box::new(Literal::String("1970-01-01".to_string()))),
267                    to: DataType::Custom {
268                        name: "DATETIME2(6)".to_string(),
269                    },
270                    trailing_comments: Vec::new(),
271                    double_colon_syntax: false,
272                    format: None,
273                    default: None,
274                    inferred_type: None,
275                }));
276
277                // Build: DATEADD(MICROSECONDS, cast_to_bigint, epoch_start)
278                let dateadd = Expression::Function(Box::new(Function::new(
279                    "DATEADD".to_string(),
280                    vec![
281                        Expression::Identifier(Identifier::new("MICROSECONDS")),
282                        cast_to_bigint,
283                        epoch_start,
284                    ],
285                )));
286
287                return Ok(dateadd);
288            }
289        }
290
291        // Delegate to T-SQL for other transformations
292        let tsql = TSQLDialect;
293        let transformed = tsql.transform_expr(expr)?;
294
295        // Apply Fabric-specific transformations to the result
296        self.transform_fabric_expr(transformed)
297    }
298}
299
300#[cfg(feature = "transpile")]
301impl FabricDialect {
302    /// Fabric-specific expression transformations
303    fn transform_fabric_expr(&self, expr: Expression) -> Result<Expression> {
304        match expr {
305            // Handle DataType expressions with Fabric-specific type mappings
306            Expression::DataType(dt) => self.transform_fabric_data_type(dt),
307
308            // Pass through everything else
309            _ => Ok(expr),
310        }
311    }
312
313    /// Transform data types according to Fabric TYPE_MAPPING
314    /// Reference: https://learn.microsoft.com/en-us/fabric/data-warehouse/data-types
315    fn transform_fabric_data_type(&self, dt: DataType) -> Result<Expression> {
316        let transformed = match dt {
317            // TIMESTAMP -> DATETIME2(6) with precision handling
318            // Note: TSQL already converts this to DATETIME2, but without precision
319            DataType::Timestamp { precision, .. } => {
320                let p = Self::cap_precision(precision, 6);
321                DataType::Custom {
322                    name: format!("DATETIME2({})", p),
323                }
324            }
325
326            // TIME -> TIME(6) default, capped at 6
327            DataType::Time { precision, .. } => {
328                let p = Self::cap_precision(precision, 6);
329                DataType::Custom {
330                    name: format!("TIME({})", p),
331                }
332            }
333
334            // INT -> INT (override TSQL which may output INTEGER)
335            DataType::Int { .. } => DataType::Custom {
336                name: "INT".to_string(),
337            },
338
339            // DECIMAL -> DECIMAL (override TSQL which converts to NUMERIC)
340            DataType::Decimal { precision, scale } => DataType::Custom {
341                name: Self::decimal_type_name(precision, scale),
342            },
343
344            // JSON -> VARCHAR
345            DataType::Json => DataType::Custom {
346                name: "VARCHAR".to_string(),
347            },
348
349            // UUID -> UNIQUEIDENTIFIER (already handled by TSQL, but ensure it's here)
350            DataType::Uuid => DataType::Custom {
351                name: "UNIQUEIDENTIFIER".to_string(),
352            },
353
354            // TinyInt -> SMALLINT
355            DataType::TinyInt { .. } => DataType::Custom {
356                name: "SMALLINT".to_string(),
357            },
358
359            // Handle Custom types for Fabric-specific mappings
360            DataType::Custom { ref name } => {
361                let upper = name.to_uppercase();
362
363                // Parse out precision and scale if present: "TYPENAME(n)" or "TYPENAME(n, m)"
364                let (base_name, precision, scale) =
365                    TSQLDialect::parse_type_precision_and_scale(&upper);
366                let has_max_length = upper.contains("(MAX)");
367
368                match base_name.as_str() {
369                    // DATETIME -> DATETIME2(6)
370                    "DATETIME" => DataType::Custom {
371                        name: "DATETIME2(6)".to_string(),
372                    },
373
374                    // SMALLDATETIME -> DATETIME2(6)
375                    "SMALLDATETIME" => DataType::Custom {
376                        name: "DATETIME2(6)".to_string(),
377                    },
378
379                    // DATETIME2 -> DATETIME2(6) default, cap at 6
380                    "DATETIME2" => {
381                        let p = Self::cap_precision(precision, 6);
382                        DataType::Custom {
383                            name: format!("DATETIME2({})", p),
384                        }
385                    }
386
387                    // DATETIMEOFFSET -> cap precision at 6
388                    "DATETIMEOFFSET" => {
389                        let p = Self::cap_precision(precision, 6);
390                        DataType::Custom {
391                            name: format!("DATETIMEOFFSET({})", p),
392                        }
393                    }
394
395                    // TIME -> TIME(6) default, cap at 6
396                    "TIME" => {
397                        let p = Self::cap_precision(precision, 6);
398                        DataType::Custom {
399                            name: format!("TIME({})", p),
400                        }
401                    }
402
403                    // TIMESTAMP -> DATETIME2(6)
404                    "TIMESTAMP" => DataType::Custom {
405                        name: "DATETIME2(6)".to_string(),
406                    },
407
408                    // TIMESTAMPNTZ -> DATETIME2(6) with precision
409                    "TIMESTAMPNTZ" => {
410                        let p = Self::cap_precision(precision, 6);
411                        DataType::Custom {
412                            name: format!("DATETIME2({})", p),
413                        }
414                    }
415
416                    // TIMESTAMPTZ -> DATETIME2(6) with precision
417                    "TIMESTAMPTZ" => {
418                        let p = Self::cap_precision(precision, 6);
419                        DataType::Custom {
420                            name: format!("DATETIME2({})", p),
421                        }
422                    }
423
424                    // IMAGE -> VARBINARY
425                    "IMAGE" => DataType::Custom {
426                        name: "VARBINARY".to_string(),
427                    },
428
429                    // MONEY -> DECIMAL
430                    "MONEY" => DataType::Custom {
431                        name: "DECIMAL".to_string(),
432                    },
433
434                    // SMALLMONEY -> DECIMAL
435                    "SMALLMONEY" => DataType::Custom {
436                        name: "DECIMAL".to_string(),
437                    },
438
439                    // NCHAR -> CHAR (with length preserved)
440                    "NCHAR" => {
441                        if has_max_length {
442                            DataType::Custom {
443                                name: "CHAR(MAX)".to_string(),
444                            }
445                        } else if let Some(len) = precision {
446                            DataType::Custom {
447                                name: format!("CHAR({})", len),
448                            }
449                        } else {
450                            DataType::Custom {
451                                name: "CHAR".to_string(),
452                            }
453                        }
454                    }
455
456                    // NVARCHAR -> VARCHAR (with length preserved)
457                    "NVARCHAR" => {
458                        if has_max_length {
459                            DataType::Custom {
460                                name: "VARCHAR(MAX)".to_string(),
461                            }
462                        } else if let Some(len) = precision {
463                            DataType::Custom {
464                                name: format!("VARCHAR({})", len),
465                            }
466                        } else {
467                            DataType::Custom {
468                                name: "VARCHAR".to_string(),
469                            }
470                        }
471                    }
472
473                    // TINYINT -> SMALLINT
474                    "TINYINT" => DataType::Custom {
475                        name: "SMALLINT".to_string(),
476                    },
477
478                    // UTINYINT -> SMALLINT
479                    "UTINYINT" => DataType::Custom {
480                        name: "SMALLINT".to_string(),
481                    },
482
483                    // VARIANT -> SQL_VARIANT
484                    "VARIANT" => DataType::Custom {
485                        name: "SQL_VARIANT".to_string(),
486                    },
487
488                    // XML -> VARCHAR
489                    "XML" => DataType::Custom {
490                        name: "VARCHAR".to_string(),
491                    },
492
493                    // NUMERIC -> DECIMAL (override TSQL's conversion)
494                    // Fabric uses DECIMAL, not NUMERIC
495                    "DECIMAL" | "NUMERIC" => DataType::Custom {
496                        name: Self::decimal_type_name(precision, scale),
497                    },
498
499                    // Pass through other custom types unchanged
500                    _ => dt,
501                }
502            }
503
504            // Keep all other types as transformed by TSQL
505            other => other,
506        };
507
508        Ok(Expression::DataType(transformed))
509    }
510
511    /// Cap precision to max value, defaulting to max if not specified
512    fn cap_precision(precision: Option<u32>, max: u32) -> u32 {
513        match precision {
514            Some(p) if p > max => max,
515            Some(p) => p,
516            None => max, // Default to max if not specified
517        }
518    }
519
520    fn decimal_type_name(precision: Option<u32>, scale: Option<u32>) -> String {
521        match (precision, scale) {
522            (Some(p), Some(s)) => format!("DECIMAL({}, {})", p, s),
523            (Some(p), None) => format!("DECIMAL({})", p),
524            (None, _) => "DECIMAL".to_string(),
525        }
526    }
527}