@malloydata/db-trino 0.0.337 → 0.0.339

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/index.js CHANGED
@@ -41,6 +41,7 @@ function configToTrinoConfig(config) {
41
41
  schema: typeof config['schema'] === 'string' ? config['schema'] : undefined,
42
42
  user: typeof config['user'] === 'string' ? config['user'] : undefined,
43
43
  password: typeof config['password'] === 'string' ? config['password'] : undefined,
44
+ setupSQL: typeof config['setupSQL'] === 'string' ? config['setupSQL'] : undefined,
44
45
  },
45
46
  };
46
47
  }
@@ -51,6 +52,13 @@ const trinoProperties = [
51
52
  { name: 'schema', displayName: 'Schema', type: 'string', optional: true },
52
53
  { name: 'user', displayName: 'User', type: 'string', optional: true },
53
54
  { name: 'password', displayName: 'Password', type: 'password', optional: true },
55
+ {
56
+ name: 'setupSQL',
57
+ displayName: 'Setup SQL',
58
+ type: 'text',
59
+ optional: true,
60
+ description: 'SQL statements to run when the connection is established',
61
+ },
54
62
  ];
55
63
  (0, malloy_1.registerConnectionType)('trino', {
56
64
  factory: (config) => {
@@ -0,0 +1,14 @@
1
+ import type { FieldDef, QueryRecord } from '@malloydata/malloy';
2
+ /**
3
+ * Converts a raw compound value (record or array) from its wire
4
+ * format into a JavaScript array.
5
+ * - Trino: identity cast (data is already an array)
6
+ * - Presto: JSON.parse (data comes as a JSON string)
7
+ */
8
+ export type Unpacker = (data: unknown) => unknown[];
9
+ /**
10
+ * Convert a raw database result row (positional array) into a
11
+ * QueryRecord (named object). Each position in the raw row
12
+ * corresponds to a column defined in the schema.
13
+ */
14
+ export declare function resultRowToQueryRecord(columns: FieldDef[], rawRow: unknown[], unpack: Unpacker): QueryRecord;
@@ -0,0 +1,123 @@
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.resultRowToQueryRecord = resultRowToQueryRecord;
8
+ const malloy_1 = require("@malloydata/malloy");
9
+ const luxon_1 = require("luxon");
10
+ /**
11
+ * Convert a raw database result row (positional array) into a
12
+ * QueryRecord (named object). Each position in the raw row
13
+ * corresponds to a column defined in the schema.
14
+ */
15
+ function resultRowToQueryRecord(columns, rawRow, unpack) {
16
+ const result = {};
17
+ for (let i = 0; i < columns.length; i++) {
18
+ result[columns[i].name] = convertValue(columns[i], rawRow[i], unpack);
19
+ }
20
+ return result;
21
+ }
22
+ /**
23
+ * Convert a single raw value to its Malloy representation,
24
+ * dispatching to the appropriate converter based on type.
25
+ * Non-atomic fields (joins, turtles) are passed through as-is.
26
+ */
27
+ function convertValue(typeDef, raw, unpack) {
28
+ if (!(0, malloy_1.isAtomic)(typeDef)) {
29
+ return [];
30
+ }
31
+ if (typeDef.type === 'record') {
32
+ return convertRecord(typeDef.fields, raw, unpack);
33
+ }
34
+ if ((0, malloy_1.isRepeatedRecord)(typeDef)) {
35
+ return convertRepeatedRecord(typeDef.fields, raw, unpack);
36
+ }
37
+ if ((0, malloy_1.isBasicArray)(typeDef)) {
38
+ return convertArray(typeDef.elementTypeDef, raw, unpack);
39
+ }
40
+ return convertScalar(typeDef, raw);
41
+ }
42
+ /**
43
+ * Convert a record (ROW/STRUCT) value: the database returns these
44
+ * as positional arrays, which we map to named fields. A null
45
+ * record stays null.
46
+ */
47
+ function convertRecord(fields, raw, unpack) {
48
+ if (raw === null || raw === undefined)
49
+ return null;
50
+ const values = unpack(raw);
51
+ const result = {};
52
+ for (let i = 0; i < fields.length; i++) {
53
+ result[fields[i].name] = convertValue(fields[i], values[i], unpack);
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * Convert an array of records. This handles both nested Malloy
59
+ * query results (nest:) and ARRAY<ROW(...)> column values. A null
60
+ * input becomes [] — for nested queries this correctly represents
61
+ * "no matching rows."
62
+ */
63
+ function convertRepeatedRecord(fields, raw, unpack) {
64
+ if (raw === null || raw === undefined)
65
+ return [];
66
+ const rows = unpack(raw);
67
+ return rows.map(row => convertRecord(fields, row, unpack));
68
+ }
69
+ /**
70
+ * Convert a basic array (number[], string[], etc.). A null array
71
+ * stays null. Elements are recursively converted through
72
+ * convertValue for proper type coercion.
73
+ */
74
+ function convertArray(elementType, raw, unpack) {
75
+ if (raw === null || raw === undefined)
76
+ return null;
77
+ const values = unpack(raw);
78
+ return values.map(el => convertValue(elementType, el, unpack));
79
+ }
80
+ /**
81
+ * Convert a scalar value, handling Trino/Presto wire format quirks:
82
+ * - Decimal numbers arrive as strings
83
+ * - Timestamps arrive as strings
84
+ * - TIMESTAMP WITH TIME ZONE uses "datetime timezone" format
85
+ * - Dates arrive as strings (e.g. "2020-01-15"), passed through
86
+ * for rowDataToDate to handle downstream
87
+ * - BIGINT precision is limited by the Trino JS client which
88
+ * delivers values as JS number (no bigint support)
89
+ */
90
+ function convertScalar(typeDef, raw) {
91
+ if (raw === null || raw === undefined)
92
+ return null;
93
+ if (typeDef.type === 'number' && typeof raw === 'string') {
94
+ // Decimals arrive as strings. BIGINT values arrive as JS number
95
+ // (already lossy for values > MAX_SAFE_INTEGER) so they don't
96
+ // hit this path — this is only for decimal-as-string coercion.
97
+ return Number(raw);
98
+ }
99
+ if ((typeDef.type === 'timestamp' || typeDef.type === 'timestamptz') &&
100
+ typeof raw === 'string') {
101
+ if (typeDef.type === 'timestamptz') {
102
+ // TIMESTAMP WITH TIME ZONE: "2020-02-20 00:00:00 America/Mexico_City"
103
+ const trinoTzPattern = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?) (.+)$/;
104
+ const match = raw.match(trinoTzPattern);
105
+ if (match) {
106
+ const [, dateTimePart, tzName] = match;
107
+ const dt = luxon_1.DateTime.fromSQL(dateTimePart, { zone: tzName });
108
+ if (dt.isValid) {
109
+ return dt.toJSDate();
110
+ }
111
+ }
112
+ }
113
+ // Plain timestamps: Trino returns UTC values
114
+ return new Date(raw + 'Z');
115
+ }
116
+ if (typeof raw === 'string' ||
117
+ typeof raw === 'number' ||
118
+ typeof raw === 'boolean') {
119
+ return raw;
120
+ }
121
+ return null;
122
+ }
123
+ //# sourceMappingURL=result-to-querydata.js.map
@@ -18,6 +18,7 @@ export interface TrinoConnectionConfiguration {
18
18
  schema?: string;
19
19
  user?: string;
20
20
  password?: string;
21
+ setupSQL?: string;
21
22
  extraConfig?: Partial<Omit<ConnectionOptions, keyof TrinoConnectionConfiguration>>;
22
23
  }
23
24
  export type TrinoConnectionOptions = ConnectionConfig;
@@ -39,7 +40,12 @@ export declare abstract class TrinoPrestoConnection extends BaseConnection imple
39
40
  private queryOptions?;
40
41
  protected readonly dialect: TrinoDialect;
41
42
  static DEFAULT_QUERY_OPTIONS: RunSQLOptions;
42
- constructor(name: string, client: BaseRunner, queryOptions?: QueryOptionsReader | undefined);
43
+ protected setupSQL: string | undefined;
44
+ private setupDone;
45
+ protected connectionConfig: TrinoConnectionConfiguration;
46
+ constructor(name: string, client: BaseRunner, queryOptions?: QueryOptionsReader | undefined, setupSQL?: string, connectionConfig?: TrinoConnectionConfiguration);
47
+ private ensureSetup;
48
+ private runSetupStatements;
43
49
  get dialectName(): string;
44
50
  private readQueryOptions;
45
51
  canPersist(): this is PersistSQLResults;
@@ -47,10 +53,7 @@ export declare abstract class TrinoPrestoConnection extends BaseConnection imple
47
53
  get supportsNesting(): boolean;
48
54
  manifestTemporaryTable(_sqlCommand: string): Promise<string>;
49
55
  unpackArray(_fields: FieldDef[], data: unknown): unknown[];
50
- convertRow(fields: FieldDef[], rawRow: unknown): {};
51
- convertNest(fields: FieldDef[], _data: unknown): unknown[];
52
56
  runSQL(sqlCommand: string, options?: RunSQLOptions, _rowIndex?: number): Promise<MalloyQueryData>;
53
- private resultRow;
54
57
  runSQLBlockAndFetchResultSchema(_sqlBlock: SQLSourceDef, _options?: RunSQLOptions): Promise<{
55
58
  data: MalloyQueryData;
56
59
  schema: StructDef;
@@ -28,7 +28,7 @@ const connection_1 = require("@malloydata/malloy/connection");
28
28
  const presto_js_client_1 = require("@prestodb/presto-js-client");
29
29
  const crypto_1 = require("crypto");
30
30
  const trino_client_1 = require("trino-client");
31
- const luxon_1 = require("luxon");
31
+ const result_to_querydata_1 = require("./result-to-querydata");
32
32
  class PrestoRunner {
33
33
  constructor(config) {
34
34
  const prestoClientConfig = {
@@ -73,10 +73,24 @@ class PrestoRunner {
73
73
  }
74
74
  class TrinoRunner {
75
75
  constructor(config) {
76
+ let server = config.server;
77
+ // trino-client has no separate port field — merge into the server URL
78
+ if (server && config.port) {
79
+ try {
80
+ const url = new URL(server);
81
+ if (!url.port) {
82
+ url.port = String(config.port);
83
+ server = url.toString().replace(/\/$/, '');
84
+ }
85
+ }
86
+ catch {
87
+ // If server isn't a parseable URL, leave it as-is
88
+ }
89
+ }
76
90
  this.client = trino_client_1.Trino.create({
77
91
  ...config.extraConfig,
78
92
  catalog: config.catalog,
79
- server: config.server,
93
+ server,
80
94
  schema: config.schema,
81
95
  auth: new trino_client_1.BasicAuth(config.user, config.password || ''),
82
96
  });
@@ -115,7 +129,7 @@ class TrinoRunner {
115
129
  }
116
130
  }
117
131
  class TrinoPrestoConnection extends connection_1.BaseConnection {
118
- constructor(name, client, queryOptions) {
132
+ constructor(name, client, queryOptions, setupSQL, connectionConfig) {
119
133
  super();
120
134
  this.name = name;
121
135
  this.client = client;
@@ -123,6 +137,27 @@ class TrinoPrestoConnection extends connection_1.BaseConnection {
123
137
  this.dialect = new malloy_1.TrinoDialect();
124
138
  this.name = name;
125
139
  this.queryOptions = queryOptions;
140
+ this.setupSQL = setupSQL;
141
+ this.connectionConfig = connectionConfig !== null && connectionConfig !== void 0 ? connectionConfig : {};
142
+ }
143
+ async ensureSetup() {
144
+ if (!this.setupDone && this.setupSQL) {
145
+ this.setupDone = this.runSetupStatements();
146
+ }
147
+ if (this.setupDone) {
148
+ await this.setupDone;
149
+ }
150
+ }
151
+ async runSetupStatements() {
152
+ for (const stmt of this.setupSQL.split(';\n')) {
153
+ const trimmed = stmt.trim();
154
+ if (trimmed) {
155
+ const r = await this.client.runSQL(trimmed, {});
156
+ if (r.error) {
157
+ throw new Error(r.error);
158
+ }
159
+ }
160
+ }
126
161
  }
127
162
  get dialectName() {
128
163
  return this.name;
@@ -145,8 +180,8 @@ class TrinoPrestoConnection extends connection_1.BaseConnection {
145
180
  return true;
146
181
  }
147
182
  getDigest() {
148
- const data = `trino:${this.name}`;
149
- return (0, malloy_1.makeDigest)(data);
183
+ const { server, port, catalog, schema, user } = this.connectionConfig;
184
+ return (0, malloy_1.makeDigest)('trino', server, port !== undefined ? String(port) : undefined, catalog, schema, user, this.setupSQL);
150
185
  }
151
186
  get supportsNesting() {
152
187
  return true;
@@ -157,101 +192,21 @@ class TrinoPrestoConnection extends connection_1.BaseConnection {
157
192
  unpackArray(_fields, data) {
158
193
  return data;
159
194
  }
160
- convertRow(fields, rawRow) {
161
- var _a;
162
- const retRow = {};
163
- const row = this.unpackArray(fields, rawRow);
164
- for (let i = 0; i < fields.length; i++) {
165
- const field = fields[i];
166
- if (field.type === 'record') {
167
- retRow[field.name] = this.convertRow(field.fields, row[i]);
168
- }
169
- else if ((0, malloy_1.isRepeatedRecord)(field)) {
170
- retRow[field.name] = this.convertNest(field.fields, row[i]);
171
- }
172
- else if ((0, malloy_1.isBasicArray)(field)) {
173
- retRow[field.name] = this.unpackArray([], row[i]);
174
- }
175
- else {
176
- retRow[field.name] = (_a = row[i]) !== null && _a !== void 0 ? _a : null;
177
- }
178
- }
179
- //console.log(retRow);
180
- return retRow;
181
- }
182
- convertNest(fields, _data) {
183
- const data = this.unpackArray(fields, _data);
184
- const ret = [];
185
- const rows = (data === null || data === undefined ? [] : data);
186
- for (const row of rows) {
187
- ret.push(this.convertRow(fields, row));
188
- }
189
- return ret;
190
- }
191
195
  async runSQL(sqlCommand, options = {},
192
196
  // TODO(figutierrez): Use.
193
197
  _rowIndex = 0) {
198
+ await this.ensureSetup();
194
199
  const r = await this.client.runSQL(sqlCommand, options);
195
200
  if (r.error) {
196
201
  throw new Error(r.error);
197
202
  }
198
203
  const { rows: inputRows, columns, profilingUrl } = r;
199
204
  const malloyColumns = columns.map(c => (0, malloy_1.mkFieldDef)(this.malloyTypeFromTrinoType(c.type), c.name));
200
- const malloyRows = [];
205
+ const unpack = (data) => this.unpackArray([], data);
201
206
  const rows = inputRows !== null && inputRows !== void 0 ? inputRows : [];
202
- for (const row of rows) {
203
- const malloyRow = {};
204
- for (let i = 0; i < columns.length; i++) {
205
- const column = columns[i];
206
- const schemaColumn = malloyColumns[i];
207
- malloyRow[column.name] = this.resultRow(schemaColumn, row[i]);
208
- }
209
- malloyRows.push(malloyRow);
210
- }
207
+ const malloyRows = rows.map(row => (0, result_to_querydata_1.resultRowToQueryRecord)(malloyColumns, row, unpack));
211
208
  return { rows: malloyRows, totalRows: malloyRows.length, profilingUrl };
212
209
  }
213
- resultRow(colSchema, rawRow) {
214
- if (colSchema.type === 'record') {
215
- return this.convertRow(colSchema.fields, rawRow);
216
- }
217
- else if ((0, malloy_1.isRepeatedRecord)(colSchema)) {
218
- return this.convertNest(colSchema.fields, rawRow);
219
- }
220
- else if ((0, malloy_1.isBasicArray)(colSchema)) {
221
- const elType = colSchema.elementTypeDef;
222
- let theArray = this.unpackArray([], rawRow);
223
- if (elType.type === 'array') {
224
- theArray = theArray.map(el => this.resultRow(elType, el));
225
- }
226
- return theArray;
227
- }
228
- else if (colSchema.type === 'number' && typeof rawRow === 'string') {
229
- // decimal numbers come back as strings
230
- return Number(rawRow);
231
- }
232
- else if ((colSchema.type === 'timestamp' || colSchema.type === 'timestamptz') &&
233
- typeof rawRow === 'string') {
234
- // timestamps come back as strings
235
- if (colSchema.type === 'timestamptz') {
236
- // TIMESTAMP WITH TIME ZONE format: "2020-02-20 00:00:00 America/Mexico_City"
237
- const trinoTzPattern = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?) (.+)$/;
238
- const match = rawRow.match(trinoTzPattern);
239
- if (match) {
240
- const [, dateTimePart, tzName] = match;
241
- // Use Luxon to parse with timezone awareness
242
- const dt = luxon_1.DateTime.fromSQL(dateTimePart, { zone: tzName });
243
- if (dt.isValid) {
244
- return dt.toJSDate();
245
- }
246
- }
247
- }
248
- // For plain timestamps, Trino returns UTC values - append 'Z' to parse as UTC
249
- return new Date(rawRow + 'Z');
250
- }
251
- else {
252
- return rawRow;
253
- }
254
- }
255
210
  async runSQLBlockAndFetchResultSchema(_sqlBlock, _options) {
256
211
  throw new Error('Not implemented 3');
257
212
  }
@@ -280,6 +235,7 @@ class TrinoPrestoConnection extends connection_1.BaseConnection {
280
235
  return structDef;
281
236
  }
282
237
  async executeAndWait(sqlBlock) {
238
+ await this.ensureSetup();
283
239
  await this.client.runSQL(sqlBlock, {});
284
240
  // TODO: make sure failure is handled correctly.
285
241
  //while (!(await result.next()).done);
@@ -299,6 +255,7 @@ class TrinoPrestoConnection extends connection_1.BaseConnection {
299
255
  async loadSchemaForSqlBlock(sqlBlock, structDef, element) {
300
256
  var _a;
301
257
  try {
258
+ await this.ensureSetup();
302
259
  const queryResult = await this.client.runSQL(sqlBlock, {});
303
260
  if (queryResult.error) {
304
261
  // TODO: handle.
@@ -343,7 +300,12 @@ TrinoPrestoConnection.DEFAULT_QUERY_OPTIONS = {
343
300
  };
344
301
  class PrestoConnection extends TrinoPrestoConnection {
345
302
  constructor(arg, queryOptions, config = {}) {
346
- super(typeof arg === 'string' ? arg : arg.name, new PrestoRunner(config), queryOptions);
303
+ const setupSQL = typeof arg === 'string'
304
+ ? config.setupSQL
305
+ : typeof arg['setupSQL'] === 'string'
306
+ ? arg['setupSQL']
307
+ : undefined;
308
+ super(typeof arg === 'string' ? arg : arg.name, new PrestoRunner(config), queryOptions, setupSQL, config);
347
309
  }
348
310
  get dialectName() {
349
311
  return 'presto';
@@ -375,7 +337,12 @@ class PrestoConnection extends TrinoPrestoConnection {
375
337
  exports.PrestoConnection = PrestoConnection;
376
338
  class TrinoConnection extends TrinoPrestoConnection {
377
339
  constructor(arg, queryOptions, config = {}) {
378
- super(typeof arg === 'string' ? arg : arg.name, new TrinoRunner(config), queryOptions);
340
+ const setupSQL = typeof arg === 'string'
341
+ ? config.setupSQL
342
+ : typeof arg['setupSQL'] === 'string'
343
+ ? arg['setupSQL']
344
+ : undefined;
345
+ super(typeof arg === 'string' ? arg : arg.name, new TrinoRunner(config), queryOptions, setupSQL, config);
379
346
  }
380
347
  get dialectName() {
381
348
  return 'trino';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/db-trino",
3
- "version": "0.0.337",
3
+ "version": "0.0.339",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "dependencies": {
25
- "@malloydata/malloy": "0.0.337",
25
+ "@malloydata/malloy": "0.0.339",
26
26
  "@prestodb/presto-js-client": "^1.0.0",
27
27
  "gaxios": "^4.2.0",
28
28
  "luxon": "^3.5.0",