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}