├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── publish-to-nest.land.yml │ └── wait-for-mysql.sh ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── cipher ├── deps.ts ├── egg.json ├── mod.ts ├── package.json ├── src ├── auth.ts ├── auth_plugin │ ├── caching_sha2_password.ts │ ├── crypt.ts │ └── index.ts ├── buffer.ts ├── client.ts ├── connection.ts ├── constant │ ├── capabilities.ts │ ├── charset.ts │ ├── errors.ts │ ├── mysql_types.ts │ ├── packet.ts │ └── server_status.ts ├── deferred.ts ├── logger.ts ├── packets │ ├── builders │ │ ├── auth.ts │ │ ├── client_capabilities.ts │ │ ├── query.ts │ │ └── tls.ts │ ├── packet.ts │ └── parsers │ │ ├── authswitch.ts │ │ ├── err.ts │ │ ├── handshake.ts │ │ └── result.ts ├── pool.ts └── util.ts ├── test.deps.ts ├── test.ts └── test.util.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [manyuanrong] 2 | ko_fi: manyuanrong 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | fmt: 7 | runs-on: ubuntu-latest 8 | continue-on-error: true 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Install Deno 1.x 12 | uses: denoland/setup-deno@v1 13 | with: 14 | deno-version: v1.x 15 | - name: Check fmt 16 | run: deno fmt --check 17 | test: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | DENO_VERSION: 23 | - v1.x 24 | DB_VERSION: 25 | - mysql:5.5 26 | - mysql:5.6 27 | - mysql:5.7 28 | - mysql:8 29 | - mysql:latest 30 | - mariadb:5.5 31 | - mariadb:10.0 32 | - mariadb:10.1 33 | - mariadb:10.2 34 | - mariadb:10.3 35 | - mariadb:10.4 36 | # - mariadb:latest 37 | 38 | steps: 39 | - uses: actions/checkout@v1 40 | - name: Install Deno ${{ matrix.DENO_VERSION }} 41 | uses: denoland/setup-deno@v1 42 | with: 43 | deno-version: ${{ matrix.DENO_VERSION }} 44 | - name: Show Deno version 45 | run: deno --version 46 | - name: Start ${{ matrix.DB_VERSION }} 47 | run: | 48 | sudo mkdir -p /var/run/mysqld/tmp 49 | sudo chmod -R 777 /var/run/mysqld 50 | docker container run --name mysql --rm -d -p 3306:3306 \ 51 | -v /var/run/mysqld:/var/run/mysqld \ 52 | -v /var/run/mysqld/tmp:/tmp \ 53 | -e MYSQL_ROOT_PASSWORD=root \ 54 | ${{ matrix.DB_VERSION }} 55 | ./.github/workflows/wait-for-mysql.sh 56 | - name: Run tests (TCP) 57 | run: | 58 | deno test --allow-env --allow-net=127.0.0.1:3306 ./test.ts 59 | - name: Run tests (--unstable) (UNIX domain socket) 60 | run: | 61 | SOCKPATH=/var/run/mysqld/mysqld.sock 62 | if [[ "${{ matrix.DB_VERSION }}" == "mysql:5.5" ]]; then 63 | SOCKPATH=/var/run/mysqld/tmp/mysql.sock 64 | fi 65 | echo "DROP USER 'root'@'localhost';" | docker exec -i mysql mysql -proot 66 | DB_SOCKPATH=$SOCKPATH TEST_METHODS=unix \ 67 | deno test --unstable --allow-env \ 68 | --allow-read=/var/run/mysqld/ --allow-write=/var/run/mysqld/ \ 69 | ./test.ts 70 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-nest.land.yml: -------------------------------------------------------------------------------- 1 | name: "publish current release to https://nest.land" 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | publishToNestDotLand: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Setup repo 14 | uses: actions/checkout@v2 15 | 16 | - name: "setup" # check: https://github.com/actions/virtual-environments/issues/1777 17 | uses: denolib/setup-deno@v2 18 | with: 19 | deno-version: v1.4.6 20 | 21 | - name: "check nest.land" 22 | run: | 23 | deno run --allow-net --allow-read --allow-run https://deno.land/x/cicd/publish-on-nest.land.ts ${{ secrets.GITHUB_TOKEN }} ${{ secrets.NESTAPIKEY }} ${{ github.repository }} 24 | -------------------------------------------------------------------------------- /.github/workflows/wait-for-mysql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Waiting for MySQL" 4 | for i in `seq 1 30`; 5 | do 6 | echo '\q' | mysql -h 127.0.0.1 -uroot --password=root -P 3306 && exit 0 7 | >&2 echo "MySQL is waking up" 8 | sleep 1 9 | done 10 | 11 | echo "Failed waiting for MySQL" && exit 1 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | tsconfig.json 3 | node_modules 4 | mysql.log 5 | docs 6 | .DS_Store 7 | .idea 8 | 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 EnokMan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno_mysql 2 | 3 | [![Build Status](https://github.com/manyuanrong/deno_mysql/workflows/ci/badge.svg?branch=master)](https://github.com/manyuanrong/deno_mysql/actions) 4 | ![GitHub](https://img.shields.io/github/license/manyuanrong/deno_mysql.svg) 5 | ![GitHub release](https://img.shields.io/github/release/manyuanrong/deno_mysql.svg) 6 | ![(Deno)](https://img.shields.io/badge/deno-1.0.0-green.svg) 7 | 8 | MySQL and MariaDB database driver for Deno. 9 | 10 | On this basis, there is also an ORM library: 11 | [Deno Simple Orm](https://github.com/manyuanrong/dso) 12 | 13 | 欢迎国内的小伙伴加我专门建的 Deno QQ 交流群:698469316 14 | 15 | ## API 16 | 17 | ### connect 18 | 19 | ```ts 20 | import { Client } from "https://deno.land/x/mysql/mod.ts"; 21 | const client = await new Client().connect({ 22 | hostname: "127.0.0.1", 23 | username: "root", 24 | db: "dbname", 25 | password: "password", 26 | }); 27 | ``` 28 | 29 | ### connect pool 30 | 31 | Create client with connection pool. 32 | 33 | pool size is auto increment from 0 to `poolSize` 34 | 35 | ```ts 36 | import { Client } from "https://deno.land/x/mysql/mod.ts"; 37 | const client = await new Client().connect({ 38 | hostname: "127.0.0.1", 39 | username: "root", 40 | db: "dbname", 41 | poolSize: 3, // connection limit 42 | password: "password", 43 | }); 44 | ``` 45 | 46 | ### create database 47 | 48 | ```ts 49 | await client.execute(`CREATE DATABASE IF NOT EXISTS enok`); 50 | await client.execute(`USE enok`); 51 | ``` 52 | 53 | ### create table 54 | 55 | ```ts 56 | await client.execute(`DROP TABLE IF EXISTS users`); 57 | await client.execute(` 58 | CREATE TABLE users ( 59 | id int(11) NOT NULL AUTO_INCREMENT, 60 | name varchar(100) NOT NULL, 61 | created_at timestamp not null default current_timestamp, 62 | PRIMARY KEY (id) 63 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 64 | `); 65 | ``` 66 | 67 | ### insert 68 | 69 | ```ts 70 | let result = await client.execute(`INSERT INTO users(name) values(?)`, [ 71 | "manyuanrong", 72 | ]); 73 | console.log(result); 74 | // { affectedRows: 1, lastInsertId: 1 } 75 | ``` 76 | 77 | ### update 78 | 79 | ```ts 80 | let result = await client.execute(`update users set ?? = ?`, ["name", "MYR"]); 81 | console.log(result); 82 | // { affectedRows: 1, lastInsertId: 0 } 83 | ``` 84 | 85 | ### delete 86 | 87 | ```ts 88 | let result = await client.execute(`delete from users where ?? = ?`, ["id", 1]); 89 | console.log(result); 90 | // { affectedRows: 1, lastInsertId: 0 } 91 | ``` 92 | 93 | ### query 94 | 95 | ```ts 96 | const username = "manyuanrong"; 97 | const users = await client.query(`select * from users`); 98 | const queryWithParams = await client.query( 99 | "select ??,name from ?? where id = ?", 100 | ["id", "users", 1], 101 | ); 102 | console.log(users, queryWithParams); 103 | ``` 104 | 105 | ### execute 106 | 107 | There are two ways to execute an SQL statement. 108 | 109 | First and default one will return you an `rows` key containing an array of rows: 110 | 111 | ```ts 112 | const { rows: users } = await client.execute(`select * from users`); 113 | console.log(users); 114 | ``` 115 | 116 | The second one will return you an `iterator` key containing an 117 | `[Symbol.asyncIterator]` property: 118 | 119 | ```ts 120 | await client.useConnection(async (conn) => { 121 | // note the third parameter of execute() method. 122 | const { iterator: users } = await conn.execute( 123 | `select * from users`, 124 | /* params: */ [], 125 | /* iterator: */ true, 126 | ); 127 | for await (const user of users) { 128 | console.log(user); 129 | } 130 | }); 131 | ``` 132 | 133 | The second method is recommended only for SELECT queries that might contain many 134 | results (e.g. 100k rows). 135 | 136 | ### transaction 137 | 138 | ```ts 139 | const users = await client.transaction(async (conn) => { 140 | await conn.execute(`insert into users(name) values(?)`, ["test"]); 141 | return await conn.query(`select ?? from ??`, ["name", "users"]); 142 | }); 143 | console.log(users.length); 144 | ``` 145 | 146 | ### TLS 147 | 148 | TLS configuration: 149 | 150 | - caCerts([]string): A list of root certificates (must be PEM format) that will 151 | be used in addition to the default root certificates to verify the peer's 152 | certificate. 153 | - mode(string): The TLS mode to use. Valid values are "disabled", 154 | "verify_identity". Defaults to "disabled". 155 | 156 | You usually need not specify the caCert, unless the certificate is not included 157 | in the default root certificates. 158 | 159 | ```ts 160 | import { Client, TLSConfig, TLSMode } from "https://deno.land/x/mysql/mod.ts"; 161 | const tlsConfig: TLSConfig = { 162 | mode: TLSMode.VERIFY_IDENTITY, 163 | caCerts: [ 164 | await Deno.readTextFile("capath"), 165 | ], 166 | }; 167 | const client = await new Client().connect({ 168 | hostname: "127.0.0.1", 169 | username: "root", 170 | db: "dbname", 171 | password: "password", 172 | tls: tlsConfig, 173 | }); 174 | ``` 175 | 176 | ### close 177 | 178 | ```ts 179 | await client.close(); 180 | ``` 181 | 182 | ## Logging 183 | 184 | The driver logs to the console by default. 185 | 186 | To disable logging: 187 | 188 | ```ts 189 | import { configLogger } from "https://deno.land/x/mysql/mod.ts"; 190 | await configLogger({ enable: false }); 191 | ``` 192 | 193 | ## Test 194 | 195 | The tests require a database to run against. 196 | 197 | ```bash 198 | docker container run --rm -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true docker.io/mariadb:latest 199 | deno test --allow-env --allow-net=127.0.0.1:3306 ./test.ts 200 | ``` 201 | 202 | Use different docker images to test against different versions of MySQL and 203 | MariaDB. Please see [ci.yml](./.github/workflows/ci.yml) for examples. 204 | -------------------------------------------------------------------------------- /cipher: -------------------------------------------------------------------------------- 1 | LãX¼q¥kï¢zƒH²‘»±_¨Ü'Ó¿sH¦Æ€§ÇÿÂMH¾nü~JYÀµÏž\ôق"ÓYâ¼Ù6Ú_ñÊ%6e™d®ˆ£tׯ›Ÿ(T¸êm_y¾[¼mé/ËM <åó£›x²š.€o™Õ ˆ& "x%’H±ÖW ’8z1×.Sß>¢½§üªØûCÄU®½ÉÇØw*â:Å"ãUþg‡eö4…V³G1›&­åŒïÉiŠ6`õÓ‹ØÀïè;U©ºp–Ä{K Ùΐ䈼ºÑ”‚öx50F&w êfâÙ"{sQ¾5¾„Ý7·«¢¤ -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export type { Deferred } from "https://deno.land/std@0.104.0/async/mod.ts"; 2 | export { deferred, delay } from "https://deno.land/std@0.104.0/async/mod.ts"; 3 | export { format as byteFormat } from "https://deno.land/x/bytes_formater@v1.4.0/mod.ts"; 4 | export { createHash } from "https://deno.land/std@0.104.0/hash/mod.ts"; 5 | export { decode as base64Decode } from "https://deno.land/std@0.104.0/encoding/base64.ts"; 6 | export type { 7 | SupportedAlgorithm, 8 | } from "https://deno.land/std@0.104.0/hash/mod.ts"; 9 | export { replaceParams } from "https://deno.land/x/sql_builder@v1.9.1/util.ts"; 10 | export * as log from "https://deno.land/std@0.104.0/log/mod.ts"; 11 | -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql", 3 | "description": "MySQL driver for Deno", 4 | "homepage": "https://github.com/manyuanrong/deno_mysql", 5 | "files": [ 6 | "./**/*.ts", 7 | "README.md" 8 | ], 9 | "entry": "./mod.ts" 10 | } 11 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export type { ClientConfig } from "./src/client.ts"; 2 | export { Client } from "./src/client.ts"; 3 | export type { TLSConfig } from "./src/client.ts"; 4 | export { TLSMode } from "./src/client.ts"; 5 | 6 | export type { ExecuteResult } from "./src/connection.ts"; 7 | export { Connection } from "./src/connection.ts"; 8 | 9 | export type { LoggerConfig } from "./src/logger.ts"; 10 | export { configLogger } from "./src/logger.ts"; 11 | 12 | export { log } from "./deps.ts"; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deno_mysql", 3 | "version": "1.0.0", 4 | "description": "[![Build Status](https://www.travis-ci.org/manyuanrong/deno_mysql.svg?branch=master)](https://www.travis-ci.org/manyuanrong/deno_mysql)", 5 | "main": "index.js", 6 | "scripts": { 7 | "docs": "typedoc --theme minimal --ignoreCompilerErrors --excludePrivate --excludeExternals --entryPoint client.ts --mode file ./src --out ./docs" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/manyuanrong/deno_mysql.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/manyuanrong/deno_mysql/issues" 18 | }, 19 | "homepage": "https://github.com/manyuanrong/deno_mysql#readme", 20 | "devDependencies": { 21 | "typedoc": "^0.14.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import { createHash, SupportedAlgorithm } from "../deps.ts"; 2 | import { xor } from "./util.ts"; 3 | import { encode } from "./buffer.ts"; 4 | 5 | function hash(algorithm: SupportedAlgorithm, data: Uint8Array): Uint8Array { 6 | return new Uint8Array(createHash(algorithm).update(data).digest()); 7 | } 8 | 9 | function mysqlNativePassword(password: string, seed: Uint8Array): Uint8Array { 10 | const pwd1 = hash("sha1", encode(password)); 11 | const pwd2 = hash("sha1", pwd1); 12 | 13 | let seedAndPwd2 = new Uint8Array(seed.length + pwd2.length); 14 | seedAndPwd2.set(seed); 15 | seedAndPwd2.set(pwd2, seed.length); 16 | seedAndPwd2 = hash("sha1", seedAndPwd2); 17 | 18 | return xor(seedAndPwd2, pwd1); 19 | } 20 | 21 | function cachingSha2Password(password: string, seed: Uint8Array): Uint8Array { 22 | const stage1 = hash("sha256", encode(password)); 23 | const stage2 = hash("sha256", stage1); 24 | const stage3 = hash("sha256", Uint8Array.from([...stage2, ...seed])); 25 | return xor(stage1, stage3); 26 | } 27 | 28 | export default function auth( 29 | authPluginName: string, 30 | password: string, 31 | seed: Uint8Array, 32 | ) { 33 | switch (authPluginName) { 34 | case "mysql_native_password": 35 | // Native password authentication only need and will need 20-byte challenge. 36 | return mysqlNativePassword(password, seed.slice(0, 20)); 37 | 38 | case "caching_sha2_password": 39 | return cachingSha2Password(password, seed); 40 | default: 41 | throw new Error("Not supported"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/auth_plugin/caching_sha2_password.ts: -------------------------------------------------------------------------------- 1 | import { xor } from "../util.ts"; 2 | import { ReceivePacket } from "../packets/packet.ts"; 3 | import { encryptWithPublicKey } from "./crypt.ts"; 4 | 5 | interface handler { 6 | done: boolean; 7 | quickRead?: boolean; 8 | next?: (packet: ReceivePacket) => any; 9 | data?: Uint8Array; 10 | } 11 | 12 | let scramble: Uint8Array, password: string; 13 | 14 | async function start( 15 | scramble_: Uint8Array, 16 | password_: string, 17 | ): Promise { 18 | scramble = scramble_; 19 | password = password_; 20 | return { done: false, next: authMoreResponse }; 21 | } 22 | 23 | async function authMoreResponse(packet: ReceivePacket): Promise { 24 | const enum AuthStatusFlags { 25 | FullAuth = 0x04, 26 | FastPath = 0x03, 27 | } 28 | const REQUEST_PUBLIC_KEY = 0x02; 29 | const statusFlag = packet.body.skip(1).readUint8(); 30 | let authMoreData, done = true, next, quickRead = false; 31 | if (statusFlag === AuthStatusFlags.FullAuth) { 32 | authMoreData = new Uint8Array([REQUEST_PUBLIC_KEY]); 33 | done = false; 34 | next = encryptWithKey; 35 | } 36 | if (statusFlag === AuthStatusFlags.FastPath) { 37 | done = false; 38 | quickRead = true; 39 | next = terminate; 40 | } 41 | return { done, next, quickRead, data: authMoreData }; 42 | } 43 | 44 | async function encryptWithKey(packet: ReceivePacket): Promise { 45 | const publicKey = parsePublicKey(packet); 46 | const len = password.length; 47 | const passwordBuffer: Uint8Array = new Uint8Array(len + 1); 48 | for (let n = 0; n < len; n++) { 49 | passwordBuffer[n] = password.charCodeAt(n); 50 | } 51 | passwordBuffer[len] = 0x00; 52 | 53 | const encryptedPassword = await encrypt(passwordBuffer, scramble, publicKey); 54 | return { 55 | done: false, 56 | next: terminate, 57 | data: new Uint8Array(encryptedPassword), 58 | }; 59 | } 60 | 61 | function parsePublicKey(packet: ReceivePacket): string { 62 | return packet.body.skip(1).readNullTerminatedString(); 63 | } 64 | 65 | async function encrypt( 66 | password: Uint8Array, 67 | scramble: Uint8Array, 68 | key: string, 69 | ): Promise { 70 | const stage1 = xor(password, scramble); 71 | return await encryptWithPublicKey(key, stage1); 72 | } 73 | 74 | function terminate() { 75 | return { done: true }; 76 | } 77 | 78 | export { start }; 79 | -------------------------------------------------------------------------------- /src/auth_plugin/crypt.ts: -------------------------------------------------------------------------------- 1 | import { base64Decode } from "../../deps.ts"; 2 | 3 | async function encryptWithPublicKey( 4 | key: string, 5 | data: Uint8Array, 6 | ): Promise { 7 | const pemHeader = "-----BEGIN PUBLIC KEY-----\n"; 8 | const pemFooter = "\n-----END PUBLIC KEY-----"; 9 | key = key.trim(); 10 | key = key.substring(pemHeader.length, key.length - pemFooter.length); 11 | const importedKey = await crypto.subtle.importKey( 12 | "spki", 13 | base64Decode(key), 14 | { name: "RSA-OAEP", hash: "SHA-256" }, 15 | false, 16 | ["encrypt"], 17 | ); 18 | 19 | return await crypto.subtle.encrypt( 20 | { 21 | name: "RSA-OAEP", 22 | }, 23 | importedKey, 24 | data, 25 | ); 26 | } 27 | 28 | export { encryptWithPublicKey }; 29 | -------------------------------------------------------------------------------- /src/auth_plugin/index.ts: -------------------------------------------------------------------------------- 1 | import * as caching_sha2_password from "./caching_sha2_password.ts"; 2 | export default { 3 | caching_sha2_password, 4 | }; 5 | -------------------------------------------------------------------------------- /src/buffer.ts: -------------------------------------------------------------------------------- 1 | const encoder = new TextEncoder(); 2 | const decoder = new TextDecoder(); 3 | 4 | /** @ignore */ 5 | export function encode(input: string) { 6 | return encoder.encode(input); 7 | } 8 | 9 | /** @ignore */ 10 | export function decode(input: BufferSource) { 11 | return decoder.decode(input); 12 | } 13 | 14 | /** @ignore */ 15 | export class BufferReader { 16 | private pos: number = 0; 17 | constructor(readonly buffer: Uint8Array) {} 18 | 19 | get finished(): boolean { 20 | return this.pos >= this.buffer.length; 21 | } 22 | 23 | skip(len: number): BufferReader { 24 | this.pos += len; 25 | return this; 26 | } 27 | 28 | readBuffer(len: number): Uint8Array { 29 | const buffer = this.buffer.slice(this.pos, this.pos + len); 30 | this.pos += len; 31 | return buffer; 32 | } 33 | 34 | readUints(len: number): number { 35 | let num = 0; 36 | for (let n = 0; n < len; n++) { 37 | num += this.buffer[this.pos++] << (8 * n); 38 | } 39 | return num; 40 | } 41 | 42 | readUint8(): number { 43 | return this.buffer[this.pos++]; 44 | } 45 | 46 | readUint16(): number { 47 | return this.readUints(2); 48 | } 49 | 50 | readUint32(): number { 51 | return this.readUints(4); 52 | } 53 | 54 | readUint64(): number { 55 | return this.readUints(8); 56 | } 57 | 58 | readNullTerminatedString(): string { 59 | let end = this.buffer.indexOf(0x00, this.pos); 60 | if (end === -1) end = this.buffer.length; 61 | const buf = this.buffer.slice(this.pos, end); 62 | this.pos += buf.length + 1; 63 | return decode(buf); 64 | } 65 | 66 | readRestOfPacketString(): Uint8Array { 67 | return this.buffer.slice(this.pos); 68 | } 69 | 70 | readString(len: number): string { 71 | const str = decode(this.buffer.slice(this.pos, this.pos + len)); 72 | this.pos += len; 73 | return str; 74 | } 75 | 76 | readEncodedLen(): number { 77 | const first = this.readUint8(); 78 | if (first < 251) { 79 | return first; 80 | } else { 81 | if (first == 0xfc) { 82 | return this.readUint16(); 83 | } else if (first == 0xfd) { 84 | return this.readUints(3); 85 | } else if (first == 0xfe) { 86 | return this.readUints(8); 87 | } 88 | } 89 | return -1; 90 | } 91 | 92 | readLenCodeString(): string | null { 93 | const len = this.readEncodedLen(); 94 | if (len == -1) return null; 95 | return this.readString(len); 96 | } 97 | } 98 | 99 | /** @ignore */ 100 | export class BufferWriter { 101 | private pos: number = 0; 102 | constructor(readonly buffer: Uint8Array) {} 103 | 104 | get wroteData(): Uint8Array { 105 | return this.buffer.slice(0, this.pos); 106 | } 107 | 108 | get length(): number { 109 | return this.pos; 110 | } 111 | 112 | get capacity(): number { 113 | return this.buffer.length - this.pos; 114 | } 115 | 116 | skip(len: number): BufferWriter { 117 | this.pos += len; 118 | return this; 119 | } 120 | 121 | writeBuffer(buffer: Uint8Array): BufferWriter { 122 | if (buffer.length > this.capacity) { 123 | buffer = buffer.slice(0, this.capacity); 124 | } 125 | this.buffer.set(buffer, this.pos); 126 | this.pos += buffer.length; 127 | return this; 128 | } 129 | 130 | write(byte: number): BufferWriter { 131 | this.buffer[this.pos++] = byte; 132 | return this; 133 | } 134 | 135 | writeInt16LE(num: number) {} 136 | 137 | writeIntLE(num: number, len: number) { 138 | const int = new Int32Array(1); 139 | int[0] = 40; 140 | console.log(int); 141 | } 142 | 143 | writeUint16(num: number): BufferWriter { 144 | return this.writeUints(2, num); 145 | } 146 | 147 | writeUint32(num: number): BufferWriter { 148 | return this.writeUints(4, num); 149 | } 150 | 151 | writeUint64(num: number): BufferWriter { 152 | return this.writeUints(8, num); 153 | } 154 | 155 | writeUints(len: number, num: number): BufferWriter { 156 | for (let n = 0; n < len; n++) { 157 | this.buffer[this.pos++] = (num >> (n * 8)) & 0xff; 158 | } 159 | return this; 160 | } 161 | 162 | writeNullTerminatedString(str: string): BufferWriter { 163 | return this.writeString(str).write(0x00); 164 | } 165 | 166 | writeString(str: string): BufferWriter { 167 | const buf = encode(str); 168 | this.buffer.set(buf, this.pos); 169 | this.pos += buf.length; 170 | return this; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { Connection, ConnectionState, ExecuteResult } from "./connection.ts"; 2 | import { ConnectionPool, PoolConnection } from "./pool.ts"; 3 | import { log } from "./logger.ts"; 4 | 5 | /** 6 | * Client Config 7 | */ 8 | export interface ClientConfig { 9 | /** Database hostname */ 10 | hostname?: string; 11 | /** Database UNIX domain socket path. When used, `hostname` and `port` are ignored. */ 12 | socketPath?: string; 13 | /** Database username */ 14 | username?: string; 15 | /** Database password */ 16 | password?: string; 17 | /** Database port */ 18 | port?: number; 19 | /** Database name */ 20 | db?: string; 21 | /** Whether to display packet debugging information */ 22 | debug?: boolean; 23 | /** Connection read timeout (default: 30 seconds) */ 24 | timeout?: number; 25 | /** Connection pool size (default: 1) */ 26 | poolSize?: number; 27 | /** Connection pool idle timeout in microseconds (default: 4 hours) */ 28 | idleTimeout?: number; 29 | /** charset */ 30 | charset?: string; 31 | /** tls config */ 32 | tls?: TLSConfig; 33 | } 34 | 35 | export enum TLSMode { 36 | DISABLED = "disabled", 37 | VERIFY_IDENTITY = "verify_identity", 38 | } 39 | /** 40 | * TLS Config 41 | */ 42 | export interface TLSConfig { 43 | /** mode of tls. only support disabled and verify_identity now*/ 44 | mode?: TLSMode; 45 | /** A list of root certificates (must be PEM format) that will be used in addition to the 46 | * default root certificates to verify the peer's certificate. */ 47 | caCerts?: string[]; 48 | } 49 | 50 | /** Transaction processor */ 51 | export interface TransactionProcessor { 52 | (connection: Connection): Promise; 53 | } 54 | 55 | /** 56 | * MySQL client 57 | */ 58 | export class Client { 59 | config: ClientConfig = {}; 60 | private _pool?: ConnectionPool; 61 | 62 | private async createConnection(): Promise { 63 | let connection = new PoolConnection(this.config); 64 | await connection.connect(); 65 | return connection; 66 | } 67 | 68 | /** get pool info */ 69 | get pool() { 70 | return this._pool?.info; 71 | } 72 | 73 | /** 74 | * connect to database 75 | * @param config config for client 76 | * @returns Client instance 77 | */ 78 | async connect(config: ClientConfig): Promise { 79 | this.config = { 80 | hostname: "127.0.0.1", 81 | username: "root", 82 | port: 3306, 83 | poolSize: 1, 84 | timeout: 30 * 1000, 85 | idleTimeout: 4 * 3600 * 1000, 86 | ...config, 87 | }; 88 | Object.freeze(this.config); 89 | this._pool = new ConnectionPool( 90 | this.config.poolSize || 10, 91 | this.createConnection.bind(this), 92 | ); 93 | return this; 94 | } 95 | 96 | /** 97 | * execute query sql 98 | * @param sql query sql string 99 | * @param params query params 100 | */ 101 | async query(sql: string, params?: any[]): Promise { 102 | return await this.useConnection(async (connection) => { 103 | return await connection.query(sql, params); 104 | }); 105 | } 106 | 107 | /** 108 | * execute sql 109 | * @param sql sql string 110 | * @param params query params 111 | */ 112 | async execute(sql: string, params?: any[]): Promise { 113 | return await this.useConnection(async (connection) => { 114 | return await connection.execute(sql, params); 115 | }); 116 | } 117 | 118 | async useConnection(fn: (conn: Connection) => Promise) { 119 | if (!this._pool) { 120 | throw new Error("Unconnected"); 121 | } 122 | const connection = await this._pool.pop(); 123 | try { 124 | return await fn(connection); 125 | } finally { 126 | if (connection.state == ConnectionState.CLOSED) { 127 | connection.removeFromPool(); 128 | } else { 129 | connection.returnToPool(); 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Execute a transaction process, and the transaction successfully 136 | * returns the return value of the transaction process 137 | * @param processor transation processor 138 | */ 139 | async transaction(processor: TransactionProcessor): Promise { 140 | return await this.useConnection(async (connection) => { 141 | try { 142 | await connection.execute("BEGIN"); 143 | const result = await processor(connection); 144 | await connection.execute("COMMIT"); 145 | return result; 146 | } catch (error) { 147 | if (connection.state == ConnectionState.CONNECTED) { 148 | log.info(`ROLLBACK: ${error.message}`); 149 | await connection.execute("ROLLBACK"); 150 | } 151 | throw error; 152 | } 153 | }); 154 | } 155 | 156 | /** 157 | * close connection 158 | */ 159 | async close() { 160 | if (this._pool) { 161 | this._pool.close(); 162 | this._pool = undefined; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/connection.ts: -------------------------------------------------------------------------------- 1 | import { ClientConfig, TLSMode } from "./client.ts"; 2 | import { 3 | ConnnectionError, 4 | ProtocolError, 5 | ReadError, 6 | ResponseTimeoutError, 7 | } from "./constant/errors.ts"; 8 | import { log } from "./logger.ts"; 9 | import { buildAuth } from "./packets/builders/auth.ts"; 10 | import { buildQuery } from "./packets/builders/query.ts"; 11 | import { ReceivePacket, SendPacket } from "./packets/packet.ts"; 12 | import { parseError } from "./packets/parsers/err.ts"; 13 | import { 14 | AuthResult, 15 | parseAuth, 16 | parseHandshake, 17 | } from "./packets/parsers/handshake.ts"; 18 | import { FieldInfo, parseField, parseRow } from "./packets/parsers/result.ts"; 19 | import { PacketType } from "./constant/packet.ts"; 20 | import authPlugin from "./auth_plugin/index.ts"; 21 | import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; 22 | import auth from "./auth.ts"; 23 | import ServerCapabilities from "./constant/capabilities.ts"; 24 | import { buildSSLRequest } from "./packets/builders/tls.ts"; 25 | 26 | /** 27 | * Connection state 28 | */ 29 | export enum ConnectionState { 30 | CONNECTING, 31 | CONNECTED, 32 | CLOSING, 33 | CLOSED, 34 | } 35 | 36 | /** 37 | * Result for execute sql 38 | */ 39 | export type ExecuteResult = { 40 | affectedRows?: number; 41 | lastInsertId?: number; 42 | fields?: FieldInfo[]; 43 | rows?: any[]; 44 | iterator?: any; 45 | }; 46 | 47 | /** Connection for mysql */ 48 | export class Connection { 49 | state: ConnectionState = ConnectionState.CONNECTING; 50 | capabilities: number = 0; 51 | serverVersion: string = ""; 52 | 53 | private conn?: Deno.Conn = undefined; 54 | private _timedOut = false; 55 | 56 | get remoteAddr(): string { 57 | return this.config.socketPath 58 | ? `unix:${this.config.socketPath}` 59 | : `${this.config.hostname}:${this.config.port}`; 60 | } 61 | 62 | constructor(readonly config: ClientConfig) {} 63 | 64 | private async _connect() { 65 | // TODO: implement connect timeout 66 | if ( 67 | this.config.tls?.mode && 68 | this.config.tls.mode !== TLSMode.DISABLED && 69 | this.config.tls.mode !== TLSMode.VERIFY_IDENTITY 70 | ) { 71 | throw new Error("unsupported tls mode"); 72 | } 73 | const { hostname, port = 3306, socketPath, username = "", password } = 74 | this.config; 75 | log.info(`connecting ${this.remoteAddr}`); 76 | this.conn = !socketPath 77 | ? await Deno.connect({ 78 | transport: "tcp", 79 | hostname, 80 | port, 81 | }) 82 | : await Deno.connect({ 83 | transport: "unix", 84 | path: socketPath, 85 | } as any); 86 | 87 | try { 88 | let receive = await this.nextPacket(); 89 | const handshakePacket = parseHandshake(receive.body); 90 | 91 | let handshakeSequenceNumber = receive.header.no; 92 | 93 | // Deno.startTls() only supports VERIFY_IDENTITY now. 94 | let isSSL = false; 95 | if ( 96 | this.config.tls?.mode === TLSMode.VERIFY_IDENTITY 97 | ) { 98 | if ( 99 | (handshakePacket.serverCapabilities & 100 | ServerCapabilities.CLIENT_SSL) === 0 101 | ) { 102 | throw new Error("Server does not support TLS"); 103 | } 104 | if ( 105 | (handshakePacket.serverCapabilities & 106 | ServerCapabilities.CLIENT_SSL) !== 0 107 | ) { 108 | const tlsData = buildSSLRequest(handshakePacket, { 109 | db: this.config.db, 110 | }); 111 | await new SendPacket(tlsData, ++handshakeSequenceNumber).send( 112 | this.conn, 113 | ); 114 | this.conn = await Deno.startTls(this.conn, { 115 | hostname, 116 | caCerts: this.config.tls?.caCerts, 117 | }); 118 | } 119 | isSSL = true; 120 | } 121 | 122 | const data = buildAuth(handshakePacket, { 123 | username, 124 | password, 125 | db: this.config.db, 126 | ssl: isSSL, 127 | }); 128 | 129 | await new SendPacket(data, ++handshakeSequenceNumber).send(this.conn); 130 | 131 | this.state = ConnectionState.CONNECTING; 132 | this.serverVersion = handshakePacket.serverVersion; 133 | this.capabilities = handshakePacket.serverCapabilities; 134 | 135 | receive = await this.nextPacket(); 136 | 137 | const authResult = parseAuth(receive); 138 | let handler; 139 | 140 | switch (authResult) { 141 | case AuthResult.AuthMoreRequired: 142 | const adaptedPlugin = 143 | (authPlugin as any)[handshakePacket.authPluginName]; 144 | handler = adaptedPlugin; 145 | break; 146 | case AuthResult.MethodMismatch: 147 | const authSwitch = parseAuthSwitch(receive.body); 148 | // If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is 149 | // sent and we have to keep using the cipher sent in the init packet. 150 | if ( 151 | authSwitch.authPluginData === undefined || 152 | authSwitch.authPluginData.length === 0 153 | ) { 154 | authSwitch.authPluginData = handshakePacket.seed; 155 | } 156 | 157 | let authData; 158 | if (password) { 159 | authData = auth( 160 | authSwitch.authPluginName, 161 | password, 162 | authSwitch.authPluginData, 163 | ); 164 | } else { 165 | authData = Uint8Array.from([]); 166 | } 167 | 168 | await new SendPacket(authData, receive.header.no + 1).send(this.conn); 169 | 170 | receive = await this.nextPacket(); 171 | const authSwitch2 = parseAuthSwitch(receive.body); 172 | if (authSwitch2.authPluginName !== "") { 173 | throw new Error( 174 | "Do not allow to change the auth plugin more than once!", 175 | ); 176 | } 177 | } 178 | 179 | let result; 180 | if (handler) { 181 | result = await handler.start(handshakePacket.seed, password!); 182 | while (!result.done) { 183 | if (result.data) { 184 | const sequenceNumber = receive.header.no + 1; 185 | await new SendPacket(result.data, sequenceNumber).send(this.conn); 186 | receive = await this.nextPacket(); 187 | } 188 | if (result.quickRead) { 189 | await this.nextPacket(); 190 | } 191 | if (result.next) { 192 | result = await result.next(receive); 193 | } 194 | } 195 | } 196 | 197 | const header = receive.body.readUint8(); 198 | if (header === 0xff) { 199 | const error = parseError(receive.body, this); 200 | log.error(`connect error(${error.code}): ${error.message}`); 201 | this.close(); 202 | throw new Error(error.message); 203 | } else { 204 | log.info(`connected to ${this.remoteAddr}`); 205 | this.state = ConnectionState.CONNECTED; 206 | } 207 | 208 | if (this.config.charset) { 209 | await this.execute(`SET NAMES ${this.config.charset}`); 210 | } 211 | } catch (error) { 212 | // Call close() to avoid leaking socket. 213 | this.close(); 214 | throw error; 215 | } 216 | } 217 | 218 | /** Connect to database */ 219 | async connect(): Promise { 220 | await this._connect(); 221 | } 222 | 223 | private async nextPacket(): Promise { 224 | if (!this.conn) { 225 | throw new ConnnectionError("Not connected"); 226 | } 227 | 228 | const timeoutTimer = this.config.timeout 229 | ? setTimeout( 230 | this._timeoutCallback, 231 | this.config.timeout, 232 | ) 233 | : null; 234 | let packet: ReceivePacket | null; 235 | try { 236 | packet = await new ReceivePacket().parse(this.conn!); 237 | } catch (error) { 238 | if (this._timedOut) { 239 | // Connection has been closed by timeoutCallback. 240 | throw new ResponseTimeoutError("Connection read timed out"); 241 | } 242 | timeoutTimer && clearTimeout(timeoutTimer); 243 | this.close(); 244 | throw error; 245 | } 246 | timeoutTimer && clearTimeout(timeoutTimer); 247 | 248 | if (!packet) { 249 | // Connection is half-closed by the remote host. 250 | // Call close() to avoid leaking socket. 251 | this.close(); 252 | throw new ReadError("Connection closed unexpectedly"); 253 | } 254 | if (packet.type === PacketType.ERR_Packet) { 255 | packet.body.skip(1); 256 | const error = parseError(packet.body, this); 257 | throw new Error(error.message); 258 | } 259 | return packet!; 260 | } 261 | 262 | private _timeoutCallback = () => { 263 | log.info("connection read timed out"); 264 | this._timedOut = true; 265 | this.close(); 266 | }; 267 | 268 | /** Close database connection */ 269 | close(): void { 270 | if (this.state != ConnectionState.CLOSED) { 271 | log.info("close connection"); 272 | this.conn?.close(); 273 | this.state = ConnectionState.CLOSED; 274 | } 275 | } 276 | 277 | /** 278 | * excute query sql 279 | * @param sql query sql string 280 | * @param params query params 281 | */ 282 | async query(sql: string, params?: any[]): Promise { 283 | const result = await this.execute(sql, params); 284 | if (result && result.rows) { 285 | return result.rows; 286 | } else { 287 | return result; 288 | } 289 | } 290 | 291 | /** 292 | * execute sql 293 | * @param sql sql string 294 | * @param params query params 295 | * @param iterator whether to return an ExecuteIteratorResult or ExecuteResult 296 | */ 297 | async execute( 298 | sql: string, 299 | params?: any[], 300 | iterator = false, 301 | ): Promise { 302 | if (this.state != ConnectionState.CONNECTED) { 303 | if (this.state == ConnectionState.CLOSED) { 304 | throw new ConnnectionError("Connection is closed"); 305 | } else { 306 | throw new ConnnectionError("Must be connected first"); 307 | } 308 | } 309 | const data = buildQuery(sql, params); 310 | try { 311 | await new SendPacket(data, 0).send(this.conn!); 312 | let receive = await this.nextPacket(); 313 | if (receive.type === PacketType.OK_Packet) { 314 | receive.body.skip(1); 315 | return { 316 | affectedRows: receive.body.readEncodedLen(), 317 | lastInsertId: receive.body.readEncodedLen(), 318 | }; 319 | } else if (receive.type !== PacketType.Result) { 320 | throw new ProtocolError(); 321 | } 322 | let fieldCount = receive.body.readEncodedLen(); 323 | const fields: FieldInfo[] = []; 324 | while (fieldCount--) { 325 | const packet = await this.nextPacket(); 326 | if (packet) { 327 | const field = parseField(packet.body); 328 | fields.push(field); 329 | } 330 | } 331 | 332 | const rows = []; 333 | if (!(this.capabilities & ServerCapabilities.CLIENT_DEPRECATE_EOF)) { 334 | // EOF(mysql < 5.7 or mariadb < 10.2) 335 | receive = await this.nextPacket(); 336 | if (receive.type !== PacketType.EOF_Packet) { 337 | throw new ProtocolError(); 338 | } 339 | } 340 | 341 | if (!iterator) { 342 | while (true) { 343 | receive = await this.nextPacket(); 344 | if (receive.type === PacketType.EOF_Packet) { 345 | break; 346 | } else { 347 | const row = parseRow(receive.body, fields); 348 | rows.push(row); 349 | } 350 | } 351 | return { rows, fields }; 352 | } 353 | 354 | return { 355 | fields, 356 | iterator: this.buildIterator(fields), 357 | }; 358 | } catch (error) { 359 | this.close(); 360 | throw error; 361 | } 362 | } 363 | 364 | private buildIterator(fields: FieldInfo[]): any { 365 | const next = async () => { 366 | const receive = await this.nextPacket(); 367 | 368 | if (receive.type === PacketType.EOF_Packet) { 369 | return { done: true }; 370 | } 371 | 372 | const value = parseRow(receive.body, fields); 373 | 374 | return { 375 | done: false, 376 | value, 377 | }; 378 | }; 379 | 380 | return { 381 | [Symbol.asyncIterator]: () => { 382 | return { 383 | next, 384 | }; 385 | }, 386 | }; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/constant/capabilities.ts: -------------------------------------------------------------------------------- 1 | enum ServerCapabilities { 2 | CLIENT_LONG_PASSWORD = 0x00000001, 3 | CLIENT_FOUND_ROWS = 0x00000002, 4 | CLIENT_LONG_FLAG = 0x00000004, 5 | CLIENT_CONNECT_WITH_DB = 0x00000008, 6 | CLIENT_NO_SCHEMA = 0x00000010, 7 | CLIENT_COMPRESS = 0x00000020, 8 | CLIENT_ODBC = 0x00000040, 9 | CLIENT_LOCAL_FILES = 0x00000080, 10 | CLIENT_IGNORE_SPACE = 0x00000100, 11 | CLIENT_PROTOCOL_41 = 0x00000200, 12 | CLIENT_INTERACTIVE = 0x00000400, 13 | CLIENT_SSL = 0x00000800, 14 | CLIENT_IGNORE_SIGPIPE = 0x00001000, 15 | CLIENT_TRANSACTIONS = 0x00002000, 16 | CLIENT_RESERVED = 0x00004000, 17 | CLIENT_SECURE_CONNECTION = 0x00008000, 18 | CLIENT_MULTI_STATEMENTS = 0x00010000, 19 | CLIENT_MULTI_RESULTS = 0x00020000, 20 | CLIENT_PS_MULTI_RESULTS = 0x00040000, 21 | CLIENT_PLUGIN_AUTH = 0x00080000, 22 | CLIENT_CONNECT_ATTRS = 0x00100000, 23 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000, 24 | CLIENT_DEPRECATE_EOF = 0x01000000, 25 | } 26 | 27 | export default ServerCapabilities; 28 | -------------------------------------------------------------------------------- /src/constant/charset.ts: -------------------------------------------------------------------------------- 1 | export enum Charset { 2 | BIG5_CHINESE_CI = 1, 3 | LATIN2_CZECH_CS = 2, 4 | DEC8_SWEDISH_CI = 3, 5 | CP850_GENERAL_CI = 4, 6 | LATIN1_GERMAN1_CI = 5, 7 | HP8_ENGLISH_CI = 6, 8 | KOI8R_GENERAL_CI = 7, 9 | LATIN1_SWEDISH_CI = 8, 10 | LATIN2_GENERAL_CI = 9, 11 | SWE7_SWEDISH_CI = 10, 12 | ASCII_GENERAL_CI = 11, 13 | UJIS_JAPANESE_CI = 12, 14 | SJIS_JAPANESE_CI = 13, 15 | CP1251_BULGARIAN_CI = 14, 16 | LATIN1_DANISH_CI = 15, 17 | HEBREW_GENERAL_CI = 16, 18 | TIS620_THAI_CI = 18, 19 | EUCKR_KOREAN_CI = 19, 20 | LATIN7_ESTONIAN_CS = 20, 21 | LATIN2_HUNGARIAN_CI = 21, 22 | KOI8U_GENERAL_CI = 22, 23 | CP1251_UKRAINIAN_CI = 23, 24 | GB2312_CHINESE_CI = 24, 25 | GREEK_GENERAL_CI = 25, 26 | CP1250_GENERAL_CI = 26, 27 | LATIN2_CROATIAN_CI = 27, 28 | GBK_CHINESE_CI = 28, 29 | CP1257_LITHUANIAN_CI = 29, 30 | LATIN5_TURKISH_CI = 30, 31 | LATIN1_GERMAN2_CI = 31, 32 | ARMSCII8_GENERAL_CI = 32, 33 | UTF8_GENERAL_CI = 33, 34 | CP1250_CZECH_CS = 34, 35 | UCS2_GENERAL_CI = 35, 36 | CP866_GENERAL_CI = 36, 37 | KEYBCS2_GENERAL_CI = 37, 38 | MACCE_GENERAL_CI = 38, 39 | MACROMAN_GENERAL_CI = 39, 40 | CP852_GENERAL_CI = 40, 41 | LATIN7_GENERAL_CI = 41, 42 | LATIN7_GENERAL_CS = 42, 43 | MACCE_BIN = 43, 44 | CP1250_CROATIAN_CI = 44, 45 | UTF8MB4_GENERAL_CI = 45, 46 | UTF8MB4_BIN = 46, 47 | LATIN1_BIN = 47, 48 | LATIN1_GENERAL_CI = 48, 49 | LATIN1_GENERAL_CS = 49, 50 | CP1251_BIN = 50, 51 | CP1251_GENERAL_CI = 51, 52 | CP1251_GENERAL_CS = 52, 53 | MACROMAN_BIN = 53, 54 | UTF16_GENERAL_CI = 54, 55 | UTF16_BIN = 55, 56 | UTF16LE_GENERAL_CI = 56, 57 | CP1256_GENERAL_CI = 57, 58 | CP1257_BIN = 58, 59 | CP1257_GENERAL_CI = 59, 60 | UTF32_GENERAL_CI = 60, 61 | UTF32_BIN = 61, 62 | UTF16LE_BIN = 62, 63 | BINARY = 63, 64 | ARMSCII8_BIN = 64, 65 | ASCII_BIN = 65, 66 | CP1250_BIN = 66, 67 | CP1256_BIN = 67, 68 | CP866_BIN = 68, 69 | DEC8_BIN = 69, 70 | GREEK_BIN = 70, 71 | HEBREW_BIN = 71, 72 | HP8_BIN = 72, 73 | KEYBCS2_BIN = 73, 74 | KOI8R_BIN = 74, 75 | KOI8U_BIN = 75, 76 | LATIN2_BIN = 77, 77 | LATIN5_BIN = 78, 78 | LATIN7_BIN = 79, 79 | CP850_BIN = 80, 80 | CP852_BIN = 81, 81 | SWE7_BIN = 82, 82 | UTF8_BIN = 83, 83 | BIG5_BIN = 84, 84 | EUCKR_BIN = 85, 85 | GB2312_BIN = 86, 86 | GBK_BIN = 87, 87 | SJIS_BIN = 88, 88 | TIS620_BIN = 89, 89 | UCS2_BIN = 90, 90 | UJIS_BIN = 91, 91 | GEOSTD8_GENERAL_CI = 92, 92 | GEOSTD8_BIN = 93, 93 | LATIN1_SPANISH_CI = 94, 94 | CP932_JAPANESE_CI = 95, 95 | CP932_BIN = 96, 96 | EUCJPMS_JAPANESE_CI = 97, 97 | EUCJPMS_BIN = 98, 98 | CP1250_POLISH_CI = 99, 99 | UTF16_UNICODE_CI = 101, 100 | UTF16_ICELANDIC_CI = 102, 101 | UTF16_LATVIAN_CI = 103, 102 | UTF16_ROMANIAN_CI = 104, 103 | UTF16_SLOVENIAN_CI = 105, 104 | UTF16_POLISH_CI = 106, 105 | UTF16_ESTONIAN_CI = 107, 106 | UTF16_SPANISH_CI = 108, 107 | UTF16_SWEDISH_CI = 109, 108 | UTF16_TURKISH_CI = 110, 109 | UTF16_CZECH_CI = 111, 110 | UTF16_DANISH_CI = 112, 111 | UTF16_LITHUANIAN_CI = 113, 112 | UTF16_SLOVAK_CI = 114, 113 | UTF16_SPANISH2_CI = 115, 114 | UTF16_ROMAN_CI = 116, 115 | UTF16_PERSIAN_CI = 117, 116 | UTF16_ESPERANTO_CI = 118, 117 | UTF16_HUNGARIAN_CI = 119, 118 | UTF16_SINHALA_CI = 120, 119 | UTF16_GERMAN2_CI = 121, 120 | UTF16_CROATIAN_MYSQL561_CI = 122, 121 | UTF16_UNICODE_520_CI = 123, 122 | UTF16_VIETNAMESE_CI = 124, 123 | UCS2_UNICODE_CI = 128, 124 | UCS2_ICELANDIC_CI = 129, 125 | UCS2_LATVIAN_CI = 130, 126 | UCS2_ROMANIAN_CI = 131, 127 | UCS2_SLOVENIAN_CI = 132, 128 | UCS2_POLISH_CI = 133, 129 | UCS2_ESTONIAN_CI = 134, 130 | UCS2_SPANISH_CI = 135, 131 | UCS2_SWEDISH_CI = 136, 132 | UCS2_TURKISH_CI = 137, 133 | UCS2_CZECH_CI = 138, 134 | UCS2_DANISH_CI = 139, 135 | UCS2_LITHUANIAN_CI = 140, 136 | UCS2_SLOVAK_CI = 141, 137 | UCS2_SPANISH2_CI = 142, 138 | UCS2_ROMAN_CI = 143, 139 | UCS2_PERSIAN_CI = 144, 140 | UCS2_ESPERANTO_CI = 145, 141 | UCS2_HUNGARIAN_CI = 146, 142 | UCS2_SINHALA_CI = 147, 143 | UCS2_GERMAN2_CI = 148, 144 | UCS2_CROATIAN_MYSQL561_CI = 149, 145 | UCS2_UNICODE_520_CI = 150, 146 | UCS2_VIETNAMESE_CI = 151, 147 | UCS2_GENERAL_MYSQL500_CI = 159, 148 | UTF32_UNICODE_CI = 160, 149 | UTF32_ICELANDIC_CI = 161, 150 | UTF32_LATVIAN_CI = 162, 151 | UTF32_ROMANIAN_CI = 163, 152 | UTF32_SLOVENIAN_CI = 164, 153 | UTF32_POLISH_CI = 165, 154 | UTF32_ESTONIAN_CI = 166, 155 | UTF32_SPANISH_CI = 167, 156 | UTF32_SWEDISH_CI = 168, 157 | UTF32_TURKISH_CI = 169, 158 | UTF32_CZECH_CI = 170, 159 | UTF32_DANISH_CI = 171, 160 | UTF32_LITHUANIAN_CI = 172, 161 | UTF32_SLOVAK_CI = 173, 162 | UTF32_SPANISH2_CI = 174, 163 | UTF32_ROMAN_CI = 175, 164 | UTF32_PERSIAN_CI = 176, 165 | UTF32_ESPERANTO_CI = 177, 166 | UTF32_HUNGARIAN_CI = 178, 167 | UTF32_SINHALA_CI = 179, 168 | UTF32_GERMAN2_CI = 180, 169 | UTF32_CROATIAN_MYSQL561_CI = 181, 170 | UTF32_UNICODE_520_CI = 182, 171 | UTF32_VIETNAMESE_CI = 183, 172 | UTF8_UNICODE_CI = 192, 173 | UTF8_ICELANDIC_CI = 193, 174 | UTF8_LATVIAN_CI = 194, 175 | UTF8_ROMANIAN_CI = 195, 176 | UTF8_SLOVENIAN_CI = 196, 177 | UTF8_POLISH_CI = 197, 178 | UTF8_ESTONIAN_CI = 198, 179 | UTF8_SPANISH_CI = 199, 180 | UTF8_SWEDISH_CI = 200, 181 | UTF8_TURKISH_CI = 201, 182 | UTF8_CZECH_CI = 202, 183 | UTF8_DANISH_CI = 203, 184 | UTF8_LITHUANIAN_CI = 204, 185 | UTF8_SLOVAK_CI = 205, 186 | UTF8_SPANISH2_CI = 206, 187 | UTF8_ROMAN_CI = 207, 188 | UTF8_PERSIAN_CI = 208, 189 | UTF8_ESPERANTO_CI = 209, 190 | UTF8_HUNGARIAN_CI = 210, 191 | UTF8_SINHALA_CI = 211, 192 | UTF8_GERMAN2_CI = 212, 193 | UTF8_CROATIAN_MYSQL561_CI = 213, 194 | UTF8_UNICODE_520_CI = 214, 195 | UTF8_VIETNAMESE_CI = 215, 196 | UTF8_GENERAL_MYSQL500_CI = 223, 197 | UTF8MB4_UNICODE_CI = 224, 198 | UTF8MB4_ICELANDIC_CI = 225, 199 | UTF8MB4_LATVIAN_CI = 226, 200 | UTF8MB4_ROMANIAN_CI = 227, 201 | UTF8MB4_SLOVENIAN_CI = 228, 202 | UTF8MB4_POLISH_CI = 229, 203 | UTF8MB4_ESTONIAN_CI = 230, 204 | UTF8MB4_SPANISH_CI = 231, 205 | UTF8MB4_SWEDISH_CI = 232, 206 | UTF8MB4_TURKISH_CI = 233, 207 | UTF8MB4_CZECH_CI = 234, 208 | UTF8MB4_DANISH_CI = 235, 209 | UTF8MB4_LITHUANIAN_CI = 236, 210 | UTF8MB4_SLOVAK_CI = 237, 211 | UTF8MB4_SPANISH2_CI = 238, 212 | UTF8MB4_ROMAN_CI = 239, 213 | UTF8MB4_PERSIAN_CI = 240, 214 | UTF8MB4_ESPERANTO_CI = 241, 215 | UTF8MB4_HUNGARIAN_CI = 242, 216 | UTF8MB4_SINHALA_CI = 243, 217 | UTF8MB4_GERMAN2_CI = 244, 218 | UTF8MB4_CROATIAN_MYSQL561_CI = 245, 219 | UTF8MB4_UNICODE_520_CI = 246, 220 | UTF8MB4_VIETNAMESE_CI = 247, 221 | UTF8_GENERAL50_CI = 253, 222 | 223 | ARMSCII8 = ARMSCII8_GENERAL_CI, 224 | ASCII = ASCII_GENERAL_CI, 225 | BIG5 = BIG5_CHINESE_CI, 226 | CP1250 = CP1250_GENERAL_CI, 227 | CP1251 = CP1251_GENERAL_CI, 228 | CP1256 = CP1256_GENERAL_CI, 229 | CP1257 = CP1257_GENERAL_CI, 230 | CP866 = CP866_GENERAL_CI, 231 | CP850 = CP850_GENERAL_CI, 232 | CP852 = CP852_GENERAL_CI, 233 | CP932 = CP932_JAPANESE_CI, 234 | DEC8 = DEC8_SWEDISH_CI, 235 | EUCJPMS = EUCJPMS_JAPANESE_CI, 236 | EUCKR = EUCKR_KOREAN_CI, 237 | GB2312 = GB2312_CHINESE_CI, 238 | GBK = GBK_CHINESE_CI, 239 | GEOSTD8 = GEOSTD8_GENERAL_CI, 240 | GREEK = GREEK_GENERAL_CI, 241 | HEBREW = HEBREW_GENERAL_CI, 242 | HP8 = HP8_ENGLISH_CI, 243 | KEYBCS2 = KEYBCS2_GENERAL_CI, 244 | KOI8R = KOI8R_GENERAL_CI, 245 | KOI8U = KOI8U_GENERAL_CI, 246 | LATIN1 = LATIN1_SWEDISH_CI, 247 | LATIN2 = LATIN2_GENERAL_CI, 248 | LATIN5 = LATIN5_TURKISH_CI, 249 | LATIN7 = LATIN7_GENERAL_CI, 250 | MACCE = MACCE_GENERAL_CI, 251 | MACROMAN = MACROMAN_GENERAL_CI, 252 | SJIS = SJIS_JAPANESE_CI, 253 | SWE7 = SWE7_SWEDISH_CI, 254 | TIS620 = TIS620_THAI_CI, 255 | UCS2 = UCS2_GENERAL_CI, 256 | UJIS = UJIS_JAPANESE_CI, 257 | UTF16 = UTF16_GENERAL_CI, 258 | UTF16LE = UTF16LE_GENERAL_CI, 259 | UTF8 = UTF8_GENERAL_CI, 260 | UTF8MB4 = UTF8MB4_GENERAL_CI, 261 | UTF32 = UTF32_GENERAL_CI, 262 | } 263 | -------------------------------------------------------------------------------- /src/constant/errors.ts: -------------------------------------------------------------------------------- 1 | export class ConnnectionError extends Error { 2 | constructor(msg?: string) { 3 | super(msg); 4 | } 5 | } 6 | 7 | export class WriteError extends ConnnectionError { 8 | constructor(msg?: string) { 9 | super(msg); 10 | } 11 | } 12 | 13 | export class ReadError extends ConnnectionError { 14 | constructor(msg?: string) { 15 | super(msg); 16 | } 17 | } 18 | 19 | export class ResponseTimeoutError extends ConnnectionError { 20 | constructor(msg?: string) { 21 | super(msg); 22 | } 23 | } 24 | 25 | export class ProtocolError extends ConnnectionError { 26 | constructor(msg?: string) { 27 | super(msg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/constant/mysql_types.ts: -------------------------------------------------------------------------------- 1 | /** @ignore */ 2 | export const MYSQL_TYPE_DECIMAL = 0x00; 3 | /** @ignore */ 4 | export const MYSQL_TYPE_TINY = 0x01; 5 | /** @ignore */ 6 | export const MYSQL_TYPE_SHORT = 0x02; 7 | /** @ignore */ 8 | export const MYSQL_TYPE_LONG = 0x03; 9 | /** @ignore */ 10 | export const MYSQL_TYPE_FLOAT = 0x04; 11 | /** @ignore */ 12 | export const MYSQL_TYPE_DOUBLE = 0x05; 13 | /** @ignore */ 14 | export const MYSQL_TYPE_NULL = 0x06; 15 | /** @ignore */ 16 | export const MYSQL_TYPE_TIMESTAMP = 0x07; 17 | /** @ignore */ 18 | export const MYSQL_TYPE_LONGLONG = 0x08; 19 | /** @ignore */ 20 | export const MYSQL_TYPE_INT24 = 0x09; 21 | /** @ignore */ 22 | export const MYSQL_TYPE_DATE = 0x0a; 23 | /** @ignore */ 24 | export const MYSQL_TYPE_TIME = 0x0b; 25 | /** @ignore */ 26 | export const MYSQL_TYPE_DATETIME = 0x0c; 27 | /** @ignore */ 28 | export const MYSQL_TYPE_YEAR = 0x0d; 29 | /** @ignore */ 30 | export const MYSQL_TYPE_NEWDATE = 0x0e; 31 | /** @ignore */ 32 | export const MYSQL_TYPE_VARCHAR = 0x0f; 33 | /** @ignore */ 34 | export const MYSQL_TYPE_BIT = 0x10; 35 | /** @ignore */ 36 | export const MYSQL_TYPE_TIMESTAMP2 = 0x11; 37 | /** @ignore */ 38 | export const MYSQL_TYPE_DATETIME2 = 0x12; 39 | /** @ignore */ 40 | export const MYSQL_TYPE_TIME2 = 0x13; 41 | /** @ignore */ 42 | export const MYSQL_TYPE_NEWDECIMAL = 0xf6; 43 | /** @ignore */ 44 | export const MYSQL_TYPE_ENUM = 0xf7; 45 | /** @ignore */ 46 | export const MYSQL_TYPE_SET = 0xf8; 47 | /** @ignore */ 48 | export const MYSQL_TYPE_TINY_BLOB = 0xf9; 49 | /** @ignore */ 50 | export const MYSQL_TYPE_MEDIUM_BLOB = 0xfa; 51 | /** @ignore */ 52 | export const MYSQL_TYPE_LONG_BLOB = 0xfb; 53 | /** @ignore */ 54 | export const MYSQL_TYPE_BLOB = 0xfc; 55 | /** @ignore */ 56 | export const MYSQL_TYPE_VAR_STRING = 0xfd; 57 | /** @ignore */ 58 | export const MYSQL_TYPE_STRING = 0xfe; 59 | /** @ignore */ 60 | export const MYSQL_TYPE_GEOMETRY = 0xff; 61 | -------------------------------------------------------------------------------- /src/constant/packet.ts: -------------------------------------------------------------------------------- 1 | export enum PacketType { 2 | OK_Packet = 0x00, 3 | EOF_Packet = 0xfe, 4 | ERR_Packet = 0xff, 5 | Result, 6 | } 7 | -------------------------------------------------------------------------------- /src/constant/server_status.ts: -------------------------------------------------------------------------------- 1 | /** @ignore */ 2 | export enum ServerStatus { 3 | IN_TRANSACTION = 0x0001, 4 | AUTO_COMMIT = 0x0002, 5 | MORE_RESULTS = 0x0008, 6 | NO_GOOD_INDEX_USED = 0x0010, 7 | NO_INDEX_USED = 0x0020, 8 | CURSOR_EXISTS = 0x0040, 9 | LAST_ROW_SENT = 0x0080, 10 | DB_DROPPED = 0x0100, 11 | NO_BACKSLASH_ESCAPES = 0x0200, 12 | METADATA_CHANGED = 0x0400, 13 | QUERY_WAS_SLOW = 0x0800, 14 | PS_OUT_PARAMS = 0x1000, 15 | IN_TRANS_READONLY = 0x2000, 16 | SESSION_STATE_CHANGED = 0x4000, 17 | } 18 | -------------------------------------------------------------------------------- /src/deferred.ts: -------------------------------------------------------------------------------- 1 | import { Deferred, deferred } from "../deps.ts"; 2 | 3 | /** @ignore */ 4 | export class DeferredStack { 5 | private _queue: Deferred[] = []; 6 | private _size = 0; 7 | 8 | constructor( 9 | readonly _maxSize: number, 10 | private _array: T[] = [], 11 | private readonly creator: () => Promise, 12 | ) { 13 | this._size = _array.length; 14 | } 15 | 16 | get size(): number { 17 | return this._size; 18 | } 19 | 20 | get maxSize(): number { 21 | return this._maxSize; 22 | } 23 | 24 | get available(): number { 25 | return this._array.length; 26 | } 27 | 28 | async pop(): Promise { 29 | if (this._array.length) { 30 | return this._array.pop()!; 31 | } else if (this._size < this._maxSize) { 32 | this._size++; 33 | let item: T; 34 | try { 35 | item = await this.creator(); 36 | } catch (err) { 37 | this._size--; 38 | throw err; 39 | } 40 | return item; 41 | } 42 | const defer = deferred(); 43 | this._queue.push(defer); 44 | return await defer; 45 | } 46 | 47 | /** Returns false if the item is consumed by a deferred pop */ 48 | push(item: T): boolean { 49 | if (this._queue.length) { 50 | this._queue.shift()!.resolve(item); 51 | return false; 52 | } else { 53 | this._array.push(item); 54 | return true; 55 | } 56 | } 57 | 58 | tryPopAvailable() { 59 | return this._array.pop(); 60 | } 61 | 62 | remove(item: T): boolean { 63 | const index = this._array.indexOf(item); 64 | if (index < 0) return false; 65 | this._array.splice(index, 1); 66 | this._size--; 67 | return true; 68 | } 69 | 70 | reduceSize() { 71 | this._size--; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { log } from "../deps.ts"; 2 | 3 | let logger = log.getLogger(); 4 | 5 | export { logger as log }; 6 | 7 | let isDebug = false; 8 | 9 | /** @ignore */ 10 | export function debug(func: Function) { 11 | if (isDebug) { 12 | func(); 13 | } 14 | } 15 | 16 | export interface LoggerConfig { 17 | /** Enable logging (default: true) */ 18 | enable?: boolean; 19 | /** The minimal level to print (default: "INFO") */ 20 | level?: log.LevelName; 21 | /** A deno_std/log.Logger instance to be used as logger. When used, `level` is ignored. */ 22 | logger?: log.Logger; 23 | } 24 | 25 | export async function configLogger(config: LoggerConfig) { 26 | let { enable = true, level = "INFO" } = config; 27 | if (config.logger) level = config.logger.levelName; 28 | isDebug = level == "DEBUG"; 29 | 30 | if (!enable) { 31 | logger = new log.Logger("fakeLogger", "NOTSET", {}); 32 | logger.level = 0; 33 | } else { 34 | if (!config.logger) { 35 | await log.setup({ 36 | handlers: { 37 | console: new log.handlers.ConsoleHandler(level), 38 | }, 39 | loggers: { 40 | default: { 41 | level: "DEBUG", 42 | handlers: ["console"], 43 | }, 44 | }, 45 | }); 46 | logger = log.getLogger(); 47 | } else { 48 | logger = config.logger; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/packets/builders/auth.ts: -------------------------------------------------------------------------------- 1 | import auth from "../../auth.ts"; 2 | import { BufferWriter } from "../../buffer.ts"; 3 | import ServerCapabilities from "../../constant/capabilities.ts"; 4 | import { Charset } from "../../constant/charset.ts"; 5 | import type { HandshakeBody } from "../parsers/handshake.ts"; 6 | import { clientCapabilities } from "./client_capabilities.ts"; 7 | 8 | /** @ignore */ 9 | export function buildAuth( 10 | packet: HandshakeBody, 11 | params: { username: string; password?: string; db?: string; ssl?: boolean }, 12 | ): Uint8Array { 13 | const clientParam: number = clientCapabilities(packet, params); 14 | 15 | if (packet.serverCapabilities & ServerCapabilities.CLIENT_PLUGIN_AUTH) { 16 | const writer = new BufferWriter(new Uint8Array(1000)); 17 | writer 18 | .writeUint32(clientParam) 19 | .writeUint32(2 ** 24 - 1) 20 | .write(Charset.UTF8_GENERAL_CI) 21 | .skip(23) 22 | .writeNullTerminatedString(params.username); 23 | if (params.password) { 24 | const authData = auth( 25 | packet.authPluginName, 26 | params.password, 27 | packet.seed, 28 | ); 29 | if ( 30 | clientParam & 31 | ServerCapabilities.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA || 32 | clientParam & ServerCapabilities.CLIENT_SECURE_CONNECTION 33 | ) { 34 | // request lenenc-int length of auth-response and string[n] auth-response 35 | writer.write(authData.length); 36 | writer.writeBuffer(authData); 37 | } else { 38 | writer.writeBuffer(authData); 39 | writer.write(0); 40 | } 41 | } else { 42 | writer.write(0); 43 | } 44 | if (clientParam & ServerCapabilities.CLIENT_CONNECT_WITH_DB && params.db) { 45 | writer.writeNullTerminatedString(params.db); 46 | } 47 | if (clientParam & ServerCapabilities.CLIENT_PLUGIN_AUTH) { 48 | writer.writeNullTerminatedString(packet.authPluginName); 49 | } 50 | return writer.wroteData; 51 | } 52 | return Uint8Array.from([]); 53 | } 54 | -------------------------------------------------------------------------------- /src/packets/builders/client_capabilities.ts: -------------------------------------------------------------------------------- 1 | import ServerCapabilities from "../../constant/capabilities.ts"; 2 | import type { HandshakeBody } from "../parsers/handshake.ts"; 3 | 4 | export function clientCapabilities( 5 | packet: HandshakeBody, 6 | params: { db?: string; ssl?: boolean }, 7 | ): number { 8 | return (params.db ? ServerCapabilities.CLIENT_CONNECT_WITH_DB : 0) | 9 | ServerCapabilities.CLIENT_PLUGIN_AUTH | 10 | ServerCapabilities.CLIENT_LONG_PASSWORD | 11 | ServerCapabilities.CLIENT_PROTOCOL_41 | 12 | ServerCapabilities.CLIENT_TRANSACTIONS | 13 | ServerCapabilities.CLIENT_MULTI_RESULTS | 14 | ServerCapabilities.CLIENT_SECURE_CONNECTION | 15 | (ServerCapabilities.CLIENT_LONG_FLAG & packet.serverCapabilities) | 16 | (ServerCapabilities.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA & 17 | packet.serverCapabilities) | 18 | (ServerCapabilities.CLIENT_DEPRECATE_EOF & packet.serverCapabilities) | 19 | (params.ssl ? ServerCapabilities.CLIENT_SSL : 0); 20 | } 21 | -------------------------------------------------------------------------------- /src/packets/builders/query.ts: -------------------------------------------------------------------------------- 1 | import { replaceParams } from "../../../deps.ts"; 2 | import { BufferWriter, encode } from "../../buffer.ts"; 3 | 4 | /** @ignore */ 5 | export function buildQuery(sql: string, params: any[] = []): Uint8Array { 6 | const data = encode(replaceParams(sql, params)); 7 | const writer = new BufferWriter(new Uint8Array(data.length + 1)); 8 | writer.write(0x03); 9 | writer.writeBuffer(data); 10 | return writer.buffer; 11 | } 12 | -------------------------------------------------------------------------------- /src/packets/builders/tls.ts: -------------------------------------------------------------------------------- 1 | import { BufferWriter } from "../../buffer.ts"; 2 | import { Charset } from "../../constant/charset.ts"; 3 | import type { HandshakeBody } from "../parsers/handshake.ts"; 4 | import { clientCapabilities } from "./client_capabilities.ts"; 5 | 6 | export function buildSSLRequest( 7 | packet: HandshakeBody, 8 | params: { db?: string }, 9 | ): Uint8Array { 10 | const clientParam: number = clientCapabilities(packet, { 11 | db: params.db, 12 | ssl: true, 13 | }); 14 | const writer = new BufferWriter(new Uint8Array(32)); 15 | writer 16 | .writeUint32(clientParam) 17 | .writeUint32(2 ** 24 - 1) 18 | .write(Charset.UTF8_GENERAL_CI) 19 | .skip(23); 20 | return writer.wroteData; 21 | } 22 | -------------------------------------------------------------------------------- /src/packets/packet.ts: -------------------------------------------------------------------------------- 1 | import { byteFormat } from "../../deps.ts"; 2 | import { BufferReader, BufferWriter } from "../buffer.ts"; 3 | import { WriteError } from "../constant/errors.ts"; 4 | import { debug, log } from "../logger.ts"; 5 | import { PacketType } from "../../src/constant/packet.ts"; 6 | 7 | /** @ignore */ 8 | interface PacketHeader { 9 | size: number; 10 | no: number; 11 | } 12 | 13 | /** @ignore */ 14 | export class SendPacket { 15 | header: PacketHeader; 16 | 17 | constructor(readonly body: Uint8Array, no: number) { 18 | this.header = { size: body.length, no }; 19 | } 20 | 21 | async send(conn: Deno.Conn) { 22 | const body = this.body as Uint8Array; 23 | const data = new BufferWriter(new Uint8Array(4 + body.length)); 24 | data.writeUints(3, this.header.size); 25 | data.write(this.header.no); 26 | data.writeBuffer(body); 27 | debug(() => { 28 | log.debug(`send: ${data.length}B \n${byteFormat(data.buffer)}\n`); 29 | }); 30 | try { 31 | let wrote = 0; 32 | do { 33 | wrote += await conn.write(data.buffer.subarray(wrote)); 34 | } while (wrote < data.length); 35 | } catch (error) { 36 | throw new WriteError(error.message); 37 | } 38 | } 39 | } 40 | 41 | /** @ignore */ 42 | export class ReceivePacket { 43 | header!: PacketHeader; 44 | body!: BufferReader; 45 | type!: PacketType; 46 | 47 | async parse(reader: Deno.Reader): Promise { 48 | const header = new BufferReader(new Uint8Array(4)); 49 | let readCount = 0; 50 | let nread = await this.read(reader, header.buffer); 51 | if (nread === null) return null; 52 | readCount = nread; 53 | const bodySize = header.readUints(3); 54 | this.header = { 55 | size: bodySize, 56 | no: header.readUint8(), 57 | }; 58 | this.body = new BufferReader(new Uint8Array(bodySize)); 59 | nread = await this.read(reader, this.body.buffer); 60 | if (nread === null) return null; 61 | readCount += nread; 62 | 63 | const { OK_Packet, ERR_Packet, EOF_Packet, Result } = PacketType; 64 | switch (this.body.buffer[0]) { 65 | case OK_Packet: 66 | this.type = OK_Packet; 67 | break; 68 | case 0xff: 69 | this.type = ERR_Packet; 70 | break; 71 | case 0xfe: 72 | this.type = EOF_Packet; 73 | break; 74 | default: 75 | this.type = Result; 76 | break; 77 | } 78 | 79 | debug(() => { 80 | const data = new Uint8Array(readCount); 81 | data.set(header.buffer); 82 | data.set(this.body.buffer, 4); 83 | log.debug( 84 | `receive: ${readCount}B, size = ${this.header.size}, no = ${this.header.no} \n${ 85 | byteFormat(data) 86 | }\n`, 87 | ); 88 | }); 89 | 90 | return this; 91 | } 92 | 93 | private async read( 94 | reader: Deno.Reader, 95 | buffer: Uint8Array, 96 | ): Promise { 97 | const size = buffer.length; 98 | let haveRead = 0; 99 | while (haveRead < size) { 100 | const nread = await reader.read(buffer.subarray(haveRead)); 101 | if (nread === null) return null; 102 | haveRead += nread; 103 | } 104 | return haveRead; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/packets/parsers/authswitch.ts: -------------------------------------------------------------------------------- 1 | import { BufferReader } from "../../buffer.ts"; 2 | 3 | /** @ignore */ 4 | export interface authSwitchBody { 5 | status: number; 6 | authPluginName: string; 7 | authPluginData: Uint8Array; 8 | } 9 | 10 | /** @ignore */ 11 | export function parseAuthSwitch(reader: BufferReader): authSwitchBody { 12 | const status = reader.readUint8(); 13 | const authPluginName = reader.readNullTerminatedString(); 14 | const authPluginData = reader.readRestOfPacketString(); 15 | 16 | return { 17 | status, 18 | authPluginName, 19 | authPluginData, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/packets/parsers/err.ts: -------------------------------------------------------------------------------- 1 | import type { BufferReader } from "../../buffer.ts"; 2 | import type { Connection } from "../../connection.ts"; 3 | import ServerCapabilities from "../../constant/capabilities.ts"; 4 | 5 | /** @ignore */ 6 | export interface ErrorPacket { 7 | code: number; 8 | sqlStateMarker?: number; 9 | sqlState?: number; 10 | message: string; 11 | } 12 | 13 | /** @ignore */ 14 | export function parseError( 15 | reader: BufferReader, 16 | conn: Connection, 17 | ): ErrorPacket { 18 | const code = reader.readUint16(); 19 | const packet: ErrorPacket = { 20 | code, 21 | message: "", 22 | }; 23 | if (conn.capabilities & ServerCapabilities.CLIENT_PROTOCOL_41) { 24 | packet.sqlStateMarker = reader.readUint8(); 25 | packet.sqlState = reader.readUints(5); 26 | } 27 | packet.message = reader.readNullTerminatedString(); 28 | return packet; 29 | } 30 | -------------------------------------------------------------------------------- /src/packets/parsers/handshake.ts: -------------------------------------------------------------------------------- 1 | import { BufferReader, BufferWriter } from "../../buffer.ts"; 2 | import ServerCapabilities from "../../constant/capabilities.ts"; 3 | import { PacketType } from "../../constant/packet.ts"; 4 | import { ReceivePacket } from "../packet.ts"; 5 | 6 | /** @ignore */ 7 | export interface HandshakeBody { 8 | protocolVersion: number; 9 | serverVersion: string; 10 | threadId: number; 11 | seed: Uint8Array; 12 | serverCapabilities: number; 13 | characterSet: number; 14 | statusFlags: number; 15 | authPluginName: string; 16 | } 17 | 18 | /** @ignore */ 19 | export function parseHandshake(reader: BufferReader): HandshakeBody { 20 | const protocolVersion = reader.readUint8(); 21 | const serverVersion = reader.readNullTerminatedString(); 22 | const threadId = reader.readUint32(); 23 | const seedWriter = new BufferWriter(new Uint8Array(20)); 24 | seedWriter.writeBuffer(reader.readBuffer(8)); 25 | reader.skip(1); 26 | let serverCapabilities = reader.readUint16(); 27 | 28 | let characterSet: number = 0, 29 | statusFlags: number = 0, 30 | authPluginDataLength: number = 0, 31 | authPluginName: string = ""; 32 | 33 | if (!reader.finished) { 34 | characterSet = reader.readUint8(); 35 | statusFlags = reader.readUint16(); 36 | serverCapabilities |= reader.readUint16() << 16; 37 | 38 | if ((serverCapabilities & ServerCapabilities.CLIENT_PLUGIN_AUTH) != 0) { 39 | authPluginDataLength = reader.readUint8(); 40 | } else { 41 | reader.skip(1); 42 | } 43 | reader.skip(10); 44 | 45 | if ( 46 | (serverCapabilities & ServerCapabilities.CLIENT_SECURE_CONNECTION) != 47 | 0 48 | ) { 49 | seedWriter.writeBuffer( 50 | reader.readBuffer(Math.max(13, authPluginDataLength - 8)), 51 | ); 52 | } 53 | 54 | if ((serverCapabilities & ServerCapabilities.CLIENT_PLUGIN_AUTH) != 0) { 55 | authPluginName = reader.readNullTerminatedString(); 56 | } 57 | } 58 | 59 | return { 60 | protocolVersion, 61 | serverVersion, 62 | threadId, 63 | seed: seedWriter.buffer, 64 | serverCapabilities, 65 | characterSet, 66 | statusFlags, 67 | authPluginName, 68 | }; 69 | } 70 | 71 | export enum AuthResult { 72 | AuthPassed, 73 | MethodMismatch, 74 | AuthMoreRequired, 75 | } 76 | export function parseAuth(packet: ReceivePacket): AuthResult { 77 | switch (packet.type) { 78 | case PacketType.EOF_Packet: 79 | return AuthResult.MethodMismatch; 80 | case PacketType.Result: 81 | return AuthResult.AuthMoreRequired; 82 | case PacketType.OK_Packet: 83 | return AuthResult.AuthPassed; 84 | default: 85 | return AuthResult.AuthPassed; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/packets/parsers/result.ts: -------------------------------------------------------------------------------- 1 | import type { BufferReader } from "../../buffer.ts"; 2 | import { 3 | MYSQL_TYPE_DATE, 4 | MYSQL_TYPE_DATETIME, 5 | MYSQL_TYPE_DATETIME2, 6 | MYSQL_TYPE_DECIMAL, 7 | MYSQL_TYPE_DOUBLE, 8 | MYSQL_TYPE_FLOAT, 9 | MYSQL_TYPE_INT24, 10 | MYSQL_TYPE_LONG, 11 | MYSQL_TYPE_LONGLONG, 12 | MYSQL_TYPE_NEWDATE, 13 | MYSQL_TYPE_NEWDECIMAL, 14 | MYSQL_TYPE_SHORT, 15 | MYSQL_TYPE_STRING, 16 | MYSQL_TYPE_TIME, 17 | MYSQL_TYPE_TIME2, 18 | MYSQL_TYPE_TIMESTAMP, 19 | MYSQL_TYPE_TIMESTAMP2, 20 | MYSQL_TYPE_TINY, 21 | MYSQL_TYPE_VAR_STRING, 22 | MYSQL_TYPE_VARCHAR, 23 | } from "../../constant/mysql_types.ts"; 24 | 25 | /** @ignore */ 26 | export interface FieldInfo { 27 | catalog: string; 28 | schema: string; 29 | table: string; 30 | originTable: string; 31 | name: string; 32 | originName: string; 33 | encoding: number; 34 | fieldLen: number; 35 | fieldType: number; 36 | fieldFlag: number; 37 | decimals: number; 38 | defaultVal: string; 39 | } 40 | 41 | /** @ignore */ 42 | export function parseField(reader: BufferReader): FieldInfo { 43 | const catalog = reader.readLenCodeString()!; 44 | const schema = reader.readLenCodeString()!; 45 | const table = reader.readLenCodeString()!; 46 | const originTable = reader.readLenCodeString()!; 47 | const name = reader.readLenCodeString()!; 48 | const originName = reader.readLenCodeString()!; 49 | reader.skip(1); 50 | const encoding = reader.readUint16()!; 51 | const fieldLen = reader.readUint32()!; 52 | const fieldType = reader.readUint8()!; 53 | const fieldFlag = reader.readUint16()!; 54 | const decimals = reader.readUint8()!; 55 | reader.skip(1); 56 | const defaultVal = reader.readLenCodeString()!; 57 | return { 58 | catalog, 59 | schema, 60 | table, 61 | originName, 62 | fieldFlag, 63 | originTable, 64 | fieldLen, 65 | name, 66 | fieldType, 67 | encoding, 68 | decimals, 69 | defaultVal, 70 | }; 71 | } 72 | 73 | /** @ignore */ 74 | export function parseRow(reader: BufferReader, fields: FieldInfo[]): any { 75 | const row: any = {}; 76 | for (const field of fields) { 77 | const name = field.name; 78 | const val = reader.readLenCodeString(); 79 | row[name] = val === null ? null : convertType(field, val); 80 | } 81 | return row; 82 | } 83 | 84 | /** @ignore */ 85 | function convertType(field: FieldInfo, val: string): any { 86 | const { fieldType, fieldLen } = field; 87 | switch (fieldType) { 88 | case MYSQL_TYPE_DECIMAL: 89 | case MYSQL_TYPE_DOUBLE: 90 | case MYSQL_TYPE_FLOAT: 91 | case MYSQL_TYPE_DATETIME2: 92 | return parseFloat(val); 93 | case MYSQL_TYPE_NEWDECIMAL: 94 | return val; // #42 MySQL's decimal type cannot be accurately represented by the Number. 95 | case MYSQL_TYPE_TINY: 96 | case MYSQL_TYPE_SHORT: 97 | case MYSQL_TYPE_LONG: 98 | case MYSQL_TYPE_INT24: 99 | return parseInt(val); 100 | case MYSQL_TYPE_LONGLONG: 101 | if ( 102 | Number(val) < Number.MIN_SAFE_INTEGER || 103 | Number(val) > Number.MAX_SAFE_INTEGER 104 | ) { 105 | return BigInt(val); 106 | } else { 107 | return parseInt(val); 108 | } 109 | case MYSQL_TYPE_VARCHAR: 110 | case MYSQL_TYPE_VAR_STRING: 111 | case MYSQL_TYPE_STRING: 112 | case MYSQL_TYPE_TIME: 113 | case MYSQL_TYPE_TIME2: 114 | return val; 115 | case MYSQL_TYPE_DATE: 116 | case MYSQL_TYPE_TIMESTAMP: 117 | case MYSQL_TYPE_DATETIME: 118 | case MYSQL_TYPE_NEWDATE: 119 | case MYSQL_TYPE_TIMESTAMP2: 120 | case MYSQL_TYPE_DATETIME2: 121 | return new Date(val); 122 | default: 123 | return val; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/pool.ts: -------------------------------------------------------------------------------- 1 | import { DeferredStack } from "./deferred.ts"; 2 | import { Connection } from "./connection.ts"; 3 | import { log } from "./logger.ts"; 4 | 5 | /** @ignore */ 6 | export class PoolConnection extends Connection { 7 | _pool?: ConnectionPool = undefined; 8 | 9 | private _idleTimer?: number = undefined; 10 | private _idle = false; 11 | 12 | /** 13 | * Should be called by the pool. 14 | */ 15 | enterIdle() { 16 | this._idle = true; 17 | if (this.config.idleTimeout) { 18 | this._idleTimer = setTimeout(() => { 19 | log.info("connection idle timeout"); 20 | this._pool!.remove(this); 21 | try { 22 | this.close(); 23 | } catch (error) { 24 | log.warning(`error closing idle connection`, error); 25 | } 26 | }, this.config.idleTimeout); 27 | try { 28 | // Don't block the event loop from finishing 29 | Deno.unrefTimer(this._idleTimer); 30 | } catch (_error) { 31 | // unrefTimer() is unstable API in older version of Deno 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Should be called by the pool. 38 | */ 39 | exitIdle() { 40 | this._idle = false; 41 | if (this._idleTimer !== undefined) { 42 | clearTimeout(this._idleTimer); 43 | } 44 | } 45 | 46 | /** 47 | * Remove the connection from the pool permanently, when the connection is not usable. 48 | */ 49 | removeFromPool() { 50 | this._pool!.reduceSize(); 51 | this._pool = undefined; 52 | } 53 | 54 | returnToPool() { 55 | this._pool?.push(this); 56 | } 57 | } 58 | 59 | /** @ignore */ 60 | export class ConnectionPool { 61 | _deferred: DeferredStack; 62 | _connections: PoolConnection[] = []; 63 | _closed: boolean = false; 64 | 65 | constructor(maxSize: number, creator: () => Promise) { 66 | this._deferred = new DeferredStack(maxSize, this._connections, async () => { 67 | const conn = await creator(); 68 | conn._pool = this; 69 | return conn; 70 | }); 71 | } 72 | 73 | get info() { 74 | return { 75 | size: this._deferred.size, 76 | maxSize: this._deferred.maxSize, 77 | available: this._deferred.available, 78 | }; 79 | } 80 | 81 | push(conn: PoolConnection) { 82 | if (this._closed) { 83 | conn.close(); 84 | this.reduceSize(); 85 | } 86 | if (this._deferred.push(conn)) { 87 | conn.enterIdle(); 88 | } 89 | } 90 | 91 | async pop(): Promise { 92 | if (this._closed) { 93 | throw new Error("Connection pool is closed"); 94 | } 95 | let conn = this._deferred.tryPopAvailable(); 96 | if (conn) { 97 | conn.exitIdle(); 98 | } else { 99 | conn = await this._deferred.pop(); 100 | } 101 | return conn; 102 | } 103 | 104 | remove(conn: PoolConnection) { 105 | return this._deferred.remove(conn); 106 | } 107 | 108 | /** 109 | * Close the pool and all connections in the pool. 110 | * 111 | * After closing, pop() will throw an error, 112 | * push() will close the connection immediately. 113 | */ 114 | close() { 115 | this._closed = true; 116 | 117 | let conn: PoolConnection | undefined; 118 | while (conn = this._deferred.tryPopAvailable()) { 119 | conn.exitIdle(); 120 | conn.close(); 121 | this.reduceSize(); 122 | } 123 | } 124 | 125 | reduceSize() { 126 | this._deferred.reduceSize(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { 2 | return a.map((byte, index) => { 3 | return byte ^ b[index]; 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test.deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | assertEquals, 3 | assertThrowsAsync, 4 | } from "https://deno.land/std@0.104.0/testing/asserts.ts"; 5 | export * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts"; 6 | export { parse } from "https://deno.land/std@0.104.0/flags/mod.ts"; 7 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrowsAsync, semver } from "./test.deps.ts"; 2 | import { 3 | ConnnectionError, 4 | ResponseTimeoutError, 5 | } from "./src/constant/errors.ts"; 6 | import { 7 | createTestDB, 8 | delay, 9 | isMariaDB, 10 | registerTests, 11 | testWithClient, 12 | } from "./test.util.ts"; 13 | import { log as stdlog } from "./deps.ts"; 14 | import { log } from "./src/logger.ts"; 15 | import { configLogger } from "./mod.ts"; 16 | 17 | testWithClient(async function testCreateDb(client) { 18 | await client.query(`CREATE DATABASE IF NOT EXISTS enok`); 19 | }); 20 | 21 | testWithClient(async function testCreateTable(client) { 22 | await client.query(`DROP TABLE IF EXISTS users`); 23 | await client.query(` 24 | CREATE TABLE users ( 25 | id int(11) NOT NULL AUTO_INCREMENT, 26 | name varchar(100) NOT NULL, 27 | is_top tinyint(1) default 0, 28 | created_at timestamp not null default current_timestamp, 29 | PRIMARY KEY (id) 30 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 31 | `); 32 | }); 33 | 34 | testWithClient(async function testInsert(client) { 35 | let result = await client.execute(`INSERT INTO users(name) values(?)`, [ 36 | "manyuanrong", 37 | ]); 38 | assertEquals(result, { affectedRows: 1, lastInsertId: 1 }); 39 | result = await client.execute(`INSERT INTO users ?? values ?`, [ 40 | ["id", "name"], 41 | [2, "MySQL"], 42 | ]); 43 | assertEquals(result, { affectedRows: 1, lastInsertId: 2 }); 44 | }); 45 | 46 | testWithClient(async function testUpdate(client) { 47 | let result = await client.execute( 48 | `update users set ?? = ?, ?? = ? WHERE id = ?`, 49 | ["name", "MYR🦕", "created_at", new Date(), 1], 50 | ); 51 | assertEquals(result, { affectedRows: 1, lastInsertId: 0 }); 52 | }); 53 | 54 | testWithClient(async function testQuery(client) { 55 | let result = await client.query( 56 | "select ??,`is_top`,`name` from ?? where id = ?", 57 | ["id", "users", 1], 58 | ); 59 | assertEquals(result, [{ id: 1, name: "MYR🦕", is_top: 0 }]); 60 | }); 61 | 62 | testWithClient(async function testQueryErrorOccurred(client) { 63 | assertEquals(client.pool, { 64 | size: 0, 65 | maxSize: client.config.poolSize, 66 | available: 0, 67 | }); 68 | await assertThrowsAsync( 69 | () => client.query("select unknownfield from `users`"), 70 | Error, 71 | ); 72 | await client.query("select 1"); 73 | assertEquals(client.pool, { 74 | size: 1, 75 | maxSize: client.config.poolSize, 76 | available: 1, 77 | }); 78 | }); 79 | 80 | testWithClient(async function testQueryList(client) { 81 | const sql = "select ??,?? from ??"; 82 | let result = await client.query(sql, ["id", "name", "users"]); 83 | assertEquals(result, [ 84 | { id: 1, name: "MYR🦕" }, 85 | { id: 2, name: "MySQL" }, 86 | ]); 87 | }); 88 | 89 | testWithClient(async function testQueryTime(client) { 90 | const sql = `SELECT CAST("09:04:10" AS time) as time`; 91 | let result = await client.query(sql); 92 | assertEquals(result, [{ time: "09:04:10" }]); 93 | }); 94 | 95 | testWithClient(async function testQueryBigint(client) { 96 | await client.query(`DROP TABLE IF EXISTS test_bigint`); 97 | await client.query(`CREATE TABLE test_bigint ( 98 | id int(11) NOT NULL AUTO_INCREMENT, 99 | bigint_column bigint NOT NULL, 100 | PRIMARY KEY (id) 101 | ) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4`); 102 | 103 | const value = "9223372036854775807"; 104 | await client.execute( 105 | "INSERT INTO test_bigint(bigint_column) VALUES (?)", 106 | [value], 107 | ); 108 | 109 | const result = await client.query("SELECT bigint_column FROM test_bigint"); 110 | assertEquals(result, [{ bigint_column: BigInt(value) }]); 111 | }); 112 | 113 | testWithClient(async function testQueryDecimal(client) { 114 | await client.query(`DROP TABLE IF EXISTS test_decimal`); 115 | await client.query(`CREATE TABLE test_decimal ( 116 | id int(11) NOT NULL AUTO_INCREMENT, 117 | decimal_column decimal(65,30) NOT NULL, 118 | PRIMARY KEY (id) 119 | ) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4`); 120 | 121 | const value = "0.012345678901234567890123456789"; 122 | await client.execute( 123 | "INSERT INTO test_decimal(decimal_column) VALUES (?)", 124 | [value], 125 | ); 126 | 127 | const result = await client.query("SELECT decimal_column FROM test_decimal"); 128 | assertEquals(result, [{ decimal_column: value }]); 129 | }); 130 | 131 | testWithClient(async function testQueryDatetime(client) { 132 | await client.useConnection(async (connection) => { 133 | if (isMariaDB(connection) || semver.lt(connection.serverVersion, "5.6.0")) { 134 | return; 135 | } 136 | 137 | await client.query(`DROP TABLE IF EXISTS test_datetime`); 138 | await client.query(`CREATE TABLE test_datetime ( 139 | id int(11) NOT NULL AUTO_INCREMENT, 140 | datetime datetime(6) NOT NULL, 141 | PRIMARY KEY (id) 142 | ) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4`); 143 | const datetime = new Date(); 144 | await client.execute( 145 | ` 146 | INSERT INTO test_datetime (datetime) 147 | VALUES (?)`, 148 | [datetime], 149 | ); 150 | 151 | const [row] = await client.query("SELECT datetime FROM test_datetime"); 152 | assertEquals(row.datetime.toISOString(), datetime.toISOString()); // See https://github.com/denoland/deno/issues/6643 153 | }); 154 | }); 155 | 156 | testWithClient(async function testDelete(client) { 157 | let result = await client.execute(`delete from users where ?? = ?`, [ 158 | "id", 159 | 1, 160 | ]); 161 | assertEquals(result, { affectedRows: 1, lastInsertId: 0 }); 162 | }); 163 | 164 | testWithClient(async function testPool(client) { 165 | assertEquals(client.pool, { 166 | maxSize: client.config.poolSize, 167 | available: 0, 168 | size: 0, 169 | }); 170 | const expect = new Array(10).fill([{ "1": 1 }]); 171 | const result = await Promise.all(expect.map(() => client.query(`select 1`))); 172 | 173 | assertEquals(client.pool, { 174 | maxSize: client.config.poolSize, 175 | available: 3, 176 | size: 3, 177 | }); 178 | assertEquals(result, expect); 179 | }); 180 | 181 | testWithClient(async function testQueryOnClosed(client) { 182 | for (const i of [0, 0, 0]) { 183 | await assertThrowsAsync(async () => { 184 | await client.transaction(async (conn) => { 185 | conn.close(); 186 | await conn.query("SELECT 1"); 187 | }); 188 | }, ConnnectionError); 189 | } 190 | assertEquals(client.pool?.size, 0); 191 | await client.query("select 1"); 192 | }); 193 | 194 | testWithClient(async function testTransactionSuccess(client) { 195 | const success = await client.transaction(async (connection) => { 196 | await connection.execute("insert into users(name) values(?)", [ 197 | "transaction1", 198 | ]); 199 | await connection.execute("delete from users where id = ?", [2]); 200 | return true; 201 | }); 202 | assertEquals(true, success); 203 | const result = await client.query("select name,id from users"); 204 | assertEquals([{ name: "transaction1", id: 3 }], result); 205 | }); 206 | 207 | testWithClient(async function testTransactionRollback(client) { 208 | let success; 209 | await assertThrowsAsync(async () => { 210 | success = await client.transaction(async (connection) => { 211 | // Insert an existing id 212 | await connection.execute("insert into users(name,id) values(?,?)", [ 213 | "transaction2", 214 | 3, 215 | ]); 216 | return true; 217 | }); 218 | }); 219 | assertEquals(undefined, success); 220 | const result = await client.query("select name from users"); 221 | assertEquals([{ name: "transaction1" }], result); 222 | }); 223 | 224 | testWithClient(async function testIdleTimeout(client) { 225 | assertEquals(client.pool, { 226 | maxSize: 3, 227 | available: 0, 228 | size: 0, 229 | }); 230 | await Promise.all(new Array(10).fill(0).map(() => client.query("select 1"))); 231 | assertEquals(client.pool, { 232 | maxSize: 3, 233 | available: 3, 234 | size: 3, 235 | }); 236 | await delay(500); 237 | assertEquals(client.pool, { 238 | maxSize: 3, 239 | available: 3, 240 | size: 3, 241 | }); 242 | await client.query("select 1"); 243 | await delay(500); 244 | assertEquals(client.pool, { 245 | maxSize: 3, 246 | available: 1, 247 | size: 1, 248 | }); 249 | await delay(500); 250 | assertEquals(client.pool, { 251 | maxSize: 3, 252 | available: 0, 253 | size: 0, 254 | }); 255 | }, { 256 | idleTimeout: 750, 257 | }); 258 | 259 | testWithClient(async function testReadTimeout(client) { 260 | await client.execute("select sleep(0.3)"); 261 | 262 | await assertThrowsAsync(async () => { 263 | await client.execute("select sleep(0.7)"); 264 | }, ResponseTimeoutError); 265 | 266 | assertEquals(client.pool, { 267 | maxSize: 3, 268 | available: 0, 269 | size: 0, 270 | }); 271 | }, { 272 | timeout: 500, 273 | }); 274 | 275 | testWithClient(async function testLargeQueryAndResponse(client) { 276 | function buildLargeString(len: number) { 277 | let str = ""; 278 | for (let i = 0; i < len; i++) { 279 | str += i % 10; 280 | } 281 | return str; 282 | } 283 | const largeString = buildLargeString(512 * 1024); 284 | assertEquals( 285 | await client.query(`select "${largeString}" as str`), 286 | [{ str: largeString }], 287 | ); 288 | }); 289 | 290 | testWithClient(async function testExecuteIterator(client) { 291 | await client.useConnection(async (conn) => { 292 | await conn.execute(`DROP TABLE IF EXISTS numbers`); 293 | await conn.execute(`CREATE TABLE numbers (num INT NOT NULL)`); 294 | await conn.execute( 295 | `INSERT INTO numbers (num) VALUES ${ 296 | new Array(64).fill(0).map((v, idx) => `(${idx})`).join(",") 297 | }`, 298 | ); 299 | const r = await conn.execute(`SELECT num FROM numbers`, [], true); 300 | let count = 0; 301 | for await (const row of r.iterator) { 302 | assertEquals(row.num, count); 303 | count++; 304 | } 305 | assertEquals(count, 64); 306 | }); 307 | }); 308 | 309 | // For MySQL 8, the default auth plugin is `caching_sha2_password`. Create user 310 | // using `mysql_native_password` to test Authentication Method Mismatch. 311 | testWithClient(async function testCreateUserWithMysqlNativePassword(client) { 312 | const { version } = (await client.query(`SELECT VERSION() as version`))[0]; 313 | if (version.startsWith("8.")) { 314 | // MySQL 8 does not have `PASSWORD()` function 315 | await client.execute( 316 | `CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password BY 'testpassword'`, 317 | ); 318 | } else { 319 | await client.execute( 320 | `CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password`, 321 | ); 322 | await client.execute( 323 | `SET PASSWORD FOR 'testuser'@'%' = PASSWORD('testpassword')`, 324 | ); 325 | } 326 | await client.execute(`GRANT ALL ON test.* TO 'testuser'@'%'`); 327 | }); 328 | 329 | testWithClient(async function testConnectWithMysqlNativePassword(client) { 330 | assertEquals( 331 | await client.query(`SELECT CURRENT_USER() AS user`), 332 | [{ user: "testuser@%" }], 333 | ); 334 | }, { username: "testuser", password: "testpassword" }); 335 | 336 | testWithClient(async function testDropUserWithMysqlNativePassword(client) { 337 | await client.execute(`DROP USER 'testuser'@'%'`); 338 | }); 339 | 340 | testWithClient(async function testSelectEmptyString(client) { 341 | assertEquals( 342 | await client.query(`SELECT '' AS a`), 343 | [{ a: "" }], 344 | ); 345 | assertEquals( 346 | await client.query(`SELECT '' AS a, '' AS b, '' AS c`), 347 | [{ a: "", b: "", c: "" }], 348 | ); 349 | assertEquals( 350 | await client.query(`SELECT '' AS a, 'b' AS b, '' AS c`), 351 | [{ a: "", b: "b", c: "" }], 352 | ); 353 | }); 354 | 355 | registerTests(); 356 | 357 | Deno.test("configLogger()", async () => { 358 | let logCount = 0; 359 | const fakeHandler = new class extends stdlog.handlers.BaseHandler { 360 | constructor() { 361 | super("INFO"); 362 | } 363 | log(msg: string) { 364 | logCount++; 365 | } 366 | }(); 367 | 368 | await stdlog.setup({ 369 | handlers: { 370 | fake: fakeHandler, 371 | }, 372 | loggers: { 373 | mysql: { 374 | handlers: ["fake"], 375 | }, 376 | }, 377 | }); 378 | await configLogger({ logger: stdlog.getLogger("mysql") }); 379 | log.info("Test log"); 380 | assertEquals(logCount, 1); 381 | 382 | await configLogger({ enable: false }); 383 | log.info("Test log"); 384 | assertEquals(logCount, 1); 385 | }); 386 | 387 | await createTestDB(); 388 | 389 | await new Promise((r) => setTimeout(r, 0)); 390 | // Workaround to https://github.com/denoland/deno/issues/7844 391 | -------------------------------------------------------------------------------- /test.util.ts: -------------------------------------------------------------------------------- 1 | import { Client, ClientConfig, Connection } from "./mod.ts"; 2 | import { assertEquals, parse } from "./test.deps.ts"; 3 | 4 | const { DB_PORT, DB_NAME, DB_PASSWORD, DB_USER, DB_HOST, DB_SOCKPATH } = Deno 5 | .env.toObject(); 6 | const port = DB_PORT ? parseInt(DB_PORT) : 3306; 7 | const db = DB_NAME || "test"; 8 | const password = DB_PASSWORD || "root"; 9 | const username = DB_USER || "root"; 10 | const hostname = DB_HOST || "127.0.0.1"; 11 | const sockPath = DB_SOCKPATH || "/var/run/mysqld/mysqld.sock"; 12 | const testMethods = 13 | Deno.env.get("TEST_METHODS")?.split(",") as ("tcp" | "unix")[] || ["tcp"]; 14 | const unixSocketOnly = testMethods.length === 1 && testMethods[0] === "unix"; 15 | 16 | const config: ClientConfig = { 17 | timeout: 10000, 18 | poolSize: 3, 19 | debug: true, 20 | hostname, 21 | username, 22 | port, 23 | db, 24 | charset: "utf8mb4", 25 | password, 26 | }; 27 | 28 | const tests: (Parameters)[] = []; 29 | 30 | export function testWithClient( 31 | fn: (client: Client) => void | Promise, 32 | overrideConfig?: ClientConfig, 33 | ): void { 34 | tests.push([fn, overrideConfig]); 35 | } 36 | 37 | export function registerTests(methods: ("tcp" | "unix")[] = testMethods) { 38 | if (methods!.includes("tcp")) { 39 | tests.forEach(([fn, overrideConfig]) => { 40 | Deno.test({ 41 | name: fn.name + " (TCP)", 42 | async fn() { 43 | await test({ ...config, ...overrideConfig }, fn); 44 | }, 45 | }); 46 | }); 47 | } 48 | if (methods!.includes("unix")) { 49 | tests.forEach(([fn, overrideConfig]) => { 50 | Deno.test({ 51 | name: fn.name + " (UNIX domain socket)", 52 | async fn() { 53 | await test( 54 | { ...config, socketPath: sockPath, ...overrideConfig }, 55 | fn, 56 | ); 57 | }, 58 | }); 59 | }); 60 | } 61 | } 62 | 63 | async function test( 64 | config: ClientConfig, 65 | fn: (client: Client) => void | Promise, 66 | ) { 67 | const resources = Deno.resources(); 68 | const client = await new Client().connect(config); 69 | try { 70 | await fn(client); 71 | } finally { 72 | await client.close(); 73 | } 74 | assertEquals( 75 | Deno.resources(), 76 | resources, 77 | "The client is leaking resources", 78 | ); 79 | } 80 | 81 | export async function createTestDB() { 82 | const client = await new Client().connect({ 83 | ...config, 84 | poolSize: 1, 85 | db: undefined, 86 | socketPath: unixSocketOnly ? sockPath : undefined, 87 | }); 88 | await client.execute(`CREATE DATABASE IF NOT EXISTS ${db}`); 89 | await client.close(); 90 | } 91 | 92 | export function isMariaDB(connection: Connection): boolean { 93 | return connection.serverVersion.includes("MariaDB"); 94 | } 95 | 96 | export function delay(ms: number) { 97 | return new Promise((resolve) => setTimeout(resolve, ms)); 98 | } 99 | --------------------------------------------------------------------------------