@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.
- package/CONTEXT.md +57 -0
- package/dist/api/core.js +2 -0
- package/dist/api/util.js +16 -24
- package/dist/dialect/dialect.d.ts +113 -23
- package/dist/dialect/dialect.js +83 -13
- package/dist/dialect/duckdb/duckdb.d.ts +1 -4
- package/dist/dialect/duckdb/duckdb.js +13 -17
- package/dist/dialect/mysql/mysql.d.ts +9 -4
- package/dist/dialect/mysql/mysql.js +18 -11
- package/dist/dialect/pg_impl.d.ts +11 -2
- package/dist/dialect/pg_impl.js +79 -15
- package/dist/dialect/postgres/postgres.d.ts +1 -4
- package/dist/dialect/postgres/postgres.js +6 -18
- package/dist/dialect/snowflake/snowflake.d.ts +10 -4
- package/dist/dialect/snowflake/snowflake.js +80 -31
- package/dist/dialect/standardsql/standardsql.d.ts +9 -4
- package/dist/dialect/standardsql/standardsql.js +21 -19
- package/dist/dialect/tiny_parser.js +1 -1
- package/dist/dialect/trino/trino.d.ts +19 -6
- package/dist/dialect/trino/trino.js +163 -31
- package/dist/index.d.ts +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/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/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/field_instance.js +1 -1
- package/dist/model/filter_compilers.d.ts +2 -1
- package/dist/model/filter_compilers.js +8 -5
- package/dist/model/malloy_types.d.ts +31 -9
- 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/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 '
|
|
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, TimeExtractExpr, TypecastExpr, RegexMatchExpr,
|
|
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
|
|
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
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
|
109
|
-
* @param timezone The target timezone
|
|
110
|
-
* @
|
|
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):
|
|
166
|
+
abstract sqlConvertToCivilTime(expr: string, timezone: string, typeDef: AtomicTypeDef): {
|
|
167
|
+
sql: string;
|
|
168
|
+
typeDef: AtomicTypeDef;
|
|
169
|
+
};
|
|
113
170
|
/**
|
|
114
|
-
* Converts
|
|
115
|
-
* The inverse of sqlConvertToCivilTime.
|
|
171
|
+
* Converts from civil time back to a Malloy timestamp type.
|
|
116
172
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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
|
|
177
|
+
* @param expr The SQL expression in civil time
|
|
124
178
|
* @param timezone The timezone of the civil time
|
|
125
|
-
* @
|
|
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
|
-
|
|
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;
|
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
|
}
|
|
@@ -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.
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expr =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 '
|
|
241
|
-
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);
|
|
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(
|
|
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(
|
|
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,
|
|
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):
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const tz =
|
|
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 `
|
|
391
|
+
return `CONVERT_TZ('${literal}', '${tz}', 'UTC')`;
|
|
388
392
|
}
|
|
389
|
-
return `TIMESTAMP '${
|
|
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,
|
|
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
|
-
|
|
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
|
}
|