@malloydata/db-postgres 0.0.1

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/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Malloy
2
+
3
+ Malloy is an experimental language for describing data relationships and transformations. It is both a semantic modeling language and a querying language that runs queries against a relational database. Malloy currently connects to BigQuery and Postgres, and natively supports DuckDB. We've built a Visual Studio Code extension to facilitate building Malloy data models, querying and transforming data, and creating simple visualizations and dashboards.
4
+
5
+ ## This package
6
+
7
+ This package facilitates using the `malloydata/malloy` library with Postgres - see [here](https://github.com/looker-open-source/malloy/blob/main/packages/malloy/README.md) for additional information.
@@ -0,0 +1 @@
1
+ export { PostgresConnection, PooledPostgresConnection, } from "./postgres_connection";
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright 2021 Google LLC
4
+ *
5
+ * This program is free software; you can redistribute it and/or
6
+ * modify it under the terms of the GNU General Public License
7
+ * version 2 as published by the Free Software Foundation.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PooledPostgresConnection = exports.PostgresConnection = void 0;
16
+ var postgres_connection_1 = require("./postgres_connection");
17
+ Object.defineProperty(exports, "PostgresConnection", { enumerable: true, get: function () { return postgres_connection_1.PostgresConnection; } });
18
+ Object.defineProperty(exports, "PooledPostgresConnection", { enumerable: true, get: function () { return postgres_connection_1.PooledPostgresConnection; } });
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,64 @@
1
+ import { StructDef, MalloyQueryData, QueryData, PooledConnection, SQLBlock, Connection, QueryDataRow } from "@malloydata/malloy";
2
+ import { FetchSchemaAndRunSimultaneously, FetchSchemaAndRunStreamSimultaneously, PersistSQLResults, StreamingConnection } from "@malloydata/malloy/src/runtime_types";
3
+ import { Client } from "pg";
4
+ import { RunSQLOptions } from "@malloydata/malloy/src/malloy";
5
+ interface PostgresQueryConfiguration {
6
+ rowLimit?: number;
7
+ }
8
+ declare type PostgresQueryConfigurationReader = PostgresQueryConfiguration | (() => PostgresQueryConfiguration) | (() => Promise<PostgresQueryConfiguration>);
9
+ interface PostgresConnectionConfiguration {
10
+ host?: string;
11
+ port?: number;
12
+ username?: string;
13
+ password?: string;
14
+ databaseName?: string;
15
+ }
16
+ declare type PostgresConnectionConfigurationReader = PostgresConnectionConfiguration | (() => Promise<PostgresConnectionConfiguration>);
17
+ export declare class PostgresConnection implements Connection, StreamingConnection, PersistSQLResults {
18
+ private schemaCache;
19
+ private sqlSchemaCache;
20
+ private queryConfigReader;
21
+ private configReader;
22
+ readonly name: string;
23
+ constructor(name: string, queryConfigReader?: PostgresQueryConfigurationReader, configReader?: PostgresConnectionConfigurationReader);
24
+ private readQueryConfig;
25
+ private readConfig;
26
+ get dialectName(): string;
27
+ isPool(): this is PooledConnection;
28
+ canPersist(): this is PersistSQLResults;
29
+ canFetchSchemaAndRunSimultaneously(): this is FetchSchemaAndRunSimultaneously;
30
+ canFetchSchemaAndRunStreamSimultaneously(): this is FetchSchemaAndRunStreamSimultaneously;
31
+ canStream(): this is StreamingConnection;
32
+ fetchSchemaForTables(missing: string[]): Promise<{
33
+ schemas: Record<string, StructDef>;
34
+ errors: Record<string, string>;
35
+ }>;
36
+ fetchSchemaForSQLBlocks(sqlRefs: SQLBlock[]): Promise<{
37
+ schemas: Record<string, StructDef>;
38
+ errors: Record<string, string>;
39
+ }>;
40
+ protected getClient(): Promise<Client>;
41
+ protected runPostgresQuery(sqlCommand: string, _pageSize: number, _rowIndex: number, deJSON: boolean): Promise<MalloyQueryData>;
42
+ private getSQLBlockSchema;
43
+ private schemaFromQuery;
44
+ private getTableSchema;
45
+ executeSQLRaw(query: string): Promise<QueryData>;
46
+ test(): Promise<void>;
47
+ runSQL(sql: string, { rowLimit }?: RunSQLOptions, rowIndex?: number): Promise<MalloyQueryData>;
48
+ runSQLStream(sqlCommand: string, options?: {
49
+ rowLimit?: number;
50
+ }): AsyncIterableIterator<QueryDataRow>;
51
+ manifestTemporaryTable(sqlCommand: string): Promise<string>;
52
+ }
53
+ export declare class PooledPostgresConnection extends PostgresConnection implements PooledConnection {
54
+ private pool;
55
+ constructor(name: string);
56
+ isPool(): true;
57
+ drain(): Promise<void>;
58
+ protected runPostgresQuery(sqlCommand: string, _pageSize: number, _rowIndex: number, deJSON: boolean): Promise<MalloyQueryData>;
59
+ private getClientFromPool;
60
+ runSQLStream(sqlCommand: string, options?: {
61
+ rowLimit?: number;
62
+ }): AsyncIterableIterator<QueryDataRow>;
63
+ }
64
+ export {};
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright 2022 Google LLC
4
+ *
5
+ * This program is free software; you can redistribute it and/or
6
+ * modify it under the terms of the GNU General Public License
7
+ * version 2 as published by the Free Software Foundation.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ var __importDefault = (this && this.__importDefault) || function (mod) {
38
+ return (mod && mod.__esModule) ? mod : { "default": mod };
39
+ };
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.PooledPostgresConnection = exports.PostgresConnection = void 0;
42
+ // LTNOTE: we need this extension to be installed to correctly index
43
+ // postgres data... We should probably do this on connection creation...
44
+ //
45
+ // create extension if not exists tsm_system_rows
46
+ //
47
+ const crypto = __importStar(require("crypto"));
48
+ const malloy_1 = require("@malloydata/malloy");
49
+ const pg_1 = require("pg");
50
+ const pg_query_stream_1 = __importDefault(require("pg-query-stream"));
51
+ const postgresToMalloyTypes = {
52
+ "character varying": "string",
53
+ name: "string",
54
+ text: "string",
55
+ date: "date",
56
+ integer: "number",
57
+ bigint: "number",
58
+ "double precision": "number",
59
+ "timestamp without time zone": "timestamp",
60
+ oid: "string",
61
+ boolean: "boolean",
62
+ // ARRAY: "string",
63
+ "timestamp with time zone": "timestamp",
64
+ timestamp: "timestamp",
65
+ '"char"': "string",
66
+ character: "string",
67
+ smallint: "number",
68
+ xid: "string",
69
+ real: "number",
70
+ interval: "string",
71
+ inet: "string",
72
+ regtype: "string",
73
+ numeric: "number",
74
+ bytea: "string",
75
+ pg_ndistinct: "number",
76
+ };
77
+ const DEFAULT_PAGE_SIZE = 1000;
78
+ const SCHEMA_PAGE_SIZE = 1000;
79
+ class PostgresConnection {
80
+ constructor(name, queryConfigReader = {}, configReader = {}) {
81
+ this.schemaCache = new Map();
82
+ this.sqlSchemaCache = new Map();
83
+ this.queryConfigReader = queryConfigReader;
84
+ this.configReader = configReader;
85
+ this.name = name;
86
+ }
87
+ async readQueryConfig() {
88
+ if (this.queryConfigReader instanceof Function) {
89
+ return this.queryConfigReader();
90
+ }
91
+ else {
92
+ return this.queryConfigReader;
93
+ }
94
+ }
95
+ async readConfig() {
96
+ if (this.configReader instanceof Function) {
97
+ return this.configReader();
98
+ }
99
+ else {
100
+ return this.configReader;
101
+ }
102
+ }
103
+ get dialectName() {
104
+ return "postgres";
105
+ }
106
+ isPool() {
107
+ return false;
108
+ }
109
+ canPersist() {
110
+ return true;
111
+ }
112
+ canFetchSchemaAndRunSimultaneously() {
113
+ // TODO feature-sql-block Implement FetchSchemaAndRunSimultaneously
114
+ return false;
115
+ }
116
+ canFetchSchemaAndRunStreamSimultaneously() {
117
+ return false;
118
+ }
119
+ canStream() {
120
+ return true;
121
+ }
122
+ async fetchSchemaForTables(missing) {
123
+ const schemas = {};
124
+ const errors = {};
125
+ for (const tableURL of missing) {
126
+ let inCache = this.schemaCache.get(tableURL);
127
+ if (!inCache) {
128
+ try {
129
+ inCache = {
130
+ schema: await this.getTableSchema(tableURL),
131
+ };
132
+ this.schemaCache.set(tableURL, inCache);
133
+ }
134
+ catch (error) {
135
+ inCache = { error: error.message };
136
+ }
137
+ }
138
+ if (inCache.schema !== undefined) {
139
+ schemas[tableURL] = inCache.schema;
140
+ }
141
+ else {
142
+ errors[tableURL] = inCache.error;
143
+ }
144
+ }
145
+ return { schemas, errors };
146
+ }
147
+ async fetchSchemaForSQLBlocks(sqlRefs) {
148
+ const schemas = {};
149
+ const errors = {};
150
+ for (const sqlRef of sqlRefs) {
151
+ const key = sqlRef.name;
152
+ let inCache = this.sqlSchemaCache.get(key);
153
+ if (!inCache) {
154
+ try {
155
+ inCache = {
156
+ schema: await this.getSQLBlockSchema(sqlRef),
157
+ };
158
+ this.schemaCache.set(key, inCache);
159
+ }
160
+ catch (error) {
161
+ inCache = { error: error.message };
162
+ }
163
+ }
164
+ if (inCache.schema !== undefined) {
165
+ schemas[key] = inCache.schema;
166
+ }
167
+ else {
168
+ errors[key] = inCache.error;
169
+ }
170
+ }
171
+ return { schemas, errors };
172
+ }
173
+ async getClient() {
174
+ const config = await this.readConfig();
175
+ return new pg_1.Client({
176
+ user: config.username,
177
+ password: config.password,
178
+ database: config.databaseName,
179
+ port: config.port,
180
+ host: config.host,
181
+ });
182
+ }
183
+ async runPostgresQuery(sqlCommand, _pageSize, _rowIndex, deJSON) {
184
+ const client = await this.getClient();
185
+ await client.connect();
186
+ let result = await client.query(sqlCommand);
187
+ if (Array.isArray(result)) {
188
+ result = result.pop();
189
+ }
190
+ if (deJSON) {
191
+ for (let i = 0; i < result.rows.length; i++) {
192
+ result.rows[i] = result.rows[i].row;
193
+ }
194
+ }
195
+ await client.end();
196
+ return { rows: result.rows, totalRows: result.rows.length };
197
+ }
198
+ async getSQLBlockSchema(sqlRef) {
199
+ const structDef = {
200
+ type: "struct",
201
+ dialect: "postgres",
202
+ name: sqlRef.name,
203
+ structSource: {
204
+ type: "sql",
205
+ method: "subquery",
206
+ sqlBlock: sqlRef,
207
+ },
208
+ structRelationship: {
209
+ type: "basetable",
210
+ connectionName: this.name,
211
+ },
212
+ fields: [],
213
+ };
214
+ // TODO -- Should be a uuid
215
+ const tempTableName = `malloy${Math.floor(Math.random() * 10000000)}`;
216
+ const infoQuery = `
217
+ drop table if exists ${tempTableName};
218
+ create temp table ${tempTableName} as SELECT * FROM (
219
+ ${sqlRef.select}
220
+ ) as x where false;
221
+ SELECT column_name, c.data_type, e.data_type as element_type
222
+ FROM information_schema.columns c LEFT JOIN information_schema.element_types e
223
+ ON ((c.table_catalog, c.table_schema, c.table_name, 'TABLE', c.dtd_identifier)
224
+ = (e.object_catalog, e.object_schema, e.object_name, e.object_type, e.collection_type_identifier))
225
+ where table_name='${tempTableName}';
226
+ `;
227
+ await this.schemaFromQuery(infoQuery, structDef);
228
+ return structDef;
229
+ }
230
+ async schemaFromQuery(infoQuery, structDef) {
231
+ const result = await this.runPostgresQuery(infoQuery, SCHEMA_PAGE_SIZE, 0, false);
232
+ for (const row of result.rows) {
233
+ const postgresDataType = row["data_type"];
234
+ let s = structDef;
235
+ let malloyType = postgresToMalloyTypes[postgresDataType];
236
+ let name = row["column_name"];
237
+ if (postgresDataType === "ARRAY") {
238
+ malloyType = postgresToMalloyTypes[row["element_type"]];
239
+ s = {
240
+ type: "struct",
241
+ name: row["column_name"],
242
+ dialect: this.dialectName,
243
+ structRelationship: { type: "nested", field: name, isArray: true },
244
+ structSource: { type: "nested" },
245
+ fields: [],
246
+ };
247
+ structDef.fields.push(s);
248
+ name = "value";
249
+ }
250
+ if (malloyType !== undefined) {
251
+ s.fields.push({
252
+ type: malloyType,
253
+ name,
254
+ });
255
+ }
256
+ else {
257
+ throw new Error(`unknown postgres type ${postgresDataType}`);
258
+ }
259
+ }
260
+ }
261
+ async getTableSchema(tableURL) {
262
+ const structDef = {
263
+ type: "struct",
264
+ name: tableURL,
265
+ dialect: "postgres",
266
+ structSource: { type: "table" },
267
+ structRelationship: {
268
+ type: "basetable",
269
+ connectionName: this.name,
270
+ },
271
+ fields: [],
272
+ };
273
+ const { tablePath: tableName } = (0, malloy_1.parseTableURL)(tableURL);
274
+ const [schema, table] = tableName.split(".");
275
+ if (table === undefined) {
276
+ throw new Error("Default schema not yet supported in Postgres");
277
+ }
278
+ const infoQuery = `
279
+ SELECT column_name, c.data_type, e.data_type as element_type
280
+ FROM information_schema.columns c LEFT JOIN information_schema.element_types e
281
+ ON ((c.table_catalog, c.table_schema, c.table_name, 'TABLE', c.dtd_identifier)
282
+ = (e.object_catalog, e.object_schema, e.object_name, e.object_type, e.collection_type_identifier))
283
+ WHERE table_name = '${table}'
284
+ AND table_schema = '${schema}'
285
+ `;
286
+ await this.schemaFromQuery(infoQuery, structDef);
287
+ return structDef;
288
+ }
289
+ async executeSQLRaw(query) {
290
+ const config = await this.readQueryConfig();
291
+ const queryData = await this.runPostgresQuery(query, config.rowLimit || DEFAULT_PAGE_SIZE, 0, false);
292
+ return queryData.rows;
293
+ }
294
+ async test() {
295
+ await this.executeSQLRaw("SELECT 1");
296
+ }
297
+ async runSQL(sql, { rowLimit } = {}, rowIndex = 0) {
298
+ var _a;
299
+ const config = await this.readQueryConfig();
300
+ return await this.runPostgresQuery(sql, (_a = rowLimit !== null && rowLimit !== void 0 ? rowLimit : config.rowLimit) !== null && _a !== void 0 ? _a : DEFAULT_PAGE_SIZE, rowIndex, true);
301
+ }
302
+ async *runSQLStream(sqlCommand, options) {
303
+ const query = new pg_query_stream_1.default(sqlCommand);
304
+ const client = await this.getClient();
305
+ client.connect();
306
+ const rowStream = client.query(query);
307
+ let index = 0;
308
+ for await (const row of rowStream) {
309
+ yield row.row;
310
+ index += 1;
311
+ if ((options === null || options === void 0 ? void 0 : options.rowLimit) !== undefined && index >= options.rowLimit) {
312
+ query.destroy();
313
+ break;
314
+ }
315
+ }
316
+ await client.end();
317
+ }
318
+ async manifestTemporaryTable(sqlCommand) {
319
+ const hash = crypto.createHash("md5").update(sqlCommand).digest("hex");
320
+ const tableName = `tt${hash}`;
321
+ const cmd = `CREATE TEMPORARY TABLE IF NOT EXISTS ${tableName} AS (${sqlCommand});`;
322
+ // console.log(cmd);
323
+ await this.runPostgresQuery(cmd, 1000, 0, false);
324
+ return tableName;
325
+ }
326
+ }
327
+ exports.PostgresConnection = PostgresConnection;
328
+ class PooledPostgresConnection extends PostgresConnection {
329
+ constructor(name) {
330
+ super(name);
331
+ this.pool = new pg_1.Pool();
332
+ }
333
+ isPool() {
334
+ return true;
335
+ }
336
+ async drain() {
337
+ await this.pool.end();
338
+ }
339
+ async runPostgresQuery(sqlCommand, _pageSize, _rowIndex, deJSON) {
340
+ let result = await this.pool.query(sqlCommand);
341
+ if (Array.isArray(result)) {
342
+ result = result.pop();
343
+ }
344
+ if (deJSON) {
345
+ for (let i = 0; i < result.rows.length; i++) {
346
+ result.rows[i] = result.rows[i].row;
347
+ }
348
+ }
349
+ return { rows: result.rows, totalRows: result.rows.length };
350
+ }
351
+ async getClientFromPool() {
352
+ return await new Promise((resolve, reject) => this.pool.connect((error, client, releaseClient) => {
353
+ if (error) {
354
+ reject(error);
355
+ }
356
+ else {
357
+ resolve([client, releaseClient]);
358
+ }
359
+ }));
360
+ }
361
+ async *runSQLStream(sqlCommand, options) {
362
+ const query = new pg_query_stream_1.default(sqlCommand);
363
+ let index = 0;
364
+ // This is a strange hack... `this.pool.query(query)` seems to return the wrong
365
+ // type. Because `query` is a `QueryStream`, the result is supposed to be a
366
+ // `QueryStream` as well, but it's not. So instead, we get a client and call
367
+ // `client.query(query)`, which does what it's supposed to.
368
+ const [client, releaseClient] = await this.getClientFromPool();
369
+ const resultStream = client.query(query);
370
+ for await (const row of resultStream) {
371
+ yield row.row;
372
+ index += 1;
373
+ if ((options === null || options === void 0 ? void 0 : options.rowLimit) !== undefined && index >= options.rowLimit) {
374
+ query.destroy();
375
+ break;
376
+ }
377
+ }
378
+ releaseClient();
379
+ }
380
+ }
381
+ exports.PooledPostgresConnection = PooledPostgresConnection;
382
+ //# sourceMappingURL=postgres_connection.js.map
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@malloydata/db-postgres",
3
+ "version": "0.0.1",
4
+ "license": "GPL-2.0",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "lint": "eslint '**/*.ts{,x}'",
9
+ "lint-fix": "eslint '**/*.ts{,x}' --fix",
10
+ "test": "jest --config=../../jest.config.js",
11
+ "build": "tsc --build",
12
+ "malloyc": "ts-node ../../scripts/malloy-to-json"
13
+ },
14
+ "dependencies": {
15
+ "@malloydata/malloy": "^0.0.1",
16
+ "@types/pg": "^8.6.1",
17
+ "pg": "^8.7.1",
18
+ "pg-query-stream": "4.2.3"
19
+ }
20
+ }