@quillsql/node 0.3.7 → 0.4.0

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.
@@ -0,0 +1,216 @@
1
+ import { connect } from "http2";
2
+ import { Client } from "../models/Client";
3
+ import mysql, { Pool as MysqlPool } from "mysql2";
4
+ import { QuillQueryResults } from "./DatabaseHelper";
5
+ import url from "url";
6
+ import { capitalize, depluralize } from "../utils/textProcessing";
7
+
8
+ export interface MysqlConnectionConfig {
9
+ host: string;
10
+ user: string;
11
+ password: string;
12
+ database: string;
13
+ }
14
+
15
+ export function formatMysqlConfig(
16
+ connectionString: string
17
+ ): MysqlConnectionConfig {
18
+ const parsedUrl = url.parse(connectionString);
19
+ const [user, password] = (parsedUrl.auth || "").split(":");
20
+ return {
21
+ host: parsedUrl.hostname || "",
22
+ user: user || "",
23
+ password: password || "",
24
+ database: (parsedUrl.pathname || "").slice(1),
25
+ };
26
+ }
27
+
28
+ export function connectToMysql(config: MysqlConnectionConfig): MysqlPool {
29
+ const pool = mysql.createPool({
30
+ ...config,
31
+ waitForConnections: true,
32
+ connectionLimit: 10,
33
+ queueLimit: 0,
34
+ });
35
+ return pool;
36
+ }
37
+
38
+ export function disconnectFromMysql(connection: MysqlPool) {
39
+ connection.end();
40
+ }
41
+
42
+ export async function runQueryMysql(
43
+ sql: string,
44
+ connection: MysqlPool
45
+ ): Promise<QuillQueryResults> {
46
+ const result: QuillQueryResults = await new Promise((resolve, reject) => {
47
+ connection.query(sql, (error, results, fields) => {
48
+ if (error) {
49
+ reject(error);
50
+ return;
51
+ }
52
+
53
+ const mappedFields = fields
54
+ ? fields.map((field) => ({
55
+ name: field.name,
56
+ dataTypeID: mysqlDataTypeIdToPostgresType(field.type || 1043), // Provide a default value for dataTypeID
57
+ }))
58
+ : [];
59
+
60
+ const processRows = Array.isArray(results)
61
+ ? results.map((row: any) => {
62
+ return JSON.parse(JSON.stringify(row));
63
+ })
64
+ : [];
65
+
66
+ resolve({ fields: mappedFields, rows: processRows });
67
+ });
68
+ });
69
+ return result;
70
+ }
71
+
72
+ export async function getSchemasMysql(
73
+ connection: MysqlPool
74
+ ): Promise<string[]> {
75
+ const sql = `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA
76
+ WHERE schema_name != 'information_schema'
77
+ AND schema_name != 'performance_schema'
78
+ and schema_name != 'sys';`;
79
+ const results = await runQueryMysql(sql, connection);
80
+ return results.rows.map((row) => row.SCHEMA_NAME);
81
+ }
82
+
83
+ export async function getTablesBySchemaMysql(
84
+ connection: MysqlPool,
85
+ schemaNames: string[]
86
+ ): Promise<{ tableName: string; schemaName: string }[]> {
87
+ const allColumns = await Promise.all(
88
+ schemaNames.map(async (schema) => {
89
+ const sql = `SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '${schema}'`;
90
+ const results = await runQueryMysql(sql, connection);
91
+ return results.rows.map((row) => {
92
+ return { tableName: row.TABLE_NAME, schemaName: row.TABLE_SCHEMA };
93
+ });
94
+ })
95
+ );
96
+ return allColumns.flat();
97
+ }
98
+
99
+ export async function getColumnsByTableMysql(
100
+ connection: MysqlPool,
101
+ schemaName: string,
102
+ tableName: string
103
+ ): Promise<string[]> {
104
+ const sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '${schemaName}' AND TABLE_NAME = '${tableName}'`;
105
+ const results = await runQueryMysql(sql, connection);
106
+ return results.rows.map((row) => row.COLUMN_NAME);
107
+ }
108
+
109
+ export async function getForeignKeysMysql(
110
+ connection: MysqlPool,
111
+ schemaName: string,
112
+ tableName: string,
113
+ primaryKey: string
114
+ ): Promise<string[]> {
115
+ const depluralizedTableName = depluralize(tableName);
116
+ let sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
117
+ WHERE TABLE_SCHEMA = '${schemaName}'
118
+ and table_name != '${tableName}'
119
+ and (column_name = '${primaryKey}'
120
+ or column_name = '${depluralizedTableName}\\_${primaryKey}'
121
+ or column_name = '${depluralizedTableName}${capitalize(primaryKey)}' )`;
122
+ const results = await runQueryMysql(sql, connection);
123
+ let foreignKeysString = results.rows.map((key) => {
124
+ return key.COLUMN_NAME;
125
+ });
126
+ // remove any foreignKeyStrings that are just 'id'
127
+ foreignKeysString = foreignKeysString.filter(
128
+ (key) => key !== "id" && key !== "_id_"
129
+ );
130
+ foreignKeysString = [...new Set(foreignKeysString)];
131
+ if (foreignKeysString.length === 0) {
132
+ sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
133
+ WHERE TABLE_SCHEMA = '${schemaName}'
134
+ and table_name != '${tableName}'
135
+ and (column_name like '${depluralizedTableName}%'
136
+ or column_name like '%\\_id'
137
+ or column_name like '%Id'
138
+ or column_name like '%\\_${primaryKey}'
139
+ or column_name like '%${capitalize(primaryKey)}')`;
140
+ const results = await runQueryMysql(sql, connection);
141
+ foreignKeysString = results.rows.map((key) => {
142
+ return key.COLUMN_NAME;
143
+ });
144
+ foreignKeysString = [...new Set(foreignKeysString)];
145
+ }
146
+ return foreignKeysString;
147
+ }
148
+
149
+ export async function getSchemaColumnInfoMysql(
150
+ connection: MysqlPool,
151
+ schemaName: string,
152
+ tableNames: { tableName: string; schemaName: string }[]
153
+ ): Promise<
154
+ { tableName: string; columns: { columnName: string; dataTypeID: number }[] }[]
155
+ > {
156
+ const allColumns = await Promise.all(
157
+ tableNames.map(async (tableName) => {
158
+ const query = `
159
+ SELECT COLUMN_NAME as columnName,
160
+ DATA_TYPE as dataType FROM INFORMATION_SCHEMA.COLUMNS
161
+ WHERE TABLE_SCHEMA = '${tableName.schemaName}'
162
+ AND TABLE_NAME = '${tableName.tableName}'
163
+ `;
164
+ const results = await runQueryMysql(query, connection);
165
+ return {
166
+ tableName: `${tableName.schemaName}.${tableName.tableName}`,
167
+ displayName: `${tableName.schemaName}.${tableName.tableName}`,
168
+ columns: results.rows.map((row: any) => ({
169
+ columnName: row.columnName,
170
+ displayName: row.columnName,
171
+ dataTypeID: mysqlTextDataTypeToPostgresOID(row.dataType),
172
+ fieldType: row.dataType,
173
+ })),
174
+ };
175
+ })
176
+ );
177
+ return allColumns;
178
+ }
179
+
180
+ function mysqlTextDataTypeToPostgresOID(type: string): number {
181
+ switch (type) {
182
+ case "bigint": // int
183
+ return 23;
184
+ case "tinyint": // int
185
+ return 23;
186
+ case "float": // float
187
+ return 700;
188
+ case "varchar": // varchar
189
+ return 1043;
190
+ case "timestamp": // date
191
+ return 1082;
192
+ default: // varchar
193
+ return 1043;
194
+ }
195
+ }
196
+
197
+ function mysqlDataTypeIdToPostgresType(type: number): number {
198
+ switch (type) {
199
+ case 8: // int
200
+ return 23;
201
+ case 3: // int
202
+ return 23;
203
+ case 2: // int
204
+ return 23;
205
+ case 5: // float
206
+ return 700;
207
+ case 253: // varchar
208
+ return 1043;
209
+ case 7: // date
210
+ return 1082;
211
+ case 7: // date
212
+ return 1082;
213
+ default: // varchar
214
+ return 1043;
215
+ }
216
+ }
@@ -0,0 +1,187 @@
1
+ import { Pool } from "pg";
2
+ import { Client } from "../models/Client";
3
+ import { disconnect } from "process";
4
+ import { QuillQueryResults } from "./DatabaseHelper";
5
+ import { capitalize, depluralize } from "../utils/textProcessing";
6
+ import { PG_TYPES } from "../assets/pgtypes";
7
+ import { run } from "node:test";
8
+
9
+ export type PostgresConnectionConfig = {
10
+ connectionString: string;
11
+ ssl?: {
12
+ rejectUnauthorized: false;
13
+ ca?: string;
14
+ key?: string;
15
+ cert?: string;
16
+ };
17
+ };
18
+
19
+ export function connectToPostgres(config: PostgresConnectionConfig): Pool {
20
+ return new Pool(config);
21
+ }
22
+
23
+ export function disconnectFromPostgres(pool: Pool) {
24
+ pool.end();
25
+ }
26
+
27
+ export async function runQueryPostgres(
28
+ sql: string,
29
+ pool: Pool
30
+ ): Promise<QuillQueryResults> {
31
+ const results = await pool.query(sql);
32
+ return {
33
+ fields: results.fields.map((field: any) => ({
34
+ name: field.name,
35
+ dataTypeID: field.dataTypeID,
36
+ })),
37
+ rows: results.rows,
38
+ };
39
+ }
40
+
41
+ export async function getSchemasPostgres(pool: Pool): Promise<string[]> {
42
+ const sql = `SELECT schema_name FROM information_schema.schemata
43
+ WHERE schema_name NOT LIKE 'pg_%' AND schema_name != 'information_schema';`;
44
+ const results = await runQueryPostgres(sql, pool);
45
+ return results.rows.map((row) => row.schema_name);
46
+ }
47
+
48
+ export async function getTablesBySchemaPostgres(
49
+ pool: Pool,
50
+ schemaNames: string[]
51
+ ): Promise<{ tableName: string; schemaName: string }[]> {
52
+ const allColumns = await Promise.all(
53
+ schemaNames.map(async (schema) => {
54
+ const sql = `SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema = '${schema}'`;
55
+ const results = await runQueryPostgres(sql, pool);
56
+ return results.rows.map((row) => {
57
+ return { tableName: row.table_name, schemaName: row.table_schema };
58
+ });
59
+ })
60
+ );
61
+ return allColumns.flat();
62
+ }
63
+
64
+ export async function getColumnsByTablePostgres(
65
+ pool: Pool,
66
+ schemaName: string,
67
+ tableName: string
68
+ ): Promise<string[]> {
69
+ const sql = `SELECT column_name FROM information_schema.columns WHERE table_schema = '${schemaName}' and table_name = '${tableName}'`;
70
+ const results = await runQueryPostgres(sql, pool);
71
+ return results.rows.map((row) => row.column_name);
72
+ }
73
+
74
+ export async function getForeignKeysPostgres(
75
+ pool: Pool,
76
+ schemaName: string,
77
+ tableName: string,
78
+ primaryKey: string
79
+ ): Promise<string[]> {
80
+ const depluralizedTableName = depluralize(tableName);
81
+ let sql = `SELECT column_name FROM information_schema.columns
82
+ WHERE table_schema = '${schemaName}'
83
+ and table_name != '${tableName}'
84
+ and (column_name = '${primaryKey}'
85
+ or column_name = '${depluralizedTableName}_${primaryKey}'
86
+ or column_name = '${depluralizedTableName}${capitalize(primaryKey)}')`;
87
+ const results = await runQueryPostgres(sql, pool);
88
+ let foreignKeysString = results.rows.map((key) => {
89
+ return key.column_name;
90
+ });
91
+ foreignKeysString = foreignKeysString.filter(
92
+ (key) => key !== "id" && key !== "_id_"
93
+ );
94
+ foreignKeysString = [...new Set(foreignKeysString)];
95
+ if (foreignKeysString.length === 0) {
96
+ sql = `SELECT column_name FROM information_schema.columns
97
+ WHERE table_schema = '${schemaName}'
98
+ and table_name != '${tableName}'
99
+ and (column_name like '${tableName}%'
100
+ or column_name like '%\\_id'
101
+ or column_name like '%Id'
102
+ or column_name like '%\\_${primaryKey}'
103
+ or column_name like '%${capitalize(primaryKey)}')`;
104
+ const results = await runQueryPostgres(sql, pool);
105
+ foreignKeysString = results.rows.map((key) => {
106
+ return key.column_name;
107
+ });
108
+ foreignKeysString = [...new Set(foreignKeysString)];
109
+ }
110
+ return foreignKeysString;
111
+ }
112
+
113
+ export async function getSchemaColumnInfoPostgress(
114
+ pool: Pool,
115
+ schemaName: string,
116
+ tableNames: { tableName: string; schemaName: string }[]
117
+ ): Promise<
118
+ { tableName: string; columns: { columnName: string; dataTypeID: number }[] }[]
119
+ > {
120
+ const allColumns = await Promise.all(
121
+ tableNames.map(async (tableName) => {
122
+ const query = `
123
+ SELECT column_name as "columnName", udt_name as "fieldType"
124
+ FROM information_schema.columns
125
+ WHERE table_schema = '${tableName.schemaName}'
126
+ AND table_name = '${tableName.tableName}'
127
+ ORDER BY ordinal_position;
128
+ `;
129
+ const results = await runQueryPostgres(query, pool);
130
+ return {
131
+ tableName: `${tableName.schemaName}.${tableName.tableName}`,
132
+ displayName: `${tableName.schemaName}.${tableName.tableName}`,
133
+ columns: results.rows.map((row) => {
134
+ let pgType = PG_TYPES.find((pgType) => {
135
+ return pgType.typname === row.fieldType;
136
+ })?.oid;
137
+ if (!pgType) {
138
+ pgType = 1043;
139
+ }
140
+ return {
141
+ columnName: row.columnName,
142
+ displayName: row.columnName,
143
+ dataTypeID: pgType,
144
+ fieldType: row.fieldType,
145
+ };
146
+ }),
147
+ };
148
+ })
149
+ );
150
+ return allColumns;
151
+ }
152
+
153
+ export function formatPostgresConfig(
154
+ connectionString: string
155
+ ): PostgresConnectionConfig {
156
+ return { connectionString, ssl: { rejectUnauthorized: false } };
157
+ }
158
+
159
+ // CURRENTLY UNUSED BUT MAYBE USEFUL IN THE FUTURE
160
+ function getSslConfig(client: Client):
161
+ | {
162
+ rejectUnauthorized: false;
163
+ ca?: string;
164
+ key?: string;
165
+ cert?: string;
166
+ }
167
+ | undefined {
168
+ if (!client.useSsl) {
169
+ return undefined;
170
+ }
171
+ if (client.serverCa && client.clientKey && client.clientCert) {
172
+ return {
173
+ rejectUnauthorized: false,
174
+ ca: client.serverCa,
175
+ key: client.clientKey,
176
+ cert: client.clientCert,
177
+ };
178
+ }
179
+ if (client.serverCa) {
180
+ return {
181
+ rejectUnauthorized: false,
182
+ ca: client.serverCa,
183
+ };
184
+ }
185
+ // if using ssl with no certificates
186
+ return { rejectUnauthorized: false };
187
+ }
@@ -0,0 +1,203 @@
1
+ import snowflake from "snowflake-sdk";
2
+ import { Client } from "../models/Client";
3
+ import { QuillQueryResults } from "./DatabaseHelper";
4
+ import { capitalize, depluralize } from "../utils/textProcessing";
5
+
6
+ const POSTGRES_SNOWFLAKE_MAP: { [type: string]: number } = {
7
+ BOOLEAN: 16,
8
+ FIXED: 1700, // DECIMAL or NUMERIC
9
+ REAL: 700, // FLOAT4
10
+ DOUBLE: 701, // FLOAT8
11
+ TEXT: 25, // TEXT in PostgreSQL
12
+ DATE: 1082,
13
+ DATETIME: 1184, // TIMESTAMP in PostgreSQL
14
+ TIME: 1083,
15
+ TIMESTAMP_LTZ: 1184,
16
+ TIMESTAMP_NTZ: 1184,
17
+ TIMESTAMP_TZ: 1184,
18
+ VARIANT: 114, // JSONB in PostgreSQL
19
+ OBJECT: 114, // JSONB, as an equivalent for structured JSON
20
+ ARRAY: 1009, // TEXT[], assuming most common case
21
+ BINARY: 17, // BYTEA
22
+ };
23
+
24
+ export type SnowflakeConnectionConfig = {
25
+ account: string;
26
+ username: string;
27
+ password: string;
28
+ database: string;
29
+ warehouse: string;
30
+ };
31
+
32
+ export async function runQuerySnowflake(
33
+ sql: string,
34
+ connection: snowflake.Connection
35
+ ): Promise<QuillQueryResults> {
36
+ const results = await new Promise((resolve, reject) => {
37
+ connection.execute({
38
+ sqlText: sql,
39
+ complete: (err, stmt, rows) => {
40
+ if (err) {
41
+ reject(err);
42
+ return { success: false, message: err.message };
43
+ } else {
44
+ resolve({
45
+ rows,
46
+ fields: stmt.getColumns().map((col) => ({
47
+ name: col.getName(),
48
+ dataTypeID: POSTGRES_SNOWFLAKE_MAP[col.getType().toUpperCase()],
49
+ })),
50
+ });
51
+ }
52
+ },
53
+ });
54
+ });
55
+ return results as QuillQueryResults;
56
+ }
57
+
58
+ export async function getSchemasSnowflake(
59
+ connection: snowflake.Connection
60
+ ): Promise<string[]> {
61
+ const sql = `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA
62
+ WHERE SCHEMA_NAME != 'INFORMATION_SCHEMA'`;
63
+ const results = await runQuerySnowflake(sql, connection);
64
+ return results.rows.map((row) => row.SCHEMA_NAME);
65
+ }
66
+
67
+ export async function getTablesBySchemaSnowflake(
68
+ connection: snowflake.Connection,
69
+ schemaNames: string[]
70
+ ): Promise<{ tableName: string; schemaName: string }[]> {
71
+ const allColumns = await Promise.all(
72
+ schemaNames.map(async (schema) => {
73
+ const query = `SELECT
74
+ TABLE_NAME as "tableName",
75
+ TABLE_SCHEMA as "schemaName"
76
+ FROM INFORMATION_SCHEMA.TABLES
77
+ WHERE TABLE_SCHEMA = '${schema}';
78
+ `;
79
+ const results = await runQuerySnowflake(query, connection);
80
+ return results.rows.map((row) => {
81
+ return { tableName: row.tableName, schemaName: row.schemaName };
82
+ });
83
+ })
84
+ );
85
+ return allColumns.flat();
86
+ }
87
+
88
+ export async function getColumnsByTableSnowflake(
89
+ connection: snowflake.Connection,
90
+ schemaName: string,
91
+ tableName: string
92
+ ): Promise<string[]> {
93
+ const sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '${schemaName}' AND TABLE_NAME = '${tableName}'`;
94
+ const results = await runQuerySnowflake(sql, connection);
95
+ return results.rows.map((row) => row.COLUMN_NAME);
96
+ }
97
+
98
+ export function formatSnowflakeConfig(
99
+ connectionString: string
100
+ ): SnowflakeConnectionConfig {
101
+ const parsed = new URL(connectionString);
102
+ return {
103
+ account: parsed.hostname,
104
+ username: parsed.username,
105
+ password: parsed.password,
106
+ database: parsed.pathname.split("/")[1],
107
+ warehouse: parsed.pathname.split("/")[2],
108
+ };
109
+ }
110
+
111
+ export function connectToSnowflake(
112
+ config: SnowflakeConnectionConfig
113
+ ): snowflake.Connection {
114
+ const connection = snowflake.createConnection(config);
115
+ connection.connect((err) => {
116
+ if (err) {
117
+ console.error(`Failed to connect to Snowflake: ${err.message}`);
118
+ }
119
+ });
120
+ return connection;
121
+ }
122
+
123
+ export async function disconnectFromSnowflake(
124
+ connection: snowflake.Connection
125
+ ) {
126
+ connection.destroy((err, conn) => {
127
+ if (err) {
128
+ console.error(`Failed to disconnect from Snowflake: ${err.message}`);
129
+ }
130
+ });
131
+ }
132
+
133
+ export async function getForeignKeysSnowflake(
134
+ connection: snowflake.Connection,
135
+ schemaName: string,
136
+ tableName: string,
137
+ primaryKey: string
138
+ ): Promise<string[]> {
139
+ let depluralizedTableName = depluralize(tableName);
140
+ let sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
141
+ WHERE TABLE_SCHEMA = '${schemaName}'
142
+ AND TABLE_NAME = '${schemaName}'
143
+ and TABLE_NAME != '${tableName}'
144
+ and (COLUMN_NAME = '${primaryKey}'
145
+ or COLUMN_NAME = '${depluralizedTableName}_${primaryKey}'
146
+ or COLUMN_NAME = '${depluralizedTableName}${capitalize(primaryKey)}')`;
147
+ const results = await runQuerySnowflake(sql, connection);
148
+ let foreignKeysString = results.rows.map((key) => {
149
+ return key.COLUMN_NAME;
150
+ });
151
+ foreignKeysString = foreignKeysString.filter(
152
+ (key) => key !== "id" && key !== "_id_"
153
+ );
154
+ foreignKeysString = [...new Set(foreignKeysString)];
155
+ if (foreignKeysString.length === 0) {
156
+ sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
157
+ WHERE TABLE_SCHEMA = '${schemaName}'
158
+ AND TABLE_NAME = '${schemaName}'
159
+ and TABLE_NAME != '${tableName}'
160
+ and (COLUMN_NAME like '${depluralizedTableName}%'
161
+ or column_name like '%\\_id'
162
+ or column_name like '%Id'
163
+ or COLUMN_NAME like '%\\_${primaryKey}'
164
+ or COLUMN_NAME like '%${capitalize(primaryKey)}')`;
165
+ const results = await runQuerySnowflake(sql, connection);
166
+ foreignKeysString = results.rows.map((key) => {
167
+ return key.COLUMN_NAME;
168
+ });
169
+ foreignKeysString = [...new Set(foreignKeysString)];
170
+ }
171
+ return foreignKeysString;
172
+ }
173
+
174
+ export async function getSchemaColumnInfoSnowflake(
175
+ connection: snowflake.Connection,
176
+ schemaName: string,
177
+ tableNames: { tableName: string; schemaName: string }[]
178
+ ) {
179
+ const allColumns = await Promise.all(
180
+ tableNames.map(async (tableName) => {
181
+ const query = `SELECT
182
+ COLUMN_NAME as "columnName", DATA_TYPE as "dataType"
183
+ FROM INFORMATION_SCHEMA.COLUMNS
184
+ WHERE TABLE_SCHEMA = '${tableName.schemaName}' AND TABLE_NAME = '${tableName.tableName}';
185
+ `;
186
+ const results = await runQuerySnowflake(query, connection);
187
+ return {
188
+ tableName: `${tableName.schemaName}.${tableName.tableName}`,
189
+ displayName: `${tableName.schemaName}.${tableName.tableName}`,
190
+ columns: results.rows.map((row) => {
191
+ const postgresType = POSTGRES_SNOWFLAKE_MAP[row.dataType];
192
+ return {
193
+ columnName: row.columnName,
194
+ displayName: row.columnName,
195
+ dataTypeID: postgresType,
196
+ fieldType: row.dataType,
197
+ };
198
+ }),
199
+ };
200
+ })
201
+ );
202
+ return allColumns;
203
+ }