@malloydata/malloy 0.0.434 → 0.0.436
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/foundation/build_targets.d.ts +5 -6
- package/dist/api/foundation/build_targets.js +74 -6
- package/dist/api/foundation/compile.d.ts +0 -1
- package/dist/api/foundation/compile.js +0 -3
- package/dist/api/foundation/core.d.ts +16 -4
- package/dist/api/foundation/core.js +23 -23
- package/dist/api/foundation/index.d.ts +1 -1
- package/dist/api/foundation/runtime.d.ts +21 -3
- package/dist/api/foundation/runtime.js +45 -54
- package/dist/api/foundation/types.d.ts +43 -4
- package/dist/connection/dbmeta.d.ts +29 -0
- package/dist/connection/dbmeta.js +46 -0
- package/dist/connection/index.d.ts +1 -0
- package/dist/connection/index.js +1 -0
- package/dist/dialect/databricks/databricks.d.ts +11 -12
- package/dist/dialect/databricks/databricks.js +24 -36
- package/dist/dialect/dialect.d.ts +70 -171
- package/dist/dialect/dialect.js +123 -92
- package/dist/dialect/dialect_map.js +2 -0
- package/dist/dialect/duckdb/duckdb.d.ts +2 -2
- package/dist/dialect/duckdb/duckdb.js +1 -1
- package/dist/dialect/index.d.ts +3 -2
- package/dist/dialect/index.js +4 -2
- package/dist/dialect/mysql/mysql.d.ts +11 -12
- package/dist/dialect/mysql/mysql.js +20 -32
- package/dist/dialect/pg_impl.d.ts +9 -10
- package/dist/dialect/pg_impl.js +27 -73
- package/dist/dialect/postgres/postgres.d.ts +4 -4
- package/dist/dialect/postgres/postgres.js +4 -12
- package/dist/dialect/snowflake/snowflake.d.ts +11 -13
- package/dist/dialect/snowflake/snowflake.js +30 -82
- package/dist/dialect/sqlserver/dialect_functions.d.ts +2 -0
- package/dist/dialect/sqlserver/dialect_functions.js +42 -0
- package/dist/dialect/sqlserver/function_overrides.d.ts +2 -0
- package/dist/dialect/sqlserver/function_overrides.js +55 -0
- package/dist/dialect/sqlserver/index.d.ts +1 -0
- package/dist/dialect/sqlserver/index.js +22 -0
- package/dist/dialect/sqlserver/sqlserver.d.ts +101 -0
- package/dist/dialect/sqlserver/sqlserver.js +537 -0
- package/dist/dialect/sqlserver/windows_zones.d.ts +1 -0
- package/dist/dialect/sqlserver/windows_zones.js +613 -0
- package/dist/dialect/standardsql/standardsql.d.ts +11 -16
- package/dist/dialect/standardsql/standardsql.js +22 -73
- package/dist/dialect/trino/trino.d.ts +16 -18
- package/dist/dialect/trino/trino.js +127 -181
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -4
- package/dist/lang/ast/expressions/boolean-value.d.ts +24 -0
- package/dist/lang/ast/expressions/boolean-value.js +41 -0
- package/dist/lang/ast/expressions/case.js +7 -0
- package/dist/lang/ast/expressions/expr-alternation-tree.js +11 -0
- package/dist/lang/ast/expressions/expr-cast.js +5 -0
- package/dist/lang/ast/expressions/expr-coalesce.js +4 -0
- package/dist/lang/ast/expressions/expr-func.js +13 -2
- package/dist/lang/ast/expressions/expr-minus.js +5 -2
- package/dist/lang/ast/expressions/expr-null.js +4 -0
- package/dist/lang/ast/expressions/pick-when.js +7 -0
- package/dist/lang/ast/field-space/index-field-space.js +13 -1
- package/dist/lang/ast/field-space/query-spaces.js +11 -0
- package/dist/lang/ast/query-builders/reduce-builder.js +1 -1
- package/dist/lang/ast/query-items/field-declaration.d.ts +5 -0
- package/dist/lang/ast/query-items/field-declaration.js +22 -16
- package/dist/lang/ast/query-items/field-references.d.ts +6 -0
- package/dist/lang/ast/query-items/field-references.js +23 -0
- package/dist/lang/ast/query-properties/filters.js +3 -1
- package/dist/lang/ast/source-elements/named-source.js +0 -2
- package/dist/lang/ast/source-elements/sql-source.js +0 -3
- package/dist/lang/ast/source-elements/table-source.js +1 -2
- package/dist/lang/ast/statements/import-statement.js +17 -0
- package/dist/lang/ast/types/expression-def.js +17 -2
- package/dist/lang/ast/types/malloy-element.d.ts +24 -8
- package/dist/lang/ast/types/malloy-element.js +63 -44
- package/dist/lang/ast/view-elements/reference-view.js +4 -0
- package/dist/lang/ast/view-elements/refine-utils.js +1 -0
- package/dist/lang/composite-source-utils.js +10 -17
- package/dist/lang/parse-log.d.ts +12 -0
- package/dist/lang/parse-log.js +2 -0
- package/dist/lang/parse-malloy.d.ts +17 -2
- package/dist/lang/parse-malloy.js +48 -14
- package/dist/lang/parse-tree-walkers/find-external-references.d.ts +3 -3
- package/dist/lang/parse-tree-walkers/find-external-references.js +17 -16
- package/dist/lang/test/test-translator.d.ts +11 -1
- package/dist/lang/test/test-translator.js +51 -3
- package/dist/model/expression_compiler.js +9 -8
- package/dist/model/filter_compilers.js +13 -12
- package/dist/model/given_binding.d.ts +8 -0
- package/dist/model/given_binding.js +13 -0
- package/dist/model/index.d.ts +1 -1
- package/dist/model/index.js +2 -1
- package/dist/model/query_query.d.ts +3 -1
- package/dist/model/query_query.js +157 -139
- package/dist/model/source_def_utils.d.ts +7 -1
- package/dist/model/source_def_utils.js +20 -0
- package/dist/model/sql_compiled.d.ts +19 -20
- package/dist/model/sql_compiled.js +29 -17
- package/dist/test/index.js +16 -8
- package/dist/test/matchers.d.ts +1 -1
- package/dist/test/matchers.js +1 -1
- package/dist/test/test-models.js +6 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/femto-config.motly +13 -1
- package/package.json +7 -5
package/dist/dialect/pg_impl.js
CHANGED
|
@@ -39,46 +39,13 @@ class PostgresBase extends dialect_1.Dialect {
|
|
|
39
39
|
sqlNowExpr() {
|
|
40
40
|
return 'LOCALTIMESTAMP';
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
|
|
46
|
-
const tz = (0, dialect_1.qtz)(qi);
|
|
47
|
-
if (tz) {
|
|
48
|
-
extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE ${this.sqlTimezoneLiteral(tz)})`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const extracted = `EXTRACT(${units} FROM ${extractFrom})`;
|
|
52
|
-
return from.units === 'day_of_week' ? `(${extracted}+1)` : extracted;
|
|
42
|
+
sqlCivilExtract(expr, units, _civilType) {
|
|
43
|
+
const extracted = `EXTRACT(${exports.timeExtractMap[units] || units} FROM ${expr})`;
|
|
44
|
+
return units === 'day_of_week' ? `(${extracted}+1)` : extracted;
|
|
53
45
|
}
|
|
54
|
-
sqlCast(
|
|
46
|
+
sqlCast(_qi, cast) {
|
|
55
47
|
const expr = cast.e.sql || '';
|
|
56
48
|
const { srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
|
|
57
|
-
const tz = (0, dialect_1.qtz)(qi);
|
|
58
|
-
// Timezone-aware casts when query timezone is set
|
|
59
|
-
if (tz && srcTypeDef && dstTypeDef) {
|
|
60
|
-
// TIMESTAMP → DATE: convert via TIMESTAMPTZ to query timezone
|
|
61
|
-
if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
|
|
62
|
-
return `CAST((${expr}::TIMESTAMPTZ) AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS DATE)`;
|
|
63
|
-
}
|
|
64
|
-
// TIMESTAMPTZ → DATE: convert to query timezone
|
|
65
|
-
if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
|
|
66
|
-
return `CAST((${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS DATE)`;
|
|
67
|
-
}
|
|
68
|
-
// DATE → TIMESTAMP: interpret date in query timezone, return UTC timestamp
|
|
69
|
-
if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
|
|
70
|
-
return `CAST((${expr})::TIMESTAMP AT TIME ZONE ${this.sqlTimezoneLiteral(tz)} AS TIMESTAMP)`;
|
|
71
|
-
}
|
|
72
|
-
// DATE → TIMESTAMPTZ: interpret date in query timezone
|
|
73
|
-
if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
|
|
74
|
-
return `(${expr})::TIMESTAMP AT TIME ZONE ${this.sqlTimezoneLiteral(tz)}`;
|
|
75
|
-
}
|
|
76
|
-
// TIMESTAMPTZ → TIMESTAMP: convert to query timezone (returns TIMESTAMP)
|
|
77
|
-
if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
|
|
78
|
-
return `(${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(tz)}`;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// No special handling needed, or no query timezone
|
|
82
49
|
if (!malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
|
|
83
50
|
const castFunc = cast.safe ? 'TRY_CAST' : 'CAST';
|
|
84
51
|
return `${castFunc}(${expr} AS ${dstSQLType})`;
|
|
@@ -91,11 +58,7 @@ class PostgresBase extends dialect_1.Dialect {
|
|
|
91
58
|
sqlDateLiteral(_qi, literal) {
|
|
92
59
|
return `DATE '${literal}'`;
|
|
93
60
|
}
|
|
94
|
-
|
|
95
|
-
const tz = timezone || (0, dialect_1.qtz)(qi);
|
|
96
|
-
if (tz) {
|
|
97
|
-
return `TIMESTAMPTZ '${literal} ${this.checkedTimezoneName(tz)}'::TIMESTAMP`;
|
|
98
|
-
}
|
|
61
|
+
sqlCivilLiteral(literal) {
|
|
99
62
|
return `TIMESTAMP '${literal}'`;
|
|
100
63
|
}
|
|
101
64
|
sqlTimestamptzLiteral(_qi, literal, timezone) {
|
|
@@ -109,44 +72,35 @@ class PostgresBase extends dialect_1.Dialect {
|
|
|
109
72
|
return 'ARRAY[' + array.join(',') + ']';
|
|
110
73
|
}
|
|
111
74
|
sqlConvertToCivilTime(expr, timezone, typeDef) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
sql: `(${expr}) AT TIME ZONE ${this.sqlTimezoneLiteral(timezone)}`,
|
|
117
|
-
typeDef: { type: 'timestamp' },
|
|
118
|
-
};
|
|
75
|
+
const tz = this.sqlTimezoneLiteral(timezone);
|
|
76
|
+
if (malloy_types_1.TD.isTimestamptz(typeDef)) {
|
|
77
|
+
return `(${expr}) AT TIME ZONE ${tz}`;
|
|
119
78
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
sql: `(${expr})::TIMESTAMPTZ AT TIME ZONE ${this.sqlTimezoneLiteral(timezone)}`,
|
|
124
|
-
typeDef: { type: 'timestamp' },
|
|
125
|
-
};
|
|
79
|
+
return timezone === 'UTC'
|
|
80
|
+
? expr
|
|
81
|
+
: `(${expr})::TIMESTAMPTZ AT TIME ZONE ${tz}`;
|
|
126
82
|
}
|
|
127
83
|
sqlConvertFromCivilTime(expr, timezone, destTypeDef) {
|
|
128
|
-
|
|
129
|
-
|
|
84
|
+
const tz = this.sqlTimezoneLiteral(timezone);
|
|
85
|
+
if (malloy_types_1.TD.isTimestamptz(destTypeDef)) {
|
|
86
|
+
return `(${expr}) AT TIME ZONE ${tz}`;
|
|
130
87
|
}
|
|
131
|
-
return
|
|
88
|
+
return timezone === 'UTC'
|
|
89
|
+
? expr
|
|
90
|
+
: `((${expr}) AT TIME ZONE ${tz})::TIMESTAMP`;
|
|
132
91
|
}
|
|
133
|
-
sqlTruncate(expr, unit,
|
|
134
|
-
//
|
|
135
|
-
// Add 1 day before truncating, subtract 1 day after
|
|
136
|
-
let truncated;
|
|
92
|
+
sqlTruncate(expr, unit, _civilType) {
|
|
93
|
+
// Weeks start on Monday here, so truncate the following day
|
|
137
94
|
if (unit === 'week') {
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
truncated = `DATE_TRUNC('${unit}', ${expr})`;
|
|
95
|
+
return `(DATE_TRUNC('${unit}', (${expr} + INTERVAL '1' DAY)) - INTERVAL '1' DAY)`;
|
|
142
96
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
97
|
+
return `DATE_TRUNC('${unit}', ${expr})`;
|
|
98
|
+
}
|
|
99
|
+
sqlCivilToDate(expr) {
|
|
100
|
+
return `CAST(${expr} AS DATE)`;
|
|
101
|
+
}
|
|
102
|
+
sqlDateToCivil(expr) {
|
|
103
|
+
return `CAST(${expr} AS TIMESTAMP)`;
|
|
150
104
|
}
|
|
151
105
|
}
|
|
152
106
|
exports.PostgresBase = PostgresBase;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Sampling, AtomicTypeDef, TypecastExpr, MeasureTimeExpr, BasicAtomicTypeDef, RecordLiteralNode, ArrayLiteralNode,
|
|
1
|
+
import type { Sampling, AtomicTypeDef, TypecastExpr, MeasureTimeExpr, BasicAtomicTypeDef, RecordLiteralNode, ArrayLiteralNode, TimestampUnit, ExtractUnit } from '../../model/malloy_types';
|
|
2
2
|
import type { DialectFunctionOverloadDef } from '../functions';
|
|
3
|
-
import { type CompiledOrderBy, type DialectFieldList, type FieldReferenceType, type QueryInfo } from '../dialect';
|
|
3
|
+
import { type CompiledOrderBy, type DialectFieldList, type FieldReferenceType, type QueryInfo, type CivilTime } from '../dialect';
|
|
4
4
|
import { PostgresBase } from '../pg_impl';
|
|
5
5
|
export declare class PostgresDialect extends PostgresBase {
|
|
6
6
|
name: string;
|
|
@@ -46,7 +46,7 @@ export declare class PostgresDialect extends PostgresBase {
|
|
|
46
46
|
sqlFinalStage(lastStageName: string, _fields: string[]): string;
|
|
47
47
|
sqlSelectAliasAsStruct(alias: string): string;
|
|
48
48
|
sqlCreateTableAsSelect(_tableName: string, _sql: string): string;
|
|
49
|
-
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit,
|
|
49
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _civilType: CivilTime): string;
|
|
50
50
|
sqlCast(qi: QueryInfo, cast: TypecastExpr): string;
|
|
51
51
|
sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
|
|
52
52
|
sqlSumDistinct(key: string, value: string, funcName: string): string;
|
|
@@ -66,5 +66,5 @@ export declare class PostgresDialect extends PostgresBase {
|
|
|
66
66
|
validateTypeName(sqlType: string): boolean;
|
|
67
67
|
sqlLiteralRecord(lit: RecordLiteralNode): string;
|
|
68
68
|
sqlLiteralArray(lit: ArrayLiteralNode): string;
|
|
69
|
-
|
|
69
|
+
sqlCivilExtract(expr: string, units: ExtractUnit, _civilType: CivilTime): string;
|
|
70
70
|
}
|
|
@@ -200,7 +200,7 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
|
|
|
200
200
|
sqlCreateTableAsSelect(_tableName, _sql) {
|
|
201
201
|
throw new Error('Not implemented Yet');
|
|
202
202
|
}
|
|
203
|
-
sqlOffsetTime(expr, op, magnitude, unit,
|
|
203
|
+
sqlOffsetTime(expr, op, magnitude, unit, _civilType) {
|
|
204
204
|
// Convert quarter/week to supported units
|
|
205
205
|
let offsetUnit = unit;
|
|
206
206
|
let offsetMag = magnitude;
|
|
@@ -387,19 +387,11 @@ class PostgresDialect extends pg_impl_1.PostgresBase {
|
|
|
387
387
|
const array = lit.kids.values.map(val => val.sql);
|
|
388
388
|
return 'JSONB_BUILD_ARRAY(' + array.join(',') + ')';
|
|
389
389
|
}
|
|
390
|
-
|
|
391
|
-
const units = pg_impl_1.timeExtractMap[from.units] || from.units;
|
|
392
|
-
let extractFrom = from.e.sql;
|
|
393
|
-
if (malloy_types_1.TD.isAnyTimestamp(from.e.typeDef)) {
|
|
394
|
-
const tz = (0, dialect_1.qtz)(qi);
|
|
395
|
-
if (tz) {
|
|
396
|
-
extractFrom = `(${extractFrom}::TIMESTAMPTZ AT TIME ZONE ${this.sqlTimezoneLiteral(tz)})`;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
390
|
+
sqlCivilExtract(expr, units, _civilType) {
|
|
399
391
|
// PostgreSQL before 14 returns a double precision for EXTRACT, cast to integer
|
|
400
392
|
// since it is common to pass an extraction to mod ( like in fitler expressions )
|
|
401
|
-
const extracted = `EXTRACT(${units} FROM ${
|
|
402
|
-
return
|
|
393
|
+
const extracted = `EXTRACT(${pg_impl_1.timeExtractMap[units] || units} FROM ${expr})::integer`;
|
|
394
|
+
return units === 'day_of_week' ? `(${extracted}+1)` : `(${extracted})`;
|
|
403
395
|
}
|
|
404
396
|
}
|
|
405
397
|
exports.PostgresDialect = PostgresDialect;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Sampling, AtomicTypeDef,
|
|
1
|
+
import type { Sampling, AtomicTypeDef, TypecastExpr, MeasureTimeExpr, RegexMatchExpr, BasicAtomicTypeDef, ArrayLiteralNode, RecordLiteralNode, ATimestampTypeDef, TimestampUnit, ExtractUnit } from '../../model/malloy_types';
|
|
2
2
|
import type { DialectFunctionOverloadDef } from '../functions';
|
|
3
3
|
import type { CompiledOrderBy, DialectFieldList, FieldReferenceType, IntegerTypeMapping, QueryInfo } from '../dialect';
|
|
4
|
-
import { Dialect } from '../dialect';
|
|
4
|
+
import { CivilTime, Dialect } from '../dialect';
|
|
5
5
|
export declare class SnowflakeDialect extends Dialect {
|
|
6
6
|
name: string;
|
|
7
7
|
experimental: boolean;
|
|
@@ -50,19 +50,17 @@ export declare class SnowflakeDialect extends Dialect {
|
|
|
50
50
|
sqlCreateFunctionCombineLastStage(_lastStageName: string): string;
|
|
51
51
|
sqlSelectAliasAsStruct(alias: string): string;
|
|
52
52
|
sqlCreateTableAsSelect(tableName: string, sql: string): string;
|
|
53
|
-
sqlConvertToCivilTime(expr: string, timezone: string, typeDef:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
sqlTimeExtractExpr(qi: QueryInfo, from: TimeExtractExpr): string;
|
|
61
|
-
private atTz;
|
|
53
|
+
sqlConvertToCivilTime(expr: string, timezone: string, typeDef: ATimestampTypeDef): string;
|
|
54
|
+
sqlConvertFromCivilTime(expr: string, timezone: string, destTypeDef: ATimestampTypeDef): string;
|
|
55
|
+
sqlTruncate(expr: string, unit: TimestampUnit, _civilType: CivilTime): string;
|
|
56
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, civilType: CivilTime): string;
|
|
57
|
+
sqlCivilExtract(expr: string, units: ExtractUnit, _civilType: CivilTime): string;
|
|
58
|
+
sqlCivilToDate(expr: string): string;
|
|
59
|
+
sqlDateToCivil(expr: string): string;
|
|
62
60
|
sqlNowExpr(): string;
|
|
63
|
-
sqlCast(
|
|
61
|
+
sqlCast(_qi: QueryInfo, cast: TypecastExpr): string;
|
|
64
62
|
sqlDateLiteral(_qi: QueryInfo, literal: string): string;
|
|
65
|
-
|
|
63
|
+
sqlCivilLiteral(literal: string): string;
|
|
66
64
|
sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
|
|
67
65
|
sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
|
|
68
66
|
sqlRegexpMatch(compare: RegexMatchExpr): string;
|
|
@@ -249,61 +249,45 @@ ${(0, utils_1.indent)(sql)}
|
|
|
249
249
|
);
|
|
250
250
|
`;
|
|
251
251
|
}
|
|
252
|
+
// Civil time is a TIMESTAMP_NTZ, which has no zone
|
|
252
253
|
sqlConvertToCivilTime(expr, timezone, typeDef) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
sql: `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(timezone)}, ${expr})`,
|
|
258
|
-
typeDef: { type: 'timestamptz' },
|
|
259
|
-
};
|
|
254
|
+
const tz = this.sqlTimezoneLiteral(timezone);
|
|
255
|
+
if (malloy_types_1.TD.isTimestamptz(typeDef)) {
|
|
256
|
+
return `CONVERT_TIMEZONE(${tz}, ${expr})::TIMESTAMP_NTZ`;
|
|
260
257
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Snowflake session is configured with WEEK_START=7 (Sunday)
|
|
275
|
-
// so DATE_TRUNC already truncates to Sunday - no adjustment needed
|
|
276
|
-
// Unlike PostgreSQL/DuckDB, Snowflake's DATE_TRUNC preserves the input type
|
|
277
|
-
// (TIMESTAMP_NTZ → TIMESTAMP_NTZ, TIMESTAMP_TZ → TIMESTAMP_TZ)
|
|
258
|
+
return timezone === 'UTC'
|
|
259
|
+
? expr
|
|
260
|
+
: `CONVERT_TIMEZONE('UTC', ${tz}, (${expr})::TIMESTAMP_NTZ)`;
|
|
261
|
+
}
|
|
262
|
+
sqlConvertFromCivilTime(expr, timezone, destTypeDef) {
|
|
263
|
+
const utc = timezone === 'UTC'
|
|
264
|
+
? expr
|
|
265
|
+
: `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(timezone)}, 'UTC', (${expr})::TIMESTAMP_NTZ)`;
|
|
266
|
+
// The session time zone is UTC
|
|
267
|
+
return malloy_types_1.TD.isTimestamptz(destTypeDef) ? `(${utc})::TIMESTAMP_TZ` : utc;
|
|
268
|
+
}
|
|
269
|
+
sqlTruncate(expr, unit, _civilType) {
|
|
270
|
+
// The session sets WEEK_START to Sunday
|
|
278
271
|
return `DATE_TRUNC('${unit}', ${expr})`;
|
|
279
272
|
}
|
|
280
|
-
sqlOffsetTime(expr, op, magnitude, unit,
|
|
281
|
-
const funcName =
|
|
273
|
+
sqlOffsetTime(expr, op, magnitude, unit, civilType) {
|
|
274
|
+
const funcName = civilType === dialect_1.CivilTime.Date ? 'DATEADD' : 'TIMESTAMPADD';
|
|
282
275
|
const n = op === '+' ? magnitude : `-(${magnitude})`;
|
|
283
276
|
return `${funcName}(${unit}, ${n}, ${expr})`;
|
|
284
277
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return `
|
|
293
|
-
}
|
|
294
|
-
atTz(sqlExpr, tz) {
|
|
295
|
-
if (tz !== undefined) {
|
|
296
|
-
return `(
|
|
297
|
-
TO_CHAR(${sqlExpr}::TIMESTAMP_NTZ, 'YYYY-MM-DD HH24:MI:SS.FF9') ||
|
|
298
|
-
TO_CHAR(CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, '1970-01-01 00:00:00'), 'TZHTZM')
|
|
299
|
-
)::TIMESTAMP_TZ`;
|
|
300
|
-
}
|
|
301
|
-
return `${sqlExpr}::TIMESTAMP_NTZ`;
|
|
278
|
+
sqlCivilExtract(expr, units, _civilType) {
|
|
279
|
+
return `EXTRACT(${extractionMap[units] || units} FROM ${expr})`;
|
|
280
|
+
}
|
|
281
|
+
sqlCivilToDate(expr) {
|
|
282
|
+
return `TO_DATE(${expr})`;
|
|
283
|
+
}
|
|
284
|
+
sqlDateToCivil(expr) {
|
|
285
|
+
return `(${expr})::TIMESTAMP_NTZ`;
|
|
302
286
|
}
|
|
303
287
|
sqlNowExpr() {
|
|
304
288
|
return 'CURRENT_TIMESTAMP()';
|
|
305
289
|
}
|
|
306
|
-
sqlCast(
|
|
290
|
+
sqlCast(_qi, cast) {
|
|
307
291
|
const src = cast.e.sql || '';
|
|
308
292
|
const { srcTypeDef, dstTypeDef, dstSQLType } = this.sqlCastPrep(cast);
|
|
309
293
|
if (malloy_types_1.TD.eq(srcTypeDef, dstTypeDef)) {
|
|
@@ -316,50 +300,14 @@ ${(0, utils_1.indent)(sql)}
|
|
|
316
300
|
throw new Error(`Snowflake dialect doesn't support safe cast for a few types:
|
|
317
301
|
refer to: https://docs.snowflake.com/en/sql-reference/functions/try_cast`);
|
|
318
302
|
}
|
|
319
|
-
const tz = (0, dialect_1.qtz)(qi);
|
|
320
|
-
// Timezone-aware casts when query timezone is set
|
|
321
|
-
if (tz && srcTypeDef && dstTypeDef) {
|
|
322
|
-
// TIMESTAMP → DATE: convert to query timezone, then to date
|
|
323
|
-
if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
|
|
324
|
-
return `TO_DATE(CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, ${src}))`;
|
|
325
|
-
}
|
|
326
|
-
// TIMESTAMPTZ → DATE: convert to query timezone, then to date
|
|
327
|
-
if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isDate(dstTypeDef)) {
|
|
328
|
-
return `TO_DATE(CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, ${src}))`;
|
|
329
|
-
}
|
|
330
|
-
// DATE → TIMESTAMP: interpret date in query timezone, return UTC timestamp
|
|
331
|
-
if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
|
|
332
|
-
const retExpr = `TO_TIMESTAMP(${src})`;
|
|
333
|
-
return this.atTz(retExpr, tz);
|
|
334
|
-
}
|
|
335
|
-
// DATE → TIMESTAMPTZ: interpret date in query timezone
|
|
336
|
-
if (malloy_types_1.TD.isDate(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
|
|
337
|
-
const retExpr = `TO_TIMESTAMP(${src})`;
|
|
338
|
-
return this.atTz(retExpr, tz);
|
|
339
|
-
}
|
|
340
|
-
// TIMESTAMPTZ → TIMESTAMP: convert to query timezone, get UTC wall clock
|
|
341
|
-
if (malloy_types_1.TD.isTimestamptz(srcTypeDef) && malloy_types_1.TD.isTimestamp(dstTypeDef)) {
|
|
342
|
-
return `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, ${src})::TIMESTAMP_NTZ`;
|
|
343
|
-
}
|
|
344
|
-
// TIMESTAMP → TIMESTAMPTZ: interpret as UTC, convert to TIMESTAMPTZ
|
|
345
|
-
if (malloy_types_1.TD.isTimestamp(srcTypeDef) && malloy_types_1.TD.isTimestamptz(dstTypeDef)) {
|
|
346
|
-
return this.atTz(src, tz);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
303
|
const castFunc = cast.safe ? 'TRY_CAST' : 'CAST';
|
|
350
304
|
return `${castFunc}(${src} AS ${dstSQLType})`;
|
|
351
305
|
}
|
|
352
306
|
sqlDateLiteral(_qi, literal) {
|
|
353
307
|
return `TO_DATE('${literal}')`;
|
|
354
308
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
let ret = `'${literal}'::TIMESTAMP_NTZ`;
|
|
358
|
-
if (tz) {
|
|
359
|
-
// Interpret the literal as being in query timezone, convert to UTC
|
|
360
|
-
ret = `CONVERT_TIMEZONE(${this.sqlTimezoneLiteral(tz)}, 'UTC', ${ret})`;
|
|
361
|
-
}
|
|
362
|
-
return ret;
|
|
309
|
+
sqlCivilLiteral(literal) {
|
|
310
|
+
return `'${literal}'::TIMESTAMP_NTZ`;
|
|
363
311
|
}
|
|
364
312
|
sqlTimestamptzLiteral(_qi, literal, timezone) {
|
|
365
313
|
// Use TIMESTAMP_TZ_FROM_PARTS to create timestamptz
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SQLSERVER_DIALECT_FUNCTIONS = void 0;
|
|
8
|
+
const util_1 = require("../functions/util");
|
|
9
|
+
const order_by = {
|
|
10
|
+
node: 'aggregate_order_by',
|
|
11
|
+
prefix: ' WITHIN GROUP (',
|
|
12
|
+
suffix: ')',
|
|
13
|
+
};
|
|
14
|
+
// STRING_AGG's result is limited to 8000 bytes unless its input is a MAX type.
|
|
15
|
+
const string_agg = {
|
|
16
|
+
default_separator: {
|
|
17
|
+
takes: { 'value': { dimension: 'string' } },
|
|
18
|
+
returns: { measure: 'string' },
|
|
19
|
+
supportsOrderBy: true,
|
|
20
|
+
impl: {
|
|
21
|
+
expr: (0, util_1.sql) `STRING_AGG(CAST(${(0, util_1.arg)('value')} AS NVARCHAR(MAX)), ',')${order_by}`,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
with_separator: {
|
|
25
|
+
takes: {
|
|
26
|
+
'value': { dimension: 'string' },
|
|
27
|
+
'separator': { literal: 'string' },
|
|
28
|
+
},
|
|
29
|
+
returns: { measure: 'string' },
|
|
30
|
+
supportsOrderBy: true,
|
|
31
|
+
impl: {
|
|
32
|
+
expr: (0, util_1.sql) `STRING_AGG(CAST(${(0, util_1.arg)('value')} AS NVARCHAR(MAX)), ${(0, util_1.arg)('separator')})${order_by}`,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// No string_agg_distinct: STRING_AGG accepts no DISTINCT, so the translator
|
|
37
|
+
// reports the function as unknown for this dialect.
|
|
38
|
+
exports.SQLSERVER_DIALECT_FUNCTIONS = {
|
|
39
|
+
string_agg,
|
|
40
|
+
...(0, util_1.def)('reverse', { 'str': 'string' }, 'string'),
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=dialect_functions.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SQLSERVER_MALLOY_STANDARD_OVERLOADS = void 0;
|
|
8
|
+
// LEN ignores trailing spaces: append one character, count, subtract one
|
|
9
|
+
const len = (arg) => `(LEN(${arg} + N'x') - 1)`;
|
|
10
|
+
exports.SQLSERVER_MALLOY_STANDARD_OVERLOADS = {
|
|
11
|
+
ceil: { function: 'CEILING' },
|
|
12
|
+
chr: { sql: "CASE WHEN ${value} = 0 THEN '' ELSE NCHAR(${value}) END" },
|
|
13
|
+
div: { sql: 'FLOOR(${dividend} / ${divisor})' },
|
|
14
|
+
// starts_with and ends_with are written as comparisons because SQL Server
|
|
15
|
+
// has no boolean values: legal as a condition (where:, having:), a server
|
|
16
|
+
// error as a value. A NULL input matches nothing, where the standard's
|
|
17
|
+
// COALESCE(..., false) returns false.
|
|
18
|
+
ends_with: {
|
|
19
|
+
sql: `RIGHT(\${value}, ${len('${suffix}')}) = \${suffix}`,
|
|
20
|
+
},
|
|
21
|
+
ifnull: { function: 'ISNULL' },
|
|
22
|
+
// A FLOAT holds neither infinity nor NaN, so neither condition is ever true
|
|
23
|
+
is_inf: { sql: '(1=0)' },
|
|
24
|
+
is_nan: { sql: '(1=0)' },
|
|
25
|
+
length: { sql: len('${value}') },
|
|
26
|
+
ln: { sql: 'LOG(${value})' },
|
|
27
|
+
log: { sql: 'LOG(${value}, ${base})' },
|
|
28
|
+
// POWER answers in the type of its base, so an integer base truncates
|
|
29
|
+
pow: { sql: 'POWER(CAST(${base} AS FLOAT(53)), ${exponent})' },
|
|
30
|
+
round: {
|
|
31
|
+
to_integer: { sql: 'ROUND(${value}, 0)' },
|
|
32
|
+
to_precision: { sql: 'ROUND(${value}, ${precision})' },
|
|
33
|
+
},
|
|
34
|
+
atan2: { function: 'ATN2' },
|
|
35
|
+
trim: { characters: { sql: 'TRIM(${trim_characters} FROM ${value})' } },
|
|
36
|
+
starts_with: {
|
|
37
|
+
sql: `LEFT(\${value}, ${len('${prefix}')}) = \${prefix}`,
|
|
38
|
+
},
|
|
39
|
+
stddev: { function: 'STDEV' },
|
|
40
|
+
string_repeat: { function: 'REPLICATE' },
|
|
41
|
+
strpos: { sql: 'CHARINDEX(${search_string}, ${test_string})' },
|
|
42
|
+
substr: {
|
|
43
|
+
position_only: {
|
|
44
|
+
sql: `SUBSTRING(\${value}, CASE WHEN \${position} < 0 THEN ${len('${value}')} + \${position} + 1 ELSE \${position} END, ${len('${value}')})`,
|
|
45
|
+
},
|
|
46
|
+
with_length: {
|
|
47
|
+
sql: `SUBSTRING(\${value}, CASE WHEN \${position} < 0 THEN ${len('${value}')} + \${position} + 1 ELSE \${position} END, \${length})`,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
trunc: {
|
|
51
|
+
to_integer: { sql: 'ROUND(${value}, 0, 1)' },
|
|
52
|
+
to_precision: { sql: 'ROUND(${value}, ${precision}, 1)' },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=function_overrides.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sqlserver';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
__exportStar(require("./sqlserver"), exports);
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Sampling, AtomicTypeDef, ATimestampTypeDef, TypecastExpr, MeasureTimeExpr, BasicAtomicTypeDef, RegexMatchExpr, TimestampUnit, OrderBy, ExtractUnit } from '../../model/malloy_types';
|
|
2
|
+
import type { DialectFunctionOverloadDef } from '../functions';
|
|
3
|
+
import type { BooleanTypeSupport, DialectFieldList, FieldReferenceType, GroupByClauseType, QueryInfo } from '../dialect';
|
|
4
|
+
import { CivilTime, Dialect } from '../dialect';
|
|
5
|
+
import type { ValidateTablePathResult } from '../table-path';
|
|
6
|
+
/** T-SQL for SQL Server 2017 and later, and Azure SQL Database. */
|
|
7
|
+
export declare class SQLServerDialect extends Dialect {
|
|
8
|
+
name: string;
|
|
9
|
+
experimental: boolean;
|
|
10
|
+
stringLiteralStyle: "doubled";
|
|
11
|
+
identifierEscapeStyle: "doubled";
|
|
12
|
+
identifierQuoteChar: string;
|
|
13
|
+
defaultNumberType: string;
|
|
14
|
+
defaultDecimalType: string;
|
|
15
|
+
udfPrefix: string;
|
|
16
|
+
hasFinalStage: boolean;
|
|
17
|
+
divisionIsInteger: boolean;
|
|
18
|
+
supportsSumDistinctFunction: boolean;
|
|
19
|
+
unnestWithNumbers: boolean;
|
|
20
|
+
defaultSampling: {
|
|
21
|
+
rows: number;
|
|
22
|
+
};
|
|
23
|
+
supportUnnestArrayAgg: boolean;
|
|
24
|
+
supportsAggDistinct: boolean;
|
|
25
|
+
supportsCTEinCoorelatedSubQueries: boolean;
|
|
26
|
+
dontUnionIndex: boolean;
|
|
27
|
+
supportsQualify: boolean;
|
|
28
|
+
supportsSafeCast: boolean;
|
|
29
|
+
supportsNesting: boolean;
|
|
30
|
+
supportsPipelinesInViews: boolean;
|
|
31
|
+
supportsFullJoin: boolean;
|
|
32
|
+
readsNestedData: boolean;
|
|
33
|
+
supportsComplexFilteredSources: boolean;
|
|
34
|
+
supportsArraysInData: boolean;
|
|
35
|
+
compoundObjectInSchema: boolean;
|
|
36
|
+
supportsSelectReplace: boolean;
|
|
37
|
+
supportsTempTables: boolean;
|
|
38
|
+
booleanType: BooleanTypeSupport;
|
|
39
|
+
hasTimestamptz: boolean;
|
|
40
|
+
supportsBigIntPrecision: boolean;
|
|
41
|
+
maxIdentifierLength: number;
|
|
42
|
+
likeEscape: boolean;
|
|
43
|
+
likeExtraWildcards: string[];
|
|
44
|
+
groupByClause: GroupByClauseType;
|
|
45
|
+
tablePathBareIdentRegex: RegExp;
|
|
46
|
+
/**
|
|
47
|
+
* `[schema].[table]` is read as the ANSI-quoted path it stands for, and the
|
|
48
|
+
* canonical form is the ANSI spelling the base validates.
|
|
49
|
+
*/
|
|
50
|
+
sqlValidateTableName(input: string): ValidateTablePathResult;
|
|
51
|
+
malloyTypeToSQLType(malloyType: AtomicTypeDef): string;
|
|
52
|
+
sqlTypeToMalloyType(sqlType: string): BasicAtomicTypeDef;
|
|
53
|
+
validateTypeName(sqlType: string): boolean;
|
|
54
|
+
sqlGroupSetTable(groupSetCount: number): string;
|
|
55
|
+
sqlAnyValue(_groupSet: number, fieldName: string): string;
|
|
56
|
+
sqlAggregateTurtle(): string;
|
|
57
|
+
sqlAnyValueTurtle(): string;
|
|
58
|
+
sqlAnyValueLastTurtle(): string;
|
|
59
|
+
sqlCoaleseMeasuresInline(): string;
|
|
60
|
+
sqlUnnestAlias(): string;
|
|
61
|
+
sqlSelectAliasAsStruct(): string;
|
|
62
|
+
sqlLiteralRecord(): string;
|
|
63
|
+
sqlLiteralArray(): string;
|
|
64
|
+
sqlFieldReference(parentAlias: string, _parentType: FieldReferenceType, childName: string, _childType: string): string;
|
|
65
|
+
private unsupported;
|
|
66
|
+
sqlUnnestPipelineHead(_isSingleton: boolean, _sourceSQLExpression: string, _fieldList?: DialectFieldList): string;
|
|
67
|
+
sqlCreateFunction(_id: string, _funcText: string): string;
|
|
68
|
+
sqlCreateFunctionCombineLastStage(_lastStageName: string, _fieldList: DialectFieldList, _orderBy: OrderBy[] | undefined): string;
|
|
69
|
+
sqlCreateTableAsSelect(tableName: string, sql: string): string;
|
|
70
|
+
sqlGenerateUUID(): string;
|
|
71
|
+
sqlLiteralString(literal: string): string;
|
|
72
|
+
sqlSumDistinctHashedKey(sqlDistinctKey: string): string;
|
|
73
|
+
sqlSumDistinct(key: string, value: string, funcName: string): string;
|
|
74
|
+
sqlAggDistinct(_key: string, _values: string[], _func: (valNames: string[]) => string): string;
|
|
75
|
+
sqlOrderByLimit(orderTerms: string[], limit: number | undefined): string;
|
|
76
|
+
sqlSampleTable(tableSQL: string, sample: Sampling | undefined, onBaseTable?: boolean): string;
|
|
77
|
+
castToString(expression: string): string;
|
|
78
|
+
concat(...values: string[]): string;
|
|
79
|
+
sqlDateToString(sqlDateExp: string): string;
|
|
80
|
+
sqlNowExpr(): string;
|
|
81
|
+
sqlCivilExtract(expr: string, units: ExtractUnit, _civilType: CivilTime): string;
|
|
82
|
+
sqlCivilToDate(expr: string): string;
|
|
83
|
+
sqlDateToCivil(expr: string): string;
|
|
84
|
+
sqlCast(_qi: QueryInfo, cast: TypecastExpr): string;
|
|
85
|
+
sqlRegexpMatch(_df: RegexMatchExpr): string;
|
|
86
|
+
sqlDateLiteral(_qi: QueryInfo, literal: string): string;
|
|
87
|
+
sqlCivilLiteral(literal: string): string;
|
|
88
|
+
sqlTimestamptzLiteral(_qi: QueryInfo, literal: string, timezone: string): string;
|
|
89
|
+
sqlTimezoneLiteral(timezone: string): string;
|
|
90
|
+
sqlConvertToCivilTime(expr: string, timezone: string, typeDef: ATimestampTypeDef): string;
|
|
91
|
+
sqlConvertFromCivilTime(expr: string, timezone: string, destTypeDef: ATimestampTypeDef): string;
|
|
92
|
+
sqlTruncate(expr: string, unit: TimestampUnit, civilType: CivilTime): string;
|
|
93
|
+
sqlOffsetTime(expr: string, op: '+' | '-', magnitude: string, unit: TimestampUnit, _civilType: CivilTime): string;
|
|
94
|
+
sqlMeasureTimeExpr(df: MeasureTimeExpr): string;
|
|
95
|
+
getDialectFunctionOverrides(): {
|
|
96
|
+
[name: string]: DialectFunctionOverloadDef[];
|
|
97
|
+
};
|
|
98
|
+
getDialectFunctions(): {
|
|
99
|
+
[name: string]: DialectFunctionOverloadDef[];
|
|
100
|
+
};
|
|
101
|
+
}
|