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