@malloydata/malloy 0.0.435 → 0.0.436

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/api/foundation/build_targets.d.ts +5 -6
  2. package/dist/api/foundation/build_targets.js +74 -6
  3. package/dist/api/foundation/core.d.ts +16 -4
  4. package/dist/api/foundation/core.js +23 -23
  5. package/dist/api/foundation/index.d.ts +1 -1
  6. package/dist/api/foundation/runtime.d.ts +21 -2
  7. package/dist/api/foundation/runtime.js +44 -31
  8. package/dist/api/foundation/types.d.ts +43 -3
  9. package/dist/connection/dbmeta.d.ts +29 -0
  10. package/dist/connection/dbmeta.js +46 -0
  11. package/dist/connection/index.d.ts +1 -0
  12. package/dist/connection/index.js +1 -0
  13. package/dist/dialect/databricks/databricks.d.ts +11 -12
  14. package/dist/dialect/databricks/databricks.js +24 -36
  15. package/dist/dialect/dialect.d.ts +56 -170
  16. package/dist/dialect/dialect.js +89 -90
  17. package/dist/dialect/duckdb/duckdb.d.ts +2 -2
  18. package/dist/dialect/duckdb/duckdb.js +1 -1
  19. package/dist/dialect/index.d.ts +1 -1
  20. package/dist/dialect/index.js +2 -2
  21. package/dist/dialect/mysql/mysql.d.ts +11 -12
  22. package/dist/dialect/mysql/mysql.js +20 -32
  23. package/dist/dialect/pg_impl.d.ts +9 -10
  24. package/dist/dialect/pg_impl.js +27 -73
  25. package/dist/dialect/postgres/postgres.d.ts +4 -4
  26. package/dist/dialect/postgres/postgres.js +4 -12
  27. package/dist/dialect/snowflake/snowflake.d.ts +11 -13
  28. package/dist/dialect/snowflake/snowflake.js +30 -82
  29. package/dist/dialect/sqlserver/sqlserver.d.ts +12 -13
  30. package/dist/dialect/sqlserver/sqlserver.js +46 -47
  31. package/dist/dialect/standardsql/standardsql.d.ts +11 -16
  32. package/dist/dialect/standardsql/standardsql.js +22 -73
  33. package/dist/dialect/trino/trino.d.ts +16 -18
  34. package/dist/dialect/trino/trino.js +127 -181
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +2 -2
  37. package/dist/lang/ast/expressions/boolean-value.d.ts +24 -0
  38. package/dist/lang/ast/expressions/boolean-value.js +41 -0
  39. package/dist/lang/ast/expressions/case.js +7 -0
  40. package/dist/lang/ast/expressions/expr-alternation-tree.js +11 -0
  41. package/dist/lang/ast/expressions/expr-cast.js +5 -0
  42. package/dist/lang/ast/expressions/expr-coalesce.js +4 -0
  43. package/dist/lang/ast/expressions/expr-func.js +13 -2
  44. package/dist/lang/ast/expressions/expr-minus.js +5 -2
  45. package/dist/lang/ast/expressions/expr-null.js +4 -0
  46. package/dist/lang/ast/expressions/pick-when.js +7 -0
  47. package/dist/lang/ast/field-space/index-field-space.js +13 -1
  48. package/dist/lang/ast/field-space/query-spaces.js +11 -0
  49. package/dist/lang/ast/query-builders/reduce-builder.js +1 -1
  50. package/dist/lang/ast/query-items/field-declaration.d.ts +5 -0
  51. package/dist/lang/ast/query-items/field-declaration.js +22 -16
  52. package/dist/lang/ast/query-items/field-references.d.ts +6 -0
  53. package/dist/lang/ast/query-items/field-references.js +23 -0
  54. package/dist/lang/ast/query-properties/filters.js +3 -1
  55. package/dist/lang/ast/statements/import-statement.js +13 -0
  56. package/dist/lang/ast/types/expression-def.js +17 -2
  57. package/dist/lang/ast/types/malloy-element.d.ts +24 -1
  58. package/dist/lang/ast/types/malloy-element.js +63 -26
  59. package/dist/lang/ast/view-elements/reference-view.js +4 -0
  60. package/dist/lang/ast/view-elements/refine-utils.js +1 -0
  61. package/dist/lang/composite-source-utils.js +10 -17
  62. package/dist/lang/parse-log.d.ts +12 -0
  63. package/dist/lang/parse-log.js +2 -0
  64. package/dist/lang/test/test-translator.d.ts +2 -1
  65. package/dist/lang/test/test-translator.js +37 -3
  66. package/dist/model/expression_compiler.js +8 -7
  67. package/dist/model/filter_compilers.js +13 -12
  68. package/dist/model/given_binding.d.ts +8 -0
  69. package/dist/model/given_binding.js +13 -0
  70. package/dist/model/index.d.ts +1 -1
  71. package/dist/model/index.js +2 -1
  72. package/dist/model/sql_compiled.d.ts +19 -20
  73. package/dist/model/sql_compiled.js +29 -17
  74. package/dist/version.d.ts +1 -1
  75. package/dist/version.js +1 -1
  76. package/package.json +4 -4
@@ -300,19 +300,19 @@ class DatabricksDialect extends dialect_1.Dialect {
300
300
  sqlNowExpr() {
301
301
  return 'CURRENT_TIMESTAMP()';
302
302
  }
303
+ // Civil time is a TIMESTAMP whose reading in the session zone, UTC, is
304
+ // the civil time
303
305
  sqlConvertToCivilTime(expr, timezone, _typeDef) {
304
- // Databricks has no timestamptz type; timestamps are stored as UTC
305
- // Convert from UTC to local timezone for civil time operations
306
- return {
307
- sql: `FROM_UTC_TIMESTAMP(${expr}, ${this.sqlTimezoneLiteral(timezone)})`,
308
- typeDef: { type: 'timestamp' },
309
- };
306
+ return timezone === 'UTC'
307
+ ? expr
308
+ : `FROM_UTC_TIMESTAMP(${expr}, ${this.sqlTimezoneLiteral(timezone)})`;
310
309
  }
311
310
  sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
312
- // Convert from local timezone back to UTC
313
- return `TO_UTC_TIMESTAMP(${expr}, ${this.sqlTimezoneLiteral(timezone)})`;
311
+ return timezone === 'UTC'
312
+ ? expr
313
+ : `TO_UTC_TIMESTAMP(${expr}, ${this.sqlTimezoneLiteral(timezone)})`;
314
314
  }
315
- sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
315
+ sqlTruncate(expr, unit, _civilType) {
316
316
  // Databricks DATE_TRUNC starts weeks on Monday; Malloy wants Sunday.
317
317
  // Add 1 day before truncating, subtract 1 day after.
318
318
  if (unit === 'week') {
@@ -320,7 +320,7 @@ class DatabricksDialect extends dialect_1.Dialect {
320
320
  }
321
321
  return `DATE_TRUNC('${unit}', ${expr})`;
322
322
  }
323
- sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
323
+ sqlOffsetTime(expr, op, magnitude, unit, _civilType) {
324
324
  // Use TIMESTAMPADD which accepts expressions for magnitude,
325
325
  // unlike INTERVAL which only accepts literals in Databricks.
326
326
  let offsetUnit = unit.toUpperCase();
@@ -336,31 +336,23 @@ class DatabricksDialect extends dialect_1.Dialect {
336
336
  const n = op === '+' ? offsetMag : `-(${offsetMag})`;
337
337
  return `TIMESTAMPADD(${offsetUnit}, ${n}, ${expr})`;
338
338
  }
339
- sqlTimeExtractExpr(qi, te) {
340
- const units = extractionMap[te.units] || te.units;
341
- let extractFrom = te.e.sql;
342
- if (malloy_types_1.TD.isTimestamp(te.e.typeDef)) {
343
- const tz = (0, dialect_1.qtz)(qi);
344
- if (tz) {
345
- extractFrom = `FROM_UTC_TIMESTAMP(${extractFrom}, ${this.sqlTimezoneLiteral(tz)})`;
346
- }
347
- }
348
- if (extractionMap[te.units]) {
339
+ sqlCivilExtract(expr, units, _civilType) {
340
+ const extractUnits = extractionMap[units];
341
+ if (extractUnits) {
349
342
  // DAYOFWEEK, DAYOFYEAR are functions
350
- return `${units}(${extractFrom})`;
343
+ return `${extractUnits}(${expr})`;
351
344
  }
352
- return `EXTRACT(${units} FROM ${extractFrom})`;
345
+ return `EXTRACT(${units} FROM ${expr})`;
346
+ }
347
+ sqlCivilToDate(expr) {
348
+ return `CAST(${expr} AS DATE)`;
349
+ }
350
+ sqlDateToCivil(expr) {
351
+ return `CAST(${expr} AS TIMESTAMP)`;
353
352
  }
354
- sqlCast(qi, cast) {
353
+ sqlCast(_qi, cast) {
355
354
  const srcSQL = cast.e.sql || 'internal-error-in-sql-generation';
356
- const { op, srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
357
- const tz = (0, dialect_1.qtz)(qi);
358
- if (op === 'timestamp::date' && tz) {
359
- return `CAST(FROM_UTC_TIMESTAMP(${srcSQL}, ${this.sqlTimezoneLiteral(tz)}) AS DATE)`;
360
- }
361
- else if (op === 'date::timestamp' && tz) {
362
- return `TO_UTC_TIMESTAMP(CAST(${srcSQL} AS TIMESTAMP_NTZ), ${this.sqlTimezoneLiteral(tz)})`;
363
- }
355
+ const { srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
364
356
  if (!malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
365
357
  if (cast.safe) {
366
358
  return `TRY_CAST(${srcSQL} AS ${dstSQLType})`;
@@ -378,11 +370,7 @@ class DatabricksDialect extends dialect_1.Dialect {
378
370
  sqlDateLiteral(_qi, literal) {
379
371
  return `DATE '${literal}'`;
380
372
  }
381
- sqlTimestampLiteral(qi, literal, timezone) {
382
- const tz = timezone || (0, dialect_1.qtz)(qi);
383
- if (tz) {
384
- return `TO_UTC_TIMESTAMP(TIMESTAMP_NTZ '${literal}', ${this.sqlTimezoneLiteral(tz)})`;
385
- }
373
+ sqlCivilLiteral(literal) {
386
374
  return `TIMESTAMP '${literal}'`;
387
375
  }
388
376
  sqlTimestamptzLiteral(_qi, _literal, _timezone) {
@@ -1,4 +1,4 @@
1
- import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralExpr, RecordLiteralNode, ArrayLiteralNode, BasicAtomicTypeDef, OrderBy, TimestampUnit, ATimestampTypeDef, TimeExpr, TemporalFieldType } from '../model/malloy_types';
1
+ import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralExpr, RecordLiteralNode, ArrayLiteralNode, BasicAtomicTypeDef, OrderBy, TimestampUnit, ATimestampTypeDef, TimeExpr, TemporalFieldType, ExtractUnit } from '../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from './functions';
3
3
  import type { ValidateTablePathResult } from './table-path';
4
4
  /**
@@ -68,10 +68,26 @@ export interface QueryInfo {
68
68
  queryTimezone?: string;
69
69
  systemTimezone?: string;
70
70
  }
71
+ /**
72
+ * The two kinds of civil time: a calendar date, and a date with a time of
73
+ * day. Neither has a zone.
74
+ */
75
+ export declare const CivilTime: {
76
+ readonly Date: "date";
77
+ readonly DateAndTime: "date_and_time";
78
+ };
79
+ export type CivilTime = (typeof CivilTime)[keyof typeof CivilTime];
80
+ /** Add or subtract `magnitude` units of time */
81
+ export interface TimeOffset {
82
+ op: '+' | '-';
83
+ magnitude: string;
84
+ unit: TimestampUnit;
85
+ }
71
86
  export type FieldReferenceType = 'table' | 'nest source' | 'array[scalar]' | 'array[record]' | 'record';
72
87
  export declare const dayIndex: number;
73
88
  export declare function inDays(units: string): boolean;
74
- export declare function qtz(qi: QueryInfo): string | undefined;
89
+ /** The query time zone, or UTC when the query has none. */
90
+ export declare function queryZone(qi: QueryInfo): string;
75
91
  /**
76
92
  * The per-element condition for an aggregate turtle (sqlAggregateTurtle). On
77
93
  * the normal path it gates each row by its group_set (and a projection's own
@@ -283,175 +299,43 @@ export declare abstract class Dialect {
283
299
  sqlLiteralNumber(literal: string): string;
284
300
  ignoreInProject(_fieldName: string): boolean;
285
301
  abstract sqlNowExpr(): string;
286
- abstract sqlTimeExtractExpr(qi: QueryInfo, xFrom: TimeExtractExpr): string;
287
302
  abstract sqlMeasureTimeExpr(e: MeasureTimeExpr): string;
288
303
  /**
289
- * Generate SQL for type casting expressions.
290
- *
291
- * Most casts are simple: `CAST(expr AS type)` or `TRY_CAST(expr AS type)` for safe casts.
292
- *
293
- * However, when a query timezone is set, casts between temporal types (date, timestamp, timestamptz)
294
- * require special handling to ensure correct timezone semantics:
295
- *
296
- * **Timezone-Aware Cast Semantics:**
297
- *
298
- * 1. **TIMESTAMP → DATE**:
299
- * - TIMESTAMP represents UTC wall clock
300
- * - Convert to query timezone, then extract date
301
- * - Example: TIMESTAMP '2020-02-20 00:00:00' with tz 'America/Mexico_City' → '2020-02-19'
302
- *
303
- * 2. **TIMESTAMPTZ → DATE**:
304
- * - TIMESTAMPTZ represents absolute instant
305
- * - Convert to query timezone, then extract date
306
- * - Example: TIMESTAMPTZ '2020-02-20 00:00:00 UTC' with tz 'America/Mexico_City' → '2020-02-19'
307
- *
308
- * 3. **DATE → TIMESTAMP**:
309
- * - DATE represents civil date
310
- * - Interpret as midnight in query timezone, return UTC wall clock
311
- * - Example: DATE '2020-02-20' with tz 'America/Mexico_City' → TIMESTAMP '2020-02-20 06:00:00' (UTC)
312
- *
313
- * 4. **DATE → TIMESTAMPTZ**:
314
- * - DATE represents civil date
315
- * - Interpret as midnight in query timezone, create instant
316
- * - Example: DATE '2020-02-20' with tz 'America/Mexico_City' → instant at 2020-02-20 06:00:00 UTC
317
- *
318
- * 5. **TIMESTAMPTZ → TIMESTAMP**:
319
- * - TIMESTAMPTZ represents absolute instant
320
- * - Extract wall clock in query timezone, return as TIMESTAMP
321
- * - Example: TIMESTAMPTZ '2020-02-20 00:00:00 UTC' with tz 'America/Mexico_City' → TIMESTAMP '2020-02-19 18:00:00'
322
- *
323
- * 6. **TIMESTAMP → TIMESTAMPTZ**:
324
- * - TIMESTAMP represents UTC wall clock
325
- * - Interpret as being in query timezone
326
- * - Example: TIMESTAMP '2020-02-20 00:00:00' with tz 'America/Mexico_City' → instant at 2020-02-20 06:00:00 UTC
327
- *
328
- * **Implementation Notes:**
329
- *
330
- * - Dialects without timestamptz support (MySQL, BigQuery, StandardSQL) only need cases 1-3
331
- * - Without query timezone, most casts are simple `CAST(expr AS type)`
332
- *
333
- * @param qi - Query info containing timezone and other context
334
- * @param cast - The typecast expression to generate SQL for
335
- * @returns SQL string for the cast operation
304
+ * Generate SQL for a cast. Casts between date, timestamp and timestamptz
305
+ * never reach here; they are made by sqlTimeCast.
336
306
  */
337
307
  abstract sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
338
308
  abstract sqlRegexpMatch(df: RegexMatchExpr): string;
309
+ /** The civil time which `timezone` shows at the instant `expr`. */
310
+ abstract sqlConvertToCivilTime(expr: string, timezone: string, typeDef: ATimestampTypeDef): string;
339
311
  /**
340
- * Converts a Malloy timestamp to "civil time" for calendar operations in a timezone.
341
- *
342
- * Each dialect selects its own SQL type to represent civil time (e.g., plain TIMESTAMP,
343
- * TIMESTAMP WITH TIME ZONE, or DATETIME). The civil space is where timezone-aware
344
- * truncation and interval arithmetic happen. Operations like sqlTruncate and sqlOffsetTime
345
- * are aware of the civil space and work correctly within it.
346
- *
347
- * @param expr The SQL expression for the Malloy timestamp (plain or timestamptz)
348
- * @param timezone The target timezone for civil operations
349
- * @param typeDef The Malloy type of the input expression
350
- * @returns Object with SQL expression and the SQL type it evaluates to (the civil type)
351
- */
352
- abstract sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
353
- sql: string;
354
- typeDef: AtomicTypeDef;
355
- };
356
- /**
357
- * Converts from civil time back to a Malloy timestamp type.
358
- *
359
- * This is the inverse of sqlConvertToCivilTime. Takes a value in the dialect's
360
- * civil space and converts it back to either a plain timestamp (UTC) or a
361
- * timestamptz, depending on the destination type.
362
- *
363
- * @param expr The SQL expression in civil time
364
- * @param timezone The timezone of the civil time
365
- * @param destTypeDef The destination Malloy timestamp type (plain or timestamptz)
366
- * @returns SQL expression representing the Malloy timestamp
312
+ * The instant at which `timezone` shows the civil time `expr`, as a
313
+ * value of `destTypeDef`.
367
314
  */
368
315
  abstract sqlConvertFromCivilTime(expr: string, timezone: string, destTypeDef: ATimestampTypeDef): string;
316
+ /** The start of the unit containing `expr`. A week starts on Sunday. */
317
+ abstract sqlTruncate(expr: string, unit: TimestampUnit, civilType: CivilTime): string;
318
+ abstract sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, civilType: CivilTime): string;
319
+ /** One field of `expr`. A day_of_week counts from 1 on Sunday. */
320
+ abstract sqlCivilExtract(expr: string, units: ExtractUnit, civilType: CivilTime): string;
321
+ /** The date part of the civil time `expr`. */
322
+ abstract sqlCivilToDate(expr: string): string;
323
+ /** Midnight at the start of the date `expr`, as civil time. */
324
+ abstract sqlDateToCivil(expr: string): string;
325
+ /** The civil time written as `literal`, 'YYYY-MM-DD HH:MM:SS'. */
326
+ abstract sqlCivilLiteral(literal: string): string;
327
+ sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
369
328
  /**
370
- * Truncates a time expression to the specified unit.
371
- *
372
- * @param expr The SQL expression to truncate
373
- * @param unit The unit to truncate to (year, month, day, hour, etc.)
374
- * @param typeDef The Malloy type of the expression (date, timestamp, etc.)
375
- * @param inCivilTime If true, the expression is already in civil (local) time and should not
376
- * be converted. If false, may need timezone conversion for timestamps.
377
- * @param timezone Optional timezone for the operation. Only provided when timezone-aware
378
- * truncation is needed but inCivilTime is false.
379
- * @returns SQL expression representing the truncated time
329
+ * Truncation and/or offset, the offset applied after the truncation.
330
+ * Doing both in one pass converts to and from civil time only once.
380
331
  */
381
- abstract sqlTruncate(expr: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, timezone?: string): string;
332
+ sqlTruncAndOffset(baseExpr: TimeExpr, qi: QueryInfo, truncateTo?: TimestampUnit, offset?: TimeOffset): string;
333
+ private civilTruncAndOffset;
382
334
  /**
383
- * Adds or subtracts a time interval from a time expression.
384
- *
385
- * @param expr The SQL expression to offset
386
- * @param op The operation: '+' for addition, '-' for subtraction
387
- * @param magnitude The SQL expression for the interval magnitude (e.g., '6', '(delta_val)')
388
- * @param unit The interval unit (year, month, day, hour, etc.)
389
- * @param typeDef The Malloy type of the expression (date, timestamp, etc.)
390
- * @param inCivilTime If true, the expression is already in civil (local) time and should not
391
- * be converted. If false, may need timezone conversion for timestamps.
392
- * @param timezone Optional timezone for the operation. Only provided when timezone-aware
393
- * offset is needed but inCivilTime is false.
394
- * @returns SQL expression representing the offset time
335
+ * A cast between date, timestamp and timestamptz, or undefined for any
336
+ * other cast.
395
337
  */
396
- abstract sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, timezone?: string): string;
397
- /**
398
- * Determines whether a truncation and/or offset operation needs to be performed in
399
- * civil (local) time, and if so, which timezone to use.
400
- *
401
- * Calendar-based operations (day, week, month, quarter, year) typically need civil time
402
- * computation because these units can cross DST boundaries, changing the UTC offset.
403
- *
404
- * Default implementation:
405
- * - Returns true for timestamps with truncation or calendar-unit offsets
406
- * - Uses the query timezone from QueryInfo, or 'UTC' if none specified
407
- * - Returns false for dates or sub-day offsets (hour, minute, second)
408
- *
409
- * Dialects can override this if they have different rules about which operations
410
- * require civil time computation.
411
- *
412
- * @param typeDef The Malloy type of the base expression
413
- * @param truncateTo The truncation unit, if any
414
- * @param offsetUnit The offset unit, if any
415
- * @param qi Query information including timezone settings
416
- * @returns Object with `needed` boolean and optional `tz` string
417
- */
418
- needsCivilTimeComputation(typeDef: AtomicTypeDef, truncateTo: TimestampUnit | undefined, offsetUnit: TimestampUnit | undefined, qi: QueryInfo): {
419
- needed: boolean;
420
- tz: string | undefined;
421
- };
422
- /**
423
- * Unified function for truncation and/or offset operations. This turns out to
424
- * be a very common operation and one which can be optimized by doing it together.
425
- *
426
- * Much of the complexity has to do with getting timezone values set up so that
427
- * they will truncate/offset in the query timezone instead of UTC.
428
- *
429
- * The intention is that this implementation will work for all dialects, and all
430
- * the dialect peculiarities are handled with the new primitives introduced to
431
- * support this function:
432
- * - needsCivilTimeComputation: Determines if operation needs civil time
433
- * - sqlConvertToCivilTime/sqlConvertFromCivilTime: Timezone conversion
434
- * - sqlTruncate: Truncation operation
435
- * - sqlOffsetTime: Interval arithmetic
436
- *
437
- * OFFSET TIMESTAMP BEHAVIOR:
438
- * - Plain timestamps (offset=false): Always return plain TIMESTAMP in UTC
439
- * - Offset timestamps (offset=true):
440
- * - Simple path: Operate in native embedded timezone, return TIMESTAMPTZ
441
- * - Civil path: Convert to query timezone (preserving instant), operate there,
442
- * return TIMESTAMPTZ in query timezone via sqlConvertFromCivilTime
443
- * - BigQuery/MySQL: No offset timestamp support, this logic never runs
444
- *
445
- * @param baseExpr The time expression to operate on (already compiled, with .sql populated)
446
- * @param qi Query information including timezone
447
- * @param truncateTo Optional truncation unit (year, month, day, etc.)
448
- * @param offset Optional offset to apply (after truncation if both present)
449
- */
450
- sqlTruncAndOffset(baseExpr: TimeExpr, qi: QueryInfo, truncateTo?: TimestampUnit, offset?: {
451
- op: '+' | '-';
452
- magnitude: string;
453
- unit: TimestampUnit;
454
- }): string;
338
+ sqlTimeCast(qi: QueryInfo, cast: TypecastExpr): string | undefined;
455
339
  /**
456
340
  * Generate SQL for a DATE literal.
457
341
  * @param literal - The date string in format 'YYYY-MM-DD'
@@ -459,18 +343,10 @@ export declare abstract class Dialect {
459
343
  */
460
344
  abstract sqlDateLiteral(qi: QueryInfo, literal: string): string;
461
345
  /**
462
- * Generate SQL for a plain TIMESTAMP literal (without timezone offset).
463
- * @param literal - The timestamp string in format 'YYYY-MM-DD HH:MM:SS'
464
- * @param timezone - Optional timezone name (e.g., 'America/Los_Angeles')
465
- * - If undefined: Create plain timestamp literal from the literal string
466
- * - If defined: The literal string represents a civil time in the given timezone.
467
- * Convert it to a plain timestamp (typically by interpreting as timestamptz
468
- * in that timezone, then casting to plain timestamp). This happens when:
469
- * 1. A constant with timezone is used (constants don't have dialect context)
470
- * 2. A literal with timezone is used in a dialect that doesn't support offset timestamps
471
- * @returns SQL that produces a plain TIMESTAMP value
346
+ * The timestamp at which `timezone`, or the query time zone when there is
347
+ * none, shows the civil time `literal`.
472
348
  */
473
- abstract sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
349
+ sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
474
350
  /**
475
351
  * Generate SQL for an offset TIMESTAMP literal (TIMESTAMP WITH TIME ZONE).
476
352
  * Only called for dialects where hasOffsetTimestamp = true.
@@ -548,6 +424,16 @@ export declare abstract class Dialect {
548
424
  * SQL to generate to get a boolean value for a boolean expression
549
425
  */
550
426
  sqlBoolean(bv: boolean): string;
427
+ /**
428
+ * Malloy's `not`: of NULL, true. A dialect with booleanType 'none' has no
429
+ * boolean value for COALESCE to return, so the test is written as a
430
+ * condition.
431
+ */
432
+ sqlNot(condition: string): string;
433
+ /**
434
+ * A condition Malloy defines as true when SQL says NULL: `!=` and `!~`
435
+ */
436
+ sqlNullIsTrue(condition: string): string;
551
437
  /**
552
438
  * What a boolean value looks like in a query result
553
439
  */
@@ -4,9 +4,9 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.Dialect = exports.dayIndex = exports.EscapeStyle = exports.MAX_DECIMAL38 = exports.MIN_DECIMAL38 = exports.MAX_INT128 = exports.MIN_INT128 = exports.MAX_INT64 = exports.MIN_INT64 = exports.MAX_INT32 = exports.MIN_INT32 = void 0;
7
+ exports.Dialect = exports.dayIndex = exports.CivilTime = exports.EscapeStyle = exports.MAX_DECIMAL38 = exports.MIN_DECIMAL38 = exports.MAX_INT128 = exports.MIN_INT128 = exports.MAX_INT64 = exports.MIN_INT64 = exports.MAX_INT32 = exports.MIN_INT32 = void 0;
8
8
  exports.inDays = inDays;
9
- exports.qtz = qtz;
9
+ exports.queryZone = queryZone;
10
10
  exports.turtleGroupSetCondition = turtleGroupSetCondition;
11
11
  const malloy_types_1 = require("../model/malloy_types");
12
12
  const table_path_1 = require("./table-path");
@@ -37,6 +37,14 @@ exports.EscapeStyle = {
37
37
  Backslash: 'backslash',
38
38
  Unset: 'unset',
39
39
  };
40
+ /**
41
+ * The two kinds of civil time: a calendar date, and a date with a time of
42
+ * day. Neither has a zone.
43
+ */
44
+ exports.CivilTime = {
45
+ Date: 'date',
46
+ DateAndTime: 'date_and_time',
47
+ };
40
48
  const allUnits = [
41
49
  'microsecond',
42
50
  'millisecond',
@@ -53,14 +61,10 @@ exports.dayIndex = allUnits.indexOf('day');
53
61
  function inDays(units) {
54
62
  return allUnits.indexOf(units) >= exports.dayIndex;
55
63
  }
56
- // Return the active query timezone, if it different than the
57
- // "native" timezone for timestamps.
58
- function qtz(qi) {
59
- const tz = qi.queryTimezone;
60
- if (tz === undefined || tz === qi.systemTimezone) {
61
- return undefined;
62
- }
63
- return tz;
64
+ /** The query time zone, or UTC when the query has none. */
65
+ function queryZone(qi) {
66
+ var _a;
67
+ return (_a = qi.queryTimezone) !== null && _a !== void 0 ? _a : 'UTC';
64
68
  }
65
69
  /**
66
70
  * The per-element condition for an aggregate turtle (sqlAggregateTurtle). On
@@ -431,96 +435,70 @@ class Dialect {
431
435
  ignoreInProject(_fieldName) {
432
436
  return false;
433
437
  }
434
- /**
435
- * Determines whether a truncation and/or offset operation needs to be performed in
436
- * civil (local) time, and if so, which timezone to use.
437
- *
438
- * Calendar-based operations (day, week, month, quarter, year) typically need civil time
439
- * computation because these units can cross DST boundaries, changing the UTC offset.
440
- *
441
- * Default implementation:
442
- * - Returns true for timestamps with truncation or calendar-unit offsets
443
- * - Uses the query timezone from QueryInfo, or 'UTC' if none specified
444
- * - Returns false for dates or sub-day offsets (hour, minute, second)
445
- *
446
- * Dialects can override this if they have different rules about which operations
447
- * require civil time computation.
448
- *
449
- * @param typeDef The Malloy type of the base expression
450
- * @param truncateTo The truncation unit, if any
451
- * @param offsetUnit The offset unit, if any
452
- * @param qi Query information including timezone settings
453
- * @returns Object with `needed` boolean and optional `tz` string
454
- */
455
- needsCivilTimeComputation(typeDef, truncateTo, offsetUnit, qi) {
456
- // Calendar units that can cross DST boundaries
457
- const calendarUnits = ['day', 'week', 'month', 'quarter', 'year'];
458
- const isCalendarTruncate = truncateTo !== undefined && calendarUnits.includes(truncateTo);
459
- const isCalendarOffset = offsetUnit !== undefined && calendarUnits.includes(offsetUnit);
460
- const tz = qtz(qi);
461
- // Timestamps with calendar truncation/offset need civil time computation
462
- // BUT only if there's actually a timezone to convert to/from
463
- const needed = malloy_types_1.TD.isAnyTimestamp(typeDef) &&
464
- (isCalendarTruncate || isCalendarOffset) &&
465
- tz !== undefined;
466
- return { needed, tz };
438
+ sqlTimeExtractExpr(qi, from) {
439
+ const time = from.e;
440
+ if (malloy_types_1.TD.isAnyTimestamp(time.typeDef)) {
441
+ const civil = this.sqlConvertToCivilTime(time.sql, queryZone(qi), time.typeDef);
442
+ return this.sqlCivilExtract(civil, from.units, exports.CivilTime.DateAndTime);
443
+ }
444
+ return this.sqlCivilExtract(time.sql, from.units, exports.CivilTime.Date);
467
445
  }
468
446
  /**
469
- * Unified function for truncation and/or offset operations. This turns out to
470
- * be a very common operation and one which can be optimized by doing it together.
471
- *
472
- * Much of the complexity has to do with getting timezone values set up so that
473
- * they will truncate/offset in the query timezone instead of UTC.
474
- *
475
- * The intention is that this implementation will work for all dialects, and all
476
- * the dialect peculiarities are handled with the new primitives introduced to
477
- * support this function:
478
- * - needsCivilTimeComputation: Determines if operation needs civil time
479
- * - sqlConvertToCivilTime/sqlConvertFromCivilTime: Timezone conversion
480
- * - sqlTruncate: Truncation operation
481
- * - sqlOffsetTime: Interval arithmetic
482
- *
483
- * OFFSET TIMESTAMP BEHAVIOR:
484
- * - Plain timestamps (offset=false): Always return plain TIMESTAMP in UTC
485
- * - Offset timestamps (offset=true):
486
- * - Simple path: Operate in native embedded timezone, return TIMESTAMPTZ
487
- * - Civil path: Convert to query timezone (preserving instant), operate there,
488
- * return TIMESTAMPTZ in query timezone via sqlConvertFromCivilTime
489
- * - BigQuery/MySQL: No offset timestamp support, this logic never runs
490
- *
491
- * @param baseExpr The time expression to operate on (already compiled, with .sql populated)
492
- * @param qi Query information including timezone
493
- * @param truncateTo Optional truncation unit (year, month, day, etc.)
494
- * @param offset Optional offset to apply (after truncation if both present)
447
+ * Truncation and/or offset, the offset applied after the truncation.
448
+ * Doing both in one pass converts to and from civil time only once.
495
449
  */
496
450
  sqlTruncAndOffset(baseExpr, qi, truncateTo, offset) {
497
- // Determine if we need to work in civil (local) time
498
- if (malloy_types_1.TD.isAnyTimestamp(baseExpr.typeDef)) {
499
- const { needed: needsCivil, tz } = this.needsCivilTimeComputation(baseExpr.typeDef, truncateTo, offset === null || offset === void 0 ? void 0 : offset.unit, qi);
500
- if (needsCivil && tz) {
501
- // Civil time path: convert to local time, operate, convert back to UTC
502
- const civilResult = this.sqlConvertToCivilTime(baseExpr.sql, tz, baseExpr.typeDef);
503
- let expr = civilResult.sql;
504
- const civilTypeDef = civilResult.typeDef;
505
- if (truncateTo) {
506
- expr = this.sqlTruncate(expr, truncateTo, civilTypeDef, true, tz);
507
- }
508
- if (offset) {
509
- expr = this.sqlOffsetTime(expr, offset.op, offset.magnitude, offset.unit, civilTypeDef, true, tz);
510
- }
511
- return this.sqlConvertFromCivilTime(expr, tz, baseExpr.typeDef);
512
- }
451
+ const timeType = baseExpr.typeDef;
452
+ if (!malloy_types_1.TD.isAnyTimestamp(timeType)) {
453
+ return this.civilTruncAndOffset(baseExpr.sql, exports.CivilTime.Date, truncateTo, offset);
513
454
  }
514
- // Simple path: no civil time conversion needed
515
- let sql = baseExpr.sql;
455
+ const usesQueryZone = truncateTo !== undefined ||
456
+ (offset !== undefined && (0, malloy_types_1.isDateUnit)(offset.unit));
457
+ const zone = usesQueryZone ? queryZone(qi) : 'UTC';
458
+ const civil = this.sqlConvertToCivilTime(baseExpr.sql, zone, timeType);
459
+ return this.sqlConvertFromCivilTime(this.civilTruncAndOffset(civil, exports.CivilTime.DateAndTime, truncateTo, offset), zone, timeType);
460
+ }
461
+ civilTruncAndOffset(expr, civilType, truncateTo, offset) {
462
+ let sql = expr;
516
463
  if (truncateTo) {
517
- sql = this.sqlTruncate(sql, truncateTo, baseExpr.typeDef, false, qtz(qi));
464
+ sql = this.sqlTruncate(sql, truncateTo, civilType);
518
465
  }
519
466
  if (offset) {
520
- sql = this.sqlOffsetTime(sql, offset.op, offset.magnitude, offset.unit, baseExpr.typeDef, false, qtz(qi));
467
+ sql = this.sqlOffsetTime(sql, offset.op, offset.magnitude, offset.unit, civilType);
521
468
  }
522
469
  return sql;
523
470
  }
471
+ /**
472
+ * A cast between date, timestamp and timestamptz, or undefined for any
473
+ * other cast.
474
+ */
475
+ sqlTimeCast(qi, cast) {
476
+ var _a;
477
+ const { srcTypeDef, dstTypeDef } = this.sqlCastPrep(cast);
478
+ const expr = (_a = cast.e.sql) !== null && _a !== void 0 ? _a : '';
479
+ if (malloy_types_1.TD.isAnyTimestamp(srcTypeDef)) {
480
+ if (malloy_types_1.TD.isDate(dstTypeDef)) {
481
+ return this.sqlCivilToDate(this.sqlConvertToCivilTime(expr, queryZone(qi), srcTypeDef));
482
+ }
483
+ if (malloy_types_1.TD.isAnyTimestamp(dstTypeDef) &&
484
+ dstTypeDef.type !== srcTypeDef.type) {
485
+ // The same instant in another SQL type. A round trip through civil
486
+ // time in any zone keeps the instant; UTC costs the least.
487
+ return this.sqlConvertFromCivilTime(this.sqlConvertToCivilTime(expr, 'UTC', srcTypeDef), 'UTC', dstTypeDef);
488
+ }
489
+ }
490
+ if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isAnyTimestamp(dstTypeDef)) {
491
+ return this.sqlConvertFromCivilTime(this.sqlDateToCivil(expr), queryZone(qi), dstTypeDef);
492
+ }
493
+ return undefined;
494
+ }
495
+ /**
496
+ * The timestamp at which `timezone`, or the query time zone when there is
497
+ * none, shows the civil time `literal`.
498
+ */
499
+ sqlTimestampLiteral(qi, literal, timezone) {
500
+ return this.sqlConvertFromCivilTime(this.sqlCivilLiteral(literal), timezone !== null && timezone !== void 0 ? timezone : queryZone(qi), { type: 'timestamp' });
501
+ }
524
502
  /**
525
503
  * Reject characters that can break SQL literals or
526
504
  * be changed by SQL formatting. Timezone-name validity belongs to the
@@ -574,6 +552,7 @@ class Dialect {
574
552
  * @returns The SQL translation of the expression, or undefined
575
553
  */
576
554
  exprToSQL(qi, df) {
555
+ var _a;
577
556
  switch (df.node) {
578
557
  case 'now':
579
558
  return this.sqlNowExpr();
@@ -603,7 +582,7 @@ class Dialect {
603
582
  case 'extract':
604
583
  return this.sqlTimeExtractExpr(qi, df);
605
584
  case 'cast':
606
- return this.sqlCast(qi, df);
585
+ return (_a = this.sqlTimeCast(qi, df)) !== null && _a !== void 0 ? _a : this.sqlCast(qi, df);
607
586
  case 'regexpMatch':
608
587
  return this.sqlRegexpMatch(df);
609
588
  case '/': {
@@ -756,6 +735,26 @@ class Dialect {
756
735
  }
757
736
  return bv ? 'true' : 'false';
758
737
  }
738
+ /**
739
+ * Malloy's `not`: of NULL, true. A dialect with booleanType 'none' has no
740
+ * boolean value for COALESCE to return, so the test is written as a
741
+ * condition.
742
+ */
743
+ sqlNot(condition) {
744
+ if (this.booleanType === 'none') {
745
+ return `(CASE WHEN ${condition} THEN 1 ELSE 0 END = 0)`;
746
+ }
747
+ return `COALESCE(NOT ${condition},TRUE)`;
748
+ }
749
+ /**
750
+ * A condition Malloy defines as true when SQL says NULL: `!=` and `!~`
751
+ */
752
+ sqlNullIsTrue(condition) {
753
+ if (this.booleanType === 'none') {
754
+ return `(CASE WHEN NOT (${condition}) THEN 1 ELSE 0 END = 0)`;
755
+ }
756
+ return `COALESCE(${condition},true)`;
757
+ }
759
758
  /**
760
759
  * What a boolean value looks like in a query result
761
760
  */
@@ -1,6 +1,6 @@
1
1
  import type { Sampling, AtomicTypeDef, RegexMatchExpr, MeasureTimeExpr, BasicAtomicTypeDef, RecordLiteralNode, OrderBy, TimestampUnit } from '../../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from '../functions';
3
- import type { CompiledOrderBy, DialectFieldList, FieldReferenceType, IntegerTypeMapping } from '../dialect';
3
+ import type { CivilTime, CompiledOrderBy, DialectFieldList, FieldReferenceType, IntegerTypeMapping } from '../dialect';
4
4
  import { PostgresBase } from '../pg_impl';
5
5
  import type { ValidateTablePathResult } from '../table-path';
6
6
  export declare class DuckDBDialect extends PostgresBase {
@@ -62,7 +62,7 @@ export declare class DuckDBDialect extends PostgresBase {
62
62
  castToString(expression: string): string;
63
63
  concat(...values: string[]): string;
64
64
  validateTypeName(sqlType: string): boolean;
65
- sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
65
+ sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _civilType: CivilTime): string;
66
66
  sqlRegexpMatch(df: RegexMatchExpr): string;
67
67
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
68
68
  sqlLiteralRecord(lit: RecordLiteralNode): string;
@@ -332,7 +332,7 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
332
332
  // Brackets: INT[ ]
333
333
  return sqlType.match(/^[A-Za-z\s(),[\]0-9]*$/) !== null;
334
334
  }
335
- sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
335
+ sqlOffsetTime(expr, op, magnitude, unit, _civilType) {
336
336
  // DuckDB doesn't support INTERVAL '1' WEEK, convert to days
337
337
  let offsetUnit = unit;
338
338
  let offsetMag = magnitude;
@@ -1,6 +1,6 @@
1
1
  export type { DialectFunctionOverloadDef, DefinitionBlueprint, DefinitionBlueprintMap, OverloadedDefinitionBlueprint, } from './functions/util';
2
2
  export { arg, anyExprType, makeParam, overload, minScalar, minAggregate, maxScalar, spread, param, variadicParam, literal, sql, } from './functions/util';
3
- export { Dialect, qtz } from './dialect';
3
+ export { Dialect, queryZone } from './dialect';
4
4
  export type { DialectFieldList, CompiledOrderBy, LateralJoinExpression, QueryInfo, FieldReferenceType, GroupByClauseType, OrderByClauseType, } from './dialect';
5
5
  export { StandardSQLDialect } from './standardsql';
6
6
  export { PostgresDialect } from './postgres';