@malloydata/malloy 0.0.322 → 0.0.323
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/dialect/dialect.d.ts +109 -3
- package/dist/dialect/dialect.js +98 -3
- package/dist/dialect/duckdb/duckdb.d.ts +5 -2
- package/dist/dialect/duckdb/duckdb.js +22 -11
- package/dist/dialect/mysql/mysql.d.ts +5 -4
- package/dist/dialect/mysql/mysql.js +31 -47
- package/dist/dialect/pg_impl.d.ts +1 -2
- package/dist/dialect/pg_impl.js +0 -30
- package/dist/dialect/postgres/postgres.d.ts +5 -2
- package/dist/dialect/postgres/postgres.js +28 -11
- package/dist/dialect/snowflake/snowflake.d.ts +5 -3
- package/dist/dialect/snowflake/snowflake.js +23 -22
- package/dist/dialect/standardsql/standardsql.d.ts +9 -3
- package/dist/dialect/standardsql/standardsql.js +42 -33
- package/dist/dialect/trino/trino.d.ts +7 -3
- package/dist/dialect/trino/trino.js +41 -43
- package/dist/lang/ast/error-factory.js +1 -1
- package/dist/lang/ast/field-space/static-space.js +5 -1
- package/dist/lang/ast/types/malloy-element.js +10 -2
- package/dist/model/expression_compiler.js +172 -145
- package/dist/model/filter_compilers.d.ts +0 -2
- package/dist/model/filter_compilers.js +132 -57
- package/dist/model/malloy_types.d.ts +19 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr,
|
|
1
|
+
import type { Expr, Sampling, AtomicTypeDef, MeasureTimeExpr, TimeExtractExpr, TypecastExpr, RegexMatchExpr, TimeLiteralNode, RecordLiteralNode, ArrayLiteralNode, BasicAtomicTypeDef, OrderBy, TimestampUnit, TimeExpr } from '../model/malloy_types';
|
|
2
2
|
import type { DialectFunctionOverloadDef } from './functions';
|
|
3
3
|
interface DialectField {
|
|
4
4
|
typeDef: AtomicTypeDef;
|
|
@@ -91,12 +91,118 @@ export declare abstract class Dialect {
|
|
|
91
91
|
sqlLiteralNumber(literal: string): string;
|
|
92
92
|
ignoreInProject(_fieldName: string): boolean;
|
|
93
93
|
abstract sqlNowExpr(): string;
|
|
94
|
-
abstract sqlTruncExpr(qi: QueryInfo, toTrunc: TimeTruncExpr): string;
|
|
95
94
|
abstract sqlTimeExtractExpr(qi: QueryInfo, xFrom: TimeExtractExpr): string;
|
|
96
95
|
abstract sqlMeasureTimeExpr(e: MeasureTimeExpr): string;
|
|
97
|
-
abstract sqlAlterTimeExpr(df: TimeDeltaExpr): string;
|
|
98
96
|
abstract sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
|
99
97
|
abstract sqlRegexpMatch(df: RegexMatchExpr): string;
|
|
98
|
+
/**
|
|
99
|
+
* Converts a UTC timestamp expression to civil (local) time in the specified timezone.
|
|
100
|
+
* Used when performing timezone-aware calendar arithmetic.
|
|
101
|
+
*
|
|
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'`
|
|
107
|
+
*
|
|
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
|
|
111
|
+
*/
|
|
112
|
+
abstract sqlConvertToCivilTime(expr: string, timezone: string): string;
|
|
113
|
+
/**
|
|
114
|
+
* Converts a civil (local) time expression back to a UTC timestamp.
|
|
115
|
+
* The inverse of sqlConvertToCivilTime.
|
|
116
|
+
*
|
|
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)`
|
|
122
|
+
*
|
|
123
|
+
* @param expr The SQL expression for the civil time
|
|
124
|
+
* @param timezone The timezone of the civil time
|
|
125
|
+
* @returns SQL expression representing the UTC timestamp
|
|
126
|
+
*/
|
|
127
|
+
abstract sqlConvertFromCivilTime(expr: string, timezone: string): string;
|
|
128
|
+
/**
|
|
129
|
+
* Truncates a time expression to the specified unit.
|
|
130
|
+
*
|
|
131
|
+
* @param expr The SQL expression to truncate
|
|
132
|
+
* @param unit The unit to truncate to (year, month, day, hour, etc.)
|
|
133
|
+
* @param typeDef The Malloy type of the expression (date, timestamp, etc.)
|
|
134
|
+
* @param inCivilTime If true, the expression is already in civil (local) time and should not
|
|
135
|
+
* be converted. If false, may need timezone conversion for timestamps.
|
|
136
|
+
* @param timezone Optional timezone for the operation. Only provided when timezone-aware
|
|
137
|
+
* truncation is needed but inCivilTime is false.
|
|
138
|
+
* @returns SQL expression representing the truncated time
|
|
139
|
+
*/
|
|
140
|
+
abstract sqlTruncate(expr: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, timezone?: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* Adds or subtracts a time interval from a time expression.
|
|
143
|
+
*
|
|
144
|
+
* @param expr The SQL expression to offset
|
|
145
|
+
* @param op The operation: '+' for addition, '-' for subtraction
|
|
146
|
+
* @param magnitude The SQL expression for the interval magnitude (e.g., '6', '(delta_val)')
|
|
147
|
+
* @param unit The interval unit (year, month, day, hour, etc.)
|
|
148
|
+
* @param typeDef The Malloy type of the expression (date, timestamp, etc.)
|
|
149
|
+
* @param inCivilTime If true, the expression is already in civil (local) time and should not
|
|
150
|
+
* be converted. If false, may need timezone conversion for timestamps.
|
|
151
|
+
* @param timezone Optional timezone for the operation. Only provided when timezone-aware
|
|
152
|
+
* offset is needed but inCivilTime is false.
|
|
153
|
+
* @returns SQL expression representing the offset time
|
|
154
|
+
*/
|
|
155
|
+
abstract sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, typeDef: AtomicTypeDef, inCivilTime: boolean, timezone?: string): string;
|
|
156
|
+
/**
|
|
157
|
+
* Determines whether a truncation and/or offset operation needs to be performed in
|
|
158
|
+
* civil (local) time, and if so, which timezone to use.
|
|
159
|
+
*
|
|
160
|
+
* Calendar-based operations (day, week, month, quarter, year) typically need civil time
|
|
161
|
+
* computation because these units can cross DST boundaries, changing the UTC offset.
|
|
162
|
+
*
|
|
163
|
+
* Default implementation:
|
|
164
|
+
* - Returns true for timestamps with truncation or calendar-unit offsets
|
|
165
|
+
* - Uses the query timezone from QueryInfo, or 'UTC' if none specified
|
|
166
|
+
* - Returns false for dates or sub-day offsets (hour, minute, second)
|
|
167
|
+
*
|
|
168
|
+
* Dialects can override this if they have different rules about which operations
|
|
169
|
+
* require civil time computation.
|
|
170
|
+
*
|
|
171
|
+
* @param typeDef The Malloy type of the base expression
|
|
172
|
+
* @param truncateTo The truncation unit, if any
|
|
173
|
+
* @param offsetUnit The offset unit, if any
|
|
174
|
+
* @param qi Query information including timezone settings
|
|
175
|
+
* @returns Object with `needed` boolean and optional `tz` string
|
|
176
|
+
*/
|
|
177
|
+
needsCivilTimeComputation(typeDef: AtomicTypeDef, truncateTo: TimestampUnit | undefined, offsetUnit: TimestampUnit | undefined, qi: QueryInfo): {
|
|
178
|
+
needed: boolean;
|
|
179
|
+
tz: string | undefined;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Unified function for truncation and/or offset operations. This turns out to
|
|
183
|
+
* be a very common operation and one which can be optimized by doing it together.
|
|
184
|
+
*
|
|
185
|
+
* Much of the complexity has to do with getting timezone values set up so that
|
|
186
|
+
* they will truncate/offset in the query timezone instead of UTC.
|
|
187
|
+
*
|
|
188
|
+
* The intention is that this implementation will work for all dialects, and all
|
|
189
|
+
* the dialect peculiarities are handled with the new primitives introduced to
|
|
190
|
+
* support this function:
|
|
191
|
+
* - needsCivilTimeComputation: Determines if operation needs civil time
|
|
192
|
+
* - sqlConvertToCivilTime/sqlConvertFromCivilTime: Timezone conversion
|
|
193
|
+
* - sqlTruncate: Truncation operation
|
|
194
|
+
* - sqlOffsetTime: Interval arithmetic
|
|
195
|
+
*
|
|
196
|
+
* @param baseExpr The time expression to operate on (already compiled, with .sql populated)
|
|
197
|
+
* @param qi Query information including timezone
|
|
198
|
+
* @param truncateTo Optional truncation unit (year, month, day, etc.)
|
|
199
|
+
* @param offset Optional offset to apply (after truncation if both present)
|
|
200
|
+
*/
|
|
201
|
+
sqlTruncAndOffset(baseExpr: TimeExpr, qi: QueryInfo, truncateTo?: TimestampUnit, offset?: {
|
|
202
|
+
op: '+' | '-';
|
|
203
|
+
magnitude: string;
|
|
204
|
+
unit: TimestampUnit;
|
|
205
|
+
}): string;
|
|
100
206
|
abstract sqlLiteralTime(qi: QueryInfo, df: TimeLiteralNode): string;
|
|
101
207
|
abstract sqlLiteralString(literal: string): string;
|
|
102
208
|
abstract sqlLiteralRegexp(literal: string): string;
|
package/dist/dialect/dialect.js
CHANGED
|
@@ -104,6 +104,84 @@ class Dialect {
|
|
|
104
104
|
ignoreInProject(_fieldName) {
|
|
105
105
|
return false;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Determines whether a truncation and/or offset operation needs to be performed in
|
|
109
|
+
* civil (local) time, and if so, which timezone to use.
|
|
110
|
+
*
|
|
111
|
+
* Calendar-based operations (day, week, month, quarter, year) typically need civil time
|
|
112
|
+
* computation because these units can cross DST boundaries, changing the UTC offset.
|
|
113
|
+
*
|
|
114
|
+
* Default implementation:
|
|
115
|
+
* - Returns true for timestamps with truncation or calendar-unit offsets
|
|
116
|
+
* - Uses the query timezone from QueryInfo, or 'UTC' if none specified
|
|
117
|
+
* - Returns false for dates or sub-day offsets (hour, minute, second)
|
|
118
|
+
*
|
|
119
|
+
* Dialects can override this if they have different rules about which operations
|
|
120
|
+
* require civil time computation.
|
|
121
|
+
*
|
|
122
|
+
* @param typeDef The Malloy type of the base expression
|
|
123
|
+
* @param truncateTo The truncation unit, if any
|
|
124
|
+
* @param offsetUnit The offset unit, if any
|
|
125
|
+
* @param qi Query information including timezone settings
|
|
126
|
+
* @returns Object with `needed` boolean and optional `tz` string
|
|
127
|
+
*/
|
|
128
|
+
needsCivilTimeComputation(typeDef, truncateTo, offsetUnit, qi) {
|
|
129
|
+
// Calendar units that can cross DST boundaries
|
|
130
|
+
const calendarUnits = ['day', 'week', 'month', 'quarter', 'year'];
|
|
131
|
+
const isCalendarTruncate = truncateTo !== undefined && calendarUnits.includes(truncateTo);
|
|
132
|
+
const isCalendarOffset = offsetUnit !== undefined && calendarUnits.includes(offsetUnit);
|
|
133
|
+
const tz = qtz(qi);
|
|
134
|
+
// Timestamps with calendar truncation/offset need civil time computation
|
|
135
|
+
// BUT only if there's actually a timezone to convert to/from
|
|
136
|
+
const needed = malloy_types_1.TD.isTimestamp(typeDef) &&
|
|
137
|
+
(isCalendarTruncate || isCalendarOffset) &&
|
|
138
|
+
tz !== undefined;
|
|
139
|
+
return { needed, tz };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Unified function for truncation and/or offset operations. This turns out to
|
|
143
|
+
* be a very common operation and one which can be optimized by doing it together.
|
|
144
|
+
*
|
|
145
|
+
* Much of the complexity has to do with getting timezone values set up so that
|
|
146
|
+
* they will truncate/offset in the query timezone instead of UTC.
|
|
147
|
+
*
|
|
148
|
+
* The intention is that this implementation will work for all dialects, and all
|
|
149
|
+
* the dialect peculiarities are handled with the new primitives introduced to
|
|
150
|
+
* support this function:
|
|
151
|
+
* - needsCivilTimeComputation: Determines if operation needs civil time
|
|
152
|
+
* - sqlConvertToCivilTime/sqlConvertFromCivilTime: Timezone conversion
|
|
153
|
+
* - sqlTruncate: Truncation operation
|
|
154
|
+
* - sqlOffsetTime: Interval arithmetic
|
|
155
|
+
*
|
|
156
|
+
* @param baseExpr The time expression to operate on (already compiled, with .sql populated)
|
|
157
|
+
* @param qi Query information including timezone
|
|
158
|
+
* @param truncateTo Optional truncation unit (year, month, day, etc.)
|
|
159
|
+
* @param offset Optional offset to apply (after truncation if both present)
|
|
160
|
+
*/
|
|
161
|
+
sqlTruncAndOffset(baseExpr, qi, truncateTo, offset) {
|
|
162
|
+
// 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);
|
|
172
|
+
}
|
|
173
|
+
return this.sqlConvertFromCivilTime(expr, tz);
|
|
174
|
+
}
|
|
175
|
+
// Simple path: no civil time conversion needed
|
|
176
|
+
let sql = baseExpr.sql;
|
|
177
|
+
if (truncateTo) {
|
|
178
|
+
sql = this.sqlTruncate(sql, truncateTo, baseExpr.typeDef, false, qtz(qi));
|
|
179
|
+
}
|
|
180
|
+
if (offset) {
|
|
181
|
+
sql = this.sqlOffsetTime(sql, offset.op, offset.magnitude, offset.unit, baseExpr.typeDef, false, qtz(qi));
|
|
182
|
+
}
|
|
183
|
+
return sql;
|
|
184
|
+
}
|
|
107
185
|
/**
|
|
108
186
|
* The dialect has a chance to over-ride how expressions are translated. If
|
|
109
187
|
* "undefined" is returned then the translation is left to the query translator.
|
|
@@ -120,10 +198,27 @@ class Dialect {
|
|
|
120
198
|
return this.sqlNowExpr();
|
|
121
199
|
case 'timeDiff':
|
|
122
200
|
return this.sqlMeasureTimeExpr(df);
|
|
123
|
-
case 'delta':
|
|
124
|
-
|
|
201
|
+
case 'delta': {
|
|
202
|
+
// Optimize: if delta's base is a trunc, combine them
|
|
203
|
+
const base = df.kids.base;
|
|
204
|
+
if (base.node === 'trunc') {
|
|
205
|
+
// Combined trunc + offset - pass the base of the truncation
|
|
206
|
+
return this.sqlTruncAndOffset(base.e, qi, base.units, {
|
|
207
|
+
op: df.op,
|
|
208
|
+
magnitude: df.kids.delta.sql,
|
|
209
|
+
unit: df.units,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// Just offset, no truncation - pass the delta's base
|
|
213
|
+
return this.sqlTruncAndOffset(base, qi, undefined, {
|
|
214
|
+
op: df.op,
|
|
215
|
+
magnitude: df.kids.delta.sql,
|
|
216
|
+
unit: df.units,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
125
219
|
case 'trunc':
|
|
126
|
-
|
|
220
|
+
// Just truncation, no offset
|
|
221
|
+
return this.sqlTruncAndOffset(df.e, qi, df.units);
|
|
127
222
|
case 'extract':
|
|
128
223
|
return this.sqlTimeExtractExpr(qi, df);
|
|
129
224
|
case 'cast':
|
|
@@ -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';
|
|
@@ -60,7 +60,10 @@ export declare class DuckDBDialect extends PostgresBase {
|
|
|
60
60
|
castToString(expression: string): string;
|
|
61
61
|
concat(...values: string[]): string;
|
|
62
62
|
validateTypeName(sqlType: string): boolean;
|
|
63
|
-
|
|
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
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
64
67
|
sqlRegexpMatch(df: RegexMatchExpr): string;
|
|
65
68
|
sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
|
|
66
69
|
sqlLiteralRecord(lit: RecordLiteralNode): string;
|
|
@@ -323,19 +323,30 @@ class DuckDBDialect extends pg_impl_1.PostgresBase {
|
|
|
323
323
|
// Brackets: INT[ ]
|
|
324
324
|
return sqlType.match(/^[A-Za-z\s(),[\]0-9]*$/) !== null;
|
|
325
325
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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)`;
|
|
332
337
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
return `DATE_TRUNC('${unit}', ${expr})`;
|
|
339
|
+
}
|
|
340
|
+
sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
|
|
341
|
+
// DuckDB doesn't support INTERVAL '1' WEEK, convert to days
|
|
342
|
+
let offsetUnit = unit;
|
|
343
|
+
let offsetMag = magnitude;
|
|
344
|
+
if (unit === 'week') {
|
|
345
|
+
offsetUnit = 'day';
|
|
346
|
+
offsetMag = `(${magnitude})*7`;
|
|
336
347
|
}
|
|
337
|
-
const interval = `INTERVAL (${
|
|
338
|
-
return
|
|
348
|
+
const interval = `INTERVAL (${offsetMag}) ${offsetUnit}`;
|
|
349
|
+
return `(${expr} ${op} ${interval})`;
|
|
339
350
|
}
|
|
340
351
|
sqlRegexpMatch(df) {
|
|
341
352
|
return `REGEXP_MATCHES(${df.kids.expr.sql},${df.kids.regex.sql})`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Sampling, MeasureTimeExpr, TimeLiteralNode, RegexMatchExpr,
|
|
1
|
+
import type { Sampling, MeasureTimeExpr, TimeLiteralNode, RegexMatchExpr, TimeExtractExpr, TypecastExpr, BasicAtomicTypeDef, AtomicTypeDef, 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,10 +56,11 @@ 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): string;
|
|
60
|
+
sqlConvertFromCivilTime(expr: string, timezone: string): string;
|
|
61
|
+
sqlTruncate(expr: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
62
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
61
63
|
sqlTimeExtractExpr(qi: QueryInfo, te: TimeExtractExpr): string;
|
|
62
|
-
sqlAlterTimeExpr(df: TimeDeltaExpr): string;
|
|
63
64
|
sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
|
64
65
|
sqlRegexpMatch(df: RegexMatchExpr): string;
|
|
65
66
|
sqlLiteralTime(qi: QueryInfo, lt: TimeLiteralNode): string;
|
|
@@ -291,38 +291,21 @@ class MySQLDialect extends dialect_1.Dialect {
|
|
|
291
291
|
sqlNowExpr() {
|
|
292
292
|
return 'LOCALTIMESTAMP';
|
|
293
293
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
: civilSource;
|
|
308
|
-
// Truncate to the appropriate unit in civil time
|
|
309
|
-
const civilTrunc = `${this.truncToUnit(adjustedSource, trunc.units)}`;
|
|
310
|
-
// Convert the truncated civil time back to UTC
|
|
311
|
-
const truncTsTz = `CONVERT_TZ(${civilTrunc}, '${tz}', 'UTC')`;
|
|
312
|
-
return `(${truncTsTz})`; // TODO: should it cast?
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
// For dates (civil time) or timestamps without query timezone
|
|
316
|
-
// do the week adjustment before truncating
|
|
317
|
-
const adjustedThis = week
|
|
318
|
-
? `DATE_SUB(${truncThis}, INTERVAL DAYOFWEEK(${truncThis}) - 1 DAY)`
|
|
319
|
-
: truncThis;
|
|
320
|
-
const result = `${this.truncToUnit(adjustedThis, trunc.units)}`;
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
truncToUnit(expr, units) {
|
|
294
|
+
sqlConvertToCivilTime(expr, timezone) {
|
|
295
|
+
return `CONVERT_TZ(${expr}, 'UTC', '${timezone}')`;
|
|
296
|
+
}
|
|
297
|
+
sqlConvertFromCivilTime(expr, timezone) {
|
|
298
|
+
return `CONVERT_TZ(${expr}, '${timezone}', 'UTC')`;
|
|
299
|
+
}
|
|
300
|
+
sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
|
|
301
|
+
// For week truncation, adjust to Sunday first
|
|
302
|
+
// DAYOFWEEK returns 1=Sunday, 2=Monday, etc., so subtract (DAYOFWEEK-1) days
|
|
303
|
+
const adjustedExpr = unit === 'week'
|
|
304
|
+
? `DATE_SUB(${expr}, INTERVAL DAYOFWEEK(${expr}) - 1 DAY)`
|
|
305
|
+
: expr;
|
|
306
|
+
// Generate truncation using DATE_FORMAT
|
|
324
307
|
let format = "'%Y-%m-%d %H:%i:%s'";
|
|
325
|
-
switch (
|
|
308
|
+
switch (unit) {
|
|
326
309
|
case 'minute':
|
|
327
310
|
format = "'%Y-%m-%d %H:%i:00'";
|
|
328
311
|
break;
|
|
@@ -337,13 +320,28 @@ class MySQLDialect extends dialect_1.Dialect {
|
|
|
337
320
|
format = "'%Y-%m-01 00:00:00'";
|
|
338
321
|
break;
|
|
339
322
|
case 'quarter':
|
|
340
|
-
format = `CASE WHEN MONTH(${
|
|
323
|
+
format = `CASE WHEN MONTH(${adjustedExpr}) > 9 THEN '%Y-10-01 00:00:00' WHEN MONTH(${adjustedExpr}) > 6 THEN '%Y-07-01 00:00:00' WHEN MONTH(${adjustedExpr}) > 3 THEN '%Y-04-01 00:00:00' ELSE '%Y-01-01 00:00:00' end`;
|
|
341
324
|
break;
|
|
342
325
|
case 'year':
|
|
343
326
|
format = "'%Y-01-01 00:00:00'";
|
|
344
327
|
break;
|
|
345
328
|
}
|
|
346
|
-
return `TIMESTAMP(DATE_FORMAT(${
|
|
329
|
+
return `TIMESTAMP(DATE_FORMAT(${adjustedExpr}, ${format}))`;
|
|
330
|
+
}
|
|
331
|
+
sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
|
|
332
|
+
// Convert quarter/week to supported units
|
|
333
|
+
let offsetUnit = unit;
|
|
334
|
+
let offsetMag = magnitude;
|
|
335
|
+
if (unit === 'quarter') {
|
|
336
|
+
offsetUnit = 'month';
|
|
337
|
+
offsetMag = `${magnitude}*3`;
|
|
338
|
+
}
|
|
339
|
+
else if (unit === 'week') {
|
|
340
|
+
offsetUnit = 'day';
|
|
341
|
+
offsetMag = `${magnitude}*7`;
|
|
342
|
+
}
|
|
343
|
+
const interval = `INTERVAL ${offsetMag} ${offsetUnit}`;
|
|
344
|
+
return `(${expr} ${op} ${interval})`;
|
|
347
345
|
}
|
|
348
346
|
sqlTimeExtractExpr(qi, te) {
|
|
349
347
|
const msUnits = msExtractionMap[te.units] || te.units;
|
|
@@ -356,20 +354,6 @@ class MySQLDialect extends dialect_1.Dialect {
|
|
|
356
354
|
}
|
|
357
355
|
return `${msUnits}(${extractFrom})`;
|
|
358
356
|
}
|
|
359
|
-
sqlAlterTimeExpr(df) {
|
|
360
|
-
let timeframe = df.units;
|
|
361
|
-
let n = df.kids.delta.sql;
|
|
362
|
-
if (timeframe === 'quarter') {
|
|
363
|
-
timeframe = 'month';
|
|
364
|
-
n = `${n}*3`;
|
|
365
|
-
}
|
|
366
|
-
else if (timeframe === 'week') {
|
|
367
|
-
timeframe = 'day';
|
|
368
|
-
n = `${n}*7`;
|
|
369
|
-
}
|
|
370
|
-
const interval = `INTERVAL ${n} ${timeframe} `;
|
|
371
|
-
return `(${df.kids.base.sql})${df.op}${interval}`;
|
|
372
|
-
}
|
|
373
357
|
sqlCast(qi, cast) {
|
|
374
358
|
const srcSQL = cast.e.sql || 'internal-error-in-sql-generation';
|
|
375
359
|
const { op, srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ArrayLiteralNode, RecordLiteralNode, RegexMatchExpr, TimeExtractExpr, TimeLiteralNode,
|
|
1
|
+
import type { ArrayLiteralNode, RecordLiteralNode, RegexMatchExpr, TimeExtractExpr, TimeLiteralNode, 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,7 +7,6 @@ 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
|
-
sqlTruncExpr(qi: QueryInfo, df: TimeTruncExpr): string;
|
|
11
10
|
sqlNowExpr(): string;
|
|
12
11
|
sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
|
|
13
12
|
sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
package/dist/dialect/pg_impl.js
CHANGED
|
@@ -18,36 +18,6 @@ exports.timeExtractMap = {
|
|
|
18
18
|
* same implementations for the much of the SQL code generation
|
|
19
19
|
*/
|
|
20
20
|
class PostgresBase extends dialect_1.Dialect {
|
|
21
|
-
sqlTruncExpr(qi, df) {
|
|
22
|
-
// adjusting for monday/sunday weeks
|
|
23
|
-
const week = df.units === 'week';
|
|
24
|
-
const truncThis = week ? `${df.e.sql} + INTERVAL '1' DAY` : df.e.sql;
|
|
25
|
-
// Only do timezone conversion for timestamps, not dates
|
|
26
|
-
if (malloy_types_1.TD.isTimestamp(df.e.typeDef)) {
|
|
27
|
-
const tz = (0, dialect_1.qtz)(qi);
|
|
28
|
-
if (tz) {
|
|
29
|
-
// get a civil version of the time in the query time zone
|
|
30
|
-
const civilSource = `((${truncThis})::TIMESTAMPTZ AT TIME ZONE '${tz}')`;
|
|
31
|
-
// do truncation in that time space
|
|
32
|
-
let civilTrunc = `DATE_TRUNC('${df.units}', ${civilSource})`;
|
|
33
|
-
if (week) {
|
|
34
|
-
civilTrunc = `(${civilTrunc} - INTERVAL '1' DAY)`;
|
|
35
|
-
}
|
|
36
|
-
// make a tstz from the civil time ... "AT TIME ZONE" of
|
|
37
|
-
// a TIMESTAMP will produce a TIMESTAMPTZ in that zone
|
|
38
|
-
// where the civi appeareance is the same as the TIMESTAMP
|
|
39
|
-
const truncTsTz = `${civilTrunc} AT TIME ZONE '${tz}'`;
|
|
40
|
-
// Now just make a system TIMESTAMP from that
|
|
41
|
-
return `(${truncTsTz})::TIMESTAMP`;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// For dates (civil time) or timestamps without query timezone
|
|
45
|
-
let result = `DATE_TRUNC('${df.units}', ${truncThis})`;
|
|
46
|
-
if (week) {
|
|
47
|
-
result = `(${result} - INTERVAL '1' DAY)`;
|
|
48
|
-
}
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
21
|
sqlNowExpr() {
|
|
52
22
|
return 'LOCALTIMESTAMP';
|
|
53
23
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Sampling, AtomicTypeDef,
|
|
1
|
+
import type { Sampling, AtomicTypeDef, TypecastExpr, MeasureTimeExpr, BasicAtomicTypeDef, RecordLiteralNode, ArrayLiteralNode, TimeExtractExpr, TimestampUnit } from '../../model/malloy_types';
|
|
2
2
|
import type { DialectFunctionOverloadDef } from '../functions';
|
|
3
3
|
import { type DialectFieldList, type FieldReferenceType, type QueryInfo } from '../dialect';
|
|
4
4
|
import { PostgresBase } from '../pg_impl';
|
|
@@ -44,7 +44,10 @@ export declare class PostgresDialect extends PostgresBase {
|
|
|
44
44
|
sqlFinalStage(lastStageName: string, _fields: string[]): string;
|
|
45
45
|
sqlSelectAliasAsStruct(alias: string): string;
|
|
46
46
|
sqlCreateTableAsSelect(_tableName: string, _sql: string): string;
|
|
47
|
-
|
|
47
|
+
sqlConvertToCivilTime(expr: string, timezone: string): string;
|
|
48
|
+
sqlConvertFromCivilTime(expr: string, timezone: string): string;
|
|
49
|
+
sqlTruncate(expr: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
50
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
48
51
|
sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
|
49
52
|
sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
|
|
50
53
|
sqlSumDistinct(key: string, value: string, funcName: string): string;
|
|
@@ -209,19 +209,36 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
|
|
|
209
209
|
sqlCreateTableAsSelect(_tableName, _sql) {
|
|
210
210
|
throw new Error('Not implemented Yet');
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
sqlConvertToCivilTime(expr, timezone) {
|
|
213
|
+
return `(${expr})::TIMESTAMPTZ AT TIME ZONE '${timezone}'`;
|
|
214
|
+
}
|
|
215
|
+
sqlConvertFromCivilTime(expr, timezone) {
|
|
216
|
+
return `((${expr}) AT TIME ZONE '${timezone}')::TIMESTAMP`;
|
|
217
|
+
}
|
|
218
|
+
sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
|
|
219
|
+
// PostgreSQL starts weeks on Monday, Malloy wants Sunday
|
|
220
|
+
// Add 1 day before truncating, subtract 1 day after
|
|
221
|
+
if (unit === 'week') {
|
|
222
|
+
return `(DATE_TRUNC('${unit}', (${expr} + INTERVAL '1' DAY)) - INTERVAL '1' DAY)`;
|
|
223
|
+
}
|
|
224
|
+
return `DATE_TRUNC('${unit}', ${expr})`;
|
|
225
|
+
}
|
|
226
|
+
sqlOffsetTime(expr, op, magnitude, unit, _typeDef, _inCivilTime, _timezone) {
|
|
227
|
+
// Convert quarter/week to supported units
|
|
228
|
+
let offsetUnit = unit;
|
|
229
|
+
let offsetMag = magnitude;
|
|
230
|
+
if (unit === 'quarter') {
|
|
231
|
+
offsetUnit = 'month';
|
|
232
|
+
offsetMag = `(${magnitude})*3`;
|
|
218
233
|
}
|
|
219
|
-
else if (
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
else if (unit === 'week') {
|
|
235
|
+
offsetUnit = 'day';
|
|
236
|
+
offsetMag = `(${magnitude})*7`;
|
|
222
237
|
}
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
// Map to make_interval parameter name
|
|
239
|
+
const intervalParam = pgMakeIntervalMap[offsetUnit];
|
|
240
|
+
const interval = `make_interval(${intervalParam}=>(${offsetMag})::integer)`;
|
|
241
|
+
return `(${expr} ${op} ${interval})`;
|
|
225
242
|
}
|
|
226
243
|
sqlCast(qi, cast) {
|
|
227
244
|
if (cast.safe) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Sampling, AtomicTypeDef,
|
|
1
|
+
import type { Sampling, AtomicTypeDef, TimeExtractExpr, TypecastExpr, TimeLiteralNode, MeasureTimeExpr, RegexMatchExpr, BasicAtomicTypeDef, ArrayLiteralNode, RecordLiteralNode } from '../../model/malloy_types';
|
|
2
2
|
import type { DialectFunctionOverloadDef } from '../functions';
|
|
3
3
|
import type { DialectFieldList, FieldReferenceType, QueryInfo } from '../dialect';
|
|
4
4
|
import { Dialect } from '../dialect';
|
|
@@ -45,9 +45,11 @@ export declare class SnowflakeDialect extends Dialect {
|
|
|
45
45
|
sqlSelectAliasAsStruct(alias: string): string;
|
|
46
46
|
sqlMaybeQuoteIdentifier(identifier: string): string;
|
|
47
47
|
sqlCreateTableAsSelect(tableName: string, sql: string): string;
|
|
48
|
-
|
|
48
|
+
sqlConvertToCivilTime(expr: string, timezone: string): string;
|
|
49
|
+
sqlConvertFromCivilTime(expr: string, timezone: string): string;
|
|
50
|
+
sqlTruncate(expr: string, unit: string, _typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
51
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: string, typeDef: AtomicTypeDef, _inCivilTime: boolean, _timezone?: string): string;
|
|
49
52
|
sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
|
|
50
|
-
sqlAlterTimeExpr(df: TimeDeltaExpr): string;
|
|
51
53
|
private atTz;
|
|
52
54
|
sqlNowExpr(): string;
|
|
53
55
|
sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
|
@@ -249,13 +249,24 @@ ${(0, utils_1.indent)(sql)}
|
|
|
249
249
|
);
|
|
250
250
|
`;
|
|
251
251
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
sqlConvertToCivilTime(expr, timezone) {
|
|
253
|
+
// 3-arg form: explicitly convert from UTC to specified timezone
|
|
254
|
+
return `CONVERT_TIMEZONE('UTC', '${timezone}', ${expr})`;
|
|
255
|
+
}
|
|
256
|
+
sqlConvertFromCivilTime(expr, timezone) {
|
|
257
|
+
// After civil time operations, we have a TIMESTAMP_NTZ in the target timezone
|
|
258
|
+
// Convert from timezone to UTC, returning TIMESTAMP_NTZ
|
|
259
|
+
return `CONVERT_TIMEZONE('${timezone}', 'UTC', (${expr})::TIMESTAMP_NTZ)`;
|
|
260
|
+
}
|
|
261
|
+
sqlTruncate(expr, unit, _typeDef, _inCivilTime, _timezone) {
|
|
262
|
+
// Snowflake session is configured with WEEK_START=7 (Sunday)
|
|
263
|
+
// so DATE_TRUNC already truncates to Sunday - no adjustment needed
|
|
264
|
+
return `DATE_TRUNC('${unit}', ${expr})`;
|
|
265
|
+
}
|
|
266
|
+
sqlOffsetTime(expr, op, magnitude, unit, typeDef, _inCivilTime, _timezone) {
|
|
267
|
+
const funcName = typeDef.type === 'date' ? 'DATEADD' : 'TIMESTAMPADD';
|
|
268
|
+
const n = op === '+' ? magnitude : `-(${magnitude})`;
|
|
269
|
+
return `${funcName}(${unit}, ${n}, ${expr})`;
|
|
259
270
|
}
|
|
260
271
|
sqlTimeExtractExpr(qi, from) {
|
|
261
272
|
const extractUnits = extractionMap[from.units] || from.units;
|
|
@@ -266,12 +277,6 @@ ${(0, utils_1.indent)(sql)}
|
|
|
266
277
|
}
|
|
267
278
|
return `EXTRACT(${extractUnits} FROM ${extractFrom})`;
|
|
268
279
|
}
|
|
269
|
-
sqlAlterTimeExpr(df) {
|
|
270
|
-
var _a;
|
|
271
|
-
const add = ((_a = df.typeDef) === null || _a === void 0 ? void 0 : _a.type) === 'date' ? 'DATEADD' : 'TIMESTAMPADD';
|
|
272
|
-
const n = df.op === '+' ? df.kids.delta.sql : `-(${df.kids.delta.sql})`;
|
|
273
|
-
return `${add}(${df.units},${n},${df.kids.base.sql})`;
|
|
274
|
-
}
|
|
275
280
|
atTz(sqlExpr, tz) {
|
|
276
281
|
if (tz !== undefined) {
|
|
277
282
|
return `(
|
|
@@ -315,19 +320,15 @@ ${(0, utils_1.indent)(sql)}
|
|
|
315
320
|
}
|
|
316
321
|
sqlLiteralTime(qi, lf) {
|
|
317
322
|
var _a;
|
|
323
|
+
if (malloy_types_1.TD.isDate(lf.typeDef)) {
|
|
324
|
+
return `TO_DATE('${lf.literal}')`;
|
|
325
|
+
}
|
|
318
326
|
const tz = (0, dialect_1.qtz)(qi);
|
|
319
|
-
// just making it explicit that timestring does not have timezone info
|
|
320
327
|
let ret = `'${lf.literal}'::TIMESTAMP_NTZ`;
|
|
321
|
-
// now do the hack to add timezone to a timestamp ntz
|
|
322
328
|
const targetTimeZone = (_a = lf.timezone) !== null && _a !== void 0 ? _a : tz;
|
|
323
329
|
if (targetTimeZone) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
ret = `${retTimeString} || ${targetTimeZoneSuffix}`;
|
|
327
|
-
ret = `(${ret})::TIMESTAMP_TZ`;
|
|
328
|
-
}
|
|
329
|
-
if (malloy_types_1.TD.isDate(lf.typeDef)) {
|
|
330
|
-
return `TO_DATE(${ret})`;
|
|
330
|
+
// Interpret the literal as being in targetTimeZone, convert to UTC
|
|
331
|
+
ret = `CONVERT_TIMEZONE('${targetTimeZone}', 'UTC', ${ret})`;
|
|
331
332
|
}
|
|
332
333
|
return ret;
|
|
333
334
|
}
|