@malloydata/malloy 0.0.323 → 0.0.325

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 (54) hide show
  1. package/CONTEXT.md +57 -0
  2. package/dist/api/core.js +2 -0
  3. package/dist/api/util.js +16 -24
  4. package/dist/dialect/dialect.d.ts +113 -23
  5. package/dist/dialect/dialect.js +83 -13
  6. package/dist/dialect/duckdb/duckdb.d.ts +1 -4
  7. package/dist/dialect/duckdb/duckdb.js +13 -17
  8. package/dist/dialect/mysql/mysql.d.ts +9 -4
  9. package/dist/dialect/mysql/mysql.js +18 -11
  10. package/dist/dialect/pg_impl.d.ts +11 -2
  11. package/dist/dialect/pg_impl.js +79 -15
  12. package/dist/dialect/postgres/postgres.d.ts +1 -4
  13. package/dist/dialect/postgres/postgres.js +6 -18
  14. package/dist/dialect/snowflake/snowflake.d.ts +10 -4
  15. package/dist/dialect/snowflake/snowflake.js +80 -31
  16. package/dist/dialect/standardsql/standardsql.d.ts +9 -4
  17. package/dist/dialect/standardsql/standardsql.js +21 -19
  18. package/dist/dialect/tiny_parser.js +1 -1
  19. package/dist/dialect/trino/trino.d.ts +19 -6
  20. package/dist/dialect/trino/trino.js +163 -31
  21. package/dist/index.d.ts +1 -1
  22. package/dist/lang/ast/expressions/expr-granular-time.js +26 -8
  23. package/dist/lang/ast/expressions/expr-props.d.ts +24 -0
  24. package/dist/lang/ast/expressions/for-range.d.ts +1 -1
  25. package/dist/lang/ast/expressions/for-range.js +5 -4
  26. package/dist/lang/ast/expressions/time-literal.d.ts +9 -7
  27. package/dist/lang/ast/expressions/time-literal.js +43 -50
  28. package/dist/lang/ast/query-items/field-declaration.js +1 -2
  29. package/dist/lang/ast/time-utils.js +1 -1
  30. package/dist/lang/ast/typedesc-utils.d.ts +1 -0
  31. package/dist/lang/ast/typedesc-utils.js +14 -1
  32. package/dist/lang/ast/types/expression-def.js +1 -1
  33. package/dist/lang/ast/types/granular-result.js +2 -1
  34. package/dist/lang/composite-source-utils.js +1 -1
  35. package/dist/lang/lib/Malloy/MalloyLexer.d.ts +76 -75
  36. package/dist/lang/lib/Malloy/MalloyLexer.js +1252 -1243
  37. package/dist/lang/lib/Malloy/MalloyParser.d.ts +77 -75
  38. package/dist/lang/lib/Malloy/MalloyParser.js +515 -510
  39. package/dist/lang/malloy-to-stable-query.js +13 -14
  40. package/dist/lang/test/expr-to-str.js +5 -1
  41. package/dist/malloy.d.ts +3 -2
  42. package/dist/malloy.js +6 -0
  43. package/dist/model/field_instance.js +1 -1
  44. package/dist/model/filter_compilers.d.ts +2 -1
  45. package/dist/model/filter_compilers.js +8 -5
  46. package/dist/model/malloy_types.d.ts +31 -9
  47. package/dist/model/malloy_types.js +49 -6
  48. package/dist/model/query_node.d.ts +2 -2
  49. package/dist/model/query_node.js +1 -0
  50. package/dist/model/query_query.js +15 -3
  51. package/dist/to_stable.js +13 -1
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/package.json +6 -6
package/CONTEXT.md ADDED
@@ -0,0 +1,57 @@
1
+ # Malloy Core Package
2
+
3
+ The `malloy` package is the heart of the Malloy language implementation. It contains the compiler, translator, and runtime system that powers Malloy's semantic modeling and query capabilities.
4
+
5
+ ## Package Structure
6
+
7
+ ```
8
+ packages/malloy/
9
+ ├── src/
10
+ │ ├── lang/ # Translator: Parse tree → AST → IR (see src/lang/CONTEXT.md)
11
+ │ ├── model/ # Compiler: IR → SQL (see src/model/CONTEXT.md)
12
+ │ ├── dialect/ # Database-specific SQL generation
13
+ │ ├── api/ # Public API interfaces
14
+ │ ├── connection/ # Database connection abstractions
15
+ │ └── malloy.ts # Main entry point
16
+ ```
17
+
18
+ ## Two-Phase Architecture
19
+
20
+ The Malloy compilation process is split into two distinct phases:
21
+
22
+ ### Phase 1: Translation (src/lang/)
23
+ The translator takes Malloy source code and transforms it into an Intermediate Representation (IR).
24
+
25
+ **Process:**
26
+ 1. ANTLR parser generates parse tree from source code
27
+ 2. Parse tree is transformed into Abstract Syntax Tree (AST)
28
+ 3. AST is analyzed and transformed into IR
29
+
30
+ **Key characteristics:**
31
+ - IR is a **serializable data format** (plain objects, not class instances)
32
+ - IR fully describes the semantic model independent of SQL
33
+ - IR can be cached, transmitted, and reused across compilations
34
+
35
+ For detailed information about the translator, see [src/lang/CONTEXT.md](src/lang/CONTEXT.md).
36
+
37
+ ### Phase 2: Compilation (src/model/)
38
+ The compiler takes IR and generates SQL queries for specific database dialects.
39
+
40
+ **Process:**
41
+ 1. IR is read and analyzed
42
+ 2. Query operations are transformed into SQL expressions
43
+ 3. Dialect-specific SQL is generated
44
+ 4. Metadata is generated for result processing
45
+
46
+ **Key characteristics:**
47
+ - Produces SQL that can be executed on target database
48
+ - Includes metadata to interpret and render results
49
+ - Dialect-agnostic until final SQL generation step
50
+
51
+ For detailed information about the compiler, see [src/model/CONTEXT.md](src/model/CONTEXT.md).
52
+
53
+ ## Subsystem Context
54
+
55
+ For deeper details on specific subsystems:
56
+ - [src/lang/CONTEXT.md](src/lang/CONTEXT.md) - Translator architecture (grammar, AST, IR generation)
57
+ - [src/model/CONTEXT.md](src/model/CONTEXT.md) - Compiler architecture (SQL generation, expression compilation)
package/dist/api/core.js CHANGED
@@ -101,6 +101,8 @@ function typeDefFromField(type) {
101
101
  return { type: 'boolean' };
102
102
  case 'timestamp_type':
103
103
  return { type: 'timestamp', timeframe: type.timeframe };
104
+ case 'timestamptz_type':
105
+ return { type: 'timestamptz', timeframe: type.timeframe };
104
106
  case 'date_type':
105
107
  return { type: 'date', timeframe: type.timeframe };
106
108
  case 'sql_native_type':
package/dist/api/util.js CHANGED
@@ -15,7 +15,6 @@ exports.nodeToLiteralValue = nodeToLiteralValue;
15
15
  exports.mapLogs = mapLogs;
16
16
  const malloy_tag_1 = require("@malloydata/malloy-tag");
17
17
  const annotation_1 = require("../annotation");
18
- const model_1 = require("../model");
19
18
  const to_stable_1 = require("../to_stable");
20
19
  const luxon_1 = require("luxon");
21
20
  function wrapLegacyInfoConnection(connection) {
@@ -92,7 +91,8 @@ function mapData(data, schema) {
92
91
  return { kind: 'null_cell' };
93
92
  }
94
93
  else if (field.type.kind === 'date_type' ||
95
- field.type.kind === 'timestamp_type') {
94
+ field.type.kind === 'timestamp_type' ||
95
+ field.type.kind === 'timestamptz_type') {
96
96
  const time_value = valueToDate(value).toISOString();
97
97
  if (field.type.kind === 'date_type') {
98
98
  return { kind: 'date_cell', date_value: time_value };
@@ -250,28 +250,20 @@ function nodeToLiteralValue(expr) {
250
250
  return { kind: 'boolean_literal', boolean_value: true };
251
251
  case 'false':
252
252
  return { kind: 'boolean_literal', boolean_value: false };
253
- case 'timeLiteral': {
254
- if (expr.typeDef.type === 'date') {
255
- if (expr.typeDef.timeframe === undefined ||
256
- (0, model_1.isDateUnit)(expr.typeDef.timeframe)) {
257
- return {
258
- kind: 'date_literal',
259
- date_value: expr.literal,
260
- timezone: expr.timezone,
261
- granularity: expr.typeDef.timeframe,
262
- };
263
- }
264
- return undefined;
265
- }
266
- else {
267
- return {
268
- kind: 'timestamp_literal',
269
- timestamp_value: expr.literal,
270
- timezone: expr.timezone,
271
- granularity: expr.typeDef.timeframe,
272
- };
273
- }
274
- }
253
+ case 'dateLiteral':
254
+ return {
255
+ kind: 'date_literal',
256
+ date_value: expr.literal,
257
+ granularity: expr.typeDef.timeframe,
258
+ };
259
+ case 'timestampLiteral':
260
+ case 'timestamptzLiteral':
261
+ return {
262
+ kind: 'timestamp_literal',
263
+ timestamp_value: expr.literal,
264
+ timezone: expr.timezone,
265
+ granularity: expr.typeDef.timeframe,
266
+ };
275
267
  default:
276
268
  return undefined;
277
269
  }
@@ -1,4 +1,4 @@
1
- import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralNode, RecordLiteralNode, ArrayLiteralNode, BasicAtomicTypeDef, OrderBy, TimestampUnit, TimeExpr } from '../model/malloy_types';
1
+ import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralExpr, RecordLiteralNode, ArrayLiteralNode, BasicAtomicTypeDef, OrderBy, TimestampUnit, ATimestampTypeDef, TimeExpr, TemporalFieldType } from '../model/malloy_types';
2
2
  import type { DialectFunctionOverloadDef } from './functions';
3
3
  interface DialectField {
4
4
  typeDef: AtomicTypeDef;
@@ -46,6 +46,7 @@ export declare abstract class Dialect {
46
46
  cantPartitionWindowFunctionsOnExpressions: boolean;
47
47
  supportsPipelinesInViews: boolean;
48
48
  supportsArraysInData: boolean;
49
+ hasTimestamptz: boolean;
49
50
  readsNestedData: boolean;
50
51
  orderByClause: OrderByClauseType;
51
52
  nullMatchesFunctionSignature: boolean;
@@ -61,6 +62,11 @@ export declare abstract class Dialect {
61
62
  compoundObjectInSchema: boolean;
62
63
  booleanType: BooleanTypeSupport;
63
64
  likeEscape: boolean;
65
+ /**
66
+ * Create the appropriate time literal IR node based on dialect support.
67
+ * Static method so it can be called with undefined dialect (e.g., ConstantFieldSpace).
68
+ */
69
+ static makeTimeLiteralNode(dialect: Dialect | undefined, literal: string, timezone: string | undefined, units: TimestampUnit | undefined, typ: TemporalFieldType): TimeLiteralExpr;
64
70
  abstract getDialectFunctionOverrides(): {
65
71
  [name: string]: DialectFunctionOverloadDef[];
66
72
  };
@@ -93,38 +99,87 @@ export declare abstract class Dialect {
93
99
  abstract sqlNowExpr(): string;
94
100
  abstract sqlTimeExtractExpr(qi: QueryInfo, xFrom: TimeExtractExpr): string;
95
101
  abstract sqlMeasureTimeExpr(e: MeasureTimeExpr): string;
102
+ /**
103
+ * Generate SQL for type casting expressions.
104
+ *
105
+ * Most casts are simple: `CAST(expr AS type)` or `TRY_CAST(expr AS type)` for safe casts.
106
+ *
107
+ * However, when a query timezone is set, casts between temporal types (date, timestamp, timestamptz)
108
+ * require special handling to ensure correct timezone semantics:
109
+ *
110
+ * **Timezone-Aware Cast Semantics:**
111
+ *
112
+ * 1. **TIMESTAMP → DATE**:
113
+ * - TIMESTAMP represents UTC wall clock
114
+ * - Convert to query timezone, then extract date
115
+ * - Example: TIMESTAMP '2020-02-20 00:00:00' with tz 'America/Mexico_City' → '2020-02-19'
116
+ *
117
+ * 2. **TIMESTAMPTZ → DATE**:
118
+ * - TIMESTAMPTZ represents absolute instant
119
+ * - Convert to query timezone, then extract date
120
+ * - Example: TIMESTAMPTZ '2020-02-20 00:00:00 UTC' with tz 'America/Mexico_City' → '2020-02-19'
121
+ *
122
+ * 3. **DATE → TIMESTAMP**:
123
+ * - DATE represents civil date
124
+ * - Interpret as midnight in query timezone, return UTC wall clock
125
+ * - Example: DATE '2020-02-20' with tz 'America/Mexico_City' → TIMESTAMP '2020-02-20 06:00:00' (UTC)
126
+ *
127
+ * 4. **DATE → TIMESTAMPTZ**:
128
+ * - DATE represents civil date
129
+ * - Interpret as midnight in query timezone, create instant
130
+ * - Example: DATE '2020-02-20' with tz 'America/Mexico_City' → instant at 2020-02-20 06:00:00 UTC
131
+ *
132
+ * 5. **TIMESTAMPTZ → TIMESTAMP**:
133
+ * - TIMESTAMPTZ represents absolute instant
134
+ * - Extract wall clock in query timezone, return as TIMESTAMP
135
+ * - Example: TIMESTAMPTZ '2020-02-20 00:00:00 UTC' with tz 'America/Mexico_City' → TIMESTAMP '2020-02-19 18:00:00'
136
+ *
137
+ * 6. **TIMESTAMP → TIMESTAMPTZ**:
138
+ * - TIMESTAMP represents UTC wall clock
139
+ * - Interpret as being in query timezone
140
+ * - Example: TIMESTAMP '2020-02-20 00:00:00' with tz 'America/Mexico_City' → instant at 2020-02-20 06:00:00 UTC
141
+ *
142
+ * **Implementation Notes:**
143
+ *
144
+ * - Dialects without timestamptz support (MySQL, BigQuery, StandardSQL) only need cases 1-3
145
+ * - Without query timezone, most casts are simple `CAST(expr AS type)`
146
+ *
147
+ * @param qi - Query info containing timezone and other context
148
+ * @param cast - The typecast expression to generate SQL for
149
+ * @returns SQL string for the cast operation
150
+ */
96
151
  abstract sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
97
152
  abstract sqlRegexpMatch(df: RegexMatchExpr): string;
98
153
  /**
99
- * Converts a UTC timestamp expression to civil (local) time in the specified timezone.
100
- * Used when performing timezone-aware calendar arithmetic.
154
+ * Converts a Malloy timestamp to "civil time" for calendar operations in a timezone.
101
155
  *
102
- * Example outputs:
103
- * - BigQuery: `DATETIME(timestamp_expr, 'Europe/Dublin')`
104
- * - PostgreSQL: `(timestamp_expr)::TIMESTAMPTZ AT TIME ZONE 'Europe/Dublin'`
105
- * - DuckDB: `(timestamp_expr)::TIMESTAMPTZ AT TIME ZONE 'Europe/Dublin'`
106
- * - Trino: `timestamp_expr AT TIME ZONE 'Europe/Dublin'`
156
+ * Each dialect selects its own SQL type to represent civil time (e.g., plain TIMESTAMP,
157
+ * TIMESTAMP WITH TIME ZONE, or DATETIME). The civil space is where timezone-aware
158
+ * truncation and interval arithmetic happen. Operations like sqlTruncate and sqlOffsetTime
159
+ * are aware of the civil space and work correctly within it.
107
160
  *
108
- * @param expr The SQL expression for the UTC timestamp
109
- * @param timezone The target timezone (e.g., 'Europe/Dublin', 'America/Los_Angeles')
110
- * @returns SQL expression representing the timestamp in civil (local) time
161
+ * @param expr The SQL expression for the Malloy timestamp (plain or timestamptz)
162
+ * @param timezone The target timezone for civil operations
163
+ * @param typeDef The Malloy type of the input expression
164
+ * @returns Object with SQL expression and the SQL type it evaluates to (the civil type)
111
165
  */
112
- abstract sqlConvertToCivilTime(expr: string, timezone: string): string;
166
+ abstract sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
167
+ sql: string;
168
+ typeDef: AtomicTypeDef;
169
+ };
113
170
  /**
114
- * Converts a civil (local) time expression back to a UTC timestamp.
115
- * The inverse of sqlConvertToCivilTime.
171
+ * Converts from civil time back to a Malloy timestamp type.
116
172
  *
117
- * Example outputs:
118
- * - BigQuery: `TIMESTAMP(datetime_expr, 'Europe/Dublin')`
119
- * - PostgreSQL: `((datetime_expr) AT TIME ZONE 'Europe/Dublin')::TIMESTAMP`
120
- * - DuckDB: `((datetime_expr) AT TIME ZONE 'Europe/Dublin')::TIMESTAMP`
121
- * - Trino: `CAST(at_timezone(timestamptz_expr, 'UTC') AS TIMESTAMP)`
173
+ * This is the inverse of sqlConvertToCivilTime. Takes a value in the dialect's
174
+ * civil space and converts it back to either a plain timestamp (UTC) or a
175
+ * timestamptz, depending on the destination type.
122
176
  *
123
- * @param expr The SQL expression for the civil time
177
+ * @param expr The SQL expression in civil time
124
178
  * @param timezone The timezone of the civil time
125
- * @returns SQL expression representing the UTC timestamp
179
+ * @param destTypeDef The destination Malloy timestamp type (plain or timestamptz)
180
+ * @returns SQL expression representing the Malloy timestamp
126
181
  */
127
- abstract sqlConvertFromCivilTime(expr: string, timezone: string): string;
182
+ abstract sqlConvertFromCivilTime(expr: string, timezone: string, destTypeDef: ATimestampTypeDef): string;
128
183
  /**
129
184
  * Truncates a time expression to the specified unit.
130
185
  *
@@ -193,6 +248,14 @@ export declare abstract class Dialect {
193
248
  * - sqlTruncate: Truncation operation
194
249
  * - sqlOffsetTime: Interval arithmetic
195
250
  *
251
+ * OFFSET TIMESTAMP BEHAVIOR:
252
+ * - Plain timestamps (offset=false): Always return plain TIMESTAMP in UTC
253
+ * - Offset timestamps (offset=true):
254
+ * - Simple path: Operate in native embedded timezone, return TIMESTAMPTZ
255
+ * - Civil path: Convert to query timezone (preserving instant), operate there,
256
+ * return TIMESTAMPTZ in query timezone via sqlConvertFromCivilTime
257
+ * - BigQuery/MySQL: No offset timestamp support, this logic never runs
258
+ *
196
259
  * @param baseExpr The time expression to operate on (already compiled, with .sql populated)
197
260
  * @param qi Query information including timezone
198
261
  * @param truncateTo Optional truncation unit (year, month, day, etc.)
@@ -203,7 +266,34 @@ export declare abstract class Dialect {
203
266
  magnitude: string;
204
267
  unit: TimestampUnit;
205
268
  }): string;
206
- abstract sqlLiteralTime(qi: QueryInfo, df: TimeLiteralNode): string;
269
+ /**
270
+ * Generate SQL for a DATE literal.
271
+ * @param literal - The date string in format 'YYYY-MM-DD'
272
+ * @returns SQL that produces a DATE value
273
+ */
274
+ abstract sqlDateLiteral(qi: QueryInfo, literal: string): string;
275
+ /**
276
+ * Generate SQL for a plain TIMESTAMP literal (without timezone offset).
277
+ * @param literal - The timestamp string in format 'YYYY-MM-DD HH:MM:SS'
278
+ * @param timezone - Optional timezone name (e.g., 'America/Los_Angeles')
279
+ * - If undefined: Create plain timestamp literal from the literal string
280
+ * - If defined: The literal string represents a civil time in the given timezone.
281
+ * Convert it to a plain timestamp (typically by interpreting as timestamptz
282
+ * in that timezone, then casting to plain timestamp). This happens when:
283
+ * 1. A constant with timezone is used (constants don't have dialect context)
284
+ * 2. A literal with timezone is used in a dialect that doesn't support offset timestamps
285
+ * @returns SQL that produces a plain TIMESTAMP value
286
+ */
287
+ abstract sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
288
+ /**
289
+ * Generate SQL for an offset TIMESTAMP literal (TIMESTAMP WITH TIME ZONE).
290
+ * Only called for dialects where hasOffsetTimestamp = true.
291
+ * @param literal - The timestamp string in format 'YYYY-MM-DD HH:MM:SS'
292
+ * @param timezone - The timezone name (e.g., 'America/Los_Angeles')
293
+ * @returns SQL that produces a TIMESTAMP WITH TIME ZONE value representing
294
+ * the civil time in the specified timezone
295
+ */
296
+ abstract sqlTimestamptzLiteral(qi: QueryInfo, literal: string, timezone: string): string;
207
297
  abstract sqlLiteralString(literal: string): string;
208
298
  abstract sqlLiteralRegexp(literal: string): string;
209
299
  abstract sqlLiteralArray(lit: ArrayLiteralNode): string;
@@ -62,6 +62,8 @@ class Dialect {
62
62
  this.supportsPipelinesInViews = true;
63
63
  // Some dialects don't supporrt arrays (mysql)
64
64
  this.supportsArraysInData = true;
65
+ // Does the dialect support timestamptz (TIMESTAMP WITH TIME ZONE)?
66
+ this.hasTimestamptz = false;
65
67
  // can read some version of ga_sample
66
68
  this.readsNestedData = true;
67
69
  // ORDER BY 1 DESC
@@ -89,6 +91,58 @@ class Dialect {
89
91
  // Like characters are escaped with ESCAPE clause
90
92
  this.likeEscape = true;
91
93
  }
94
+ /**
95
+ * Create the appropriate time literal IR node based on dialect support.
96
+ * Static method so it can be called with undefined dialect (e.g., ConstantFieldSpace).
97
+ */
98
+ static makeTimeLiteralNode(dialect, literal, timezone, units, typ) {
99
+ var _a;
100
+ // ConstantFieldSpace.dialectObj() returns undefined, so constants default to false
101
+ const hasTimestamptz = (_a = dialect === null || dialect === void 0 ? void 0 : dialect.hasTimestamptz) !== null && _a !== void 0 ? _a : false;
102
+ if (typ === 'date') {
103
+ return {
104
+ node: 'dateLiteral',
105
+ literal,
106
+ typeDef: {
107
+ type: 'date',
108
+ timeframe: units !== undefined && (0, malloy_types_1.isDateUnit)(units) ? units : undefined,
109
+ },
110
+ };
111
+ }
112
+ // typ === 'timestamp'
113
+ if (timezone && hasTimestamptz) {
114
+ // Dialect supports timestamptz - create timestamptzLiteral
115
+ return {
116
+ node: 'timestamptzLiteral',
117
+ literal,
118
+ typeDef: {
119
+ type: 'timestamptz',
120
+ timeframe: units,
121
+ },
122
+ timezone,
123
+ };
124
+ }
125
+ // Plain timestamp (either no timezone, or dialect doesn't support timestamptz)
126
+ if (timezone) {
127
+ return {
128
+ node: 'timestampLiteral',
129
+ literal,
130
+ typeDef: {
131
+ type: 'timestamp',
132
+ timeframe: units,
133
+ },
134
+ timezone,
135
+ };
136
+ }
137
+ return {
138
+ node: 'timestampLiteral',
139
+ literal,
140
+ typeDef: {
141
+ type: 'timestamp',
142
+ timeframe: units,
143
+ },
144
+ };
145
+ }
92
146
  sqlFinalStage(_lastStageName, _fields) {
93
147
  throw new Error('Dialect has no final Stage but called Anyway');
94
148
  }
@@ -133,7 +187,7 @@ class Dialect {
133
187
  const tz = qtz(qi);
134
188
  // Timestamps with calendar truncation/offset need civil time computation
135
189
  // BUT only if there's actually a timezone to convert to/from
136
- const needed = malloy_types_1.TD.isTimestamp(typeDef) &&
190
+ const needed = malloy_types_1.TD.isAnyTimestamp(typeDef) &&
137
191
  (isCalendarTruncate || isCalendarOffset) &&
138
192
  tz !== undefined;
139
193
  return { needed, tz };
@@ -153,6 +207,14 @@ class Dialect {
153
207
  * - sqlTruncate: Truncation operation
154
208
  * - sqlOffsetTime: Interval arithmetic
155
209
  *
210
+ * OFFSET TIMESTAMP BEHAVIOR:
211
+ * - Plain timestamps (offset=false): Always return plain TIMESTAMP in UTC
212
+ * - Offset timestamps (offset=true):
213
+ * - Simple path: Operate in native embedded timezone, return TIMESTAMPTZ
214
+ * - Civil path: Convert to query timezone (preserving instant), operate there,
215
+ * return TIMESTAMPTZ in query timezone via sqlConvertFromCivilTime
216
+ * - BigQuery/MySQL: No offset timestamp support, this logic never runs
217
+ *
156
218
  * @param baseExpr The time expression to operate on (already compiled, with .sql populated)
157
219
  * @param qi Query information including timezone
158
220
  * @param truncateTo Optional truncation unit (year, month, day, etc.)
@@ -160,17 +222,21 @@ class Dialect {
160
222
  */
161
223
  sqlTruncAndOffset(baseExpr, qi, truncateTo, offset) {
162
224
  // Determine if we need to work in civil (local) time
163
- const { needed: needsCivil, tz } = this.needsCivilTimeComputation(baseExpr.typeDef, truncateTo, offset === null || offset === void 0 ? void 0 : offset.unit, qi);
164
- if (needsCivil && tz) {
165
- // Civil time path: convert to local time, operate, convert back to UTC
166
- let expr = this.sqlConvertToCivilTime(baseExpr.sql, tz);
167
- if (truncateTo) {
168
- expr = this.sqlTruncate(expr, truncateTo, baseExpr.typeDef, true, tz);
169
- }
170
- if (offset) {
171
- expr = this.sqlOffsetTime(expr, offset.op, offset.magnitude, offset.unit, baseExpr.typeDef, true, tz);
225
+ if (malloy_types_1.TD.isAnyTimestamp(baseExpr.typeDef)) {
226
+ const { needed: needsCivil, tz } = this.needsCivilTimeComputation(baseExpr.typeDef, truncateTo, offset === null || offset === void 0 ? void 0 : offset.unit, qi);
227
+ if (needsCivil && tz) {
228
+ // Civil time path: convert to local time, operate, convert back to UTC
229
+ const civilResult = this.sqlConvertToCivilTime(baseExpr.sql, tz, baseExpr.typeDef);
230
+ let expr = civilResult.sql;
231
+ const civilTypeDef = civilResult.typeDef;
232
+ if (truncateTo) {
233
+ expr = this.sqlTruncate(expr, truncateTo, civilTypeDef, true, tz);
234
+ }
235
+ if (offset) {
236
+ expr = this.sqlOffsetTime(expr, offset.op, offset.magnitude, offset.unit, civilTypeDef, true, tz);
237
+ }
238
+ return this.sqlConvertFromCivilTime(expr, tz, baseExpr.typeDef);
172
239
  }
173
- return this.sqlConvertFromCivilTime(expr, tz);
174
240
  }
175
241
  // Simple path: no civil time conversion needed
176
242
  let sql = baseExpr.sql;
@@ -237,8 +303,12 @@ class Dialect {
237
303
  }
238
304
  return;
239
305
  }
240
- case 'timeLiteral':
241
- return this.sqlLiteralTime(qi, df);
306
+ case 'dateLiteral':
307
+ return this.sqlDateLiteral(qi, df.literal);
308
+ case 'timestampLiteral':
309
+ return this.sqlTimestampLiteral(qi, df.literal, df.timezone);
310
+ case 'timestamptzLiteral':
311
+ return this.sqlTimestamptzLiteral(qi, df.literal, df.timezone);
242
312
  case 'stringLiteral':
243
313
  return this.sqlLiteralString(df.literal);
244
314
  case 'numberLiteral':
@@ -56,13 +56,10 @@ export declare class DuckDBDialect extends PostgresBase {
56
56
  };
57
57
  malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
58
58
  parseDuckDBType(sqlType: string): AtomicTypeDef;
59
- sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
59
+ sqlTypeToMalloyType(rawSqlType: string): BasicAtomicTypeDef;
60
60
  castToString(expression: string): string;
61
61
  concat(...values: string[]): string;
62
62
  validateTypeName(sqlType: string): boolean;
63
- sqlConvertToCivilTime(expr: string, timezone: string): string;
64
- sqlConvertFromCivilTime(expr: string, timezone: string): string;
65
- sqlTruncate(expr: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
66
63
  sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
67
64
  sqlRegexpMatch(df: RegexMatchExpr): string;
68
65
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
@@ -282,6 +282,9 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
282
282
  else if (malloyType.type === 'string') {
283
283
  return 'varchar';
284
284
  }
285
+ if (malloyType.type === 'timestamptz') {
286
+ return 'timestamp with time zone';
287
+ }
285
288
  return malloyType.type;
286
289
  }
287
290
  parseDuckDBType(sqlType) {
@@ -298,8 +301,12 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
298
301
  }
299
302
  }
300
303
  }
301
- sqlTypeToMalloyType(sqlType) {
304
+ sqlTypeToMalloyType(rawSqlType) {
302
305
  var _a, _b, _c;
306
+ const sqlType = rawSqlType.toUpperCase();
307
+ if (sqlType === 'TIMESTAMP WITH TIME ZONE') {
308
+ return { type: 'timestamptz' };
309
+ }
303
310
  // Remove decimal precision
304
311
  const ddbType = sqlType.replace(/^DECIMAL\(\d+,\d+\)/g, 'DECIMAL');
305
312
  // Remove trailing params
@@ -323,20 +330,6 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
323
330
  // Brackets: INT[ ]
324
331
  return sqlType.match(/^[A-Za-z\s(),[\]0-9]*$/) !== null;
325
332
  }
326
- sqlConvertToCivilTime(expr, timezone) {
327
- return `(${expr})::TIMESTAMPTZ AT TIME ZONE '${timezone}'`;
328
- }
329
- sqlConvertFromCivilTime(expr, timezone) {
330
- return `((${expr}) AT TIME ZONE '${timezone}')::TIMESTAMP`;
331
- }
332
- sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
333
- // DuckDB starts weeks on Monday, Malloy wants Sunday
334
- // Add 1 day before truncating, subtract 1 day after
335
- if (unit === 'week') {
336
- return `(DATE_TRUNC('${unit}', (${expr} + INTERVAL '1' DAY)) - INTERVAL '1' DAY)`;
337
- }
338
- return `DATE_TRUNC('${unit}', ${expr})`;
339
- }
340
333
  sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
341
334
  // DuckDB doesn't support INTERVAL '1' WEEK, convert to days
342
335
  let offsetUnit = unit;
@@ -412,10 +405,13 @@ class DuckDBTypeParser extends tiny_parser_1.TinyParser {
412
405
  baseType = { type: 'number', numberType: 'float' };
413
406
  }
414
407
  else if (id === 'TIMESTAMP') {
415
- if (this.peek().text === 'WITH') {
408
+ if (this.peek().text.toUpperCase() === 'WITH') {
416
409
  this.nextText('WITH', 'TIME', 'ZONE');
410
+ baseType = { type: 'timestamptz' };
411
+ }
412
+ else {
413
+ baseType = { type: 'timestamp' };
417
414
  }
418
- baseType = { type: 'timestamp' };
419
415
  }
420
416
  else if (duckDBToMalloyTypes[id]) {
421
417
  baseType = duckDBToMalloyTypes[id];
@@ -1,4 +1,4 @@
1
- import type { Sampling, MeasureTimeExpr, TimeLiteralNode, RegexMatchExpr, TimeExtractExpr, TypecastExpr, BasicAtomicTypeDef, AtomicTypeDef, ArrayLiteralNode, RecordLiteralNode } from '../../model/malloy_types';
1
+ import type { Sampling, MeasureTimeExpr, RegexMatchExpr, TimeExtractExpr, TypecastExpr, BasicAtomicTypeDef, AtomicTypeDef, TimestampTypeDef, ArrayLiteralNode, RecordLiteralNode } from '../../model/malloy_types';
2
2
  import type { BooleanTypeSupport, DialectFieldList, FieldReferenceType, OrderByClauseType, QueryInfo } from '../dialect';
3
3
  import { Dialect } from '../dialect';
4
4
  import type { DialectFunctionOverloadDef } from '../functions';
@@ -56,14 +56,19 @@ export declare class MySQLDialect extends Dialect {
56
56
  sqlMaybeQuoteIdentifier(identifier: string): string;
57
57
  sqlCreateTableAsSelect(_tableName: string, _sql: string): string;
58
58
  sqlNowExpr(): string;
59
- sqlConvertToCivilTime(expr: string, timezone: string): string;
60
- sqlConvertFromCivilTime(expr: string, timezone: string): string;
59
+ sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
60
+ sql: string;
61
+ typeDef: AtomicTypeDef;
62
+ };
63
+ sqlConvertFromCivilTime(expr: string, timezone: string, _destTypeDef: TimestampTypeDef): string;
61
64
  sqlTruncate(expr: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
62
65
  sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
63
66
  sqlTimeExtractExpr(qi: QueryInfo, te: TimeExtractExpr): string;
64
67
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
65
68
  sqlRegexpMatch(df: RegexMatchExpr): string;
66
- sqlLiteralTime(qi: QueryInfo, lt: TimeLiteralNode): string;
69
+ sqlDateLiteral(_qi: QueryInfo, literal: string): string;
70
+ sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
71
+ sqlTimestamptzLiteral(_qi: QueryInfo, _literal: string, _timezone: string): string;
67
72
  sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
68
73
  sqlAggDistinct(_key: string, _values: string[], _func: (valNames: string[]) => string): string;
69
74
  sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
@@ -291,10 +291,14 @@ class MySQLDialect extends dialect_1.Dialect {
291
291
  sqlNowExpr() {
292
292
  return 'LOCALTIMESTAMP';
293
293
  }
294
- sqlConvertToCivilTime(expr, timezone) {
295
- return `CONVERT_TZ(${expr}, 'UTC', '${timezone}')`;
296
- }
297
- sqlConvertFromCivilTime(expr, timezone) {
294
+ sqlConvertToCivilTime(expr, timezone, _typeDef) {
295
+ // MySQL has no timestamptz type, so typeDef.timestamptz will never be true
296
+ return {
297
+ sql: `CONVERT_TZ(${expr}, 'UTC', '${timezone}')`,
298
+ typeDef: { type: 'timestamp' },
299
+ };
300
+ }
301
+ sqlConvertFromCivilTime(expr, timezone, _destTypeDef) {
298
302
  return `CONVERT_TZ(${expr}, '${timezone}', 'UTC')`;
299
303
  }
300
304
  sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
@@ -378,15 +382,18 @@ class MySQLDialect extends dialect_1.Dialect {
378
382
  sqlRegexpMatch(df) {
379
383
  return `REGEXP_LIKE(${df.kids.expr.sql}, ${df.kids.regex.sql})`;
380
384
  }
381
- sqlLiteralTime(qi, lt) {
382
- if (malloy_types_1.TD.isDate(lt.typeDef)) {
383
- return `DATE '${lt.literal}'`;
384
- }
385
- const tz = lt.timezone || (0, dialect_1.qtz)(qi);
385
+ sqlDateLiteral(_qi, literal) {
386
+ return `DATE '${literal}'`;
387
+ }
388
+ sqlTimestampLiteral(qi, literal, timezone) {
389
+ const tz = timezone || (0, dialect_1.qtz)(qi);
386
390
  if (tz) {
387
- return ` CONVERT_TZ('${lt.literal}', '${tz}', 'UTC')`;
391
+ return `CONVERT_TZ('${literal}', '${tz}', 'UTC')`;
388
392
  }
389
- return `TIMESTAMP '${lt.literal}'`;
393
+ return `TIMESTAMP '${literal}'`;
394
+ }
395
+ sqlTimestamptzLiteral(_qi, _literal, _timezone) {
396
+ throw new Error('MySQL does not support timestamptz');
390
397
  }
391
398
  sqlMeasureTimeExpr(df) {
392
399
  let lVal = df.kids.left.sql;
@@ -1,4 +1,4 @@
1
- import type { ArrayLiteralNode, RecordLiteralNode, RegexMatchExpr, TimeExtractExpr, TimeLiteralNode, TypecastExpr } from '../model/malloy_types';
1
+ import type { ArrayLiteralNode, AtomicTypeDef, ATimestampTypeDef, RecordLiteralNode, RegexMatchExpr, TimeExtractExpr, TimestampUnit, TypecastExpr } from '../model/malloy_types';
2
2
  import type { QueryInfo } from './dialect';
3
3
  import { Dialect } from './dialect';
4
4
  export declare const timeExtractMap: Record<string, string>;
@@ -7,12 +7,21 @@ export declare const timeExtractMap: Record<string, string>;
7
7
  * same implementations for the much of the SQL code generation
8
8
  */
9
9
  export declare abstract class PostgresBase extends Dialect {
10
+ hasTimestamptz: boolean;
10
11
  sqlNowExpr(): string;
11
12
  sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
12
13
  sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
13
14
  sqlRegexpMatch(df: RegexMatchExpr): string;
14
- sqlLiteralTime(qi: QueryInfo, lt: TimeLiteralNode): string;
15
+ sqlDateLiteral(_qi: QueryInfo, literal: string): string;
16
+ sqlTimestampLiteral(qi: QueryInfo, literal: string, timezone: string | undefined): string;
17
+ sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
15
18
  sqlLiteralRecord(_lit: RecordLiteralNode): string;
16
19
  sqlLiteralArray(lit: ArrayLiteralNode): string;
17
20
  sqlMaybeQuoteIdentifier(identifier: string): string;
21
+ sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
22
+ sql: string;
23
+ typeDef: AtomicTypeDef;
24
+ };
25
+ sqlConvertFromCivilTime(expr: string, timezone: string, destTypeDef: ATimestampTypeDef): string;
26
+ sqlTruncate(expr: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, inCivilTime: boolean, _timezone?: string): string;
18
27
  }