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}