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