@malloydata/malloy 0.0.322 → 0.0.324
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.
- package/dist/api/core.js +2 -0
- package/dist/api/util.js +16 -24
- package/dist/dialect/dialect.d.ts +200 -4
- package/dist/dialect/dialect.js +170 -5
- package/dist/dialect/duckdb/duckdb.d.ts +3 -3
- package/dist/dialect/duckdb/duckdb.js +22 -15
- package/dist/dialect/mysql/mysql.d.ts +11 -5
- package/dist/dialect/mysql/mysql.js +45 -54
- package/dist/dialect/pg_impl.d.ts +11 -3
- package/dist/dialect/pg_impl.js +78 -44
- package/dist/dialect/postgres/postgres.d.ts +3 -3
- package/dist/dialect/postgres/postgres.js +20 -15
- package/dist/dialect/snowflake/snowflake.d.ts +12 -4
- package/dist/dialect/snowflake/snowflake.js +92 -42
- package/dist/dialect/standardsql/standardsql.d.ts +15 -4
- package/dist/dialect/standardsql/standardsql.js +59 -48
- package/dist/dialect/tiny_parser.js +1 -1
- package/dist/dialect/trino/trino.d.ts +21 -4
- package/dist/dialect/trino/trino.js +192 -62
- package/dist/index.d.ts +1 -1
- package/dist/lang/ast/error-factory.js +1 -1
- package/dist/lang/ast/expressions/expr-granular-time.js +26 -8
- package/dist/lang/ast/expressions/expr-props.d.ts +24 -0
- package/dist/lang/ast/expressions/for-range.d.ts +1 -1
- package/dist/lang/ast/expressions/for-range.js +5 -4
- package/dist/lang/ast/expressions/time-literal.d.ts +9 -7
- package/dist/lang/ast/expressions/time-literal.js +43 -50
- package/dist/lang/ast/field-space/static-space.js +5 -1
- package/dist/lang/ast/query-items/field-declaration.js +1 -2
- package/dist/lang/ast/time-utils.js +1 -1
- package/dist/lang/ast/typedesc-utils.d.ts +1 -0
- package/dist/lang/ast/typedesc-utils.js +14 -1
- package/dist/lang/ast/types/expression-def.js +1 -1
- package/dist/lang/ast/types/granular-result.js +2 -1
- package/dist/lang/ast/types/malloy-element.js +10 -2
- package/dist/lang/composite-source-utils.js +1 -1
- package/dist/lang/lib/Malloy/MalloyLexer.d.ts +76 -75
- package/dist/lang/lib/Malloy/MalloyLexer.js +1252 -1243
- package/dist/lang/lib/Malloy/MalloyParser.d.ts +77 -75
- package/dist/lang/lib/Malloy/MalloyParser.js +515 -510
- package/dist/lang/malloy-to-stable-query.js +13 -14
- package/dist/lang/test/expr-to-str.js +5 -1
- package/dist/malloy.d.ts +3 -2
- package/dist/malloy.js +6 -0
- package/dist/model/expression_compiler.js +172 -145
- package/dist/model/field_instance.js +1 -1
- package/dist/model/filter_compilers.d.ts +2 -3
- package/dist/model/filter_compilers.js +140 -62
- package/dist/model/malloy_types.d.ts +50 -16
- package/dist/model/malloy_types.js +49 -6
- package/dist/model/query_node.d.ts +2 -2
- package/dist/model/query_node.js +1 -0
- package/dist/model/query_query.js +15 -3
- package/dist/to_stable.js +13 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
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 '
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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,
|
|
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
|
};
|
|
@@ -91,13 +97,203 @@ export declare abstract class Dialect {
|
|
|
91
97
|
sqlLiteralNumber(literal: string): string;
|
|
92
98
|
ignoreInProject(_fieldName: string): boolean;
|
|
93
99
|
abstract sqlNowExpr(): string;
|
|
94
|
-
abstract sqlTruncExpr(qi: QueryInfo, toTrunc: TimeTruncExpr): string;
|
|
95
100
|
abstract sqlTimeExtractExpr(qi: QueryInfo, xFrom: TimeExtractExpr): string;
|
|
96
101
|
abstract sqlMeasureTimeExpr(e: MeasureTimeExpr): string;
|
|
97
|
-
|
|
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
|
+
*/
|
|
98
151
|
abstract sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
|
99
152
|
abstract sqlRegexpMatch(df: RegexMatchExpr): string;
|
|
100
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Converts a Malloy timestamp to "civil time" for calendar operations in a timezone.
|
|
155
|
+
*
|
|
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.
|
|
160
|
+
*
|
|
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)
|
|
165
|
+
*/
|
|
166
|
+
abstract sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
|
|
167
|
+
sql: string;
|
|
168
|
+
typeDef: AtomicTypeDef;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Converts from civil time back to a Malloy timestamp type.
|
|
172
|
+
*
|
|
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.
|
|
176
|
+
*
|
|
177
|
+
* @param expr The SQL expression in civil time
|
|
178
|
+
* @param timezone The timezone of the civil time
|
|
179
|
+
* @param destTypeDef The destination Malloy timestamp type (plain or timestamptz)
|
|
180
|
+
* @returns SQL expression representing the Malloy timestamp
|
|
181
|
+
*/
|
|
182
|
+
abstract sqlConvertFromCivilTime(expr: string, timezone: string, destTypeDef: ATimestampTypeDef): string;
|
|
183
|
+
/**
|
|
184
|
+
* Truncates a time expression to the specified unit.
|
|
185
|
+
*
|
|
186
|
+
* @param expr The SQL expression to truncate
|
|
187
|
+
* @param unit The unit to truncate to (year, month, day, hour, etc.)
|
|
188
|
+
* @param typeDef The Malloy type of the expression (date, timestamp, etc.)
|
|
189
|
+
* @param inCivilTime If true, the expression is already in civil (local) time and should not
|
|
190
|
+
* be converted. If false, may need timezone conversion for timestamps.
|
|
191
|
+
* @param timezone Optional timezone for the operation. Only provided when timezone-aware
|
|
192
|
+
* truncation is needed but inCivilTime is false.
|
|
193
|
+
* @returns SQL expression representing the truncated time
|
|
194
|
+
*/
|
|
195
|
+
abstract sqlTruncate(expr: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, timezone?: string): string;
|
|
196
|
+
/**
|
|
197
|
+
* Adds or subtracts a time interval from a time expression.
|
|
198
|
+
*
|
|
199
|
+
* @param expr The SQL expression to offset
|
|
200
|
+
* @param op The operation: '+' for addition, '-' for subtraction
|
|
201
|
+
* @param magnitude The SQL expression for the interval magnitude (e.g., '6', '(delta_val)')
|
|
202
|
+
* @param unit The interval unit (year, month, day, hour, etc.)
|
|
203
|
+
* @param typeDef The Malloy type of the expression (date, timestamp, etc.)
|
|
204
|
+
* @param inCivilTime If true, the expression is already in civil (local) time and should not
|
|
205
|
+
* be converted. If false, may need timezone conversion for timestamps.
|
|
206
|
+
* @param timezone Optional timezone for the operation. Only provided when timezone-aware
|
|
207
|
+
* offset is needed but inCivilTime is false.
|
|
208
|
+
* @returns SQL expression representing the offset time
|
|
209
|
+
*/
|
|
210
|
+
abstract sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, timezone?: string): string;
|
|
211
|
+
/**
|
|
212
|
+
* Determines whether a truncation and/or offset operation needs to be performed in
|
|
213
|
+
* civil (local) time, and if so, which timezone to use.
|
|
214
|
+
*
|
|
215
|
+
* Calendar-based operations (day, week, month, quarter, year) typically need civil time
|
|
216
|
+
* computation because these units can cross DST boundaries, changing the UTC offset.
|
|
217
|
+
*
|
|
218
|
+
* Default implementation:
|
|
219
|
+
* - Returns true for timestamps with truncation or calendar-unit offsets
|
|
220
|
+
* - Uses the query timezone from QueryInfo, or 'UTC' if none specified
|
|
221
|
+
* - Returns false for dates or sub-day offsets (hour, minute, second)
|
|
222
|
+
*
|
|
223
|
+
* Dialects can override this if they have different rules about which operations
|
|
224
|
+
* require civil time computation.
|
|
225
|
+
*
|
|
226
|
+
* @param typeDef The Malloy type of the base expression
|
|
227
|
+
* @param truncateTo The truncation unit, if any
|
|
228
|
+
* @param offsetUnit The offset unit, if any
|
|
229
|
+
* @param qi Query information including timezone settings
|
|
230
|
+
* @returns Object with `needed` boolean and optional `tz` string
|
|
231
|
+
*/
|
|
232
|
+
needsCivilTimeComputation(typeDef: AtomicTypeDef, truncateTo: TimestampUnit | undefined, offsetUnit: TimestampUnit | undefined, qi: QueryInfo): {
|
|
233
|
+
needed: boolean;
|
|
234
|
+
tz: string | undefined;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Unified function for truncation and/or offset operations. This turns out to
|
|
238
|
+
* be a very common operation and one which can be optimized by doing it together.
|
|
239
|
+
*
|
|
240
|
+
* Much of the complexity has to do with getting timezone values set up so that
|
|
241
|
+
* they will truncate/offset in the query timezone instead of UTC.
|
|
242
|
+
*
|
|
243
|
+
* The intention is that this implementation will work for all dialects, and all
|
|
244
|
+
* the dialect peculiarities are handled with the new primitives introduced to
|
|
245
|
+
* support this function:
|
|
246
|
+
* - needsCivilTimeComputation: Determines if operation needs civil time
|
|
247
|
+
* - sqlConvertToCivilTime/sqlConvertFromCivilTime: Timezone conversion
|
|
248
|
+
* - sqlTruncate: Truncation operation
|
|
249
|
+
* - sqlOffsetTime: Interval arithmetic
|
|
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
|
+
*
|
|
259
|
+
* @param baseExpr The time expression to operate on (already compiled, with .sql populated)
|
|
260
|
+
* @param qi Query information including timezone
|
|
261
|
+
* @param truncateTo Optional truncation unit (year, month, day, etc.)
|
|
262
|
+
* @param offset Optional offset to apply (after truncation if both present)
|
|
263
|
+
*/
|
|
264
|
+
sqlTruncAndOffset(baseExpr: TimeExpr, qi: QueryInfo, truncateTo?: TimestampUnit, offset?: {
|
|
265
|
+
op: '+' | '-';
|
|
266
|
+
magnitude: string;
|
|
267
|
+
unit: TimestampUnit;
|
|
268
|
+
}): 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;
|
|
101
297
|
abstract sqlLiteralString(literal: string): string;
|
|
102
298
|
abstract sqlLiteralRegexp(literal: string): string;
|
|
103
299
|
abstract sqlLiteralArray(lit: ArrayLiteralNode): string;
|
package/dist/dialect/dialect.js
CHANGED
|
@@ -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
|
}
|
|
@@ -104,6 +158,96 @@ class Dialect {
|
|
|
104
158
|
ignoreInProject(_fieldName) {
|
|
105
159
|
return false;
|
|
106
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Determines whether a truncation and/or offset operation needs to be performed in
|
|
163
|
+
* civil (local) time, and if so, which timezone to use.
|
|
164
|
+
*
|
|
165
|
+
* Calendar-based operations (day, week, month, quarter, year) typically need civil time
|
|
166
|
+
* computation because these units can cross DST boundaries, changing the UTC offset.
|
|
167
|
+
*
|
|
168
|
+
* Default implementation:
|
|
169
|
+
* - Returns true for timestamps with truncation or calendar-unit offsets
|
|
170
|
+
* - Uses the query timezone from QueryInfo, or 'UTC' if none specified
|
|
171
|
+
* - Returns false for dates or sub-day offsets (hour, minute, second)
|
|
172
|
+
*
|
|
173
|
+
* Dialects can override this if they have different rules about which operations
|
|
174
|
+
* require civil time computation.
|
|
175
|
+
*
|
|
176
|
+
* @param typeDef The Malloy type of the base expression
|
|
177
|
+
* @param truncateTo The truncation unit, if any
|
|
178
|
+
* @param offsetUnit The offset unit, if any
|
|
179
|
+
* @param qi Query information including timezone settings
|
|
180
|
+
* @returns Object with `needed` boolean and optional `tz` string
|
|
181
|
+
*/
|
|
182
|
+
needsCivilTimeComputation(typeDef, truncateTo, offsetUnit, qi) {
|
|
183
|
+
// Calendar units that can cross DST boundaries
|
|
184
|
+
const calendarUnits = ['day', 'week', 'month', 'quarter', 'year'];
|
|
185
|
+
const isCalendarTruncate = truncateTo !== undefined && calendarUnits.includes(truncateTo);
|
|
186
|
+
const isCalendarOffset = offsetUnit !== undefined && calendarUnits.includes(offsetUnit);
|
|
187
|
+
const tz = qtz(qi);
|
|
188
|
+
// Timestamps with calendar truncation/offset need civil time computation
|
|
189
|
+
// BUT only if there's actually a timezone to convert to/from
|
|
190
|
+
const needed = malloy_types_1.TD.isAnyTimestamp(typeDef) &&
|
|
191
|
+
(isCalendarTruncate || isCalendarOffset) &&
|
|
192
|
+
tz !== undefined;
|
|
193
|
+
return { needed, tz };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Unified function for truncation and/or offset operations. This turns out to
|
|
197
|
+
* be a very common operation and one which can be optimized by doing it together.
|
|
198
|
+
*
|
|
199
|
+
* Much of the complexity has to do with getting timezone values set up so that
|
|
200
|
+
* they will truncate/offset in the query timezone instead of UTC.
|
|
201
|
+
*
|
|
202
|
+
* The intention is that this implementation will work for all dialects, and all
|
|
203
|
+
* the dialect peculiarities are handled with the new primitives introduced to
|
|
204
|
+
* support this function:
|
|
205
|
+
* - needsCivilTimeComputation: Determines if operation needs civil time
|
|
206
|
+
* - sqlConvertToCivilTime/sqlConvertFromCivilTime: Timezone conversion
|
|
207
|
+
* - sqlTruncate: Truncation operation
|
|
208
|
+
* - sqlOffsetTime: Interval arithmetic
|
|
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
|
+
*
|
|
218
|
+
* @param baseExpr The time expression to operate on (already compiled, with .sql populated)
|
|
219
|
+
* @param qi Query information including timezone
|
|
220
|
+
* @param truncateTo Optional truncation unit (year, month, day, etc.)
|
|
221
|
+
* @param offset Optional offset to apply (after truncation if both present)
|
|
222
|
+
*/
|
|
223
|
+
sqlTruncAndOffset(baseExpr, qi, truncateTo, offset) {
|
|
224
|
+
// Determine if we need to work in civil (local) time
|
|
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);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Simple path: no civil time conversion needed
|
|
242
|
+
let sql = baseExpr.sql;
|
|
243
|
+
if (truncateTo) {
|
|
244
|
+
sql = this.sqlTruncate(sql, truncateTo, baseExpr.typeDef, false, qtz(qi));
|
|
245
|
+
}
|
|
246
|
+
if (offset) {
|
|
247
|
+
sql = this.sqlOffsetTime(sql, offset.op, offset.magnitude, offset.unit, baseExpr.typeDef, false, qtz(qi));
|
|
248
|
+
}
|
|
249
|
+
return sql;
|
|
250
|
+
}
|
|
107
251
|
/**
|
|
108
252
|
* The dialect has a chance to over-ride how expressions are translated. If
|
|
109
253
|
* "undefined" is returned then the translation is left to the query translator.
|
|
@@ -120,10 +264,27 @@ class Dialect {
|
|
|
120
264
|
return this.sqlNowExpr();
|
|
121
265
|
case 'timeDiff':
|
|
122
266
|
return this.sqlMeasureTimeExpr(df);
|
|
123
|
-
case 'delta':
|
|
124
|
-
|
|
267
|
+
case 'delta': {
|
|
268
|
+
// Optimize: if delta's base is a trunc, combine them
|
|
269
|
+
const base = df.kids.base;
|
|
270
|
+
if (base.node === 'trunc') {
|
|
271
|
+
// Combined trunc + offset - pass the base of the truncation
|
|
272
|
+
return this.sqlTruncAndOffset(base.e, qi, base.units, {
|
|
273
|
+
op: df.op,
|
|
274
|
+
magnitude: df.kids.delta.sql,
|
|
275
|
+
unit: df.units,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// Just offset, no truncation - pass the delta's base
|
|
279
|
+
return this.sqlTruncAndOffset(base, qi, undefined, {
|
|
280
|
+
op: df.op,
|
|
281
|
+
magnitude: df.kids.delta.sql,
|
|
282
|
+
unit: df.units,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
125
285
|
case 'trunc':
|
|
126
|
-
|
|
286
|
+
// Just truncation, no offset
|
|
287
|
+
return this.sqlTruncAndOffset(df.e, qi, df.units);
|
|
127
288
|
case 'extract':
|
|
128
289
|
return this.sqlTimeExtractExpr(qi, df);
|
|
129
290
|
case 'cast':
|
|
@@ -142,8 +303,12 @@ class Dialect {
|
|
|
142
303
|
}
|
|
143
304
|
return;
|
|
144
305
|
}
|
|
145
|
-
case '
|
|
146
|
-
return this.
|
|
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);
|
|
147
312
|
case 'stringLiteral':
|
|
148
313
|
return this.sqlLiteralString(df.literal);
|
|
149
314
|
case 'numberLiteral':
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Sampling, AtomicTypeDef,
|
|
1
|
+
import type { Sampling, AtomicTypeDef, RegexMatchExpr, MeasureTimeExpr, BasicAtomicTypeDef, RecordLiteralNode, OrderBy, TimestampUnit } from '../../model/malloy_types';
|
|
2
2
|
import type { DialectFunctionOverloadDef } from '../functions';
|
|
3
3
|
import type { DialectFieldList, FieldReferenceType } from '../dialect';
|
|
4
4
|
import { PostgresBase } from '../pg_impl';
|
|
@@ -56,11 +56,11 @@ export declare class DuckDBDialect extends PostgresBase {
|
|
|
56
56
|
};
|
|
57
57
|
malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
|
|
58
58
|
parseDuckDBType(sqlType: string): AtomicTypeDef;
|
|
59
|
-
sqlTypeToMalloyType(
|
|
59
|
+
sqlTypeToMalloyType(rawSqlType: string): BasicAtomicTypeDef;
|
|
60
60
|
castToString(expression: string): string;
|
|
61
61
|
concat(...values: string[]): string;
|
|
62
62
|
validateTypeName(sqlType: string): boolean;
|
|
63
|
-
|
|
63
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
64
64
|
sqlRegexpMatch(df: RegexMatchExpr): string;
|
|
65
65
|
sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
|
|
66
66
|
sqlLiteralRecord(lit: RecordLiteralNode): 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(
|
|
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,19 +330,16 @@ 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
|
-
|
|
327
|
-
|
|
328
|
-
let
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
else if (timeframe === 'week') {
|
|
334
|
-
timeframe = 'day';
|
|
335
|
-
n = `${n}*7`;
|
|
333
|
+
sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
|
|
334
|
+
// DuckDB doesn't support INTERVAL '1' WEEK, convert to days
|
|
335
|
+
let offsetUnit = unit;
|
|
336
|
+
let offsetMag = magnitude;
|
|
337
|
+
if (unit === 'week') {
|
|
338
|
+
offsetUnit = 'day';
|
|
339
|
+
offsetMag = `(${magnitude})*7`;
|
|
336
340
|
}
|
|
337
|
-
const interval = `INTERVAL (${
|
|
338
|
-
return
|
|
341
|
+
const interval = `INTERVAL (${offsetMag}) ${offsetUnit}`;
|
|
342
|
+
return `(${expr} ${op} ${interval})`;
|
|
339
343
|
}
|
|
340
344
|
sqlRegexpMatch(df) {
|
|
341
345
|
return `REGEXP_MATCHES(${df.kids.expr.sql},${df.kids.regex.sql})`;
|
|
@@ -401,10 +405,13 @@ class DuckDBTypeParser extends tiny_parser_1.TinyParser {
|
|
|
401
405
|
baseType = { type: 'number', numberType: 'float' };
|
|
402
406
|
}
|
|
403
407
|
else if (id === 'TIMESTAMP') {
|
|
404
|
-
if (this.peek().text === 'WITH') {
|
|
408
|
+
if (this.peek().text.toUpperCase() === 'WITH') {
|
|
405
409
|
this.nextText('WITH', 'TIME', 'ZONE');
|
|
410
|
+
baseType = { type: 'timestamptz' };
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
baseType = { type: 'timestamp' };
|
|
406
414
|
}
|
|
407
|
-
baseType = { type: 'timestamp' };
|
|
408
415
|
}
|
|
409
416
|
else if (duckDBToMalloyTypes[id]) {
|
|
410
417
|
baseType = duckDBToMalloyTypes[id];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Sampling, MeasureTimeExpr,
|
|
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,13 +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
|
-
|
|
60
|
-
|
|
59
|
+
sqlConvertToCivilTime(expr: string, timezone: string, _typeDef: AtomicTypeDef): {
|
|
60
|
+
sql: string;
|
|
61
|
+
typeDef: AtomicTypeDef;
|
|
62
|
+
};
|
|
63
|
+
sqlConvertFromCivilTime(expr: string, timezone: string, _destTypeDef: TimestampTypeDef): string;
|
|
64
|
+
sqlTruncate(expr: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
65
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
61
66
|
sqlTimeExtractExpr(qi: QueryInfo, te: TimeExtractExpr): string;
|
|
62
|
-
sqlAlterTimeExpr(df: TimeDeltaExpr): string;
|
|
63
67
|
sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
|
64
68
|
sqlRegexpMatch(df: RegexMatchExpr): string;
|
|
65
|
-
|
|
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;
|
|
66
72
|
sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
|
|
67
73
|
sqlAggDistinct(_key: string, _values: string[], _func: (valNames: string[]) => string): string;
|
|
68
74
|
sqlSampleTable(tableSQL: string, sample: Sampling | undefined): string;
|