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}