From 11f2dc002a86ec63142aff582d90e0f331a0dff0 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 8 Oct 2024 21:16:28 +0700 Subject: [PATCH] add mysql connection --- package-lock.json | 142 +++++++++++++++++++- package.json | 1 + src/connections/index.ts | 10 +- src/connections/mysql.ts | 200 ++++++++++++++++++++++++++++ src/index.ts | 1 + src/query-builder/dialects/mysql.ts | 27 +++- 6 files changed, 372 insertions(+), 9 deletions(-) create mode 100644 src/connections/mysql.ts diff --git a/package-lock.json b/package-lock.json index db5eb59..238889a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,10 @@ "@jest/globals": "^29.7.0", "@types/node": "^20.12.12", "@types/ws": "^8.5.10", + "husky": "^9.0.11", "jest": "^29.7.0", "lint-staged": "^15.2.4", + "mysql2": "^3.11.3", "prettier": "^3.2.5", "ts-jest": "^29.1.3", "ts-node": "^10.9.2", @@ -1811,6 +1813,16 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2642,6 +2654,16 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -3131,6 +3153,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3369,11 +3401,27 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, + "devOptional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -3514,6 +3562,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -5213,6 +5268,13 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5222,6 +5284,22 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5578,6 +5656,50 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mysql2": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.3.tgz", + "integrity": "sha512-Qpu2ADfbKzyLdwC/5d4W7+5Yz7yBzCU05YWt5npWzACST37wJsB23wgOSo00qi043urkiRwXtEvJc9UnuLX/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6387,7 +6509,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true + "devOptional": true }, "node_modules/semver": { "version": "6.3.1", @@ -6398,6 +6520,12 @@ "semver": "bin/semver.js" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "dev": true + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -6534,6 +6662,16 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ssri": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", diff --git a/package.json b/package.json index f7c1dac..1faf3f9 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "husky": "^9.0.11", "jest": "^29.7.0", "lint-staged": "^15.2.4", + "mysql2": "^3.11.3", "prettier": "^3.2.5", "ts-jest": "^29.1.3", "ts-node": "^10.9.2", diff --git a/src/connections/index.ts b/src/connections/index.ts index 87ad937..5838cb9 100644 --- a/src/connections/index.ts +++ b/src/connections/index.ts @@ -9,6 +9,12 @@ export type OperationResponse = { error: Error | null } +export interface QueryResult> { + data: T[] + error: Error | null + query: string +} + export interface Connection { queryType: QueryType dialect: AbstractDialect @@ -18,9 +24,9 @@ export interface Connection { disconnect: () => Promise // Raw query execution method that can be used to execute any query. - query: ( + query: >( query: Query - ) => Promise<{ data: any; error: Error | null; query: string }> + ) => Promise> // Retrieve metadata about the database, useful for introspection. fetchDatabaseSchema?: () => Promise diff --git a/src/connections/mysql.ts b/src/connections/mysql.ts new file mode 100644 index 0000000..6222517 --- /dev/null +++ b/src/connections/mysql.ts @@ -0,0 +1,200 @@ +import { type Connection, type RowDataPacket } from 'mysql2' +import { Connection as BaseConnection, QueryResult } from '.' +import { constructRawQuery, Query } from '../query' +import { QueryType } from '../query-params' +import { Constraint, Database, Table, TableColumn } from './../models/database' +import { MySQLDialect } from './../query-builder/dialects/mysql' + +export class MySQLConnection implements BaseConnection { + protected conn: Connection + public dialect = new MySQLDialect() + queryType = QueryType.positional + + constructor(conn: Connection) { + this.conn = conn + } + + async query>( + query: Query + ): Promise> { + const rows = await new Promise((r) => + this.conn.query(query.query, query.parameters, (_, result) => { + r((result as RowDataPacket[]) ?? []) + }) + ) + + return { + data: rows.map((r) => ({ ...r }) as T), + error: null, + query: constructRawQuery(query), + } + } + + async fetchDatabaseSchema(): Promise { + // Get all the schema list + const { data: schemaList } = await this.query<{ SCHEMA_NAME: string }>({ + query: "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')", + }) + + const result = schemaList.reduce((db, schema) => { + db[schema.SCHEMA_NAME] = {} + return db + }, {}) + + // Get table list + const tableLookup: Record = {} + const { data: tableList } = await this.query<{ + TABLE_SCHEMA: string + TABLE_NAME: string + }>({ + query: "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')", + }) + + for (const table of tableList) { + if (!result[table.TABLE_SCHEMA]) { + result[table.TABLE_SCHEMA] = {} + } + + if (!result[table.TABLE_SCHEMA][table.TABLE_NAME]) { + const tableObject = { + name: table.TABLE_NAME, + columns: [], + indexes: [], + constraints: [], + } + + tableLookup[table.TABLE_SCHEMA + '.' + table.TABLE_NAME] = + tableObject + result[table.TABLE_SCHEMA][table.TABLE_NAME] = tableObject + } + } + + // Get column list + const columnLookup: Record = {} + const { data: columnList } = await this.query<{ + TABLE_SCHEMA: string + TABLE_NAME: string + COLUMN_NAME: string + COLUMN_TYPE: string + ORDINAL_POSITION: number + IS_NULLABLE: string + COLUMN_DEFAULT: string | null + COLUMN_KEY: string + EXTRA: string + }>({ + query: "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, ORDINAL_POSITION, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA FROM information_schema.columns WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')", + }) + + for (const column of columnList) { + const table = + tableLookup[column.TABLE_SCHEMA + '.' + column.TABLE_NAME] + + if (!table) continue + + const columnObject = { + name: column.COLUMN_NAME, + type: column.COLUMN_TYPE, + position: column.ORDINAL_POSITION, + nullable: column.IS_NULLABLE === 'YES', + default: column.COLUMN_DEFAULT, + primary: column.COLUMN_KEY === 'PRI', + unique: column.EXTRA === 'UNI', + references: [], + } + + columnLookup[ + column.TABLE_SCHEMA + + '.' + + column.TABLE_NAME + + '.' + + column.COLUMN_NAME + ] = columnObject + + table.columns.push(columnObject) + } + + // Get constraints list + const constraintLookup: Record = {} + const { data: constraintsList } = await this.query<{ + TABLE_SCHEMA: string + TABLE_NAME: string + CONSTRAINT_NAME: string + CONSTRAINT_TYPE: string + }>({ + query: `SELECT TABLE_SCHEMA, TABLE_NAME, CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.table_constraints WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') AND CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE', 'FOREIGN KEY')`, + }) + + for (const constraint of constraintsList) { + const table = + tableLookup[ + constraint.TABLE_SCHEMA + '.' + constraint.TABLE_NAME + ] + + if (!table) continue + + const constraintObject = { + name: constraint.CONSTRAINT_NAME, + schema: constraint.TABLE_SCHEMA, + tableName: constraint.TABLE_NAME, + type: constraint.CONSTRAINT_TYPE, + columns: [], + } + + constraintLookup[ + constraint.TABLE_SCHEMA + '.' + constraint.CONSTRAINT_NAME + ] = constraintObject + + table.constraints.push(constraintObject) + } + + // Get constraint columns + const { data: constraintColumnsList } = await this.query<{ + TABLE_SCHEMA: string + TABLE_NAME: string + COLUMN_NAME: string + CONSTRAINT_NAME: string + REFERENCED_TABLE_SCHEMA: string + REFERENCED_TABLE_NAME: string + REFERENCED_COLUMN_NAME: string + }>({ + query: `SELECT CONSTRAINT_NAME, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_SCHEMA, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.key_column_usage WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')`, + }) + + for (const constraintColumn of constraintColumnsList) { + const constraint = + constraintLookup[ + constraintColumn.TABLE_SCHEMA + + '.' + + constraintColumn.CONSTRAINT_NAME + ] + + if (!constraint) continue + + const currentColumn = + columnLookup[ + constraintColumn.TABLE_SCHEMA + + '.' + + constraintColumn.TABLE_NAME + + '.' + + constraintColumn.COLUMN_NAME + ] + if (currentColumn && constraintColumn.REFERENCED_COLUMN_NAME) { + currentColumn.references.push({ + table: constraintColumn.REFERENCED_TABLE_NAME, + column: constraintColumn.REFERENCED_COLUMN_NAME, + }) + } + + constraint.columns.push({ + columnName: constraintColumn.COLUMN_NAME, + }) + } + + return result + } + + async connect(): Promise {} + async disconnect(): Promise { + this.conn.destroy() + } +} diff --git a/src/index.ts b/src/index.ts index d17576c..44f3318 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from './connections/motherduck' export * from './connections/bigquery' export * from './connections/starbase' export * from './connections/mongodb' +export * from './connections/mysql' export * from './client' export * from './models' export * from './models/decorators' diff --git a/src/query-builder/dialects/mysql.ts b/src/query-builder/dialects/mysql.ts index ea1c01c..472e31a 100644 --- a/src/query-builder/dialects/mysql.ts +++ b/src/query-builder/dialects/mysql.ts @@ -1,14 +1,31 @@ -import { AbstractDialect, ColumnDataType } from '../index'; +import { AbstractDialect, ColumnDataType } from '../index' export class MySQLDialect extends AbstractDialect { + formatSchemaAndTable(schema: string | undefined, table: string): string { + if (schema) { + return `\`${schema}\`.\`${table}\`` + } + return `\`${table}\`` + } + + formatFromSchemaAndTable( + schema: string | undefined, + table: string + ): string { + if (schema) { + return `\`${schema}\`.\`${table}\`` + } + return `\`${table}\`` + } + mapDataType(dataType: ColumnDataType): string { switch (dataType.toLowerCase()) { case ColumnDataType.STRING: - return 'VARCHAR(255)'; + return 'VARCHAR(255)' case ColumnDataType.BOOLEAN: - return 'TINYINT(1)'; + return 'TINYINT(1)' default: - return super.mapDataType(dataType); + return super.mapDataType(dataType) } } -} \ No newline at end of file +}