-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathDBSQLParameter.ts
More file actions
163 lines (146 loc) · 5.89 KB
/
Copy pathDBSQLParameter.ts
File metadata and controls
163 lines (146 loc) · 5.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import Int64 from 'node-int64';
import { TSparkParameter, TSparkParameterValue } from '../thrift/TCLIService_types';
export type DBSQLParameterValue = undefined | null | boolean | number | bigint | Int64 | Date | string;
export enum DBSQLParameterType {
VOID = 'VOID', // aka NULL
STRING = 'STRING',
DATE = 'DATE',
TIMESTAMP = 'TIMESTAMP',
// `TIMESTAMP_NTZ` binds a timezone-free (wall-clock) timestamp. It is a real
// Spark type, bound natively on both the Thrift and kernel backends (requires
// a server that supports TIMESTAMP_NTZ; Spark 3.4+ / recent DBR).
TIMESTAMP_NTZ = 'TIMESTAMP_NTZ',
// `TIMESTAMP_LTZ` is an alias for `TIMESTAMP`: Spark has no distinct
// TIMESTAMP_LTZ type — `TIMESTAMP` already carries local/instant (LTZ)
// semantics. `toSparkParameter` therefore binds it as `TIMESTAMP` on the wire
// (valid on both backends); it exists only as a self-documenting alias.
TIMESTAMP_LTZ = 'TIMESTAMP_LTZ',
FLOAT = 'FLOAT',
DECIMAL = 'DECIMAL',
DOUBLE = 'DOUBLE',
INTEGER = 'INTEGER',
BIGINT = 'BIGINT',
SMALLINT = 'SMALLINT',
TINYINT = 'TINYINT',
BOOLEAN = 'BOOLEAN',
INTERVALMONTH = 'INTERVAL MONTH',
INTERVALDAY = 'INTERVAL DAY',
}
// 32-bit signed integer bounds — the range of the Spark `INT` type.
const INT32_MIN = -2147483648;
const INT32_MAX = 2147483647;
/**
* Infer the Spark parameter type for a JS `number` when the caller didn't set
* one explicitly.
*
* A JS `number` is an IEEE-754 double, so a whole-number value can still be far
* outside the `INT` range (e.g. `1e30`). Typing such a value as `INTEGER`
* makes the server reject it (`invalid INT literal "1e+30"`). Pick the
* narrowest type that actually fits:
* - non-integer / non-finite → `DOUBLE`
* - integer within INT (i32) range → `INTEGER`
* - integer within the safe-integer range → `BIGINT`
* - anything larger → `DOUBLE` (can't be represented exactly as an integer
* anyway; callers needing exact 64-bit integers should pass a `bigint`).
*/
function inferNumberType(value: number): DBSQLParameterType {
if (!Number.isInteger(value)) {
return DBSQLParameterType.DOUBLE;
}
if (value >= INT32_MIN && value <= INT32_MAX) {
return DBSQLParameterType.INTEGER;
}
if (Number.isSafeInteger(value)) {
return DBSQLParameterType.BIGINT;
}
return DBSQLParameterType.DOUBLE;
}
interface DBSQLParameterOptions {
type?: DBSQLParameterType;
value: DBSQLParameterValue;
}
interface ToSparkParameterOptions {
name?: string;
}
export class DBSQLParameter {
public readonly type?: string;
public readonly value: DBSQLParameterValue;
constructor({ type, value }: DBSQLParameterOptions) {
this.type = type;
this.value = value;
}
public toSparkParameter({ name }: ToSparkParameterOptions = {}): TSparkParameter {
// If VOID type was set explicitly - ignore value
if (this.type === DBSQLParameterType.VOID) {
return new TSparkParameter({ name }); // for NULL neither `type` nor `value` should be set
}
// Infer NULL values
if (this.value === undefined || this.value === null) {
return new TSparkParameter({ name }); // for NULL neither `type` nor `value` should be set
}
// Map timezone-explicit timestamp aliases to their Spark wire type. Spark
// has no distinct TIMESTAMP_LTZ type (TIMESTAMP carries LTZ semantics), so
// bind it as TIMESTAMP — valid on both the Thrift and kernel backends.
// TIMESTAMP_NTZ is a real Spark type and is bound natively.
const wireType = this.type === DBSQLParameterType.TIMESTAMP_LTZ ? DBSQLParameterType.TIMESTAMP : this.type;
if (typeof this.value === 'boolean') {
return new TSparkParameter({
name,
type: wireType ?? DBSQLParameterType.BOOLEAN,
value: new TSparkParameterValue({
stringValue: this.value ? 'TRUE' : 'FALSE',
}),
});
}
if (typeof this.value === 'number') {
return new TSparkParameter({
name,
type: wireType ?? inferNumberType(this.value),
value: new TSparkParameterValue({
stringValue: Number(this.value).toString(),
}),
});
}
if (this.value instanceof Int64 || typeof this.value === 'bigint') {
return new TSparkParameter({
name,
type: wireType ?? DBSQLParameterType.BIGINT,
value: new TSparkParameterValue({
stringValue: this.value.toString(),
}),
});
}
if (this.value instanceof Date) {
// A `Date` bound as `DATE` must project a calendar date (`yyyy-mm-dd`),
// not a full ISO-8601 timestamp: the SEA wire rejects
// `2024-03-14T00:00:00.000Z` as a DATE literal ("trailing input"), and
// Thrift accepts the date-only form just as well. Without an explicit
// DATE type the value still binds as a TIMESTAMP from the full ISO string.
const isDateType = wireType === DBSQLParameterType.DATE;
return new TSparkParameter({
name,
type: wireType ?? DBSQLParameterType.TIMESTAMP,
value: new TSparkParameterValue({
// For DATE, project the *calendar* date using local-time accessors
// rather than `toISOString().slice(0, 10)`. `toISOString()` first
// converts to UTC, so a `new Date(2024, 2, 14)` constructed in a
// positive-offset zone (e.g. UTC+10, internal `2024-03-13T14:00Z`)
// would yield "2024-03-13" — off by one. Users reason about a DATE
// as the wall-calendar date they constructed, so extract that.
stringValue: isDateType
? `${this.value.getFullYear()}-${String(this.value.getMonth() + 1).padStart(2, '0')}-${String(
this.value.getDate(),
).padStart(2, '0')}`
: this.value.toISOString(),
}),
});
}
return new TSparkParameter({
name,
type: wireType ?? DBSQLParameterType.STRING,
value: new TSparkParameterValue({
stringValue: this.value,
}),
});
}
}