From a3171801d3596f009f4d4067604056943120d298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 9 Apr 2024 00:13:53 +0200 Subject: [PATCH 01/38] added vendor --- vendor/deno.land/std@0.51.0/fmt/colors.ts | 207 +++++++ .../deno.land/std@0.51.0/testing/asserts.ts | 381 +++++++++++++ vendor/deno.land/std@0.51.0/testing/diff.ts | 221 ++++++++ vendor/deno.land/std@0.77.0/fmt/colors.ts | 522 ++++++++++++++++++ .../deno.land/x/bytes_formater@v1.4.0/deps.ts | 4 + .../x/bytes_formater@v1.4.0/format.ts | 46 ++ .../deno.land/x/bytes_formater@v1.4.0/mod.ts | 2 + vendor/deno.land/x/sql_builder@v1.9.1/deps.ts | 5 + vendor/deno.land/x/sql_builder@v1.9.1/join.ts | 30 + vendor/deno.land/x/sql_builder@v1.9.1/mod.ts | 5 + .../deno.land/x/sql_builder@v1.9.1/order.ts | 18 + .../deno.land/x/sql_builder@v1.9.1/query.ts | 222 ++++++++ vendor/deno.land/x/sql_builder@v1.9.1/util.ts | 84 +++ .../deno.land/x/sql_builder@v1.9.1/where.ts | 121 ++++ 14 files changed, 1868 insertions(+) create mode 100644 vendor/deno.land/std@0.51.0/fmt/colors.ts create mode 100644 vendor/deno.land/std@0.51.0/testing/asserts.ts create mode 100644 vendor/deno.land/std@0.51.0/testing/diff.ts create mode 100644 vendor/deno.land/std@0.77.0/fmt/colors.ts create mode 100644 vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts create mode 100644 vendor/deno.land/x/bytes_formater@v1.4.0/format.ts create mode 100644 vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts create mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/deps.ts create mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/join.ts create mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/mod.ts create mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/order.ts create mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/query.ts create mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/util.ts create mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/where.ts diff --git a/vendor/deno.land/std@0.51.0/fmt/colors.ts b/vendor/deno.land/std@0.51.0/fmt/colors.ts new file mode 100644 index 0000000..64b4458 --- /dev/null +++ b/vendor/deno.land/std@0.51.0/fmt/colors.ts @@ -0,0 +1,207 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +/** + * A module to print ANSI terminal colors. Inspired by chalk, kleur, and colors + * on npm. + * + * ``` + * import { bgBlue, red, bold } from "https://deno.land/std/fmt/colors.ts"; + * console.log(bgBlue(red(bold("Hello world!")))); + * ``` + * + * This module supports `NO_COLOR` environmental variable disabling any coloring + * if `NO_COLOR` is set. + */ +const { noColor } = Deno; + +interface Code { + open: string; + close: string; + regexp: RegExp; +} + +/** RGB 8-bits per channel. Each in range `0->255` or `0x00->0xff` */ +interface Rgb { + r: number; + g: number; + b: number; +} + +let enabled = !noColor; + +export function setColorEnabled(value: boolean): void { + if (noColor) { + return; + } + + enabled = value; +} + +export function getColorEnabled(): boolean { + return enabled; +} + +function code(open: number[], close: number): Code { + return { + open: `\x1b[${open.join(";")}m`, + close: `\x1b[${close}m`, + regexp: new RegExp(`\\x1b\\[${close}m`, "g"), + }; +} + +function run(str: string, code: Code): string { + return enabled + ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` + : str; +} + +export function reset(str: string): string { + return run(str, code([0], 0)); +} + +export function bold(str: string): string { + return run(str, code([1], 22)); +} + +export function dim(str: string): string { + return run(str, code([2], 22)); +} + +export function italic(str: string): string { + return run(str, code([3], 23)); +} + +export function underline(str: string): string { + return run(str, code([4], 24)); +} + +export function inverse(str: string): string { + return run(str, code([7], 27)); +} + +export function hidden(str: string): string { + return run(str, code([8], 28)); +} + +export function strikethrough(str: string): string { + return run(str, code([9], 29)); +} + +export function black(str: string): string { + return run(str, code([30], 39)); +} + +export function red(str: string): string { + return run(str, code([31], 39)); +} + +export function green(str: string): string { + return run(str, code([32], 39)); +} + +export function yellow(str: string): string { + return run(str, code([33], 39)); +} + +export function blue(str: string): string { + return run(str, code([34], 39)); +} + +export function magenta(str: string): string { + return run(str, code([35], 39)); +} + +export function cyan(str: string): string { + return run(str, code([36], 39)); +} + +export function white(str: string): string { + return run(str, code([37], 39)); +} + +export function gray(str: string): string { + return run(str, code([90], 39)); +} + +export function bgBlack(str: string): string { + return run(str, code([40], 49)); +} + +export function bgRed(str: string): string { + return run(str, code([41], 49)); +} + +export function bgGreen(str: string): string { + return run(str, code([42], 49)); +} + +export function bgYellow(str: string): string { + return run(str, code([43], 49)); +} + +export function bgBlue(str: string): string { + return run(str, code([44], 49)); +} + +export function bgMagenta(str: string): string { + return run(str, code([45], 49)); +} + +export function bgCyan(str: string): string { + return run(str, code([46], 49)); +} + +export function bgWhite(str: string): string { + return run(str, code([47], 49)); +} + +/* Special Color Sequences */ + +function clampAndTruncate(n: number, max = 255, min = 0): number { + return Math.trunc(Math.max(Math.min(n, max), min)); +} + +/** Set text color using paletted 8bit colors. + * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit */ +export function rgb8(str: string, color: number): string { + return run(str, code([38, 5, clampAndTruncate(color)], 39)); +} + +/** Set background color using paletted 8bit colors. + * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit */ +export function bgRgb8(str: string, color: number): string { + return run(str, code([48, 5, clampAndTruncate(color)], 49)); +} + +/** Set text color using 24bit rgb. */ +export function rgb24(str: string, color: Rgb): string { + return run( + str, + code( + [ + 38, + 2, + clampAndTruncate(color.r), + clampAndTruncate(color.g), + clampAndTruncate(color.b), + ], + 39 + ) + ); +} + +/** Set background color using 24bit rgb. */ +export function bgRgb24(str: string, color: Rgb): string { + return run( + str, + code( + [ + 48, + 2, + clampAndTruncate(color.r), + clampAndTruncate(color.g), + clampAndTruncate(color.b), + ], + 49 + ) + ); +} diff --git a/vendor/deno.land/std@0.51.0/testing/asserts.ts b/vendor/deno.land/std@0.51.0/testing/asserts.ts new file mode 100644 index 0000000..6dd3af2 --- /dev/null +++ b/vendor/deno.land/std@0.51.0/testing/asserts.ts @@ -0,0 +1,381 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { red, green, white, gray, bold } from "../fmt/colors.ts"; +import diff, { DiffType, DiffResult } from "./diff.ts"; + +const CAN_NOT_DISPLAY = "[Cannot display]"; + +interface Constructor { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new (...args: any[]): any; +} + +export class AssertionError extends Error { + constructor(message: string) { + super(message); + this.name = "AssertionError"; + } +} + +function format(v: unknown): string { + let string = Deno.inspect(v); + if (typeof v == "string") { + string = `"${string.replace(/(?=["\\])/g, "\\")}"`; + } + return string; +} + +function createColor(diffType: DiffType): (s: string) => string { + switch (diffType) { + case DiffType.added: + return (s: string): string => green(bold(s)); + case DiffType.removed: + return (s: string): string => red(bold(s)); + default: + return white; + } +} + +function createSign(diffType: DiffType): string { + switch (diffType) { + case DiffType.added: + return "+ "; + case DiffType.removed: + return "- "; + default: + return " "; + } +} + +function buildMessage(diffResult: ReadonlyArray>): string[] { + const messages: string[] = []; + messages.push(""); + messages.push(""); + messages.push( + ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${green( + bold("Expected") + )}` + ); + messages.push(""); + messages.push(""); + diffResult.forEach((result: DiffResult): void => { + const c = createColor(result.type); + messages.push(c(`${createSign(result.type)}${result.value}`)); + }); + messages.push(""); + + return messages; +} + +function isKeyedCollection(x: unknown): x is Set { + return [Symbol.iterator, "size"].every((k) => k in (x as Set)); +} + +export function equal(c: unknown, d: unknown): boolean { + const seen = new Map(); + return (function compare(a: unknown, b: unknown): boolean { + // Have to render RegExp & Date for string comparison + // unless it's mistreated as object + if ( + a && + b && + ((a instanceof RegExp && b instanceof RegExp) || + (a instanceof Date && b instanceof Date)) + ) { + return String(a) === String(b); + } + if (Object.is(a, b)) { + return true; + } + if (a && typeof a === "object" && b && typeof b === "object") { + if (seen.get(a) === b) { + return true; + } + if (Object.keys(a || {}).length !== Object.keys(b || {}).length) { + return false; + } + if (isKeyedCollection(a) && isKeyedCollection(b)) { + if (a.size !== b.size) { + return false; + } + + let unmatchedEntries = a.size; + + for (const [aKey, aValue] of a.entries()) { + for (const [bKey, bValue] of b.entries()) { + /* Given that Map keys can be references, we need + * to ensure that they are also deeply equal */ + if ( + (aKey === aValue && bKey === bValue && compare(aKey, bKey)) || + (compare(aKey, bKey) && compare(aValue, bValue)) + ) { + unmatchedEntries--; + } + } + } + + return unmatchedEntries === 0; + } + const merged = { ...a, ...b }; + for (const key in merged) { + type Key = keyof typeof merged; + if (!compare(a && a[key as Key], b && b[key as Key])) { + return false; + } + } + seen.set(a, b); + return true; + } + return false; + })(c, d); +} + +/** Make an assertion, if not `true`, then throw. */ +export function assert(expr: unknown, msg = ""): asserts expr { + if (!expr) { + throw new AssertionError(msg); + } +} + +/** + * Make an assertion that `actual` and `expected` are equal, deeply. If not + * deeply equal, then throw. + */ +export function assertEquals( + actual: unknown, + expected: unknown, + msg?: string +): void { + if (equal(actual, expected)) { + return; + } + let message = ""; + const actualString = format(actual); + const expectedString = format(expected); + try { + const diffResult = diff( + actualString.split("\n"), + expectedString.split("\n") + ); + message = buildMessage(diffResult).join("\n"); + } catch (e) { + message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`; + } + if (msg) { + message = msg; + } + throw new AssertionError(message); +} + +/** + * Make an assertion that `actual` and `expected` are not equal, deeply. + * If not then throw. + */ +export function assertNotEquals( + actual: unknown, + expected: unknown, + msg?: string +): void { + if (!equal(actual, expected)) { + return; + } + let actualString: string; + let expectedString: string; + try { + actualString = String(actual); + } catch (e) { + actualString = "[Cannot display]"; + } + try { + expectedString = String(expected); + } catch (e) { + expectedString = "[Cannot display]"; + } + if (!msg) { + msg = `actual: ${actualString} expected: ${expectedString}`; + } + throw new AssertionError(msg); +} + +/** + * Make an assertion that `actual` and `expected` are strictly equal. If + * not then throw. + */ +export function assertStrictEq( + actual: unknown, + expected: unknown, + msg?: string +): void { + if (actual !== expected) { + let actualString: string; + let expectedString: string; + try { + actualString = String(actual); + } catch (e) { + actualString = "[Cannot display]"; + } + try { + expectedString = String(expected); + } catch (e) { + expectedString = "[Cannot display]"; + } + if (!msg) { + msg = `actual: ${actualString} expected: ${expectedString}`; + } + throw new AssertionError(msg); + } +} + +/** + * Make an assertion that actual contains expected. If not + * then thrown. + */ +export function assertStrContains( + actual: string, + expected: string, + msg?: string +): void { + if (!actual.includes(expected)) { + if (!msg) { + msg = `actual: "${actual}" expected to contains: "${expected}"`; + } + throw new AssertionError(msg); + } +} + +/** + * Make an assertion that `actual` contains the `expected` values + * If not then thrown. + */ +export function assertArrayContains( + actual: unknown[], + expected: unknown[], + msg?: string +): void { + const missing: unknown[] = []; + for (let i = 0; i < expected.length; i++) { + let found = false; + for (let j = 0; j < actual.length; j++) { + if (equal(expected[i], actual[j])) { + found = true; + break; + } + } + if (!found) { + missing.push(expected[i]); + } + } + if (missing.length === 0) { + return; + } + if (!msg) { + msg = `actual: "${actual}" expected to contains: "${expected}"`; + msg += "\n"; + msg += `missing: ${missing}`; + } + throw new AssertionError(msg); +} + +/** + * Make an assertion that `actual` match RegExp `expected`. If not + * then thrown + */ +export function assertMatch( + actual: string, + expected: RegExp, + msg?: string +): void { + if (!expected.test(actual)) { + if (!msg) { + msg = `actual: "${actual}" expected to match: "${expected}"`; + } + throw new AssertionError(msg); + } +} + +/** + * Forcefully throws a failed assertion + */ +export function fail(msg?: string): void { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + assert(false, `Failed assertion${msg ? `: ${msg}` : "."}`); +} + +/** Executes a function, expecting it to throw. If it does not, then it + * throws. An error class and a string that should be included in the + * error message can also be asserted. + */ +export function assertThrows( + fn: () => void, + ErrorClass?: Constructor, + msgIncludes = "", + msg?: string +): Error { + let doesThrow = false; + let error = null; + try { + fn(); + } catch (e) { + if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { + msg = `Expected error to be instance of "${ErrorClass.name}", but was "${ + e.constructor.name + }"${msg ? `: ${msg}` : "."}`; + throw new AssertionError(msg); + } + if (msgIncludes && !e.message.includes(msgIncludes)) { + msg = `Expected error message to include "${msgIncludes}", but got "${ + e.message + }"${msg ? `: ${msg}` : "."}`; + throw new AssertionError(msg); + } + doesThrow = true; + error = e; + } + if (!doesThrow) { + msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; + throw new AssertionError(msg); + } + return error; +} + +export async function assertThrowsAsync( + fn: () => Promise, + ErrorClass?: Constructor, + msgIncludes = "", + msg?: string +): Promise { + let doesThrow = false; + let error = null; + try { + await fn(); + } catch (e) { + if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { + msg = `Expected error to be instance of "${ErrorClass.name}", but got "${ + e.name + }"${msg ? `: ${msg}` : "."}`; + throw new AssertionError(msg); + } + if (msgIncludes && !e.message.includes(msgIncludes)) { + msg = `Expected error message to include "${msgIncludes}", but got "${ + e.message + }"${msg ? `: ${msg}` : "."}`; + throw new AssertionError(msg); + } + doesThrow = true; + error = e; + } + if (!doesThrow) { + msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; + throw new AssertionError(msg); + } + return error; +} + +/** Use this to stub out methods that will throw when invoked. */ +export function unimplemented(msg?: string): never { + throw new AssertionError(msg || "unimplemented"); +} + +/** Use this to assert unreachable code. */ +export function unreachable(): never { + throw new AssertionError("unreachable"); +} diff --git a/vendor/deno.land/std@0.51.0/testing/diff.ts b/vendor/deno.land/std@0.51.0/testing/diff.ts new file mode 100644 index 0000000..97baa08 --- /dev/null +++ b/vendor/deno.land/std@0.51.0/testing/diff.ts @@ -0,0 +1,221 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +interface FarthestPoint { + y: number; + id: number; +} + +export enum DiffType { + removed = "removed", + common = "common", + added = "added", +} + +export interface DiffResult { + type: DiffType; + value: T; +} + +const REMOVED = 1; +const COMMON = 2; +const ADDED = 3; + +function createCommon(A: T[], B: T[], reverse?: boolean): T[] { + const common = []; + if (A.length === 0 || B.length === 0) return []; + for (let i = 0; i < Math.min(A.length, B.length); i += 1) { + if ( + A[reverse ? A.length - i - 1 : i] === B[reverse ? B.length - i - 1 : i] + ) { + common.push(A[reverse ? A.length - i - 1 : i]); + } else { + return common; + } + } + return common; +} + +export default function diff(A: T[], B: T[]): Array> { + const prefixCommon = createCommon(A, B); + const suffixCommon = createCommon( + A.slice(prefixCommon.length), + B.slice(prefixCommon.length), + true + ).reverse(); + A = suffixCommon.length + ? A.slice(prefixCommon.length, -suffixCommon.length) + : A.slice(prefixCommon.length); + B = suffixCommon.length + ? B.slice(prefixCommon.length, -suffixCommon.length) + : B.slice(prefixCommon.length); + const swapped = B.length > A.length; + [A, B] = swapped ? [B, A] : [A, B]; + const M = A.length; + const N = B.length; + if (!M && !N && !suffixCommon.length && !prefixCommon.length) return []; + if (!N) { + return [ + ...prefixCommon.map( + (c): DiffResult => ({ type: DiffType.common, value: c }) + ), + ...A.map( + (a): DiffResult => ({ + type: swapped ? DiffType.added : DiffType.removed, + value: a, + }) + ), + ...suffixCommon.map( + (c): DiffResult => ({ type: DiffType.common, value: c }) + ), + ]; + } + const offset = N; + const delta = M - N; + const size = M + N + 1; + const fp = new Array(size).fill({ y: -1 }); + /** + * INFO: + * This buffer is used to save memory and improve performance. + * The first half is used to save route and last half is used to save diff + * type. + * This is because, when I kept new uint8array area to save type,performance + * worsened. + */ + const routes = new Uint32Array((M * N + size + 1) * 2); + const diffTypesPtrOffset = routes.length / 2; + let ptr = 0; + let p = -1; + + function backTrace( + A: T[], + B: T[], + current: FarthestPoint, + swapped: boolean + ): Array<{ + type: DiffType; + value: T; + }> { + const M = A.length; + const N = B.length; + const result = []; + let a = M - 1; + let b = N - 1; + let j = routes[current.id]; + let type = routes[current.id + diffTypesPtrOffset]; + while (true) { + if (!j && !type) break; + const prev = j; + if (type === REMOVED) { + result.unshift({ + type: swapped ? DiffType.removed : DiffType.added, + value: B[b], + }); + b -= 1; + } else if (type === ADDED) { + result.unshift({ + type: swapped ? DiffType.added : DiffType.removed, + value: A[a], + }); + a -= 1; + } else { + result.unshift({ type: DiffType.common, value: A[a] }); + a -= 1; + b -= 1; + } + j = routes[prev]; + type = routes[prev + diffTypesPtrOffset]; + } + return result; + } + + function createFP( + slide: FarthestPoint, + down: FarthestPoint, + k: number, + M: number + ): FarthestPoint { + if (slide && slide.y === -1 && down && down.y === -1) { + return { y: 0, id: 0 }; + } + if ( + (down && down.y === -1) || + k === M || + (slide && slide.y) > (down && down.y) + 1 + ) { + const prev = slide.id; + ptr++; + routes[ptr] = prev; + routes[ptr + diffTypesPtrOffset] = ADDED; + return { y: slide.y, id: ptr }; + } else { + const prev = down.id; + ptr++; + routes[ptr] = prev; + routes[ptr + diffTypesPtrOffset] = REMOVED; + return { y: down.y + 1, id: ptr }; + } + } + + function snake( + k: number, + slide: FarthestPoint, + down: FarthestPoint, + _offset: number, + A: T[], + B: T[] + ): FarthestPoint { + const M = A.length; + const N = B.length; + if (k < -N || M < k) return { y: -1, id: -1 }; + const fp = createFP(slide, down, k, M); + while (fp.y + k < M && fp.y < N && A[fp.y + k] === B[fp.y]) { + const prev = fp.id; + ptr++; + fp.id = ptr; + fp.y += 1; + routes[ptr] = prev; + routes[ptr + diffTypesPtrOffset] = COMMON; + } + return fp; + } + + while (fp[delta + offset].y < N) { + p = p + 1; + for (let k = -p; k < delta; ++k) { + fp[k + offset] = snake( + k, + fp[k - 1 + offset], + fp[k + 1 + offset], + offset, + A, + B + ); + } + for (let k = delta + p; k > delta; --k) { + fp[k + offset] = snake( + k, + fp[k - 1 + offset], + fp[k + 1 + offset], + offset, + A, + B + ); + } + fp[delta + offset] = snake( + delta, + fp[delta - 1 + offset], + fp[delta + 1 + offset], + offset, + A, + B + ); + } + return [ + ...prefixCommon.map( + (c): DiffResult => ({ type: DiffType.common, value: c }) + ), + ...backTrace(A, B, fp[delta + offset], swapped), + ...suffixCommon.map( + (c): DiffResult => ({ type: DiffType.common, value: c }) + ), + ]; +} diff --git a/vendor/deno.land/std@0.77.0/fmt/colors.ts b/vendor/deno.land/std@0.77.0/fmt/colors.ts new file mode 100644 index 0000000..6c98671 --- /dev/null +++ b/vendor/deno.land/std@0.77.0/fmt/colors.ts @@ -0,0 +1,522 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +/** A module to print ANSI terminal colors. Inspired by chalk, kleur, and colors + * on npm. + * + * ``` + * import { bgBlue, red, bold } from "https://deno.land/std/fmt/colors.ts"; + * console.log(bgBlue(red(bold("Hello world!")))); + * ``` + * + * This module supports `NO_COLOR` environmental variable disabling any coloring + * if `NO_COLOR` is set. + * + * This module is browser compatible. */ + +const noColor = globalThis.Deno?.noColor ?? true; + +interface Code { + open: string; + close: string; + regexp: RegExp; +} + +/** RGB 8-bits per channel. Each in range `0->255` or `0x00->0xff` */ +interface Rgb { + r: number; + g: number; + b: number; +} + +let enabled = !noColor; + +/** + * Set changing text color to enabled or disabled + * @param value + */ +export function setColorEnabled(value: boolean): void { + if (noColor) { + return; + } + + enabled = value; +} + +/** Get whether text color change is enabled or disabled. */ +export function getColorEnabled(): boolean { + return enabled; +} + +/** + * Builds color code + * @param open + * @param close + */ +function code(open: number[], close: number): Code { + return { + open: `\x1b[${open.join(";")}m`, + close: `\x1b[${close}m`, + regexp: new RegExp(`\\x1b\\[${close}m`, "g"), + }; +} + +/** + * Applies color and background based on color code and its associated text + * @param str text to apply color settings to + * @param code color code to apply + */ +function run(str: string, code: Code): string { + return enabled + ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` + : str; +} + +/** + * Reset the text modified + * @param str text to reset + */ +export function reset(str: string): string { + return run(str, code([0], 0)); +} + +/** + * Make the text bold. + * @param str text to make bold + */ +export function bold(str: string): string { + return run(str, code([1], 22)); +} + +/** + * The text emits only a small amount of light. + * @param str text to dim + */ +export function dim(str: string): string { + return run(str, code([2], 22)); +} + +/** + * Make the text italic. + * @param str text to make italic + */ +export function italic(str: string): string { + return run(str, code([3], 23)); +} + +/** + * Make the text underline. + * @param str text to underline + */ +export function underline(str: string): string { + return run(str, code([4], 24)); +} + +/** + * Invert background color and text color. + * @param str text to invert its color + */ +export function inverse(str: string): string { + return run(str, code([7], 27)); +} + +/** + * Make the text hidden. + * @param str text to hide + */ +export function hidden(str: string): string { + return run(str, code([8], 28)); +} + +/** + * Put horizontal line through the center of the text. + * @param str text to strike through + */ +export function strikethrough(str: string): string { + return run(str, code([9], 29)); +} + +/** + * Set text color to black. + * @param str text to make black + */ +export function black(str: string): string { + return run(str, code([30], 39)); +} + +/** + * Set text color to red. + * @param str text to make red + */ +export function red(str: string): string { + return run(str, code([31], 39)); +} + +/** + * Set text color to green. + * @param str text to make green + */ +export function green(str: string): string { + return run(str, code([32], 39)); +} + +/** + * Set text color to yellow. + * @param str text to make yellow + */ +export function yellow(str: string): string { + return run(str, code([33], 39)); +} + +/** + * Set text color to blue. + * @param str text to make blue + */ +export function blue(str: string): string { + return run(str, code([34], 39)); +} + +/** + * Set text color to magenta. + * @param str text to make magenta + */ +export function magenta(str: string): string { + return run(str, code([35], 39)); +} + +/** + * Set text color to cyan. + * @param str text to make cyan + */ +export function cyan(str: string): string { + return run(str, code([36], 39)); +} + +/** + * Set text color to white. + * @param str text to make white + */ +export function white(str: string): string { + return run(str, code([37], 39)); +} + +/** + * Set text color to gray. + * @param str text to make gray + */ +export function gray(str: string): string { + return brightBlack(str); +} + +/** + * Set text color to bright black. + * @param str text to make bright-black + */ +export function brightBlack(str: string): string { + return run(str, code([90], 39)); +} + +/** + * Set text color to bright red. + * @param str text to make bright-red + */ +export function brightRed(str: string): string { + return run(str, code([91], 39)); +} + +/** + * Set text color to bright green. + * @param str text to make bright-green + */ +export function brightGreen(str: string): string { + return run(str, code([92], 39)); +} + +/** + * Set text color to bright yellow. + * @param str text to make bright-yellow + */ +export function brightYellow(str: string): string { + return run(str, code([93], 39)); +} + +/** + * Set text color to bright blue. + * @param str text to make bright-blue + */ +export function brightBlue(str: string): string { + return run(str, code([94], 39)); +} + +/** + * Set text color to bright magenta. + * @param str text to make bright-magenta + */ +export function brightMagenta(str: string): string { + return run(str, code([95], 39)); +} + +/** + * Set text color to bright cyan. + * @param str text to make bright-cyan + */ +export function brightCyan(str: string): string { + return run(str, code([96], 39)); +} + +/** + * Set text color to bright white. + * @param str text to make bright-white + */ +export function brightWhite(str: string): string { + return run(str, code([97], 39)); +} + +/** + * Set background color to black. + * @param str text to make its background black + */ +export function bgBlack(str: string): string { + return run(str, code([40], 49)); +} + +/** + * Set background color to red. + * @param str text to make its background red + */ +export function bgRed(str: string): string { + return run(str, code([41], 49)); +} + +/** + * Set background color to green. + * @param str text to make its background green + */ +export function bgGreen(str: string): string { + return run(str, code([42], 49)); +} + +/** + * Set background color to yellow. + * @param str text to make its background yellow + */ +export function bgYellow(str: string): string { + return run(str, code([43], 49)); +} + +/** + * Set background color to blue. + * @param str text to make its background blue + */ +export function bgBlue(str: string): string { + return run(str, code([44], 49)); +} + +/** + * Set background color to magenta. + * @param str text to make its background magenta + */ +export function bgMagenta(str: string): string { + return run(str, code([45], 49)); +} + +/** + * Set background color to cyan. + * @param str text to make its background cyan + */ +export function bgCyan(str: string): string { + return run(str, code([46], 49)); +} + +/** + * Set background color to white. + * @param str text to make its background white + */ +export function bgWhite(str: string): string { + return run(str, code([47], 49)); +} + +/** + * Set background color to bright black. + * @param str text to make its background bright-black + */ +export function bgBrightBlack(str: string): string { + return run(str, code([100], 49)); +} + +/** + * Set background color to bright red. + * @param str text to make its background bright-red + */ +export function bgBrightRed(str: string): string { + return run(str, code([101], 49)); +} + +/** + * Set background color to bright green. + * @param str text to make its background bright-green + */ +export function bgBrightGreen(str: string): string { + return run(str, code([102], 49)); +} + +/** + * Set background color to bright yellow. + * @param str text to make its background bright-yellow + */ +export function bgBrightYellow(str: string): string { + return run(str, code([103], 49)); +} + +/** + * Set background color to bright blue. + * @param str text to make its background bright-blue + */ +export function bgBrightBlue(str: string): string { + return run(str, code([104], 49)); +} + +/** + * Set background color to bright magenta. + * @param str text to make its background bright-magenta + */ +export function bgBrightMagenta(str: string): string { + return run(str, code([105], 49)); +} + +/** + * Set background color to bright cyan. + * @param str text to make its background bright-cyan + */ +export function bgBrightCyan(str: string): string { + return run(str, code([106], 49)); +} + +/** + * Set background color to bright white. + * @param str text to make its background bright-white + */ +export function bgBrightWhite(str: string): string { + return run(str, code([107], 49)); +} + +/* Special Color Sequences */ + +/** + * Clam and truncate color codes + * @param n + * @param max number to truncate to + * @param min number to truncate from + */ +function clampAndTruncate(n: number, max = 255, min = 0): number { + return Math.trunc(Math.max(Math.min(n, max), min)); +} + +/** + * Set text color using paletted 8bit colors. + * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + * @param str text color to apply paletted 8bit colors to + * @param color code + */ +export function rgb8(str: string, color: number): string { + return run(str, code([38, 5, clampAndTruncate(color)], 39)); +} + +/** + * Set background color using paletted 8bit colors. + * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + * @param str text color to apply paletted 8bit background colors to + * @param color code + */ +export function bgRgb8(str: string, color: number): string { + return run(str, code([48, 5, clampAndTruncate(color)], 49)); +} + +/** + * Set text color using 24bit rgb. + * `color` can be a number in range `0x000000` to `0xffffff` or + * an `Rgb`. + * + * To produce the color magenta: + * + * rgba24("foo", 0xff00ff); + * rgba24("foo", {r: 255, g: 0, b: 255}); + * @param str text color to apply 24bit rgb to + * @param color code + */ +export function rgb24(str: string, color: number | Rgb): string { + if (typeof color === "number") { + return run( + str, + code( + [38, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], + 39, + ), + ); + } + return run( + str, + code( + [ + 38, + 2, + clampAndTruncate(color.r), + clampAndTruncate(color.g), + clampAndTruncate(color.b), + ], + 39, + ), + ); +} + +/** + * Set background color using 24bit rgb. + * `color` can be a number in range `0x000000` to `0xffffff` or + * an `Rgb`. + * + * To produce the color magenta: + * + * bgRgba24("foo", 0xff00ff); + * bgRgba24("foo", {r: 255, g: 0, b: 255}); + * @param str text color to apply 24bit rgb to + * @param color code + */ +export function bgRgb24(str: string, color: number | Rgb): string { + if (typeof color === "number") { + return run( + str, + code( + [48, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], + 49, + ), + ); + } + return run( + str, + code( + [ + 48, + 2, + clampAndTruncate(color.r), + clampAndTruncate(color.g), + clampAndTruncate(color.b), + ], + 49, + ), + ); +} + +// https://github.com/chalk/ansi-regex/blob/2b56fb0c7a07108e5b54241e8faec160d393aedb/index.js +const ANSI_PATTERN = new RegExp( + [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", + ].join("|"), + "g", +); + +/** + * Remove ANSI escape codes from the string. + * @param string to remove ANSI escape codes from + */ +export function stripColor(string: string): string { + return string.replace(ANSI_PATTERN, ""); +} diff --git a/vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts b/vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts new file mode 100644 index 0000000..dc3fcd7 --- /dev/null +++ b/vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts @@ -0,0 +1,4 @@ +export { + green, + setColorEnabled, +} from "https://deno.land/std@0.77.0/fmt/colors.ts"; diff --git a/vendor/deno.land/x/bytes_formater@v1.4.0/format.ts b/vendor/deno.land/x/bytes_formater@v1.4.0/format.ts new file mode 100644 index 0000000..707aaf0 --- /dev/null +++ b/vendor/deno.land/x/bytes_formater@v1.4.0/format.ts @@ -0,0 +1,46 @@ +import { green } from "./deps.ts"; + +export function format(data: ArrayBufferView) { + const bytes = new Uint8Array(data.buffer); + let out = " +-------------------------------------------------+\n"; + out += ` |${ + green(" 0 1 2 3 4 5 6 7 8 9 a b c d e f ") + }|\n`; + out += + "+--------+-------------------------------------------------+----------------+\n"; + + const lineCount = Math.ceil(bytes.length / 16); + + for (let line = 0; line < lineCount; line++) { + const start = line * 16; + const addr = start.toString(16).padStart(8, "0"); + const lineBytes = bytes.slice(start, start + 16); + + out += `|${green(addr)}| `; + + lineBytes.forEach( + (byte) => (out += byte.toString(16).padStart(2, "0") + " "), + ); + + if (lineBytes.length < 16) { + out += " ".repeat(16 - lineBytes.length); + } + + out += "|"; + + lineBytes.forEach(function (byte) { + return (out += byte > 31 && byte < 127 + ? green(String.fromCharCode(byte)) + : "."); + }); + + if (lineBytes.length < 16) { + out += " ".repeat(16 - lineBytes.length); + } + + out += "|\n"; + } + out += + "+--------+-------------------------------------------------+----------------+"; + return out; +} diff --git a/vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts b/vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts new file mode 100644 index 0000000..8a00e1f --- /dev/null +++ b/vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts @@ -0,0 +1,2 @@ +export { format } from "./format.ts"; +export { setColorEnabled } from "./deps.ts"; diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/deps.ts b/vendor/deno.land/x/sql_builder@v1.9.1/deps.ts new file mode 100644 index 0000000..e56adf6 --- /dev/null +++ b/vendor/deno.land/x/sql_builder@v1.9.1/deps.ts @@ -0,0 +1,5 @@ +export { + assert, + assertEquals, +} from "https://deno.land/std@0.51.0/testing/asserts.ts"; +export { replaceParams } from "./util.ts"; diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/join.ts b/vendor/deno.land/x/sql_builder@v1.9.1/join.ts new file mode 100644 index 0000000..384cb61 --- /dev/null +++ b/vendor/deno.land/x/sql_builder@v1.9.1/join.ts @@ -0,0 +1,30 @@ +import { replaceParams } from "./util.ts"; + +export class Join { + value: string = ""; + constructor(type: string, readonly table: string, readonly alias?: string) { + const name = alias ? "?? ??" : "??"; + this.value = replaceParams(`${type} ${name}`, [table, alias]); + } + + static inner(table: string, alias?: string): Join { + return new Join("INNER JOIN", table, alias); + } + + static full(table: string, alias?: string): Join { + return new Join("FULL OUTER JOIN", table, alias); + } + + static left(table: string, alias?: string): Join { + return new Join("LEFT OUTER JOIN", table, alias); + } + + static right(table: string, alias?: string): Join { + return new Join("RIGHT OUTER JOIN", table, alias); + } + + on(a: string, b: string) { + this.value += replaceParams(` ON ?? = ??`, [a, b]); + return this; + } +} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/mod.ts b/vendor/deno.land/x/sql_builder@v1.9.1/mod.ts new file mode 100644 index 0000000..96ffe40 --- /dev/null +++ b/vendor/deno.land/x/sql_builder@v1.9.1/mod.ts @@ -0,0 +1,5 @@ +export { Join } from "./join.ts"; +export { Order } from "./order.ts"; +export { Query } from "./query.ts"; +export { replaceParams } from "./util.ts"; +export { Where } from "./where.ts"; diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/order.ts b/vendor/deno.land/x/sql_builder@v1.9.1/order.ts new file mode 100644 index 0000000..01338c0 --- /dev/null +++ b/vendor/deno.land/x/sql_builder@v1.9.1/order.ts @@ -0,0 +1,18 @@ +import { replaceParams } from "./util.ts"; + +export class Order { + value: string = ""; + static by(field: string) { + const order = new Order(); + return { + get desc() { + order.value = replaceParams("?? DESC", [field]); + return order; + }, + get asc() { + order.value = replaceParams("?? ASC", [field]); + return order; + }, + }; + } +} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/query.ts b/vendor/deno.land/x/sql_builder@v1.9.1/query.ts new file mode 100644 index 0000000..c80b12c --- /dev/null +++ b/vendor/deno.land/x/sql_builder@v1.9.1/query.ts @@ -0,0 +1,222 @@ +import { assert, replaceParams } from "./deps.ts"; +import { Order } from "./order.ts"; +import { Where } from "./where.ts"; +import { Join } from "./join.ts"; + +export class Query { + private _type?: "select" | "insert" | "update" | "delete"; + private _table?: string; + private _where: string[] = []; + private _joins: string[] = []; + private _orders: Order[] = []; + private _fields: string[] = []; + private _groupBy: string[] = []; + private _having: string[] = []; + private _insertValues: any[] = []; + private _updateValue?: any; + private _limit?: { start: number; size: number }; + + private get orderSQL() { + if (this._orders && this._orders.length) { + return `ORDER BY ` + this._orders.map((order) => order.value).join(", "); + } + } + + private get whereSQL() { + if (this._where && this._where.length) { + return `WHERE ` + this._where.join(" AND "); + } + } + + private get havingSQL() { + if (this._having && this._having.length) { + return `HAVING ` + this._having.join(" AND "); + } + } + + private get joinSQL() { + if (this._joins && this._joins.length) { + return this._joins.join(" "); + } + } + + private get groupSQL() { + if (this._groupBy && this._groupBy.length) { + return ( + "GROUP BY " + + this._groupBy.map((f) => replaceParams("??", [f])).join(", ") + ); + } + } + private get limitSQL() { + if (this._limit) { + return `LIMIT ${this._limit.start}, ${this._limit.size}`; + } + } + + private get selectSQL() { + return [ + "SELECT", + this._fields.join(", "), + "FROM", + replaceParams("??", [this._table]), + this.joinSQL, + this.whereSQL, + this.groupSQL, + this.havingSQL, + this.orderSQL, + this.limitSQL, + ] + .filter((str) => str) + .join(" "); + } + + private get insertSQL() { + const len = this._insertValues.length; + const fields = Object.keys(this._insertValues[0]); + const values = this._insertValues.map((row) => { + return fields.map((key) => row[key]!); + }); + return replaceParams(`INSERT INTO ?? ?? VALUES ${"? ".repeat(len)}`, [ + this._table, + fields, + ...values, + ]); + } + + private get updateSQL() { + assert(!!this._updateValue); + const set = Object.keys(this._updateValue) + .map((key) => { + return replaceParams(`?? = ?`, [key, this._updateValue[key]]); + }) + .join(", "); + return [ + replaceParams(`UPDATE ?? SET ${set}`, [this._table]), + this.whereSQL, + ].join(" "); + } + + private get deleteSQL() { + return [replaceParams(`DELETE FROM ??`, [this._table]), this.whereSQL].join( + " ", + ); + } + + table(name: string) { + this._table = name; + return this; + } + + order(...orders: Order[]) { + this._orders = this._orders.concat(orders); + return this; + } + + groupBy(...fields: string[]) { + this._groupBy = fields; + return this; + } + + where(where: Where | string) { + if (typeof where === "string") { + this._where.push(where); + } else { + this._where.push(where.value); + } + return this; + } + + having(where: Where | string) { + if (typeof where === "string") { + this._having.push(where); + } else { + this._having.push(where.value); + } + return this; + } + + limit(start: number, size: number) { + this._limit = { start, size }; + return this; + } + + join(join: Join | string) { + if (typeof join === "string") { + this._joins.push(join); + } else { + this._joins.push(join.value); + } + return this; + } + + select(...fields: string[]) { + this._type = "select"; + assert(fields.length > 0); + this._fields = this._fields.concat( + fields.map((field) => { + if (field.toLocaleLowerCase().indexOf(" as ") > -1) { + return field; + } else if (field.split(".").length > 1) { + return replaceParams("??.??", field.split(".")); + } else { + return replaceParams("??", [field]); + } + }), + ); + return this; + } + + insert(data: Object[] | Object) { + this._type = "insert"; + if (!(data instanceof Array)) { + data = [data]; + } + this._insertValues = data as []; + return this; + } + + update(data: Object) { + this._type = "update"; + this._updateValue = data; + return this; + } + + delete(table?: string) { + if (table) this._table = table; + this._type = "delete"; + return this; + } + + clone() { + const newQuery = new Query(); + newQuery._type = this._type; + newQuery._table = this._table; + newQuery._where = this._where; + newQuery._joins = this._joins; + newQuery._orders = this._orders; + newQuery._fields = this._fields; + newQuery._groupBy = this._groupBy; + newQuery._having = this._having; + newQuery._insertValues = this._insertValues; + newQuery._updateValue = this._updateValue; + newQuery._limit = this._limit; + return newQuery; + } + + build(): string { + assert(!!this._table); + switch (this._type) { + case "select": + return this.selectSQL; + case "insert": + return this.insertSQL; + case "update": + return this.updateSQL; + case "delete": + return this.deleteSQL; + default: + return ""; + } + } +} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/util.ts b/vendor/deno.land/x/sql_builder@v1.9.1/util.ts new file mode 100644 index 0000000..cabb057 --- /dev/null +++ b/vendor/deno.land/x/sql_builder@v1.9.1/util.ts @@ -0,0 +1,84 @@ +export function replaceParams(sql: string, params: any | any[]): string { + if (!params) return sql; + let paramIndex = 0; + sql = sql.replace(/('[^'\\]*(?:\\.[^'\\]*)*')|("[^"\\]*(?:\\.[^"\\]*)*")|(\?\?)|(\?)/g, (str) => { + if (paramIndex >= params.length) return str; + // ignore + if (/".*"/g.test(str) || /'.*'/g.test(str)) { + return str; + } + // identifier + if (str === "??") { + const val = params[paramIndex++]; + if (val instanceof Array) { + return `(${val.map((item) => replaceParams("??", [item])).join(",")})`; + } else if (val === "*") { + return val; + } else if (typeof val === "string" && val.includes(".")) { + // a.b => `a`.`b` + const _arr = val.split("."); + return replaceParams(_arr.map(() => "??").join("."), _arr); + } else if ( + typeof val === "string" && + (val.includes(" as ") || val.includes(" AS ")) + ) { + // a as b => `a` AS `b` + const newVal = val.replace(" as ", " AS "); + const _arr = newVal.split(" AS "); + return replaceParams(_arr.map(() => "??").join(" AS "), _arr); + } else { + return ["`", val, "`"].join(""); + } + } + // value + const val = params[paramIndex++]; + if (val === null) return "NULL"; + switch (typeof val) { + case "object": + if (val instanceof Date) return `"${formatDate(val)}"`; + if (val instanceof Array) { + return `(${val.map((item) => replaceParams("?", [item])).join(",")})`; + } + case "string": + return `"${escapeString(val)}"`; + case "undefined": + return "NULL"; + case "number": + case "boolean": + default: + return val; + } + }); + return sql; +} + +function formatDate(date: Date) { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const days = date + .getDate() + .toString() + .padStart(2, "0"); + const hours = date + .getHours() + .toString() + .padStart(2, "0"); + const minutes = date + .getMinutes() + .toString() + .padStart(2, "0"); + const seconds = date + .getSeconds() + .toString() + .padStart(2, "0"); + // Date does not support microseconds precision, so we only keep the milliseconds part. + const milliseconds = date + .getMilliseconds() + .toString() + .padStart(3, "0"); + return `${year}-${month}-${days} ${hours}:${minutes}:${seconds}.${milliseconds}`; +} + +function escapeString(str: string) { + return str.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); +} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/where.ts b/vendor/deno.land/x/sql_builder@v1.9.1/where.ts new file mode 100644 index 0000000..cd18b26 --- /dev/null +++ b/vendor/deno.land/x/sql_builder@v1.9.1/where.ts @@ -0,0 +1,121 @@ +import { replaceParams } from "./util.ts"; + +/** + * Where sub sql builder + */ +export class Where { + private expr: string; + private params: any[]; + constructor(expr: string, params: any[]) { + this.expr = expr; + this.params = params; + } + + get value(): string { + return this.toString(); + } + + toString(): string { + return replaceParams(this.expr, this.params); + } + + static expr(expr: string, ...params: any[]): Where { + return new Where(expr, params); + } + + static eq(field: string, value: any) { + return this.expr("?? = ?", field, value); + } + + /** + * eq from object + * @param data + */ + static from(data: any): Where { + const conditions = Object.keys(data).map((key) => this.eq(key, data[key])); + return this.and(...conditions); + } + + static gt(field: string, value: any) { + return this.expr("?? > ?", field, value); + } + + static gte(field: string, value: any) { + return this.expr("?? >= ?", field, value); + } + + static lt(field: string, value: any) { + return this.expr("?? < ?", field, value); + } + + static lte(field: string, value: any) { + return this.expr("?? <= ?", field, value); + } + + static ne(field: string, value: any) { + return this.expr("?? != ?", field, value); + } + + static isNull(field: string) { + return this.expr("?? IS NULL", field); + } + + static notNull(field: string) { + return this.expr("?? NOT NULL", field); + } + + static in(field: string, ...values: any[]) { + const params: any[] = values.length > 1 ? values : values[0]; + return this.expr("?? IN ?", field, params); + } + + static notIn(field: string, ...values: any[]) { + const params: any[] = values.length > 1 ? values : values[0]; + return this.expr("?? NOT IN ?", field, params); + } + + static like(field: string, value: any) { + return this.expr("?? LIKE ?", field, value); + } + + static between(field: string, startValue: any, endValue: any) { + return this.expr("?? BETWEEN ? AND ?", field, startValue, endValue); + } + + static field(name: string) { + return { + gt: (value: any) => this.gt(name, value), + gte: (value: any) => this.gte(name, value), + lt: (value: any) => this.lt(name, value), + lte: (value: any) => this.lte(name, value), + ne: (value: any) => this.ne(name, value), + eq: (value: any) => this.eq(name, value), + isNull: () => this.isNull(name), + notNull: () => this.notNull(name), + in: (...values: any[]) => this.in(name, ...values), + notIn: (...values: any[]) => this.notIn(name, ...values), + like: (value: any) => this.like(name, value), + between: (start: any, end: any) => this.between(name, start, end), + }; + } + + static and(...expr: (null | undefined | Where)[]): Where { + const sql = `(${ + expr + .filter((e) => e) + .map((e) => e!.value) + .join(" AND ") + })`; + return new Where(sql, []); + } + + static or(...expr: (null | undefined | Where)[]): Where { + const sql = `(${ + expr + .filter((e) => e) + .map((e) => e!.value) + .join(" OR ") + })`; + return new Where(sql, []); + } +} From 6ff309fbd1789e6be9db9406f4d55031e6a1e6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 9 Apr 2024 01:08:13 +0200 Subject: [PATCH 02/38] Updated vendor --- vendor/deno.land/std@0.51.0/fmt/colors.ts | 8 +- .../deno.land/std@0.51.0/testing/asserts.ts | 58 ++++++----- vendor/deno.land/std@0.51.0/testing/diff.ts | 24 ++--- .../deno.land/x/sql_builder@v1.9.1/query.ts | 6 +- vendor/deno.land/x/sql_builder@v1.9.1/util.ts | 97 ++++++++++--------- 5 files changed, 103 insertions(+), 90 deletions(-) diff --git a/vendor/deno.land/std@0.51.0/fmt/colors.ts b/vendor/deno.land/std@0.51.0/fmt/colors.ts index 64b4458..a963aa4 100644 --- a/vendor/deno.land/std@0.51.0/fmt/colors.ts +++ b/vendor/deno.land/std@0.51.0/fmt/colors.ts @@ -184,8 +184,8 @@ export function rgb24(str: string, color: Rgb): string { clampAndTruncate(color.g), clampAndTruncate(color.b), ], - 39 - ) + 39, + ), ); } @@ -201,7 +201,7 @@ export function bgRgb24(str: string, color: Rgb): string { clampAndTruncate(color.g), clampAndTruncate(color.b), ], - 49 - ) + 49, + ), ); } diff --git a/vendor/deno.land/std@0.51.0/testing/asserts.ts b/vendor/deno.land/std@0.51.0/testing/asserts.ts index 6dd3af2..9f72293 100644 --- a/vendor/deno.land/std@0.51.0/testing/asserts.ts +++ b/vendor/deno.land/std@0.51.0/testing/asserts.ts @@ -1,6 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { red, green, white, gray, bold } from "../fmt/colors.ts"; -import diff, { DiffType, DiffResult } from "./diff.ts"; +import { bold, gray, green, red, white } from "../fmt/colors.ts"; +import diff, { type DiffResult, DiffType } from "./diff.ts"; const CAN_NOT_DISPLAY = "[Cannot display]"; @@ -51,9 +51,11 @@ function buildMessage(diffResult: ReadonlyArray>): string[] { messages.push(""); messages.push(""); messages.push( - ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${green( - bold("Expected") - )}` + ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${ + green( + bold("Expected"), + ) + }`, ); messages.push(""); messages.push(""); @@ -143,7 +145,7 @@ export function assert(expr: unknown, msg = ""): asserts expr { export function assertEquals( actual: unknown, expected: unknown, - msg?: string + msg?: string, ): void { if (equal(actual, expected)) { return; @@ -154,7 +156,7 @@ export function assertEquals( try { const diffResult = diff( actualString.split("\n"), - expectedString.split("\n") + expectedString.split("\n"), ); message = buildMessage(diffResult).join("\n"); } catch (e) { @@ -173,7 +175,7 @@ export function assertEquals( export function assertNotEquals( actual: unknown, expected: unknown, - msg?: string + msg?: string, ): void { if (!equal(actual, expected)) { return; @@ -203,7 +205,7 @@ export function assertNotEquals( export function assertStrictEq( actual: unknown, expected: unknown, - msg?: string + msg?: string, ): void { if (actual !== expected) { let actualString: string; @@ -232,7 +234,7 @@ export function assertStrictEq( export function assertStrContains( actual: string, expected: string, - msg?: string + msg?: string, ): void { if (!actual.includes(expected)) { if (!msg) { @@ -249,7 +251,7 @@ export function assertStrContains( export function assertArrayContains( actual: unknown[], expected: unknown[], - msg?: string + msg?: string, ): void { const missing: unknown[] = []; for (let i = 0; i < expected.length; i++) { @@ -282,7 +284,7 @@ export function assertArrayContains( export function assertMatch( actual: string, expected: RegExp, - msg?: string + msg?: string, ): void { if (!expected.test(actual)) { if (!msg) { @@ -308,7 +310,7 @@ export function assertThrows( fn: () => void, ErrorClass?: Constructor, msgIncludes = "", - msg?: string + msg?: string, ): Error { let doesThrow = false; let error = null; @@ -316,15 +318,17 @@ export function assertThrows( fn(); } catch (e) { if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { - msg = `Expected error to be instance of "${ErrorClass.name}", but was "${ - e.constructor.name - }"${msg ? `: ${msg}` : "."}`; + msg = + `Expected error to be instance of "${ErrorClass.name}", but was "${e.constructor.name}"${ + msg ? `: ${msg}` : "." + }`; throw new AssertionError(msg); } if (msgIncludes && !e.message.includes(msgIncludes)) { - msg = `Expected error message to include "${msgIncludes}", but got "${ - e.message - }"${msg ? `: ${msg}` : "."}`; + msg = + `Expected error message to include "${msgIncludes}", but got "${e.message}"${ + msg ? `: ${msg}` : "." + }`; throw new AssertionError(msg); } doesThrow = true; @@ -341,7 +345,7 @@ export async function assertThrowsAsync( fn: () => Promise, ErrorClass?: Constructor, msgIncludes = "", - msg?: string + msg?: string, ): Promise { let doesThrow = false; let error = null; @@ -349,15 +353,17 @@ export async function assertThrowsAsync( await fn(); } catch (e) { if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { - msg = `Expected error to be instance of "${ErrorClass.name}", but got "${ - e.name - }"${msg ? `: ${msg}` : "."}`; + msg = + `Expected error to be instance of "${ErrorClass.name}", but got "${e.name}"${ + msg ? `: ${msg}` : "." + }`; throw new AssertionError(msg); } if (msgIncludes && !e.message.includes(msgIncludes)) { - msg = `Expected error message to include "${msgIncludes}", but got "${ - e.message - }"${msg ? `: ${msg}` : "."}`; + msg = + `Expected error message to include "${msgIncludes}", but got "${e.message}"${ + msg ? `: ${msg}` : "." + }`; throw new AssertionError(msg); } doesThrow = true; diff --git a/vendor/deno.land/std@0.51.0/testing/diff.ts b/vendor/deno.land/std@0.51.0/testing/diff.ts index 97baa08..1bc22be 100644 --- a/vendor/deno.land/std@0.51.0/testing/diff.ts +++ b/vendor/deno.land/std@0.51.0/testing/diff.ts @@ -39,7 +39,7 @@ export default function diff(A: T[], B: T[]): Array> { const suffixCommon = createCommon( A.slice(prefixCommon.length), B.slice(prefixCommon.length), - true + true, ).reverse(); A = suffixCommon.length ? A.slice(prefixCommon.length, -suffixCommon.length) @@ -55,16 +55,16 @@ export default function diff(A: T[], B: T[]): Array> { if (!N) { return [ ...prefixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }) + (c): DiffResult => ({ type: DiffType.common, value: c }), ), ...A.map( (a): DiffResult => ({ type: swapped ? DiffType.added : DiffType.removed, value: a, - }) + }), ), ...suffixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }) + (c): DiffResult => ({ type: DiffType.common, value: c }), ), ]; } @@ -89,7 +89,7 @@ export default function diff(A: T[], B: T[]): Array> { A: T[], B: T[], current: FarthestPoint, - swapped: boolean + swapped: boolean, ): Array<{ type: DiffType; value: T; @@ -131,7 +131,7 @@ export default function diff(A: T[], B: T[]): Array> { slide: FarthestPoint, down: FarthestPoint, k: number, - M: number + M: number, ): FarthestPoint { if (slide && slide.y === -1 && down && down.y === -1) { return { y: 0, id: 0 }; @@ -161,7 +161,7 @@ export default function diff(A: T[], B: T[]): Array> { down: FarthestPoint, _offset: number, A: T[], - B: T[] + B: T[], ): FarthestPoint { const M = A.length; const N = B.length; @@ -187,7 +187,7 @@ export default function diff(A: T[], B: T[]): Array> { fp[k + 1 + offset], offset, A, - B + B, ); } for (let k = delta + p; k > delta; --k) { @@ -197,7 +197,7 @@ export default function diff(A: T[], B: T[]): Array> { fp[k + 1 + offset], offset, A, - B + B, ); } fp[delta + offset] = snake( @@ -206,16 +206,16 @@ export default function diff(A: T[], B: T[]): Array> { fp[delta + 1 + offset], offset, A, - B + B, ); } return [ ...prefixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }) + (c): DiffResult => ({ type: DiffType.common, value: c }), ), ...backTrace(A, B, fp[delta + offset], swapped), ...suffixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }) + (c): DiffResult => ({ type: DiffType.common, value: c }), ), ]; } diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/query.ts b/vendor/deno.land/x/sql_builder@v1.9.1/query.ts index c80b12c..a87f7c9 100644 --- a/vendor/deno.land/x/sql_builder@v1.9.1/query.ts +++ b/vendor/deno.land/x/sql_builder@v1.9.1/query.ts @@ -1,7 +1,7 @@ import { assert, replaceParams } from "./deps.ts"; -import { Order } from "./order.ts"; -import { Where } from "./where.ts"; -import { Join } from "./join.ts"; +import type { Order } from "./order.ts"; +import type { Where } from "./where.ts"; +import type { Join } from "./join.ts"; export class Query { private _type?: "select" | "insert" | "update" | "delete"; diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/util.ts b/vendor/deno.land/x/sql_builder@v1.9.1/util.ts index cabb057..e6658f9 100644 --- a/vendor/deno.land/x/sql_builder@v1.9.1/util.ts +++ b/vendor/deno.land/x/sql_builder@v1.9.1/util.ts @@ -1,54 +1,61 @@ export function replaceParams(sql: string, params: any | any[]): string { if (!params) return sql; let paramIndex = 0; - sql = sql.replace(/('[^'\\]*(?:\\.[^'\\]*)*')|("[^"\\]*(?:\\.[^"\\]*)*")|(\?\?)|(\?)/g, (str) => { - if (paramIndex >= params.length) return str; - // ignore - if (/".*"/g.test(str) || /'.*'/g.test(str)) { - return str; - } - // identifier - if (str === "??") { - const val = params[paramIndex++]; - if (val instanceof Array) { - return `(${val.map((item) => replaceParams("??", [item])).join(",")})`; - } else if (val === "*") { - return val; - } else if (typeof val === "string" && val.includes(".")) { - // a.b => `a`.`b` - const _arr = val.split("."); - return replaceParams(_arr.map(() => "??").join("."), _arr); - } else if ( - typeof val === "string" && - (val.includes(" as ") || val.includes(" AS ")) - ) { - // a as b => `a` AS `b` - const newVal = val.replace(" as ", " AS "); - const _arr = newVal.split(" AS "); - return replaceParams(_arr.map(() => "??").join(" AS "), _arr); - } else { - return ["`", val, "`"].join(""); + sql = sql.replace( + /('[^'\\]*(?:\\.[^'\\]*)*')|("[^"\\]*(?:\\.[^"\\]*)*")|(\?\?)|(\?)/g, + (str) => { + if (paramIndex >= params.length) return str; + // ignore + if (/".*"/g.test(str) || /'.*'/g.test(str)) { + return str; } - } - // value - const val = params[paramIndex++]; - if (val === null) return "NULL"; - switch (typeof val) { - case "object": - if (val instanceof Date) return `"${formatDate(val)}"`; + // identifier + if (str === "??") { + const val = params[paramIndex++]; if (val instanceof Array) { - return `(${val.map((item) => replaceParams("?", [item])).join(",")})`; + return `(${ + val.map((item) => replaceParams("??", [item])).join(",") + })`; + } else if (val === "*") { + return val; + } else if (typeof val === "string" && val.includes(".")) { + // a.b => `a`.`b` + const _arr = val.split("."); + return replaceParams(_arr.map(() => "??").join("."), _arr); + } else if ( + typeof val === "string" && + (val.includes(" as ") || val.includes(" AS ")) + ) { + // a as b => `a` AS `b` + const newVal = val.replace(" as ", " AS "); + const _arr = newVal.split(" AS "); + return replaceParams(_arr.map(() => "??").join(" AS "), _arr); + } else { + return ["`", val, "`"].join(""); } - case "string": - return `"${escapeString(val)}"`; - case "undefined": - return "NULL"; - case "number": - case "boolean": - default: - return val; - } - }); + } + // value + const val = params[paramIndex++]; + if (val === null) return "NULL"; + switch (typeof val) { + case "object": + if (val instanceof Date) return `"${formatDate(val)}"`; + if (val instanceof Array) { + return `(${ + val.map((item) => replaceParams("?", [item])).join(",") + })`; + } + case "string": + return `"${escapeString(val)}"`; + case "undefined": + return "NULL"; + case "number": + case "boolean": + default: + return val; + } + }, + ); return sql; } From 22dc36ecd304a0d280363eb1b6cbf7bad145aaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 9 Apr 2024 01:09:54 +0200 Subject: [PATCH 03/38] added deno.json --- deno.json | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 deno.json diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..c6ae6b7 --- /dev/null +++ b/deno.json @@ -0,0 +1,37 @@ +{ + "name": "@db/mysql", + "version": "2.12.2", + "exports": "./mod.ts", + "lock": false, + "tasks": { + "check": "deno task format:check && deno task lint:check && deno task type:check", + "lint:check": "deno lint", + "format:check": "deno fmt --check", + "type:check": "deno check mod.ts", + "doc:check": "deno doc --lint src", + "test": "deno task db:restart && deno test --allow-env --allow-net=127.0.0.1:3306 ./test.ts; deno task db:stop", + "db:restart": "deno task db:stop && deno task db:start", + "db:start": "docker compose up -d --remove-orphans --wait && sleep 2", + "db:stop": "docker compose down --remove-orphans --volumes", + "typedoc": "deno run -A npm:typedoc --theme minimal --ignoreCompilerErrors --excludePrivate --excludeExternals --entryPoint client.ts --mode file ./src --out ./docs" + }, + "imports": { + "@std/assert": "jsr:@std/assert@^0.221.0", + "@std/async": "jsr:@std/async@^0.221.0", + "@std/encoding": "jsr:@std/encoding@^0.221.0", + "@std/flags": "jsr:@std/flags@^0.221.0", + "@std/crypto": "jsr:@std/crypto@^0.221.0", + "@std/log": "jsr:@std/log@^0.221.0", + "@std/semver": "jsr:@std/semver@^0.220.1", + "@std/testing": "jsr:@std/testing@^0.221.0", + "bytes_formater/": "./vendor/deno.land/x/bytes_formater@v1.4.0/", + "https://deno.land/": "./vendor/deno.land/", + "sql_builder/": "./vendor/deno.land/x/sql_builder@v1.9.1/" + }, + "lint": { + "exclude": ["vendor"] + }, + "fmt": { + "exclude": ["vendor"] + } +} From 139cab58d02469aad62e24e3708aab7bb501999c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 9 Apr 2024 01:10:23 +0200 Subject: [PATCH 04/38] removed old config files --- egg.json | 10 ---------- package.json | 23 ----------------------- 2 files changed, 33 deletions(-) delete mode 100644 egg.json delete mode 100644 package.json diff --git a/egg.json b/egg.json deleted file mode 100644 index e78ef5a..0000000 --- a/egg.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "mysql", - "description": "MySQL driver for Deno", - "homepage": "https://github.com/manyuanrong/deno_mysql", - "files": [ - "./**/*.ts", - "README.md" - ], - "entry": "./mod.ts" -} diff --git a/package.json b/package.json deleted file mode 100644 index 1a9fcac..0000000 --- a/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "deno_mysql", - "version": "1.0.0", - "description": "[![Build Status](https://www.travis-ci.org/manyuanrong/deno_mysql.svg?branch=master)](https://www.travis-ci.org/manyuanrong/deno_mysql)", - "main": "index.js", - "scripts": { - "docs": "typedoc --theme minimal --ignoreCompilerErrors --excludePrivate --excludeExternals --entryPoint client.ts --mode file ./src --out ./docs" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/manyuanrong/deno_mysql.git" - }, - "keywords": [], - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/manyuanrong/deno_mysql/issues" - }, - "homepage": "https://github.com/manyuanrong/deno_mysql#readme", - "devDependencies": { - "typedoc": "^0.14.2" - } -} From 083230df0d5f9552ee7c2403a185e0958168f432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 9 Apr 2024 01:10:30 +0200 Subject: [PATCH 05/38] added docker compose --- compose.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 compose.yml diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..b18af86 --- /dev/null +++ b/compose.yml @@ -0,0 +1,13 @@ +services: + mysql: + image: mysql + restart: always + ports: + - 3306:3306 + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: true + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--user", "root"] + interval: 3s + timeout: 3s + retries: 10 From 2ceb6d616fc8ed86f360ea4c38a2ffb0253c1d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 9 Apr 2024 01:10:53 +0200 Subject: [PATCH 06/38] updated dependencies to use jsr --- deps.ts | 10 -------- mod.ts | 2 +- src/auth.ts | 31 +++++++++++++++--------- src/auth_plugin/caching_sha2_password.ts | 2 +- src/auth_plugin/crypt.ts | 4 +-- src/client.ts | 6 ++++- src/connection.ts | 24 +++++++++++------- src/constant/errors.ts | 10 ++++---- src/deferred.ts | 8 +++--- src/logger.ts | 4 +-- src/packets/builders/query.ts | 2 +- src/packets/packet.ts | 2 +- src/packets/parsers/authswitch.ts | 2 +- src/packets/parsers/handshake.ts | 4 +-- src/pool.ts | 2 +- test.deps.ts | 6 ----- test.ts | 24 ++++++++++-------- test.util.ts | 4 +-- 18 files changed, 76 insertions(+), 71 deletions(-) delete mode 100644 deps.ts delete mode 100644 test.deps.ts diff --git a/deps.ts b/deps.ts deleted file mode 100644 index 93a948b..0000000 --- a/deps.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type { Deferred } from "https://deno.land/std@0.104.0/async/mod.ts"; -export { deferred, delay } from "https://deno.land/std@0.104.0/async/mod.ts"; -export { format as byteFormat } from "https://deno.land/x/bytes_formater@v1.4.0/mod.ts"; -export { createHash } from "https://deno.land/std@0.104.0/hash/mod.ts"; -export { decode as base64Decode } from "https://deno.land/std@0.104.0/encoding/base64.ts"; -export type { - SupportedAlgorithm, -} from "https://deno.land/std@0.104.0/hash/mod.ts"; -export { replaceParams } from "https://deno.land/x/sql_builder@v1.9.1/util.ts"; -export * as log from "https://deno.land/std@0.104.0/log/mod.ts"; diff --git a/mod.ts b/mod.ts index 193240d..1f7ee96 100644 --- a/mod.ts +++ b/mod.ts @@ -9,4 +9,4 @@ export { Connection } from "./src/connection.ts"; export type { LoggerConfig } from "./src/logger.ts"; export { configLogger } from "./src/logger.ts"; -export { log } from "./deps.ts"; +export * as log from "@std/log"; diff --git a/src/auth.ts b/src/auth.ts index deafa1d..e092ef0 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,27 +1,36 @@ -import { createHash, SupportedAlgorithm } from "../deps.ts"; +import { crypto, type DigestAlgorithm } from "@std/crypto"; import { xor } from "./util.ts"; import { encode } from "./buffer.ts"; -function hash(algorithm: SupportedAlgorithm, data: Uint8Array): Uint8Array { - return new Uint8Array(createHash(algorithm).update(data).digest()); +async function hash( + algorithm: DigestAlgorithm, + data: Uint8Array, +): Promise { + return new Uint8Array(await crypto.subtle.digest(algorithm, data)); } -function mysqlNativePassword(password: string, seed: Uint8Array): Uint8Array { - const pwd1 = hash("sha1", encode(password)); - const pwd2 = hash("sha1", pwd1); +async function mysqlNativePassword( + password: string, + seed: Uint8Array, +): Promise { + const pwd1 = await hash("SHA-1", encode(password)); + const pwd2 = await hash("SHA-1", pwd1); let seedAndPwd2 = new Uint8Array(seed.length + pwd2.length); seedAndPwd2.set(seed); seedAndPwd2.set(pwd2, seed.length); - seedAndPwd2 = hash("sha1", seedAndPwd2); + seedAndPwd2 = await hash("SHA-1", seedAndPwd2); return xor(seedAndPwd2, pwd1); } -function cachingSha2Password(password: string, seed: Uint8Array): Uint8Array { - const stage1 = hash("sha256", encode(password)); - const stage2 = hash("sha256", stage1); - const stage3 = hash("sha256", Uint8Array.from([...stage2, ...seed])); +async function cachingSha2Password( + password: string, + seed: Uint8Array, +): Promise { + const stage1 = await hash("SHA-256", encode(password)); + const stage2 = await hash("SHA-256", stage1); + const stage3 = await hash("SHA-256", Uint8Array.from([...stage2, ...seed])); return xor(stage1, stage3); } diff --git a/src/auth_plugin/caching_sha2_password.ts b/src/auth_plugin/caching_sha2_password.ts index 1e8cbbe..8d45ed2 100644 --- a/src/auth_plugin/caching_sha2_password.ts +++ b/src/auth_plugin/caching_sha2_password.ts @@ -1,5 +1,5 @@ import { xor } from "../util.ts"; -import { ReceivePacket } from "../packets/packet.ts"; +import type { ReceivePacket } from "../packets/packet.ts"; import { encryptWithPublicKey } from "./crypt.ts"; interface handler { diff --git a/src/auth_plugin/crypt.ts b/src/auth_plugin/crypt.ts index 8eb2339..665931e 100644 --- a/src/auth_plugin/crypt.ts +++ b/src/auth_plugin/crypt.ts @@ -1,4 +1,4 @@ -import { base64Decode } from "../../deps.ts"; +import { decodeBase64 } from "@std/encoding/base64"; async function encryptWithPublicKey( key: string, @@ -10,7 +10,7 @@ async function encryptWithPublicKey( key = key.substring(pemHeader.length, key.length - pemFooter.length); const importedKey = await crypto.subtle.importKey( "spki", - base64Decode(key), + decodeBase64(key), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"], diff --git a/src/client.ts b/src/client.ts index 7d91489..ddd36ff 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,4 +1,8 @@ -import { Connection, ConnectionState, ExecuteResult } from "./connection.ts"; +import { + type Connection, + ConnectionState, + type ExecuteResult, +} from "./connection.ts"; import { ConnectionPool, PoolConnection } from "./pool.ts"; import { log } from "./logger.ts"; diff --git a/src/connection.ts b/src/connection.ts index 3f2b23f..d2cf765 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1,6 +1,6 @@ -import { ClientConfig, TLSMode } from "./client.ts"; +import { type ClientConfig, TLSMode } from "./client.ts"; import { - ConnnectionError, + ConnectionError, ProtocolError, ReadError, ResponseTimeoutError, @@ -15,7 +15,11 @@ import { parseAuth, parseHandshake, } from "./packets/parsers/handshake.ts"; -import { FieldInfo, parseField, parseRow } from "./packets/parsers/result.ts"; +import { + type FieldInfo, + parseField, + parseRow, +} from "./packets/parsers/result.ts"; import { PacketType } from "./constant/packet.ts"; import authPlugin from "./auth_plugin/index.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; @@ -138,12 +142,13 @@ export class Connection { let handler; switch (authResult) { - case AuthResult.AuthMoreRequired: + case AuthResult.AuthMoreRequired: { const adaptedPlugin = (authPlugin as any)[handshakePacket.authPluginName]; handler = adaptedPlugin; break; - case AuthResult.MethodMismatch: + } + case AuthResult.MethodMismatch: { const authSwitch = parseAuthSwitch(receive.body); // If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is // sent and we have to keep using the cipher sent in the init packet. @@ -156,7 +161,7 @@ export class Connection { let authData; if (password) { - authData = auth( + authData = await auth( authSwitch.authPluginName, password, authSwitch.authPluginData, @@ -174,6 +179,7 @@ export class Connection { "Do not allow to change the auth plugin more than once!", ); } + } } let result; @@ -222,7 +228,7 @@ export class Connection { private async nextPacket(): Promise { if (!this.conn) { - throw new ConnnectionError("Not connected"); + throw new ConnectionError("Not connected"); } const timeoutTimer = this.config.timeout @@ -301,9 +307,9 @@ export class Connection { ): Promise { if (this.state != ConnectionState.CONNECTED) { if (this.state == ConnectionState.CLOSED) { - throw new ConnnectionError("Connection is closed"); + throw new ConnectionError("Connection is closed"); } else { - throw new ConnnectionError("Must be connected first"); + throw new ConnectionError("Must be connected first"); } } const data = buildQuery(sql, params); diff --git a/src/constant/errors.ts b/src/constant/errors.ts index bd79fdb..4c22894 100644 --- a/src/constant/errors.ts +++ b/src/constant/errors.ts @@ -1,28 +1,28 @@ -export class ConnnectionError extends Error { +export class ConnectionError extends Error { constructor(msg?: string) { super(msg); } } -export class WriteError extends ConnnectionError { +export class WriteError extends ConnectionError { constructor(msg?: string) { super(msg); } } -export class ReadError extends ConnnectionError { +export class ReadError extends ConnectionError { constructor(msg?: string) { super(msg); } } -export class ResponseTimeoutError extends ConnnectionError { +export class ResponseTimeoutError extends ConnectionError { constructor(msg?: string) { super(msg); } } -export class ProtocolError extends ConnnectionError { +export class ProtocolError extends ConnectionError { constructor(msg?: string) { super(msg); } diff --git a/src/deferred.ts b/src/deferred.ts index 0b3e95b..cd9fcc2 100644 --- a/src/deferred.ts +++ b/src/deferred.ts @@ -1,8 +1,6 @@ -import { Deferred, deferred } from "../deps.ts"; - /** @ignore */ export class DeferredStack { - private _queue: Deferred[] = []; + private _queue: PromiseWithResolvers[] = []; private _size = 0; constructor( @@ -39,9 +37,9 @@ export class DeferredStack { } return item; } - const defer = deferred(); + const defer = Promise.withResolvers(); this._queue.push(defer); - return await defer; + return await defer.promise; } /** Returns false if the item is consumed by a deferred pop */ diff --git a/src/logger.ts b/src/logger.ts index dad062a..543dc82 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,4 @@ -import { log } from "../deps.ts"; +import * as log from "@std/log"; let logger = log.getLogger(); @@ -34,7 +34,7 @@ export async function configLogger(config: LoggerConfig) { if (!config.logger) { await log.setup({ handlers: { - console: new log.handlers.ConsoleHandler(level), + console: new log.ConsoleHandler(level), }, loggers: { default: { diff --git a/src/packets/builders/query.ts b/src/packets/builders/query.ts index 8882f06..2425c78 100644 --- a/src/packets/builders/query.ts +++ b/src/packets/builders/query.ts @@ -1,4 +1,4 @@ -import { replaceParams } from "../../../deps.ts"; +import { replaceParams } from "sql_builder/util.ts"; import { BufferWriter, encode } from "../../buffer.ts"; /** @ignore */ diff --git a/src/packets/packet.ts b/src/packets/packet.ts index d58c41c..82a085b 100644 --- a/src/packets/packet.ts +++ b/src/packets/packet.ts @@ -1,4 +1,4 @@ -import { byteFormat } from "../../deps.ts"; +import { format as byteFormat } from "bytes_formater/mod.ts"; import { BufferReader, BufferWriter } from "../buffer.ts"; import { WriteError } from "../constant/errors.ts"; import { debug, log } from "../logger.ts"; diff --git a/src/packets/parsers/authswitch.ts b/src/packets/parsers/authswitch.ts index ac8b728..5c25968 100644 --- a/src/packets/parsers/authswitch.ts +++ b/src/packets/parsers/authswitch.ts @@ -1,4 +1,4 @@ -import { BufferReader } from "../../buffer.ts"; +import type { BufferReader } from "../../buffer.ts"; /** @ignore */ export interface authSwitchBody { diff --git a/src/packets/parsers/handshake.ts b/src/packets/parsers/handshake.ts index 959e028..e999908 100644 --- a/src/packets/parsers/handshake.ts +++ b/src/packets/parsers/handshake.ts @@ -1,7 +1,7 @@ -import { BufferReader, BufferWriter } from "../../buffer.ts"; +import { type BufferReader, BufferWriter } from "../../buffer.ts"; import ServerCapabilities from "../../constant/capabilities.ts"; import { PacketType } from "../../constant/packet.ts"; -import { ReceivePacket } from "../packet.ts"; +import type { ReceivePacket } from "../packet.ts"; /** @ignore */ export interface HandshakeBody { diff --git a/src/pool.ts b/src/pool.ts index f1de757..545b466 100644 --- a/src/pool.ts +++ b/src/pool.ts @@ -21,7 +21,7 @@ export class PoolConnection extends Connection { try { this.close(); } catch (error) { - log.warning(`error closing idle connection`, error); + log.warn(`error closing idle connection`, error); } }, this.config.idleTimeout); try { diff --git a/test.deps.ts b/test.deps.ts deleted file mode 100644 index c48f9b8..0000000 --- a/test.deps.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - assertEquals, - assertThrowsAsync, -} from "https://deno.land/std@0.104.0/testing/asserts.ts"; -export * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts"; -export { parse } from "https://deno.land/std@0.104.0/flags/mod.ts"; diff --git a/test.ts b/test.ts index e030e8c..d151833 100644 --- a/test.ts +++ b/test.ts @@ -1,6 +1,7 @@ -import { assertEquals, assertThrowsAsync, semver } from "./test.deps.ts"; +import { assertEquals, assertRejects } from "@std/assert"; +import { lessThan, parse } from "@std/semver"; import { - ConnnectionError, + ConnectionError, ResponseTimeoutError, } from "./src/constant/errors.ts"; import { @@ -10,7 +11,7 @@ import { registerTests, testWithClient, } from "./test.util.ts"; -import { log as stdlog } from "./deps.ts"; +import * as stdlog from "@std/log"; import { log } from "./src/logger.ts"; import { configLogger } from "./mod.ts"; @@ -65,7 +66,7 @@ testWithClient(async function testQueryErrorOccurred(client) { maxSize: client.config.poolSize, available: 0, }); - await assertThrowsAsync( + await assertRejects( () => client.query("select unknownfield from `users`"), Error, ); @@ -130,7 +131,10 @@ testWithClient(async function testQueryDecimal(client) { testWithClient(async function testQueryDatetime(client) { await client.useConnection(async (connection) => { - if (isMariaDB(connection) || semver.lt(connection.serverVersion, "5.6.0")) { + if ( + isMariaDB(connection) || + lessThan(parse(connection.serverVersion), parse("5.6.0")) + ) { return; } @@ -180,12 +184,12 @@ testWithClient(async function testPool(client) { testWithClient(async function testQueryOnClosed(client) { for (const i of [0, 0, 0]) { - await assertThrowsAsync(async () => { + await assertRejects(async () => { await client.transaction(async (conn) => { conn.close(); await conn.query("SELECT 1"); }); - }, ConnnectionError); + }, ConnectionError); } assertEquals(client.pool?.size, 0); await client.query("select 1"); @@ -206,7 +210,7 @@ testWithClient(async function testTransactionSuccess(client) { testWithClient(async function testTransactionRollback(client) { let success; - await assertThrowsAsync(async () => { + await assertRejects(async () => { success = await client.transaction(async (connection) => { // Insert an existing id await connection.execute("insert into users(name,id) values(?,?)", [ @@ -259,7 +263,7 @@ testWithClient(async function testIdleTimeout(client) { testWithClient(async function testReadTimeout(client) { await client.execute("select sleep(0.3)"); - await assertThrowsAsync(async () => { + await assertRejects(async () => { await client.execute("select sleep(0.7)"); }, ResponseTimeoutError); @@ -356,7 +360,7 @@ registerTests(); Deno.test("configLogger()", async () => { let logCount = 0; - const fakeHandler = new class extends stdlog.handlers.BaseHandler { + const fakeHandler = new class extends stdlog.BaseHandler { constructor() { super("INFO"); } diff --git a/test.util.ts b/test.util.ts index 985fb7f..e0dcab1 100644 --- a/test.util.ts +++ b/test.util.ts @@ -1,5 +1,5 @@ -import { Client, ClientConfig, Connection } from "./mod.ts"; -import { assertEquals, parse } from "./test.deps.ts"; +import { Client, type ClientConfig, type Connection } from "./mod.ts"; +import { assertEquals } from "@std/assert"; const { DB_PORT, DB_NAME, DB_PASSWORD, DB_USER, DB_HOST, DB_SOCKPATH } = Deno .env.toObject(); From cb8d50e47f190b0cf50d1f87df896f846d2716e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 10 Apr 2024 21:56:05 +0200 Subject: [PATCH 07/38] removed vendor and copied over files instead --- src/packets/builders/query.ts | 2 +- src/packets/packet.ts | 2 +- src/util.ts | 156 ++++++ vendor/deno.land/std@0.51.0/fmt/colors.ts | 207 ------- .../deno.land/std@0.51.0/testing/asserts.ts | 387 ------------- vendor/deno.land/std@0.51.0/testing/diff.ts | 221 -------- vendor/deno.land/std@0.77.0/fmt/colors.ts | 522 ------------------ .../deno.land/x/bytes_formater@v1.4.0/deps.ts | 4 - .../x/bytes_formater@v1.4.0/format.ts | 46 -- .../deno.land/x/bytes_formater@v1.4.0/mod.ts | 2 - vendor/deno.land/x/sql_builder@v1.9.1/deps.ts | 5 - vendor/deno.land/x/sql_builder@v1.9.1/join.ts | 30 - vendor/deno.land/x/sql_builder@v1.9.1/mod.ts | 5 - .../deno.land/x/sql_builder@v1.9.1/order.ts | 18 - .../deno.land/x/sql_builder@v1.9.1/query.ts | 222 -------- vendor/deno.land/x/sql_builder@v1.9.1/util.ts | 91 --- .../deno.land/x/sql_builder@v1.9.1/where.ts | 121 ---- 17 files changed, 158 insertions(+), 1883 deletions(-) delete mode 100644 vendor/deno.land/std@0.51.0/fmt/colors.ts delete mode 100644 vendor/deno.land/std@0.51.0/testing/asserts.ts delete mode 100644 vendor/deno.land/std@0.51.0/testing/diff.ts delete mode 100644 vendor/deno.land/std@0.77.0/fmt/colors.ts delete mode 100644 vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts delete mode 100644 vendor/deno.land/x/bytes_formater@v1.4.0/format.ts delete mode 100644 vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts delete mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/deps.ts delete mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/join.ts delete mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/mod.ts delete mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/order.ts delete mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/query.ts delete mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/util.ts delete mode 100644 vendor/deno.land/x/sql_builder@v1.9.1/where.ts diff --git a/src/packets/builders/query.ts b/src/packets/builders/query.ts index 2425c78..c310702 100644 --- a/src/packets/builders/query.ts +++ b/src/packets/builders/query.ts @@ -1,4 +1,4 @@ -import { replaceParams } from "sql_builder/util.ts"; +import { replaceParams } from "../../util.ts"; import { BufferWriter, encode } from "../../buffer.ts"; /** @ignore */ diff --git a/src/packets/packet.ts b/src/packets/packet.ts index 82a085b..6a0b3bb 100644 --- a/src/packets/packet.ts +++ b/src/packets/packet.ts @@ -1,4 +1,4 @@ -import { format as byteFormat } from "bytes_formater/mod.ts"; +import { byteFormat } from "../util.ts"; import { BufferReader, BufferWriter } from "../buffer.ts"; import { WriteError } from "../constant/errors.ts"; import { debug, log } from "../logger.ts"; diff --git a/src/util.ts b/src/util.ts index 1e4efd9..0631e9d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,161 @@ +import { green } from "@std/fmt/colors"; + export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { return a.map((byte, index) => { return byte ^ b[index]; }); } + +/** + * Formats a byte array into a human-readable hexdump. + * + * Taken from https://github.com/manyuanrong/bytes_formater/blob/master/format.ts + */ +export function byteFormat(data: ArrayBufferView) { + const bytes = new Uint8Array(data.buffer); + let out = " +-------------------------------------------------+\n"; + out += ` |${ + green(" 0 1 2 3 4 5 6 7 8 9 a b c d e f ") + }|\n`; + out += + "+--------+-------------------------------------------------+----------------+\n"; + + const lineCount = Math.ceil(bytes.length / 16); + + for (let line = 0; line < lineCount; line++) { + const start = line * 16; + const addr = start.toString(16).padStart(8, "0"); + const lineBytes = bytes.slice(start, start + 16); + + out += `|${green(addr)}| `; + + lineBytes.forEach( + (byte) => (out += byte.toString(16).padStart(2, "0") + " "), + ); + + if (lineBytes.length < 16) { + out += " ".repeat(16 - lineBytes.length); + } + + out += "|"; + + lineBytes.forEach(function (byte) { + return (out += byte > 31 && byte < 127 + ? green(String.fromCharCode(byte)) + : "."); + }); + + if (lineBytes.length < 16) { + out += " ".repeat(16 - lineBytes.length); + } + + out += "|\n"; + } + out += + "+--------+-------------------------------------------------+----------------+"; + return out; +} + +/** + * Replaces parameters in a SQL query with the given values. + * + * Taken from https://github.com/manyuanrong/sql-builder/blob/master/util.ts + */ +export function replaceParams(sql: string, params: any | any[]): string { + if (!params) return sql; + let paramIndex = 0; + sql = sql.replace( + /('[^'\\]*(?:\\.[^'\\]*)*')|("[^"\\]*(?:\\.[^"\\]*)*")|(\?\?)|(\?)/g, + (str) => { + if (paramIndex >= params.length) return str; + // ignore + if (/".*"/g.test(str) || /'.*'/g.test(str)) { + return str; + } + // identifier + if (str === "??") { + const val = params[paramIndex++]; + if (val instanceof Array) { + return `(${ + val.map((item) => replaceParams("??", [item])).join(",") + })`; + } else if (val === "*") { + return val; + } else if (typeof val === "string" && val.includes(".")) { + // a.b => `a`.`b` + const _arr = val.split("."); + return replaceParams(_arr.map(() => "??").join("."), _arr); + } else if ( + typeof val === "string" && + (val.includes(" as ") || val.includes(" AS ")) + ) { + // a as b => `a` AS `b` + const newVal = val.replace(" as ", " AS "); + const _arr = newVal.split(" AS "); + return replaceParams(_arr.map(() => "??").join(" AS "), _arr); + } else { + return ["`", val, "`"].join(""); + } + } + // value + const val = params[paramIndex++]; + if (val === null) return "NULL"; + switch (typeof val) { + // deno-lint-ignore no-fallthrough + case "object": + if (val instanceof Date) return `"${formatDate(val)}"`; + if (val instanceof Array) { + return `(${ + val.map((item) => replaceParams("?", [item])).join(",") + })`; + } + case "string": + return `"${escapeString(val)}"`; + case "undefined": + return "NULL"; + case "number": + case "boolean": + default: + return val; + } + }, + ); + return sql; +} + +/** + * Formats date to a 'YYYY-MM-DD HH:MM:SS.SSS' string. + */ +function formatDate(date: Date) { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const days = date + .getDate() + .toString() + .padStart(2, "0"); + const hours = date + .getHours() + .toString() + .padStart(2, "0"); + const minutes = date + .getMinutes() + .toString() + .padStart(2, "0"); + const seconds = date + .getSeconds() + .toString() + .padStart(2, "0"); + // Date does not support microseconds precision, so we only keep the milliseconds part. + const milliseconds = date + .getMilliseconds() + .toString() + .padStart(3, "0"); + return `${year}-${month}-${days} ${hours}:${minutes}:${seconds}.${milliseconds}`; +} + +/** + * Escapes a string for use in a SQL query. + */ +function escapeString(str: string) { + return str.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); +} diff --git a/vendor/deno.land/std@0.51.0/fmt/colors.ts b/vendor/deno.land/std@0.51.0/fmt/colors.ts deleted file mode 100644 index a963aa4..0000000 --- a/vendor/deno.land/std@0.51.0/fmt/colors.ts +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -/** - * A module to print ANSI terminal colors. Inspired by chalk, kleur, and colors - * on npm. - * - * ``` - * import { bgBlue, red, bold } from "https://deno.land/std/fmt/colors.ts"; - * console.log(bgBlue(red(bold("Hello world!")))); - * ``` - * - * This module supports `NO_COLOR` environmental variable disabling any coloring - * if `NO_COLOR` is set. - */ -const { noColor } = Deno; - -interface Code { - open: string; - close: string; - regexp: RegExp; -} - -/** RGB 8-bits per channel. Each in range `0->255` or `0x00->0xff` */ -interface Rgb { - r: number; - g: number; - b: number; -} - -let enabled = !noColor; - -export function setColorEnabled(value: boolean): void { - if (noColor) { - return; - } - - enabled = value; -} - -export function getColorEnabled(): boolean { - return enabled; -} - -function code(open: number[], close: number): Code { - return { - open: `\x1b[${open.join(";")}m`, - close: `\x1b[${close}m`, - regexp: new RegExp(`\\x1b\\[${close}m`, "g"), - }; -} - -function run(str: string, code: Code): string { - return enabled - ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` - : str; -} - -export function reset(str: string): string { - return run(str, code([0], 0)); -} - -export function bold(str: string): string { - return run(str, code([1], 22)); -} - -export function dim(str: string): string { - return run(str, code([2], 22)); -} - -export function italic(str: string): string { - return run(str, code([3], 23)); -} - -export function underline(str: string): string { - return run(str, code([4], 24)); -} - -export function inverse(str: string): string { - return run(str, code([7], 27)); -} - -export function hidden(str: string): string { - return run(str, code([8], 28)); -} - -export function strikethrough(str: string): string { - return run(str, code([9], 29)); -} - -export function black(str: string): string { - return run(str, code([30], 39)); -} - -export function red(str: string): string { - return run(str, code([31], 39)); -} - -export function green(str: string): string { - return run(str, code([32], 39)); -} - -export function yellow(str: string): string { - return run(str, code([33], 39)); -} - -export function blue(str: string): string { - return run(str, code([34], 39)); -} - -export function magenta(str: string): string { - return run(str, code([35], 39)); -} - -export function cyan(str: string): string { - return run(str, code([36], 39)); -} - -export function white(str: string): string { - return run(str, code([37], 39)); -} - -export function gray(str: string): string { - return run(str, code([90], 39)); -} - -export function bgBlack(str: string): string { - return run(str, code([40], 49)); -} - -export function bgRed(str: string): string { - return run(str, code([41], 49)); -} - -export function bgGreen(str: string): string { - return run(str, code([42], 49)); -} - -export function bgYellow(str: string): string { - return run(str, code([43], 49)); -} - -export function bgBlue(str: string): string { - return run(str, code([44], 49)); -} - -export function bgMagenta(str: string): string { - return run(str, code([45], 49)); -} - -export function bgCyan(str: string): string { - return run(str, code([46], 49)); -} - -export function bgWhite(str: string): string { - return run(str, code([47], 49)); -} - -/* Special Color Sequences */ - -function clampAndTruncate(n: number, max = 255, min = 0): number { - return Math.trunc(Math.max(Math.min(n, max), min)); -} - -/** Set text color using paletted 8bit colors. - * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit */ -export function rgb8(str: string, color: number): string { - return run(str, code([38, 5, clampAndTruncate(color)], 39)); -} - -/** Set background color using paletted 8bit colors. - * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit */ -export function bgRgb8(str: string, color: number): string { - return run(str, code([48, 5, clampAndTruncate(color)], 49)); -} - -/** Set text color using 24bit rgb. */ -export function rgb24(str: string, color: Rgb): string { - return run( - str, - code( - [ - 38, - 2, - clampAndTruncate(color.r), - clampAndTruncate(color.g), - clampAndTruncate(color.b), - ], - 39, - ), - ); -} - -/** Set background color using 24bit rgb. */ -export function bgRgb24(str: string, color: Rgb): string { - return run( - str, - code( - [ - 48, - 2, - clampAndTruncate(color.r), - clampAndTruncate(color.g), - clampAndTruncate(color.b), - ], - 49, - ), - ); -} diff --git a/vendor/deno.land/std@0.51.0/testing/asserts.ts b/vendor/deno.land/std@0.51.0/testing/asserts.ts deleted file mode 100644 index 9f72293..0000000 --- a/vendor/deno.land/std@0.51.0/testing/asserts.ts +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { bold, gray, green, red, white } from "../fmt/colors.ts"; -import diff, { type DiffResult, DiffType } from "./diff.ts"; - -const CAN_NOT_DISPLAY = "[Cannot display]"; - -interface Constructor { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (...args: any[]): any; -} - -export class AssertionError extends Error { - constructor(message: string) { - super(message); - this.name = "AssertionError"; - } -} - -function format(v: unknown): string { - let string = Deno.inspect(v); - if (typeof v == "string") { - string = `"${string.replace(/(?=["\\])/g, "\\")}"`; - } - return string; -} - -function createColor(diffType: DiffType): (s: string) => string { - switch (diffType) { - case DiffType.added: - return (s: string): string => green(bold(s)); - case DiffType.removed: - return (s: string): string => red(bold(s)); - default: - return white; - } -} - -function createSign(diffType: DiffType): string { - switch (diffType) { - case DiffType.added: - return "+ "; - case DiffType.removed: - return "- "; - default: - return " "; - } -} - -function buildMessage(diffResult: ReadonlyArray>): string[] { - const messages: string[] = []; - messages.push(""); - messages.push(""); - messages.push( - ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${ - green( - bold("Expected"), - ) - }`, - ); - messages.push(""); - messages.push(""); - diffResult.forEach((result: DiffResult): void => { - const c = createColor(result.type); - messages.push(c(`${createSign(result.type)}${result.value}`)); - }); - messages.push(""); - - return messages; -} - -function isKeyedCollection(x: unknown): x is Set { - return [Symbol.iterator, "size"].every((k) => k in (x as Set)); -} - -export function equal(c: unknown, d: unknown): boolean { - const seen = new Map(); - return (function compare(a: unknown, b: unknown): boolean { - // Have to render RegExp & Date for string comparison - // unless it's mistreated as object - if ( - a && - b && - ((a instanceof RegExp && b instanceof RegExp) || - (a instanceof Date && b instanceof Date)) - ) { - return String(a) === String(b); - } - if (Object.is(a, b)) { - return true; - } - if (a && typeof a === "object" && b && typeof b === "object") { - if (seen.get(a) === b) { - return true; - } - if (Object.keys(a || {}).length !== Object.keys(b || {}).length) { - return false; - } - if (isKeyedCollection(a) && isKeyedCollection(b)) { - if (a.size !== b.size) { - return false; - } - - let unmatchedEntries = a.size; - - for (const [aKey, aValue] of a.entries()) { - for (const [bKey, bValue] of b.entries()) { - /* Given that Map keys can be references, we need - * to ensure that they are also deeply equal */ - if ( - (aKey === aValue && bKey === bValue && compare(aKey, bKey)) || - (compare(aKey, bKey) && compare(aValue, bValue)) - ) { - unmatchedEntries--; - } - } - } - - return unmatchedEntries === 0; - } - const merged = { ...a, ...b }; - for (const key in merged) { - type Key = keyof typeof merged; - if (!compare(a && a[key as Key], b && b[key as Key])) { - return false; - } - } - seen.set(a, b); - return true; - } - return false; - })(c, d); -} - -/** Make an assertion, if not `true`, then throw. */ -export function assert(expr: unknown, msg = ""): asserts expr { - if (!expr) { - throw new AssertionError(msg); - } -} - -/** - * Make an assertion that `actual` and `expected` are equal, deeply. If not - * deeply equal, then throw. - */ -export function assertEquals( - actual: unknown, - expected: unknown, - msg?: string, -): void { - if (equal(actual, expected)) { - return; - } - let message = ""; - const actualString = format(actual); - const expectedString = format(expected); - try { - const diffResult = diff( - actualString.split("\n"), - expectedString.split("\n"), - ); - message = buildMessage(diffResult).join("\n"); - } catch (e) { - message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`; - } - if (msg) { - message = msg; - } - throw new AssertionError(message); -} - -/** - * Make an assertion that `actual` and `expected` are not equal, deeply. - * If not then throw. - */ -export function assertNotEquals( - actual: unknown, - expected: unknown, - msg?: string, -): void { - if (!equal(actual, expected)) { - return; - } - let actualString: string; - let expectedString: string; - try { - actualString = String(actual); - } catch (e) { - actualString = "[Cannot display]"; - } - try { - expectedString = String(expected); - } catch (e) { - expectedString = "[Cannot display]"; - } - if (!msg) { - msg = `actual: ${actualString} expected: ${expectedString}`; - } - throw new AssertionError(msg); -} - -/** - * Make an assertion that `actual` and `expected` are strictly equal. If - * not then throw. - */ -export function assertStrictEq( - actual: unknown, - expected: unknown, - msg?: string, -): void { - if (actual !== expected) { - let actualString: string; - let expectedString: string; - try { - actualString = String(actual); - } catch (e) { - actualString = "[Cannot display]"; - } - try { - expectedString = String(expected); - } catch (e) { - expectedString = "[Cannot display]"; - } - if (!msg) { - msg = `actual: ${actualString} expected: ${expectedString}`; - } - throw new AssertionError(msg); - } -} - -/** - * Make an assertion that actual contains expected. If not - * then thrown. - */ -export function assertStrContains( - actual: string, - expected: string, - msg?: string, -): void { - if (!actual.includes(expected)) { - if (!msg) { - msg = `actual: "${actual}" expected to contains: "${expected}"`; - } - throw new AssertionError(msg); - } -} - -/** - * Make an assertion that `actual` contains the `expected` values - * If not then thrown. - */ -export function assertArrayContains( - actual: unknown[], - expected: unknown[], - msg?: string, -): void { - const missing: unknown[] = []; - for (let i = 0; i < expected.length; i++) { - let found = false; - for (let j = 0; j < actual.length; j++) { - if (equal(expected[i], actual[j])) { - found = true; - break; - } - } - if (!found) { - missing.push(expected[i]); - } - } - if (missing.length === 0) { - return; - } - if (!msg) { - msg = `actual: "${actual}" expected to contains: "${expected}"`; - msg += "\n"; - msg += `missing: ${missing}`; - } - throw new AssertionError(msg); -} - -/** - * Make an assertion that `actual` match RegExp `expected`. If not - * then thrown - */ -export function assertMatch( - actual: string, - expected: RegExp, - msg?: string, -): void { - if (!expected.test(actual)) { - if (!msg) { - msg = `actual: "${actual}" expected to match: "${expected}"`; - } - throw new AssertionError(msg); - } -} - -/** - * Forcefully throws a failed assertion - */ -export function fail(msg?: string): void { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - assert(false, `Failed assertion${msg ? `: ${msg}` : "."}`); -} - -/** Executes a function, expecting it to throw. If it does not, then it - * throws. An error class and a string that should be included in the - * error message can also be asserted. - */ -export function assertThrows( - fn: () => void, - ErrorClass?: Constructor, - msgIncludes = "", - msg?: string, -): Error { - let doesThrow = false; - let error = null; - try { - fn(); - } catch (e) { - if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { - msg = - `Expected error to be instance of "${ErrorClass.name}", but was "${e.constructor.name}"${ - msg ? `: ${msg}` : "." - }`; - throw new AssertionError(msg); - } - if (msgIncludes && !e.message.includes(msgIncludes)) { - msg = - `Expected error message to include "${msgIncludes}", but got "${e.message}"${ - msg ? `: ${msg}` : "." - }`; - throw new AssertionError(msg); - } - doesThrow = true; - error = e; - } - if (!doesThrow) { - msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } - return error; -} - -export async function assertThrowsAsync( - fn: () => Promise, - ErrorClass?: Constructor, - msgIncludes = "", - msg?: string, -): Promise { - let doesThrow = false; - let error = null; - try { - await fn(); - } catch (e) { - if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { - msg = - `Expected error to be instance of "${ErrorClass.name}", but got "${e.name}"${ - msg ? `: ${msg}` : "." - }`; - throw new AssertionError(msg); - } - if (msgIncludes && !e.message.includes(msgIncludes)) { - msg = - `Expected error message to include "${msgIncludes}", but got "${e.message}"${ - msg ? `: ${msg}` : "." - }`; - throw new AssertionError(msg); - } - doesThrow = true; - error = e; - } - if (!doesThrow) { - msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } - return error; -} - -/** Use this to stub out methods that will throw when invoked. */ -export function unimplemented(msg?: string): never { - throw new AssertionError(msg || "unimplemented"); -} - -/** Use this to assert unreachable code. */ -export function unreachable(): never { - throw new AssertionError("unreachable"); -} diff --git a/vendor/deno.land/std@0.51.0/testing/diff.ts b/vendor/deno.land/std@0.51.0/testing/diff.ts deleted file mode 100644 index 1bc22be..0000000 --- a/vendor/deno.land/std@0.51.0/testing/diff.ts +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -interface FarthestPoint { - y: number; - id: number; -} - -export enum DiffType { - removed = "removed", - common = "common", - added = "added", -} - -export interface DiffResult { - type: DiffType; - value: T; -} - -const REMOVED = 1; -const COMMON = 2; -const ADDED = 3; - -function createCommon(A: T[], B: T[], reverse?: boolean): T[] { - const common = []; - if (A.length === 0 || B.length === 0) return []; - for (let i = 0; i < Math.min(A.length, B.length); i += 1) { - if ( - A[reverse ? A.length - i - 1 : i] === B[reverse ? B.length - i - 1 : i] - ) { - common.push(A[reverse ? A.length - i - 1 : i]); - } else { - return common; - } - } - return common; -} - -export default function diff(A: T[], B: T[]): Array> { - const prefixCommon = createCommon(A, B); - const suffixCommon = createCommon( - A.slice(prefixCommon.length), - B.slice(prefixCommon.length), - true, - ).reverse(); - A = suffixCommon.length - ? A.slice(prefixCommon.length, -suffixCommon.length) - : A.slice(prefixCommon.length); - B = suffixCommon.length - ? B.slice(prefixCommon.length, -suffixCommon.length) - : B.slice(prefixCommon.length); - const swapped = B.length > A.length; - [A, B] = swapped ? [B, A] : [A, B]; - const M = A.length; - const N = B.length; - if (!M && !N && !suffixCommon.length && !prefixCommon.length) return []; - if (!N) { - return [ - ...prefixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }), - ), - ...A.map( - (a): DiffResult => ({ - type: swapped ? DiffType.added : DiffType.removed, - value: a, - }), - ), - ...suffixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }), - ), - ]; - } - const offset = N; - const delta = M - N; - const size = M + N + 1; - const fp = new Array(size).fill({ y: -1 }); - /** - * INFO: - * This buffer is used to save memory and improve performance. - * The first half is used to save route and last half is used to save diff - * type. - * This is because, when I kept new uint8array area to save type,performance - * worsened. - */ - const routes = new Uint32Array((M * N + size + 1) * 2); - const diffTypesPtrOffset = routes.length / 2; - let ptr = 0; - let p = -1; - - function backTrace( - A: T[], - B: T[], - current: FarthestPoint, - swapped: boolean, - ): Array<{ - type: DiffType; - value: T; - }> { - const M = A.length; - const N = B.length; - const result = []; - let a = M - 1; - let b = N - 1; - let j = routes[current.id]; - let type = routes[current.id + diffTypesPtrOffset]; - while (true) { - if (!j && !type) break; - const prev = j; - if (type === REMOVED) { - result.unshift({ - type: swapped ? DiffType.removed : DiffType.added, - value: B[b], - }); - b -= 1; - } else if (type === ADDED) { - result.unshift({ - type: swapped ? DiffType.added : DiffType.removed, - value: A[a], - }); - a -= 1; - } else { - result.unshift({ type: DiffType.common, value: A[a] }); - a -= 1; - b -= 1; - } - j = routes[prev]; - type = routes[prev + diffTypesPtrOffset]; - } - return result; - } - - function createFP( - slide: FarthestPoint, - down: FarthestPoint, - k: number, - M: number, - ): FarthestPoint { - if (slide && slide.y === -1 && down && down.y === -1) { - return { y: 0, id: 0 }; - } - if ( - (down && down.y === -1) || - k === M || - (slide && slide.y) > (down && down.y) + 1 - ) { - const prev = slide.id; - ptr++; - routes[ptr] = prev; - routes[ptr + diffTypesPtrOffset] = ADDED; - return { y: slide.y, id: ptr }; - } else { - const prev = down.id; - ptr++; - routes[ptr] = prev; - routes[ptr + diffTypesPtrOffset] = REMOVED; - return { y: down.y + 1, id: ptr }; - } - } - - function snake( - k: number, - slide: FarthestPoint, - down: FarthestPoint, - _offset: number, - A: T[], - B: T[], - ): FarthestPoint { - const M = A.length; - const N = B.length; - if (k < -N || M < k) return { y: -1, id: -1 }; - const fp = createFP(slide, down, k, M); - while (fp.y + k < M && fp.y < N && A[fp.y + k] === B[fp.y]) { - const prev = fp.id; - ptr++; - fp.id = ptr; - fp.y += 1; - routes[ptr] = prev; - routes[ptr + diffTypesPtrOffset] = COMMON; - } - return fp; - } - - while (fp[delta + offset].y < N) { - p = p + 1; - for (let k = -p; k < delta; ++k) { - fp[k + offset] = snake( - k, - fp[k - 1 + offset], - fp[k + 1 + offset], - offset, - A, - B, - ); - } - for (let k = delta + p; k > delta; --k) { - fp[k + offset] = snake( - k, - fp[k - 1 + offset], - fp[k + 1 + offset], - offset, - A, - B, - ); - } - fp[delta + offset] = snake( - delta, - fp[delta - 1 + offset], - fp[delta + 1 + offset], - offset, - A, - B, - ); - } - return [ - ...prefixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }), - ), - ...backTrace(A, B, fp[delta + offset], swapped), - ...suffixCommon.map( - (c): DiffResult => ({ type: DiffType.common, value: c }), - ), - ]; -} diff --git a/vendor/deno.land/std@0.77.0/fmt/colors.ts b/vendor/deno.land/std@0.77.0/fmt/colors.ts deleted file mode 100644 index 6c98671..0000000 --- a/vendor/deno.land/std@0.77.0/fmt/colors.ts +++ /dev/null @@ -1,522 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -/** A module to print ANSI terminal colors. Inspired by chalk, kleur, and colors - * on npm. - * - * ``` - * import { bgBlue, red, bold } from "https://deno.land/std/fmt/colors.ts"; - * console.log(bgBlue(red(bold("Hello world!")))); - * ``` - * - * This module supports `NO_COLOR` environmental variable disabling any coloring - * if `NO_COLOR` is set. - * - * This module is browser compatible. */ - -const noColor = globalThis.Deno?.noColor ?? true; - -interface Code { - open: string; - close: string; - regexp: RegExp; -} - -/** RGB 8-bits per channel. Each in range `0->255` or `0x00->0xff` */ -interface Rgb { - r: number; - g: number; - b: number; -} - -let enabled = !noColor; - -/** - * Set changing text color to enabled or disabled - * @param value - */ -export function setColorEnabled(value: boolean): void { - if (noColor) { - return; - } - - enabled = value; -} - -/** Get whether text color change is enabled or disabled. */ -export function getColorEnabled(): boolean { - return enabled; -} - -/** - * Builds color code - * @param open - * @param close - */ -function code(open: number[], close: number): Code { - return { - open: `\x1b[${open.join(";")}m`, - close: `\x1b[${close}m`, - regexp: new RegExp(`\\x1b\\[${close}m`, "g"), - }; -} - -/** - * Applies color and background based on color code and its associated text - * @param str text to apply color settings to - * @param code color code to apply - */ -function run(str: string, code: Code): string { - return enabled - ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` - : str; -} - -/** - * Reset the text modified - * @param str text to reset - */ -export function reset(str: string): string { - return run(str, code([0], 0)); -} - -/** - * Make the text bold. - * @param str text to make bold - */ -export function bold(str: string): string { - return run(str, code([1], 22)); -} - -/** - * The text emits only a small amount of light. - * @param str text to dim - */ -export function dim(str: string): string { - return run(str, code([2], 22)); -} - -/** - * Make the text italic. - * @param str text to make italic - */ -export function italic(str: string): string { - return run(str, code([3], 23)); -} - -/** - * Make the text underline. - * @param str text to underline - */ -export function underline(str: string): string { - return run(str, code([4], 24)); -} - -/** - * Invert background color and text color. - * @param str text to invert its color - */ -export function inverse(str: string): string { - return run(str, code([7], 27)); -} - -/** - * Make the text hidden. - * @param str text to hide - */ -export function hidden(str: string): string { - return run(str, code([8], 28)); -} - -/** - * Put horizontal line through the center of the text. - * @param str text to strike through - */ -export function strikethrough(str: string): string { - return run(str, code([9], 29)); -} - -/** - * Set text color to black. - * @param str text to make black - */ -export function black(str: string): string { - return run(str, code([30], 39)); -} - -/** - * Set text color to red. - * @param str text to make red - */ -export function red(str: string): string { - return run(str, code([31], 39)); -} - -/** - * Set text color to green. - * @param str text to make green - */ -export function green(str: string): string { - return run(str, code([32], 39)); -} - -/** - * Set text color to yellow. - * @param str text to make yellow - */ -export function yellow(str: string): string { - return run(str, code([33], 39)); -} - -/** - * Set text color to blue. - * @param str text to make blue - */ -export function blue(str: string): string { - return run(str, code([34], 39)); -} - -/** - * Set text color to magenta. - * @param str text to make magenta - */ -export function magenta(str: string): string { - return run(str, code([35], 39)); -} - -/** - * Set text color to cyan. - * @param str text to make cyan - */ -export function cyan(str: string): string { - return run(str, code([36], 39)); -} - -/** - * Set text color to white. - * @param str text to make white - */ -export function white(str: string): string { - return run(str, code([37], 39)); -} - -/** - * Set text color to gray. - * @param str text to make gray - */ -export function gray(str: string): string { - return brightBlack(str); -} - -/** - * Set text color to bright black. - * @param str text to make bright-black - */ -export function brightBlack(str: string): string { - return run(str, code([90], 39)); -} - -/** - * Set text color to bright red. - * @param str text to make bright-red - */ -export function brightRed(str: string): string { - return run(str, code([91], 39)); -} - -/** - * Set text color to bright green. - * @param str text to make bright-green - */ -export function brightGreen(str: string): string { - return run(str, code([92], 39)); -} - -/** - * Set text color to bright yellow. - * @param str text to make bright-yellow - */ -export function brightYellow(str: string): string { - return run(str, code([93], 39)); -} - -/** - * Set text color to bright blue. - * @param str text to make bright-blue - */ -export function brightBlue(str: string): string { - return run(str, code([94], 39)); -} - -/** - * Set text color to bright magenta. - * @param str text to make bright-magenta - */ -export function brightMagenta(str: string): string { - return run(str, code([95], 39)); -} - -/** - * Set text color to bright cyan. - * @param str text to make bright-cyan - */ -export function brightCyan(str: string): string { - return run(str, code([96], 39)); -} - -/** - * Set text color to bright white. - * @param str text to make bright-white - */ -export function brightWhite(str: string): string { - return run(str, code([97], 39)); -} - -/** - * Set background color to black. - * @param str text to make its background black - */ -export function bgBlack(str: string): string { - return run(str, code([40], 49)); -} - -/** - * Set background color to red. - * @param str text to make its background red - */ -export function bgRed(str: string): string { - return run(str, code([41], 49)); -} - -/** - * Set background color to green. - * @param str text to make its background green - */ -export function bgGreen(str: string): string { - return run(str, code([42], 49)); -} - -/** - * Set background color to yellow. - * @param str text to make its background yellow - */ -export function bgYellow(str: string): string { - return run(str, code([43], 49)); -} - -/** - * Set background color to blue. - * @param str text to make its background blue - */ -export function bgBlue(str: string): string { - return run(str, code([44], 49)); -} - -/** - * Set background color to magenta. - * @param str text to make its background magenta - */ -export function bgMagenta(str: string): string { - return run(str, code([45], 49)); -} - -/** - * Set background color to cyan. - * @param str text to make its background cyan - */ -export function bgCyan(str: string): string { - return run(str, code([46], 49)); -} - -/** - * Set background color to white. - * @param str text to make its background white - */ -export function bgWhite(str: string): string { - return run(str, code([47], 49)); -} - -/** - * Set background color to bright black. - * @param str text to make its background bright-black - */ -export function bgBrightBlack(str: string): string { - return run(str, code([100], 49)); -} - -/** - * Set background color to bright red. - * @param str text to make its background bright-red - */ -export function bgBrightRed(str: string): string { - return run(str, code([101], 49)); -} - -/** - * Set background color to bright green. - * @param str text to make its background bright-green - */ -export function bgBrightGreen(str: string): string { - return run(str, code([102], 49)); -} - -/** - * Set background color to bright yellow. - * @param str text to make its background bright-yellow - */ -export function bgBrightYellow(str: string): string { - return run(str, code([103], 49)); -} - -/** - * Set background color to bright blue. - * @param str text to make its background bright-blue - */ -export function bgBrightBlue(str: string): string { - return run(str, code([104], 49)); -} - -/** - * Set background color to bright magenta. - * @param str text to make its background bright-magenta - */ -export function bgBrightMagenta(str: string): string { - return run(str, code([105], 49)); -} - -/** - * Set background color to bright cyan. - * @param str text to make its background bright-cyan - */ -export function bgBrightCyan(str: string): string { - return run(str, code([106], 49)); -} - -/** - * Set background color to bright white. - * @param str text to make its background bright-white - */ -export function bgBrightWhite(str: string): string { - return run(str, code([107], 49)); -} - -/* Special Color Sequences */ - -/** - * Clam and truncate color codes - * @param n - * @param max number to truncate to - * @param min number to truncate from - */ -function clampAndTruncate(n: number, max = 255, min = 0): number { - return Math.trunc(Math.max(Math.min(n, max), min)); -} - -/** - * Set text color using paletted 8bit colors. - * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit - * @param str text color to apply paletted 8bit colors to - * @param color code - */ -export function rgb8(str: string, color: number): string { - return run(str, code([38, 5, clampAndTruncate(color)], 39)); -} - -/** - * Set background color using paletted 8bit colors. - * https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit - * @param str text color to apply paletted 8bit background colors to - * @param color code - */ -export function bgRgb8(str: string, color: number): string { - return run(str, code([48, 5, clampAndTruncate(color)], 49)); -} - -/** - * Set text color using 24bit rgb. - * `color` can be a number in range `0x000000` to `0xffffff` or - * an `Rgb`. - * - * To produce the color magenta: - * - * rgba24("foo", 0xff00ff); - * rgba24("foo", {r: 255, g: 0, b: 255}); - * @param str text color to apply 24bit rgb to - * @param color code - */ -export function rgb24(str: string, color: number | Rgb): string { - if (typeof color === "number") { - return run( - str, - code( - [38, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], - 39, - ), - ); - } - return run( - str, - code( - [ - 38, - 2, - clampAndTruncate(color.r), - clampAndTruncate(color.g), - clampAndTruncate(color.b), - ], - 39, - ), - ); -} - -/** - * Set background color using 24bit rgb. - * `color` can be a number in range `0x000000` to `0xffffff` or - * an `Rgb`. - * - * To produce the color magenta: - * - * bgRgba24("foo", 0xff00ff); - * bgRgba24("foo", {r: 255, g: 0, b: 255}); - * @param str text color to apply 24bit rgb to - * @param color code - */ -export function bgRgb24(str: string, color: number | Rgb): string { - if (typeof color === "number") { - return run( - str, - code( - [48, 2, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff], - 49, - ), - ); - } - return run( - str, - code( - [ - 48, - 2, - clampAndTruncate(color.r), - clampAndTruncate(color.g), - clampAndTruncate(color.b), - ], - 49, - ), - ); -} - -// https://github.com/chalk/ansi-regex/blob/2b56fb0c7a07108e5b54241e8faec160d393aedb/index.js -const ANSI_PATTERN = new RegExp( - [ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", - ].join("|"), - "g", -); - -/** - * Remove ANSI escape codes from the string. - * @param string to remove ANSI escape codes from - */ -export function stripColor(string: string): string { - return string.replace(ANSI_PATTERN, ""); -} diff --git a/vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts b/vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts deleted file mode 100644 index dc3fcd7..0000000 --- a/vendor/deno.land/x/bytes_formater@v1.4.0/deps.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - green, - setColorEnabled, -} from "https://deno.land/std@0.77.0/fmt/colors.ts"; diff --git a/vendor/deno.land/x/bytes_formater@v1.4.0/format.ts b/vendor/deno.land/x/bytes_formater@v1.4.0/format.ts deleted file mode 100644 index 707aaf0..0000000 --- a/vendor/deno.land/x/bytes_formater@v1.4.0/format.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { green } from "./deps.ts"; - -export function format(data: ArrayBufferView) { - const bytes = new Uint8Array(data.buffer); - let out = " +-------------------------------------------------+\n"; - out += ` |${ - green(" 0 1 2 3 4 5 6 7 8 9 a b c d e f ") - }|\n`; - out += - "+--------+-------------------------------------------------+----------------+\n"; - - const lineCount = Math.ceil(bytes.length / 16); - - for (let line = 0; line < lineCount; line++) { - const start = line * 16; - const addr = start.toString(16).padStart(8, "0"); - const lineBytes = bytes.slice(start, start + 16); - - out += `|${green(addr)}| `; - - lineBytes.forEach( - (byte) => (out += byte.toString(16).padStart(2, "0") + " "), - ); - - if (lineBytes.length < 16) { - out += " ".repeat(16 - lineBytes.length); - } - - out += "|"; - - lineBytes.forEach(function (byte) { - return (out += byte > 31 && byte < 127 - ? green(String.fromCharCode(byte)) - : "."); - }); - - if (lineBytes.length < 16) { - out += " ".repeat(16 - lineBytes.length); - } - - out += "|\n"; - } - out += - "+--------+-------------------------------------------------+----------------+"; - return out; -} diff --git a/vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts b/vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts deleted file mode 100644 index 8a00e1f..0000000 --- a/vendor/deno.land/x/bytes_formater@v1.4.0/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { format } from "./format.ts"; -export { setColorEnabled } from "./deps.ts"; diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/deps.ts b/vendor/deno.land/x/sql_builder@v1.9.1/deps.ts deleted file mode 100644 index e56adf6..0000000 --- a/vendor/deno.land/x/sql_builder@v1.9.1/deps.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - assert, - assertEquals, -} from "https://deno.land/std@0.51.0/testing/asserts.ts"; -export { replaceParams } from "./util.ts"; diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/join.ts b/vendor/deno.land/x/sql_builder@v1.9.1/join.ts deleted file mode 100644 index 384cb61..0000000 --- a/vendor/deno.land/x/sql_builder@v1.9.1/join.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { replaceParams } from "./util.ts"; - -export class Join { - value: string = ""; - constructor(type: string, readonly table: string, readonly alias?: string) { - const name = alias ? "?? ??" : "??"; - this.value = replaceParams(`${type} ${name}`, [table, alias]); - } - - static inner(table: string, alias?: string): Join { - return new Join("INNER JOIN", table, alias); - } - - static full(table: string, alias?: string): Join { - return new Join("FULL OUTER JOIN", table, alias); - } - - static left(table: string, alias?: string): Join { - return new Join("LEFT OUTER JOIN", table, alias); - } - - static right(table: string, alias?: string): Join { - return new Join("RIGHT OUTER JOIN", table, alias); - } - - on(a: string, b: string) { - this.value += replaceParams(` ON ?? = ??`, [a, b]); - return this; - } -} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/mod.ts b/vendor/deno.land/x/sql_builder@v1.9.1/mod.ts deleted file mode 100644 index 96ffe40..0000000 --- a/vendor/deno.land/x/sql_builder@v1.9.1/mod.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { Join } from "./join.ts"; -export { Order } from "./order.ts"; -export { Query } from "./query.ts"; -export { replaceParams } from "./util.ts"; -export { Where } from "./where.ts"; diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/order.ts b/vendor/deno.land/x/sql_builder@v1.9.1/order.ts deleted file mode 100644 index 01338c0..0000000 --- a/vendor/deno.land/x/sql_builder@v1.9.1/order.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { replaceParams } from "./util.ts"; - -export class Order { - value: string = ""; - static by(field: string) { - const order = new Order(); - return { - get desc() { - order.value = replaceParams("?? DESC", [field]); - return order; - }, - get asc() { - order.value = replaceParams("?? ASC", [field]); - return order; - }, - }; - } -} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/query.ts b/vendor/deno.land/x/sql_builder@v1.9.1/query.ts deleted file mode 100644 index a87f7c9..0000000 --- a/vendor/deno.land/x/sql_builder@v1.9.1/query.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { assert, replaceParams } from "./deps.ts"; -import type { Order } from "./order.ts"; -import type { Where } from "./where.ts"; -import type { Join } from "./join.ts"; - -export class Query { - private _type?: "select" | "insert" | "update" | "delete"; - private _table?: string; - private _where: string[] = []; - private _joins: string[] = []; - private _orders: Order[] = []; - private _fields: string[] = []; - private _groupBy: string[] = []; - private _having: string[] = []; - private _insertValues: any[] = []; - private _updateValue?: any; - private _limit?: { start: number; size: number }; - - private get orderSQL() { - if (this._orders && this._orders.length) { - return `ORDER BY ` + this._orders.map((order) => order.value).join(", "); - } - } - - private get whereSQL() { - if (this._where && this._where.length) { - return `WHERE ` + this._where.join(" AND "); - } - } - - private get havingSQL() { - if (this._having && this._having.length) { - return `HAVING ` + this._having.join(" AND "); - } - } - - private get joinSQL() { - if (this._joins && this._joins.length) { - return this._joins.join(" "); - } - } - - private get groupSQL() { - if (this._groupBy && this._groupBy.length) { - return ( - "GROUP BY " + - this._groupBy.map((f) => replaceParams("??", [f])).join(", ") - ); - } - } - private get limitSQL() { - if (this._limit) { - return `LIMIT ${this._limit.start}, ${this._limit.size}`; - } - } - - private get selectSQL() { - return [ - "SELECT", - this._fields.join(", "), - "FROM", - replaceParams("??", [this._table]), - this.joinSQL, - this.whereSQL, - this.groupSQL, - this.havingSQL, - this.orderSQL, - this.limitSQL, - ] - .filter((str) => str) - .join(" "); - } - - private get insertSQL() { - const len = this._insertValues.length; - const fields = Object.keys(this._insertValues[0]); - const values = this._insertValues.map((row) => { - return fields.map((key) => row[key]!); - }); - return replaceParams(`INSERT INTO ?? ?? VALUES ${"? ".repeat(len)}`, [ - this._table, - fields, - ...values, - ]); - } - - private get updateSQL() { - assert(!!this._updateValue); - const set = Object.keys(this._updateValue) - .map((key) => { - return replaceParams(`?? = ?`, [key, this._updateValue[key]]); - }) - .join(", "); - return [ - replaceParams(`UPDATE ?? SET ${set}`, [this._table]), - this.whereSQL, - ].join(" "); - } - - private get deleteSQL() { - return [replaceParams(`DELETE FROM ??`, [this._table]), this.whereSQL].join( - " ", - ); - } - - table(name: string) { - this._table = name; - return this; - } - - order(...orders: Order[]) { - this._orders = this._orders.concat(orders); - return this; - } - - groupBy(...fields: string[]) { - this._groupBy = fields; - return this; - } - - where(where: Where | string) { - if (typeof where === "string") { - this._where.push(where); - } else { - this._where.push(where.value); - } - return this; - } - - having(where: Where | string) { - if (typeof where === "string") { - this._having.push(where); - } else { - this._having.push(where.value); - } - return this; - } - - limit(start: number, size: number) { - this._limit = { start, size }; - return this; - } - - join(join: Join | string) { - if (typeof join === "string") { - this._joins.push(join); - } else { - this._joins.push(join.value); - } - return this; - } - - select(...fields: string[]) { - this._type = "select"; - assert(fields.length > 0); - this._fields = this._fields.concat( - fields.map((field) => { - if (field.toLocaleLowerCase().indexOf(" as ") > -1) { - return field; - } else if (field.split(".").length > 1) { - return replaceParams("??.??", field.split(".")); - } else { - return replaceParams("??", [field]); - } - }), - ); - return this; - } - - insert(data: Object[] | Object) { - this._type = "insert"; - if (!(data instanceof Array)) { - data = [data]; - } - this._insertValues = data as []; - return this; - } - - update(data: Object) { - this._type = "update"; - this._updateValue = data; - return this; - } - - delete(table?: string) { - if (table) this._table = table; - this._type = "delete"; - return this; - } - - clone() { - const newQuery = new Query(); - newQuery._type = this._type; - newQuery._table = this._table; - newQuery._where = this._where; - newQuery._joins = this._joins; - newQuery._orders = this._orders; - newQuery._fields = this._fields; - newQuery._groupBy = this._groupBy; - newQuery._having = this._having; - newQuery._insertValues = this._insertValues; - newQuery._updateValue = this._updateValue; - newQuery._limit = this._limit; - return newQuery; - } - - build(): string { - assert(!!this._table); - switch (this._type) { - case "select": - return this.selectSQL; - case "insert": - return this.insertSQL; - case "update": - return this.updateSQL; - case "delete": - return this.deleteSQL; - default: - return ""; - } - } -} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/util.ts b/vendor/deno.land/x/sql_builder@v1.9.1/util.ts deleted file mode 100644 index e6658f9..0000000 --- a/vendor/deno.land/x/sql_builder@v1.9.1/util.ts +++ /dev/null @@ -1,91 +0,0 @@ -export function replaceParams(sql: string, params: any | any[]): string { - if (!params) return sql; - let paramIndex = 0; - sql = sql.replace( - /('[^'\\]*(?:\\.[^'\\]*)*')|("[^"\\]*(?:\\.[^"\\]*)*")|(\?\?)|(\?)/g, - (str) => { - if (paramIndex >= params.length) return str; - // ignore - if (/".*"/g.test(str) || /'.*'/g.test(str)) { - return str; - } - // identifier - if (str === "??") { - const val = params[paramIndex++]; - if (val instanceof Array) { - return `(${ - val.map((item) => replaceParams("??", [item])).join(",") - })`; - } else if (val === "*") { - return val; - } else if (typeof val === "string" && val.includes(".")) { - // a.b => `a`.`b` - const _arr = val.split("."); - return replaceParams(_arr.map(() => "??").join("."), _arr); - } else if ( - typeof val === "string" && - (val.includes(" as ") || val.includes(" AS ")) - ) { - // a as b => `a` AS `b` - const newVal = val.replace(" as ", " AS "); - const _arr = newVal.split(" AS "); - return replaceParams(_arr.map(() => "??").join(" AS "), _arr); - } else { - return ["`", val, "`"].join(""); - } - } - // value - const val = params[paramIndex++]; - if (val === null) return "NULL"; - switch (typeof val) { - case "object": - if (val instanceof Date) return `"${formatDate(val)}"`; - if (val instanceof Array) { - return `(${ - val.map((item) => replaceParams("?", [item])).join(",") - })`; - } - case "string": - return `"${escapeString(val)}"`; - case "undefined": - return "NULL"; - case "number": - case "boolean": - default: - return val; - } - }, - ); - return sql; -} - -function formatDate(date: Date) { - const year = date.getFullYear(); - const month = (date.getMonth() + 1).toString().padStart(2, "0"); - const days = date - .getDate() - .toString() - .padStart(2, "0"); - const hours = date - .getHours() - .toString() - .padStart(2, "0"); - const minutes = date - .getMinutes() - .toString() - .padStart(2, "0"); - const seconds = date - .getSeconds() - .toString() - .padStart(2, "0"); - // Date does not support microseconds precision, so we only keep the milliseconds part. - const milliseconds = date - .getMilliseconds() - .toString() - .padStart(3, "0"); - return `${year}-${month}-${days} ${hours}:${minutes}:${seconds}.${milliseconds}`; -} - -function escapeString(str: string) { - return str.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); -} diff --git a/vendor/deno.land/x/sql_builder@v1.9.1/where.ts b/vendor/deno.land/x/sql_builder@v1.9.1/where.ts deleted file mode 100644 index cd18b26..0000000 --- a/vendor/deno.land/x/sql_builder@v1.9.1/where.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { replaceParams } from "./util.ts"; - -/** - * Where sub sql builder - */ -export class Where { - private expr: string; - private params: any[]; - constructor(expr: string, params: any[]) { - this.expr = expr; - this.params = params; - } - - get value(): string { - return this.toString(); - } - - toString(): string { - return replaceParams(this.expr, this.params); - } - - static expr(expr: string, ...params: any[]): Where { - return new Where(expr, params); - } - - static eq(field: string, value: any) { - return this.expr("?? = ?", field, value); - } - - /** - * eq from object - * @param data - */ - static from(data: any): Where { - const conditions = Object.keys(data).map((key) => this.eq(key, data[key])); - return this.and(...conditions); - } - - static gt(field: string, value: any) { - return this.expr("?? > ?", field, value); - } - - static gte(field: string, value: any) { - return this.expr("?? >= ?", field, value); - } - - static lt(field: string, value: any) { - return this.expr("?? < ?", field, value); - } - - static lte(field: string, value: any) { - return this.expr("?? <= ?", field, value); - } - - static ne(field: string, value: any) { - return this.expr("?? != ?", field, value); - } - - static isNull(field: string) { - return this.expr("?? IS NULL", field); - } - - static notNull(field: string) { - return this.expr("?? NOT NULL", field); - } - - static in(field: string, ...values: any[]) { - const params: any[] = values.length > 1 ? values : values[0]; - return this.expr("?? IN ?", field, params); - } - - static notIn(field: string, ...values: any[]) { - const params: any[] = values.length > 1 ? values : values[0]; - return this.expr("?? NOT IN ?", field, params); - } - - static like(field: string, value: any) { - return this.expr("?? LIKE ?", field, value); - } - - static between(field: string, startValue: any, endValue: any) { - return this.expr("?? BETWEEN ? AND ?", field, startValue, endValue); - } - - static field(name: string) { - return { - gt: (value: any) => this.gt(name, value), - gte: (value: any) => this.gte(name, value), - lt: (value: any) => this.lt(name, value), - lte: (value: any) => this.lte(name, value), - ne: (value: any) => this.ne(name, value), - eq: (value: any) => this.eq(name, value), - isNull: () => this.isNull(name), - notNull: () => this.notNull(name), - in: (...values: any[]) => this.in(name, ...values), - notIn: (...values: any[]) => this.notIn(name, ...values), - like: (value: any) => this.like(name, value), - between: (start: any, end: any) => this.between(name, start, end), - }; - } - - static and(...expr: (null | undefined | Where)[]): Where { - const sql = `(${ - expr - .filter((e) => e) - .map((e) => e!.value) - .join(" AND ") - })`; - return new Where(sql, []); - } - - static or(...expr: (null | undefined | Where)[]): Where { - const sql = `(${ - expr - .filter((e) => e) - .map((e) => e!.value) - .join(" OR ") - })`; - return new Where(sql, []); - } -} From 6962c5cd00049a4065d3660bc29e6f15ddb5b0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 12 Apr 2024 12:36:31 +0200 Subject: [PATCH 08/38] renamed src folder to lib --- {src => lib}/auth.ts | 0 .../auth_plugin/caching_sha2_password.ts | 0 {src => lib}/auth_plugin/crypt.ts | 0 {src => lib}/auth_plugin/index.ts | 0 {src => lib}/buffer.ts | 0 {src => lib}/client.ts | 0 {src => lib}/connection.ts | 0 {src => lib}/constant/capabilities.ts | 0 {src => lib}/constant/charset.ts | 0 {src => lib}/constant/errors.ts | 0 {src => lib}/constant/mysql_types.ts | 0 {src => lib}/constant/packet.ts | 0 {src => lib}/constant/server_status.ts | 0 {src => lib}/deferred.ts | 0 {src => lib}/logger.ts | 0 {src => lib}/packets/builders/auth.ts | 0 .../packets/builders/client_capabilities.ts | 0 {src => lib}/packets/builders/query.ts | 0 {src => lib}/packets/builders/tls.ts | 0 {src => lib}/packets/packet.ts | 2 +- {src => lib}/packets/parsers/authswitch.ts | 0 {src => lib}/packets/parsers/err.ts | 0 {src => lib}/packets/parsers/handshake.ts | 0 {src => lib}/packets/parsers/result.ts | 0 {src => lib}/pool.ts | 0 {src => lib}/util.ts | 0 mod.ts | 16 ++++++++-------- test.ts | 4 ++-- 28 files changed, 11 insertions(+), 11 deletions(-) rename {src => lib}/auth.ts (100%) rename {src => lib}/auth_plugin/caching_sha2_password.ts (100%) rename {src => lib}/auth_plugin/crypt.ts (100%) rename {src => lib}/auth_plugin/index.ts (100%) rename {src => lib}/buffer.ts (100%) rename {src => lib}/client.ts (100%) rename {src => lib}/connection.ts (100%) rename {src => lib}/constant/capabilities.ts (100%) rename {src => lib}/constant/charset.ts (100%) rename {src => lib}/constant/errors.ts (100%) rename {src => lib}/constant/mysql_types.ts (100%) rename {src => lib}/constant/packet.ts (100%) rename {src => lib}/constant/server_status.ts (100%) rename {src => lib}/deferred.ts (100%) rename {src => lib}/logger.ts (100%) rename {src => lib}/packets/builders/auth.ts (100%) rename {src => lib}/packets/builders/client_capabilities.ts (100%) rename {src => lib}/packets/builders/query.ts (100%) rename {src => lib}/packets/builders/tls.ts (100%) rename {src => lib}/packets/packet.ts (97%) rename {src => lib}/packets/parsers/authswitch.ts (100%) rename {src => lib}/packets/parsers/err.ts (100%) rename {src => lib}/packets/parsers/handshake.ts (100%) rename {src => lib}/packets/parsers/result.ts (100%) rename {src => lib}/pool.ts (100%) rename {src => lib}/util.ts (100%) diff --git a/src/auth.ts b/lib/auth.ts similarity index 100% rename from src/auth.ts rename to lib/auth.ts diff --git a/src/auth_plugin/caching_sha2_password.ts b/lib/auth_plugin/caching_sha2_password.ts similarity index 100% rename from src/auth_plugin/caching_sha2_password.ts rename to lib/auth_plugin/caching_sha2_password.ts diff --git a/src/auth_plugin/crypt.ts b/lib/auth_plugin/crypt.ts similarity index 100% rename from src/auth_plugin/crypt.ts rename to lib/auth_plugin/crypt.ts diff --git a/src/auth_plugin/index.ts b/lib/auth_plugin/index.ts similarity index 100% rename from src/auth_plugin/index.ts rename to lib/auth_plugin/index.ts diff --git a/src/buffer.ts b/lib/buffer.ts similarity index 100% rename from src/buffer.ts rename to lib/buffer.ts diff --git a/src/client.ts b/lib/client.ts similarity index 100% rename from src/client.ts rename to lib/client.ts diff --git a/src/connection.ts b/lib/connection.ts similarity index 100% rename from src/connection.ts rename to lib/connection.ts diff --git a/src/constant/capabilities.ts b/lib/constant/capabilities.ts similarity index 100% rename from src/constant/capabilities.ts rename to lib/constant/capabilities.ts diff --git a/src/constant/charset.ts b/lib/constant/charset.ts similarity index 100% rename from src/constant/charset.ts rename to lib/constant/charset.ts diff --git a/src/constant/errors.ts b/lib/constant/errors.ts similarity index 100% rename from src/constant/errors.ts rename to lib/constant/errors.ts diff --git a/src/constant/mysql_types.ts b/lib/constant/mysql_types.ts similarity index 100% rename from src/constant/mysql_types.ts rename to lib/constant/mysql_types.ts diff --git a/src/constant/packet.ts b/lib/constant/packet.ts similarity index 100% rename from src/constant/packet.ts rename to lib/constant/packet.ts diff --git a/src/constant/server_status.ts b/lib/constant/server_status.ts similarity index 100% rename from src/constant/server_status.ts rename to lib/constant/server_status.ts diff --git a/src/deferred.ts b/lib/deferred.ts similarity index 100% rename from src/deferred.ts rename to lib/deferred.ts diff --git a/src/logger.ts b/lib/logger.ts similarity index 100% rename from src/logger.ts rename to lib/logger.ts diff --git a/src/packets/builders/auth.ts b/lib/packets/builders/auth.ts similarity index 100% rename from src/packets/builders/auth.ts rename to lib/packets/builders/auth.ts diff --git a/src/packets/builders/client_capabilities.ts b/lib/packets/builders/client_capabilities.ts similarity index 100% rename from src/packets/builders/client_capabilities.ts rename to lib/packets/builders/client_capabilities.ts diff --git a/src/packets/builders/query.ts b/lib/packets/builders/query.ts similarity index 100% rename from src/packets/builders/query.ts rename to lib/packets/builders/query.ts diff --git a/src/packets/builders/tls.ts b/lib/packets/builders/tls.ts similarity index 100% rename from src/packets/builders/tls.ts rename to lib/packets/builders/tls.ts diff --git a/src/packets/packet.ts b/lib/packets/packet.ts similarity index 97% rename from src/packets/packet.ts rename to lib/packets/packet.ts index 6a0b3bb..b6c8708 100644 --- a/src/packets/packet.ts +++ b/lib/packets/packet.ts @@ -2,7 +2,7 @@ import { byteFormat } from "../util.ts"; import { BufferReader, BufferWriter } from "../buffer.ts"; import { WriteError } from "../constant/errors.ts"; import { debug, log } from "../logger.ts"; -import { PacketType } from "../../src/constant/packet.ts"; +import { PacketType } from "../constant/packet.ts"; /** @ignore */ interface PacketHeader { diff --git a/src/packets/parsers/authswitch.ts b/lib/packets/parsers/authswitch.ts similarity index 100% rename from src/packets/parsers/authswitch.ts rename to lib/packets/parsers/authswitch.ts diff --git a/src/packets/parsers/err.ts b/lib/packets/parsers/err.ts similarity index 100% rename from src/packets/parsers/err.ts rename to lib/packets/parsers/err.ts diff --git a/src/packets/parsers/handshake.ts b/lib/packets/parsers/handshake.ts similarity index 100% rename from src/packets/parsers/handshake.ts rename to lib/packets/parsers/handshake.ts diff --git a/src/packets/parsers/result.ts b/lib/packets/parsers/result.ts similarity index 100% rename from src/packets/parsers/result.ts rename to lib/packets/parsers/result.ts diff --git a/src/pool.ts b/lib/pool.ts similarity index 100% rename from src/pool.ts rename to lib/pool.ts diff --git a/src/util.ts b/lib/util.ts similarity index 100% rename from src/util.ts rename to lib/util.ts diff --git a/mod.ts b/mod.ts index 1f7ee96..f42e155 100644 --- a/mod.ts +++ b/mod.ts @@ -1,12 +1,12 @@ -export type { ClientConfig } from "./src/client.ts"; -export { Client } from "./src/client.ts"; -export type { TLSConfig } from "./src/client.ts"; -export { TLSMode } from "./src/client.ts"; +export type { ClientConfig } from "./lib/client.ts"; +export { Client } from "./lib/client.ts"; +export type { TLSConfig } from "./lib/client.ts"; +export { TLSMode } from "./lib/client.ts"; -export type { ExecuteResult } from "./src/connection.ts"; -export { Connection } from "./src/connection.ts"; +export type { ExecuteResult } from "./lib/connection.ts"; +export { Connection } from "./lib/connection.ts"; -export type { LoggerConfig } from "./src/logger.ts"; -export { configLogger } from "./src/logger.ts"; +export type { LoggerConfig } from "./lib/logger.ts"; +export { configLogger } from "./lib/logger.ts"; export * as log from "@std/log"; diff --git a/test.ts b/test.ts index d151833..422cd41 100644 --- a/test.ts +++ b/test.ts @@ -3,7 +3,7 @@ import { lessThan, parse } from "@std/semver"; import { ConnectionError, ResponseTimeoutError, -} from "./src/constant/errors.ts"; +} from "./lib/constant/errors.ts"; import { createTestDB, delay, @@ -12,7 +12,7 @@ import { testWithClient, } from "./test.util.ts"; import * as stdlog from "@std/log"; -import { log } from "./src/logger.ts"; +import { log } from "./lib/logger.ts"; import { configLogger } from "./mod.ts"; testWithClient(async function testCreateDb(client) { From 5d7bf6a135a0c18292fc9a39b670c0fb4ef8a82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 12 Apr 2024 12:43:06 +0200 Subject: [PATCH 09/38] added fmt and added module name and version to util --- deno.json | 5 ++--- lib/util.ts | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/deno.json b/deno.json index c6ae6b7..21dedbe 100644 --- a/deno.json +++ b/deno.json @@ -20,13 +20,12 @@ "@std/async": "jsr:@std/async@^0.221.0", "@std/encoding": "jsr:@std/encoding@^0.221.0", "@std/flags": "jsr:@std/flags@^0.221.0", + "@std/fmt": "jsr:@std/fmt@^0.221.0", "@std/crypto": "jsr:@std/crypto@^0.221.0", "@std/log": "jsr:@std/log@^0.221.0", "@std/semver": "jsr:@std/semver@^0.220.1", "@std/testing": "jsr:@std/testing@^0.221.0", - "bytes_formater/": "./vendor/deno.land/x/bytes_formater@v1.4.0/", - "https://deno.land/": "./vendor/deno.land/", - "sql_builder/": "./vendor/deno.land/x/sql_builder@v1.9.1/" + "@halvardm/sqlx": "../deno-sqlx/mod.ts" }, "lint": { "exclude": ["vendor"] diff --git a/lib/util.ts b/lib/util.ts index 0631e9d..5025a1a 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -1,4 +1,8 @@ import { green } from "@std/fmt/colors"; +import meta from "../deno.json" with { type: "json" }; + +export const MODULE_NAME = meta.name; +export const VERSION = meta.version; export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { return a.map((byte, index) => { From e8c497adb56bbcef077b5eaa2de055ff9b9d2a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 12 Apr 2024 12:47:33 +0200 Subject: [PATCH 10/38] updated logger --- lib/client.ts | 4 +-- lib/connection.ts | 12 ++++----- lib/logger.ts | 61 ++++++++----------------------------------- lib/packets/packet.ts | 16 +++++------- lib/pool.ts | 6 ++--- test.ts | 6 ++--- 6 files changed, 31 insertions(+), 74 deletions(-) diff --git a/lib/client.ts b/lib/client.ts index ddd36ff..3ec10ee 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -4,7 +4,7 @@ import { type ExecuteResult, } from "./connection.ts"; import { ConnectionPool, PoolConnection } from "./pool.ts"; -import { log } from "./logger.ts"; +import { logger } from "./logger.ts"; /** * Client Config @@ -149,7 +149,7 @@ export class Client { return result; } catch (error) { if (connection.state == ConnectionState.CONNECTED) { - log.info(`ROLLBACK: ${error.message}`); + logger().info(`ROLLBACK: ${error.message}`); await connection.execute("ROLLBACK"); } throw error; diff --git a/lib/connection.ts b/lib/connection.ts index d2cf765..98d82f9 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -5,7 +5,6 @@ import { ReadError, ResponseTimeoutError, } from "./constant/errors.ts"; -import { log } from "./logger.ts"; import { buildAuth } from "./packets/builders/auth.ts"; import { buildQuery } from "./packets/builders/query.ts"; import { ReceivePacket, SendPacket } from "./packets/packet.ts"; @@ -26,6 +25,7 @@ import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; import ServerCapabilities from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; +import { logger } from "./logger.ts"; /** * Connection state @@ -76,7 +76,7 @@ export class Connection { } const { hostname, port = 3306, socketPath, username = "", password } = this.config; - log.info(`connecting ${this.remoteAddr}`); + logger().info(`connecting ${this.remoteAddr}`); this.conn = !socketPath ? await Deno.connect({ transport: "tcp", @@ -203,11 +203,11 @@ export class Connection { const header = receive.body.readUint8(); if (header === 0xff) { const error = parseError(receive.body, this); - log.error(`connect error(${error.code}): ${error.message}`); + logger().error(`connect error(${error.code}): ${error.message}`); this.close(); throw new Error(error.message); } else { - log.info(`connected to ${this.remoteAddr}`); + logger().info(`connected to ${this.remoteAddr}`); this.state = ConnectionState.CONNECTED; } @@ -266,7 +266,7 @@ export class Connection { } private _timeoutCallback = () => { - log.info("connection read timed out"); + logger().info("connection read timed out"); this._timedOut = true; this.close(); }; @@ -274,7 +274,7 @@ export class Connection { /** Close database connection */ close(): void { if (this.state != ConnectionState.CLOSED) { - log.info("close connection"); + logger().info("close connection"); this.conn?.close(); this.state = ConnectionState.CLOSED; } diff --git a/lib/logger.ts b/lib/logger.ts index 543dc82..82c2c47 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -1,51 +1,12 @@ -import * as log from "@std/log"; - -let logger = log.getLogger(); - -export { logger as log }; - -let isDebug = false; - -/** @ignore */ -export function debug(func: Function) { - if (isDebug) { - func(); - } -} - -export interface LoggerConfig { - /** Enable logging (default: true) */ - enable?: boolean; - /** The minimal level to print (default: "INFO") */ - level?: log.LevelName; - /** A deno_std/log.Logger instance to be used as logger. When used, `level` is ignored. */ - logger?: log.Logger; -} - -export async function configLogger(config: LoggerConfig) { - let { enable = true, level = "INFO" } = config; - if (config.logger) level = config.logger.levelName; - isDebug = level == "DEBUG"; - - if (!enable) { - logger = new log.Logger("fakeLogger", "NOTSET", {}); - logger.level = 0; - } else { - if (!config.logger) { - await log.setup({ - handlers: { - console: new log.ConsoleHandler(level), - }, - loggers: { - default: { - level: "DEBUG", - handlers: ["console"], - }, - }, - }); - logger = log.getLogger(); - } else { - logger = config.logger; - } - } +import { getLogger } from "@std/log"; +import { MODULE_NAME } from "./util.ts"; + +/** + * Used for internal module logging, + * do not import this directly outside of this module. + * + * @see {@link https://deno.land/std/log/mod.ts} + */ +export function logger() { + return getLogger(MODULE_NAME); } diff --git a/lib/packets/packet.ts b/lib/packets/packet.ts index b6c8708..07aa71c 100644 --- a/lib/packets/packet.ts +++ b/lib/packets/packet.ts @@ -1,8 +1,8 @@ import { byteFormat } from "../util.ts"; import { BufferReader, BufferWriter } from "../buffer.ts"; import { WriteError } from "../constant/errors.ts"; -import { debug, log } from "../logger.ts"; import { PacketType } from "../constant/packet.ts"; +import { logger } from "../logger.ts"; /** @ignore */ interface PacketHeader { @@ -24,9 +24,7 @@ export class SendPacket { data.writeUints(3, this.header.size); data.write(this.header.no); data.writeBuffer(body); - debug(() => { - log.debug(`send: ${data.length}B \n${byteFormat(data.buffer)}\n`); - }); + logger().debug(`send: ${data.length}B \n${byteFormat(data.buffer)}\n`); try { let wrote = 0; do { @@ -76,15 +74,13 @@ export class ReceivePacket { break; } - debug(() => { + logger().debug(() => { const data = new Uint8Array(readCount); data.set(header.buffer); data.set(this.body.buffer, 4); - log.debug( - `receive: ${readCount}B, size = ${this.header.size}, no = ${this.header.no} \n${ - byteFormat(data) - }\n`, - ); + return `receive: ${readCount}B, size = ${this.header.size}, no = ${this.header.no} \n${ + byteFormat(data) + }\n`; }); return this; diff --git a/lib/pool.ts b/lib/pool.ts index 545b466..307b42f 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -1,6 +1,6 @@ import { DeferredStack } from "./deferred.ts"; import { Connection } from "./connection.ts"; -import { log } from "./logger.ts"; +import { logger } from "./logger.ts"; /** @ignore */ export class PoolConnection extends Connection { @@ -16,12 +16,12 @@ export class PoolConnection extends Connection { this._idle = true; if (this.config.idleTimeout) { this._idleTimer = setTimeout(() => { - log.info("connection idle timeout"); + logger().info("connection idle timeout"); this._pool!.remove(this); try { this.close(); } catch (error) { - log.warn(`error closing idle connection`, error); + logger().warn(`error closing idle connection`, error); } }, this.config.idleTimeout); try { diff --git a/test.ts b/test.ts index 422cd41..4291062 100644 --- a/test.ts +++ b/test.ts @@ -12,8 +12,8 @@ import { testWithClient, } from "./test.util.ts"; import * as stdlog from "@std/log"; -import { log } from "./lib/logger.ts"; import { configLogger } from "./mod.ts"; +import { logger } from "./lib/logger.ts"; testWithClient(async function testCreateDb(client) { await client.query(`CREATE DATABASE IF NOT EXISTS enok`); @@ -380,11 +380,11 @@ Deno.test("configLogger()", async () => { }, }); await configLogger({ logger: stdlog.getLogger("mysql") }); - log.info("Test log"); + logger().info("Test log"); assertEquals(logCount, 1); await configLogger({ enable: false }); - log.info("Test log"); + logger().info("Test log"); assertEquals(logCount, 1); }); From df0d4b117a3697823e4ed6ab1dd512bbc7778d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 12 Apr 2024 12:56:50 +0200 Subject: [PATCH 11/38] updated packet classes --- lib/auth_plugin/caching_sha2_password.ts | 10 +- lib/connection.ts | 40 +++++-- lib/packets/packet.ts | 141 +++++++++++++++-------- lib/packets/parsers/handshake.ts | 4 +- 4 files changed, 132 insertions(+), 63 deletions(-) diff --git a/lib/auth_plugin/caching_sha2_password.ts b/lib/auth_plugin/caching_sha2_password.ts index 8d45ed2..2111d1e 100644 --- a/lib/auth_plugin/caching_sha2_password.ts +++ b/lib/auth_plugin/caching_sha2_password.ts @@ -1,11 +1,11 @@ import { xor } from "../util.ts"; -import type { ReceivePacket } from "../packets/packet.ts"; +import type { PacketReader } from "../packets/packet.ts"; import { encryptWithPublicKey } from "./crypt.ts"; interface handler { done: boolean; quickRead?: boolean; - next?: (packet: ReceivePacket) => any; + next?: (packet: PacketReader) => any; data?: Uint8Array; } @@ -20,7 +20,7 @@ async function start( return { done: false, next: authMoreResponse }; } -async function authMoreResponse(packet: ReceivePacket): Promise { +async function authMoreResponse(packet: PacketReader): Promise { const enum AuthStatusFlags { FullAuth = 0x04, FastPath = 0x03, @@ -41,7 +41,7 @@ async function authMoreResponse(packet: ReceivePacket): Promise { return { done, next, quickRead, data: authMoreData }; } -async function encryptWithKey(packet: ReceivePacket): Promise { +async function encryptWithKey(packet: PacketReader): Promise { const publicKey = parsePublicKey(packet); const len = password.length; const passwordBuffer: Uint8Array = new Uint8Array(len + 1); @@ -58,7 +58,7 @@ async function encryptWithKey(packet: ReceivePacket): Promise { }; } -function parsePublicKey(packet: ReceivePacket): string { +function parsePublicKey(packet: PacketReader): string { return packet.body.skip(1).readNullTerminatedString(); } diff --git a/lib/connection.ts b/lib/connection.ts index 98d82f9..73be757 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -7,7 +7,7 @@ import { } from "./constant/errors.ts"; import { buildAuth } from "./packets/builders/auth.ts"; import { buildQuery } from "./packets/builders/query.ts"; -import { ReceivePacket, SendPacket } from "./packets/packet.ts"; +import { PacketReader, PacketWriter } from "./packets/packet.ts"; import { parseError } from "./packets/parsers/err.ts"; import { AuthResult, @@ -54,9 +54,27 @@ export class Connection { capabilities: number = 0; serverVersion: string = ""; - private conn?: Deno.Conn = undefined; + protected _conn: Deno.Conn | null = null; private _timedOut = false; + get conn(): Deno.Conn { + if (!this._conn) { + throw new ConnectionError("Not connected"); + } + if (this.state != ConnectionState.CONNECTED) { + if (this.state == ConnectionState.CLOSED) { + throw new ConnectionError("Connection is closed"); + } else { + throw new ConnectionError("Must be connected first"); + } + } + return this._conn; + } + + set conn(conn: Deno.Conn | null) { + this._conn = conn; + } + get remoteAddr(): string { return this.config.socketPath ? `unix:${this.config.socketPath}` @@ -112,8 +130,10 @@ export class Connection { const tlsData = buildSSLRequest(handshakePacket, { db: this.config.db, }); - await new SendPacket(tlsData, ++handshakeSequenceNumber).send( + await PacketWriter.write( this.conn, + tlsData, + ++handshakeSequenceNumber, ); this.conn = await Deno.startTls(this.conn, { hostname, @@ -130,7 +150,7 @@ export class Connection { ssl: isSSL, }); - await new SendPacket(data, ++handshakeSequenceNumber).send(this.conn); + await PacketWriter.write(this.conn, data, ++handshakeSequenceNumber); this.state = ConnectionState.CONNECTING; this.serverVersion = handshakePacket.serverVersion; @@ -170,7 +190,7 @@ export class Connection { authData = Uint8Array.from([]); } - await new SendPacket(authData, receive.header.no + 1).send(this.conn); + await PacketWriter.write(this.conn, authData, receive.header.no + 1); receive = await this.nextPacket(); const authSwitch2 = parseAuthSwitch(receive.body); @@ -188,7 +208,7 @@ export class Connection { while (!result.done) { if (result.data) { const sequenceNumber = receive.header.no + 1; - await new SendPacket(result.data, sequenceNumber).send(this.conn); + await PacketWriter.write(this.conn, result.data, sequenceNumber); receive = await this.nextPacket(); } if (result.quickRead) { @@ -226,7 +246,7 @@ export class Connection { await this._connect(); } - private async nextPacket(): Promise { + private async nextPacket(): Promise { if (!this.conn) { throw new ConnectionError("Not connected"); } @@ -237,9 +257,9 @@ export class Connection { this.config.timeout, ) : null; - let packet: ReceivePacket | null; + let packet: PacketReader | null; try { - packet = await new ReceivePacket().parse(this.conn!); + packet = await PacketReader.read(this.conn); } catch (error) { if (this._timedOut) { // Connection has been closed by timeoutCallback. @@ -314,7 +334,7 @@ export class Connection { } const data = buildQuery(sql, params); try { - await new SendPacket(data, 0).send(this.conn!); + await PacketWriter.write(this.conn, data, 0); let receive = await this.nextPacket(); if (receive.type === PacketType.OK_Packet) { receive.body.skip(1); diff --git a/lib/packets/packet.ts b/lib/packets/packet.ts index 07aa71c..5f58d8d 100644 --- a/lib/packets/packet.ts +++ b/lib/packets/packet.ts @@ -1,8 +1,8 @@ import { byteFormat } from "../util.ts"; import { BufferReader, BufferWriter } from "../buffer.ts"; import { WriteError } from "../constant/errors.ts"; -import { PacketType } from "../constant/packet.ts"; import { logger } from "../logger.ts"; +import { PacketType } from "../constant/packet.ts"; /** @ignore */ interface PacketHeader { @@ -10,16 +10,26 @@ interface PacketHeader { no: number; } -/** @ignore */ -export class SendPacket { +/** + * Helper for sending a packet through the connection + */ +export class PacketWriter { header: PacketHeader; + body: Uint8Array; - constructor(readonly body: Uint8Array, no: number) { + constructor(body: Uint8Array, no: number) { + this.body = body; this.header = { size: body.length, no }; } - async send(conn: Deno.Conn) { - const body = this.body as Uint8Array; + /** + * Send the packet through the connection + * + * @param conn The connection + */ + async write(conn: Deno.Conn) { + const body = this.body; + const data = new BufferWriter(new Uint8Array(4 + body.length)); data.writeUints(3, this.header.size); data.write(this.header.no); @@ -34,69 +44,108 @@ export class SendPacket { throw new WriteError(error.message); } } + + /** + * Send a packet through the connection + * + * @param conn The connection + * @param body The packet body + * @param no The packet number + * @returns SendPacket instance + */ + static async write( + conn: Deno.Conn, + body: Uint8Array, + no: number, + ): Promise { + const packet = new PacketWriter(body, no); + await packet.write(conn); + return packet; + } } -/** @ignore */ -export class ReceivePacket { - header!: PacketHeader; - body!: BufferReader; - type!: PacketType; +/** + * Helper for receiving a packet through the connection + */ +export class PacketReader { + header: PacketHeader; + body: BufferReader; + type: PacketType; + + constructor(header: PacketHeader, body: BufferReader, type: PacketType) { + this.header = header; + this.body = body; + this.type = type; + } + + /** + * Read a subarray from the connection + * + * @param conn The connection + * @param buffer The buffer to read into + * @returns The number of bytes read + */ + static async _readSubarray( + conn: Deno.Conn, + buffer: Uint8Array, + ): Promise { + const size = buffer.length; + let haveRead = 0; + while (haveRead < size) { + const nread = await conn.read(buffer.subarray(haveRead)); + if (nread === null) return null; + haveRead += nread; + } + return haveRead; + } - async parse(reader: Deno.Reader): Promise { - const header = new BufferReader(new Uint8Array(4)); + /** + * Read a subarray from the connection + * + * @param conn + * @returns The PacketReader instance or null if nothing could be read + */ + static async read(conn: Deno.Conn): Promise { + const headerReader = new BufferReader(new Uint8Array(4)); let readCount = 0; - let nread = await this.read(reader, header.buffer); + let nread = await this._readSubarray(conn, headerReader.buffer); if (nread === null) return null; readCount = nread; - const bodySize = header.readUints(3); - this.header = { + const bodySize = headerReader.readUints(3); + const header = { size: bodySize, - no: header.readUint8(), + no: headerReader.readUint8(), }; - this.body = new BufferReader(new Uint8Array(bodySize)); - nread = await this.read(reader, this.body.buffer); + const bodyReader = new BufferReader(new Uint8Array(bodySize)); + nread = await this._readSubarray(conn, bodyReader.buffer); if (nread === null) return null; readCount += nread; - const { OK_Packet, ERR_Packet, EOF_Packet, Result } = PacketType; - switch (this.body.buffer[0]) { - case OK_Packet: - this.type = OK_Packet; + let type: PacketType; + switch (bodyReader.buffer[0]) { + case PacketType.OK_Packet: + type = PacketType.OK_Packet; break; - case 0xff: - this.type = ERR_Packet; + case PacketType.ERR_Packet: + type = PacketType.ERR_Packet; break; - case 0xfe: - this.type = EOF_Packet; + case PacketType.EOF_Packet: + type = PacketType.EOF_Packet; break; default: - this.type = Result; + type = PacketType.Result; break; } logger().debug(() => { const data = new Uint8Array(readCount); - data.set(header.buffer); - data.set(this.body.buffer, 4); - return `receive: ${readCount}B, size = ${this.header.size}, no = ${this.header.no} \n${ + data.set(headerReader.buffer); + data.set(bodyReader.buffer, 4); + return `receive: ${readCount}B, size = ${header.size}, no = ${header.no} \n${ byteFormat(data) }\n`; }); - return this; - } - - private async read( - reader: Deno.Reader, - buffer: Uint8Array, - ): Promise { - const size = buffer.length; - let haveRead = 0; - while (haveRead < size) { - const nread = await reader.read(buffer.subarray(haveRead)); - if (nread === null) return null; - haveRead += nread; - } - return haveRead; + return new PacketReader(header, bodyReader, type); } } diff --git a/lib/packets/parsers/handshake.ts b/lib/packets/parsers/handshake.ts index e999908..7ae38b5 100644 --- a/lib/packets/parsers/handshake.ts +++ b/lib/packets/parsers/handshake.ts @@ -1,7 +1,7 @@ import { type BufferReader, BufferWriter } from "../../buffer.ts"; import ServerCapabilities from "../../constant/capabilities.ts"; import { PacketType } from "../../constant/packet.ts"; -import type { ReceivePacket } from "../packet.ts"; +import type { PacketReader } from "../packet.ts"; /** @ignore */ export interface HandshakeBody { @@ -73,7 +73,7 @@ export enum AuthResult { MethodMismatch, AuthMoreRequired, } -export function parseAuth(packet: ReceivePacket): AuthResult { +export function parseAuth(packet: PacketReader): AuthResult { switch (packet.type) { case PacketType.EOF_Packet: return AuthResult.MethodMismatch; From 3d355558f2c057849b98b2ca81d7d98efdc28e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Fri, 12 Apr 2024 15:58:32 +0200 Subject: [PATCH 12/38] Added new connection --- deno.json | 7 +- lib/connection.test.ts | 199 ++++++++++++ lib/connection.ts | 8 +- lib/connection2.ts | 587 ++++++++++++++++++++++++++++++++++ lib/packets/builders/auth.ts | 8 +- lib/packets/parsers/result.ts | 21 +- mod.ts | 3 - test.util.ts | 3 + 8 files changed, 820 insertions(+), 16 deletions(-) create mode 100644 lib/connection.test.ts create mode 100644 lib/connection2.ts diff --git a/deno.json b/deno.json index 21dedbe..1fbe418 100644 --- a/deno.json +++ b/deno.json @@ -16,16 +16,19 @@ "typedoc": "deno run -A npm:typedoc --theme minimal --ignoreCompilerErrors --excludePrivate --excludeExternals --entryPoint client.ts --mode file ./src --out ./docs" }, "imports": { + "@halvardm/sqlx": "../deno-sqlx/mod.ts", "@std/assert": "jsr:@std/assert@^0.221.0", "@std/async": "jsr:@std/async@^0.221.0", + "@std/crypto": "jsr:@std/crypto@^0.221.0", "@std/encoding": "jsr:@std/encoding@^0.221.0", "@std/flags": "jsr:@std/flags@^0.221.0", "@std/fmt": "jsr:@std/fmt@^0.221.0", - "@std/crypto": "jsr:@std/crypto@^0.221.0", + "@std/fs": "jsr:@std/fs@^0.222.1", "@std/log": "jsr:@std/log@^0.221.0", + "@std/path": "jsr:@std/path@^0.222.1", "@std/semver": "jsr:@std/semver@^0.220.1", "@std/testing": "jsr:@std/testing@^0.221.0", - "@halvardm/sqlx": "../deno-sqlx/mod.ts" + "@std/text": "jsr:@std/text@^0.222.1" }, "lint": { "exclude": ["vendor"] diff --git a/lib/connection.test.ts b/lib/connection.test.ts new file mode 100644 index 0000000..a41f949 --- /dev/null +++ b/lib/connection.test.ts @@ -0,0 +1,199 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; +import { emptyDir } from "@std/fs"; +import { join } from "@std/path"; +import { MysqlConnection } from "./connection2.ts"; +import { DIR_TMP_TEST } from "../test.util.ts"; +import { buildQuery } from "./packets/builders/query.ts"; + +Deno.test("Connection", async (t) => { + await emptyDir(DIR_TMP_TEST); + + const PATH_PEM_CA = join(DIR_TMP_TEST, "ca.pem"); + const PATH_PEM_CA2 = join(DIR_TMP_TEST, "ca2.pem"); + const PATH_PEM_CERT = join(DIR_TMP_TEST, "cert.pem"); + const PATH_PEM_KEY = join(DIR_TMP_TEST, "key.pem"); + + await Deno.writeTextFile(PATH_PEM_CA, "ca"); + await Deno.writeTextFile(PATH_PEM_CA2, "ca2"); + await Deno.writeTextFile(PATH_PEM_CERT, "cert"); + await Deno.writeTextFile(PATH_PEM_KEY, "key"); + + await t.step("can construct", async (t) => { + const connection = new MysqlConnection("mysql://127.0.0.1:3306"); + + assertInstanceOf(connection, MysqlConnection); + assertEquals(connection.connectionUrl, "mysql://127.0.0.1:3306"); + + await t.step("can parse connection config simple", () => { + const url = new URL("mysql://user:pass@127.0.0.1:3306/db"); + + const c = new MysqlConnection(url.toString()); + + assertEquals(c.config, { + protocol: "mysql", + username: "user", + password: "pass", + hostname: "127.0.0.1", + port: 3306, + schema: "db", + socket: undefined, + tls: undefined, + parameters: {}, + }); + }); + await t.step("can parse connection config full", () => { + const url = new URL("mysql://user:pass@127.0.0.1:3306/db"); + url.searchParams.set("socket", "/tmp/mysql.sock"); + url.searchParams.set("ssl-mode", "VERIFY_IDENTITY"); + url.searchParams.set("ssl-ca", PATH_PEM_CA); + url.searchParams.set("ssl-capath", DIR_TMP_TEST); + url.searchParams.set("ssl-cert", PATH_PEM_CERT); + url.searchParams.set("ssl-cipher", "cipher"); + url.searchParams.set("ssl-crl", "crl.pem"); + url.searchParams.set("ssl-crlpath", "crlpath.pem"); + url.searchParams.set("ssl-key", PATH_PEM_KEY); + url.searchParams.set("tls-version", "TLSv1.2,TLSv1.3"); + url.searchParams.set("tls-versions", "[TLSv1.2,TLSv1.3]"); + url.searchParams.set("tls-ciphersuites", "ciphersuites"); + url.searchParams.set("auth-method", "AUTO"); + url.searchParams.set("get-server-public-key", "true"); + url.searchParams.set("server-public-key-path", "key.pem"); + url.searchParams.set("ssh", "usr@host:port"); + url.searchParams.set("uri", "mysql://user@127.0.0.1:3306"); + url.searchParams.set("ssh-password", "pass"); + url.searchParams.set("ssh-config-file", "config"); + url.searchParams.set("ssh-config-file", "config"); + url.searchParams.set("ssh-identity-file", "identity"); + url.searchParams.set("ssh-identity-pass", "identitypass"); + url.searchParams.set("connect-timeout", "10"); + url.searchParams.set("compression", "preferred"); + url.searchParams.set("compression-algorithms", "algo"); + url.searchParams.set("compression-level", "level"); + url.searchParams.set("connection-attributes", "true"); + + const c = new MysqlConnection(url.toString()); + + assertEquals(c.config, { + protocol: "mysql", + username: "user", + password: "pass", + hostname: "127.0.0.1", + port: 3306, + socket: "/tmp/mysql.sock", + schema: "db", + tls: { + mode: "VERIFY_IDENTITY", + caCerts: [ + "ca", + "key", + "cert", + "ca2", + ], + cert: "cert", + hostname: "127.0.0.1", + key: "key", + port: 3306, + }, + parameters: { + socket: "/tmp/mysql.sock", + sslMode: "VERIFY_IDENTITY", + sslCa: [PATH_PEM_CA], + sslCapath: [DIR_TMP_TEST], + sslCert: PATH_PEM_CERT, + sslCipher: "cipher", + sslCrl: "crl.pem", + sslCrlpath: "crlpath.pem", + sslKey: PATH_PEM_KEY, + tlsVersion: "TLSv1.2,TLSv1.3", + tlsVersions: "[TLSv1.2,TLSv1.3]", + tlsCiphersuites: "ciphersuites", + authMethod: "AUTO", + getServerPublicKey: true, + serverPublicKeyPath: "key.pem", + ssh: "usr@host:port", + uri: "mysql://user@127.0.0.1:3306", + sshPassword: "pass", + sshConfigFile: "config", + sshIdentityFile: "identity", + sshIdentityPass: "identitypass", + connectTimeout: 10, + compression: "preferred", + compressionAlgorithms: "algo", + compressionLevel: "level", + connectionAttributes: "true", + }, + }); + }); + }); + + const connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + assertEquals(connection.connected, false); + + await t.step("can connect and close", async () => { + await connection.connect(); + assertEquals(connection.connected, true); + await connection.close(); + assertEquals(connection.connected, false); + }); + + await t.step("can reconnect", async () => { + await connection.connect(); + assertEquals(connection.connected, true); + await connection.close(); + assertEquals(connection.connected, false); + }); + + await t.step("can connect with using and dispose", async () => { + await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + assertEquals(connection.connected, false); + await connection.connect(); + assertEquals(connection.connected, true); + }); + + await t.step("can execute", async (t) => { + await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + await connection.connect(); + const data = buildQuery("SELECT 1+1 AS result"); + const result = await connection.execute(data); + assertEquals(result, { affectedRows: 0, lastInsertId: null }); + }); + + await t.step("can execute twice", async (t) => { + await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + await connection.connect(); + const data = buildQuery("SELECT 1+1 AS result;"); + const result1 = await connection.execute(data); + assertEquals(result1, { affectedRows: 0, lastInsertId: null }); + const result2 = await connection.execute(data); + assertEquals(result2, { affectedRows: 0, lastInsertId: null }); + }); + + await t.step("can sendData", async (t) => { + await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + await connection.connect(); + const data = buildQuery("SELECT 1+1 AS result;"); + for await (const result1 of connection.sendData(data)) { + assertEquals(result1, { + row: [2], + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 129, + fieldLen: 3, + fieldType: 8, + name: "result", + originName: "", + originTable: "", + schema: "", + table: "", + }, + ], + }); + } + }); + + await emptyDir(DIR_TMP_TEST); +}); diff --git a/lib/connection.ts b/lib/connection.ts index 73be757..085aa09 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -17,7 +17,7 @@ import { import { type FieldInfo, parseField, - parseRow, + parseRowObject, } from "./packets/parsers/result.ts"; import { PacketType } from "./constant/packet.ts"; import authPlugin from "./auth_plugin/index.ts"; @@ -143,7 +143,7 @@ export class Connection { isSSL = true; } - const data = buildAuth(handshakePacket, { + const data = await buildAuth(handshakePacket, { username, password, db: this.config.db, @@ -370,7 +370,7 @@ export class Connection { if (receive.type === PacketType.EOF_Packet) { break; } else { - const row = parseRow(receive.body, fields); + const row = parseRowObject(receive.body, fields); rows.push(row); } } @@ -395,7 +395,7 @@ export class Connection { return { done: true }; } - const value = parseRow(receive.body, fields); + const value = parseRowObject(receive.body, fields); return { done: false, diff --git a/lib/connection2.ts b/lib/connection2.ts new file mode 100644 index 0000000..7f7a06e --- /dev/null +++ b/lib/connection2.ts @@ -0,0 +1,587 @@ +import { + ConnectionError, + ProtocolError, + ReadError, + ResponseTimeoutError, +} from "./constant/errors.ts"; +import { buildAuth } from "./packets/builders/auth.ts"; +import { PacketReader, PacketWriter } from "./packets/packet.ts"; +import { parseError } from "./packets/parsers/err.ts"; +import { + AuthResult, + parseAuth, + parseHandshake, +} from "./packets/parsers/handshake.ts"; +import { + type FieldInfo, + parseField, + parseRow, + parseRowObject, +} from "./packets/parsers/result.ts"; +import { PacketType } from "./constant/packet.ts"; +import authPlugin from "./auth_plugin/index.ts"; +import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; +import auth from "./auth.ts"; +import ServerCapabilities from "./constant/capabilities.ts"; +import { buildSSLRequest } from "./packets/builders/tls.ts"; +import { logger } from "./logger.ts"; +import type { + ArrayRow, + SqlxConnectable, + SqlxConnectionOptions, + SqlxParameterType, +} from "@halvardm/sqlx"; +import { buildQuery } from "./packets/builders/query.ts"; +import { VERSION } from "./util.ts"; +import { parse, resolve } from "@std/path"; +import { toCamelCase } from "@std/text"; +export type MysqlParameterType = SqlxParameterType; + +/** + * Connection state + */ +export enum ConnectionState { + CONNECTING, + CONNECTED, + CLOSING, + CLOSED, +} + +export type ConnectionSendDataResult = { + affectedRows: number; + lastInsertId: number | null; +} | undefined; + +export type ConnectionSendDataNext = { + row: ArrayRow; + fields: FieldInfo[]; +}; + +export interface ConnectionOptions extends SqlxConnectionOptions { +} + +/** + * Tls mode for mysql connection + * + * @see {@link https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode} + */ +export const TlsMode = { + Preferred: "PREFERRED", + Disabled: "DISABLED", + Required: "REQUIRED", + VerifyCa: "VERIFY_CA", + VerifyIdentity: "VERIFY_IDENTITY", +} as const; +export type TlsMode = typeof TlsMode[keyof typeof TlsMode]; + +export interface TlsOptions extends Deno.ConnectTlsOptions { + mode: TlsMode; +} + +/** + * Aditional connection parameters + * + * @see {@link https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html#connecting-using-uri} + */ +export interface ConnectionParameters { + socket?: string; + sslMode?: TlsMode; + sslCa?: string[]; + sslCapath?: string[]; + sslCert?: string; + sslCipher?: string; + sslCrl?: string; + sslCrlpath?: string; + sslKey?: string; + tlsVersion?: string; + tlsVersions?: string; + tlsCiphersuites?: string; + authMethod?: string; + getServerPublicKey?: boolean; + serverPublicKeyPath?: string; + ssh?: string; + uri?: string; + sshPassword?: string; + sshConfigFile?: string; + sshIdentityFile?: string; + sshIdentityPass?: string; + connectTimeout?: number; + compression?: string; + compressionAlgorithms?: string; + compressionLevel?: string; + connectionAttributes?: string; +} + +export interface ConnectionConfig { + protocol: string; + username: string; + password?: string; + hostname: string; + port: number; + socket?: string; + schema?: string; + /** + * Tls options + */ + tls?: Partial; + /** + * Aditional connection parameters + */ + parameters: ConnectionParameters; +} + +/** Connection for mysql */ +export class MysqlConnection implements SqlxConnectable { + state: ConnectionState = ConnectionState.CONNECTING; + capabilities: number = 0; + serverVersion: string = ""; + + protected _conn: Deno.Conn | null = null; + private _timedOut = false; + + readonly connectionUrl: string; + readonly connectionOptions: ConnectionOptions; + readonly config: ConnectionConfig; + readonly sqlxVersion: string = VERSION; + + get conn(): Deno.Conn { + if (!this._conn) { + throw new ConnectionError("Not connected"); + } + if (this.state != ConnectionState.CONNECTED) { + if (this.state == ConnectionState.CLOSED) { + throw new ConnectionError("Connection is closed"); + } else { + throw new ConnectionError("Must be connected first"); + } + } + return this._conn; + } + + set conn(conn: Deno.Conn | null) { + this._conn = conn; + } + + constructor( + connectionUrl: string | URL, + connectionOptions: ConnectionOptions = {}, + ) { + this.connectionUrl = connectionUrl.toString().split("?")[0]; + this.connectionOptions = connectionOptions; + this.config = this.#parseConnectionConfig( + connectionUrl, + connectionOptions, + ); + } + get connected(): boolean { + return this.state === ConnectionState.CONNECTED; + } + + async connect(): Promise { + // TODO: implement connect timeout + if ( + this.config.tls?.mode && + this.config.tls?.mode !== TlsMode.Disabled && + this.config.tls?.mode !== TlsMode.VerifyIdentity + ) { + throw new Error("unsupported tls mode"); + } + + logger().info(`connecting ${this.connectionUrl}`); + + if (this.config.socket) { + this.conn = await Deno.connect({ + transport: "unix", + path: this.config.socket, + }); + } else { + this.conn = await Deno.connect({ + transport: "tcp", + hostname: this.config.hostname, + port: this.config.port, + }); + } + + try { + let receive = await this.#nextPacket(); + const handshakePacket = parseHandshake(receive.body); + + let handshakeSequenceNumber = receive.header.no; + + // Deno.startTls() only supports VERIFY_IDENTITY now. + let isSSL = false; + if ( + this.config.tls?.mode === TlsMode.VerifyIdentity + ) { + if ( + (handshakePacket.serverCapabilities & + ServerCapabilities.CLIENT_SSL) === 0 + ) { + throw new Error("Server does not support TLS"); + } + if ( + (handshakePacket.serverCapabilities & + ServerCapabilities.CLIENT_SSL) !== 0 + ) { + const tlsData = buildSSLRequest(handshakePacket, { + db: this.config.schema, + }); + await PacketWriter.write( + this.conn, + tlsData, + ++handshakeSequenceNumber, + ); + this.conn = await Deno.startTls(this.conn, { + hostname: this.config.hostname, + caCerts: this.config.tls?.caCerts, + }); + } + isSSL = true; + } + + const data = await buildAuth(handshakePacket, { + username: this.config.username, + password: this.config.password, + db: this.config.schema, + ssl: isSSL, + }); + + await PacketWriter.write(this._conn!, data, ++handshakeSequenceNumber); + + this.state = ConnectionState.CONNECTING; + this.serverVersion = handshakePacket.serverVersion; + this.capabilities = handshakePacket.serverCapabilities; + + receive = await this.#nextPacket(); + + const authResult = parseAuth(receive); + let handler; + + switch (authResult) { + case AuthResult.AuthMoreRequired: { + const adaptedPlugin = + (authPlugin as any)[handshakePacket.authPluginName]; + handler = adaptedPlugin; + break; + } + case AuthResult.MethodMismatch: { + const authSwitch = parseAuthSwitch(receive.body); + // If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is + // sent and we have to keep using the cipher sent in the init packet. + if ( + authSwitch.authPluginData === undefined || + authSwitch.authPluginData.length === 0 + ) { + authSwitch.authPluginData = handshakePacket.seed; + } + + let authData; + if (this.config.password) { + authData = await auth( + authSwitch.authPluginName, + this.config.password, + authSwitch.authPluginData, + ); + } else { + authData = Uint8Array.from([]); + } + + await PacketWriter.write( + this.conn, + authData, + receive.header.no + 1, + ); + + receive = await this.#nextPacket(); + const authSwitch2 = parseAuthSwitch(receive.body); + if (authSwitch2.authPluginName !== "") { + throw new Error( + "Do not allow to change the auth plugin more than once!", + ); + } + } + } + + let result; + if (handler) { + result = await handler.start( + handshakePacket.seed, + this.config.password!, + ); + while (!result.done) { + if (result.data) { + const sequenceNumber = receive.header.no + 1; + await PacketWriter.write( + this.conn, + result.data, + sequenceNumber, + ); + receive = await this.#nextPacket(); + } + if (result.quickRead) { + await this.#nextPacket(); + } + if (result.next) { + result = await result.next(receive); + } + } + } + + const header = receive.body.readUint8(); + if (header === 0xff) { + const error = parseError(receive.body, this as any); + logger().error(`connect error(${error.code}): ${error.message}`); + this.close(); + throw new Error(error.message); + } else { + logger().info(`connected to ${this.connectionUrl}`); + this.state = ConnectionState.CONNECTED; + } + } catch (error) { + // Call close() to avoid leaking socket. + this.close(); + throw error; + } + } + + async close(): Promise { + if (this.state != ConnectionState.CLOSED) { + logger().info("close connection"); + this._conn?.close(); + this.state = ConnectionState.CLOSED; + } + } + + /** + * Parses the connection url and options into a connection config + */ + #parseConnectionConfig( + connectionUrl: string | URL, + connectionOptions: ConnectionOptions, + ): ConnectionConfig { + function parseParameters(url: URL): ConnectionParameters { + const parameters: ConnectionParameters = {}; + for (const [key, value] of url.searchParams) { + const pKey = toCamelCase(key); + if (pKey === "sslCa") { + if (!parameters.sslCa) { + parameters.sslCa = []; + } + parameters.sslCa.push(value); + } else if (pKey === "sslCapath") { + if (!parameters.sslCapath) { + parameters.sslCapath = []; + } + parameters.sslCapath.push(value); + } else if (pKey === "getServerPublicKey") { + parameters.getServerPublicKey = value === "true"; + } else if (pKey === "connectTimeout") { + parameters.connectTimeout = parseInt(value); + } else { + parameters[pKey as keyof ConnectionParameters] = value as any; + } + } + return parameters; + } + + function parseTlsOptions(config: ConnectionConfig): TlsOptions | undefined { + const baseTlsOptions: TlsOptions = { + port: config.port, + hostname: config.hostname, + mode: TlsMode.Preferred, + }; + + if (connectionOptions.tls) { + return { + ...baseTlsOptions, + ...connectionOptions.tls, + }; + } + + if (config.parameters.sslMode) { + const tlsOptions: TlsOptions = { + ...baseTlsOptions, + mode: config.parameters.sslMode, + }; + + const caCertPaths = new Set(); + + if (config.parameters.sslCa?.length) { + for (const caCert of config.parameters.sslCa) { + caCertPaths.add(resolve(caCert)); + } + } + + if (config.parameters.sslCapath?.length) { + for (const caPath of config.parameters.sslCapath) { + for (const f of Deno.readDirSync(caPath)) { + if (f.isFile && f.name.endsWith(".pem")) { + caCertPaths.add(resolve(caPath, f.name)); + } + } + } + } + + if (caCertPaths.size) { + tlsOptions.caCerts = []; + for (const caCert of caCertPaths) { + const content = Deno.readTextFileSync(caCert); + tlsOptions.caCerts.push(content); + } + } + + if (config.parameters.sslKey) { + tlsOptions.key = Deno.readTextFileSync( + resolve(config.parameters.sslKey), + ); + } + + if (config.parameters.sslCert) { + tlsOptions.cert = Deno.readTextFileSync( + resolve(config.parameters.sslCert), + ); + } + + return tlsOptions; + } + return undefined; + } + + const url = new URL(connectionUrl); + const parameters = parseParameters(url); + const config: ConnectionConfig = { + protocol: url.protocol.slice(0, -1), + username: url.username, + password: url.password || undefined, + hostname: url.hostname, + port: parseInt(url.port || "3306"), + schema: url.pathname.slice(1), + parameters: parameters, + socket: parameters.socket, + }; + + config.tls = parseTlsOptions(config); + + return config; + } + + async #nextPacket(): Promise { + if (!this._conn) { + throw new ConnectionError("Not connected"); + } + + const timeoutTimer = this.config.parameters.connectTimeout + ? setTimeout( + this.#timeoutCallback, + this.config.parameters.connectTimeout, + ) + : null; + let packet: PacketReader | null; + try { + packet = await PacketReader.read(this._conn); + } catch (error) { + if (this._timedOut) { + // Connection has been closed by timeoutCallback. + throw new ResponseTimeoutError("Connection read timed out"); + } + timeoutTimer && clearTimeout(timeoutTimer); + this.close(); + throw error; + } + timeoutTimer && clearTimeout(timeoutTimer); + + if (!packet) { + // Connection is half-closed by the remote host. + // Call close() to avoid leaking socket. + this.close(); + throw new ReadError("Connection closed unexpectedly"); + } + if (packet.type === PacketType.ERR_Packet) { + packet.body.skip(1); + const error = parseError(packet.body, this as any); + throw new Error(error.message); + } + return packet!; + } + + #timeoutCallback = () => { + logger().info("connection read timed out"); + this._timedOut = true; + this.close(); + }; + + async *sendData( + data: Uint8Array, + ): AsyncGenerator { + try { + await PacketWriter.write(this.conn, data, 0); + let receive = await this.#nextPacket(); + if (receive.type === PacketType.OK_Packet) { + receive.body.skip(1); + return { + affectedRows: receive.body.readEncodedLen(), + lastInsertId: receive.body.readEncodedLen(), + }; + } else if (receive.type !== PacketType.Result) { + throw new ProtocolError(); + } + let fieldCount = receive.body.readEncodedLen(); + const fields: FieldInfo[] = []; + while (fieldCount--) { + const packet = await this.#nextPacket(); + if (packet) { + const field = parseField(packet.body); + fields.push(field); + } + } + + if (!(this.capabilities & ServerCapabilities.CLIENT_DEPRECATE_EOF)) { + // EOF(mysql < 5.7 or mariadb < 10.2) + receive = await this.#nextPacket(); + if (receive.type !== PacketType.EOF_Packet) { + throw new ProtocolError(); + } + } + + receive = await this.#nextPacket(); + + while (receive.type !== PacketType.EOF_Packet) { + const row = parseRow(receive.body, fields); + yield { row, fields }; + receive = await this.#nextPacket(); + } + } catch (error) { + this.close(); + throw error; + } + } + + async execute( + data: Uint8Array, + ): Promise { + try { + await PacketWriter.write(this.conn, data, 0); + const receive = await this.#nextPacket(); + if (receive.type === PacketType.OK_Packet) { + receive.body.skip(1); + return { + affectedRows: receive.body.readEncodedLen(), + lastInsertId: receive.body.readEncodedLen(), + }; + } else if (receive.type !== PacketType.Result) { + throw new ProtocolError(); + } + return { + affectedRows: 0, + lastInsertId: null, + }; + } catch (error) { + this.close(); + throw error; + } + } + + async [Symbol.asyncDispose](): Promise { + await this.close(); + } +} diff --git a/lib/packets/builders/auth.ts b/lib/packets/builders/auth.ts index 194c485..2a34054 100644 --- a/lib/packets/builders/auth.ts +++ b/lib/packets/builders/auth.ts @@ -6,10 +6,10 @@ import type { HandshakeBody } from "../parsers/handshake.ts"; import { clientCapabilities } from "./client_capabilities.ts"; /** @ignore */ -export function buildAuth( +export async function buildAuth( packet: HandshakeBody, - params: { username: string; password?: string; db?: string; ssl?: boolean }, -): Uint8Array { + params: { username: string; password?: string; db?: string; ssl: boolean }, +): Promise { const clientParam: number = clientCapabilities(packet, params); if (packet.serverCapabilities & ServerCapabilities.CLIENT_PLUGIN_AUTH) { @@ -21,7 +21,7 @@ export function buildAuth( .skip(23) .writeNullTerminatedString(params.username); if (params.password) { - const authData = auth( + const authData = await auth( packet.authPluginName, params.password, packet.seed, diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index 83adab6..af1633e 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -71,16 +71,31 @@ export function parseField(reader: BufferReader): FieldInfo { } /** @ignore */ -export function parseRow(reader: BufferReader, fields: FieldInfo[]): any { - const row: any = {}; +export function parseRowObject(reader: BufferReader, fields: FieldInfo[]): any { + const rowArray = parseRow(reader, fields); + return getRowObject(fields, rowArray); +} + +export function parseRow(reader: BufferReader, fields: FieldInfo[]): unknown[] { + const row: unknown[] = []; for (const field of fields) { const name = field.name; const val = reader.readLenCodeString(); - row[name] = val === null ? null : convertType(field, val); + const parsedVal = val === null ? null : convertType(field, val); + row.push(parsedVal); } return row; } +export function getRowObject(fields: FieldInfo[], row: unknown[]): any { + const obj: any = {}; + for (const [i, field] of fields.entries()) { + const name = field.name; + obj[name] = row[i]; + } + return obj; +} + /** @ignore */ function convertType(field: FieldInfo, val: string): any { const { fieldType, fieldLen } = field; diff --git a/mod.ts b/mod.ts index f42e155..b62c9ed 100644 --- a/mod.ts +++ b/mod.ts @@ -6,7 +6,4 @@ export { TLSMode } from "./lib/client.ts"; export type { ExecuteResult } from "./lib/connection.ts"; export { Connection } from "./lib/connection.ts"; -export type { LoggerConfig } from "./lib/logger.ts"; -export { configLogger } from "./lib/logger.ts"; - export * as log from "@std/log"; diff --git a/test.util.ts b/test.util.ts index e0dcab1..5f56f1f 100644 --- a/test.util.ts +++ b/test.util.ts @@ -1,6 +1,9 @@ +import { resolve } from "@std/path"; import { Client, type ClientConfig, type Connection } from "./mod.ts"; import { assertEquals } from "@std/assert"; +export const DIR_TMP_TEST = resolve("tmp_test"); + const { DB_PORT, DB_NAME, DB_PASSWORD, DB_USER, DB_HOST, DB_SOCKPATH } = Deno .env.toObject(); const port = DB_PORT ? parseInt(DB_PORT) : 3306; From f51ff9be847259da16d90eff9c8d9babc6e283d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 19:31:14 +0200 Subject: [PATCH 13/38] added logger --- lib/utils/logger.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/utils/logger.ts diff --git a/lib/utils/logger.ts b/lib/utils/logger.ts new file mode 100644 index 0000000..f70b71d --- /dev/null +++ b/lib/utils/logger.ts @@ -0,0 +1,12 @@ +import { getLogger } from "@std/log"; +import { MODULE_NAME } from "./meta.ts"; + +/** + * Used for internal module logging, + * do not import this directly outside of this module. + * + * @see {@link https://deno.land/std/log/mod.ts} + */ +export function logger() { + return getLogger(MODULE_NAME); +} From 0329304a02516b3a020f906cf5b3690947713925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 19:31:28 +0200 Subject: [PATCH 14/38] added encryptWithPublicKey --- lib/utils/crypto.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 lib/utils/crypto.ts diff --git a/lib/utils/crypto.ts b/lib/utils/crypto.ts new file mode 100644 index 0000000..af4f947 --- /dev/null +++ b/lib/utils/crypto.ts @@ -0,0 +1,26 @@ +import { decodeBase64 } from "@std/encoding/base64"; + +export async function encryptWithPublicKey( + key: string, + data: Uint8Array, +): Promise { + const pemHeader = "-----BEGIN PUBLIC KEY-----\n"; + const pemFooter = "\n-----END PUBLIC KEY-----"; + key = key.trim(); + key = key.substring(pemHeader.length, key.length - pemFooter.length); + const importedKey = await crypto.subtle.importKey( + "spki", + decodeBase64(key), + { name: "RSA-OAEP", hash: "SHA-256" }, + false, + ["encrypt"], + ); + + return await crypto.subtle.encrypt( + { + name: "RSA-OAEP", + }, + importedKey, + data, + ); +} From 2f74b3e24e9cdc26717c61e3bdaa82fbe5ed6521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 19:31:48 +0200 Subject: [PATCH 15/38] cleaned up packet --- lib/packets/packet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/packets/packet.ts b/lib/packets/packet.ts index 5f58d8d..4a68a2d 100644 --- a/lib/packets/packet.ts +++ b/lib/packets/packet.ts @@ -85,7 +85,7 @@ export class PacketReader { * @param buffer The buffer to read into * @returns The number of bytes read */ - static async _readSubarray( + static async #readSubarray( conn: Deno.Conn, buffer: Uint8Array, ): Promise { @@ -108,7 +108,7 @@ export class PacketReader { static async read(conn: Deno.Conn): Promise { const headerReader = new BufferReader(new Uint8Array(4)); let readCount = 0; - let nread = await this._readSubarray(conn, headerReader.buffer); + let nread = await this.#readSubarray(conn, headerReader.buffer); if (nread === null) return null; readCount = nread; const bodySize = headerReader.readUints(3); @@ -117,7 +117,7 @@ export class PacketReader { no: headerReader.readUint8(), }; const bodyReader = new BufferReader(new Uint8Array(bodySize)); - nread = await this._readSubarray(conn, bodyReader.buffer); + nread = await this.#readSubarray(conn, bodyReader.buffer); if (nread === null) return null; readCount += nread; From 9a32a53fd3fd4b8505c2bf94d98ed058208ca2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 19:32:26 +0200 Subject: [PATCH 16/38] refactored auth plugins --- lib/auth_plugin/caching_sha2_password.ts | 78 ----------------- lib/auth_plugin/crypt.ts | 28 ------ lib/auth_plugin/index.ts | 4 - .../caching_sha2_password.test.ts | 86 +++++++++++++++++++ lib/auth_plugins/caching_sha2_password.ts | 84 ++++++++++++++++++ lib/auth_plugins/mod.ts | 12 +++ lib/connection.ts | 49 +++++++---- lib/connection2.ts | 59 +++++++------ 8 files changed, 243 insertions(+), 157 deletions(-) delete mode 100644 lib/auth_plugin/caching_sha2_password.ts delete mode 100644 lib/auth_plugin/crypt.ts delete mode 100644 lib/auth_plugin/index.ts create mode 100644 lib/auth_plugins/caching_sha2_password.test.ts create mode 100644 lib/auth_plugins/caching_sha2_password.ts create mode 100644 lib/auth_plugins/mod.ts diff --git a/lib/auth_plugin/caching_sha2_password.ts b/lib/auth_plugin/caching_sha2_password.ts deleted file mode 100644 index 2111d1e..0000000 --- a/lib/auth_plugin/caching_sha2_password.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { xor } from "../util.ts"; -import type { PacketReader } from "../packets/packet.ts"; -import { encryptWithPublicKey } from "./crypt.ts"; - -interface handler { - done: boolean; - quickRead?: boolean; - next?: (packet: PacketReader) => any; - data?: Uint8Array; -} - -let scramble: Uint8Array, password: string; - -async function start( - scramble_: Uint8Array, - password_: string, -): Promise { - scramble = scramble_; - password = password_; - return { done: false, next: authMoreResponse }; -} - -async function authMoreResponse(packet: PacketReader): Promise { - const enum AuthStatusFlags { - FullAuth = 0x04, - FastPath = 0x03, - } - const REQUEST_PUBLIC_KEY = 0x02; - const statusFlag = packet.body.skip(1).readUint8(); - let authMoreData, done = true, next, quickRead = false; - if (statusFlag === AuthStatusFlags.FullAuth) { - authMoreData = new Uint8Array([REQUEST_PUBLIC_KEY]); - done = false; - next = encryptWithKey; - } - if (statusFlag === AuthStatusFlags.FastPath) { - done = false; - quickRead = true; - next = terminate; - } - return { done, next, quickRead, data: authMoreData }; -} - -async function encryptWithKey(packet: PacketReader): Promise { - const publicKey = parsePublicKey(packet); - const len = password.length; - const passwordBuffer: Uint8Array = new Uint8Array(len + 1); - for (let n = 0; n < len; n++) { - passwordBuffer[n] = password.charCodeAt(n); - } - passwordBuffer[len] = 0x00; - - const encryptedPassword = await encrypt(passwordBuffer, scramble, publicKey); - return { - done: false, - next: terminate, - data: new Uint8Array(encryptedPassword), - }; -} - -function parsePublicKey(packet: PacketReader): string { - return packet.body.skip(1).readNullTerminatedString(); -} - -async function encrypt( - password: Uint8Array, - scramble: Uint8Array, - key: string, -): Promise { - const stage1 = xor(password, scramble); - return await encryptWithPublicKey(key, stage1); -} - -function terminate() { - return { done: true }; -} - -export { start }; diff --git a/lib/auth_plugin/crypt.ts b/lib/auth_plugin/crypt.ts deleted file mode 100644 index 665931e..0000000 --- a/lib/auth_plugin/crypt.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { decodeBase64 } from "@std/encoding/base64"; - -async function encryptWithPublicKey( - key: string, - data: Uint8Array, -): Promise { - const pemHeader = "-----BEGIN PUBLIC KEY-----\n"; - const pemFooter = "\n-----END PUBLIC KEY-----"; - key = key.trim(); - key = key.substring(pemHeader.length, key.length - pemFooter.length); - const importedKey = await crypto.subtle.importKey( - "spki", - decodeBase64(key), - { name: "RSA-OAEP", hash: "SHA-256" }, - false, - ["encrypt"], - ); - - return await crypto.subtle.encrypt( - { - name: "RSA-OAEP", - }, - importedKey, - data, - ); -} - -export { encryptWithPublicKey }; diff --git a/lib/auth_plugin/index.ts b/lib/auth_plugin/index.ts deleted file mode 100644 index 198e023..0000000 --- a/lib/auth_plugin/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as caching_sha2_password from "./caching_sha2_password.ts"; -export default { - caching_sha2_password, -}; diff --git a/lib/auth_plugins/caching_sha2_password.test.ts b/lib/auth_plugins/caching_sha2_password.test.ts new file mode 100644 index 0000000..1b18aab --- /dev/null +++ b/lib/auth_plugins/caching_sha2_password.test.ts @@ -0,0 +1,86 @@ +import { assertEquals } from "@std/assert"; +import { PacketReader } from "../packets/packet.ts"; +import { + AuthPluginCachingSha2Password, + AuthStatusFlags, +} from "./caching_sha2_password.ts"; +import { PacketType } from "../constant/packet.ts"; +import { BufferReader } from "../buffer.ts"; + +Deno.test("AuthPluginCachingSha2Password", async (t) => { + await t.step("statusFlag FastPath", async () => { + const scramble = new Uint8Array([1, 2, 3]); + const password = "password"; + const authPlugin = new AuthPluginCachingSha2Password(scramble, password); + + assertEquals(authPlugin.scramble, scramble); + assertEquals(authPlugin.password, password); + assertEquals(authPlugin.done, false); + assertEquals(authPlugin.quickRead, false); + assertEquals(authPlugin.data, undefined); + + const bodyReader = new BufferReader( + new Uint8Array([0x00, AuthStatusFlags.FastPath]), + ); + await authPlugin.next( + new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + ); + + assertEquals(authPlugin.done, false); + assertEquals(authPlugin.data, undefined); + assertEquals(authPlugin.quickRead, true); + + await authPlugin.next( + new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + ); + + assertEquals(authPlugin.done, true); + }); + + await t.step("statusFlag FullAuth", async () => { + const scramble = new Uint8Array([1, 2, 3]); + const password = "password"; + const authPlugin = new AuthPluginCachingSha2Password(scramble, password); + + assertEquals(authPlugin.scramble, scramble); + assertEquals(authPlugin.password, password); + assertEquals(authPlugin.done, false); + assertEquals(authPlugin.quickRead, false); + assertEquals(authPlugin.data, undefined); + + let bodyReader = new BufferReader( + new Uint8Array([0x00, AuthStatusFlags.FullAuth]), + ); + await authPlugin.next( + new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + ); + + assertEquals(authPlugin.done, false); + assertEquals(authPlugin.data, new Uint8Array([0x02])); + assertEquals(authPlugin.quickRead, false); + + const publicKey = `-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkFF85HndOJoTVsuYBNOu4N63s +bVMWfMVZ/ZXFVYeFE7H6Vp0jhu2d6JUUx9WCXV5JOt/mXoCirywhz2LM+f7kaBCh +0YIFh5JKS43a3COC9BJupj2dco/iWEmOFqRvCn/ErQNdmataqQlePq3SitusJwuj +PQogsoytp/nSKLsTLwIDA/+/ +-----END PUBLIC KEY-----`; + + const encodedPublicKey = new TextEncoder().encode(publicKey); + + bodyReader = new BufferReader(new Uint8Array([0x00, ...encodedPublicKey])); + await authPlugin.next( + new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + ); + + assertEquals(authPlugin.done, false); + assertEquals(authPlugin.data?.length, 128); + assertEquals(authPlugin.quickRead, false); + + await authPlugin.next( + new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + ); + + assertEquals(authPlugin.done, true); + }); +}); diff --git a/lib/auth_plugins/caching_sha2_password.ts b/lib/auth_plugins/caching_sha2_password.ts new file mode 100644 index 0000000..3e64567 --- /dev/null +++ b/lib/auth_plugins/caching_sha2_password.ts @@ -0,0 +1,84 @@ +import { xor } from "../util.ts"; +import type { PacketReader } from "../packets/packet.ts"; +import { encryptWithPublicKey } from "../utils/crypto.ts"; + +export const enum AuthStatusFlags { + FullAuth = 0x04, + FastPath = 0x03, +} + +export class AuthPluginCachingSha2Password { + readonly scramble: Uint8Array; + readonly password: string; + done: boolean = false; + quickRead: boolean = false; + data: Uint8Array | undefined = undefined; + + next: (packet: PacketReader) => Promise = this.authMoreResponse.bind( + this, + ); + + constructor(scramble: Uint8Array, password: string) { + this.scramble = scramble; + this.password = password; + } + + protected terminate() { + this.done = true; + return Promise.resolve(this); + } + + protected authMoreResponse(packet: PacketReader): Promise { + const REQUEST_PUBLIC_KEY = 0x02; + const statusFlag = packet.body.skip(1).readUint8(); + + switch (statusFlag) { + case AuthStatusFlags.FullAuth: { + this.data = new Uint8Array([REQUEST_PUBLIC_KEY]); + this.next = this.encryptWithKey.bind(this); + break; + } + case AuthStatusFlags.FastPath: { + this.quickRead = true; + this.next = this.terminate.bind(this); + break; + } + default: + this.done = true; + } + + return Promise.resolve(this); + } + + protected async encryptWithKey(packet: PacketReader): Promise { + const publicKey = this.parsePublicKey(packet); + const len = this.password.length; + const passwordBuffer: Uint8Array = new Uint8Array(len + 1); + for (let n = 0; n < len; n++) { + passwordBuffer[n] = this.password.charCodeAt(n); + } + passwordBuffer[len] = 0x00; + + const encryptedPassword = await this.encrypt( + passwordBuffer, + this.scramble, + publicKey, + ); + this.next = this.terminate.bind(this); + this.data = new Uint8Array(encryptedPassword); + return this; + } + + protected parsePublicKey(packet: PacketReader): string { + return packet.body.skip(1).readNullTerminatedString(); + } + + async encrypt( + password: Uint8Array, + scramble: Uint8Array, + key: string, + ): Promise { + const stage1 = xor(password, scramble); + return await encryptWithPublicKey(key, stage1); + } +} diff --git a/lib/auth_plugins/mod.ts b/lib/auth_plugins/mod.ts new file mode 100644 index 0000000..58a459d --- /dev/null +++ b/lib/auth_plugins/mod.ts @@ -0,0 +1,12 @@ +import { AuthPluginCachingSha2Password } from "./caching_sha2_password.ts"; + +export { AuthPluginCachingSha2Password }; + +export const AuthPluginName = { + CachingSha2Password: "caching_sha2_password", +} as const; +export type AuthPluginName = typeof AuthPluginName[keyof typeof AuthPluginName]; + +export const AuthPlugins = { + caching_sha2_password: AuthPluginCachingSha2Password, +} as const; diff --git a/lib/connection.ts b/lib/connection.ts index 085aa09..680efe5 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -20,7 +20,7 @@ import { parseRowObject, } from "./packets/parsers/result.ts"; import { PacketType } from "./constant/packet.ts"; -import authPlugin from "./auth_plugin/index.ts"; +import { AuthPluginName, AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; import ServerCapabilities from "./constant/capabilities.ts"; @@ -159,13 +159,11 @@ export class Connection { receive = await this.nextPacket(); const authResult = parseAuth(receive); - let handler; + let authPlugin: AuthPluginName | undefined = undefined; switch (authResult) { case AuthResult.AuthMoreRequired: { - const adaptedPlugin = - (authPlugin as any)[handshakePacket.authPluginName]; - handler = adaptedPlugin; + authPlugin = handshakePacket.authPluginName as AuthPluginName; break; } case AuthResult.MethodMismatch: { @@ -202,21 +200,34 @@ export class Connection { } } - let result; - if (handler) { - result = await handler.start(handshakePacket.seed, password!); - while (!result.done) { - if (result.data) { - const sequenceNumber = receive.header.no + 1; - await PacketWriter.write(this.conn, result.data, sequenceNumber); - receive = await this.nextPacket(); - } - if (result.quickRead) { - await this.nextPacket(); - } - if (result.next) { - result = await result.next(receive); + if (authPlugin) { + switch (authPlugin) { + case AuthPluginName.CachingSha2Password: { + const plugin = new AuthPlugins[authPlugin]( + handshakePacket.seed, + this.config.password!, + ); + + while (!plugin.done) { + if (plugin.data) { + const sequenceNumber = receive.header.no + 1; + await PacketWriter.write( + this.conn, + plugin.data, + sequenceNumber, + ); + receive = await this.nextPacket(); + } + if (plugin.quickRead) { + await this.nextPacket(); + } + + await plugin.next(receive); + } + break; } + default: + throw new Error("Unsupported auth plugin"); } } diff --git a/lib/connection2.ts b/lib/connection2.ts index 7f7a06e..05f5dc4 100644 --- a/lib/connection2.ts +++ b/lib/connection2.ts @@ -16,10 +16,9 @@ import { type FieldInfo, parseField, parseRow, - parseRowObject, } from "./packets/parsers/result.ts"; import { PacketType } from "./constant/packet.ts"; -import authPlugin from "./auth_plugin/index.ts"; +import { AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; import ServerCapabilities from "./constant/capabilities.ts"; @@ -31,10 +30,10 @@ import type { SqlxConnectionOptions, SqlxParameterType, } from "@halvardm/sqlx"; -import { buildQuery } from "./packets/builders/query.ts"; import { VERSION } from "./util.ts"; -import { parse, resolve } from "@std/path"; +import { resolve } from "@std/path"; import { toCamelCase } from "@std/text"; +import { AuthPluginName } from "./auth_plugins/mod.ts"; export type MysqlParameterType = SqlxParameterType; /** @@ -255,13 +254,11 @@ export class MysqlConnection implements SqlxConnectable { receive = await this.#nextPacket(); const authResult = parseAuth(receive); - let handler; + let authPlugin: AuthPluginName | undefined = undefined; switch (authResult) { case AuthResult.AuthMoreRequired: { - const adaptedPlugin = - (authPlugin as any)[handshakePacket.authPluginName]; - handler = adaptedPlugin; + authPlugin = handshakePacket.authPluginName as AuthPluginName; break; } case AuthResult.MethodMismatch: { @@ -302,28 +299,34 @@ export class MysqlConnection implements SqlxConnectable { } } - let result; - if (handler) { - result = await handler.start( - handshakePacket.seed, - this.config.password!, - ); - while (!result.done) { - if (result.data) { - const sequenceNumber = receive.header.no + 1; - await PacketWriter.write( - this.conn, - result.data, - sequenceNumber, + if (authPlugin) { + switch (authPlugin) { + case AuthPluginName.CachingSha2Password: { + const plugin = new AuthPlugins[authPlugin]( + handshakePacket.seed, + this.config.password!, ); - receive = await this.#nextPacket(); - } - if (result.quickRead) { - await this.#nextPacket(); - } - if (result.next) { - result = await result.next(receive); + + while (!plugin.done) { + if (plugin.data) { + const sequenceNumber = receive.header.no + 1; + await PacketWriter.write( + this.conn, + plugin.data, + sequenceNumber, + ); + receive = await this.#nextPacket(); + } + if (plugin.quickRead) { + await this.#nextPacket(); + } + + await plugin.next(receive); + } + break; } + default: + throw new Error("Unsupported auth plugin"); } } From a150f6a51475f6508c13dce28e43aa96c6955782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 19:35:28 +0200 Subject: [PATCH 17/38] Refactored export of ServerCapabilities --- lib/connection.ts | 2 +- lib/connection2.ts | 2 +- lib/constant/capabilities.ts | 4 +--- lib/packets/builders/auth.ts | 2 +- lib/packets/builders/client_capabilities.ts | 2 +- lib/packets/parsers/err.ts | 2 +- lib/packets/parsers/handshake.ts | 2 +- 7 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/connection.ts b/lib/connection.ts index 680efe5..b709f16 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -23,7 +23,7 @@ import { PacketType } from "./constant/packet.ts"; import { AuthPluginName, AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; -import ServerCapabilities from "./constant/capabilities.ts"; +import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; import { logger } from "./logger.ts"; diff --git a/lib/connection2.ts b/lib/connection2.ts index 05f5dc4..690b22e 100644 --- a/lib/connection2.ts +++ b/lib/connection2.ts @@ -21,7 +21,7 @@ import { PacketType } from "./constant/packet.ts"; import { AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; -import ServerCapabilities from "./constant/capabilities.ts"; +import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; import { logger } from "./logger.ts"; import type { diff --git a/lib/constant/capabilities.ts b/lib/constant/capabilities.ts index a411d79..33181fa 100644 --- a/lib/constant/capabilities.ts +++ b/lib/constant/capabilities.ts @@ -1,4 +1,4 @@ -enum ServerCapabilities { +export enum ServerCapabilities { CLIENT_LONG_PASSWORD = 0x00000001, CLIENT_FOUND_ROWS = 0x00000002, CLIENT_LONG_FLAG = 0x00000004, @@ -23,5 +23,3 @@ enum ServerCapabilities { CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000, CLIENT_DEPRECATE_EOF = 0x01000000, } - -export default ServerCapabilities; diff --git a/lib/packets/builders/auth.ts b/lib/packets/builders/auth.ts index 2a34054..feb15ba 100644 --- a/lib/packets/builders/auth.ts +++ b/lib/packets/builders/auth.ts @@ -1,6 +1,6 @@ import auth from "../../auth.ts"; import { BufferWriter } from "../../buffer.ts"; -import ServerCapabilities from "../../constant/capabilities.ts"; +import { ServerCapabilities } from "../../constant/capabilities.ts"; import { Charset } from "../../constant/charset.ts"; import type { HandshakeBody } from "../parsers/handshake.ts"; import { clientCapabilities } from "./client_capabilities.ts"; diff --git a/lib/packets/builders/client_capabilities.ts b/lib/packets/builders/client_capabilities.ts index 842fdcb..b03125e 100644 --- a/lib/packets/builders/client_capabilities.ts +++ b/lib/packets/builders/client_capabilities.ts @@ -1,4 +1,4 @@ -import ServerCapabilities from "../../constant/capabilities.ts"; +import { ServerCapabilities } from "../../constant/capabilities.ts"; import type { HandshakeBody } from "../parsers/handshake.ts"; export function clientCapabilities( diff --git a/lib/packets/parsers/err.ts b/lib/packets/parsers/err.ts index 8589446..114b78c 100644 --- a/lib/packets/parsers/err.ts +++ b/lib/packets/parsers/err.ts @@ -1,6 +1,6 @@ import type { BufferReader } from "../../buffer.ts"; import type { Connection } from "../../connection.ts"; -import ServerCapabilities from "../../constant/capabilities.ts"; +import { ServerCapabilities } from "../../constant/capabilities.ts"; /** @ignore */ export interface ErrorPacket { diff --git a/lib/packets/parsers/handshake.ts b/lib/packets/parsers/handshake.ts index 7ae38b5..291b84e 100644 --- a/lib/packets/parsers/handshake.ts +++ b/lib/packets/parsers/handshake.ts @@ -1,5 +1,5 @@ import { type BufferReader, BufferWriter } from "../../buffer.ts"; -import ServerCapabilities from "../../constant/capabilities.ts"; +import { ServerCapabilities } from "../../constant/capabilities.ts"; import { PacketType } from "../../constant/packet.ts"; import type { PacketReader } from "../packet.ts"; From 2b2ebf35f728f4ca418e6ff1286495a9b4393fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 19:43:08 +0200 Subject: [PATCH 18/38] Refactored errors --- lib/auth.ts | 3 ++- lib/client.ts | 3 ++- lib/connection.ts | 43 +++++++++++++++++++++-------------------- lib/connection2.ts | 28 +++++++++++++-------------- lib/constant/errors.ts | 29 ---------------------------- lib/packets/packet.ts | 4 ++-- lib/pool.ts | 3 ++- lib/utils/errors.ts | 44 ++++++++++++++++++++++++++++++++++++++++++ test.ts | 10 +++++----- 9 files changed, 93 insertions(+), 74 deletions(-) delete mode 100644 lib/constant/errors.ts create mode 100644 lib/utils/errors.ts diff --git a/lib/auth.ts b/lib/auth.ts index e092ef0..2bac059 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,6 +1,7 @@ import { crypto, type DigestAlgorithm } from "@std/crypto"; import { xor } from "./util.ts"; import { encode } from "./buffer.ts"; +import { MysqlError } from "./utils/errors.ts"; async function hash( algorithm: DigestAlgorithm, @@ -47,6 +48,6 @@ export default function auth( case "caching_sha2_password": return cachingSha2Password(password, seed); default: - throw new Error("Not supported"); + throw new MysqlError("Not supported"); } } diff --git a/lib/client.ts b/lib/client.ts index 3ec10ee..a6338ec 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -5,6 +5,7 @@ import { } from "./connection.ts"; import { ConnectionPool, PoolConnection } from "./pool.ts"; import { logger } from "./logger.ts"; +import { MysqlError } from "./utils/errors.ts"; /** * Client Config @@ -121,7 +122,7 @@ export class Client { async useConnection(fn: (conn: Connection) => Promise) { if (!this._pool) { - throw new Error("Unconnected"); + throw new MysqlError("Unconnected"); } const connection = await this._pool.pop(); try { diff --git a/lib/connection.ts b/lib/connection.ts index b709f16..5dca3f5 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -1,10 +1,11 @@ import { type ClientConfig, TLSMode } from "./client.ts"; import { - ConnectionError, - ProtocolError, - ReadError, - ResponseTimeoutError, -} from "./constant/errors.ts"; + MysqlConnectionError, + MysqlError, + MysqlProtocolError, + MysqlReadError, + MysqlResponseTimeoutError, +} from "./utils/errors.ts"; import { buildAuth } from "./packets/builders/auth.ts"; import { buildQuery } from "./packets/builders/query.ts"; import { PacketReader, PacketWriter } from "./packets/packet.ts"; @@ -59,13 +60,13 @@ export class Connection { get conn(): Deno.Conn { if (!this._conn) { - throw new ConnectionError("Not connected"); + throw new MysqlConnectionError("Not connected"); } if (this.state != ConnectionState.CONNECTED) { if (this.state == ConnectionState.CLOSED) { - throw new ConnectionError("Connection is closed"); + throw new MysqlConnectionError("Connection is closed"); } else { - throw new ConnectionError("Must be connected first"); + throw new MysqlConnectionError("Must be connected first"); } } return this._conn; @@ -90,7 +91,7 @@ export class Connection { this.config.tls.mode !== TLSMode.DISABLED && this.config.tls.mode !== TLSMode.VERIFY_IDENTITY ) { - throw new Error("unsupported tls mode"); + throw new MysqlError("unsupported tls mode"); } const { hostname, port = 3306, socketPath, username = "", password } = this.config; @@ -121,7 +122,7 @@ export class Connection { (handshakePacket.serverCapabilities & ServerCapabilities.CLIENT_SSL) === 0 ) { - throw new Error("Server does not support TLS"); + throw new MysqlError("Server does not support TLS"); } if ( (handshakePacket.serverCapabilities & @@ -193,7 +194,7 @@ export class Connection { receive = await this.nextPacket(); const authSwitch2 = parseAuthSwitch(receive.body); if (authSwitch2.authPluginName !== "") { - throw new Error( + throw new MysqlError( "Do not allow to change the auth plugin more than once!", ); } @@ -227,7 +228,7 @@ export class Connection { break; } default: - throw new Error("Unsupported auth plugin"); + throw new MysqlError("Unsupported auth plugin"); } } @@ -236,7 +237,7 @@ export class Connection { const error = parseError(receive.body, this); logger().error(`connect error(${error.code}): ${error.message}`); this.close(); - throw new Error(error.message); + throw new MysqlError(error.message); } else { logger().info(`connected to ${this.remoteAddr}`); this.state = ConnectionState.CONNECTED; @@ -259,7 +260,7 @@ export class Connection { private async nextPacket(): Promise { if (!this.conn) { - throw new ConnectionError("Not connected"); + throw new MysqlConnectionError("Not connected"); } const timeoutTimer = this.config.timeout @@ -274,7 +275,7 @@ export class Connection { } catch (error) { if (this._timedOut) { // Connection has been closed by timeoutCallback. - throw new ResponseTimeoutError("Connection read timed out"); + throw new MysqlResponseTimeoutError("Connection read timed out"); } timeoutTimer && clearTimeout(timeoutTimer); this.close(); @@ -286,12 +287,12 @@ export class Connection { // Connection is half-closed by the remote host. // Call close() to avoid leaking socket. this.close(); - throw new ReadError("Connection closed unexpectedly"); + throw new MysqlReadError("Connection closed unexpectedly"); } if (packet.type === PacketType.ERR_Packet) { packet.body.skip(1); const error = parseError(packet.body, this); - throw new Error(error.message); + throw new MysqlError(error.message); } return packet!; } @@ -338,9 +339,9 @@ export class Connection { ): Promise { if (this.state != ConnectionState.CONNECTED) { if (this.state == ConnectionState.CLOSED) { - throw new ConnectionError("Connection is closed"); + throw new MysqlConnectionError("Connection is closed"); } else { - throw new ConnectionError("Must be connected first"); + throw new MysqlConnectionError("Must be connected first"); } } const data = buildQuery(sql, params); @@ -354,7 +355,7 @@ export class Connection { lastInsertId: receive.body.readEncodedLen(), }; } else if (receive.type !== PacketType.Result) { - throw new ProtocolError(); + throw new MysqlProtocolError(receive.type.toString()); } let fieldCount = receive.body.readEncodedLen(); const fields: FieldInfo[] = []; @@ -371,7 +372,7 @@ export class Connection { // EOF(mysql < 5.7 or mariadb < 10.2) receive = await this.nextPacket(); if (receive.type !== PacketType.EOF_Packet) { - throw new ProtocolError(); + throw new MysqlProtocolError(receive.type.toString()); } } diff --git a/lib/connection2.ts b/lib/connection2.ts index 690b22e..53c8f9f 100644 --- a/lib/connection2.ts +++ b/lib/connection2.ts @@ -1,9 +1,9 @@ import { - ConnectionError, - ProtocolError, - ReadError, - ResponseTimeoutError, -} from "./constant/errors.ts"; + MysqlConnectionError, + MysqlProtocolError, + MysqlReadError, + MysqlResponseTimeoutError, +} from "./utils/errors.ts"; import { buildAuth } from "./packets/builders/auth.ts"; import { PacketReader, PacketWriter } from "./packets/packet.ts"; import { parseError } from "./packets/parsers/err.ts"; @@ -145,13 +145,13 @@ export class MysqlConnection implements SqlxConnectable { get conn(): Deno.Conn { if (!this._conn) { - throw new ConnectionError("Not connected"); + throw new MysqlConnectionError("Not connected"); } if (this.state != ConnectionState.CONNECTED) { if (this.state == ConnectionState.CLOSED) { - throw new ConnectionError("Connection is closed"); + throw new MysqlConnectionError("Connection is closed"); } else { - throw new ConnectionError("Must be connected first"); + throw new MysqlConnectionError("Must be connected first"); } } return this._conn; @@ -470,7 +470,7 @@ export class MysqlConnection implements SqlxConnectable { async #nextPacket(): Promise { if (!this._conn) { - throw new ConnectionError("Not connected"); + throw new MysqlConnectionError("Not connected"); } const timeoutTimer = this.config.parameters.connectTimeout @@ -485,7 +485,7 @@ export class MysqlConnection implements SqlxConnectable { } catch (error) { if (this._timedOut) { // Connection has been closed by timeoutCallback. - throw new ResponseTimeoutError("Connection read timed out"); + throw new MysqlResponseTimeoutError("Connection read timed out"); } timeoutTimer && clearTimeout(timeoutTimer); this.close(); @@ -497,7 +497,7 @@ export class MysqlConnection implements SqlxConnectable { // Connection is half-closed by the remote host. // Call close() to avoid leaking socket. this.close(); - throw new ReadError("Connection closed unexpectedly"); + throw new MysqlReadError("Connection closed unexpectedly"); } if (packet.type === PacketType.ERR_Packet) { packet.body.skip(1); @@ -526,7 +526,7 @@ export class MysqlConnection implements SqlxConnectable { lastInsertId: receive.body.readEncodedLen(), }; } else if (receive.type !== PacketType.Result) { - throw new ProtocolError(); + throw new MysqlProtocolError(receive.type.toString()); } let fieldCount = receive.body.readEncodedLen(); const fields: FieldInfo[] = []; @@ -542,7 +542,7 @@ export class MysqlConnection implements SqlxConnectable { // EOF(mysql < 5.7 or mariadb < 10.2) receive = await this.#nextPacket(); if (receive.type !== PacketType.EOF_Packet) { - throw new ProtocolError(); + throw new MysqlProtocolError(receive.type.toString()); } } @@ -572,7 +572,7 @@ export class MysqlConnection implements SqlxConnectable { lastInsertId: receive.body.readEncodedLen(), }; } else if (receive.type !== PacketType.Result) { - throw new ProtocolError(); + throw new MysqlProtocolError(receive.type.toString()); } return { affectedRows: 0, diff --git a/lib/constant/errors.ts b/lib/constant/errors.ts deleted file mode 100644 index 4c22894..0000000 --- a/lib/constant/errors.ts +++ /dev/null @@ -1,29 +0,0 @@ -export class ConnectionError extends Error { - constructor(msg?: string) { - super(msg); - } -} - -export class WriteError extends ConnectionError { - constructor(msg?: string) { - super(msg); - } -} - -export class ReadError extends ConnectionError { - constructor(msg?: string) { - super(msg); - } -} - -export class ResponseTimeoutError extends ConnectionError { - constructor(msg?: string) { - super(msg); - } -} - -export class ProtocolError extends ConnectionError { - constructor(msg?: string) { - super(msg); - } -} diff --git a/lib/packets/packet.ts b/lib/packets/packet.ts index 4a68a2d..96f6ba2 100644 --- a/lib/packets/packet.ts +++ b/lib/packets/packet.ts @@ -1,6 +1,6 @@ import { byteFormat } from "../util.ts"; import { BufferReader, BufferWriter } from "../buffer.ts"; -import { WriteError } from "../constant/errors.ts"; +import { MysqlWriteError } from "../utils/errors.ts"; import { logger } from "../logger.ts"; import { PacketType } from "../constant/packet.ts"; @@ -41,7 +41,7 @@ export class PacketWriter { wrote += await conn.write(data.buffer.subarray(wrote)); } while (wrote < data.length); } catch (error) { - throw new WriteError(error.message); + throw new MysqlWriteError(error.message); } } diff --git a/lib/pool.ts b/lib/pool.ts index 307b42f..e359b55 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -1,6 +1,7 @@ import { DeferredStack } from "./deferred.ts"; import { Connection } from "./connection.ts"; import { logger } from "./logger.ts"; +import { MysqlError } from "./utils/errors.ts"; /** @ignore */ export class PoolConnection extends Connection { @@ -90,7 +91,7 @@ export class ConnectionPool { async pop(): Promise { if (this._closed) { - throw new Error("Connection pool is closed"); + throw new MysqlError("Connection pool is closed"); } let conn = this._deferred.tryPopAvailable(); if (conn) { diff --git a/lib/utils/errors.ts b/lib/utils/errors.ts new file mode 100644 index 0000000..94eaaef --- /dev/null +++ b/lib/utils/errors.ts @@ -0,0 +1,44 @@ +import { SqlxError } from "@halvardm/sqlx"; + +export class MysqlError extends SqlxError { + constructor(msg: string) { + super(msg); + } +} + +export class MysqlConnectionError extends MysqlError { + constructor(msg: string) { + super(msg); + } +} + +export class MysqlWriteError extends MysqlError { + constructor(msg: string) { + super(msg); + } +} + +export class MysqlReadError extends MysqlError { + constructor(msg: string) { + super(msg); + } +} + +export class MysqlResponseTimeoutError extends MysqlError { + constructor(msg: string) { + super(msg); + } +} + +export class MysqlProtocolError extends MysqlError { + constructor(msg: string) { + super(msg); + } +} + +/** + * Check if an error is a MysqlError + */ +export function isMysqlError(err: unknown): err is MysqlError { + return err instanceof MysqlError; +} diff --git a/test.ts b/test.ts index 4291062..72c9772 100644 --- a/test.ts +++ b/test.ts @@ -1,9 +1,9 @@ import { assertEquals, assertRejects } from "@std/assert"; import { lessThan, parse } from "@std/semver"; import { - ConnectionError, - ResponseTimeoutError, -} from "./lib/constant/errors.ts"; + MysqlConnectionError, + MysqlResponseTimeoutError, +} from "./lib/utils/errors.ts"; import { createTestDB, delay, @@ -189,7 +189,7 @@ testWithClient(async function testQueryOnClosed(client) { conn.close(); await conn.query("SELECT 1"); }); - }, ConnectionError); + }, MysqlConnectionError); } assertEquals(client.pool?.size, 0); await client.query("select 1"); @@ -265,7 +265,7 @@ testWithClient(async function testReadTimeout(client) { await assertRejects(async () => { await client.execute("select sleep(0.7)"); - }, ResponseTimeoutError); + }, MysqlResponseTimeoutError); assertEquals(client.pool, { maxSize: 3, From d005bf4741b53e6f10b8fc9fabdf99ede25e3322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 19:58:26 +0200 Subject: [PATCH 19/38] Refactored consts, added parameter type, and added doc --- .../caching_sha2_password.test.ts | 32 ++++- lib/connection.ts | 14 +- lib/connection2.ts | 20 +-- lib/constant/capabilities.ts | 3 + lib/constant/charset.ts | 3 + lib/constant/mysql_types.ts | 95 +++++--------- lib/constant/packet.ts | 5 +- lib/constant/server_status.ts | 4 +- lib/packets/packet.ts | 26 ++-- lib/packets/parsers/handshake.ts | 8 +- lib/packets/parsers/result.ts | 120 +++++++++--------- 11 files changed, 171 insertions(+), 159 deletions(-) diff --git a/lib/auth_plugins/caching_sha2_password.test.ts b/lib/auth_plugins/caching_sha2_password.test.ts index 1b18aab..ee7c505 100644 --- a/lib/auth_plugins/caching_sha2_password.test.ts +++ b/lib/auth_plugins/caching_sha2_password.test.ts @@ -4,7 +4,7 @@ import { AuthPluginCachingSha2Password, AuthStatusFlags, } from "./caching_sha2_password.ts"; -import { PacketType } from "../constant/packet.ts"; +import { ComQueryResponsePacket } from "../constant/packet.ts"; import { BufferReader } from "../buffer.ts"; Deno.test("AuthPluginCachingSha2Password", async (t) => { @@ -23,7 +23,11 @@ Deno.test("AuthPluginCachingSha2Password", async (t) => { new Uint8Array([0x00, AuthStatusFlags.FastPath]), ); await authPlugin.next( - new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + new PacketReader( + { size: 2, no: 0 }, + bodyReader, + ComQueryResponsePacket.OK_Packet, + ), ); assertEquals(authPlugin.done, false); @@ -31,7 +35,11 @@ Deno.test("AuthPluginCachingSha2Password", async (t) => { assertEquals(authPlugin.quickRead, true); await authPlugin.next( - new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + new PacketReader( + { size: 2, no: 0 }, + bodyReader, + ComQueryResponsePacket.OK_Packet, + ), ); assertEquals(authPlugin.done, true); @@ -52,7 +60,11 @@ Deno.test("AuthPluginCachingSha2Password", async (t) => { new Uint8Array([0x00, AuthStatusFlags.FullAuth]), ); await authPlugin.next( - new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + new PacketReader( + { size: 2, no: 0 }, + bodyReader, + ComQueryResponsePacket.OK_Packet, + ), ); assertEquals(authPlugin.done, false); @@ -70,7 +82,11 @@ PQogsoytp/nSKLsTLwIDA/+/ bodyReader = new BufferReader(new Uint8Array([0x00, ...encodedPublicKey])); await authPlugin.next( - new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + new PacketReader( + { size: 2, no: 0 }, + bodyReader, + ComQueryResponsePacket.OK_Packet, + ), ); assertEquals(authPlugin.done, false); @@ -78,7 +94,11 @@ PQogsoytp/nSKLsTLwIDA/+/ assertEquals(authPlugin.quickRead, false); await authPlugin.next( - new PacketReader({ size: 2, no: 0 }, bodyReader, PacketType.OK_Packet), + new PacketReader( + { size: 2, no: 0 }, + bodyReader, + ComQueryResponsePacket.OK_Packet, + ), ); assertEquals(authPlugin.done, true); diff --git a/lib/connection.ts b/lib/connection.ts index 5dca3f5..cc5ebfb 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -20,7 +20,7 @@ import { parseField, parseRowObject, } from "./packets/parsers/result.ts"; -import { PacketType } from "./constant/packet.ts"; +import { ComQueryResponsePacket } from "./constant/packet.ts"; import { AuthPluginName, AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; @@ -289,7 +289,7 @@ export class Connection { this.close(); throw new MysqlReadError("Connection closed unexpectedly"); } - if (packet.type === PacketType.ERR_Packet) { + if (packet.type === ComQueryResponsePacket.ERR_Packet) { packet.body.skip(1); const error = parseError(packet.body, this); throw new MysqlError(error.message); @@ -348,13 +348,13 @@ export class Connection { try { await PacketWriter.write(this.conn, data, 0); let receive = await this.nextPacket(); - if (receive.type === PacketType.OK_Packet) { + if (receive.type === ComQueryResponsePacket.OK_Packet) { receive.body.skip(1); return { affectedRows: receive.body.readEncodedLen(), lastInsertId: receive.body.readEncodedLen(), }; - } else if (receive.type !== PacketType.Result) { + } else if (receive.type !== ComQueryResponsePacket.Result) { throw new MysqlProtocolError(receive.type.toString()); } let fieldCount = receive.body.readEncodedLen(); @@ -371,7 +371,7 @@ export class Connection { if (!(this.capabilities & ServerCapabilities.CLIENT_DEPRECATE_EOF)) { // EOF(mysql < 5.7 or mariadb < 10.2) receive = await this.nextPacket(); - if (receive.type !== PacketType.EOF_Packet) { + if (receive.type !== ComQueryResponsePacket.EOF_Packet) { throw new MysqlProtocolError(receive.type.toString()); } } @@ -379,7 +379,7 @@ export class Connection { if (!iterator) { while (true) { receive = await this.nextPacket(); - if (receive.type === PacketType.EOF_Packet) { + if (receive.type === ComQueryResponsePacket.EOF_Packet) { break; } else { const row = parseRowObject(receive.body, fields); @@ -403,7 +403,7 @@ export class Connection { const next = async () => { const receive = await this.nextPacket(); - if (receive.type === PacketType.EOF_Packet) { + if (receive.type === ComQueryResponsePacket.EOF_Packet) { return { done: true }; } diff --git a/lib/connection2.ts b/lib/connection2.ts index 53c8f9f..a2f29fa 100644 --- a/lib/connection2.ts +++ b/lib/connection2.ts @@ -15,9 +15,9 @@ import { import { type FieldInfo, parseField, - parseRow, + parseRowArray, } from "./packets/parsers/result.ts"; -import { PacketType } from "./constant/packet.ts"; +import { ComQueryResponsePacket } from "./constant/packet.ts"; import { AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./auth.ts"; @@ -499,7 +499,7 @@ export class MysqlConnection implements SqlxConnectable { this.close(); throw new MysqlReadError("Connection closed unexpectedly"); } - if (packet.type === PacketType.ERR_Packet) { + if (packet.type === ComQueryResponsePacket.ERR_Packet) { packet.body.skip(1); const error = parseError(packet.body, this as any); throw new Error(error.message); @@ -519,13 +519,13 @@ export class MysqlConnection implements SqlxConnectable { try { await PacketWriter.write(this.conn, data, 0); let receive = await this.#nextPacket(); - if (receive.type === PacketType.OK_Packet) { + if (receive.type === ComQueryResponsePacket.OK_Packet) { receive.body.skip(1); return { affectedRows: receive.body.readEncodedLen(), lastInsertId: receive.body.readEncodedLen(), }; - } else if (receive.type !== PacketType.Result) { + } else if (receive.type !== ComQueryResponsePacket.Result) { throw new MysqlProtocolError(receive.type.toString()); } let fieldCount = receive.body.readEncodedLen(); @@ -541,15 +541,15 @@ export class MysqlConnection implements SqlxConnectable { if (!(this.capabilities & ServerCapabilities.CLIENT_DEPRECATE_EOF)) { // EOF(mysql < 5.7 or mariadb < 10.2) receive = await this.#nextPacket(); - if (receive.type !== PacketType.EOF_Packet) { + if (receive.type !== ComQueryResponsePacket.EOF_Packet) { throw new MysqlProtocolError(receive.type.toString()); } } receive = await this.#nextPacket(); - while (receive.type !== PacketType.EOF_Packet) { - const row = parseRow(receive.body, fields); + while (receive.type !== ComQueryResponsePacket.EOF_Packet) { + const row = parseRowArray(receive.body, fields); yield { row, fields }; receive = await this.#nextPacket(); } @@ -565,13 +565,13 @@ export class MysqlConnection implements SqlxConnectable { try { await PacketWriter.write(this.conn, data, 0); const receive = await this.#nextPacket(); - if (receive.type === PacketType.OK_Packet) { + if (receive.type === ComQueryResponsePacket.OK_Packet) { receive.body.skip(1); return { affectedRows: receive.body.readEncodedLen(), lastInsertId: receive.body.readEncodedLen(), }; - } else if (receive.type !== PacketType.Result) { + } else if (receive.type !== ComQueryResponsePacket.Result) { throw new MysqlProtocolError(receive.type.toString()); } return { diff --git a/lib/constant/capabilities.ts b/lib/constant/capabilities.ts index 33181fa..d5cab54 100644 --- a/lib/constant/capabilities.ts +++ b/lib/constant/capabilities.ts @@ -1,3 +1,6 @@ +/** + * MySQL Server Capabilities + */ export enum ServerCapabilities { CLIENT_LONG_PASSWORD = 0x00000001, CLIENT_FOUND_ROWS = 0x00000002, diff --git a/lib/constant/charset.ts b/lib/constant/charset.ts index 40447c9..f450832 100644 --- a/lib/constant/charset.ts +++ b/lib/constant/charset.ts @@ -1,3 +1,6 @@ +/** + * MySQL Charset + */ export enum Charset { BIG5_CHINESE_CI = 1, LATIN2_CZECH_CS = 2, diff --git a/lib/constant/mysql_types.ts b/lib/constant/mysql_types.ts index dd6a62c..997fba8 100644 --- a/lib/constant/mysql_types.ts +++ b/lib/constant/mysql_types.ts @@ -1,60 +1,35 @@ -/** @ignore */ -export const MYSQL_TYPE_DECIMAL = 0x00; -/** @ignore */ -export const MYSQL_TYPE_TINY = 0x01; -/** @ignore */ -export const MYSQL_TYPE_SHORT = 0x02; -/** @ignore */ -export const MYSQL_TYPE_LONG = 0x03; -/** @ignore */ -export const MYSQL_TYPE_FLOAT = 0x04; -/** @ignore */ -export const MYSQL_TYPE_DOUBLE = 0x05; -/** @ignore */ -export const MYSQL_TYPE_NULL = 0x06; -/** @ignore */ -export const MYSQL_TYPE_TIMESTAMP = 0x07; -/** @ignore */ -export const MYSQL_TYPE_LONGLONG = 0x08; -/** @ignore */ -export const MYSQL_TYPE_INT24 = 0x09; -/** @ignore */ -export const MYSQL_TYPE_DATE = 0x0a; -/** @ignore */ -export const MYSQL_TYPE_TIME = 0x0b; -/** @ignore */ -export const MYSQL_TYPE_DATETIME = 0x0c; -/** @ignore */ -export const MYSQL_TYPE_YEAR = 0x0d; -/** @ignore */ -export const MYSQL_TYPE_NEWDATE = 0x0e; -/** @ignore */ -export const MYSQL_TYPE_VARCHAR = 0x0f; -/** @ignore */ -export const MYSQL_TYPE_BIT = 0x10; -/** @ignore */ -export const MYSQL_TYPE_TIMESTAMP2 = 0x11; -/** @ignore */ -export const MYSQL_TYPE_DATETIME2 = 0x12; -/** @ignore */ -export const MYSQL_TYPE_TIME2 = 0x13; -/** @ignore */ -export const MYSQL_TYPE_NEWDECIMAL = 0xf6; -/** @ignore */ -export const MYSQL_TYPE_ENUM = 0xf7; -/** @ignore */ -export const MYSQL_TYPE_SET = 0xf8; -/** @ignore */ -export const MYSQL_TYPE_TINY_BLOB = 0xf9; -/** @ignore */ -export const MYSQL_TYPE_MEDIUM_BLOB = 0xfa; -/** @ignore */ -export const MYSQL_TYPE_LONG_BLOB = 0xfb; -/** @ignore */ -export const MYSQL_TYPE_BLOB = 0xfc; -/** @ignore */ -export const MYSQL_TYPE_VAR_STRING = 0xfd; -/** @ignore */ -export const MYSQL_TYPE_STRING = 0xfe; -/** @ignore */ -export const MYSQL_TYPE_GEOMETRY = 0xff; +/** + * MySQL data types + */ +export const MysqlDataType = { + Decimal: 0x00, + Tiny: 0x01, + Short: 0x02, + Long: 0x03, + Float: 0x04, + Double: 0x05, + Null: 0x06, + Timestamp: 0x07, + LongLong: 0x08, + Int24: 0x09, + Date: 0x0a, + Time: 0x0b, + DateTime: 0x0c, + Year: 0x0d, + NewDate: 0x0e, + VarChar: 0x0f, + Bit: 0x10, + Timestamp2: 0x11, + DateTime2: 0x12, + Time2: 0x13, + NewDecimal: 0xf6, + Enum: 0xf7, + Set: 0xf8, + TinyBlob: 0xf9, + MediumBlob: 0xfa, + LongBlob: 0xfb, + Blob: 0xfc, + VarString: 0xfd, + String: 0xfe, + Geometry: 0xff, +} as const; diff --git a/lib/constant/packet.ts b/lib/constant/packet.ts index 715e411..0cd49fe 100644 --- a/lib/constant/packet.ts +++ b/lib/constant/packet.ts @@ -1,4 +1,7 @@ -export enum PacketType { +/** + * PacketType + */ +export enum ComQueryResponsePacket { OK_Packet = 0x00, EOF_Packet = 0xfe, ERR_Packet = 0xff, diff --git a/lib/constant/server_status.ts b/lib/constant/server_status.ts index 146889f..d38c5d2 100644 --- a/lib/constant/server_status.ts +++ b/lib/constant/server_status.ts @@ -1,4 +1,6 @@ -/** @ignore */ +/** + * Server status flags + */ export enum ServerStatus { IN_TRANSACTION = 0x0001, AUTO_COMMIT = 0x0002, diff --git a/lib/packets/packet.ts b/lib/packets/packet.ts index 96f6ba2..8c5eb35 100644 --- a/lib/packets/packet.ts +++ b/lib/packets/packet.ts @@ -2,7 +2,7 @@ import { byteFormat } from "../util.ts"; import { BufferReader, BufferWriter } from "../buffer.ts"; import { MysqlWriteError } from "../utils/errors.ts"; import { logger } from "../logger.ts"; -import { PacketType } from "../constant/packet.ts"; +import { ComQueryResponsePacket } from "../constant/packet.ts"; /** @ignore */ interface PacketHeader { @@ -70,9 +70,13 @@ export class PacketWriter { export class PacketReader { header: PacketHeader; body: BufferReader; - type: PacketType; + type: ComQueryResponsePacket; - constructor(header: PacketHeader, body: BufferReader, type: PacketType) { + constructor( + header: PacketHeader, + body: BufferReader, + type: ComQueryResponsePacket, + ) { this.header = header; this.body = body; this.type = type; @@ -121,19 +125,19 @@ export class PacketReader { if (nread === null) return null; readCount += nread; - let type: PacketType; + let type: ComQueryResponsePacket; switch (bodyReader.buffer[0]) { - case PacketType.OK_Packet: - type = PacketType.OK_Packet; + case ComQueryResponsePacket.OK_Packet: + type = ComQueryResponsePacket.OK_Packet; break; - case PacketType.ERR_Packet: - type = PacketType.ERR_Packet; + case ComQueryResponsePacket.ERR_Packet: + type = ComQueryResponsePacket.ERR_Packet; break; - case PacketType.EOF_Packet: - type = PacketType.EOF_Packet; + case ComQueryResponsePacket.EOF_Packet: + type = ComQueryResponsePacket.EOF_Packet; break; default: - type = PacketType.Result; + type = ComQueryResponsePacket.Result; break; } diff --git a/lib/packets/parsers/handshake.ts b/lib/packets/parsers/handshake.ts index 291b84e..180ec67 100644 --- a/lib/packets/parsers/handshake.ts +++ b/lib/packets/parsers/handshake.ts @@ -1,6 +1,6 @@ import { type BufferReader, BufferWriter } from "../../buffer.ts"; import { ServerCapabilities } from "../../constant/capabilities.ts"; -import { PacketType } from "../../constant/packet.ts"; +import { ComQueryResponsePacket } from "../../constant/packet.ts"; import type { PacketReader } from "../packet.ts"; /** @ignore */ @@ -75,11 +75,11 @@ export enum AuthResult { } export function parseAuth(packet: PacketReader): AuthResult { switch (packet.type) { - case PacketType.EOF_Packet: + case ComQueryResponsePacket.EOF_Packet: return AuthResult.MethodMismatch; - case PacketType.Result: + case ComQueryResponsePacket.Result: return AuthResult.AuthMoreRequired; - case PacketType.OK_Packet: + case ComQueryResponsePacket.OK_Packet: return AuthResult.AuthPassed; default: return AuthResult.AuthPassed; diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index af1633e..24c6c88 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -1,28 +1,14 @@ import type { BufferReader } from "../../buffer.ts"; -import { - MYSQL_TYPE_DATE, - MYSQL_TYPE_DATETIME, - MYSQL_TYPE_DATETIME2, - MYSQL_TYPE_DECIMAL, - MYSQL_TYPE_DOUBLE, - MYSQL_TYPE_FLOAT, - MYSQL_TYPE_INT24, - MYSQL_TYPE_LONG, - MYSQL_TYPE_LONGLONG, - MYSQL_TYPE_NEWDATE, - MYSQL_TYPE_NEWDECIMAL, - MYSQL_TYPE_SHORT, - MYSQL_TYPE_STRING, - MYSQL_TYPE_TIME, - MYSQL_TYPE_TIME2, - MYSQL_TYPE_TIMESTAMP, - MYSQL_TYPE_TIMESTAMP2, - MYSQL_TYPE_TINY, - MYSQL_TYPE_VAR_STRING, - MYSQL_TYPE_VARCHAR, -} from "../../constant/mysql_types.ts"; +import { MysqlDataType } from "../../constant/mysql_types.ts"; +import type { ArrayRow, Row, SqlxParameterType } from "@halvardm/sqlx"; -/** @ignore */ +export type MysqlParameterType = SqlxParameterType< + string | number | bigint | Date | null +>; + +/** + * Field information + */ export interface FieldInfo { catalog: string; schema: string; @@ -38,7 +24,9 @@ export interface FieldInfo { defaultVal: string; } -/** @ignore */ +/** + * Parses the field + */ export function parseField(reader: BufferReader): FieldInfo { const catalog = reader.readLenCodeString()!; const schema = reader.readLenCodeString()!; @@ -70,16 +58,15 @@ export function parseField(reader: BufferReader): FieldInfo { }; } -/** @ignore */ -export function parseRowObject(reader: BufferReader, fields: FieldInfo[]): any { - const rowArray = parseRow(reader, fields); - return getRowObject(fields, rowArray); -} - -export function parseRow(reader: BufferReader, fields: FieldInfo[]): unknown[] { - const row: unknown[] = []; +/** + * Parse the row as an array + */ +export function parseRowArray( + reader: BufferReader, + fields: FieldInfo[], +): ArrayRow { + const row: MysqlParameterType[] = []; for (const field of fields) { - const name = field.name; const val = reader.readLenCodeString(); const parsedVal = val === null ? null : convertType(field, val); row.push(parsedVal); @@ -87,8 +74,22 @@ export function parseRow(reader: BufferReader, fields: FieldInfo[]): unknown[] { return row; } -export function getRowObject(fields: FieldInfo[], row: unknown[]): any { - const obj: any = {}; +/** + * Parses the row as an object + */ +export function parseRowObject( + reader: BufferReader, + fields: FieldInfo[], +): Row { + const rowArray = parseRowArray(reader, fields); + return getRowObject(fields, rowArray); +} + +export function getRowObject( + fields: FieldInfo[], + row: ArrayRow, +): Row { + const obj: Row = {}; for (const [i, field] of fields.entries()) { const name = field.name; obj[name] = row[i]; @@ -96,23 +97,25 @@ export function getRowObject(fields: FieldInfo[], row: unknown[]): any { return obj; } -/** @ignore */ -function convertType(field: FieldInfo, val: string): any { - const { fieldType, fieldLen } = field; +/** + * Converts the value to the correct type + */ +function convertType(field: FieldInfo, val: string): MysqlParameterType { + const { fieldType } = field; switch (fieldType) { - case MYSQL_TYPE_DECIMAL: - case MYSQL_TYPE_DOUBLE: - case MYSQL_TYPE_FLOAT: - case MYSQL_TYPE_DATETIME2: + case MysqlDataType.Decimal: + case MysqlDataType.Double: + case MysqlDataType.Float: + case MysqlDataType.DateTime2: return parseFloat(val); - case MYSQL_TYPE_NEWDECIMAL: + case MysqlDataType.NewDecimal: return val; // #42 MySQL's decimal type cannot be accurately represented by the Number. - case MYSQL_TYPE_TINY: - case MYSQL_TYPE_SHORT: - case MYSQL_TYPE_LONG: - case MYSQL_TYPE_INT24: + case MysqlDataType.Tiny: + case MysqlDataType.Short: + case MysqlDataType.Long: + case MysqlDataType.Int24: return parseInt(val); - case MYSQL_TYPE_LONGLONG: + case MysqlDataType.LongLong: if ( Number(val) < Number.MIN_SAFE_INTEGER || Number(val) > Number.MAX_SAFE_INTEGER @@ -121,18 +124,17 @@ function convertType(field: FieldInfo, val: string): any { } else { return parseInt(val); } - case MYSQL_TYPE_VARCHAR: - case MYSQL_TYPE_VAR_STRING: - case MYSQL_TYPE_STRING: - case MYSQL_TYPE_TIME: - case MYSQL_TYPE_TIME2: + case MysqlDataType.VarChar: + case MysqlDataType.VarString: + case MysqlDataType.String: + case MysqlDataType.Time: + case MysqlDataType.Time2: return val; - case MYSQL_TYPE_DATE: - case MYSQL_TYPE_TIMESTAMP: - case MYSQL_TYPE_DATETIME: - case MYSQL_TYPE_NEWDATE: - case MYSQL_TYPE_TIMESTAMP2: - case MYSQL_TYPE_DATETIME2: + case MysqlDataType.Date: + case MysqlDataType.Timestamp: + case MysqlDataType.DateTime: + case MysqlDataType.NewDate: + case MysqlDataType.Timestamp2: return new Date(val); default: return val; From 88ca32439a974546df4c3a3ddd520f3c99cd7fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 20:02:42 +0200 Subject: [PATCH 20/38] Moved and renamed auth helper --- lib/connection.ts | 2 +- lib/connection2.ts | 2 +- lib/packets/builders/auth.ts | 2 +- lib/{auth.ts => utils/hash.ts} | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) rename lib/{auth.ts => utils/hash.ts} (92%) diff --git a/lib/connection.ts b/lib/connection.ts index cc5ebfb..5a65e6e 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -23,7 +23,7 @@ import { import { ComQueryResponsePacket } from "./constant/packet.ts"; import { AuthPluginName, AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; -import auth from "./auth.ts"; +import auth from "./utils/hash.ts"; import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; import { logger } from "./logger.ts"; diff --git a/lib/connection2.ts b/lib/connection2.ts index a2f29fa..e4b621d 100644 --- a/lib/connection2.ts +++ b/lib/connection2.ts @@ -20,7 +20,7 @@ import { import { ComQueryResponsePacket } from "./constant/packet.ts"; import { AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; -import auth from "./auth.ts"; +import auth from "./utils/hash.ts"; import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; import { logger } from "./logger.ts"; diff --git a/lib/packets/builders/auth.ts b/lib/packets/builders/auth.ts index feb15ba..2c62bdf 100644 --- a/lib/packets/builders/auth.ts +++ b/lib/packets/builders/auth.ts @@ -1,4 +1,4 @@ -import auth from "../../auth.ts"; +import auth from "../../utils/hash.ts"; import { BufferWriter } from "../../buffer.ts"; import { ServerCapabilities } from "../../constant/capabilities.ts"; import { Charset } from "../../constant/charset.ts"; diff --git a/lib/auth.ts b/lib/utils/hash.ts similarity index 92% rename from lib/auth.ts rename to lib/utils/hash.ts index 2bac059..ddea298 100644 --- a/lib/auth.ts +++ b/lib/utils/hash.ts @@ -1,7 +1,7 @@ import { crypto, type DigestAlgorithm } from "@std/crypto"; -import { xor } from "./util.ts"; -import { encode } from "./buffer.ts"; -import { MysqlError } from "./utils/errors.ts"; +import { xor } from "../util.ts"; +import { encode } from "../buffer.ts"; +import { MysqlError } from "./errors.ts"; async function hash( algorithm: DigestAlgorithm, From 5a6297f5b3d7f4b868c1be45e6a6ea61680d8f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 20:09:53 +0200 Subject: [PATCH 21/38] Refactored code location and names --- .../caching_sha2_password.test.ts | 2 +- lib/auth_plugins/caching_sha2_password.ts | 2 +- lib/client.ts | 2 +- lib/connection.ts | 6 +- lib/connection2.ts | 4 +- lib/logger.ts | 12 -- lib/packets/builders/auth.ts | 2 +- lib/packets/builders/query.ts | 5 +- lib/packets/builders/tls.ts | 2 +- lib/packets/packet.ts | 10 +- lib/packets/parsers/authswitch.ts | 2 +- lib/packets/parsers/err.ts | 2 +- lib/packets/parsers/handshake.ts | 2 +- lib/packets/parsers/result.ts | 2 +- lib/pool.ts | 2 +- lib/{ => utils}/buffer.ts | 29 +--- lib/utils/bytes.test.ts | 143 ++++++++++++++++++ lib/utils/bytes.ts | 53 +++++++ lib/utils/encoding.ts | 16 ++ lib/utils/hash.ts | 4 +- lib/utils/meta.ts | 4 + lib/{util.ts => utils/query.ts} | 62 -------- lib/utils/testing.ts | 6 + 23 files changed, 256 insertions(+), 118 deletions(-) delete mode 100644 lib/logger.ts rename lib/{ => utils}/buffer.ts (87%) create mode 100644 lib/utils/bytes.test.ts create mode 100644 lib/utils/bytes.ts create mode 100644 lib/utils/encoding.ts create mode 100644 lib/utils/meta.ts rename lib/{util.ts => utils/query.ts} (64%) create mode 100644 lib/utils/testing.ts diff --git a/lib/auth_plugins/caching_sha2_password.test.ts b/lib/auth_plugins/caching_sha2_password.test.ts index ee7c505..dac791a 100644 --- a/lib/auth_plugins/caching_sha2_password.test.ts +++ b/lib/auth_plugins/caching_sha2_password.test.ts @@ -5,7 +5,7 @@ import { AuthStatusFlags, } from "./caching_sha2_password.ts"; import { ComQueryResponsePacket } from "../constant/packet.ts"; -import { BufferReader } from "../buffer.ts"; +import { BufferReader } from "../utils/buffer.ts"; Deno.test("AuthPluginCachingSha2Password", async (t) => { await t.step("statusFlag FastPath", async () => { diff --git a/lib/auth_plugins/caching_sha2_password.ts b/lib/auth_plugins/caching_sha2_password.ts index 3e64567..9a35c2b 100644 --- a/lib/auth_plugins/caching_sha2_password.ts +++ b/lib/auth_plugins/caching_sha2_password.ts @@ -1,4 +1,4 @@ -import { xor } from "../util.ts"; +import { xor } from "../utils/bytes.ts"; import type { PacketReader } from "../packets/packet.ts"; import { encryptWithPublicKey } from "../utils/crypto.ts"; diff --git a/lib/client.ts b/lib/client.ts index a6338ec..a6de1f4 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -4,7 +4,7 @@ import { type ExecuteResult, } from "./connection.ts"; import { ConnectionPool, PoolConnection } from "./pool.ts"; -import { logger } from "./logger.ts"; +import { logger } from "./utils/logger.ts"; import { MysqlError } from "./utils/errors.ts"; /** diff --git a/lib/connection.ts b/lib/connection.ts index 5a65e6e..1d375d6 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -26,7 +26,7 @@ import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./utils/hash.ts"; import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; -import { logger } from "./logger.ts"; +import { logger } from "./utils/logger.ts"; /** * Connection state @@ -82,6 +82,10 @@ export class Connection { : `${this.config.hostname}:${this.config.port}`; } + get isMariaDB(): boolean { + return this.serverVersion.includes("MariaDB"); + } + constructor(readonly config: ClientConfig) {} private async _connect() { diff --git a/lib/connection2.ts b/lib/connection2.ts index e4b621d..cc041e9 100644 --- a/lib/connection2.ts +++ b/lib/connection2.ts @@ -23,14 +23,14 @@ import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./utils/hash.ts"; import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; -import { logger } from "./logger.ts"; +import { logger } from "./utils/logger.ts"; import type { ArrayRow, SqlxConnectable, SqlxConnectionOptions, SqlxParameterType, } from "@halvardm/sqlx"; -import { VERSION } from "./util.ts"; +import { VERSION } from "./utils/meta.ts"; import { resolve } from "@std/path"; import { toCamelCase } from "@std/text"; import { AuthPluginName } from "./auth_plugins/mod.ts"; diff --git a/lib/logger.ts b/lib/logger.ts deleted file mode 100644 index 82c2c47..0000000 --- a/lib/logger.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { getLogger } from "@std/log"; -import { MODULE_NAME } from "./util.ts"; - -/** - * Used for internal module logging, - * do not import this directly outside of this module. - * - * @see {@link https://deno.land/std/log/mod.ts} - */ -export function logger() { - return getLogger(MODULE_NAME); -} diff --git a/lib/packets/builders/auth.ts b/lib/packets/builders/auth.ts index 2c62bdf..3cff7ac 100644 --- a/lib/packets/builders/auth.ts +++ b/lib/packets/builders/auth.ts @@ -1,5 +1,5 @@ import auth from "../../utils/hash.ts"; -import { BufferWriter } from "../../buffer.ts"; +import { BufferWriter } from "../../utils/buffer.ts"; import { ServerCapabilities } from "../../constant/capabilities.ts"; import { Charset } from "../../constant/charset.ts"; import type { HandshakeBody } from "../parsers/handshake.ts"; diff --git a/lib/packets/builders/query.ts b/lib/packets/builders/query.ts index c310702..ed592bd 100644 --- a/lib/packets/builders/query.ts +++ b/lib/packets/builders/query.ts @@ -1,5 +1,6 @@ -import { replaceParams } from "../../util.ts"; -import { BufferWriter, encode } from "../../buffer.ts"; +import { replaceParams } from "../../utils/query.ts"; +import { BufferWriter } from "../../utils/buffer.ts"; +import { encode } from "../../utils/encoding.ts"; /** @ignore */ export function buildQuery(sql: string, params: any[] = []): Uint8Array { diff --git a/lib/packets/builders/tls.ts b/lib/packets/builders/tls.ts index 487301a..5963c01 100644 --- a/lib/packets/builders/tls.ts +++ b/lib/packets/builders/tls.ts @@ -1,4 +1,4 @@ -import { BufferWriter } from "../../buffer.ts"; +import { BufferWriter } from "../../utils/buffer.ts"; import { Charset } from "../../constant/charset.ts"; import type { HandshakeBody } from "../parsers/handshake.ts"; import { clientCapabilities } from "./client_capabilities.ts"; diff --git a/lib/packets/packet.ts b/lib/packets/packet.ts index 8c5eb35..0f938dd 100644 --- a/lib/packets/packet.ts +++ b/lib/packets/packet.ts @@ -1,7 +1,7 @@ -import { byteFormat } from "../util.ts"; -import { BufferReader, BufferWriter } from "../buffer.ts"; +import { hexdump } from "../utils/bytes.ts"; +import { BufferReader, BufferWriter } from "../utils/buffer.ts"; import { MysqlWriteError } from "../utils/errors.ts"; -import { logger } from "../logger.ts"; +import { logger } from "../utils/logger.ts"; import { ComQueryResponsePacket } from "../constant/packet.ts"; /** @ignore */ @@ -34,7 +34,7 @@ export class PacketWriter { data.writeUints(3, this.header.size); data.write(this.header.no); data.writeBuffer(body); - logger().debug(`send: ${data.length}B \n${byteFormat(data.buffer)}\n`); + logger().debug(`send: ${data.length}B \n${hexdump(data.buffer)}\n`); try { let wrote = 0; do { @@ -146,7 +146,7 @@ export class PacketReader { data.set(headerReader.buffer); data.set(bodyReader.buffer, 4); return `receive: ${readCount}B, size = ${header.size}, no = ${header.no} \n${ - byteFormat(data) + hexdump(data) }\n`; }); diff --git a/lib/packets/parsers/authswitch.ts b/lib/packets/parsers/authswitch.ts index 5c25968..698b186 100644 --- a/lib/packets/parsers/authswitch.ts +++ b/lib/packets/parsers/authswitch.ts @@ -1,4 +1,4 @@ -import type { BufferReader } from "../../buffer.ts"; +import type { BufferReader } from "../../utils/buffer.ts"; /** @ignore */ export interface authSwitchBody { diff --git a/lib/packets/parsers/err.ts b/lib/packets/parsers/err.ts index 114b78c..448405c 100644 --- a/lib/packets/parsers/err.ts +++ b/lib/packets/parsers/err.ts @@ -1,4 +1,4 @@ -import type { BufferReader } from "../../buffer.ts"; +import type { BufferReader } from "../../utils/buffer.ts"; import type { Connection } from "../../connection.ts"; import { ServerCapabilities } from "../../constant/capabilities.ts"; diff --git a/lib/packets/parsers/handshake.ts b/lib/packets/parsers/handshake.ts index 180ec67..c89ef1a 100644 --- a/lib/packets/parsers/handshake.ts +++ b/lib/packets/parsers/handshake.ts @@ -1,4 +1,4 @@ -import { type BufferReader, BufferWriter } from "../../buffer.ts"; +import { type BufferReader, BufferWriter } from "../../utils/buffer.ts"; import { ServerCapabilities } from "../../constant/capabilities.ts"; import { ComQueryResponsePacket } from "../../constant/packet.ts"; import type { PacketReader } from "../packet.ts"; diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index 24c6c88..08ce798 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -1,4 +1,4 @@ -import type { BufferReader } from "../../buffer.ts"; +import type { BufferReader } from "../../utils/buffer.ts"; import { MysqlDataType } from "../../constant/mysql_types.ts"; import type { ArrayRow, Row, SqlxParameterType } from "@halvardm/sqlx"; diff --git a/lib/pool.ts b/lib/pool.ts index e359b55..c4a38e6 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -1,6 +1,6 @@ import { DeferredStack } from "./deferred.ts"; import { Connection } from "./connection.ts"; -import { logger } from "./logger.ts"; +import { logger } from "./utils/logger.ts"; import { MysqlError } from "./utils/errors.ts"; /** @ignore */ diff --git a/lib/buffer.ts b/lib/utils/buffer.ts similarity index 87% rename from lib/buffer.ts rename to lib/utils/buffer.ts index 5c3e48b..d1ab5ed 100644 --- a/lib/buffer.ts +++ b/lib/utils/buffer.ts @@ -1,17 +1,8 @@ -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); +import { decode, encode } from "./encoding.ts"; -/** @ignore */ -export function encode(input: string) { - return encoder.encode(input); -} - -/** @ignore */ -export function decode(input: BufferSource) { - return decoder.decode(input); -} - -/** @ignore */ +/** + * Buffer reader utility class + */ export class BufferReader { private pos: number = 0; constructor(readonly buffer: Uint8Array) {} @@ -96,7 +87,9 @@ export class BufferReader { } } -/** @ignore */ +/** + * Buffer writer utility class + */ export class BufferWriter { private pos: number = 0; constructor(readonly buffer: Uint8Array) {} @@ -132,14 +125,6 @@ export class BufferWriter { return this; } - writeInt16LE(num: number) {} - - writeIntLE(num: number, len: number) { - const int = new Int32Array(1); - int[0] = 40; - console.log(int); - } - writeUint16(num: number): BufferWriter { return this.writeUints(2, num); } diff --git a/lib/utils/bytes.test.ts b/lib/utils/bytes.test.ts new file mode 100644 index 0000000..347865b --- /dev/null +++ b/lib/utils/bytes.test.ts @@ -0,0 +1,143 @@ +import { assertEquals } from "@std/assert"; +import { hexdump } from "./bytes.ts"; + +Deno.test("hexdump", async (t) => { + const data = + "This is a test string that is longer than 16 bytes and will be split into multiple lines for the hexdump. The quick brown fox jumps over the lazy dog. Foo bar baz."; + const buffer8Compatible = new TextEncoder().encode(data); + + function bufferPad(buffer: ArrayBufferView, multipleOf: number): number[] { + const bufferLength = buffer.byteLength; + const remainder = Math.ceil(bufferLength / multipleOf); + const padCeil = remainder * multipleOf; + const missing = padCeil - bufferLength; + + const result = []; + for (let i = 0; i < missing; i++) { + result.push(0); + } + + return result; + } + + const buffer16Compatible = + new Uint8Array([...buffer8Compatible, ...bufferPad(buffer8Compatible, 2)]) + .buffer; + const buffer32Compatible = + new Uint8Array([...buffer8Compatible, ...bufferPad(buffer8Compatible, 4)]) + .buffer; + const buffer64Compatible = + new Uint8Array([...buffer8Compatible, ...bufferPad(buffer8Compatible, 8)]) + .buffer; + + const buffer8Result = + `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| +00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| +00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| +00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| +00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| +00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| +00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| +00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| +00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| +00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| +000000a0 61 7a 2e |az.|`; + + const buffer16Result = + `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| +00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| +00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| +00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| +00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| +00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| +00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| +00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| +00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| +00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| +000000a0 61 7a 2e 00 |az..|`; + + const buffer32Result = + `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| +00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| +00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| +00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| +00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| +00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| +00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| +00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| +00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| +00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| +000000a0 61 7a 2e 00 |az..|`; + const buffer64Result = + `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| +00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| +00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| +00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| +00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| +00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| +00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| +00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| +00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| +00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| +000000a0 61 7a 2e 00 00 00 00 00 |az......|`; + + await t.step("Uint8Array", () => { + const result = hexdump(buffer8Compatible); + assertEquals(result, buffer8Result); + }); + + await t.step("Uint16Array", () => { + const result = hexdump(new Uint16Array(buffer16Compatible)); + assertEquals(result, buffer16Result); + }); + + await t.step("Uint32Array", () => { + const result = hexdump(new Uint32Array(buffer32Compatible)); + assertEquals(result, buffer32Result); + }); + + await t.step("Uint8ClampedArray", () => { + const result = hexdump(new Uint8ClampedArray(buffer8Compatible.buffer)); + assertEquals(result, buffer8Result); + }); + + await t.step("Int8Array", () => { + const result = hexdump(new Int8Array(buffer8Compatible.buffer)); + assertEquals(result, buffer8Result); + }); + + await t.step("Int16Array", () => { + const result = hexdump(new Int16Array(buffer16Compatible)); + assertEquals(result, buffer16Result); + }); + + await t.step("Int32Array", () => { + const result = hexdump(new Int32Array(buffer32Compatible)); + assertEquals(result, buffer32Result); + }); + + await t.step("Float32Array", () => { + const result = hexdump(new Float32Array(buffer32Compatible)); + assertEquals(result, buffer32Result); + }); + + await t.step("Float64Array", () => { + const result = hexdump(new Float64Array(buffer64Compatible)); + assertEquals(result, buffer64Result); + }); + + await t.step("BigInt64Array", () => { + const result = hexdump(new BigInt64Array(buffer64Compatible)); + assertEquals(result, buffer64Result); + }); + + await t.step("BigUint64Array", () => { + const result = hexdump(new BigUint64Array(buffer64Compatible)); + assertEquals(result, buffer64Result); + }); + + await t.step("DataView", () => { + const result = hexdump(new DataView(buffer8Compatible.buffer)); + assertEquals(result, buffer8Result); + }); +}); diff --git a/lib/utils/bytes.ts b/lib/utils/bytes.ts new file mode 100644 index 0000000..87f644c --- /dev/null +++ b/lib/utils/bytes.ts @@ -0,0 +1,53 @@ +/** + * Convert a buffer to a hexdump string. + * + * @example + * ```ts + * const buffer = new TextEncoder().encode("The quick brown fox jumps over the lazy dog."); + * console.log(hexdump(buffer)); + * // 00000000 54 68 65 20 71 75 69 63 6b 20 62 72 6f 77 6e 20 |The quick brown | + * // 00000010 66 6f 78 20 6a 75 6d 70 73 20 6f 76 65 72 20 74 |fox jumps over t| + * // 00000020 68 65 20 6c 61 7a 79 20 64 6f 67 2e |he lazy dog.| + * ``` + */ +export function hexdump(bufferView: ArrayBufferView): string { + const bytes = new Uint8Array(bufferView.buffer); + const lines = []; + + for (let i = 0; i < bytes.length; i += 16) { + const address = i.toString(16).padStart(8, "0"); + const block = bytes.slice(i, i + 16); // cut buffer into blocks of 16 + const hexArray = []; + const asciiArray = []; + let padding = ""; + + for (const value of block) { + hexArray.push(value.toString(16).padStart(2, "0")); + asciiArray.push( + value >= 0x20 && value < 0x7f ? String.fromCharCode(value) : ".", + ); + } + + if (hexArray.length < 16) { + const space = 16 - hexArray.length; + padding = " ".repeat(space * 2 + space + (hexArray.length < 9 ? 1 : 0)); + } + + const hexString = hexArray.length > 8 + ? hexArray.slice(0, 8).join(" ") + " " + hexArray.slice(8).join(" ") + : hexArray.join(" "); + + const asciiString = asciiArray.join(""); + const line = `${address} ${hexString} ${padding}|${asciiString}|`; + + lines.push(line); + } + + return lines.join("\n"); +} + +export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { + return a.map((byte, index) => { + return byte ^ b[index]; + }); +} diff --git a/lib/utils/encoding.ts b/lib/utils/encoding.ts new file mode 100644 index 0000000..c535c0a --- /dev/null +++ b/lib/utils/encoding.ts @@ -0,0 +1,16 @@ +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +/** + * Shorthand for `new TextEncoder().encode(input)`. + */ +export function encode(input: string) { + return encoder.encode(input); +} + +/** + * Shorthand for `new TextDecoder().decode(input)`. + */ +export function decode(input: BufferSource) { + return decoder.decode(input); +} diff --git a/lib/utils/hash.ts b/lib/utils/hash.ts index ddea298..b3f8aa5 100644 --- a/lib/utils/hash.ts +++ b/lib/utils/hash.ts @@ -1,7 +1,7 @@ import { crypto, type DigestAlgorithm } from "@std/crypto"; -import { xor } from "../util.ts"; -import { encode } from "../buffer.ts"; +import { xor } from "./bytes.ts"; import { MysqlError } from "./errors.ts"; +import { encode } from "./encoding.ts"; async function hash( algorithm: DigestAlgorithm, diff --git a/lib/utils/meta.ts b/lib/utils/meta.ts new file mode 100644 index 0000000..4b71791 --- /dev/null +++ b/lib/utils/meta.ts @@ -0,0 +1,4 @@ +import meta from "../../deno.json" with { type: "json" }; + +export const MODULE_NAME = meta.name; +export const VERSION = meta.version; diff --git a/lib/util.ts b/lib/utils/query.ts similarity index 64% rename from lib/util.ts rename to lib/utils/query.ts index 5025a1a..896a565 100644 --- a/lib/util.ts +++ b/lib/utils/query.ts @@ -1,65 +1,3 @@ -import { green } from "@std/fmt/colors"; -import meta from "../deno.json" with { type: "json" }; - -export const MODULE_NAME = meta.name; -export const VERSION = meta.version; - -export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { - return a.map((byte, index) => { - return byte ^ b[index]; - }); -} - -/** - * Formats a byte array into a human-readable hexdump. - * - * Taken from https://github.com/manyuanrong/bytes_formater/blob/master/format.ts - */ -export function byteFormat(data: ArrayBufferView) { - const bytes = new Uint8Array(data.buffer); - let out = " +-------------------------------------------------+\n"; - out += ` |${ - green(" 0 1 2 3 4 5 6 7 8 9 a b c d e f ") - }|\n`; - out += - "+--------+-------------------------------------------------+----------------+\n"; - - const lineCount = Math.ceil(bytes.length / 16); - - for (let line = 0; line < lineCount; line++) { - const start = line * 16; - const addr = start.toString(16).padStart(8, "0"); - const lineBytes = bytes.slice(start, start + 16); - - out += `|${green(addr)}| `; - - lineBytes.forEach( - (byte) => (out += byte.toString(16).padStart(2, "0") + " "), - ); - - if (lineBytes.length < 16) { - out += " ".repeat(16 - lineBytes.length); - } - - out += "|"; - - lineBytes.forEach(function (byte) { - return (out += byte > 31 && byte < 127 - ? green(String.fromCharCode(byte)) - : "."); - }); - - if (lineBytes.length < 16) { - out += " ".repeat(16 - lineBytes.length); - } - - out += "|\n"; - } - out += - "+--------+-------------------------------------------------+----------------+"; - return out; -} - /** * Replaces parameters in a SQL query with the given values. * diff --git a/lib/utils/testing.ts b/lib/utils/testing.ts new file mode 100644 index 0000000..a33981d --- /dev/null +++ b/lib/utils/testing.ts @@ -0,0 +1,6 @@ +import { resolve } from "@std/path"; + +export const DIR_TMP_TEST = resolve("tmp_test"); + +//socket "/var/run/mysqld/mysqld.sock"; +export const URL_TEST_CONNECTION = Deno.env.get("DENO_MYSQL_CONNECTION_URL")||"mysql://root@0.0.0.0:3306/testdb" From 51c26f1369f91daf34dd93a6e02240c77a5441b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 20:16:20 +0200 Subject: [PATCH 22/38] Refactored tests --- deno.json | 1 + lib/client.test.ts | 25 ++++++++++ lib/connection.test.ts | 2 +- test.util.ts | 101 ----------------------------------------- test.ts => testold.ts | 2 +- 5 files changed, 28 insertions(+), 103 deletions(-) create mode 100644 lib/client.test.ts delete mode 100644 test.util.ts rename test.ts => testold.ts (99%) diff --git a/deno.json b/deno.json index 1fbe418..04ccd24 100644 --- a/deno.json +++ b/deno.json @@ -17,6 +17,7 @@ }, "imports": { "@halvardm/sqlx": "../deno-sqlx/mod.ts", + "@halvardm/sqlx/testing": "../deno-sqlx/lib/testing.ts", "@std/assert": "jsr:@std/assert@^0.221.0", "@std/async": "jsr:@std/async@^0.221.0", "@std/crypto": "jsr:@std/crypto@^0.221.0", diff --git a/lib/client.test.ts b/lib/client.test.ts new file mode 100644 index 0000000..f861732 --- /dev/null +++ b/lib/client.test.ts @@ -0,0 +1,25 @@ +// import { MysqlClient } from "./client.ts"; +// import { URL_TEST_CONNECTION } from "./utils/testing.ts"; +// import { implementationTest } from "@halvardm/sqlx/testing"; + +// Deno.test("MysqlClient", async (t) => { +// await implementationTest({ +// t, +// Client: MysqlClient, +// connectionUrl: URL_TEST_CONNECTION, +// connectionOptions: {}, +// queries:{ +// createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", +// dropTable: "DROP TABLE IF EXISTS sqlxtesttable", +// insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", +// insertManyToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", +// selectOneFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", +// selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", +// selectManyFromTable: "SELECT * FROM sqlxtesttable", +// select1AsString: "SELECT '1' as result", +// select1Plus1AsNumber: "SELECT 1+1 as result", +// deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", +// deleteAllFromTable: "DELETE FROM sqlxtesttable", +// } +// }); +// }); diff --git a/lib/connection.test.ts b/lib/connection.test.ts index a41f949..be25f0a 100644 --- a/lib/connection.test.ts +++ b/lib/connection.test.ts @@ -2,7 +2,7 @@ import { assertEquals, assertInstanceOf } from "@std/assert"; import { emptyDir } from "@std/fs"; import { join } from "@std/path"; import { MysqlConnection } from "./connection2.ts"; -import { DIR_TMP_TEST } from "../test.util.ts"; +import { DIR_TMP_TEST } from "./utils/testing.ts"; import { buildQuery } from "./packets/builders/query.ts"; Deno.test("Connection", async (t) => { diff --git a/test.util.ts b/test.util.ts deleted file mode 100644 index 5f56f1f..0000000 --- a/test.util.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { resolve } from "@std/path"; -import { Client, type ClientConfig, type Connection } from "./mod.ts"; -import { assertEquals } from "@std/assert"; - -export const DIR_TMP_TEST = resolve("tmp_test"); - -const { DB_PORT, DB_NAME, DB_PASSWORD, DB_USER, DB_HOST, DB_SOCKPATH } = Deno - .env.toObject(); -const port = DB_PORT ? parseInt(DB_PORT) : 3306; -const db = DB_NAME || "test"; -const password = DB_PASSWORD || "root"; -const username = DB_USER || "root"; -const hostname = DB_HOST || "127.0.0.1"; -const sockPath = DB_SOCKPATH || "/var/run/mysqld/mysqld.sock"; -const testMethods = - Deno.env.get("TEST_METHODS")?.split(",") as ("tcp" | "unix")[] || ["tcp"]; -const unixSocketOnly = testMethods.length === 1 && testMethods[0] === "unix"; - -const config: ClientConfig = { - timeout: 10000, - poolSize: 3, - debug: true, - hostname, - username, - port, - db, - charset: "utf8mb4", - password, -}; - -const tests: (Parameters)[] = []; - -export function testWithClient( - fn: (client: Client) => void | Promise, - overrideConfig?: ClientConfig, -): void { - tests.push([fn, overrideConfig]); -} - -export function registerTests(methods: ("tcp" | "unix")[] = testMethods) { - if (methods!.includes("tcp")) { - tests.forEach(([fn, overrideConfig]) => { - Deno.test({ - name: fn.name + " (TCP)", - async fn() { - await test({ ...config, ...overrideConfig }, fn); - }, - }); - }); - } - if (methods!.includes("unix")) { - tests.forEach(([fn, overrideConfig]) => { - Deno.test({ - name: fn.name + " (UNIX domain socket)", - async fn() { - await test( - { ...config, socketPath: sockPath, ...overrideConfig }, - fn, - ); - }, - }); - }); - } -} - -async function test( - config: ClientConfig, - fn: (client: Client) => void | Promise, -) { - const resources = Deno.resources(); - const client = await new Client().connect(config); - try { - await fn(client); - } finally { - await client.close(); - } - assertEquals( - Deno.resources(), - resources, - "The client is leaking resources", - ); -} - -export async function createTestDB() { - const client = await new Client().connect({ - ...config, - poolSize: 1, - db: undefined, - socketPath: unixSocketOnly ? sockPath : undefined, - }); - await client.execute(`CREATE DATABASE IF NOT EXISTS ${db}`); - await client.close(); -} - -export function isMariaDB(connection: Connection): boolean { - return connection.serverVersion.includes("MariaDB"); -} - -export function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/test.ts b/testold.ts similarity index 99% rename from test.ts rename to testold.ts index 72c9772..9463348 100644 --- a/test.ts +++ b/testold.ts @@ -10,7 +10,7 @@ import { isMariaDB, registerTests, testWithClient, -} from "./test.util.ts"; +} from "./lib/utils/testing.ts"; import * as stdlog from "@std/log"; import { configLogger } from "./mod.ts"; import { logger } from "./lib/logger.ts"; From 62225bd29a4bd25dd1f7533e9fea300b8d4b52ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 14 Apr 2024 20:16:39 +0200 Subject: [PATCH 23/38] fmt --- lib/utils/testing.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/testing.ts b/lib/utils/testing.ts index a33981d..e5de69f 100644 --- a/lib/utils/testing.ts +++ b/lib/utils/testing.ts @@ -3,4 +3,5 @@ import { resolve } from "@std/path"; export const DIR_TMP_TEST = resolve("tmp_test"); //socket "/var/run/mysqld/mysqld.sock"; -export const URL_TEST_CONNECTION = Deno.env.get("DENO_MYSQL_CONNECTION_URL")||"mysql://root@0.0.0.0:3306/testdb" +export const URL_TEST_CONNECTION = Deno.env.get("DENO_MYSQL_CONNECTION_URL") || + "mysql://root@0.0.0.0:3306/testdb"; From 4ce8069850d6560d2ed0b0895fe66306c432bf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Mon, 15 Apr 2024 11:48:12 +0200 Subject: [PATCH 24/38] Refactored client and improved connection --- lib/client.test.ts | 50 +-- lib/client.ts | 522 +++++++++++++++++++++--------- lib/connection.test.ts | 252 ++++++++++++--- lib/connection.ts | 499 +++++++++++++++++++--------- lib/connection2.ts | 590 ---------------------------------- lib/packets/builders/query.ts | 6 +- lib/packets/parsers/err.ts | 4 +- lib/packets/parsers/result.ts | 22 +- lib/utils/logger.ts | 19 +- lib/utils/query.ts | 15 +- 10 files changed, 1005 insertions(+), 974 deletions(-) delete mode 100644 lib/connection2.ts diff --git a/lib/client.test.ts b/lib/client.test.ts index f861732..2508467 100644 --- a/lib/client.test.ts +++ b/lib/client.test.ts @@ -1,25 +1,27 @@ -// import { MysqlClient } from "./client.ts"; -// import { URL_TEST_CONNECTION } from "./utils/testing.ts"; -// import { implementationTest } from "@halvardm/sqlx/testing"; +import { MysqlClient } from "./client.ts"; +import { URL_TEST_CONNECTION } from "./utils/testing.ts"; +import { implementationTest } from "@halvardm/sqlx/testing"; -// Deno.test("MysqlClient", async (t) => { -// await implementationTest({ -// t, -// Client: MysqlClient, -// connectionUrl: URL_TEST_CONNECTION, -// connectionOptions: {}, -// queries:{ -// createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", -// dropTable: "DROP TABLE IF EXISTS sqlxtesttable", -// insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", -// insertManyToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", -// selectOneFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", -// selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", -// selectManyFromTable: "SELECT * FROM sqlxtesttable", -// select1AsString: "SELECT '1' as result", -// select1Plus1AsNumber: "SELECT 1+1 as result", -// deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", -// deleteAllFromTable: "DELETE FROM sqlxtesttable", -// } -// }); -// }); +Deno.test("MysqlClient", async (t) => { + await implementationTest({ + t, + Client: MysqlClient, + connectionUrl: URL_TEST_CONNECTION, + connectionOptions: {}, + queries: { + createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", + dropTable: "DROP TABLE IF EXISTS sqlxtesttable", + insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", + insertManyToTable: + "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", + selectOneFromTable: + "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", + selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", + selectManyFromTable: "SELECT * FROM sqlxtesttable", + select1AsString: "SELECT '1' as result", + select1Plus1AsNumber: "SELECT 1+1 as result", + deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", + deleteAllFromTable: "DELETE FROM sqlxtesttable", + }, + }); +}); diff --git a/lib/client.ts b/lib/client.ts index a6de1f4..6779730 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,170 +1,392 @@ import { - type Connection, - ConnectionState, - type ExecuteResult, -} from "./connection.ts"; -import { ConnectionPool, PoolConnection } from "./pool.ts"; -import { logger } from "./utils/logger.ts"; -import { MysqlError } from "./utils/errors.ts"; + type ArrayRow, + type Row, + type SqlxConnection, + SqlxConnectionCloseEvent, + SqlxConnectionConnectEvent, + type SqlxConnectionEventType, + type SqlxPreparable, + type SqlxPreparedQueriable, + type SqlxQueriable, + type SqlxQueryOptions, + type SqlxTransactionable, + type SqlxTransactionOptions, + type SqlxTransactionQueriable, + VERSION, +} from "@halvardm/sqlx"; +import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; +import { buildQuery } from "./packets/builders/query.ts"; +import { + getRowObject, + type MysqlParameterType, +} from "./packets/parsers/result.ts"; -/** - * Client Config - */ -export interface ClientConfig { - /** Database hostname */ - hostname?: string; - /** Database UNIX domain socket path. When used, `hostname` and `port` are ignored. */ - socketPath?: string; - /** Database username */ - username?: string; - /** Database password */ - password?: string; - /** Database port */ - port?: number; - /** Database name */ - db?: string; - /** Whether to display packet debugging information */ - debug?: boolean; - /** Connection read timeout (default: 30 seconds) */ - timeout?: number; - /** Connection pool size (default: 1) */ - poolSize?: number; - /** Connection pool idle timeout in microseconds (default: 4 hours) */ - idleTimeout?: number; - /** charset */ - charset?: string; - /** tls config */ - tls?: TLSConfig; +export interface MysqlTransactionOptions extends SqlxTransactionOptions { + beginTransactionOptions: { + withConsistentSnapshot?: boolean; + readWrite?: "READ WRITE" | "READ ONLY"; + }; + commitTransactionOptions: { + chain?: boolean; + release?: boolean; + }; + rollbackTransactionOptions: { + chain?: boolean; + release?: boolean; + savepoint?: string; + }; +} + +export interface MysqlClientOptions extends MysqlConnectionOptions { } -export enum TLSMode { - DISABLED = "disabled", - VERIFY_IDENTITY = "verify_identity", +export interface MysqlQueryOptions extends SqlxQueryOptions { } + /** - * TLS Config + * Prepared statement + * + * @todo implement prepared statements properly */ -export interface TLSConfig { - /** mode of tls. only support disabled and verify_identity now*/ - mode?: TLSMode; - /** A list of root certificates (must be PEM format) that will be used in addition to the - * default root certificates to verify the peer's certificate. */ - caCerts?: string[]; +export class MysqlPrepared + implements SqlxPreparedQueriable { + readonly sqlxVersion = VERSION; + readonly queryOptions: MysqlQueryOptions; + + #sql: string; + + #queriable: MysqlQueriable; + + constructor( + connection: MysqlConnection, + sql: string, + options: MysqlQueryOptions = {}, + ) { + this.#queriable = new MysqlQueriable(connection); + this.#sql = sql; + this.queryOptions = options; + } + + execute( + params?: MysqlParameterType[] | undefined, + _options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.execute(this.#sql, params); + } + query = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.query(this.#sql, params, options); + } + queryOne = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryOne(this.#sql, params, options); + } + queryMany = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncIterableIterator { + return this.#queriable.queryMany(this.#sql, params, options); + } + queryArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryArray(this.#sql, params, options); + } + queryOneArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryOneArray(this.#sql, params, options); + } + queryManyArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncIterableIterator { + return this.#queriable.queryManyArray(this.#sql, params, options); + } +} + +export class MysqlQueriable + implements SqlxQueriable { + protected readonly connection: MysqlConnection; + readonly queryOptions: MysqlQueryOptions; + readonly sqlxVersion: string = VERSION; + + constructor( + connection: MysqlConnection, + queryOptions: MysqlQueryOptions = {}, + ) { + this.connection = connection; + this.queryOptions = queryOptions; + } + + execute( + sql: string, + params?: MysqlParameterType[] | undefined, + _options?: MysqlQueryOptions | undefined, + ): Promise { + const data = buildQuery(sql, params); + return this.connection.executeRaw(data); + } + query = Row>( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryMany(sql, params, options)); + } + async queryOne = Row>( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + const res = await this.query(sql, params, options); + return res[0]; + } + async *queryMany = Row>( + sql: string, + params?: MysqlParameterType[], + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + const data = buildQuery(sql, params); + for await ( + const res of this.connection.queryManyObjectRaw(data, options) + ) { + yield res; + } + } + + queryArray< + T extends ArrayRow = ArrayRow, + >( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryManyArray(sql, params, options)); + } + async queryOneArray< + T extends ArrayRow = ArrayRow, + >( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + const res = await this.queryArray(sql, params, options); + return res[0]; + } + async *queryManyArray< + T extends ArrayRow = ArrayRow, + >( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncIterableIterator { + const data = buildQuery(sql, params); + for await ( + const res of this.connection.queryManyArrayRaw(data, options) + ) { + yield res; + } + } + sql = Row>( + strings: TemplateStringsArray, + ...parameters: MysqlParameterType[] + ): Promise { + return this.query(strings.join("?"), parameters); + } + sqlArray< + T extends ArrayRow = ArrayRow, + >( + strings: TemplateStringsArray, + ...parameters: MysqlParameterType[] + ): Promise { + return this.queryArray(strings.join("?"), parameters); + } +} + +export class MysqlPreparable extends MysqlQueriable + implements + SqlxPreparable { + prepare(sql: string, options?: MysqlQueryOptions | undefined): MysqlPrepared { + return new MysqlPrepared(this.connection, sql, options); + } } -/** Transaction processor */ -export interface TransactionProcessor { - (connection: Connection): Promise; +export class MySqlTransaction extends MysqlPreparable + implements + SqlxTransactionQueriable< + MysqlParameterType, + MysqlQueryOptions, + MysqlTransactionOptions + > { + async commitTransaction( + options?: MysqlTransactionOptions["commitTransactionOptions"], + ): Promise { + let sql = "COMMIT"; + + if (options?.chain === true) { + sql += " AND CHAIN"; + } else if (options?.chain === false) { + sql += " AND NO CHAIN"; + } + + if (options?.release === true) { + sql += " RELEASE"; + } else if (options?.release === false) { + sql += " NO RELEASE"; + } + await this.execute(sql); + } + async rollbackTransaction( + options?: MysqlTransactionOptions["rollbackTransactionOptions"], + ): Promise { + let sql = "ROLLBACK"; + + if (options?.savepoint) { + sql += ` TO ${options.savepoint}`; + await this.execute(sql); + return; + } + + if (options?.chain === true) { + sql += " AND CHAIN"; + } else if (options?.chain === false) { + sql += " AND NO CHAIN"; + } + + if (options?.release === true) { + sql += " RELEASE"; + } else if (options?.release === false) { + sql += " NO RELEASE"; + } + + await this.execute(sql); + } + async createSavepoint(name: string = `\t_bm.\t`): Promise { + await this.execute(`SAVEPOINT ${name}`); + } + async releaseSavepoint(name: string = `\t_bm.\t`): Promise { + await this.execute(`RELEASE SAVEPOINT ${name}`); + } } /** - * MySQL client + * Represents a queriable class that can be used to run transactions. */ -export class Client { - config: ClientConfig = {}; - private _pool?: ConnectionPool; - - private async createConnection(): Promise { - let connection = new PoolConnection(this.config); - await connection.connect(); - return connection; - } - - /** get pool info */ - get pool() { - return this._pool?.info; - } - - /** - * connect to database - * @param config config for client - * @returns Client instance - */ - async connect(config: ClientConfig): Promise { - this.config = { - hostname: "127.0.0.1", - username: "root", - port: 3306, - poolSize: 1, - timeout: 30 * 1000, - idleTimeout: 4 * 3600 * 1000, - ...config, - }; - Object.freeze(this.config); - this._pool = new ConnectionPool( - this.config.poolSize || 10, - this.createConnection.bind(this), - ); - return this; - } - - /** - * execute query sql - * @param sql query sql string - * @param params query params - */ - async query(sql: string, params?: any[]): Promise { - return await this.useConnection(async (connection) => { - return await connection.query(sql, params); - }); - } - - /** - * execute sql - * @param sql sql string - * @param params query params - */ - async execute(sql: string, params?: any[]): Promise { - return await this.useConnection(async (connection) => { - return await connection.execute(sql, params); - }); - } - - async useConnection(fn: (conn: Connection) => Promise) { - if (!this._pool) { - throw new MysqlError("Unconnected"); +export class MysqlTransactionable extends MysqlPreparable + implements + SqlxTransactionable< + MysqlParameterType, + MysqlQueryOptions, + MysqlTransactionOptions, + MySqlTransaction + > { + async beginTransaction( + options?: MysqlTransactionOptions["beginTransactionOptions"], + ): Promise { + let sql = "START TRANSACTION"; + if (options?.withConsistentSnapshot) { + sql += ` WITH CONSISTENT SNAPSHOT`; } - const connection = await this._pool.pop(); - try { - return await fn(connection); - } finally { - if (connection.state == ConnectionState.CLOSED) { - connection.removeFromPool(); - } else { - connection.returnToPool(); - } + + if (options?.readWrite) { + sql += ` ${options.readWrite}`; } + + await this.execute(sql); + + return new MySqlTransaction(this.connection, this.queryOptions); } - /** - * Execute a transaction process, and the transaction successfully - * returns the return value of the transaction process - * @param processor transation processor - */ - async transaction(processor: TransactionProcessor): Promise { - return await this.useConnection(async (connection) => { - try { - await connection.execute("BEGIN"); - const result = await processor(connection); - await connection.execute("COMMIT"); - return result; - } catch (error) { - if (connection.state == ConnectionState.CONNECTED) { - logger().info(`ROLLBACK: ${error.message}`); - await connection.execute("ROLLBACK"); - } - throw error; - } - }); - } - - /** - * close connection - */ - async close() { - if (this._pool) { - this._pool.close(); - this._pool = undefined; + async transaction( + fn: (t: MySqlTransaction) => Promise, + options?: MysqlTransactionOptions, + ): Promise { + const transaction = await this.beginTransaction( + options?.beginTransactionOptions, + ); + + try { + const result = await fn(transaction); + await transaction.commitTransaction(options?.commitTransactionOptions); + return result; + } catch (error) { + await transaction.rollbackTransaction( + options?.rollbackTransactionOptions, + ); + throw error; } } } + +/** + * MySQL client + */ +export class MysqlClient extends MysqlTransactionable implements + SqlxConnection< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + SqlxConnectionEventType, + MysqlConnectionOptions + > { + readonly connectionUrl: string; + readonly connectionOptions: MysqlConnectionOptions; + readonly eventTarget: EventTarget; + get connected(): boolean { + throw new Error("Method not implemented."); + } + + constructor( + connectionUrl: string | URL, + connectionOptions: MysqlClientOptions = {}, + ) { + const conn = new MysqlConnection(connectionUrl, connectionOptions); + super(conn); + this.connectionUrl = conn.connectionUrl; + this.connectionOptions = conn.connectionOptions; + this.eventTarget = new EventTarget(); + } + async connect(): Promise { + await this.connection.connect(); + this.dispatchEvent(new SqlxConnectionConnectEvent()); + } + async close(): Promise { + this.dispatchEvent(new SqlxConnectionCloseEvent()); + await this.connection.close(); + } + async [Symbol.asyncDispose](): Promise { + await this.close(); + } + addEventListener( + type: SqlxConnectionEventType, + listener: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions, + ): void { + this.eventTarget.addEventListener(type, listener, options); + } + removeEventListener( + type: SqlxConnectionEventType, + callback: EventListenerOrEventListenerObject | null, + options?: boolean | EventListenerOptions, + ): void { + this.eventTarget.removeEventListener(type, callback, options); + } + dispatchEvent(event: Event): boolean { + return this.eventTarget.dispatchEvent(event); + } +} diff --git a/lib/connection.test.ts b/lib/connection.test.ts index be25f0a..bf15dc9 100644 --- a/lib/connection.test.ts +++ b/lib/connection.test.ts @@ -1,9 +1,10 @@ import { assertEquals, assertInstanceOf } from "@std/assert"; import { emptyDir } from "@std/fs"; import { join } from "@std/path"; -import { MysqlConnection } from "./connection2.ts"; +import { MysqlConnection } from "./connection.ts"; import { DIR_TMP_TEST } from "./utils/testing.ts"; import { buildQuery } from "./packets/builders/query.ts"; +import { URL_TEST_CONNECTION } from "./utils/testing.ts"; Deno.test("Connection", async (t) => { await emptyDir(DIR_TMP_TEST); @@ -19,10 +20,10 @@ Deno.test("Connection", async (t) => { await Deno.writeTextFile(PATH_PEM_KEY, "key"); await t.step("can construct", async (t) => { - const connection = new MysqlConnection("mysql://127.0.0.1:3306"); + const connection = new MysqlConnection(URL_TEST_CONNECTION); assertInstanceOf(connection, MysqlConnection); - assertEquals(connection.connectionUrl, "mysql://127.0.0.1:3306"); + assertEquals(connection.connectionUrl, URL_TEST_CONNECTION); await t.step("can parse connection config simple", () => { const url = new URL("mysql://user:pass@127.0.0.1:3306/db"); @@ -126,7 +127,7 @@ Deno.test("Connection", async (t) => { }); }); - const connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + const connection = new MysqlConnection(URL_TEST_CONNECTION); assertEquals(connection.connected, false); await t.step("can connect and close", async () => { @@ -144,55 +145,218 @@ Deno.test("Connection", async (t) => { }); await t.step("can connect with using and dispose", async () => { - await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + await using connection = new MysqlConnection(URL_TEST_CONNECTION); assertEquals(connection.connected, false); await connection.connect(); assertEquals(connection.connected, true); }); - await t.step("can execute", async (t) => { - await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); - await connection.connect(); - const data = buildQuery("SELECT 1+1 AS result"); - const result = await connection.execute(data); - assertEquals(result, { affectedRows: 0, lastInsertId: null }); - }); + // await t.step("can execute", async (t) => { + // await using connection = new MysqlConnection(URL_TEST_CONNECTION); + // await connection.connect(); + // const data = buildQuery("SELECT 1+1 AS result"); + // const result = await connection.execute(data); + // assertEquals(result, { affectedRows: 0, lastInsertId: null }); + // }); - await t.step("can execute twice", async (t) => { - await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); - await connection.connect(); - const data = buildQuery("SELECT 1+1 AS result;"); - const result1 = await connection.execute(data); - assertEquals(result1, { affectedRows: 0, lastInsertId: null }); - const result2 = await connection.execute(data); - assertEquals(result2, { affectedRows: 0, lastInsertId: null }); - }); + // await t.step("can execute twice", async (t) => { + // await using connection = new MysqlConnection(URL_TEST_CONNECTION); + // await connection.connect(); + // const data = buildQuery("SELECT 1+1 AS result;"); + // const result1 = await connection.execute(data); + // assertEquals(result1, { affectedRows: 0, lastInsertId: null }); + // const result2 = await connection.execute(data); + // assertEquals(result2, { affectedRows: 0, lastInsertId: null }); + // }); - await t.step("can sendData", async (t) => { - await using connection = new MysqlConnection("mysql://root@0.0.0.0:3306"); + await t.step("can query database", async (t) => { + await using connection = new MysqlConnection(URL_TEST_CONNECTION); await connection.connect(); - const data = buildQuery("SELECT 1+1 AS result;"); - for await (const result1 of connection.sendData(data)) { - assertEquals(result1, { - row: [2], - fields: [ - { - catalog: "def", - decimals: 0, - defaultVal: "", - encoding: 63, - fieldFlag: 129, - fieldLen: 3, - fieldType: 8, - name: "result", - originName: "", - originTable: "", - schema: "", - table: "", - }, - ], + await t.step("can sendData", async () => { + const data = buildQuery("SELECT 1+1 AS result;"); + for await (const result1 of connection.sendData(data)) { + assertEquals(result1, { + row: [2], + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 129, + fieldLen: 3, + fieldType: 8, + name: "result", + originName: "", + originTable: "", + schema: "", + table: "", + }, + ], + }); + } + }); + + await t.step("can drop and create table", async () => { + const dropTableSql = buildQuery("DROP TABLE IF EXISTS test;"); + const dropTableReturned = connection.sendData(dropTableSql); + assertEquals(await dropTableReturned.next(), { + done: true, + value: { affectedRows: 0, lastInsertId: 0 }, + }); + const createTableSql = buildQuery( + "CREATE TABLE IF NOT EXISTS test (id INT);", + ); + const createTableReturned = connection.sendData(createTableSql); + assertEquals(await createTableReturned.next(), { + done: true, + value: { affectedRows: 0, lastInsertId: 0 }, + }); + const result = await Array.fromAsync(createTableReturned); + assertEquals(result, []); + }); + + await t.step("can insert to table", async () => { + const data = buildQuery("INSERT INTO test (id) VALUES (1),(2),(3);"); + const returned = connection.sendData(data); + assertEquals(await returned.next(), { + done: true, + value: { affectedRows: 3, lastInsertId: 0 }, + }); + const result = await Array.fromAsync(returned); + assertEquals(result, []); + }); + + await t.step("can select from table using sendData", async () => { + const data = buildQuery("SELECT * FROM test;"); + const returned = connection.sendData(data); + const result = await Array.fromAsync(returned); + assertEquals(result, [ + { + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 0, + fieldLen: 11, + fieldType: 3, + name: "id", + originName: "id", + originTable: "test", + schema: "testdb", + table: "test", + }, + ], + row: [ + 1, + ], + }, + { + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 0, + fieldLen: 11, + fieldType: 3, + name: "id", + originName: "id", + originTable: "test", + schema: "testdb", + table: "test", + }, + ], + row: [ + 2, + ], + }, + { + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 0, + fieldLen: 11, + fieldType: 3, + name: "id", + originName: "id", + originTable: "test", + schema: "testdb", + table: "test", + }, + ], + row: [ + 3, + ], + }, + ]); + }); + + await t.step("can insert to table using executeRaw", async () => { + const data = buildQuery("INSERT INTO test (id) VALUES (4);"); + const result = await connection.executeRaw(data); + assertEquals(result, 1); + }); + + await t.step("can select from table using executeRaw", async () => { + const data = buildQuery("SELECT * FROM test;"); + const result = await connection.executeRaw(data); + assertEquals(result, undefined); + }); + + await t.step("can insert to table using queryManyObjectRaw", async () => { + const data = buildQuery("INSERT INTO test (id) VALUES (5);"); + const result = await Array.fromAsync(connection.queryManyObjectRaw(data)); + assertEquals(result, []); + }); + + await t.step("can select from table using queryManyObjectRaw", async () => { + const data = buildQuery("SELECT * FROM test;"); + const result = await Array.fromAsync(connection.queryManyObjectRaw(data)); + assertEquals(result, [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]); + }); + + await t.step("can insert to table using queryManyArrayRaw", async () => { + const data = buildQuery("INSERT INTO test (id) VALUES (6);"); + const result = await Array.fromAsync(connection.queryManyArrayRaw(data)); + assertEquals(result, []); + }); + + await t.step("can select from table using queryManyArrayRaw", async () => { + const data = buildQuery("SELECT * FROM test;"); + const result = await Array.fromAsync(connection.queryManyArrayRaw(data)); + assertEquals(result, [ + [1], + [2], + [3], + [4], + [5], + [6], + ]); + }); + + await t.step("can drop table", async () => { + const data = buildQuery("DROP TABLE IF EXISTS test;"); + const returned = connection.sendData(data); + assertEquals(await returned.next(), { + done: true, + value: { affectedRows: 0, lastInsertId: 0 }, }); - } + const result = await Array.fromAsync(returned); + assertEquals(result, []); + }); }); await emptyDir(DIR_TMP_TEST); diff --git a/lib/connection.ts b/lib/connection.ts index 1d375d6..8c361e9 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -1,4 +1,3 @@ -import { type ClientConfig, TLSMode } from "./client.ts"; import { MysqlConnectionError, MysqlError, @@ -7,7 +6,6 @@ import { MysqlResponseTimeoutError, } from "./utils/errors.ts"; import { buildAuth } from "./packets/builders/auth.ts"; -import { buildQuery } from "./packets/builders/query.ts"; import { PacketReader, PacketWriter } from "./packets/packet.ts"; import { parseError } from "./packets/parsers/err.ts"; import { @@ -16,17 +14,31 @@ import { parseHandshake, } from "./packets/parsers/handshake.ts"; import { + ConvertTypeOptions, type FieldInfo, + getRowObject, + type MysqlParameterType, parseField, - parseRowObject, + parseRowArray, } from "./packets/parsers/result.ts"; import { ComQueryResponsePacket } from "./constant/packet.ts"; -import { AuthPluginName, AuthPlugins } from "./auth_plugins/mod.ts"; +import { AuthPlugins } from "./auth_plugins/mod.ts"; import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; import auth from "./utils/hash.ts"; import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; import { logger } from "./utils/logger.ts"; +import type { + ArrayRow, + Row, + SqlxConnectable, + SqlxConnectionOptions, +} from "@halvardm/sqlx"; +import { VERSION } from "./utils/meta.ts"; +import { resolve } from "@std/path"; +import { toCamelCase } from "@std/text"; +import { AuthPluginName } from "./auth_plugins/mod.ts"; +import type { MysqlQueryOptions } from "./client.ts"; /** * Connection state @@ -38,19 +50,91 @@ export enum ConnectionState { CLOSED, } +export type ConnectionSendDataNext = { + row: ArrayRow; + fields: FieldInfo[]; +}; +export type ConnectionSendDataResult = { + affectedRows: number | undefined; + lastInsertId: number | undefined; +}; + /** - * Result for execute sql + * Tls mode for mysql connection + * + * @see {@link https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode} */ -export type ExecuteResult = { - affectedRows?: number; - lastInsertId?: number; - fields?: FieldInfo[]; - rows?: any[]; - iterator?: any; -}; +export const TlsMode = { + Preferred: "PREFERRED", + Disabled: "DISABLED", + Required: "REQUIRED", + VerifyCa: "VERIFY_CA", + VerifyIdentity: "VERIFY_IDENTITY", +} as const; +export type TlsMode = typeof TlsMode[keyof typeof TlsMode]; + +export interface TlsOptions extends Deno.ConnectTlsOptions { + mode: TlsMode; +} + +/** + * Aditional connection parameters + * + * @see {@link https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html#connecting-using-uri} + */ +export interface ConnectionParameters { + socket?: string; + sslMode?: TlsMode; + sslCa?: string[]; + sslCapath?: string[]; + sslCert?: string; + sslCipher?: string; + sslCrl?: string; + sslCrlpath?: string; + sslKey?: string; + tlsVersion?: string; + tlsVersions?: string; + tlsCiphersuites?: string; + authMethod?: string; + getServerPublicKey?: boolean; + serverPublicKeyPath?: string; + ssh?: string; + uri?: string; + sshPassword?: string; + sshConfigFile?: string; + sshIdentityFile?: string; + sshIdentityPass?: string; + connectTimeout?: number; + compression?: string; + compressionAlgorithms?: string; + compressionLevel?: string; + connectionAttributes?: string; +} + +export interface ConnectionConfig { + protocol: string; + username: string; + password?: string; + hostname: string; + port: number; + socket?: string; + schema?: string; + /** + * Tls options + */ + tls?: Partial; + /** + * Aditional connection parameters + */ + parameters: ConnectionParameters; +} + +export interface MysqlConnectionOptions extends SqlxConnectionOptions { +} /** Connection for mysql */ -export class Connection { +export class MysqlConnection + implements SqlxConnectable { state: ConnectionState = ConnectionState.CONNECTING; capabilities: number = 0; serverVersion: string = ""; @@ -58,6 +142,11 @@ export class Connection { protected _conn: Deno.Conn | null = null; private _timedOut = false; + readonly connectionUrl: string; + readonly connectionOptions: MysqlConnectionOptions; + readonly config: ConnectionConfig; + readonly sqlxVersion: string = VERSION; + get conn(): Deno.Conn { if (!this._conn) { throw new MysqlConnectionError("Not connected"); @@ -76,43 +165,48 @@ export class Connection { this._conn = conn; } - get remoteAddr(): string { - return this.config.socketPath - ? `unix:${this.config.socketPath}` - : `${this.config.hostname}:${this.config.port}`; + constructor( + connectionUrl: string | URL, + connectionOptions: MysqlConnectionOptions = {}, + ) { + this.connectionUrl = connectionUrl.toString().split("?")[0]; + this.connectionOptions = connectionOptions; + this.config = this.#parseConnectionConfig( + connectionUrl, + connectionOptions, + ); } - - get isMariaDB(): boolean { - return this.serverVersion.includes("MariaDB"); + get connected(): boolean { + return this.state === ConnectionState.CONNECTED; } - constructor(readonly config: ClientConfig) {} - - private async _connect() { + async connect(): Promise { // TODO: implement connect timeout if ( this.config.tls?.mode && - this.config.tls.mode !== TLSMode.DISABLED && - this.config.tls.mode !== TLSMode.VERIFY_IDENTITY + this.config.tls?.mode !== TlsMode.Disabled && + this.config.tls?.mode !== TlsMode.VerifyIdentity ) { - throw new MysqlError("unsupported tls mode"); + throw new Error("unsupported tls mode"); } - const { hostname, port = 3306, socketPath, username = "", password } = - this.config; - logger().info(`connecting ${this.remoteAddr}`); - this.conn = !socketPath - ? await Deno.connect({ - transport: "tcp", - hostname, - port, - }) - : await Deno.connect({ + + logger().info(`connecting ${this.connectionUrl}`); + + if (this.config.socket) { + this.conn = await Deno.connect({ transport: "unix", - path: socketPath, - } as any); + path: this.config.socket, + }); + } else { + this.conn = await Deno.connect({ + transport: "tcp", + hostname: this.config.hostname, + port: this.config.port, + }); + } try { - let receive = await this.nextPacket(); + let receive = await this.#nextPacket(); const handshakePacket = parseHandshake(receive.body); let handshakeSequenceNumber = receive.header.no; @@ -120,20 +214,20 @@ export class Connection { // Deno.startTls() only supports VERIFY_IDENTITY now. let isSSL = false; if ( - this.config.tls?.mode === TLSMode.VERIFY_IDENTITY + this.config.tls?.mode === TlsMode.VerifyIdentity ) { if ( (handshakePacket.serverCapabilities & ServerCapabilities.CLIENT_SSL) === 0 ) { - throw new MysqlError("Server does not support TLS"); + throw new Error("Server does not support TLS"); } if ( (handshakePacket.serverCapabilities & ServerCapabilities.CLIENT_SSL) !== 0 ) { const tlsData = buildSSLRequest(handshakePacket, { - db: this.config.db, + db: this.config.schema, }); await PacketWriter.write( this.conn, @@ -141,7 +235,7 @@ export class Connection { ++handshakeSequenceNumber, ); this.conn = await Deno.startTls(this.conn, { - hostname, + hostname: this.config.hostname, caCerts: this.config.tls?.caCerts, }); } @@ -149,19 +243,19 @@ export class Connection { } const data = await buildAuth(handshakePacket, { - username, - password, - db: this.config.db, + username: this.config.username, + password: this.config.password, + db: this.config.schema, ssl: isSSL, }); - await PacketWriter.write(this.conn, data, ++handshakeSequenceNumber); + await PacketWriter.write(this._conn!, data, ++handshakeSequenceNumber); this.state = ConnectionState.CONNECTING; this.serverVersion = handshakePacket.serverVersion; this.capabilities = handshakePacket.serverCapabilities; - receive = await this.nextPacket(); + receive = await this.#nextPacket(); const authResult = parseAuth(receive); let authPlugin: AuthPluginName | undefined = undefined; @@ -183,22 +277,26 @@ export class Connection { } let authData; - if (password) { + if (this.config.password) { authData = await auth( authSwitch.authPluginName, - password, + this.config.password, authSwitch.authPluginData, ); } else { authData = Uint8Array.from([]); } - await PacketWriter.write(this.conn, authData, receive.header.no + 1); + await PacketWriter.write( + this.conn, + authData, + receive.header.no + 1, + ); - receive = await this.nextPacket(); + receive = await this.#nextPacket(); const authSwitch2 = parseAuthSwitch(receive.body); if (authSwitch2.authPluginName !== "") { - throw new MysqlError( + throw new Error( "Do not allow to change the auth plugin more than once!", ); } @@ -221,10 +319,10 @@ export class Connection { plugin.data, sequenceNumber, ); - receive = await this.nextPacket(); + receive = await this.#nextPacket(); } if (plugin.quickRead) { - await this.nextPacket(); + await this.#nextPacket(); } await plugin.next(receive); @@ -232,7 +330,7 @@ export class Connection { break; } default: - throw new MysqlError("Unsupported auth plugin"); + throw new Error("Unsupported auth plugin"); } } @@ -241,15 +339,11 @@ export class Connection { const error = parseError(receive.body, this); logger().error(`connect error(${error.code}): ${error.message}`); this.close(); - throw new MysqlError(error.message); + throw new Error(error.message); } else { - logger().info(`connected to ${this.remoteAddr}`); + logger().info(`connected to ${this.connectionUrl}`); this.state = ConnectionState.CONNECTED; } - - if (this.config.charset) { - await this.execute(`SET NAMES ${this.config.charset}`); - } } catch (error) { // Call close() to avoid leaking socket. this.close(); @@ -257,25 +351,143 @@ export class Connection { } } - /** Connect to database */ - async connect(): Promise { - await this._connect(); + close(): Promise { + if (this.state != ConnectionState.CLOSED) { + logger().info("close connection"); + this._conn?.close(); + this.state = ConnectionState.CLOSED; + } + return Promise.resolve(); + } + + /** + * Parses the connection url and options into a connection config + */ + #parseConnectionConfig( + connectionUrl: string | URL, + connectionOptions: MysqlConnectionOptions, + ): ConnectionConfig { + function parseParameters(url: URL): ConnectionParameters { + const parameters: ConnectionParameters = {}; + for (const [key, value] of url.searchParams) { + const pKey = toCamelCase(key); + if (pKey === "sslCa") { + if (!parameters.sslCa) { + parameters.sslCa = []; + } + parameters.sslCa.push(value); + } else if (pKey === "sslCapath") { + if (!parameters.sslCapath) { + parameters.sslCapath = []; + } + parameters.sslCapath.push(value); + } else if (pKey === "getServerPublicKey") { + parameters.getServerPublicKey = value === "true"; + } else if (pKey === "connectTimeout") { + parameters.connectTimeout = parseInt(value); + } else { + // deno-lint-ignore no-explicit-any + parameters[pKey as keyof ConnectionParameters] = value as any; + } + } + return parameters; + } + + function parseTlsOptions(config: ConnectionConfig): TlsOptions | undefined { + const baseTlsOptions: TlsOptions = { + port: config.port, + hostname: config.hostname, + mode: TlsMode.Preferred, + }; + + if (connectionOptions.tls) { + return { + ...baseTlsOptions, + ...connectionOptions.tls, + }; + } + + if (config.parameters.sslMode) { + const tlsOptions: TlsOptions = { + ...baseTlsOptions, + mode: config.parameters.sslMode, + }; + + const caCertPaths = new Set(); + + if (config.parameters.sslCa?.length) { + for (const caCert of config.parameters.sslCa) { + caCertPaths.add(resolve(caCert)); + } + } + + if (config.parameters.sslCapath?.length) { + for (const caPath of config.parameters.sslCapath) { + for (const f of Deno.readDirSync(caPath)) { + if (f.isFile && f.name.endsWith(".pem")) { + caCertPaths.add(resolve(caPath, f.name)); + } + } + } + } + + if (caCertPaths.size) { + tlsOptions.caCerts = []; + for (const caCert of caCertPaths) { + const content = Deno.readTextFileSync(caCert); + tlsOptions.caCerts.push(content); + } + } + + if (config.parameters.sslKey) { + tlsOptions.key = Deno.readTextFileSync( + resolve(config.parameters.sslKey), + ); + } + + if (config.parameters.sslCert) { + tlsOptions.cert = Deno.readTextFileSync( + resolve(config.parameters.sslCert), + ); + } + + return tlsOptions; + } + return undefined; + } + + const url = new URL(connectionUrl); + const parameters = parseParameters(url); + const config: ConnectionConfig = { + protocol: url.protocol.slice(0, -1), + username: url.username, + password: url.password || undefined, + hostname: url.hostname, + port: parseInt(url.port || "3306"), + schema: url.pathname.slice(1), + parameters: parameters, + socket: parameters.socket, + }; + + config.tls = parseTlsOptions(config); + + return config; } - private async nextPacket(): Promise { - if (!this.conn) { + async #nextPacket(): Promise { + if (!this._conn) { throw new MysqlConnectionError("Not connected"); } - const timeoutTimer = this.config.timeout + const timeoutTimer = this.config.parameters.connectTimeout ? setTimeout( - this._timeoutCallback, - this.config.timeout, + this.#timeoutCallback, + this.config.parameters.connectTimeout, ) : null; let packet: PacketReader | null; try { - packet = await PacketReader.read(this.conn); + packet = await PacketReader.read(this._conn); } catch (error) { if (this._timedOut) { // Connection has been closed by timeoutCallback. @@ -296,62 +508,28 @@ export class Connection { if (packet.type === ComQueryResponsePacket.ERR_Packet) { packet.body.skip(1); const error = parseError(packet.body, this); - throw new MysqlError(error.message); + throw new Error(error.message); } - return packet!; + return packet; } - private _timeoutCallback = () => { + #timeoutCallback = () => { logger().info("connection read timed out"); this._timedOut = true; this.close(); }; - /** Close database connection */ - close(): void { - if (this.state != ConnectionState.CLOSED) { - logger().info("close connection"); - this.conn?.close(); - this.state = ConnectionState.CLOSED; - } - } - - /** - * excute query sql - * @param sql query sql string - * @param params query params - */ - async query(sql: string, params?: any[]): Promise { - const result = await this.execute(sql, params); - if (result && result.rows) { - return result.rows; - } else { - return result; - } - } - - /** - * execute sql - * @param sql sql string - * @param params query params - * @param iterator whether to return an ExecuteIteratorResult or ExecuteResult - */ - async execute( - sql: string, - params?: any[], - iterator = false, - ): Promise { - if (this.state != ConnectionState.CONNECTED) { - if (this.state == ConnectionState.CLOSED) { - throw new MysqlConnectionError("Connection is closed"); - } else { - throw new MysqlConnectionError("Must be connected first"); - } - } - const data = buildQuery(sql, params); + async *sendData( + data: Uint8Array, + options?: ConvertTypeOptions, + ): AsyncGenerator< + ConnectionSendDataNext, + ConnectionSendDataResult | undefined + > { try { await PacketWriter.write(this.conn, data, 0); - let receive = await this.nextPacket(); + let receive = await this.#nextPacket(); + logger().debug(`packet type: ${receive.type.toString()}`); if (receive.type === ComQueryResponsePacket.OK_Packet) { receive.body.skip(1); return { @@ -364,67 +542,78 @@ export class Connection { let fieldCount = receive.body.readEncodedLen(); const fields: FieldInfo[] = []; while (fieldCount--) { - const packet = await this.nextPacket(); + const packet = await this.#nextPacket(); if (packet) { const field = parseField(packet.body); fields.push(field); } } - const rows = []; if (!(this.capabilities & ServerCapabilities.CLIENT_DEPRECATE_EOF)) { // EOF(mysql < 5.7 or mariadb < 10.2) - receive = await this.nextPacket(); + receive = await this.#nextPacket(); if (receive.type !== ComQueryResponsePacket.EOF_Packet) { throw new MysqlProtocolError(receive.type.toString()); } } - if (!iterator) { - while (true) { - receive = await this.nextPacket(); - if (receive.type === ComQueryResponsePacket.EOF_Packet) { - break; - } else { - const row = parseRowObject(receive.body, fields); - rows.push(row); - } - } - return { rows, fields }; - } + receive = await this.#nextPacket(); - return { - fields, - iterator: this.buildIterator(fields), - }; + while (receive.type !== ComQueryResponsePacket.EOF_Packet) { + const row = parseRowArray(receive.body, fields, options); + yield { + row, + fields, + }; + receive = await this.#nextPacket(); + } } catch (error) { this.close(); throw error; } } - private buildIterator(fields: FieldInfo[]): any { - const next = async () => { - const receive = await this.nextPacket(); + async executeRaw( + data: Uint8Array, + options?: ConvertTypeOptions, + ): Promise { + const gen = this.sendData(data, options); + let result = await gen.next(); + if (result.done) { + return result.value?.affectedRows; + } - if (receive.type === ComQueryResponsePacket.EOF_Packet) { - return { done: true }; - } + const debugRest = []; + debugRest.push(result); + while (!result.done) { + result = await gen.next(); + debugRest.push(result); + logger().debug(`executeRaw overflow: ${JSON.stringify(debugRest)}`); + } + logger().debug(`executeRaw overflow: ${JSON.stringify(debugRest)}`); + return undefined; + } - const value = parseRowObject(receive.body, fields); + async *queryManyObjectRaw = Row>( + data: Uint8Array, + options?: ConvertTypeOptions, + ): AsyncIterableIterator { + for await (const res of this.sendData(data, options)) { + yield getRowObject(res.fields, res.row) as T; + } + } - return { - done: false, - value, - }; - }; + async *queryManyArrayRaw = ArrayRow>( + data: Uint8Array, + options?: ConvertTypeOptions, + ): AsyncIterableIterator { + for await (const res of this.sendData(data, options)) { + const row = res.row as T; + yield row as T; + } + } - return { - [Symbol.asyncIterator]: () => { - return { - next, - }; - }, - }; + async [Symbol.asyncDispose](): Promise { + await this.close(); } } diff --git a/lib/connection2.ts b/lib/connection2.ts deleted file mode 100644 index cc041e9..0000000 --- a/lib/connection2.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { - MysqlConnectionError, - MysqlProtocolError, - MysqlReadError, - MysqlResponseTimeoutError, -} from "./utils/errors.ts"; -import { buildAuth } from "./packets/builders/auth.ts"; -import { PacketReader, PacketWriter } from "./packets/packet.ts"; -import { parseError } from "./packets/parsers/err.ts"; -import { - AuthResult, - parseAuth, - parseHandshake, -} from "./packets/parsers/handshake.ts"; -import { - type FieldInfo, - parseField, - parseRowArray, -} from "./packets/parsers/result.ts"; -import { ComQueryResponsePacket } from "./constant/packet.ts"; -import { AuthPlugins } from "./auth_plugins/mod.ts"; -import { parseAuthSwitch } from "./packets/parsers/authswitch.ts"; -import auth from "./utils/hash.ts"; -import { ServerCapabilities } from "./constant/capabilities.ts"; -import { buildSSLRequest } from "./packets/builders/tls.ts"; -import { logger } from "./utils/logger.ts"; -import type { - ArrayRow, - SqlxConnectable, - SqlxConnectionOptions, - SqlxParameterType, -} from "@halvardm/sqlx"; -import { VERSION } from "./utils/meta.ts"; -import { resolve } from "@std/path"; -import { toCamelCase } from "@std/text"; -import { AuthPluginName } from "./auth_plugins/mod.ts"; -export type MysqlParameterType = SqlxParameterType; - -/** - * Connection state - */ -export enum ConnectionState { - CONNECTING, - CONNECTED, - CLOSING, - CLOSED, -} - -export type ConnectionSendDataResult = { - affectedRows: number; - lastInsertId: number | null; -} | undefined; - -export type ConnectionSendDataNext = { - row: ArrayRow; - fields: FieldInfo[]; -}; - -export interface ConnectionOptions extends SqlxConnectionOptions { -} - -/** - * Tls mode for mysql connection - * - * @see {@link https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode} - */ -export const TlsMode = { - Preferred: "PREFERRED", - Disabled: "DISABLED", - Required: "REQUIRED", - VerifyCa: "VERIFY_CA", - VerifyIdentity: "VERIFY_IDENTITY", -} as const; -export type TlsMode = typeof TlsMode[keyof typeof TlsMode]; - -export interface TlsOptions extends Deno.ConnectTlsOptions { - mode: TlsMode; -} - -/** - * Aditional connection parameters - * - * @see {@link https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html#connecting-using-uri} - */ -export interface ConnectionParameters { - socket?: string; - sslMode?: TlsMode; - sslCa?: string[]; - sslCapath?: string[]; - sslCert?: string; - sslCipher?: string; - sslCrl?: string; - sslCrlpath?: string; - sslKey?: string; - tlsVersion?: string; - tlsVersions?: string; - tlsCiphersuites?: string; - authMethod?: string; - getServerPublicKey?: boolean; - serverPublicKeyPath?: string; - ssh?: string; - uri?: string; - sshPassword?: string; - sshConfigFile?: string; - sshIdentityFile?: string; - sshIdentityPass?: string; - connectTimeout?: number; - compression?: string; - compressionAlgorithms?: string; - compressionLevel?: string; - connectionAttributes?: string; -} - -export interface ConnectionConfig { - protocol: string; - username: string; - password?: string; - hostname: string; - port: number; - socket?: string; - schema?: string; - /** - * Tls options - */ - tls?: Partial; - /** - * Aditional connection parameters - */ - parameters: ConnectionParameters; -} - -/** Connection for mysql */ -export class MysqlConnection implements SqlxConnectable { - state: ConnectionState = ConnectionState.CONNECTING; - capabilities: number = 0; - serverVersion: string = ""; - - protected _conn: Deno.Conn | null = null; - private _timedOut = false; - - readonly connectionUrl: string; - readonly connectionOptions: ConnectionOptions; - readonly config: ConnectionConfig; - readonly sqlxVersion: string = VERSION; - - get conn(): Deno.Conn { - if (!this._conn) { - throw new MysqlConnectionError("Not connected"); - } - if (this.state != ConnectionState.CONNECTED) { - if (this.state == ConnectionState.CLOSED) { - throw new MysqlConnectionError("Connection is closed"); - } else { - throw new MysqlConnectionError("Must be connected first"); - } - } - return this._conn; - } - - set conn(conn: Deno.Conn | null) { - this._conn = conn; - } - - constructor( - connectionUrl: string | URL, - connectionOptions: ConnectionOptions = {}, - ) { - this.connectionUrl = connectionUrl.toString().split("?")[0]; - this.connectionOptions = connectionOptions; - this.config = this.#parseConnectionConfig( - connectionUrl, - connectionOptions, - ); - } - get connected(): boolean { - return this.state === ConnectionState.CONNECTED; - } - - async connect(): Promise { - // TODO: implement connect timeout - if ( - this.config.tls?.mode && - this.config.tls?.mode !== TlsMode.Disabled && - this.config.tls?.mode !== TlsMode.VerifyIdentity - ) { - throw new Error("unsupported tls mode"); - } - - logger().info(`connecting ${this.connectionUrl}`); - - if (this.config.socket) { - this.conn = await Deno.connect({ - transport: "unix", - path: this.config.socket, - }); - } else { - this.conn = await Deno.connect({ - transport: "tcp", - hostname: this.config.hostname, - port: this.config.port, - }); - } - - try { - let receive = await this.#nextPacket(); - const handshakePacket = parseHandshake(receive.body); - - let handshakeSequenceNumber = receive.header.no; - - // Deno.startTls() only supports VERIFY_IDENTITY now. - let isSSL = false; - if ( - this.config.tls?.mode === TlsMode.VerifyIdentity - ) { - if ( - (handshakePacket.serverCapabilities & - ServerCapabilities.CLIENT_SSL) === 0 - ) { - throw new Error("Server does not support TLS"); - } - if ( - (handshakePacket.serverCapabilities & - ServerCapabilities.CLIENT_SSL) !== 0 - ) { - const tlsData = buildSSLRequest(handshakePacket, { - db: this.config.schema, - }); - await PacketWriter.write( - this.conn, - tlsData, - ++handshakeSequenceNumber, - ); - this.conn = await Deno.startTls(this.conn, { - hostname: this.config.hostname, - caCerts: this.config.tls?.caCerts, - }); - } - isSSL = true; - } - - const data = await buildAuth(handshakePacket, { - username: this.config.username, - password: this.config.password, - db: this.config.schema, - ssl: isSSL, - }); - - await PacketWriter.write(this._conn!, data, ++handshakeSequenceNumber); - - this.state = ConnectionState.CONNECTING; - this.serverVersion = handshakePacket.serverVersion; - this.capabilities = handshakePacket.serverCapabilities; - - receive = await this.#nextPacket(); - - const authResult = parseAuth(receive); - let authPlugin: AuthPluginName | undefined = undefined; - - switch (authResult) { - case AuthResult.AuthMoreRequired: { - authPlugin = handshakePacket.authPluginName as AuthPluginName; - break; - } - case AuthResult.MethodMismatch: { - const authSwitch = parseAuthSwitch(receive.body); - // If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is - // sent and we have to keep using the cipher sent in the init packet. - if ( - authSwitch.authPluginData === undefined || - authSwitch.authPluginData.length === 0 - ) { - authSwitch.authPluginData = handshakePacket.seed; - } - - let authData; - if (this.config.password) { - authData = await auth( - authSwitch.authPluginName, - this.config.password, - authSwitch.authPluginData, - ); - } else { - authData = Uint8Array.from([]); - } - - await PacketWriter.write( - this.conn, - authData, - receive.header.no + 1, - ); - - receive = await this.#nextPacket(); - const authSwitch2 = parseAuthSwitch(receive.body); - if (authSwitch2.authPluginName !== "") { - throw new Error( - "Do not allow to change the auth plugin more than once!", - ); - } - } - } - - if (authPlugin) { - switch (authPlugin) { - case AuthPluginName.CachingSha2Password: { - const plugin = new AuthPlugins[authPlugin]( - handshakePacket.seed, - this.config.password!, - ); - - while (!plugin.done) { - if (plugin.data) { - const sequenceNumber = receive.header.no + 1; - await PacketWriter.write( - this.conn, - plugin.data, - sequenceNumber, - ); - receive = await this.#nextPacket(); - } - if (plugin.quickRead) { - await this.#nextPacket(); - } - - await plugin.next(receive); - } - break; - } - default: - throw new Error("Unsupported auth plugin"); - } - } - - const header = receive.body.readUint8(); - if (header === 0xff) { - const error = parseError(receive.body, this as any); - logger().error(`connect error(${error.code}): ${error.message}`); - this.close(); - throw new Error(error.message); - } else { - logger().info(`connected to ${this.connectionUrl}`); - this.state = ConnectionState.CONNECTED; - } - } catch (error) { - // Call close() to avoid leaking socket. - this.close(); - throw error; - } - } - - async close(): Promise { - if (this.state != ConnectionState.CLOSED) { - logger().info("close connection"); - this._conn?.close(); - this.state = ConnectionState.CLOSED; - } - } - - /** - * Parses the connection url and options into a connection config - */ - #parseConnectionConfig( - connectionUrl: string | URL, - connectionOptions: ConnectionOptions, - ): ConnectionConfig { - function parseParameters(url: URL): ConnectionParameters { - const parameters: ConnectionParameters = {}; - for (const [key, value] of url.searchParams) { - const pKey = toCamelCase(key); - if (pKey === "sslCa") { - if (!parameters.sslCa) { - parameters.sslCa = []; - } - parameters.sslCa.push(value); - } else if (pKey === "sslCapath") { - if (!parameters.sslCapath) { - parameters.sslCapath = []; - } - parameters.sslCapath.push(value); - } else if (pKey === "getServerPublicKey") { - parameters.getServerPublicKey = value === "true"; - } else if (pKey === "connectTimeout") { - parameters.connectTimeout = parseInt(value); - } else { - parameters[pKey as keyof ConnectionParameters] = value as any; - } - } - return parameters; - } - - function parseTlsOptions(config: ConnectionConfig): TlsOptions | undefined { - const baseTlsOptions: TlsOptions = { - port: config.port, - hostname: config.hostname, - mode: TlsMode.Preferred, - }; - - if (connectionOptions.tls) { - return { - ...baseTlsOptions, - ...connectionOptions.tls, - }; - } - - if (config.parameters.sslMode) { - const tlsOptions: TlsOptions = { - ...baseTlsOptions, - mode: config.parameters.sslMode, - }; - - const caCertPaths = new Set(); - - if (config.parameters.sslCa?.length) { - for (const caCert of config.parameters.sslCa) { - caCertPaths.add(resolve(caCert)); - } - } - - if (config.parameters.sslCapath?.length) { - for (const caPath of config.parameters.sslCapath) { - for (const f of Deno.readDirSync(caPath)) { - if (f.isFile && f.name.endsWith(".pem")) { - caCertPaths.add(resolve(caPath, f.name)); - } - } - } - } - - if (caCertPaths.size) { - tlsOptions.caCerts = []; - for (const caCert of caCertPaths) { - const content = Deno.readTextFileSync(caCert); - tlsOptions.caCerts.push(content); - } - } - - if (config.parameters.sslKey) { - tlsOptions.key = Deno.readTextFileSync( - resolve(config.parameters.sslKey), - ); - } - - if (config.parameters.sslCert) { - tlsOptions.cert = Deno.readTextFileSync( - resolve(config.parameters.sslCert), - ); - } - - return tlsOptions; - } - return undefined; - } - - const url = new URL(connectionUrl); - const parameters = parseParameters(url); - const config: ConnectionConfig = { - protocol: url.protocol.slice(0, -1), - username: url.username, - password: url.password || undefined, - hostname: url.hostname, - port: parseInt(url.port || "3306"), - schema: url.pathname.slice(1), - parameters: parameters, - socket: parameters.socket, - }; - - config.tls = parseTlsOptions(config); - - return config; - } - - async #nextPacket(): Promise { - if (!this._conn) { - throw new MysqlConnectionError("Not connected"); - } - - const timeoutTimer = this.config.parameters.connectTimeout - ? setTimeout( - this.#timeoutCallback, - this.config.parameters.connectTimeout, - ) - : null; - let packet: PacketReader | null; - try { - packet = await PacketReader.read(this._conn); - } catch (error) { - if (this._timedOut) { - // Connection has been closed by timeoutCallback. - throw new MysqlResponseTimeoutError("Connection read timed out"); - } - timeoutTimer && clearTimeout(timeoutTimer); - this.close(); - throw error; - } - timeoutTimer && clearTimeout(timeoutTimer); - - if (!packet) { - // Connection is half-closed by the remote host. - // Call close() to avoid leaking socket. - this.close(); - throw new MysqlReadError("Connection closed unexpectedly"); - } - if (packet.type === ComQueryResponsePacket.ERR_Packet) { - packet.body.skip(1); - const error = parseError(packet.body, this as any); - throw new Error(error.message); - } - return packet!; - } - - #timeoutCallback = () => { - logger().info("connection read timed out"); - this._timedOut = true; - this.close(); - }; - - async *sendData( - data: Uint8Array, - ): AsyncGenerator { - try { - await PacketWriter.write(this.conn, data, 0); - let receive = await this.#nextPacket(); - if (receive.type === ComQueryResponsePacket.OK_Packet) { - receive.body.skip(1); - return { - affectedRows: receive.body.readEncodedLen(), - lastInsertId: receive.body.readEncodedLen(), - }; - } else if (receive.type !== ComQueryResponsePacket.Result) { - throw new MysqlProtocolError(receive.type.toString()); - } - let fieldCount = receive.body.readEncodedLen(); - const fields: FieldInfo[] = []; - while (fieldCount--) { - const packet = await this.#nextPacket(); - if (packet) { - const field = parseField(packet.body); - fields.push(field); - } - } - - if (!(this.capabilities & ServerCapabilities.CLIENT_DEPRECATE_EOF)) { - // EOF(mysql < 5.7 or mariadb < 10.2) - receive = await this.#nextPacket(); - if (receive.type !== ComQueryResponsePacket.EOF_Packet) { - throw new MysqlProtocolError(receive.type.toString()); - } - } - - receive = await this.#nextPacket(); - - while (receive.type !== ComQueryResponsePacket.EOF_Packet) { - const row = parseRowArray(receive.body, fields); - yield { row, fields }; - receive = await this.#nextPacket(); - } - } catch (error) { - this.close(); - throw error; - } - } - - async execute( - data: Uint8Array, - ): Promise { - try { - await PacketWriter.write(this.conn, data, 0); - const receive = await this.#nextPacket(); - if (receive.type === ComQueryResponsePacket.OK_Packet) { - receive.body.skip(1); - return { - affectedRows: receive.body.readEncodedLen(), - lastInsertId: receive.body.readEncodedLen(), - }; - } else if (receive.type !== ComQueryResponsePacket.Result) { - throw new MysqlProtocolError(receive.type.toString()); - } - return { - affectedRows: 0, - lastInsertId: null, - }; - } catch (error) { - this.close(); - throw error; - } - } - - async [Symbol.asyncDispose](): Promise { - await this.close(); - } -} diff --git a/lib/packets/builders/query.ts b/lib/packets/builders/query.ts index ed592bd..0ba9d75 100644 --- a/lib/packets/builders/query.ts +++ b/lib/packets/builders/query.ts @@ -1,9 +1,13 @@ import { replaceParams } from "../../utils/query.ts"; import { BufferWriter } from "../../utils/buffer.ts"; import { encode } from "../../utils/encoding.ts"; +import type { MysqlParameterType } from "../parsers/result.ts"; /** @ignore */ -export function buildQuery(sql: string, params: any[] = []): Uint8Array { +export function buildQuery( + sql: string, + params: MysqlParameterType[] = [], +): Uint8Array { const data = encode(replaceParams(sql, params)); const writer = new BufferWriter(new Uint8Array(data.length + 1)); writer.write(0x03); diff --git a/lib/packets/parsers/err.ts b/lib/packets/parsers/err.ts index 448405c..dac14ef 100644 --- a/lib/packets/parsers/err.ts +++ b/lib/packets/parsers/err.ts @@ -1,5 +1,5 @@ import type { BufferReader } from "../../utils/buffer.ts"; -import type { Connection } from "../../connection.ts"; +import type { MysqlConnection } from "../../connection.ts"; import { ServerCapabilities } from "../../constant/capabilities.ts"; /** @ignore */ @@ -13,7 +13,7 @@ export interface ErrorPacket { /** @ignore */ export function parseError( reader: BufferReader, - conn: Connection, + conn: MysqlConnection, ): ErrorPacket { const code = reader.readUint16(); const packet: ErrorPacket = { diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index 08ce798..eb9c525 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -1,6 +1,11 @@ import type { BufferReader } from "../../utils/buffer.ts"; import { MysqlDataType } from "../../constant/mysql_types.ts"; -import type { ArrayRow, Row, SqlxParameterType } from "@halvardm/sqlx"; +import type { + ArrayRow, + Row, + SqlxParameterType, + SqlxQueryOptions, +} from "@halvardm/sqlx"; export type MysqlParameterType = SqlxParameterType< string | number | bigint | Date | null @@ -24,6 +29,8 @@ export interface FieldInfo { defaultVal: string; } +export type ConvertTypeOptions = Pick; + /** * Parses the field */ @@ -64,11 +71,12 @@ export function parseField(reader: BufferReader): FieldInfo { export function parseRowArray( reader: BufferReader, fields: FieldInfo[], + options?: ConvertTypeOptions, ): ArrayRow { const row: MysqlParameterType[] = []; for (const field of fields) { const val = reader.readLenCodeString(); - const parsedVal = val === null ? null : convertType(field, val); + const parsedVal = val === null ? null : convertType(field, val, options); row.push(parsedVal); } return row; @@ -100,7 +108,15 @@ export function getRowObject( /** * Converts the value to the correct type */ -function convertType(field: FieldInfo, val: string): MysqlParameterType { +function convertType( + field: FieldInfo, + val: string, + options?: ConvertTypeOptions, +): MysqlParameterType { + if (options?.transformType) { + // deno-lint-ignore no-explicit-any + return options.transformType(val) as any; + } const { fieldType } = field; switch (fieldType) { case MysqlDataType.Decimal: diff --git a/lib/utils/logger.ts b/lib/utils/logger.ts index f70b71d..9cd7c34 100644 --- a/lib/utils/logger.ts +++ b/lib/utils/logger.ts @@ -1,4 +1,4 @@ -import { getLogger } from "@std/log"; +import { ConsoleHandler, getLogger, setup } from "@std/log"; import { MODULE_NAME } from "./meta.ts"; /** @@ -10,3 +10,20 @@ import { MODULE_NAME } from "./meta.ts"; export function logger() { return getLogger(MODULE_NAME); } + +setup({ + handlers: { + console: new ConsoleHandler("DEBUG"), + }, + loggers: { + // configure default logger available via short-hand methods above + default: { + level: "INFO", + handlers: ["console"], + }, + [MODULE_NAME]: { + level: "INFO", + handlers: ["console"], + }, + }, +}); diff --git a/lib/utils/query.ts b/lib/utils/query.ts index 896a565..4179d1c 100644 --- a/lib/utils/query.ts +++ b/lib/utils/query.ts @@ -1,9 +1,14 @@ +import type { MysqlParameterType } from "../packets/parsers/result.ts"; + /** * Replaces parameters in a SQL query with the given values. * * Taken from https://github.com/manyuanrong/sql-builder/blob/master/util.ts */ -export function replaceParams(sql: string, params: any | any[]): string { +export function replaceParams( + sql: string, + params: MysqlParameterType[], +): string { if (!params) return sql; let paramIndex = 0; sql = sql.replace( @@ -46,9 +51,10 @@ export function replaceParams(sql: string, params: any | any[]): string { // deno-lint-ignore no-fallthrough case "object": if (val instanceof Date) return `"${formatDate(val)}"`; - if (val instanceof Array) { + if ((val as unknown) instanceof Array) { return `(${ - val.map((item) => replaceParams("?", [item])).join(",") + (val as Array).map((item) => replaceParams("?", [item])) + .join(",") })`; } case "string": @@ -58,7 +64,7 @@ export function replaceParams(sql: string, params: any | any[]): string { case "number": case "boolean": default: - return val; + return val.toString(); } }, ); @@ -69,6 +75,7 @@ export function replaceParams(sql: string, params: any | any[]): string { * Formats date to a 'YYYY-MM-DD HH:MM:SS.SSS' string. */ function formatDate(date: Date) { + date.toISOString(); const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, "0"); const days = date From 7d5841d5a06d7ff5e7f3132bdf52ad70016a7946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Tue, 16 Apr 2024 00:52:42 +0200 Subject: [PATCH 25/38] added pool support --- lib/client.ts | 7 +- lib/connection.test.ts | 129 +++++++++ lib/connection.ts | 11 +- lib/deferred.ts | 71 ----- lib/pool.ts | 383 +++++++++++++++++++------- lib/{client.test.ts => sqlx.test.ts} | 4 +- lib/utils/deferred.ts | 51 ++++ lib/utils/events.ts | 71 +++++ testold.ts | 394 --------------------------- 9 files changed, 544 insertions(+), 577 deletions(-) delete mode 100644 lib/deferred.ts rename lib/{client.test.ts => sqlx.test.ts} (89%) create mode 100644 lib/utils/deferred.ts create mode 100644 lib/utils/events.ts delete mode 100644 testold.ts diff --git a/lib/client.ts b/lib/client.ts index 6779730..13df578 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -16,10 +16,7 @@ import { } from "@halvardm/sqlx"; import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; import { buildQuery } from "./packets/builders/query.ts"; -import { - getRowObject, - type MysqlParameterType, -} from "./packets/parsers/result.ts"; +import type { MysqlParameterType } from "./packets/parsers/result.ts"; export interface MysqlTransactionOptions extends SqlxTransactionOptions { beginTransactionOptions: { @@ -346,7 +343,7 @@ export class MysqlClient extends MysqlTransactionable implements > { readonly connectionUrl: string; readonly connectionOptions: MysqlConnectionOptions; - readonly eventTarget: EventTarget; + eventTarget: EventTarget; get connected(): boolean { throw new Error("Method not implemented."); } diff --git a/lib/connection.test.ts b/lib/connection.test.ts index bf15dc9..98408bb 100644 --- a/lib/connection.test.ts +++ b/lib/connection.test.ts @@ -197,6 +197,135 @@ Deno.test("Connection", async (t) => { } }); + await t.step("can parse time", async () => { + const data = buildQuery(`SELECT CAST("09:04:10" AS time) as time`); + for await (const result1 of connection.sendData(data)) { + assertEquals(result1, { + row: ["09:04:10"], + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 128, + fieldLen: 10, + fieldType: 11, + name: "time", + originName: "", + originTable: "", + schema: "", + table: "", + }, + ], + }); + } + }); + + await t.step("can parse date", async () => { + const data = buildQuery( + `SELECT CAST("2024-04-15 09:04:10" AS date) as date`, + ); + for await (const result1 of connection.sendData(data)) { + assertEquals(result1, { + row: [new Date("2024-04-15T00:00:00.000Z")], + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 128, + fieldLen: 10, + fieldType: 10, + name: "date", + originName: "", + originTable: "", + schema: "", + table: "", + }, + ], + }); + } + }); + + await t.step("can parse bigint", async () => { + const data = buildQuery(`SELECT 9223372036854775807 as result`); + for await (const result1 of connection.sendData(data)) { + assertEquals(result1, { + row: [9223372036854775807n], + fields: [ + { + catalog: "def", + decimals: 0, + defaultVal: "", + encoding: 63, + fieldFlag: 129, + fieldLen: 20, + fieldType: 8, + name: "result", + originName: "", + originTable: "", + schema: "", + table: "", + }, + ], + }); + } + }); + + await t.step("can parse decimal", async () => { + const data = buildQuery( + `SELECT 0.012345678901234567890123456789 as result`, + ); + for await (const result1 of connection.sendData(data)) { + assertEquals(result1, { + row: ["0.012345678901234567890123456789"], + fields: [ + { + catalog: "def", + decimals: 30, + defaultVal: "", + encoding: 63, + fieldFlag: 129, + fieldLen: 33, + fieldType: 246, + name: "result", + originName: "", + originTable: "", + schema: "", + table: "", + }, + ], + }); + } + }); + + await t.step("can parse empty string", async () => { + const data = buildQuery(`SELECT '' as result`); + for await (const result1 of connection.sendData(data)) { + assertEquals(result1, { + row: [""], + fields: [ + { + catalog: "def", + decimals: 31, + defaultVal: "", + encoding: 33, + fieldFlag: 1, + fieldLen: 0, + fieldType: 253, + name: "result", + originName: "", + originTable: "", + schema: "", + table: "", + }, + ], + }); + } + }); + await t.step("can drop and create table", async () => { const dropTableSql = buildQuery("DROP TABLE IF EXISTS test;"); const dropTableReturned = connection.sendData(dropTableSql); diff --git a/lib/connection.ts b/lib/connection.ts index 8c361e9..4795f36 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -1,6 +1,5 @@ import { MysqlConnectionError, - MysqlError, MysqlProtocolError, MysqlReadError, MysqlResponseTimeoutError, @@ -14,7 +13,7 @@ import { parseHandshake, } from "./packets/parsers/handshake.ts"; import { - ConvertTypeOptions, + type ConvertTypeOptions, type FieldInfo, getRowObject, type MysqlParameterType, @@ -38,7 +37,6 @@ import { VERSION } from "./utils/meta.ts"; import { resolve } from "@std/path"; import { toCamelCase } from "@std/text"; import { AuthPluginName } from "./auth_plugins/mod.ts"; -import type { MysqlQueryOptions } from "./client.ts"; /** * Connection state @@ -164,7 +162,9 @@ export class MysqlConnection set conn(conn: Deno.Conn | null) { this._conn = conn; } - + get connected(): boolean { + return this.state === ConnectionState.CONNECTED; + } constructor( connectionUrl: string | URL, connectionOptions: MysqlConnectionOptions = {}, @@ -176,9 +176,6 @@ export class MysqlConnection connectionOptions, ); } - get connected(): boolean { - return this.state === ConnectionState.CONNECTED; - } async connect(): Promise { // TODO: implement connect timeout diff --git a/lib/deferred.ts b/lib/deferred.ts deleted file mode 100644 index cd9fcc2..0000000 --- a/lib/deferred.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** @ignore */ -export class DeferredStack { - private _queue: PromiseWithResolvers[] = []; - private _size = 0; - - constructor( - readonly _maxSize: number, - private _array: T[] = [], - private readonly creator: () => Promise, - ) { - this._size = _array.length; - } - - get size(): number { - return this._size; - } - - get maxSize(): number { - return this._maxSize; - } - - get available(): number { - return this._array.length; - } - - async pop(): Promise { - if (this._array.length) { - return this._array.pop()!; - } else if (this._size < this._maxSize) { - this._size++; - let item: T; - try { - item = await this.creator(); - } catch (err) { - this._size--; - throw err; - } - return item; - } - const defer = Promise.withResolvers(); - this._queue.push(defer); - return await defer.promise; - } - - /** Returns false if the item is consumed by a deferred pop */ - push(item: T): boolean { - if (this._queue.length) { - this._queue.shift()!.resolve(item); - return false; - } else { - this._array.push(item); - return true; - } - } - - tryPopAvailable() { - return this._array.pop(); - } - - remove(item: T): boolean { - const index = this._array.indexOf(item); - if (index < 0) return false; - this._array.splice(index, 1); - this._size--; - return true; - } - - reduceSize() { - this._size--; - } -} diff --git a/lib/pool.ts b/lib/pool.ts index c4a38e6..987b3d2 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -1,129 +1,314 @@ -import { DeferredStack } from "./deferred.ts"; -import { Connection } from "./connection.ts"; +import { + type SqlxConnectionPool, + type SqlxConnectionPoolOptions, + SqlxError, + type SqlxPoolConnection, + type SqlxPoolConnectionEventType, + VERSION, +} from "@halvardm/sqlx"; +import { + MysqlClient, + type MysqlPrepared, + type MysqlQueryOptions, + type MySqlTransaction, + type MysqlTransactionOptions, +} from "./client.ts"; +import type { MysqlConnectionOptions } from "./connection.ts"; +import type { MysqlParameterType } from "./packets/parsers/result.ts"; +import { DeferredStack } from "./utils/deferred.ts"; +import type { ArrayRow, Row } from "../../deno-sqlx/lib/interfaces.ts"; +import { + MysqlPoolConnectionAcquireEvent, + MysqlPoolConnectionDestroyEvent, + MysqlPoolConnectionReleaseEvent, +} from "./utils/events.ts"; import { logger } from "./utils/logger.ts"; import { MysqlError } from "./utils/errors.ts"; -/** @ignore */ -export class PoolConnection extends Connection { - _pool?: ConnectionPool = undefined; - - private _idleTimer?: number = undefined; - private _idle = false; +export interface MysqlClientPoolOptions + extends MysqlConnectionOptions, SqlxConnectionPoolOptions { +} - /** - * Should be called by the pool. - */ - enterIdle() { - this._idle = true; - if (this.config.idleTimeout) { - this._idleTimer = setTimeout(() => { - logger().info("connection idle timeout"); - this._pool!.remove(this); - try { - this.close(); - } catch (error) { - logger().warn(`error closing idle connection`, error); - } - }, this.config.idleTimeout); - try { - // Don't block the event loop from finishing - Deno.unrefTimer(this._idleTimer); - } catch (_error) { - // unrefTimer() is unstable API in older version of Deno - } - } +export class MysqlPoolClient extends MysqlClient implements + SqlxPoolConnection< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction + > { + release(): Promise { + throw new Error("Method not implemented."); } +} - /** - * Should be called by the pool. - */ - exitIdle() { - this._idle = false; - if (this._idleTimer !== undefined) { - clearTimeout(this._idleTimer); - } +export class MysqlClientPool implements + SqlxConnectionPool< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + SqlxPoolConnectionEventType, + MysqlClientPoolOptions, + MysqlPoolClient, + DeferredStack + > { + readonly sqlxVersion: string = VERSION; + readonly connectionUrl: string; + readonly connectionOptions: MysqlClientPoolOptions; + readonly queryOptions: MysqlQueryOptions; + readonly eventTarget: EventTarget; + readonly deferredStack: DeferredStack; + get connected(): boolean { + throw new Error("Method not implemented."); } - /** - * Remove the connection from the pool permanently, when the connection is not usable. - */ - removeFromPool() { - this._pool!.reduceSize(); - this._pool = undefined; + constructor( + connectionUrl: string | URL, + connectionOptions: MysqlClientPoolOptions = {}, + ) { + this.connectionUrl = connectionUrl.toString(); + this.connectionOptions = connectionOptions; + this.queryOptions = connectionOptions; + this.eventTarget = new EventTarget(); + this.deferredStack = new DeferredStack(connectionOptions); } - returnToPool() { - this._pool?.push(this); + async execute( + sql: string, + params?: (MysqlParameterType)[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + const conn = await this.acquire(); + let res: number | undefined = undefined; + let err: Error | undefined = undefined; + try { + res = await conn.execute(sql, params, options); + } catch (e) { + err = e; + } + await this.release(conn); + if (err) { + throw err; + } + return res; } -} - -/** @ignore */ -export class ConnectionPool { - _deferred: DeferredStack; - _connections: PoolConnection[] = []; - _closed: boolean = false; - - constructor(maxSize: number, creator: () => Promise) { - this._deferred = new DeferredStack(maxSize, this._connections, async () => { - const conn = await creator(); - conn._pool = this; - return conn; - }); + query< + T extends Row = Row< + MysqlParameterType + >, + >( + sql: string, + params?: (MysqlParameterType)[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queryWrapper((conn) => conn.query(sql, params, options)); } - - get info() { - return { - size: this._deferred.size, - maxSize: this._deferred.maxSize, - available: this._deferred.available, - }; + queryOne< + T extends Row = Row< + MysqlParameterType + >, + >( + sql: string, + params?: (MysqlParameterType)[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queryWrapper((conn) => conn.queryOne(sql, params, options)); } - - push(conn: PoolConnection) { - if (this._closed) { - conn.close(); - this.reduceSize(); + async *queryMany< + T extends Row = Row< + MysqlParameterType + >, + >( + sql: string, + params?: (MysqlParameterType)[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + const conn = await this.acquire(); + let err: Error | undefined = undefined; + try { + for await (const row of conn.queryMany(sql, params, options)) { + yield row; + } + } catch (e) { + err = e; } - if (this._deferred.push(conn)) { - conn.enterIdle(); + await this.release(conn); + if (err) { + throw err; } } + queryArray< + T extends ArrayRow = ArrayRow< + MysqlParameterType + >, + >( + sql: string, + params?: (MysqlParameterType)[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queryWrapper((conn) => + conn.queryArray(sql, params, options) + ); + } + queryOneArray< + T extends ArrayRow = ArrayRow< + MysqlParameterType + >, + >( + sql: string, + params?: (MysqlParameterType)[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queryWrapper((conn) => + conn.queryOneArray(sql, params, options) + ); + } + async *queryManyArray< + T extends ArrayRow = ArrayRow< + MysqlParameterType + >, + >( + sql: string, + params?: (MysqlParameterType)[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + const conn = await this.acquire(); + let err: Error | undefined = undefined; + try { + for await (const row of conn.queryManyArray(sql, params, options)) { + yield row; + } + } catch (e) { + err = e; + } + await this.release(conn); + if (err) { + throw err; + } + } + sql< + T extends Row = Row< + MysqlParameterType + >, + >( + strings: TemplateStringsArray, + ...parameters: (MysqlParameterType)[] + ): Promise { + return this.#queryWrapper((conn) => conn.sql(strings, ...parameters)); + } + sqlArray< + T extends ArrayRow = ArrayRow< + MysqlParameterType + >, + >( + strings: TemplateStringsArray, + ...parameters: (MysqlParameterType)[] + ): Promise { + return this.#queryWrapper((conn) => + conn.sqlArray(strings, ...parameters) + ); + } - async pop(): Promise { - if (this._closed) { - throw new MysqlError("Connection pool is closed"); + beginTransaction( + options?: { + withConsistentSnapshot?: boolean | undefined; + readWrite?: "READ WRITE" | "READ ONLY" | undefined; + } | undefined, + ): Promise { + return this.#queryWrapper((conn) => conn.beginTransaction(options)); + } + transaction(fn: (t: MySqlTransaction) => Promise): Promise { + return this.#queryWrapper((conn) => conn.transaction(fn)); + } + async connect(): Promise { + for (let i = 0; i < this.deferredStack.maxSize; i++) { + const client = new MysqlPoolClient( + this.connectionUrl, + this.connectionOptions, + ); + client.release = () => this.release(client); + client.eventTarget = this.eventTarget; + if (!this.connectionOptions.lazyInitialization) { + await client.connect(); + } + this.deferredStack.push(client); } - let conn = this._deferred.tryPopAvailable(); - if (conn) { - conn.exitIdle(); - } else { - conn = await this._deferred.pop(); + } + async close(): Promise { + for (const client of this.deferredStack.stack) { + await client.close(); } + } + addEventListener( + type: SqlxPoolConnectionEventType, + listener: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions | undefined, + ): void { + return this.eventTarget.addEventListener(type, listener, options); + } + removeEventListener( + type: SqlxPoolConnectionEventType, + callback: EventListenerOrEventListenerObject | null, + options?: boolean | EventListenerOptions | undefined, + ): void { + return this.eventTarget.removeEventListener(type, callback, options); + } + + dispatchEvent(event: Event): boolean { + return this.eventTarget.dispatchEvent(event); + } + + async acquire(): Promise { + const conn = await this.deferredStack.pop(); + dispatchEvent(new MysqlPoolConnectionAcquireEvent({ connection: conn })); return conn; } - remove(conn: PoolConnection) { - return this._deferred.remove(conn); + async release(connection: MysqlPoolClient): Promise { + dispatchEvent( + new MysqlPoolConnectionReleaseEvent({ connection: connection }), + ); + try { + this.deferredStack.push(connection); + } catch (e) { + if (e instanceof SqlxError && e.message === "Max pool size reached") { + logger().debug(e.message); + await connection.close(); + } else { + throw e; + } + } } - /** - * Close the pool and all connections in the pool. - * - * After closing, pop() will throw an error, - * push() will close the connection immediately. - */ - close() { - this._closed = true; + async destroy(connection: MysqlPoolClient): Promise { + dispatchEvent( + new MysqlPoolConnectionDestroyEvent({ connection: connection }), + ); + await connection.close(); + } - let conn: PoolConnection | undefined; - while (conn = this._deferred.tryPopAvailable()) { - conn.exitIdle(); - conn.close(); - this.reduceSize(); + async #queryWrapper(fn: (connection: MysqlClient) => Promise) { + const conn = await this.acquire(); + let res: T | undefined = undefined; + let err: Error | undefined = undefined; + try { + res = await fn(conn); + } catch (e) { + err = e; + } + await this.release(conn); + if (err) { + throw err; + } + if (!res) { + throw new MysqlError("No result"); } + return res; } - reduceSize() { - this._deferred.reduceSize(); + async [Symbol.asyncDispose](): Promise { + await this.close(); } } diff --git a/lib/client.test.ts b/lib/sqlx.test.ts similarity index 89% rename from lib/client.test.ts rename to lib/sqlx.test.ts index 2508467..04397cf 100644 --- a/lib/client.test.ts +++ b/lib/sqlx.test.ts @@ -1,11 +1,13 @@ import { MysqlClient } from "./client.ts"; +import { MysqlClientPool } from "./pool.ts"; import { URL_TEST_CONNECTION } from "./utils/testing.ts"; import { implementationTest } from "@halvardm/sqlx/testing"; -Deno.test("MysqlClient", async (t) => { +Deno.test("MySQL SQLx", async (t) => { await implementationTest({ t, Client: MysqlClient, + PoolClient: MysqlClientPool as any, connectionUrl: URL_TEST_CONNECTION, connectionOptions: {}, queries: { diff --git a/lib/utils/deferred.ts b/lib/utils/deferred.ts new file mode 100644 index 0000000..02aae1f --- /dev/null +++ b/lib/utils/deferred.ts @@ -0,0 +1,51 @@ +import { + type SqlxConnectionPoolOptions, + type SqlxDeferredStack, + SqlxError, +} from "@halvardm/sqlx"; +import type { MysqlPoolClient } from "../pool.ts"; + +export type DeferredStackOptions = SqlxConnectionPoolOptions; + +export class DeferredStack implements SqlxDeferredStack { + readonly maxSize: number; + stack: Array; + queue: Array>; + + get availableCount(): number { + return this.stack.length; + } + get queuedCount(): number { + return this.queue.length; + } + constructor(options: DeferredStackOptions) { + this.maxSize = options.poolSize ?? 10; + this.stack = []; + this.queue = []; + } + + push(client: MysqlPoolClient): void { + if (this.queue.length) { + const p = this.queue.shift()!; + p.resolve(client); + } else if (this.queue.length >= this.maxSize) { + throw new SqlxError("Max pool size reached"); + } else { + this.stack.push(client); + } + } + + async pop(): Promise { + const res = this.stack.pop(); + + if (res) { + await res.connect(); + return res; + } + + const p = Promise.withResolvers(); + this.queue.push(p); + + return p.promise; + } +} diff --git a/lib/utils/events.ts b/lib/utils/events.ts new file mode 100644 index 0000000..5c5df33 --- /dev/null +++ b/lib/utils/events.ts @@ -0,0 +1,71 @@ +import { + type SqlxConnectionEventInit, + SqlxPoolConnectionAcquireEvent, + SqlxPoolConnectionDestroyEvent, + SqlxPoolConnectionReleaseEvent, +} from "@halvardm/sqlx"; +import type { MysqlParameterType } from "../packets/parsers/result.ts"; +import type { + MysqlPrepared, + MysqlQueryOptions, + MySqlTransaction, + MysqlTransactionOptions, +} from "../client.ts"; +import type { MysqlPoolClient } from "../pool.ts"; + +export class MysqlPoolConnectionAcquireEvent + extends SqlxPoolConnectionAcquireEvent< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + MysqlPoolClient, + SqlxConnectionEventInit< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + MysqlPoolClient + > + > { +} + +export class MysqlPoolConnectionReleaseEvent + extends SqlxPoolConnectionReleaseEvent< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + MysqlPoolClient, + SqlxConnectionEventInit< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + MysqlPoolClient + > + > { +} + +export class MysqlPoolConnectionDestroyEvent + extends SqlxPoolConnectionDestroyEvent< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + MysqlPoolClient, + SqlxConnectionEventInit< + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction, + MysqlPoolClient + > + > { +} diff --git a/testold.ts b/testold.ts deleted file mode 100644 index 9463348..0000000 --- a/testold.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { assertEquals, assertRejects } from "@std/assert"; -import { lessThan, parse } from "@std/semver"; -import { - MysqlConnectionError, - MysqlResponseTimeoutError, -} from "./lib/utils/errors.ts"; -import { - createTestDB, - delay, - isMariaDB, - registerTests, - testWithClient, -} from "./lib/utils/testing.ts"; -import * as stdlog from "@std/log"; -import { configLogger } from "./mod.ts"; -import { logger } from "./lib/logger.ts"; - -testWithClient(async function testCreateDb(client) { - await client.query(`CREATE DATABASE IF NOT EXISTS enok`); -}); - -testWithClient(async function testCreateTable(client) { - await client.query(`DROP TABLE IF EXISTS users`); - await client.query(` - CREATE TABLE users ( - id int(11) NOT NULL AUTO_INCREMENT, - name varchar(100) NOT NULL, - is_top tinyint(1) default 0, - created_at timestamp not null default current_timestamp, - PRIMARY KEY (id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `); -}); - -testWithClient(async function testInsert(client) { - let result = await client.execute(`INSERT INTO users(name) values(?)`, [ - "manyuanrong", - ]); - assertEquals(result, { affectedRows: 1, lastInsertId: 1 }); - result = await client.execute(`INSERT INTO users ?? values ?`, [ - ["id", "name"], - [2, "MySQL"], - ]); - assertEquals(result, { affectedRows: 1, lastInsertId: 2 }); -}); - -testWithClient(async function testUpdate(client) { - let result = await client.execute( - `update users set ?? = ?, ?? = ? WHERE id = ?`, - ["name", "MYR🦕", "created_at", new Date(), 1], - ); - assertEquals(result, { affectedRows: 1, lastInsertId: 0 }); -}); - -testWithClient(async function testQuery(client) { - let result = await client.query( - "select ??,`is_top`,`name` from ?? where id = ?", - ["id", "users", 1], - ); - assertEquals(result, [{ id: 1, name: "MYR🦕", is_top: 0 }]); -}); - -testWithClient(async function testQueryErrorOccurred(client) { - assertEquals(client.pool, { - size: 0, - maxSize: client.config.poolSize, - available: 0, - }); - await assertRejects( - () => client.query("select unknownfield from `users`"), - Error, - ); - await client.query("select 1"); - assertEquals(client.pool, { - size: 1, - maxSize: client.config.poolSize, - available: 1, - }); -}); - -testWithClient(async function testQueryList(client) { - const sql = "select ??,?? from ??"; - let result = await client.query(sql, ["id", "name", "users"]); - assertEquals(result, [ - { id: 1, name: "MYR🦕" }, - { id: 2, name: "MySQL" }, - ]); -}); - -testWithClient(async function testQueryTime(client) { - const sql = `SELECT CAST("09:04:10" AS time) as time`; - let result = await client.query(sql); - assertEquals(result, [{ time: "09:04:10" }]); -}); - -testWithClient(async function testQueryBigint(client) { - await client.query(`DROP TABLE IF EXISTS test_bigint`); - await client.query(`CREATE TABLE test_bigint ( - id int(11) NOT NULL AUTO_INCREMENT, - bigint_column bigint NOT NULL, - PRIMARY KEY (id) - ) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4`); - - const value = "9223372036854775807"; - await client.execute( - "INSERT INTO test_bigint(bigint_column) VALUES (?)", - [value], - ); - - const result = await client.query("SELECT bigint_column FROM test_bigint"); - assertEquals(result, [{ bigint_column: BigInt(value) }]); -}); - -testWithClient(async function testQueryDecimal(client) { - await client.query(`DROP TABLE IF EXISTS test_decimal`); - await client.query(`CREATE TABLE test_decimal ( - id int(11) NOT NULL AUTO_INCREMENT, - decimal_column decimal(65,30) NOT NULL, - PRIMARY KEY (id) - ) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4`); - - const value = "0.012345678901234567890123456789"; - await client.execute( - "INSERT INTO test_decimal(decimal_column) VALUES (?)", - [value], - ); - - const result = await client.query("SELECT decimal_column FROM test_decimal"); - assertEquals(result, [{ decimal_column: value }]); -}); - -testWithClient(async function testQueryDatetime(client) { - await client.useConnection(async (connection) => { - if ( - isMariaDB(connection) || - lessThan(parse(connection.serverVersion), parse("5.6.0")) - ) { - return; - } - - await client.query(`DROP TABLE IF EXISTS test_datetime`); - await client.query(`CREATE TABLE test_datetime ( - id int(11) NOT NULL AUTO_INCREMENT, - datetime datetime(6) NOT NULL, - PRIMARY KEY (id) - ) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4`); - const datetime = new Date(); - await client.execute( - ` - INSERT INTO test_datetime (datetime) - VALUES (?)`, - [datetime], - ); - - const [row] = await client.query("SELECT datetime FROM test_datetime"); - assertEquals(row.datetime.toISOString(), datetime.toISOString()); // See https://github.com/denoland/deno/issues/6643 - }); -}); - -testWithClient(async function testDelete(client) { - let result = await client.execute(`delete from users where ?? = ?`, [ - "id", - 1, - ]); - assertEquals(result, { affectedRows: 1, lastInsertId: 0 }); -}); - -testWithClient(async function testPool(client) { - assertEquals(client.pool, { - maxSize: client.config.poolSize, - available: 0, - size: 0, - }); - const expect = new Array(10).fill([{ "1": 1 }]); - const result = await Promise.all(expect.map(() => client.query(`select 1`))); - - assertEquals(client.pool, { - maxSize: client.config.poolSize, - available: 3, - size: 3, - }); - assertEquals(result, expect); -}); - -testWithClient(async function testQueryOnClosed(client) { - for (const i of [0, 0, 0]) { - await assertRejects(async () => { - await client.transaction(async (conn) => { - conn.close(); - await conn.query("SELECT 1"); - }); - }, MysqlConnectionError); - } - assertEquals(client.pool?.size, 0); - await client.query("select 1"); -}); - -testWithClient(async function testTransactionSuccess(client) { - const success = await client.transaction(async (connection) => { - await connection.execute("insert into users(name) values(?)", [ - "transaction1", - ]); - await connection.execute("delete from users where id = ?", [2]); - return true; - }); - assertEquals(true, success); - const result = await client.query("select name,id from users"); - assertEquals([{ name: "transaction1", id: 3 }], result); -}); - -testWithClient(async function testTransactionRollback(client) { - let success; - await assertRejects(async () => { - success = await client.transaction(async (connection) => { - // Insert an existing id - await connection.execute("insert into users(name,id) values(?,?)", [ - "transaction2", - 3, - ]); - return true; - }); - }); - assertEquals(undefined, success); - const result = await client.query("select name from users"); - assertEquals([{ name: "transaction1" }], result); -}); - -testWithClient(async function testIdleTimeout(client) { - assertEquals(client.pool, { - maxSize: 3, - available: 0, - size: 0, - }); - await Promise.all(new Array(10).fill(0).map(() => client.query("select 1"))); - assertEquals(client.pool, { - maxSize: 3, - available: 3, - size: 3, - }); - await delay(500); - assertEquals(client.pool, { - maxSize: 3, - available: 3, - size: 3, - }); - await client.query("select 1"); - await delay(500); - assertEquals(client.pool, { - maxSize: 3, - available: 1, - size: 1, - }); - await delay(500); - assertEquals(client.pool, { - maxSize: 3, - available: 0, - size: 0, - }); -}, { - idleTimeout: 750, -}); - -testWithClient(async function testReadTimeout(client) { - await client.execute("select sleep(0.3)"); - - await assertRejects(async () => { - await client.execute("select sleep(0.7)"); - }, MysqlResponseTimeoutError); - - assertEquals(client.pool, { - maxSize: 3, - available: 0, - size: 0, - }); -}, { - timeout: 500, -}); - -testWithClient(async function testLargeQueryAndResponse(client) { - function buildLargeString(len: number) { - let str = ""; - for (let i = 0; i < len; i++) { - str += i % 10; - } - return str; - } - const largeString = buildLargeString(512 * 1024); - assertEquals( - await client.query(`select "${largeString}" as str`), - [{ str: largeString }], - ); -}); - -testWithClient(async function testExecuteIterator(client) { - await client.useConnection(async (conn) => { - await conn.execute(`DROP TABLE IF EXISTS numbers`); - await conn.execute(`CREATE TABLE numbers (num INT NOT NULL)`); - await conn.execute( - `INSERT INTO numbers (num) VALUES ${ - new Array(64).fill(0).map((v, idx) => `(${idx})`).join(",") - }`, - ); - const r = await conn.execute(`SELECT num FROM numbers`, [], true); - let count = 0; - for await (const row of r.iterator) { - assertEquals(row.num, count); - count++; - } - assertEquals(count, 64); - }); -}); - -// For MySQL 8, the default auth plugin is `caching_sha2_password`. Create user -// using `mysql_native_password` to test Authentication Method Mismatch. -testWithClient(async function testCreateUserWithMysqlNativePassword(client) { - const { version } = (await client.query(`SELECT VERSION() as version`))[0]; - if (version.startsWith("8.")) { - // MySQL 8 does not have `PASSWORD()` function - await client.execute( - `CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password BY 'testpassword'`, - ); - } else { - await client.execute( - `CREATE USER 'testuser'@'%' IDENTIFIED WITH mysql_native_password`, - ); - await client.execute( - `SET PASSWORD FOR 'testuser'@'%' = PASSWORD('testpassword')`, - ); - } - await client.execute(`GRANT ALL ON test.* TO 'testuser'@'%'`); -}); - -testWithClient(async function testConnectWithMysqlNativePassword(client) { - assertEquals( - await client.query(`SELECT CURRENT_USER() AS user`), - [{ user: "testuser@%" }], - ); -}, { username: "testuser", password: "testpassword" }); - -testWithClient(async function testDropUserWithMysqlNativePassword(client) { - await client.execute(`DROP USER 'testuser'@'%'`); -}); - -testWithClient(async function testSelectEmptyString(client) { - assertEquals( - await client.query(`SELECT '' AS a`), - [{ a: "" }], - ); - assertEquals( - await client.query(`SELECT '' AS a, '' AS b, '' AS c`), - [{ a: "", b: "", c: "" }], - ); - assertEquals( - await client.query(`SELECT '' AS a, 'b' AS b, '' AS c`), - [{ a: "", b: "b", c: "" }], - ); -}); - -registerTests(); - -Deno.test("configLogger()", async () => { - let logCount = 0; - const fakeHandler = new class extends stdlog.BaseHandler { - constructor() { - super("INFO"); - } - log(msg: string) { - logCount++; - } - }(); - - await stdlog.setup({ - handlers: { - fake: fakeHandler, - }, - loggers: { - mysql: { - handlers: ["fake"], - }, - }, - }); - await configLogger({ logger: stdlog.getLogger("mysql") }); - logger().info("Test log"); - assertEquals(logCount, 1); - - await configLogger({ enable: false }); - logger().info("Test log"); - assertEquals(logCount, 1); -}); - -await createTestDB(); - -await new Promise((r) => setTimeout(r, 0)); -// Workaround to https://github.com/denoland/deno/issues/7844 From 64a7d2f0b367cc8f42e9f6f62ae1dc43eb2f99cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 21 Apr 2024 17:30:29 +0200 Subject: [PATCH 26/38] implemented SQLx interface --- compose.yml | 14 ++ deno.json | 5 +- lib/client.ts | 332 +++++++++++++++++++--------------- lib/connection.ts | 14 +- lib/packets/parsers/result.ts | 11 +- lib/pool.ts | 317 +++++++++----------------------- lib/sqlx.test.ts | 32 +++- lib/utils/bytes.test.ts | 5 + lib/utils/bytes.ts | 12 +- lib/utils/deferred.ts | 51 ------ lib/utils/errors.ts | 6 + lib/utils/events.ts | 93 ++++------ lib/utils/logger.ts | 19 +- lib/utils/testing.ts | 25 ++- mod.ts | 15 +- 15 files changed, 421 insertions(+), 530 deletions(-) delete mode 100644 lib/utils/deferred.ts diff --git a/compose.yml b/compose.yml index b18af86..cfb258a 100644 --- a/compose.yml +++ b/compose.yml @@ -6,8 +6,22 @@ services: - 3306:3306 environment: MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_DATABASE: testdb healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--user", "root"] interval: 3s timeout: 3s retries: 10 + mariadb: + image: mariadb + restart: always + ports: + - 3307:3306 + environment: + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true + MARIADB_DATABASE: testdb + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1"] + interval: 3s + timeout: 3s + retries: 10 diff --git a/deno.json b/deno.json index 04ccd24..56dc7d1 100644 --- a/deno.json +++ b/deno.json @@ -9,11 +9,10 @@ "format:check": "deno fmt --check", "type:check": "deno check mod.ts", "doc:check": "deno doc --lint src", - "test": "deno task db:restart && deno test --allow-env --allow-net=127.0.0.1:3306 ./test.ts; deno task db:stop", + "test": "deno task db:restart && deno test -A; deno task db:stop", "db:restart": "deno task db:stop && deno task db:start", "db:start": "docker compose up -d --remove-orphans --wait && sleep 2", - "db:stop": "docker compose down --remove-orphans --volumes", - "typedoc": "deno run -A npm:typedoc --theme minimal --ignoreCompilerErrors --excludePrivate --excludeExternals --entryPoint client.ts --mode file ./src --out ./docs" + "db:stop": "docker compose down --remove-orphans --volumes" }, "imports": { "@halvardm/sqlx": "../deno-sqlx/mod.ts", diff --git a/lib/client.ts b/lib/client.ts index 13df578..307976f 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,10 +1,10 @@ import { type ArrayRow, type Row, - type SqlxConnection, + SqlxBase, + type SqlxClient, SqlxConnectionCloseEvent, SqlxConnectionConnectEvent, - type SqlxConnectionEventType, type SqlxPreparable, type SqlxPreparedQueriable, type SqlxQueriable, @@ -12,11 +12,12 @@ import { type SqlxTransactionable, type SqlxTransactionOptions, type SqlxTransactionQueriable, - VERSION, } from "@halvardm/sqlx"; import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; import { buildQuery } from "./packets/builders/query.ts"; import type { MysqlParameterType } from "./packets/parsers/result.ts"; +import { MysqlTransactionError } from "./utils/errors.ts"; +import { MysqlEventTarget } from "./utils/events.ts"; export interface MysqlTransactionOptions extends SqlxTransactionOptions { beginTransactionOptions: { @@ -40,90 +41,26 @@ export interface MysqlClientOptions extends MysqlConnectionOptions { export interface MysqlQueryOptions extends SqlxQueryOptions { } -/** - * Prepared statement - * - * @todo implement prepared statements properly - */ -export class MysqlPrepared - implements SqlxPreparedQueriable { - readonly sqlxVersion = VERSION; +export class MysqlQueriable extends SqlxBase implements + SqlxQueriable< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions + > { + readonly connection: MysqlConnection; readonly queryOptions: MysqlQueryOptions; - #sql: string; - - #queriable: MysqlQueriable; - - constructor( - connection: MysqlConnection, - sql: string, - options: MysqlQueryOptions = {}, - ) { - this.#queriable = new MysqlQueriable(connection); - this.#sql = sql; - this.queryOptions = options; - } - - execute( - params?: MysqlParameterType[] | undefined, - _options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.execute(this.#sql, params); - } - query = Row>( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.query(this.#sql, params, options); - } - queryOne = Row>( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.queryOne(this.#sql, params, options); - } - queryMany = Row>( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): AsyncIterableIterator { - return this.#queriable.queryMany(this.#sql, params, options); - } - queryArray< - T extends ArrayRow = ArrayRow, - >( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.queryArray(this.#sql, params, options); - } - queryOneArray< - T extends ArrayRow = ArrayRow, - >( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.queryOneArray(this.#sql, params, options); - } - queryManyArray< - T extends ArrayRow = ArrayRow, - >( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): AsyncIterableIterator { - return this.#queriable.queryManyArray(this.#sql, params, options); + get connected(): boolean { + throw new Error("Method not implemented."); } -} - -export class MysqlQueriable - implements SqlxQueriable { - protected readonly connection: MysqlConnection; - readonly queryOptions: MysqlQueryOptions; - readonly sqlxVersion: string = VERSION; constructor( connection: MysqlConnection, queryOptions: MysqlQueryOptions = {}, ) { + super(); this.connection = connection; this.queryOptions = queryOptions; } @@ -189,7 +126,7 @@ export class MysqlQueriable sql: string, params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, - ): AsyncIterableIterator { + ): AsyncGenerator { const data = buildQuery(sql, params); for await ( const res of this.connection.queryManyArrayRaw(data, options) @@ -213,9 +150,101 @@ export class MysqlQueriable } } -export class MysqlPreparable extends MysqlQueriable - implements - SqlxPreparable { +/** + * Prepared statement + * + * @todo implement prepared statements properly + */ +export class MysqlPrepared extends SqlxBase implements + SqlxPreparedQueriable< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions + > { + readonly sql: string; + readonly queryOptions: MysqlQueryOptions; + + #queriable: MysqlQueriable; + + connection: MysqlConnection; + + get connected(): boolean { + return this.connection.connected; + } + + constructor( + connection: MysqlConnection, + sql: string, + options: MysqlQueryOptions = {}, + ) { + super(); + this.connection = connection; + this.#queriable = new MysqlQueriable(connection); + this.sql = sql; + this.queryOptions = options; + } + + execute( + params?: MysqlParameterType[] | undefined, + _options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.execute(this.sql, params); + } + query = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.query(this.sql, params, options); + } + queryOne = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryOne(this.sql, params, options); + } + queryMany = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + return this.#queriable.queryMany(this.sql, params, options); + } + queryArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryArray(this.sql, params, options); + } + queryOneArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryOneArray(this.sql, params, options); + } + queryManyArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + return this.#queriable.queryManyArray(this.sql, params, options); + } +} + +export class MysqlPreparable extends MysqlQueriable implements + SqlxPreparable< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared + > { prepare(sql: string, options?: MysqlQueryOptions | undefined): MysqlPrepared { return new MysqlPrepared(this.connection, sql, options); } @@ -224,52 +253,80 @@ export class MysqlPreparable extends MysqlQueriable export class MySqlTransaction extends MysqlPreparable implements SqlxTransactionQueriable< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, MysqlParameterType, MysqlQueryOptions, MysqlTransactionOptions > { + #inTransaction: boolean = true; + get inTransaction(): boolean { + return this.connected && this.#inTransaction; + } + + get connected(): boolean { + if (!this.#inTransaction) { + throw new MysqlTransactionError( + "Transaction is not active, create a new one using beginTransaction", + ); + } + + return super.connected; + } + async commitTransaction( options?: MysqlTransactionOptions["commitTransactionOptions"], ): Promise { - let sql = "COMMIT"; + try { + let sql = "COMMIT"; - if (options?.chain === true) { - sql += " AND CHAIN"; - } else if (options?.chain === false) { - sql += " AND NO CHAIN"; - } + if (options?.chain === true) { + sql += " AND CHAIN"; + } else if (options?.chain === false) { + sql += " AND NO CHAIN"; + } - if (options?.release === true) { - sql += " RELEASE"; - } else if (options?.release === false) { - sql += " NO RELEASE"; + if (options?.release === true) { + sql += " RELEASE"; + } else if (options?.release === false) { + sql += " NO RELEASE"; + } + await this.execute(sql); + } catch (e) { + this.#inTransaction = false; + throw e; } - await this.execute(sql); } async rollbackTransaction( options?: MysqlTransactionOptions["rollbackTransactionOptions"], ): Promise { - let sql = "ROLLBACK"; + try { + let sql = "ROLLBACK"; - if (options?.savepoint) { - sql += ` TO ${options.savepoint}`; - await this.execute(sql); - return; - } + if (options?.savepoint) { + sql += ` TO ${options.savepoint}`; + await this.execute(sql); + return; + } - if (options?.chain === true) { - sql += " AND CHAIN"; - } else if (options?.chain === false) { - sql += " AND NO CHAIN"; - } + if (options?.chain === true) { + sql += " AND CHAIN"; + } else if (options?.chain === false) { + sql += " AND NO CHAIN"; + } - if (options?.release === true) { - sql += " RELEASE"; - } else if (options?.release === false) { - sql += " NO RELEASE"; - } + if (options?.release === true) { + sql += " RELEASE"; + } else if (options?.release === false) { + sql += " NO RELEASE"; + } - await this.execute(sql); + await this.execute(sql); + } catch (e) { + this.#inTransaction = false; + throw e; + } } async createSavepoint(name: string = `\t_bm.\t`): Promise { await this.execute(`SAVEPOINT ${name}`); @@ -285,6 +342,9 @@ export class MySqlTransaction extends MysqlPreparable export class MysqlTransactionable extends MysqlPreparable implements SqlxTransactionable< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, MysqlParameterType, MysqlQueryOptions, MysqlTransactionOptions, @@ -332,21 +392,19 @@ export class MysqlTransactionable extends MysqlPreparable * MySQL client */ export class MysqlClient extends MysqlTransactionable implements - SqlxConnection< + SqlxClient< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, MysqlParameterType, MysqlQueryOptions, MysqlPrepared, MysqlTransactionOptions, - MySqlTransaction, - SqlxConnectionEventType, - MysqlConnectionOptions + MySqlTransaction > { - readonly connectionUrl: string; - readonly connectionOptions: MysqlConnectionOptions; - eventTarget: EventTarget; - get connected(): boolean { - throw new Error("Method not implemented."); - } + eventTarget: MysqlEventTarget; + connectionUrl: string; + connectionOptions: MysqlConnectionOptions; constructor( connectionUrl: string | URL, @@ -354,36 +412,24 @@ export class MysqlClient extends MysqlTransactionable implements ) { const conn = new MysqlConnection(connectionUrl, connectionOptions); super(conn); - this.connectionUrl = conn.connectionUrl; - this.connectionOptions = conn.connectionOptions; - this.eventTarget = new EventTarget(); + this.connectionUrl = connectionUrl.toString(); + this.connectionOptions = connectionOptions; + this.eventTarget = new MysqlEventTarget(); } async connect(): Promise { await this.connection.connect(); - this.dispatchEvent(new SqlxConnectionConnectEvent()); + this.eventTarget.dispatchEvent( + new SqlxConnectionConnectEvent({ connectable: this }), + ); } async close(): Promise { - this.dispatchEvent(new SqlxConnectionCloseEvent()); + this.eventTarget.dispatchEvent( + new SqlxConnectionCloseEvent({ connectable: this }), + ); await this.connection.close(); } + async [Symbol.asyncDispose](): Promise { await this.close(); } - addEventListener( - type: SqlxConnectionEventType, - listener: EventListenerOrEventListenerObject | null, - options?: boolean | AddEventListenerOptions, - ): void { - this.eventTarget.addEventListener(type, listener, options); - } - removeEventListener( - type: SqlxConnectionEventType, - callback: EventListenerOrEventListenerObject | null, - options?: boolean | EventListenerOptions, - ): void { - this.eventTarget.removeEventListener(type, callback, options); - } - dispatchEvent(event: Event): boolean { - return this.eventTarget.dispatchEvent(event); - } } diff --git a/lib/connection.ts b/lib/connection.ts index 4795f36..7b01486 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -30,13 +30,14 @@ import { logger } from "./utils/logger.ts"; import type { ArrayRow, Row, - SqlxConnectable, + SqlxConnection, SqlxConnectionOptions, } from "@halvardm/sqlx"; import { VERSION } from "./utils/meta.ts"; import { resolve } from "@std/path"; import { toCamelCase } from "@std/text"; import { AuthPluginName } from "./auth_plugins/mod.ts"; +import type { MysqlEventTarget } from "./utils/events.ts"; /** * Connection state @@ -131,8 +132,11 @@ export interface MysqlConnectionOptions extends SqlxConnectionOptions { } /** Connection for mysql */ -export class MysqlConnection - implements SqlxConnectable { +export class MysqlConnection implements + SqlxConnection< + MysqlEventTarget, + MysqlConnectionOptions + > { state: ConnectionState = ConnectionState.CONNECTING; capabilities: number = 0; serverVersion: string = ""; @@ -144,6 +148,7 @@ export class MysqlConnection readonly connectionOptions: MysqlConnectionOptions; readonly config: ConnectionConfig; readonly sqlxVersion: string = VERSION; + eventTarget: MysqlEventTarget; get conn(): Deno.Conn { if (!this._conn) { @@ -175,6 +180,7 @@ export class MysqlConnection connectionUrl, connectionOptions, ); + this.eventTarget = new EventTarget(); } async connect(): Promise { @@ -231,7 +237,7 @@ export class MysqlConnection tlsData, ++handshakeSequenceNumber, ); - this.conn = await Deno.startTls(this.conn, { + this.conn = await Deno.startTls(this.conn as Deno.TcpConn, { hostname: this.config.hostname, caCerts: this.config.tls?.caCerts, }); diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index eb9c525..2460000 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -1,15 +1,8 @@ import type { BufferReader } from "../../utils/buffer.ts"; import { MysqlDataType } from "../../constant/mysql_types.ts"; -import type { - ArrayRow, - Row, - SqlxParameterType, - SqlxQueryOptions, -} from "@halvardm/sqlx"; +import type { ArrayRow, Row, SqlxQueryOptions } from "@halvardm/sqlx"; -export type MysqlParameterType = SqlxParameterType< - string | number | bigint | Date | null ->; +export type MysqlParameterType = string | number | bigint | Date | null; /** * Field information diff --git a/lib/pool.ts b/lib/pool.ts index 987b3d2..00545dd 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -1,311 +1,166 @@ import { - type SqlxConnectionPool, - type SqlxConnectionPoolOptions, + SqlxBase, + type SqlxClientPool, + type SqlxClientPoolOptions, + type SqlxConnectionOptions, + SqlxDeferredStack, SqlxError, - type SqlxPoolConnection, - type SqlxPoolConnectionEventType, - VERSION, + type SqlxPoolClient, } from "@halvardm/sqlx"; import { - MysqlClient, type MysqlPrepared, type MysqlQueryOptions, type MySqlTransaction, + MysqlTransactionable, type MysqlTransactionOptions, } from "./client.ts"; -import type { MysqlConnectionOptions } from "./connection.ts"; +import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; import type { MysqlParameterType } from "./packets/parsers/result.ts"; -import { DeferredStack } from "./utils/deferred.ts"; -import type { ArrayRow, Row } from "../../deno-sqlx/lib/interfaces.ts"; import { + MysqlConnectionCloseEvent, + MysqlConnectionConnectEvent, MysqlPoolConnectionAcquireEvent, MysqlPoolConnectionDestroyEvent, MysqlPoolConnectionReleaseEvent, } from "./utils/events.ts"; import { logger } from "./utils/logger.ts"; -import { MysqlError } from "./utils/errors.ts"; +import { MysqlEventTarget } from "./utils/events.ts"; export interface MysqlClientPoolOptions - extends MysqlConnectionOptions, SqlxConnectionPoolOptions { + extends MysqlConnectionOptions, SqlxClientPoolOptions { } -export class MysqlPoolClient extends MysqlClient implements - SqlxPoolConnection< - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared, - MysqlTransactionOptions, - MySqlTransaction - > { +export class MysqlPoolClient extends MysqlTransactionable + implements + SqlxPoolClient< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared, + MysqlTransactionOptions, + MySqlTransaction + > { + /** + * Must be set by the client pool on creation + * @inheritdoc + */ release(): Promise { throw new Error("Method not implemented."); } + + async [Symbol.asyncDispose](): Promise { + await this.release(); + } } -export class MysqlClientPool implements - SqlxConnectionPool< +export class MysqlClientPool extends SqlxBase implements + SqlxClientPool< + MysqlEventTarget, + MysqlConnectionOptions, + MysqlConnection, MysqlParameterType, MysqlQueryOptions, MysqlPrepared, MysqlTransactionOptions, MySqlTransaction, - SqlxPoolConnectionEventType, - MysqlClientPoolOptions, MysqlPoolClient, - DeferredStack + SqlxDeferredStack > { - readonly sqlxVersion: string = VERSION; readonly connectionUrl: string; - readonly connectionOptions: MysqlClientPoolOptions; - readonly queryOptions: MysqlQueryOptions; + readonly connectionOptions: SqlxConnectionOptions; readonly eventTarget: EventTarget; - readonly deferredStack: DeferredStack; + readonly deferredStack: SqlxDeferredStack; + readonly queryOptions: MysqlQueryOptions; + + #connected: boolean = false; + get connected(): boolean { - throw new Error("Method not implemented."); + return this.#connected; } constructor( connectionUrl: string | URL, connectionOptions: MysqlClientPoolOptions = {}, ) { + super(); this.connectionUrl = connectionUrl.toString(); this.connectionOptions = connectionOptions; this.queryOptions = connectionOptions; - this.eventTarget = new EventTarget(); - this.deferredStack = new DeferredStack(connectionOptions); - } - - async execute( - sql: string, - params?: (MysqlParameterType)[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - const conn = await this.acquire(); - let res: number | undefined = undefined; - let err: Error | undefined = undefined; - try { - res = await conn.execute(sql, params, options); - } catch (e) { - err = e; - } - await this.release(conn); - if (err) { - throw err; - } - return res; - } - query< - T extends Row = Row< - MysqlParameterType - >, - >( - sql: string, - params?: (MysqlParameterType)[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queryWrapper((conn) => conn.query(sql, params, options)); - } - queryOne< - T extends Row = Row< - MysqlParameterType - >, - >( - sql: string, - params?: (MysqlParameterType)[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queryWrapper((conn) => conn.queryOne(sql, params, options)); - } - async *queryMany< - T extends Row = Row< - MysqlParameterType - >, - >( - sql: string, - params?: (MysqlParameterType)[] | undefined, - options?: MysqlQueryOptions | undefined, - ): AsyncGenerator { - const conn = await this.acquire(); - let err: Error | undefined = undefined; - try { - for await (const row of conn.queryMany(sql, params, options)) { - yield row; - } - } catch (e) { - err = e; - } - await this.release(conn); - if (err) { - throw err; - } - } - queryArray< - T extends ArrayRow = ArrayRow< - MysqlParameterType - >, - >( - sql: string, - params?: (MysqlParameterType)[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queryWrapper((conn) => - conn.queryArray(sql, params, options) - ); - } - queryOneArray< - T extends ArrayRow = ArrayRow< - MysqlParameterType - >, - >( - sql: string, - params?: (MysqlParameterType)[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queryWrapper((conn) => - conn.queryOneArray(sql, params, options) - ); - } - async *queryManyArray< - T extends ArrayRow = ArrayRow< - MysqlParameterType - >, - >( - sql: string, - params?: (MysqlParameterType)[] | undefined, - options?: MysqlQueryOptions | undefined, - ): AsyncGenerator { - const conn = await this.acquire(); - let err: Error | undefined = undefined; - try { - for await (const row of conn.queryManyArray(sql, params, options)) { - yield row; - } - } catch (e) { - err = e; - } - await this.release(conn); - if (err) { - throw err; - } - } - sql< - T extends Row = Row< - MysqlParameterType - >, - >( - strings: TemplateStringsArray, - ...parameters: (MysqlParameterType)[] - ): Promise { - return this.#queryWrapper((conn) => conn.sql(strings, ...parameters)); - } - sqlArray< - T extends ArrayRow = ArrayRow< - MysqlParameterType - >, - >( - strings: TemplateStringsArray, - ...parameters: (MysqlParameterType)[] - ): Promise { - return this.#queryWrapper((conn) => - conn.sqlArray(strings, ...parameters) + this.eventTarget = new MysqlEventTarget(); + this.deferredStack = new SqlxDeferredStack( + connectionOptions, ); } - beginTransaction( - options?: { - withConsistentSnapshot?: boolean | undefined; - readWrite?: "READ WRITE" | "READ ONLY" | undefined; - } | undefined, - ): Promise { - return this.#queryWrapper((conn) => conn.beginTransaction(options)); - } - transaction(fn: (t: MySqlTransaction) => Promise): Promise { - return this.#queryWrapper((conn) => conn.transaction(fn)); - } async connect(): Promise { for (let i = 0; i < this.deferredStack.maxSize; i++) { - const client = new MysqlPoolClient( + const conn = new MysqlConnection( this.connectionUrl, this.connectionOptions, ); + const client = new MysqlPoolClient( + conn, + this.queryOptions, + ); client.release = () => this.release(client); - client.eventTarget = this.eventTarget; + if (!this.connectionOptions.lazyInitialization) { - await client.connect(); + await client.connection.connect(); + this.eventTarget.dispatchEvent( + new MysqlConnectionConnectEvent({ connectable: client }), + ); } + this.deferredStack.push(client); } + + this.#connected = true; } + async close(): Promise { - for (const client of this.deferredStack.stack) { - await client.close(); - } - } - addEventListener( - type: SqlxPoolConnectionEventType, - listener: EventListenerOrEventListenerObject | null, - options?: boolean | AddEventListenerOptions | undefined, - ): void { - return this.eventTarget.addEventListener(type, listener, options); - } - removeEventListener( - type: SqlxPoolConnectionEventType, - callback: EventListenerOrEventListenerObject | null, - options?: boolean | EventListenerOptions | undefined, - ): void { - return this.eventTarget.removeEventListener(type, callback, options); - } + this.#connected = false; - dispatchEvent(event: Event): boolean { - return this.eventTarget.dispatchEvent(event); + for (const client of this.deferredStack.elements) { + this.eventTarget.dispatchEvent( + new MysqlConnectionCloseEvent({ connectable: client }), + ); + await client.connection.close(); + } } async acquire(): Promise { - const conn = await this.deferredStack.pop(); - dispatchEvent(new MysqlPoolConnectionAcquireEvent({ connection: conn })); - return conn; + const client = await this.deferredStack.pop(); + + this.eventTarget.dispatchEvent( + new MysqlPoolConnectionAcquireEvent({ connectable: client }), + ); + return client; } - async release(connection: MysqlPoolClient): Promise { - dispatchEvent( - new MysqlPoolConnectionReleaseEvent({ connection: connection }), + async release(client: MysqlPoolClient): Promise { + this.eventTarget.dispatchEvent( + new MysqlPoolConnectionReleaseEvent({ connectable: client }), ); try { - this.deferredStack.push(connection); + this.deferredStack.push(client); } catch (e) { if (e instanceof SqlxError && e.message === "Max pool size reached") { logger().debug(e.message); - await connection.close(); + await client.connection.close(); + throw e; } else { throw e; } } } - async destroy(connection: MysqlPoolClient): Promise { - dispatchEvent( - new MysqlPoolConnectionDestroyEvent({ connection: connection }), + async destroy(client: MysqlPoolClient): Promise { + this.eventTarget.dispatchEvent( + new MysqlPoolConnectionDestroyEvent({ connectable: client }), ); - await connection.close(); - } - - async #queryWrapper(fn: (connection: MysqlClient) => Promise) { - const conn = await this.acquire(); - let res: T | undefined = undefined; - let err: Error | undefined = undefined; - try { - res = await fn(conn); - } catch (e) { - err = e; - } - await this.release(conn); - if (err) { - throw err; - } - if (!res) { - throw new MysqlError("No result"); - } - return res; + await client.connection.close(); } async [Symbol.asyncDispose](): Promise { diff --git a/lib/sqlx.test.ts b/lib/sqlx.test.ts index 04397cf..679362f 100644 --- a/lib/sqlx.test.ts +++ b/lib/sqlx.test.ts @@ -1,12 +1,16 @@ import { MysqlClient } from "./client.ts"; import { MysqlClientPool } from "./pool.ts"; -import { URL_TEST_CONNECTION } from "./utils/testing.ts"; +import { + URL_TEST_CONNECTION, + URL_TEST_CONNECTION_MARIADB, +} from "./utils/testing.ts"; import { implementationTest } from "@halvardm/sqlx/testing"; Deno.test("MySQL SQLx", async (t) => { await implementationTest({ t, Client: MysqlClient, + // deno-lint-ignore no-explicit-any PoolClient: MysqlClientPool as any, connectionUrl: URL_TEST_CONNECTION, connectionOptions: {}, @@ -27,3 +31,29 @@ Deno.test("MySQL SQLx", async (t) => { }, }); }); + +Deno.test("MariaDB SQLx", async (t) => { + await implementationTest({ + t, + Client: MysqlClient, + // deno-lint-ignore no-explicit-any + PoolClient: MysqlClientPool as any, + connectionUrl: URL_TEST_CONNECTION_MARIADB, + connectionOptions: {}, + queries: { + createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", + dropTable: "DROP TABLE IF EXISTS sqlxtesttable", + insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", + insertManyToTable: + "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", + selectOneFromTable: + "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", + selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", + selectManyFromTable: "SELECT * FROM sqlxtesttable", + select1AsString: "SELECT '1' as result", + select1Plus1AsNumber: "SELECT 1+1 as result", + deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", + deleteAllFromTable: "DELETE FROM sqlxtesttable", + }, + }); +}); diff --git a/lib/utils/bytes.test.ts b/lib/utils/bytes.test.ts index 347865b..6b88a44 100644 --- a/lib/utils/bytes.test.ts +++ b/lib/utils/bytes.test.ts @@ -140,4 +140,9 @@ Deno.test("hexdump", async (t) => { const result = hexdump(new DataView(buffer8Compatible.buffer)); assertEquals(result, buffer8Result); }); + + await t.step("ArrayBuffer", () => { + const result = hexdump(buffer8Compatible.buffer); + assertEquals(result, buffer8Result); + }); }); diff --git a/lib/utils/bytes.ts b/lib/utils/bytes.ts index 87f644c..5a5d38b 100644 --- a/lib/utils/bytes.ts +++ b/lib/utils/bytes.ts @@ -10,13 +10,19 @@ * // 00000020 68 65 20 6c 61 7a 79 20 64 6f 67 2e |he lazy dog.| * ``` */ -export function hexdump(bufferView: ArrayBufferView): string { - const bytes = new Uint8Array(bufferView.buffer); +export function hexdump(bufferView: ArrayBufferView | ArrayBuffer): string { + let bytes: Uint8Array; + if (ArrayBuffer.isView(bufferView)) { + bytes = new Uint8Array(bufferView.buffer); + } else { + bytes = new Uint8Array(bufferView); + } + const lines = []; for (let i = 0; i < bytes.length; i += 16) { const address = i.toString(16).padStart(8, "0"); - const block = bytes.slice(i, i + 16); // cut buffer into blocks of 16 + const block = bytes.slice(i, i + 16); const hexArray = []; const asciiArray = []; let padding = ""; diff --git a/lib/utils/deferred.ts b/lib/utils/deferred.ts deleted file mode 100644 index 02aae1f..0000000 --- a/lib/utils/deferred.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - type SqlxConnectionPoolOptions, - type SqlxDeferredStack, - SqlxError, -} from "@halvardm/sqlx"; -import type { MysqlPoolClient } from "../pool.ts"; - -export type DeferredStackOptions = SqlxConnectionPoolOptions; - -export class DeferredStack implements SqlxDeferredStack { - readonly maxSize: number; - stack: Array; - queue: Array>; - - get availableCount(): number { - return this.stack.length; - } - get queuedCount(): number { - return this.queue.length; - } - constructor(options: DeferredStackOptions) { - this.maxSize = options.poolSize ?? 10; - this.stack = []; - this.queue = []; - } - - push(client: MysqlPoolClient): void { - if (this.queue.length) { - const p = this.queue.shift()!; - p.resolve(client); - } else if (this.queue.length >= this.maxSize) { - throw new SqlxError("Max pool size reached"); - } else { - this.stack.push(client); - } - } - - async pop(): Promise { - const res = this.stack.pop(); - - if (res) { - await res.connect(); - return res; - } - - const p = Promise.withResolvers(); - this.queue.push(p); - - return p.promise; - } -} diff --git a/lib/utils/errors.ts b/lib/utils/errors.ts index 94eaaef..2e68cd9 100644 --- a/lib/utils/errors.ts +++ b/lib/utils/errors.ts @@ -36,6 +36,12 @@ export class MysqlProtocolError extends MysqlError { } } +export class MysqlTransactionError extends MysqlError { + constructor(msg: string) { + super(msg); + } +} + /** * Check if an error is a MysqlError */ diff --git a/lib/utils/events.ts b/lib/utils/events.ts index 5c5df33..4f3ff12 100644 --- a/lib/utils/events.ts +++ b/lib/utils/events.ts @@ -1,71 +1,50 @@ import { - type SqlxConnectionEventInit, + type SqlxConnectableBase, + SqlxConnectionCloseEvent, + SqlxConnectionConnectEvent, + type SqlxEventInit, + SqlxEventTarget, + type SqlxEventType, SqlxPoolConnectionAcquireEvent, SqlxPoolConnectionDestroyEvent, SqlxPoolConnectionReleaseEvent, } from "@halvardm/sqlx"; -import type { MysqlParameterType } from "../packets/parsers/result.ts"; -import type { - MysqlPrepared, - MysqlQueryOptions, - MySqlTransaction, - MysqlTransactionOptions, -} from "../client.ts"; -import type { MysqlPoolClient } from "../pool.ts"; +import type { MysqlConnectionOptions } from "../connection.ts"; +import type { MysqlConnection } from "../connection.ts"; + +export class MysqlEventTarget extends SqlxEventTarget< + MysqlConnectionOptions, + MysqlConnection, + SqlxEventType, + MysqlClientConnectionEventInit, + MysqlEvents +> { +} + +export type MysqlClientConnectionEventInit = SqlxEventInit< + SqlxConnectableBase +>; + +export class MysqlConnectionConnectEvent + extends SqlxConnectionConnectEvent {} +export class MysqlConnectionCloseEvent + extends SqlxConnectionCloseEvent {} export class MysqlPoolConnectionAcquireEvent - extends SqlxPoolConnectionAcquireEvent< - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared, - MysqlTransactionOptions, - MySqlTransaction, - MysqlPoolClient, - SqlxConnectionEventInit< - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared, - MysqlTransactionOptions, - MySqlTransaction, - MysqlPoolClient - > - > { + extends SqlxPoolConnectionAcquireEvent { } export class MysqlPoolConnectionReleaseEvent - extends SqlxPoolConnectionReleaseEvent< - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared, - MysqlTransactionOptions, - MySqlTransaction, - MysqlPoolClient, - SqlxConnectionEventInit< - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared, - MysqlTransactionOptions, - MySqlTransaction, - MysqlPoolClient - > - > { + extends SqlxPoolConnectionReleaseEvent { } export class MysqlPoolConnectionDestroyEvent - extends SqlxPoolConnectionDestroyEvent< - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared, - MysqlTransactionOptions, - MySqlTransaction, - MysqlPoolClient, - SqlxConnectionEventInit< - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared, - MysqlTransactionOptions, - MySqlTransaction, - MysqlPoolClient - > - > { + extends SqlxPoolConnectionDestroyEvent { } + +export type MysqlEvents = + | MysqlConnectionConnectEvent + | MysqlConnectionCloseEvent + | MysqlPoolConnectionAcquireEvent + | MysqlPoolConnectionReleaseEvent + | MysqlPoolConnectionDestroyEvent; diff --git a/lib/utils/logger.ts b/lib/utils/logger.ts index 9cd7c34..f70b71d 100644 --- a/lib/utils/logger.ts +++ b/lib/utils/logger.ts @@ -1,4 +1,4 @@ -import { ConsoleHandler, getLogger, setup } from "@std/log"; +import { getLogger } from "@std/log"; import { MODULE_NAME } from "./meta.ts"; /** @@ -10,20 +10,3 @@ import { MODULE_NAME } from "./meta.ts"; export function logger() { return getLogger(MODULE_NAME); } - -setup({ - handlers: { - console: new ConsoleHandler("DEBUG"), - }, - loggers: { - // configure default logger available via short-hand methods above - default: { - level: "INFO", - handlers: ["console"], - }, - [MODULE_NAME]: { - level: "INFO", - handlers: ["console"], - }, - }, -}); diff --git a/lib/utils/testing.ts b/lib/utils/testing.ts index e5de69f..e7c6487 100644 --- a/lib/utils/testing.ts +++ b/lib/utils/testing.ts @@ -1,7 +1,30 @@ import { resolve } from "@std/path"; +import { ConsoleHandler, setup } from "@std/log"; +import { MODULE_NAME } from "./meta.ts"; -export const DIR_TMP_TEST = resolve("tmp_test"); +setup({ + handlers: { + console: new ConsoleHandler("DEBUG"), + }, + loggers: { + // configure default logger available via short-hand methods above + default: { + level: "WARN", + handlers: ["console"], + }, + [MODULE_NAME]: { + level: "WARN", + handlers: ["console"], + }, + }, +}); + +export const DIR_TMP_TEST = resolve(Deno.cwd(), "tmp_test"); +console.log(DIR_TMP_TEST); //socket "/var/run/mysqld/mysqld.sock"; export const URL_TEST_CONNECTION = Deno.env.get("DENO_MYSQL_CONNECTION_URL") || "mysql://root@0.0.0.0:3306/testdb"; +export const URL_TEST_CONNECTION_MARIADB = + Deno.env.get("DENO_MARIADB_CONNECTION_URL") || + "mysql://root@0.0.0.0:3307/testdb"; diff --git a/mod.ts b/mod.ts index b62c9ed..97922ec 100644 --- a/mod.ts +++ b/mod.ts @@ -1,9 +1,6 @@ -export type { ClientConfig } from "./lib/client.ts"; -export { Client } from "./lib/client.ts"; -export type { TLSConfig } from "./lib/client.ts"; -export { TLSMode } from "./lib/client.ts"; - -export type { ExecuteResult } from "./lib/connection.ts"; -export { Connection } from "./lib/connection.ts"; - -export * as log from "@std/log"; +export * from "./lib/client.ts"; +export * from "./lib/connection.ts"; +export * from "./lib/pool.ts"; +export * from "./lib/utils/errors.ts"; +export * from "./lib/utils/events.ts"; +export * from "./lib/utils/meta.ts"; From 1dfba2beee87543f7a02fc718b699a47246d40af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 21 Apr 2024 19:57:45 +0200 Subject: [PATCH 27/38] Added types to parameter type --- lib/packets/parsers/result.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index 2460000..9e70113 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -2,7 +2,17 @@ import type { BufferReader } from "../../utils/buffer.ts"; import { MysqlDataType } from "../../constant/mysql_types.ts"; import type { ArrayRow, Row, SqlxQueryOptions } from "@halvardm/sqlx"; -export type MysqlParameterType = string | number | bigint | Date | null; +export type MysqlParameterType = + | null + | string + | number + | boolean + | bigint + | Date + // deno-lint-ignore no-explicit-any + | Array + | object + | undefined; /** * Field information From 52112b9432f807445945f026f1d78c76669f2ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 21 Apr 2024 19:58:19 +0200 Subject: [PATCH 28/38] cleanup and fixes --- deno.json | 3 +-- lib/client.ts | 4 ++-- lib/utils/errors.ts | 4 ++-- lib/utils/logger.ts | 4 ++-- lib/utils/query.ts | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/deno.json b/deno.json index 56dc7d1..235c009 100644 --- a/deno.json +++ b/deno.json @@ -15,8 +15,7 @@ "db:stop": "docker compose down --remove-orphans --volumes" }, "imports": { - "@halvardm/sqlx": "../deno-sqlx/mod.ts", - "@halvardm/sqlx/testing": "../deno-sqlx/lib/testing.ts", + "@halvardm/sqlx": "jsr:@halvardm/sqlx@0.0.0-11", "@std/assert": "jsr:@std/assert@^0.221.0", "@std/async": "jsr:@std/async@^0.221.0", "@std/crypto": "jsr:@std/crypto@^0.221.0", diff --git a/lib/client.ts b/lib/client.ts index 307976f..821f6dc 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -53,7 +53,7 @@ export class MysqlQueriable extends SqlxBase implements readonly queryOptions: MysqlQueryOptions; get connected(): boolean { - throw new Error("Method not implemented."); + return this.connection.connected; } constructor( @@ -181,9 +181,9 @@ export class MysqlPrepared extends SqlxBase implements ) { super(); this.connection = connection; - this.#queriable = new MysqlQueriable(connection); this.sql = sql; this.queryOptions = options; + this.#queriable = new MysqlQueriable(connection, this.queryOptions); } execute( diff --git a/lib/utils/errors.ts b/lib/utils/errors.ts index 2e68cd9..be73e89 100644 --- a/lib/utils/errors.ts +++ b/lib/utils/errors.ts @@ -1,4 +1,4 @@ -import { SqlxError } from "@halvardm/sqlx"; +import { isSqlxError, SqlxError } from "@halvardm/sqlx"; export class MysqlError extends SqlxError { constructor(msg: string) { @@ -46,5 +46,5 @@ export class MysqlTransactionError extends MysqlError { * Check if an error is a MysqlError */ export function isMysqlError(err: unknown): err is MysqlError { - return err instanceof MysqlError; + return isSqlxError(err) && err instanceof MysqlError; } diff --git a/lib/utils/logger.ts b/lib/utils/logger.ts index f70b71d..28830cd 100644 --- a/lib/utils/logger.ts +++ b/lib/utils/logger.ts @@ -1,4 +1,4 @@ -import { getLogger } from "@std/log"; +import { getLogger, type Logger } from "@std/log"; import { MODULE_NAME } from "./meta.ts"; /** @@ -7,6 +7,6 @@ import { MODULE_NAME } from "./meta.ts"; * * @see {@link https://deno.land/std/log/mod.ts} */ -export function logger() { +export function logger(): Logger { return getLogger(MODULE_NAME); } diff --git a/lib/utils/query.ts b/lib/utils/query.ts index 4179d1c..1d4e5fc 100644 --- a/lib/utils/query.ts +++ b/lib/utils/query.ts @@ -58,7 +58,7 @@ export function replaceParams( })`; } case "string": - return `"${escapeString(val)}"`; + return `"${escapeString(val as string)}"`; case "undefined": return "NULL"; case "number": From d1101efbb72db3ffc7a833050eb9c78d894bae05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 24 Apr 2024 17:08:04 +0200 Subject: [PATCH 29/38] Updated interfaces acording to sqlx --- .gitignore | 3 +- cipher | Bin 390 -> 0 bytes compose.yml | 67 +++++- deno.json | 10 +- lib/client.test.ts | 50 +++++ lib/client.ts | 407 ++-------------------------------- lib/connection.test.ts | 44 +--- lib/connection.ts | 35 ++- lib/packets/parsers/result.ts | 10 +- lib/pool.test.ts | 50 +++++ lib/pool.ts | 31 ++- lib/sqlx.test.ts | 59 ----- lib/sqlx.ts | 377 +++++++++++++++++++++++++++++++ lib/utils/events.ts | 84 ++++--- lib/utils/testing.ts | 73 +++++- 15 files changed, 723 insertions(+), 577 deletions(-) delete mode 100644 cipher create mode 100644 lib/client.test.ts create mode 100644 lib/pool.test.ts delete mode 100644 lib/sqlx.test.ts create mode 100644 lib/sqlx.ts diff --git a/.gitignore b/.gitignore index 520cb2b..17b78fb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ mysql.log docs .DS_Store .idea - +dbtmp +tmp_test diff --git a/cipher b/cipher deleted file mode 100644 index 356b2f827f0c8ab871363d9d86f4a76a2f575f72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmWNMT}x8|0ESJ|C88ijOPM7DS9oCwIYq^rpho3|*fJ?t7pX+74-A|e$e7dlQTee_ zkJF4ZSGuS%zY;wj;d%d9=k~pLp67k6>@%znme02*W_;)!g$4B zX2^cjDg`oZ-70;;vGU>2IQDa_uy${2zPGZmqFXublO+>jkM$Pf@7x&r*2qkmk00Jh z6UzaLTJa22v)mEA+ImH+E&bZ+!Q{+ow3HyPHJ#p5GU!OpZfI61x7Nm*S&-SODX+4@ z64|vQq=s~K?JiR_uBYrjXeHz0k8*rcL9?%!^RxI;he}^$-T}s)b*Jl6u!dbu-m{`) z>`|2rePL1C&Nqt_E%B9vjv{S!d}i~`_4~fDYUM8v%Pf`Qjg9tUw_%giW;_4*?Idsi E2byixWB>pF diff --git a/compose.yml b/compose.yml index cfb258a..0b984f8 100644 --- a/compose.yml +++ b/compose.yml @@ -1,9 +1,39 @@ services: mysql: - image: mysql + image: mysql:latest + ports: + - 3313:3306 + pull_policy: always + restart: always + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_DATABASE: testdb + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--user", "root"] + interval: 3s + timeout: 3s + retries: 10 + mysql5: + image: mysql:5 + platform: linux/amd64 + ports: + - 3311:3306 + pull_policy: always restart: always + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_DATABASE: testdb + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--user", "root"] + interval: 3s + timeout: 3s + retries: 10 + mysql8: + image: mysql:8 ports: - - 3306:3306 + - 3312:3306 + pull_policy: always + restart: always environment: MYSQL_ALLOW_EMPTY_PASSWORD: true MYSQL_DATABASE: testdb @@ -13,10 +43,39 @@ services: timeout: 3s retries: 10 mariadb: - image: mariadb + image: mariadb:latest + ports: + - 3316:3306 + pull_policy: always + restart: always + environment: + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true + MARIADB_DATABASE: testdb + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1"] + interval: 3s + timeout: 3s + retries: 10 + mariadb10: + image: mariadb:10 + ports: + - 3314:3306 + pull_policy: always restart: always + environment: + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true + MARIADB_DATABASE: testdb + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-h", "127.0.0.1"] + interval: 3s + timeout: 3s + retries: 10 + mariadb11: + image: mariadb:11 ports: - - 3307:3306 + - 3315:3306 + pull_policy: always + restart: always environment: MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true MARIADB_DATABASE: testdb diff --git a/deno.json b/deno.json index 235c009..2f02305 100644 --- a/deno.json +++ b/deno.json @@ -10,6 +10,7 @@ "type:check": "deno check mod.ts", "doc:check": "deno doc --lint src", "test": "deno task db:restart && deno test -A; deno task db:stop", + "test:ga": "deno task db:start && deno test -A && deno task db:stop", "db:restart": "deno task db:stop && deno task db:start", "db:start": "docker compose up -d --remove-orphans --wait && sleep 2", "db:stop": "docker compose down --remove-orphans --volumes" @@ -27,12 +28,7 @@ "@std/path": "jsr:@std/path@^0.222.1", "@std/semver": "jsr:@std/semver@^0.220.1", "@std/testing": "jsr:@std/testing@^0.221.0", - "@std/text": "jsr:@std/text@^0.222.1" - }, - "lint": { - "exclude": ["vendor"] - }, - "fmt": { - "exclude": ["vendor"] + "@std/text": "jsr:@std/text@^0.222.1", + "@std/yaml": "jsr:@std/yaml@^0.223.0" } } diff --git a/lib/client.test.ts b/lib/client.test.ts new file mode 100644 index 0000000..ef1bfc6 --- /dev/null +++ b/lib/client.test.ts @@ -0,0 +1,50 @@ +import { MysqlClient } from "./client.ts"; +import { QUERIES, services } from "./utils/testing.ts"; +import { clientTest } from "@halvardm/sqlx/testing"; + +Deno.test("Client Test", async (t) => { + for (const service of services) { + await t.step(`Testing ${service.name}`, async (t) => { + await t.step(`TCP`, async (t) => { + await clientTest({ + t, + Client: MysqlClient, + connectionUrl: service.url, + connectionOptions: {}, + queries: QUERIES, + }); + }); + + // Enable once socket connection issue is fixed + // + // await t.step(`UNIX Socket`, async (t) => { + // await implementationTest({ + // t, + // Client: MysqlClient, + // // deno-lint-ignore no-explicit-any + // PoolClient: MysqlClientPool as any, + // connectionUrl: service.urlSocket, + // connectionOptions: {}, + // queries: { + // createTable: + // "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", + // dropTable: "DROP TABLE IF EXISTS sqlxtesttable", + // insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", + // insertManyToTable: + // "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", + // selectOneFromTable: + // "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", + // selectByMatchFromTable: + // "SELECT * FROM sqlxtesttable WHERE testcol = ?", + // selectManyFromTable: "SELECT * FROM sqlxtesttable", + // select1AsString: "SELECT '1' as result", + // select1Plus1AsNumber: "SELECT 1+1 as result", + // deleteByMatchFromTable: + // "DELETE FROM sqlxtesttable WHERE testcol = ?", + // deleteAllFromTable: "DELETE FROM sqlxtesttable", + // }, + // }); + // }); + }); + } +}); diff --git a/lib/client.ts b/lib/client.ts index 821f6dc..1a29909 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,399 +1,28 @@ -import { - type ArrayRow, - type Row, - SqlxBase, - type SqlxClient, - SqlxConnectionCloseEvent, - SqlxConnectionConnectEvent, - type SqlxPreparable, - type SqlxPreparedQueriable, - type SqlxQueriable, - type SqlxQueryOptions, - type SqlxTransactionable, - type SqlxTransactionOptions, - type SqlxTransactionQueriable, -} from "@halvardm/sqlx"; +import { SqlxClient } from "@halvardm/sqlx"; import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; -import { buildQuery } from "./packets/builders/query.ts"; import type { MysqlParameterType } from "./packets/parsers/result.ts"; -import { MysqlTransactionError } from "./utils/errors.ts"; -import { MysqlEventTarget } from "./utils/events.ts"; - -export interface MysqlTransactionOptions extends SqlxTransactionOptions { - beginTransactionOptions: { - withConsistentSnapshot?: boolean; - readWrite?: "READ WRITE" | "READ ONLY"; - }; - commitTransactionOptions: { - chain?: boolean; - release?: boolean; - }; - rollbackTransactionOptions: { - chain?: boolean; - release?: boolean; - savepoint?: string; - }; -} +import { + MysqlClientCloseEvent, + MysqlClientEventTarget, +} from "./utils/events.ts"; +import { MysqlClientConnectEvent } from "../mod.ts"; +import { + type MysqlPrepared, + type MysqlQueryOptions, + type MySqlTransaction, + MysqlTransactionable, + type MysqlTransactionOptions, +} from "./sqlx.ts"; export interface MysqlClientOptions extends MysqlConnectionOptions { } -export interface MysqlQueryOptions extends SqlxQueryOptions { -} - -export class MysqlQueriable extends SqlxBase implements - SqlxQueriable< - MysqlEventTarget, - MysqlConnectionOptions, - MysqlConnection, - MysqlParameterType, - MysqlQueryOptions - > { - readonly connection: MysqlConnection; - readonly queryOptions: MysqlQueryOptions; - - get connected(): boolean { - return this.connection.connected; - } - - constructor( - connection: MysqlConnection, - queryOptions: MysqlQueryOptions = {}, - ) { - super(); - this.connection = connection; - this.queryOptions = queryOptions; - } - - execute( - sql: string, - params?: MysqlParameterType[] | undefined, - _options?: MysqlQueryOptions | undefined, - ): Promise { - const data = buildQuery(sql, params); - return this.connection.executeRaw(data); - } - query = Row>( - sql: string, - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return Array.fromAsync(this.queryMany(sql, params, options)); - } - async queryOne = Row>( - sql: string, - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - const res = await this.query(sql, params, options); - return res[0]; - } - async *queryMany = Row>( - sql: string, - params?: MysqlParameterType[], - options?: MysqlQueryOptions | undefined, - ): AsyncGenerator { - const data = buildQuery(sql, params); - for await ( - const res of this.connection.queryManyObjectRaw(data, options) - ) { - yield res; - } - } - - queryArray< - T extends ArrayRow = ArrayRow, - >( - sql: string, - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return Array.fromAsync(this.queryManyArray(sql, params, options)); - } - async queryOneArray< - T extends ArrayRow = ArrayRow, - >( - sql: string, - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - const res = await this.queryArray(sql, params, options); - return res[0]; - } - async *queryManyArray< - T extends ArrayRow = ArrayRow, - >( - sql: string, - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): AsyncGenerator { - const data = buildQuery(sql, params); - for await ( - const res of this.connection.queryManyArrayRaw(data, options) - ) { - yield res; - } - } - sql = Row>( - strings: TemplateStringsArray, - ...parameters: MysqlParameterType[] - ): Promise { - return this.query(strings.join("?"), parameters); - } - sqlArray< - T extends ArrayRow = ArrayRow, - >( - strings: TemplateStringsArray, - ...parameters: MysqlParameterType[] - ): Promise { - return this.queryArray(strings.join("?"), parameters); - } -} - -/** - * Prepared statement - * - * @todo implement prepared statements properly - */ -export class MysqlPrepared extends SqlxBase implements - SqlxPreparedQueriable< - MysqlEventTarget, - MysqlConnectionOptions, - MysqlConnection, - MysqlParameterType, - MysqlQueryOptions - > { - readonly sql: string; - readonly queryOptions: MysqlQueryOptions; - - #queriable: MysqlQueriable; - - connection: MysqlConnection; - - get connected(): boolean { - return this.connection.connected; - } - - constructor( - connection: MysqlConnection, - sql: string, - options: MysqlQueryOptions = {}, - ) { - super(); - this.connection = connection; - this.sql = sql; - this.queryOptions = options; - this.#queriable = new MysqlQueriable(connection, this.queryOptions); - } - - execute( - params?: MysqlParameterType[] | undefined, - _options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.execute(this.sql, params); - } - query = Row>( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.query(this.sql, params, options); - } - queryOne = Row>( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.queryOne(this.sql, params, options); - } - queryMany = Row>( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): AsyncGenerator { - return this.#queriable.queryMany(this.sql, params, options); - } - queryArray< - T extends ArrayRow = ArrayRow, - >( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.queryArray(this.sql, params, options); - } - queryOneArray< - T extends ArrayRow = ArrayRow, - >( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): Promise { - return this.#queriable.queryOneArray(this.sql, params, options); - } - queryManyArray< - T extends ArrayRow = ArrayRow, - >( - params?: MysqlParameterType[] | undefined, - options?: MysqlQueryOptions | undefined, - ): AsyncGenerator { - return this.#queriable.queryManyArray(this.sql, params, options); - } -} - -export class MysqlPreparable extends MysqlQueriable implements - SqlxPreparable< - MysqlEventTarget, - MysqlConnectionOptions, - MysqlConnection, - MysqlParameterType, - MysqlQueryOptions, - MysqlPrepared - > { - prepare(sql: string, options?: MysqlQueryOptions | undefined): MysqlPrepared { - return new MysqlPrepared(this.connection, sql, options); - } -} - -export class MySqlTransaction extends MysqlPreparable - implements - SqlxTransactionQueriable< - MysqlEventTarget, - MysqlConnectionOptions, - MysqlConnection, - MysqlParameterType, - MysqlQueryOptions, - MysqlTransactionOptions - > { - #inTransaction: boolean = true; - get inTransaction(): boolean { - return this.connected && this.#inTransaction; - } - - get connected(): boolean { - if (!this.#inTransaction) { - throw new MysqlTransactionError( - "Transaction is not active, create a new one using beginTransaction", - ); - } - - return super.connected; - } - - async commitTransaction( - options?: MysqlTransactionOptions["commitTransactionOptions"], - ): Promise { - try { - let sql = "COMMIT"; - - if (options?.chain === true) { - sql += " AND CHAIN"; - } else if (options?.chain === false) { - sql += " AND NO CHAIN"; - } - - if (options?.release === true) { - sql += " RELEASE"; - } else if (options?.release === false) { - sql += " NO RELEASE"; - } - await this.execute(sql); - } catch (e) { - this.#inTransaction = false; - throw e; - } - } - async rollbackTransaction( - options?: MysqlTransactionOptions["rollbackTransactionOptions"], - ): Promise { - try { - let sql = "ROLLBACK"; - - if (options?.savepoint) { - sql += ` TO ${options.savepoint}`; - await this.execute(sql); - return; - } - - if (options?.chain === true) { - sql += " AND CHAIN"; - } else if (options?.chain === false) { - sql += " AND NO CHAIN"; - } - - if (options?.release === true) { - sql += " RELEASE"; - } else if (options?.release === false) { - sql += " NO RELEASE"; - } - - await this.execute(sql); - } catch (e) { - this.#inTransaction = false; - throw e; - } - } - async createSavepoint(name: string = `\t_bm.\t`): Promise { - await this.execute(`SAVEPOINT ${name}`); - } - async releaseSavepoint(name: string = `\t_bm.\t`): Promise { - await this.execute(`RELEASE SAVEPOINT ${name}`); - } -} - -/** - * Represents a queriable class that can be used to run transactions. - */ -export class MysqlTransactionable extends MysqlPreparable - implements - SqlxTransactionable< - MysqlEventTarget, - MysqlConnectionOptions, - MysqlConnection, - MysqlParameterType, - MysqlQueryOptions, - MysqlTransactionOptions, - MySqlTransaction - > { - async beginTransaction( - options?: MysqlTransactionOptions["beginTransactionOptions"], - ): Promise { - let sql = "START TRANSACTION"; - if (options?.withConsistentSnapshot) { - sql += ` WITH CONSISTENT SNAPSHOT`; - } - - if (options?.readWrite) { - sql += ` ${options.readWrite}`; - } - - await this.execute(sql); - - return new MySqlTransaction(this.connection, this.queryOptions); - } - - async transaction( - fn: (t: MySqlTransaction) => Promise, - options?: MysqlTransactionOptions, - ): Promise { - const transaction = await this.beginTransaction( - options?.beginTransactionOptions, - ); - - try { - const result = await fn(transaction); - await transaction.commitTransaction(options?.commitTransactionOptions); - return result; - } catch (error) { - await transaction.rollbackTransaction( - options?.rollbackTransactionOptions, - ); - throw error; - } - } -} - /** * MySQL client */ export class MysqlClient extends MysqlTransactionable implements SqlxClient< - MysqlEventTarget, + MysqlClientEventTarget, MysqlConnectionOptions, MysqlConnection, MysqlParameterType, @@ -402,7 +31,7 @@ export class MysqlClient extends MysqlTransactionable implements MysqlTransactionOptions, MySqlTransaction > { - eventTarget: MysqlEventTarget; + eventTarget: MysqlClientEventTarget; connectionUrl: string; connectionOptions: MysqlConnectionOptions; @@ -414,17 +43,17 @@ export class MysqlClient extends MysqlTransactionable implements super(conn); this.connectionUrl = connectionUrl.toString(); this.connectionOptions = connectionOptions; - this.eventTarget = new MysqlEventTarget(); + this.eventTarget = new MysqlClientEventTarget(); } async connect(): Promise { await this.connection.connect(); this.eventTarget.dispatchEvent( - new SqlxConnectionConnectEvent({ connectable: this }), + new MysqlClientConnectEvent({ connectable: this }), ); } async close(): Promise { this.eventTarget.dispatchEvent( - new SqlxConnectionCloseEvent({ connectable: this }), + new MysqlClientCloseEvent({ connectable: this }), ); await this.connection.close(); } diff --git a/lib/connection.test.ts b/lib/connection.test.ts index 98408bb..1735ba4 100644 --- a/lib/connection.test.ts +++ b/lib/connection.test.ts @@ -5,6 +5,7 @@ import { MysqlConnection } from "./connection.ts"; import { DIR_TMP_TEST } from "./utils/testing.ts"; import { buildQuery } from "./packets/builders/query.ts"; import { URL_TEST_CONNECTION } from "./utils/testing.ts"; +import { connectionConstructorTest } from "@halvardm/sqlx/testing"; Deno.test("Connection", async (t) => { await emptyDir(DIR_TMP_TEST); @@ -125,50 +126,17 @@ Deno.test("Connection", async (t) => { }, }); }); - }); - - const connection = new MysqlConnection(URL_TEST_CONNECTION); - assertEquals(connection.connected, false); - await t.step("can connect and close", async () => { - await connection.connect(); - assertEquals(connection.connected, true); await connection.close(); - assertEquals(connection.connected, false); }); - await t.step("can reconnect", async () => { - await connection.connect(); - assertEquals(connection.connected, true); - await connection.close(); - assertEquals(connection.connected, false); + await connectionConstructorTest({ + t, + Connection: MysqlConnection, + connectionUrl: URL_TEST_CONNECTION, + connectionOptions: {}, }); - await t.step("can connect with using and dispose", async () => { - await using connection = new MysqlConnection(URL_TEST_CONNECTION); - assertEquals(connection.connected, false); - await connection.connect(); - assertEquals(connection.connected, true); - }); - - // await t.step("can execute", async (t) => { - // await using connection = new MysqlConnection(URL_TEST_CONNECTION); - // await connection.connect(); - // const data = buildQuery("SELECT 1+1 AS result"); - // const result = await connection.execute(data); - // assertEquals(result, { affectedRows: 0, lastInsertId: null }); - // }); - - // await t.step("can execute twice", async (t) => { - // await using connection = new MysqlConnection(URL_TEST_CONNECTION); - // await connection.connect(); - // const data = buildQuery("SELECT 1+1 AS result;"); - // const result1 = await connection.execute(data); - // assertEquals(result1, { affectedRows: 0, lastInsertId: null }); - // const result2 = await connection.execute(data); - // assertEquals(result2, { affectedRows: 0, lastInsertId: null }); - // }); - await t.step("can query database", async (t) => { await using connection = new MysqlConnection(URL_TEST_CONNECTION); await connection.connect(); diff --git a/lib/connection.ts b/lib/connection.ts index 7b01486..d7ec47a 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -13,7 +13,6 @@ import { parseHandshake, } from "./packets/parsers/handshake.ts"; import { - type ConvertTypeOptions, type FieldInfo, getRowObject, type MysqlParameterType, @@ -27,17 +26,17 @@ import auth from "./utils/hash.ts"; import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; import { logger } from "./utils/logger.ts"; -import type { - ArrayRow, - Row, - SqlxConnection, - SqlxConnectionOptions, +import { + type ArrayRow, + type Row, + SqlxBase, + type SqlxConnection, + type SqlxConnectionOptions, + type SqlxQueryOptions, } from "@halvardm/sqlx"; -import { VERSION } from "./utils/meta.ts"; import { resolve } from "@std/path"; import { toCamelCase } from "@std/text"; import { AuthPluginName } from "./auth_plugins/mod.ts"; -import type { MysqlEventTarget } from "./utils/events.ts"; /** * Connection state @@ -129,12 +128,12 @@ export interface ConnectionConfig { } export interface MysqlConnectionOptions extends SqlxConnectionOptions { + tls?: Partial; } /** Connection for mysql */ -export class MysqlConnection implements +export class MysqlConnection extends SqlxBase implements SqlxConnection< - MysqlEventTarget, MysqlConnectionOptions > { state: ConnectionState = ConnectionState.CONNECTING; @@ -147,8 +146,6 @@ export class MysqlConnection implements readonly connectionUrl: string; readonly connectionOptions: MysqlConnectionOptions; readonly config: ConnectionConfig; - readonly sqlxVersion: string = VERSION; - eventTarget: MysqlEventTarget; get conn(): Deno.Conn { if (!this._conn) { @@ -174,13 +171,13 @@ export class MysqlConnection implements connectionUrl: string | URL, connectionOptions: MysqlConnectionOptions = {}, ) { + super(); this.connectionUrl = connectionUrl.toString().split("?")[0]; this.connectionOptions = connectionOptions; this.config = this.#parseConnectionConfig( connectionUrl, connectionOptions, ); - this.eventTarget = new EventTarget(); } async connect(): Promise { @@ -193,7 +190,9 @@ export class MysqlConnection implements throw new Error("unsupported tls mode"); } - logger().info(`connecting ${this.connectionUrl}`); + logger().info( + `connecting ${this.connectionUrl},${JSON.stringify(this.config)}`, + ); if (this.config.socket) { this.conn = await Deno.connect({ @@ -524,7 +523,7 @@ export class MysqlConnection implements async *sendData( data: Uint8Array, - options?: ConvertTypeOptions, + options?: SqlxQueryOptions, ): AsyncGenerator< ConnectionSendDataNext, ConnectionSendDataResult | undefined @@ -578,7 +577,7 @@ export class MysqlConnection implements async executeRaw( data: Uint8Array, - options?: ConvertTypeOptions, + options?: SqlxQueryOptions, ): Promise { const gen = this.sendData(data, options); let result = await gen.next(); @@ -599,7 +598,7 @@ export class MysqlConnection implements async *queryManyObjectRaw = Row>( data: Uint8Array, - options?: ConvertTypeOptions, + options?: SqlxQueryOptions, ): AsyncIterableIterator { for await (const res of this.sendData(data, options)) { yield getRowObject(res.fields, res.row) as T; @@ -608,7 +607,7 @@ export class MysqlConnection implements async *queryManyArrayRaw = ArrayRow>( data: Uint8Array, - options?: ConvertTypeOptions, + options?: SqlxQueryOptions, ): AsyncIterableIterator { for await (const res of this.sendData(data, options)) { const row = res.row as T; diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index 9e70113..2b889e1 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -32,8 +32,6 @@ export interface FieldInfo { defaultVal: string; } -export type ConvertTypeOptions = Pick; - /** * Parses the field */ @@ -74,7 +72,7 @@ export function parseField(reader: BufferReader): FieldInfo { export function parseRowArray( reader: BufferReader, fields: FieldInfo[], - options?: ConvertTypeOptions, + options?: SqlxQueryOptions, ): ArrayRow { const row: MysqlParameterType[] = []; for (const field of fields) { @@ -114,11 +112,11 @@ export function getRowObject( function convertType( field: FieldInfo, val: string, - options?: ConvertTypeOptions, + options?: SqlxQueryOptions, ): MysqlParameterType { - if (options?.transformType) { + if (options?.transformOutput) { // deno-lint-ignore no-explicit-any - return options.transformType(val) as any; + return options.transformOutput(val) as any; } const { fieldType } = field; switch (fieldType) { diff --git a/lib/pool.test.ts b/lib/pool.test.ts new file mode 100644 index 0000000..0309a62 --- /dev/null +++ b/lib/pool.test.ts @@ -0,0 +1,50 @@ +import { MysqlClientPool } from "./pool.ts"; +import { QUERIES, services } from "./utils/testing.ts"; +import { clientPoolTest } from "@halvardm/sqlx/testing"; + +Deno.test("Pool Test", async (t) => { + for (const service of services) { + await t.step(`Testing ${service.name}`, async (t) => { + await t.step(`TCP`, async (t) => { + await clientPoolTest({ + t, + Client: MysqlClientPool, + connectionUrl: service.url, + connectionOptions: {}, + queries: QUERIES, + }); + }); + + // Enable once socket connection issue is fixed + // + // await t.step(`UNIX Socket`, async (t) => { + // await implementationTest({ + // t, + // Client: MysqlClient, + // // deno-lint-ignore no-explicit-any + // PoolClient: MysqlClientPool as any, + // connectionUrl: service.urlSocket, + // connectionOptions: {}, + // queries: { + // createTable: + // "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", + // dropTable: "DROP TABLE IF EXISTS sqlxtesttable", + // insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", + // insertManyToTable: + // "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", + // selectOneFromTable: + // "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", + // selectByMatchFromTable: + // "SELECT * FROM sqlxtesttable WHERE testcol = ?", + // selectManyFromTable: "SELECT * FROM sqlxtesttable", + // select1AsString: "SELECT '1' as result", + // select1Plus1AsNumber: "SELECT 1+1 as result", + // deleteByMatchFromTable: + // "DELETE FROM sqlxtesttable WHERE testcol = ?", + // deleteAllFromTable: "DELETE FROM sqlxtesttable", + // }, + // }); + // }); + }); + } +}); diff --git a/lib/pool.ts b/lib/pool.ts index 00545dd..0229ab7 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -2,7 +2,6 @@ import { SqlxBase, type SqlxClientPool, type SqlxClientPoolOptions, - type SqlxConnectionOptions, SqlxDeferredStack, SqlxError, type SqlxPoolClient, @@ -13,18 +12,18 @@ import { type MySqlTransaction, MysqlTransactionable, type MysqlTransactionOptions, -} from "./client.ts"; +} from "./sqlx.ts"; import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; import type { MysqlParameterType } from "./packets/parsers/result.ts"; import { - MysqlConnectionCloseEvent, - MysqlConnectionConnectEvent, - MysqlPoolConnectionAcquireEvent, - MysqlPoolConnectionDestroyEvent, - MysqlPoolConnectionReleaseEvent, + MysqlPoolAcquireEvent, + MysqlPoolCloseEvent, + MysqlPoolConnectEvent, + MysqlPoolDestroyEvent, + MysqlPoolReleaseEvent, } from "./utils/events.ts"; import { logger } from "./utils/logger.ts"; -import { MysqlEventTarget } from "./utils/events.ts"; +import { MysqlClientEventTarget } from "./utils/events.ts"; export interface MysqlClientPoolOptions extends MysqlConnectionOptions, SqlxClientPoolOptions { @@ -33,7 +32,6 @@ export interface MysqlClientPoolOptions export class MysqlPoolClient extends MysqlTransactionable implements SqlxPoolClient< - MysqlEventTarget, MysqlConnectionOptions, MysqlConnection, MysqlParameterType, @@ -57,7 +55,6 @@ export class MysqlPoolClient extends MysqlTransactionable export class MysqlClientPool extends SqlxBase implements SqlxClientPool< - MysqlEventTarget, MysqlConnectionOptions, MysqlConnection, MysqlParameterType, @@ -69,7 +66,7 @@ export class MysqlClientPool extends SqlxBase implements SqlxDeferredStack > { readonly connectionUrl: string; - readonly connectionOptions: SqlxConnectionOptions; + readonly connectionOptions: MysqlClientPoolOptions; readonly eventTarget: EventTarget; readonly deferredStack: SqlxDeferredStack; readonly queryOptions: MysqlQueryOptions; @@ -88,7 +85,7 @@ export class MysqlClientPool extends SqlxBase implements this.connectionUrl = connectionUrl.toString(); this.connectionOptions = connectionOptions; this.queryOptions = connectionOptions; - this.eventTarget = new MysqlEventTarget(); + this.eventTarget = new MysqlClientEventTarget(); this.deferredStack = new SqlxDeferredStack( connectionOptions, ); @@ -109,7 +106,7 @@ export class MysqlClientPool extends SqlxBase implements if (!this.connectionOptions.lazyInitialization) { await client.connection.connect(); this.eventTarget.dispatchEvent( - new MysqlConnectionConnectEvent({ connectable: client }), + new MysqlPoolConnectEvent({ connectable: client }), ); } @@ -124,7 +121,7 @@ export class MysqlClientPool extends SqlxBase implements for (const client of this.deferredStack.elements) { this.eventTarget.dispatchEvent( - new MysqlConnectionCloseEvent({ connectable: client }), + new MysqlPoolCloseEvent({ connectable: client }), ); await client.connection.close(); } @@ -134,14 +131,14 @@ export class MysqlClientPool extends SqlxBase implements const client = await this.deferredStack.pop(); this.eventTarget.dispatchEvent( - new MysqlPoolConnectionAcquireEvent({ connectable: client }), + new MysqlPoolAcquireEvent({ connectable: client }), ); return client; } async release(client: MysqlPoolClient): Promise { this.eventTarget.dispatchEvent( - new MysqlPoolConnectionReleaseEvent({ connectable: client }), + new MysqlPoolReleaseEvent({ connectable: client }), ); try { this.deferredStack.push(client); @@ -158,7 +155,7 @@ export class MysqlClientPool extends SqlxBase implements async destroy(client: MysqlPoolClient): Promise { this.eventTarget.dispatchEvent( - new MysqlPoolConnectionDestroyEvent({ connectable: client }), + new MysqlPoolDestroyEvent({ connectable: client }), ); await client.connection.close(); } diff --git a/lib/sqlx.test.ts b/lib/sqlx.test.ts deleted file mode 100644 index 679362f..0000000 --- a/lib/sqlx.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MysqlClient } from "./client.ts"; -import { MysqlClientPool } from "./pool.ts"; -import { - URL_TEST_CONNECTION, - URL_TEST_CONNECTION_MARIADB, -} from "./utils/testing.ts"; -import { implementationTest } from "@halvardm/sqlx/testing"; - -Deno.test("MySQL SQLx", async (t) => { - await implementationTest({ - t, - Client: MysqlClient, - // deno-lint-ignore no-explicit-any - PoolClient: MysqlClientPool as any, - connectionUrl: URL_TEST_CONNECTION, - connectionOptions: {}, - queries: { - createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", - dropTable: "DROP TABLE IF EXISTS sqlxtesttable", - insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", - insertManyToTable: - "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", - selectOneFromTable: - "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", - selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", - selectManyFromTable: "SELECT * FROM sqlxtesttable", - select1AsString: "SELECT '1' as result", - select1Plus1AsNumber: "SELECT 1+1 as result", - deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", - deleteAllFromTable: "DELETE FROM sqlxtesttable", - }, - }); -}); - -Deno.test("MariaDB SQLx", async (t) => { - await implementationTest({ - t, - Client: MysqlClient, - // deno-lint-ignore no-explicit-any - PoolClient: MysqlClientPool as any, - connectionUrl: URL_TEST_CONNECTION_MARIADB, - connectionOptions: {}, - queries: { - createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", - dropTable: "DROP TABLE IF EXISTS sqlxtesttable", - insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", - insertManyToTable: - "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", - selectOneFromTable: - "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", - selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", - selectManyFromTable: "SELECT * FROM sqlxtesttable", - select1AsString: "SELECT '1' as result", - select1Plus1AsNumber: "SELECT 1+1 as result", - deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", - deleteAllFromTable: "DELETE FROM sqlxtesttable", - }, - }); -}); diff --git a/lib/sqlx.ts b/lib/sqlx.ts new file mode 100644 index 0000000..0dc5679 --- /dev/null +++ b/lib/sqlx.ts @@ -0,0 +1,377 @@ +import { + type ArrayRow, + type Row, + SqlxBase, + SqlxPreparable, + SqlxPreparedQueriable, + SqlxQueriable, + type SqlxQueryOptions, + SqlxTransactionable, + type SqlxTransactionOptions, + SqlxTransactionQueriable, +} from "@halvardm/sqlx"; +import type { MysqlConnection, MysqlConnectionOptions } from "./connection.ts"; +import { buildQuery } from "./packets/builders/query.ts"; +import type { MysqlParameterType } from "./packets/parsers/result.ts"; +import { MysqlTransactionError } from "./utils/errors.ts"; + +export interface MysqlQueryOptions extends SqlxQueryOptions { +} + +export interface MysqlTransactionOptions extends SqlxTransactionOptions { + beginTransactionOptions: { + withConsistentSnapshot?: boolean; + readWrite?: "READ WRITE" | "READ ONLY"; + }; + commitTransactionOptions: { + chain?: boolean; + release?: boolean; + }; + rollbackTransactionOptions: { + chain?: boolean; + release?: boolean; + savepoint?: string; + }; +} + +export class MysqlQueriable extends SqlxBase implements + SqlxQueriable< + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions + > { + readonly connection: MysqlConnection; + readonly queryOptions: MysqlQueryOptions; + + get connected(): boolean { + return this.connection.connected; + } + + constructor( + connection: MysqlConnection, + queryOptions: MysqlQueryOptions = {}, + ) { + super(); + this.connection = connection; + this.queryOptions = queryOptions; + } + + execute( + sql: string, + params?: MysqlParameterType[] | undefined, + _options?: MysqlQueryOptions | undefined, + ): Promise { + const data = buildQuery(sql, params); + return this.connection.executeRaw(data); + } + query = Row>( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryMany(sql, params, options)); + } + async queryOne = Row>( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + const res = await this.query(sql, params, options); + return res[0]; + } + async *queryMany = Row>( + sql: string, + params?: MysqlParameterType[], + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + const data = buildQuery(sql, params); + for await ( + const res of this.connection.queryManyObjectRaw(data, options) + ) { + yield res; + } + } + + queryArray< + T extends ArrayRow = ArrayRow, + >( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return Array.fromAsync(this.queryManyArray(sql, params, options)); + } + async queryOneArray< + T extends ArrayRow = ArrayRow, + >( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + const res = await this.queryArray(sql, params, options); + return res[0]; + } + async *queryManyArray< + T extends ArrayRow = ArrayRow, + >( + sql: string, + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + const data = buildQuery(sql, params); + for await ( + const res of this.connection.queryManyArrayRaw(data, options) + ) { + yield res; + } + } + sql = Row>( + strings: TemplateStringsArray, + ...parameters: MysqlParameterType[] + ): Promise { + return this.query(strings.join("?"), parameters); + } + sqlArray< + T extends ArrayRow = ArrayRow, + >( + strings: TemplateStringsArray, + ...parameters: MysqlParameterType[] + ): Promise { + return this.queryArray(strings.join("?"), parameters); + } +} + +/** + * Prepared statement + * + * @todo implement prepared statements properly + */ +export class MysqlPrepared extends SqlxBase implements + SqlxPreparedQueriable< + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions + > { + readonly sql: string; + readonly queryOptions: MysqlQueryOptions; + + #queriable: MysqlQueriable; + + connection: MysqlConnection; + + get connected(): boolean { + return this.connection.connected; + } + + constructor( + connection: MysqlConnection, + sql: string, + options: MysqlQueryOptions = {}, + ) { + super(); + this.connection = connection; + this.sql = sql; + this.queryOptions = options; + this.#queriable = new MysqlQueriable(connection, this.queryOptions); + } + + execute( + params?: MysqlParameterType[] | undefined, + _options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.execute(this.sql, params); + } + query = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.query(this.sql, params, options); + } + queryOne = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryOne(this.sql, params, options); + } + queryMany = Row>( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + return this.#queriable.queryMany(this.sql, params, options); + } + queryArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryArray(this.sql, params, options); + } + queryOneArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): Promise { + return this.#queriable.queryOneArray(this.sql, params, options); + } + queryManyArray< + T extends ArrayRow = ArrayRow, + >( + params?: MysqlParameterType[] | undefined, + options?: MysqlQueryOptions | undefined, + ): AsyncGenerator { + return this.#queriable.queryManyArray(this.sql, params, options); + } +} + +export class MysqlPreparable extends MysqlQueriable implements + SqlxPreparable< + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions, + MysqlPrepared + > { + prepare(sql: string, options?: MysqlQueryOptions | undefined): MysqlPrepared { + return new MysqlPrepared(this.connection, sql, options); + } +} + +export class MySqlTransaction extends MysqlPreparable + implements + SqlxTransactionQueriable< + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions, + MysqlTransactionOptions + > { + #inTransaction: boolean = true; + get inTransaction(): boolean { + return this.connected && this.#inTransaction; + } + + get connected(): boolean { + if (!this.#inTransaction) { + throw new MysqlTransactionError( + "Transaction is not active, create a new one using beginTransaction", + ); + } + + return super.connected; + } + + async commitTransaction( + options?: MysqlTransactionOptions["commitTransactionOptions"], + ): Promise { + try { + let sql = "COMMIT"; + + if (options?.chain === true) { + sql += " AND CHAIN"; + } else if (options?.chain === false) { + sql += " AND NO CHAIN"; + } + + if (options?.release === true) { + sql += " RELEASE"; + } else if (options?.release === false) { + sql += " NO RELEASE"; + } + await this.execute(sql); + } catch (e) { + this.#inTransaction = false; + throw e; + } + } + async rollbackTransaction( + options?: MysqlTransactionOptions["rollbackTransactionOptions"], + ): Promise { + try { + let sql = "ROLLBACK"; + + if (options?.savepoint) { + sql += ` TO ${options.savepoint}`; + await this.execute(sql); + return; + } + + if (options?.chain === true) { + sql += " AND CHAIN"; + } else if (options?.chain === false) { + sql += " AND NO CHAIN"; + } + + if (options?.release === true) { + sql += " RELEASE"; + } else if (options?.release === false) { + sql += " NO RELEASE"; + } + + await this.execute(sql); + } catch (e) { + this.#inTransaction = false; + throw e; + } + } + async createSavepoint(name: string = `\t_bm.\t`): Promise { + await this.execute(`SAVEPOINT ${name}`); + } + async releaseSavepoint(name: string = `\t_bm.\t`): Promise { + await this.execute(`RELEASE SAVEPOINT ${name}`); + } +} + +/** + * Represents a queriable class that can be used to run transactions. + */ +export class MysqlTransactionable extends MysqlPreparable + implements + SqlxTransactionable< + MysqlConnectionOptions, + MysqlConnection, + MysqlParameterType, + MysqlQueryOptions, + MysqlTransactionOptions, + MySqlTransaction + > { + async beginTransaction( + options?: MysqlTransactionOptions["beginTransactionOptions"], + ): Promise { + let sql = "START TRANSACTION"; + if (options?.withConsistentSnapshot) { + sql += ` WITH CONSISTENT SNAPSHOT`; + } + + if (options?.readWrite) { + sql += ` ${options.readWrite}`; + } + + await this.execute(sql); + + return new MySqlTransaction(this.connection, this.queryOptions); + } + + async transaction( + fn: (t: MySqlTransaction) => Promise, + options?: MysqlTransactionOptions, + ): Promise { + const transaction = await this.beginTransaction( + options?.beginTransactionOptions, + ); + + try { + const result = await fn(transaction); + await transaction.commitTransaction(options?.commitTransactionOptions); + return result; + } catch (error) { + await transaction.rollbackTransaction( + options?.rollbackTransactionOptions, + ); + throw error; + } + } +} diff --git a/lib/utils/events.ts b/lib/utils/events.ts index 4f3ff12..069c83d 100644 --- a/lib/utils/events.ts +++ b/lib/utils/events.ts @@ -1,50 +1,74 @@ import { - type SqlxConnectableBase, - SqlxConnectionCloseEvent, - SqlxConnectionConnectEvent, - type SqlxEventInit, + type SqlxClientEventType, + SqlxConnectableCloseEvent, + SqlxConnectableConnectEvent, + type SqlxConnectableEventInit, SqlxEventTarget, - type SqlxEventType, - SqlxPoolConnectionAcquireEvent, - SqlxPoolConnectionDestroyEvent, - SqlxPoolConnectionReleaseEvent, + SqlxPoolConnectableAcquireEvent, + SqlxPoolConnectableDestroyEvent, + SqlxPoolConnectableReleaseEvent, + type SqlxPoolConnectionEventType, } from "@halvardm/sqlx"; import type { MysqlConnectionOptions } from "../connection.ts"; import type { MysqlConnection } from "../connection.ts"; +import type { MysqlClient } from "../client.ts"; +import type { MysqlPoolClient } from "../pool.ts"; -export class MysqlEventTarget extends SqlxEventTarget< +export class MysqlClientEventTarget extends SqlxEventTarget< MysqlConnectionOptions, MysqlConnection, - SqlxEventType, - MysqlClientConnectionEventInit, - MysqlEvents + SqlxClientEventType, + MysqlClientEventInit, + MysqlClientEvents +> { +} +export class MysqlPoolClientEventTarget extends SqlxEventTarget< + MysqlConnectionOptions, + MysqlConnection, + SqlxPoolConnectionEventType, + MysqlPoolEventInit, + MysqlPoolEvents > { } -export type MysqlClientConnectionEventInit = SqlxEventInit< - SqlxConnectableBase +export type MysqlClientEventInit = SqlxConnectableEventInit< + MysqlClient >; -export class MysqlConnectionConnectEvent - extends SqlxConnectionConnectEvent {} -export class MysqlConnectionCloseEvent - extends SqlxConnectionCloseEvent {} +export type MysqlPoolEventInit = SqlxConnectableEventInit< + MysqlPoolClient +>; -export class MysqlPoolConnectionAcquireEvent - extends SqlxPoolConnectionAcquireEvent { +export class MysqlClientConnectEvent + extends SqlxConnectableConnectEvent {} + +export class MysqlClientCloseEvent + extends SqlxConnectableCloseEvent {} +export class MysqlPoolConnectEvent + extends SqlxConnectableConnectEvent {} + +export class MysqlPoolCloseEvent + extends SqlxConnectableCloseEvent {} + +export class MysqlPoolAcquireEvent + extends SqlxPoolConnectableAcquireEvent { } -export class MysqlPoolConnectionReleaseEvent - extends SqlxPoolConnectionReleaseEvent { +export class MysqlPoolReleaseEvent + extends SqlxPoolConnectableReleaseEvent { } -export class MysqlPoolConnectionDestroyEvent - extends SqlxPoolConnectionDestroyEvent { +export class MysqlPoolDestroyEvent + extends SqlxPoolConnectableDestroyEvent { } -export type MysqlEvents = - | MysqlConnectionConnectEvent - | MysqlConnectionCloseEvent - | MysqlPoolConnectionAcquireEvent - | MysqlPoolConnectionReleaseEvent - | MysqlPoolConnectionDestroyEvent; +export type MysqlClientEvents = + | MysqlClientConnectEvent + | MysqlClientCloseEvent; + +export type MysqlPoolEvents = + | MysqlClientConnectEvent + | MysqlClientCloseEvent + | MysqlPoolAcquireEvent + | MysqlPoolReleaseEvent + | MysqlPoolDestroyEvent; diff --git a/lib/utils/testing.ts b/lib/utils/testing.ts index e7c6487..013ceaf 100644 --- a/lib/utils/testing.ts +++ b/lib/utils/testing.ts @@ -1,6 +1,28 @@ import { resolve } from "@std/path"; import { ConsoleHandler, setup } from "@std/log"; import { MODULE_NAME } from "./meta.ts"; +import { parse } from "@std/yaml"; +import type { BaseQueriableTestOptions } from "@halvardm/sqlx/testing"; + +type DockerCompose = { + services: { + [key: string]: { + image: string; + ports: string[]; + environment: Record; + volumes: string[]; + }; + }; +}; + +type ServiceParsed = { + name: string; + port: string; + database: string; + // socket: string; + url: string; + // urlSocket: string; +}; setup({ handlers: { @@ -20,11 +42,46 @@ setup({ }); export const DIR_TMP_TEST = resolve(Deno.cwd(), "tmp_test"); -console.log(DIR_TMP_TEST); - -//socket "/var/run/mysqld/mysqld.sock"; -export const URL_TEST_CONNECTION = Deno.env.get("DENO_MYSQL_CONNECTION_URL") || - "mysql://root@0.0.0.0:3306/testdb"; -export const URL_TEST_CONNECTION_MARIADB = - Deno.env.get("DENO_MARIADB_CONNECTION_URL") || - "mysql://root@0.0.0.0:3307/testdb"; + +const composeParsed = parse( + Deno.readTextFileSync(resolve(Deno.cwd(), "compose.yml")), + { "onWarning": console.warn }, +) as DockerCompose; + +export const services: ServiceParsed[] = Object.entries(composeParsed.services) + .map( + ([key, value]) => { + const port = value.ports[0].split(":")[0]; + const database = Object.entries(value.environment).find(([e]) => + e.includes("DATABASE") + )?.[1] as string; + // const socket = resolve(value.volumes[0].split(":")[0])+"/mysqld.sock"; + const url = `mysql://root@0.0.0.0:${port}/${database}`; + // const urlSocket = `${url}?socket=${socket}`; + return { + name: key, + port, + database, + // socket, + url, + // urlSocket, + }; + }, + ); + +export const URL_TEST_CONNECTION = services.find((s) => s.name === "mysql") + ?.url as string; + +export const QUERIES: BaseQueriableTestOptions["queries"] = { + createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", + dropTable: "DROP TABLE IF EXISTS sqlxtesttable", + insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", + insertManyToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", + selectOneFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", + selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", + selectManyFromTable: "SELECT * FROM sqlxtesttable", + select1AsString: "SELECT '1' as result", + select1Plus1AsNumber: "SELECT 1+1 as result", + deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", + deleteAllFromTable: "DELETE FROM sqlxtesttable", +}; From 4851fc88b78df7f22436b27d3a69ccb88fc6d79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 24 Apr 2024 17:08:18 +0200 Subject: [PATCH 30/38] Updated CI --- .github/workflows/ci.yml | 107 ++++++++++----------- .github/workflows/publish-to-nest.land.yml | 23 ----- .github/workflows/publish.yml | 29 ++++++ .github/workflows/wait-for-mysql.sh | 11 --- 4 files changed, 80 insertions(+), 90 deletions(-) delete mode 100644 .github/workflows/publish-to-nest.land.yml create mode 100644 .github/workflows/publish.yml delete mode 100755 .github/workflows/wait-for-mysql.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3860f8a..96d79be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,69 +1,64 @@ name: ci -on: [push, pull_request] +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + DENO_VERSION: vx.x.x jobs: - fmt: + check: + name: Check format and lint + runs-on: ubuntu-latest + + steps: + - name: Clone repo + uses: actions/checkout@v4 + + - name: Install deno + uses: denoland/setup-deno@v1 + with: + deno-version: ${{env.DENO_VERSION}} + + - name: Check + run: deno task check + + tests: + name: Run tests runs-on: ubuntu-latest - continue-on-error: true + steps: - - uses: actions/checkout@v1 - - name: Install Deno 1.x + - name: Clone repo + uses: actions/checkout@v4 + + - name: Install deno uses: denoland/setup-deno@v1 with: - deno-version: v1.x - - name: Check fmt - run: deno fmt --check - test: + deno-version: ${{env.DENO_VERSION}} + + - name: Test + run: deno task test:ga + + publish: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - DENO_VERSION: - - v1.x - DB_VERSION: - - mysql:5.5 - - mysql:5.6 - - mysql:5.7 - - mysql:8 - - mysql:latest - - mariadb:5.5 - - mariadb:10.0 - - mariadb:10.1 - - mariadb:10.2 - - mariadb:10.3 - - mariadb:10.4 -# - mariadb:latest + + permissions: + contents: read + id-token: write steps: - - uses: actions/checkout@v1 - - name: Install Deno ${{ matrix.DENO_VERSION }} + - name: Checkout + uses: actions/checkout@v4 + + - name: Install deno uses: denoland/setup-deno@v1 with: - deno-version: ${{ matrix.DENO_VERSION }} - - name: Show Deno version - run: deno --version - - name: Start ${{ matrix.DB_VERSION }} - run: | - sudo mkdir -p /var/run/mysqld/tmp - sudo chmod -R 777 /var/run/mysqld - docker container run --name mysql --rm -d -p 3306:3306 \ - -v /var/run/mysqld:/var/run/mysqld \ - -v /var/run/mysqld/tmp:/tmp \ - -e MYSQL_ROOT_PASSWORD=root \ - ${{ matrix.DB_VERSION }} - ./.github/workflows/wait-for-mysql.sh - - name: Run tests (TCP) - run: | - deno test --allow-env --allow-net=127.0.0.1:3306 ./test.ts - - name: Run tests (--unstable) (UNIX domain socket) - run: | - SOCKPATH=/var/run/mysqld/mysqld.sock - if [[ "${{ matrix.DB_VERSION }}" == "mysql:5.5" ]]; then - SOCKPATH=/var/run/mysqld/tmp/mysql.sock - fi - echo "DROP USER 'root'@'localhost';" | docker exec -i mysql mysql -proot - DB_SOCKPATH=$SOCKPATH TEST_METHODS=unix \ - deno test --unstable --allow-env \ - --allow-read=/var/run/mysqld/ --allow-write=/var/run/mysqld/ \ - ./test.ts + deno-version: ${{env.DENO_VERSION}} + + - name: Publish (dry run) + run: deno publish --dry-run diff --git a/.github/workflows/publish-to-nest.land.yml b/.github/workflows/publish-to-nest.land.yml deleted file mode 100644 index 87cc581..0000000 --- a/.github/workflows/publish-to-nest.land.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: "publish current release to https://nest.land" - -on: - release: - types: - - published - -jobs: - publishToNestDotLand: - runs-on: ubuntu-latest - - steps: - - name: Setup repo - uses: actions/checkout@v2 - - - name: "setup" # check: https://github.com/actions/virtual-environments/issues/1777 - uses: denolib/setup-deno@v2 - with: - deno-version: v1.4.6 - - - name: "check nest.land" - run: | - 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 }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..58837de --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,29 @@ +name: Publish + +on: + release: + types: [published] + +env: + DENO_VERSION: vx.x.x + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Deno + uses: denoland/setup-deno@v1 + with: + deno-version: ${{env.DENO_VERSION}} + + - name: Publish + if: github.event_name == 'release' + run: deno publish diff --git a/.github/workflows/wait-for-mysql.sh b/.github/workflows/wait-for-mysql.sh deleted file mode 100755 index 13302dd..0000000 --- a/.github/workflows/wait-for-mysql.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -echo "Waiting for MySQL" -for i in `seq 1 30`; -do - echo '\q' | mysql -h 127.0.0.1 -uroot --password=root -P 3306 && exit 0 - >&2 echo "MySQL is waking up" - sleep 1 -done - -echo "Failed waiting for MySQL" && exit 1 From 3958a440b717cab6744ff414fc44ac8b5f19f269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 24 Apr 2024 17:09:58 +0200 Subject: [PATCH 31/38] bump sqlx --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 2f02305..990d4e4 100644 --- a/deno.json +++ b/deno.json @@ -16,7 +16,7 @@ "db:stop": "docker compose down --remove-orphans --volumes" }, "imports": { - "@halvardm/sqlx": "jsr:@halvardm/sqlx@0.0.0-11", + "@halvardm/sqlx": "jsr:@halvardm/sqlx@0.0.0-12", "@std/assert": "jsr:@std/assert@^0.221.0", "@std/async": "jsr:@std/async@^0.221.0", "@std/crypto": "jsr:@std/crypto@^0.221.0", From 4b6be5c6380fcac80e2896fa8f752880f2267f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 24 Apr 2024 17:14:59 +0200 Subject: [PATCH 32/38] updated ci --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96d79be..939c8ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,10 @@ name: ci on: push: branches: - - main + - master pull_request: branches: - - main + - master env: DENO_VERSION: vx.x.x From 20b2a5420736115e5701d858fa9d82feee8aad6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Sun, 28 Apr 2024 23:59:40 +0200 Subject: [PATCH 33/38] bump sqlx --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 990d4e4..4c4cb06 100644 --- a/deno.json +++ b/deno.json @@ -16,7 +16,7 @@ "db:stop": "docker compose down --remove-orphans --volumes" }, "imports": { - "@halvardm/sqlx": "jsr:@halvardm/sqlx@0.0.0-12", + "@halvardm/sqlx": "jsr:@halvardm/sqlx@0.0.0-13", "@std/assert": "jsr:@std/assert@^0.221.0", "@std/async": "jsr:@std/async@^0.221.0", "@std/crypto": "jsr:@std/crypto@^0.221.0", From 2980d63020b7cfe5b9ec9ec177b349b8fa629df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Mon, 29 Apr 2024 00:00:49 +0200 Subject: [PATCH 34/38] Cleanup hexdump --- deno.json | 3 +- lib/packets/packet.ts | 6 +- lib/utils/bytes.test.ts | 148 ---------------------------------------- lib/utils/bytes.ts | 54 --------------- 4 files changed, 5 insertions(+), 206 deletions(-) delete mode 100644 lib/utils/bytes.test.ts diff --git a/deno.json b/deno.json index 4c4cb06..5f40acb 100644 --- a/deno.json +++ b/deno.json @@ -29,6 +29,7 @@ "@std/semver": "jsr:@std/semver@^0.220.1", "@std/testing": "jsr:@std/testing@^0.221.0", "@std/text": "jsr:@std/text@^0.222.1", - "@std/yaml": "jsr:@std/yaml@^0.223.0" + "@std/yaml": "jsr:@std/yaml@^0.223.0", + "@stdext/encoding": "jsr:@stdext/encoding@^0.0.2" } } diff --git a/lib/packets/packet.ts b/lib/packets/packet.ts index 0f938dd..9af5894 100644 --- a/lib/packets/packet.ts +++ b/lib/packets/packet.ts @@ -1,4 +1,4 @@ -import { hexdump } from "../utils/bytes.ts"; +import { dump } from "@stdext/encoding/hex"; import { BufferReader, BufferWriter } from "../utils/buffer.ts"; import { MysqlWriteError } from "../utils/errors.ts"; import { logger } from "../utils/logger.ts"; @@ -34,7 +34,7 @@ export class PacketWriter { data.writeUints(3, this.header.size); data.write(this.header.no); data.writeBuffer(body); - logger().debug(`send: ${data.length}B \n${hexdump(data.buffer)}\n`); + logger().debug(`send: ${data.length}B \n${dump(data.buffer)}\n`); try { let wrote = 0; do { @@ -146,7 +146,7 @@ export class PacketReader { data.set(headerReader.buffer); data.set(bodyReader.buffer, 4); return `receive: ${readCount}B, size = ${header.size}, no = ${header.no} \n${ - hexdump(data) + dump(data) }\n`; }); diff --git a/lib/utils/bytes.test.ts b/lib/utils/bytes.test.ts deleted file mode 100644 index 6b88a44..0000000 --- a/lib/utils/bytes.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { hexdump } from "./bytes.ts"; - -Deno.test("hexdump", async (t) => { - const data = - "This is a test string that is longer than 16 bytes and will be split into multiple lines for the hexdump. The quick brown fox jumps over the lazy dog. Foo bar baz."; - const buffer8Compatible = new TextEncoder().encode(data); - - function bufferPad(buffer: ArrayBufferView, multipleOf: number): number[] { - const bufferLength = buffer.byteLength; - const remainder = Math.ceil(bufferLength / multipleOf); - const padCeil = remainder * multipleOf; - const missing = padCeil - bufferLength; - - const result = []; - for (let i = 0; i < missing; i++) { - result.push(0); - } - - return result; - } - - const buffer16Compatible = - new Uint8Array([...buffer8Compatible, ...bufferPad(buffer8Compatible, 2)]) - .buffer; - const buffer32Compatible = - new Uint8Array([...buffer8Compatible, ...bufferPad(buffer8Compatible, 4)]) - .buffer; - const buffer64Compatible = - new Uint8Array([...buffer8Compatible, ...bufferPad(buffer8Compatible, 8)]) - .buffer; - - const buffer8Result = - `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| -00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| -00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| -00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| -00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| -00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| -00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| -00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| -00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| -00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| -000000a0 61 7a 2e |az.|`; - - const buffer16Result = - `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| -00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| -00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| -00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| -00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| -00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| -00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| -00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| -00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| -00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| -000000a0 61 7a 2e 00 |az..|`; - - const buffer32Result = - `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| -00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| -00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| -00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| -00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| -00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| -00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| -00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| -00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| -00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| -000000a0 61 7a 2e 00 |az..|`; - const buffer64Result = - `00000000 54 68 69 73 20 69 73 20 61 20 74 65 73 74 20 73 |This is a test s| -00000010 74 72 69 6e 67 20 74 68 61 74 20 69 73 20 6c 6f |tring that is lo| -00000020 6e 67 65 72 20 74 68 61 6e 20 31 36 20 62 79 74 |nger than 16 byt| -00000030 65 73 20 61 6e 64 20 77 69 6c 6c 20 62 65 20 73 |es and will be s| -00000040 70 6c 69 74 20 69 6e 74 6f 20 6d 75 6c 74 69 70 |plit into multip| -00000050 6c 65 20 6c 69 6e 65 73 20 66 6f 72 20 74 68 65 |le lines for the| -00000060 20 68 65 78 64 75 6d 70 2e 20 54 68 65 20 71 75 | hexdump. The qu| -00000070 69 63 6b 20 62 72 6f 77 6e 20 66 6f 78 20 6a 75 |ick brown fox ju| -00000080 6d 70 73 20 6f 76 65 72 20 74 68 65 20 6c 61 7a |mps over the laz| -00000090 79 20 64 6f 67 2e 20 46 6f 6f 20 62 61 72 20 62 |y dog. Foo bar b| -000000a0 61 7a 2e 00 00 00 00 00 |az......|`; - - await t.step("Uint8Array", () => { - const result = hexdump(buffer8Compatible); - assertEquals(result, buffer8Result); - }); - - await t.step("Uint16Array", () => { - const result = hexdump(new Uint16Array(buffer16Compatible)); - assertEquals(result, buffer16Result); - }); - - await t.step("Uint32Array", () => { - const result = hexdump(new Uint32Array(buffer32Compatible)); - assertEquals(result, buffer32Result); - }); - - await t.step("Uint8ClampedArray", () => { - const result = hexdump(new Uint8ClampedArray(buffer8Compatible.buffer)); - assertEquals(result, buffer8Result); - }); - - await t.step("Int8Array", () => { - const result = hexdump(new Int8Array(buffer8Compatible.buffer)); - assertEquals(result, buffer8Result); - }); - - await t.step("Int16Array", () => { - const result = hexdump(new Int16Array(buffer16Compatible)); - assertEquals(result, buffer16Result); - }); - - await t.step("Int32Array", () => { - const result = hexdump(new Int32Array(buffer32Compatible)); - assertEquals(result, buffer32Result); - }); - - await t.step("Float32Array", () => { - const result = hexdump(new Float32Array(buffer32Compatible)); - assertEquals(result, buffer32Result); - }); - - await t.step("Float64Array", () => { - const result = hexdump(new Float64Array(buffer64Compatible)); - assertEquals(result, buffer64Result); - }); - - await t.step("BigInt64Array", () => { - const result = hexdump(new BigInt64Array(buffer64Compatible)); - assertEquals(result, buffer64Result); - }); - - await t.step("BigUint64Array", () => { - const result = hexdump(new BigUint64Array(buffer64Compatible)); - assertEquals(result, buffer64Result); - }); - - await t.step("DataView", () => { - const result = hexdump(new DataView(buffer8Compatible.buffer)); - assertEquals(result, buffer8Result); - }); - - await t.step("ArrayBuffer", () => { - const result = hexdump(buffer8Compatible.buffer); - assertEquals(result, buffer8Result); - }); -}); diff --git a/lib/utils/bytes.ts b/lib/utils/bytes.ts index 5a5d38b..1e4efd9 100644 --- a/lib/utils/bytes.ts +++ b/lib/utils/bytes.ts @@ -1,57 +1,3 @@ -/** - * Convert a buffer to a hexdump string. - * - * @example - * ```ts - * const buffer = new TextEncoder().encode("The quick brown fox jumps over the lazy dog."); - * console.log(hexdump(buffer)); - * // 00000000 54 68 65 20 71 75 69 63 6b 20 62 72 6f 77 6e 20 |The quick brown | - * // 00000010 66 6f 78 20 6a 75 6d 70 73 20 6f 76 65 72 20 74 |fox jumps over t| - * // 00000020 68 65 20 6c 61 7a 79 20 64 6f 67 2e |he lazy dog.| - * ``` - */ -export function hexdump(bufferView: ArrayBufferView | ArrayBuffer): string { - let bytes: Uint8Array; - if (ArrayBuffer.isView(bufferView)) { - bytes = new Uint8Array(bufferView.buffer); - } else { - bytes = new Uint8Array(bufferView); - } - - const lines = []; - - for (let i = 0; i < bytes.length; i += 16) { - const address = i.toString(16).padStart(8, "0"); - const block = bytes.slice(i, i + 16); - const hexArray = []; - const asciiArray = []; - let padding = ""; - - for (const value of block) { - hexArray.push(value.toString(16).padStart(2, "0")); - asciiArray.push( - value >= 0x20 && value < 0x7f ? String.fromCharCode(value) : ".", - ); - } - - if (hexArray.length < 16) { - const space = 16 - hexArray.length; - padding = " ".repeat(space * 2 + space + (hexArray.length < 9 ? 1 : 0)); - } - - const hexString = hexArray.length > 8 - ? hexArray.slice(0, 8).join(" ") + " " + hexArray.slice(8).join(" ") - : hexArray.join(" "); - - const asciiString = asciiArray.join(""); - const line = `${address} ${hexString} ${padding}|${asciiString}|`; - - lines.push(line); - } - - return lines.join("\n"); -} - export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { return a.map((byte, index) => { return byte ^ b[index]; From 4b475f42417c2e7afbd815c70af3e893c63cd8f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Mon, 29 Apr 2024 00:01:32 +0200 Subject: [PATCH 35/38] cleanup pool --- lib/pool.ts | 13 +++---------- lib/utils/events.ts | 8 +------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/lib/pool.ts b/lib/pool.ts index 0229ab7..0228ccf 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -19,10 +19,8 @@ import { MysqlPoolAcquireEvent, MysqlPoolCloseEvent, MysqlPoolConnectEvent, - MysqlPoolDestroyEvent, MysqlPoolReleaseEvent, } from "./utils/events.ts"; -import { logger } from "./utils/logger.ts"; import { MysqlClientEventTarget } from "./utils/events.ts"; export interface MysqlClientPoolOptions @@ -129,6 +127,9 @@ export class MysqlClientPool extends SqlxBase implements async acquire(): Promise { const client = await this.deferredStack.pop(); + if (!client.connected) { + await client.connection.connect(); + } this.eventTarget.dispatchEvent( new MysqlPoolAcquireEvent({ connectable: client }), @@ -144,7 +145,6 @@ export class MysqlClientPool extends SqlxBase implements this.deferredStack.push(client); } catch (e) { if (e instanceof SqlxError && e.message === "Max pool size reached") { - logger().debug(e.message); await client.connection.close(); throw e; } else { @@ -153,13 +153,6 @@ export class MysqlClientPool extends SqlxBase implements } } - async destroy(client: MysqlPoolClient): Promise { - this.eventTarget.dispatchEvent( - new MysqlPoolDestroyEvent({ connectable: client }), - ); - await client.connection.close(); - } - async [Symbol.asyncDispose](): Promise { await this.close(); } diff --git a/lib/utils/events.ts b/lib/utils/events.ts index 069c83d..ba89720 100644 --- a/lib/utils/events.ts +++ b/lib/utils/events.ts @@ -5,7 +5,6 @@ import { type SqlxConnectableEventInit, SqlxEventTarget, SqlxPoolConnectableAcquireEvent, - SqlxPoolConnectableDestroyEvent, SqlxPoolConnectableReleaseEvent, type SqlxPoolConnectionEventType, } from "@halvardm/sqlx"; @@ -58,10 +57,6 @@ export class MysqlPoolReleaseEvent extends SqlxPoolConnectableReleaseEvent { } -export class MysqlPoolDestroyEvent - extends SqlxPoolConnectableDestroyEvent { -} - export type MysqlClientEvents = | MysqlClientConnectEvent | MysqlClientCloseEvent; @@ -70,5 +65,4 @@ export type MysqlPoolEvents = | MysqlClientConnectEvent | MysqlClientCloseEvent | MysqlPoolAcquireEvent - | MysqlPoolReleaseEvent - | MysqlPoolDestroyEvent; + | MysqlPoolReleaseEvent; From fe9e36324b8982ba2aa40511201a5bed6d787b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Mon, 29 Apr 2024 00:17:20 +0200 Subject: [PATCH 36/38] Fixed test due to sort --- lib/connection.test.ts | 4 ++-- lib/connection.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/connection.test.ts b/lib/connection.test.ts index 1735ba4..25c1ff9 100644 --- a/lib/connection.test.ts +++ b/lib/connection.test.ts @@ -87,9 +87,9 @@ Deno.test("Connection", async (t) => { mode: "VERIFY_IDENTITY", caCerts: [ "ca", - "key", - "cert", "ca2", + "cert", + "key", ], cert: "cert", hostname: "127.0.0.1", diff --git a/lib/connection.ts b/lib/connection.ts index d7ec47a..03c6157 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -439,6 +439,8 @@ export class MysqlConnection extends SqlxBase implements const content = Deno.readTextFileSync(caCert); tlsOptions.caCerts.push(content); } + // Due to some random bug in CI, we need to sort this for the test to pass consistently. + tlsOptions.caCerts.sort(); } if (config.parameters.sslKey) { From c499637411c36cbe1bfc654c28cda77454a4ea2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 26 Jun 2024 16:42:23 +0200 Subject: [PATCH 37/38] updated to reflect @stdext/sql --- deno.json | 5 +- lib/client.test.ts | 59 +++++------ lib/client.ts | 24 ++--- lib/connection.test.ts | 60 ++++++----- lib/connection.ts | 97 +++++++++++------ lib/core.test.ts | 42 ++++++++ lib/{sqlx.ts => core.ts} | 186 +++++++++++++++------------------ lib/packets/parsers/result.ts | 14 +-- lib/pool.test.ts | 63 +++++------ lib/pool.ts | 170 ++++++++++++++++-------------- lib/utils/errors.ts | 6 +- lib/utils/events.test.ts | 10 ++ lib/utils/events.ts | 77 ++++++-------- lib/utils/testing.ts | 191 +++++++++++++++++++++++++++++++--- 14 files changed, 612 insertions(+), 392 deletions(-) create mode 100644 lib/core.test.ts rename lib/{sqlx.ts => core.ts} (70%) create mode 100644 lib/utils/events.test.ts diff --git a/deno.json b/deno.json index 5f40acb..cd94249 100644 --- a/deno.json +++ b/deno.json @@ -16,7 +16,6 @@ "db:stop": "docker compose down --remove-orphans --volumes" }, "imports": { - "@halvardm/sqlx": "jsr:@halvardm/sqlx@0.0.0-13", "@std/assert": "jsr:@std/assert@^0.221.0", "@std/async": "jsr:@std/async@^0.221.0", "@std/crypto": "jsr:@std/crypto@^0.221.0", @@ -30,6 +29,8 @@ "@std/testing": "jsr:@std/testing@^0.221.0", "@std/text": "jsr:@std/text@^0.222.1", "@std/yaml": "jsr:@std/yaml@^0.223.0", - "@stdext/encoding": "jsr:@stdext/encoding@^0.0.2" + "@stdext/collections": "jsr:@stdext/collections@^0.0.5", + "@stdext/encoding": "jsr:@stdext/encoding@^0.0.2", + "@stdext/sql": "jsr:@stdext/sql@0.0.0-5" } } diff --git a/lib/client.test.ts b/lib/client.test.ts index ef1bfc6..23d61f2 100644 --- a/lib/client.test.ts +++ b/lib/client.test.ts @@ -1,49 +1,38 @@ import { MysqlClient } from "./client.ts"; -import { QUERIES, services } from "./utils/testing.ts"; -import { clientTest } from "@halvardm/sqlx/testing"; +import { services, testTransactionable } from "./utils/testing.ts"; +import { testClientConnection, testSqlClient } from "@stdext/sql/testing"; Deno.test("Client Test", async (t) => { for (const service of services) { await t.step(`Testing ${service.name}`, async (t) => { + testSqlClient(new MysqlClient(service.url), { + connectionUrl: service.url, + options: {}, + }); + + async function connectionTest(t: Deno.TestContext) { + await testClientConnection(t, MysqlClient, [service.url, {}]); + await using client = new MysqlClient(service.url); + await client.connect(); + await client.execute("DROP TABLE IF EXISTS sqltesttable"); + await client.execute( + "CREATE TABLE IF NOT EXISTS sqltesttable (testcol TEXT)", + ); + try { + await testTransactionable(client); + } finally { + await client.execute("DROP TABLE IF EXISTS sqltesttable"); + } + } + await t.step(`TCP`, async (t) => { - await clientTest({ - t, - Client: MysqlClient, - connectionUrl: service.url, - connectionOptions: {}, - queries: QUERIES, - }); + await connectionTest(t); }); // Enable once socket connection issue is fixed // // await t.step(`UNIX Socket`, async (t) => { - // await implementationTest({ - // t, - // Client: MysqlClient, - // // deno-lint-ignore no-explicit-any - // PoolClient: MysqlClientPool as any, - // connectionUrl: service.urlSocket, - // connectionOptions: {}, - // queries: { - // createTable: - // "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", - // dropTable: "DROP TABLE IF EXISTS sqlxtesttable", - // insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", - // insertManyToTable: - // "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", - // selectOneFromTable: - // "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", - // selectByMatchFromTable: - // "SELECT * FROM sqlxtesttable WHERE testcol = ?", - // selectManyFromTable: "SELECT * FROM sqlxtesttable", - // select1AsString: "SELECT '1' as result", - // select1Plus1AsNumber: "SELECT 1+1 as result", - // deleteByMatchFromTable: - // "DELETE FROM sqlxtesttable WHERE testcol = ?", - // deleteAllFromTable: "DELETE FROM sqlxtesttable", - // }, - // }); + // await connectionTest(t) // }); }); } diff --git a/lib/client.ts b/lib/client.ts index 1a29909..798ab2c 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,18 +1,18 @@ -import { SqlxClient } from "@halvardm/sqlx"; +import type { SqlClient } from "@stdext/sql"; import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; import type { MysqlParameterType } from "./packets/parsers/result.ts"; import { - MysqlClientCloseEvent, MysqlClientEventTarget, + MysqlCloseEvent, + MysqlConnectEvent, } from "./utils/events.ts"; -import { MysqlClientConnectEvent } from "../mod.ts"; import { - type MysqlPrepared, + type MysqlPreparedStatement, type MysqlQueryOptions, - type MySqlTransaction, + type MysqlTransaction, MysqlTransactionable, type MysqlTransactionOptions, -} from "./sqlx.ts"; +} from "./core.ts"; export interface MysqlClientOptions extends MysqlConnectionOptions { } @@ -21,15 +21,15 @@ export interface MysqlClientOptions extends MysqlConnectionOptions { * MySQL client */ export class MysqlClient extends MysqlTransactionable implements - SqlxClient< + SqlClient< MysqlClientEventTarget, MysqlConnectionOptions, - MysqlConnection, MysqlParameterType, MysqlQueryOptions, - MysqlPrepared, + MysqlConnection, + MysqlPreparedStatement, MysqlTransactionOptions, - MySqlTransaction + MysqlTransaction > { eventTarget: MysqlClientEventTarget; connectionUrl: string; @@ -48,12 +48,12 @@ export class MysqlClient extends MysqlTransactionable implements async connect(): Promise { await this.connection.connect(); this.eventTarget.dispatchEvent( - new MysqlClientConnectEvent({ connectable: this }), + new MysqlConnectEvent({ connection: this.connection }), ); } async close(): Promise { this.eventTarget.dispatchEvent( - new MysqlClientCloseEvent({ connectable: this }), + new MysqlCloseEvent({ connection: this.connection }), ); await this.connection.close(); } diff --git a/lib/connection.test.ts b/lib/connection.test.ts index 25c1ff9..e361dcc 100644 --- a/lib/connection.test.ts +++ b/lib/connection.test.ts @@ -1,11 +1,25 @@ import { assertEquals, assertInstanceOf } from "@std/assert"; import { emptyDir } from "@std/fs"; import { join } from "@std/path"; -import { MysqlConnection } from "./connection.ts"; -import { DIR_TMP_TEST } from "./utils/testing.ts"; +import { MysqlConnectable, MysqlConnection } from "./connection.ts"; +import { DIR_TMP_TEST, services } from "./utils/testing.ts"; import { buildQuery } from "./packets/builders/query.ts"; import { URL_TEST_CONNECTION } from "./utils/testing.ts"; -import { connectionConstructorTest } from "@halvardm/sqlx/testing"; +import { assertIsSqlConnection } from "@stdext/sql"; +import { _testSqlConnectable, testSqlConnection } from "@stdext/sql/testing"; + +Deno.test("connection and connectable contstructs", async (t) => { + for (const service of services) { + await t.step(`Testing ${service.name}`, async (t) => { + await t.step(`TCP`, () => { + const connection = new MysqlConnection(service.url); + testSqlConnection(connection, { connectionUrl: service.url }); + const connectable = new MysqlConnectable(connection); + _testSqlConnectable(connectable, connection); + }); + }); + } +}); Deno.test("Connection", async (t) => { await emptyDir(DIR_TMP_TEST); @@ -25,6 +39,7 @@ Deno.test("Connection", async (t) => { assertInstanceOf(connection, MysqlConnection); assertEquals(connection.connectionUrl, URL_TEST_CONNECTION); + assertIsSqlConnection(connection); await t.step("can parse connection config simple", () => { const url = new URL("mysql://user:pass@127.0.0.1:3306/db"); @@ -130,13 +145,6 @@ Deno.test("Connection", async (t) => { await connection.close(); }); - await connectionConstructorTest({ - t, - Connection: MysqlConnection, - connectionUrl: URL_TEST_CONNECTION, - connectionOptions: {}, - }); - await t.step("can query database", async (t) => { await using connection = new MysqlConnection(URL_TEST_CONNECTION); await connection.connect(); @@ -395,27 +403,29 @@ Deno.test("Connection", async (t) => { ]); }); - await t.step("can insert to table using executeRaw", async () => { - const data = buildQuery("INSERT INTO test (id) VALUES (4);"); - const result = await connection.executeRaw(data); + await t.step("can insert to table using execute", async () => { + const result = await connection.execute( + "INSERT INTO test (id) VALUES (4);", + ); assertEquals(result, 1); }); - await t.step("can select from table using executeRaw", async () => { - const data = buildQuery("SELECT * FROM test;"); - const result = await connection.executeRaw(data); + await t.step("can select from table using execute", async () => { + const result = await connection.execute("SELECT * FROM test;"); assertEquals(result, undefined); }); await t.step("can insert to table using queryManyObjectRaw", async () => { - const data = buildQuery("INSERT INTO test (id) VALUES (5);"); - const result = await Array.fromAsync(connection.queryManyObjectRaw(data)); + const result = await Array.fromAsync( + connection.queryMany("INSERT INTO test (id) VALUES (5);"), + ); assertEquals(result, []); }); await t.step("can select from table using queryManyObjectRaw", async () => { - const data = buildQuery("SELECT * FROM test;"); - const result = await Array.fromAsync(connection.queryManyObjectRaw(data)); + const result = await Array.fromAsync( + connection.queryMany("SELECT * FROM test;"), + ); assertEquals(result, [ { id: 1 }, { id: 2 }, @@ -426,14 +436,16 @@ Deno.test("Connection", async (t) => { }); await t.step("can insert to table using queryManyArrayRaw", async () => { - const data = buildQuery("INSERT INTO test (id) VALUES (6);"); - const result = await Array.fromAsync(connection.queryManyArrayRaw(data)); + const result = await Array.fromAsync( + connection.queryManyArray("INSERT INTO test (id) VALUES (6);"), + ); assertEquals(result, []); }); await t.step("can select from table using queryManyArrayRaw", async () => { - const data = buildQuery("SELECT * FROM test;"); - const result = await Array.fromAsync(connection.queryManyArrayRaw(data)); + const result = await Array.fromAsync( + connection.queryManyArray("SELECT * FROM test;"), + ); assertEquals(result, [ [1], [2], diff --git a/lib/connection.ts b/lib/connection.ts index 03c6157..fa1423d 100644 --- a/lib/connection.ts +++ b/lib/connection.ts @@ -26,17 +26,19 @@ import auth from "./utils/hash.ts"; import { ServerCapabilities } from "./constant/capabilities.ts"; import { buildSSLRequest } from "./packets/builders/tls.ts"; import { logger } from "./utils/logger.ts"; -import { - type ArrayRow, - type Row, - SqlxBase, - type SqlxConnection, - type SqlxConnectionOptions, - type SqlxQueryOptions, -} from "@halvardm/sqlx"; +import type { + ArrayRow, + Row, + SqlConnectable, + SqlConnection, + SqlConnectionOptions, + SqlQueryOptions, +} from "@stdext/sql"; import { resolve } from "@std/path"; import { toCamelCase } from "@std/text"; import { AuthPluginName } from "./auth_plugins/mod.ts"; +import { buildQuery } from "./packets/builders/query.ts"; +import type { MysqlQueryOptions } from "./core.ts"; /** * Connection state @@ -49,7 +51,7 @@ export enum ConnectionState { } export type ConnectionSendDataNext = { - row: ArrayRow; + row: ArrayRow; fields: FieldInfo[]; }; export type ConnectionSendDataResult = { @@ -71,9 +73,12 @@ export const TlsMode = { } as const; export type TlsMode = typeof TlsMode[keyof typeof TlsMode]; -export interface TlsOptions extends Deno.ConnectTlsOptions { - mode: TlsMode; -} +export type TlsOptions = + & Deno.ConnectTlsOptions + & Partial + & { + mode: TlsMode; + }; /** * Aditional connection parameters @@ -127,14 +132,16 @@ export interface ConnectionConfig { parameters: ConnectionParameters; } -export interface MysqlConnectionOptions extends SqlxConnectionOptions { +export interface MysqlConnectionOptions extends SqlConnectionOptions { tls?: Partial; } /** Connection for mysql */ -export class MysqlConnection extends SqlxBase implements - SqlxConnection< - MysqlConnectionOptions +export class MysqlConnection implements + SqlConnection< + MysqlConnectionOptions, + MysqlParameterType, + MysqlQueryOptions > { state: ConnectionState = ConnectionState.CONNECTING; capabilities: number = 0; @@ -144,7 +151,7 @@ export class MysqlConnection extends SqlxBase implements private _timedOut = false; readonly connectionUrl: string; - readonly connectionOptions: MysqlConnectionOptions; + readonly options: MysqlConnectionOptions; readonly config: ConnectionConfig; get conn(): Deno.Conn { @@ -171,9 +178,8 @@ export class MysqlConnection extends SqlxBase implements connectionUrl: string | URL, connectionOptions: MysqlConnectionOptions = {}, ) { - super(); this.connectionUrl = connectionUrl.toString().split("?")[0]; - this.connectionOptions = connectionOptions; + this.options = connectionOptions; this.config = this.#parseConnectionConfig( connectionUrl, connectionOptions, @@ -525,7 +531,7 @@ export class MysqlConnection extends SqlxBase implements async *sendData( data: Uint8Array, - options?: SqlxQueryOptions, + options?: SqlQueryOptions, ): AsyncGenerator< ConnectionSendDataNext, ConnectionSendDataResult | undefined @@ -577,10 +583,12 @@ export class MysqlConnection extends SqlxBase implements } } - async executeRaw( - data: Uint8Array, - options?: SqlxQueryOptions, + async execute( + sql: string, + params?: MysqlParameterType[], + options?: MysqlQueryOptions, ): Promise { + const data = buildQuery(sql, params); const gen = this.sendData(data, options); let result = await gen.next(); if (result.done) { @@ -597,20 +605,22 @@ export class MysqlConnection extends SqlxBase implements logger().debug(`executeRaw overflow: ${JSON.stringify(debugRest)}`); return undefined; } - - async *queryManyObjectRaw = Row>( - data: Uint8Array, - options?: SqlxQueryOptions, - ): AsyncIterableIterator { + async *queryMany = Row>( + sql: string, + params?: MysqlParameterType[], + options?: MysqlQueryOptions, + ): AsyncGenerator { + const data = buildQuery(sql, params); for await (const res of this.sendData(data, options)) { yield getRowObject(res.fields, res.row) as T; } } - - async *queryManyArrayRaw = ArrayRow>( - data: Uint8Array, - options?: SqlxQueryOptions, - ): AsyncIterableIterator { + async *queryManyArray = ArrayRow>( + sql: string, + params?: MysqlParameterType[], + options?: MysqlQueryOptions, + ): AsyncGenerator { + const data = buildQuery(sql, params); for await (const res of this.sendData(data, options)) { const row = res.row as T; yield row as T; @@ -621,3 +631,24 @@ export class MysqlConnection extends SqlxBase implements await this.close(); } } + +export class MysqlConnectable + implements SqlConnectable { + readonly connection: MysqlConnection; + readonly options: MysqlConnectionOptions; + + get connected(): boolean { + return this.connection.connected; + } + + constructor( + connection: MysqlConnectable["connection"], + options: MysqlConnectable["options"] = {}, + ) { + this.connection = connection; + this.options = options; + } + [Symbol.asyncDispose](): Promise { + return this.connection.close(); + } +} diff --git a/lib/core.test.ts b/lib/core.test.ts new file mode 100644 index 0000000..a38635f --- /dev/null +++ b/lib/core.test.ts @@ -0,0 +1,42 @@ +import { + testSqlPreparedStatement, + testSqlTransaction, +} from "@stdext/sql/testing"; +import { MysqlConnection } from "./connection.ts"; +import { MysqlPreparedStatement, MysqlTransaction } from "./core.ts"; +import { services } from "./utils/testing.ts"; + +Deno.test(`sql/type test`, async (t) => { + for (const service of services) { + const connectionUrl = service.url; + const options: MysqlTransaction["options"] = {}; + + await t.step(`Testing ${service.name}`, async (t) => { + const sql = "SELECT 1 as one;"; + + await using connection = new MysqlConnection(connectionUrl, options); + await connection.connect(); + const preparedStatement = new MysqlPreparedStatement( + connection, + sql, + options, + ); + const transaction = new MysqlTransaction(connection, options); + + const expects = { + connectionUrl, + options, + clientPoolOptions: options, + sql, + }; + + await t.step(`sql/PreparedStatement`, () => { + testSqlPreparedStatement(preparedStatement, expects); + }); + + await t.step(`sql/SqlTransaction`, () => { + testSqlTransaction(transaction, expects); + }); + }); + } +}); diff --git a/lib/sqlx.ts b/lib/core.ts similarity index 70% rename from lib/sqlx.ts rename to lib/core.ts index 0dc5679..06db8e8 100644 --- a/lib/sqlx.ts +++ b/lib/core.ts @@ -1,24 +1,26 @@ +import type { + ArrayRow, + Row, + SqlPreparable, + SqlPreparedStatement, + SqlQueriable, + SqlQueryOptions, + SqlTransaction, + SqlTransactionable, + SqlTransactionOptions, +} from "@stdext/sql"; import { - type ArrayRow, - type Row, - SqlxBase, - SqlxPreparable, - SqlxPreparedQueriable, - SqlxQueriable, - type SqlxQueryOptions, - SqlxTransactionable, - type SqlxTransactionOptions, - SqlxTransactionQueriable, -} from "@halvardm/sqlx"; -import type { MysqlConnection, MysqlConnectionOptions } from "./connection.ts"; -import { buildQuery } from "./packets/builders/query.ts"; + MysqlConnectable, + type MysqlConnection, + type MysqlConnectionOptions, +} from "./connection.ts"; import type { MysqlParameterType } from "./packets/parsers/result.ts"; import { MysqlTransactionError } from "./utils/errors.ts"; -export interface MysqlQueryOptions extends SqlxQueryOptions { +export interface MysqlQueryOptions extends SqlQueryOptions { } -export interface MysqlTransactionOptions extends SqlxTransactionOptions { +export interface MysqlTransactionOptions extends SqlTransactionOptions { beginTransactionOptions: { withConsistentSnapshot?: boolean; readWrite?: "READ WRITE" | "READ ONLY"; @@ -34,36 +36,27 @@ export interface MysqlTransactionOptions extends SqlxTransactionOptions { }; } -export class MysqlQueriable extends SqlxBase implements - SqlxQueriable< +export class MysqlQueriable extends MysqlConnectable implements + SqlQueriable< MysqlConnectionOptions, - MysqlConnection, MysqlParameterType, - MysqlQueryOptions + MysqlQueryOptions, + MysqlConnection > { - readonly connection: MysqlConnection; - readonly queryOptions: MysqlQueryOptions; - - get connected(): boolean { - return this.connection.connected; - } - + declare options: MysqlConnectionOptions & MysqlQueryOptions; constructor( - connection: MysqlConnection, - queryOptions: MysqlQueryOptions = {}, + connection: MysqlQueriable["connection"], + options: MysqlQueriable["options"] = {}, ) { - super(); - this.connection = connection; - this.queryOptions = queryOptions; + super(connection, options); } execute( sql: string, params?: MysqlParameterType[] | undefined, - _options?: MysqlQueryOptions | undefined, + options?: MysqlQueryOptions | undefined, ): Promise { - const data = buildQuery(sql, params); - return this.connection.executeRaw(data); + return this.connection.execute(sql, params, options); } query = Row>( sql: string, @@ -80,17 +73,12 @@ export class MysqlQueriable extends SqlxBase implements const res = await this.query(sql, params, options); return res[0]; } - async *queryMany = Row>( + queryMany = Row>( sql: string, params?: MysqlParameterType[], options?: MysqlQueryOptions | undefined, ): AsyncGenerator { - const data = buildQuery(sql, params); - for await ( - const res of this.connection.queryManyObjectRaw(data, options) - ) { - yield res; - } + return this.connection.queryMany(sql, params, options); } queryArray< @@ -112,19 +100,14 @@ export class MysqlQueriable extends SqlxBase implements const res = await this.queryArray(sql, params, options); return res[0]; } - async *queryManyArray< + queryManyArray< T extends ArrayRow = ArrayRow, >( sql: string, params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, ): AsyncGenerator { - const data = buildQuery(sql, params); - for await ( - const res of this.connection.queryManyArrayRaw(data, options) - ) { - yield res; - } + return this.connection.queryManyArray(sql, params, options); } sql = Row>( strings: TemplateStringsArray, @@ -147,59 +130,50 @@ export class MysqlQueriable extends SqlxBase implements * * @todo implement prepared statements properly */ -export class MysqlPrepared extends SqlxBase implements - SqlxPreparedQueriable< - MysqlConnectionOptions, - MysqlConnection, - MysqlParameterType, - MysqlQueryOptions - > { +export class MysqlPreparedStatement extends MysqlConnectable + implements + SqlPreparedStatement< + MysqlConnectionOptions, + MysqlParameterType, + MysqlQueryOptions, + MysqlConnection + > { + declare options: MysqlConnectionOptions & MysqlQueryOptions; readonly sql: string; - readonly queryOptions: MysqlQueryOptions; - - #queriable: MysqlQueriable; - - connection: MysqlConnection; - - get connected(): boolean { - return this.connection.connected; - } constructor( - connection: MysqlConnection, + connection: MysqlPreparedStatement["connection"], sql: string, - options: MysqlQueryOptions = {}, + options: MysqlPreparedStatement["options"] = {}, ) { - super(); - this.connection = connection; + super(connection, options); this.sql = sql; - this.queryOptions = options; - this.#queriable = new MysqlQueriable(connection, this.queryOptions); } execute( params?: MysqlParameterType[] | undefined, - _options?: MysqlQueryOptions | undefined, + options?: MysqlQueryOptions | undefined, ): Promise { - return this.#queriable.execute(this.sql, params); + return this.connection.execute(this.sql, params, options); } query = Row>( params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, ): Promise { - return this.#queriable.query(this.sql, params, options); + return Array.fromAsync(this.queryMany(params, options)); } - queryOne = Row>( + async queryOne = Row>( params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, ): Promise { - return this.#queriable.queryOne(this.sql, params, options); + const res = await this.query(params, options); + return res[0]; } queryMany = Row>( params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, ): AsyncGenerator { - return this.#queriable.queryMany(this.sql, params, options); + return this.connection.queryMany(this.sql, params, options); } queryArray< T extends ArrayRow = ArrayRow, @@ -207,15 +181,16 @@ export class MysqlPrepared extends SqlxBase implements params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, ): Promise { - return this.#queriable.queryArray(this.sql, params, options); + return Array.fromAsync(this.queryManyArray(params, options)); } - queryOneArray< + async queryOneArray< T extends ArrayRow = ArrayRow, >( params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, ): Promise { - return this.#queriable.queryOneArray(this.sql, params, options); + const res = await this.queryArray(params, options); + return res[0]; } queryManyArray< T extends ArrayRow = ArrayRow, @@ -223,32 +198,38 @@ export class MysqlPrepared extends SqlxBase implements params?: MysqlParameterType[] | undefined, options?: MysqlQueryOptions | undefined, ): AsyncGenerator { - return this.#queriable.queryManyArray(this.sql, params, options); + return this.connection.queryManyArray(this.sql, params, options); } } export class MysqlPreparable extends MysqlQueriable implements - SqlxPreparable< + SqlPreparable< MysqlConnectionOptions, - MysqlConnection, MysqlParameterType, MysqlQueryOptions, - MysqlPrepared + MysqlConnection, + MysqlPreparedStatement > { - prepare(sql: string, options?: MysqlQueryOptions | undefined): MysqlPrepared { - return new MysqlPrepared(this.connection, sql, options); + prepare( + sql: string, + options?: MysqlQueryOptions, + ): MysqlPreparedStatement { + return new MysqlPreparedStatement(this.connection, sql, options); } } -export class MySqlTransaction extends MysqlPreparable - implements - SqlxTransactionQueriable< - MysqlConnectionOptions, - MysqlConnection, - MysqlParameterType, - MysqlQueryOptions, - MysqlTransactionOptions - > { +export class MysqlTransaction extends MysqlPreparable implements + SqlTransaction< + MysqlConnectionOptions, + MysqlParameterType, + MysqlQueryOptions, + MysqlConnection, + MysqlPreparedStatement, + MysqlTransactionOptions + > { + declare readonly options: + & MysqlConnectionOptions + & MysqlQueryOptions; #inTransaction: boolean = true; get inTransaction(): boolean { return this.connected && this.#inTransaction; @@ -330,17 +311,22 @@ export class MySqlTransaction extends MysqlPreparable */ export class MysqlTransactionable extends MysqlPreparable implements - SqlxTransactionable< + SqlTransactionable< MysqlConnectionOptions, - MysqlConnection, MysqlParameterType, MysqlQueryOptions, + MysqlConnection, + MysqlPreparedStatement, MysqlTransactionOptions, - MySqlTransaction + MysqlTransaction > { + declare readonly options: + & MysqlConnectionOptions + & MysqlQueryOptions; + async beginTransaction( options?: MysqlTransactionOptions["beginTransactionOptions"], - ): Promise { + ): Promise { let sql = "START TRANSACTION"; if (options?.withConsistentSnapshot) { sql += ` WITH CONSISTENT SNAPSHOT`; @@ -352,11 +338,11 @@ export class MysqlTransactionable extends MysqlPreparable await this.execute(sql); - return new MySqlTransaction(this.connection, this.queryOptions); + return new MysqlTransaction(this.connection, this.options); } async transaction( - fn: (t: MySqlTransaction) => Promise, + fn: (t: MysqlTransaction) => Promise, options?: MysqlTransactionOptions, ): Promise { const transaction = await this.beginTransaction( diff --git a/lib/packets/parsers/result.ts b/lib/packets/parsers/result.ts index 2b889e1..8dda4af 100644 --- a/lib/packets/parsers/result.ts +++ b/lib/packets/parsers/result.ts @@ -1,6 +1,6 @@ import type { BufferReader } from "../../utils/buffer.ts"; import { MysqlDataType } from "../../constant/mysql_types.ts"; -import type { ArrayRow, Row, SqlxQueryOptions } from "@halvardm/sqlx"; +import type { ArrayRow, Row, SqlQueryOptions } from "@stdext/sql"; export type MysqlParameterType = | null @@ -72,7 +72,7 @@ export function parseField(reader: BufferReader): FieldInfo { export function parseRowArray( reader: BufferReader, fields: FieldInfo[], - options?: SqlxQueryOptions, + options?: SqlQueryOptions, ): ArrayRow { const row: MysqlParameterType[] = []; for (const field of fields) { @@ -89,16 +89,16 @@ export function parseRowArray( export function parseRowObject( reader: BufferReader, fields: FieldInfo[], -): Row { +): Row { const rowArray = parseRowArray(reader, fields); return getRowObject(fields, rowArray); } export function getRowObject( fields: FieldInfo[], - row: ArrayRow, -): Row { - const obj: Row = {}; + row: ArrayRow, +): Row { + const obj: Row = {}; for (const [i, field] of fields.entries()) { const name = field.name; obj[name] = row[i]; @@ -112,7 +112,7 @@ export function getRowObject( function convertType( field: FieldInfo, val: string, - options?: SqlxQueryOptions, + options?: SqlQueryOptions, ): MysqlParameterType { if (options?.transformOutput) { // deno-lint-ignore no-explicit-any diff --git a/lib/pool.test.ts b/lib/pool.test.ts index 0309a62..0e65083 100644 --- a/lib/pool.test.ts +++ b/lib/pool.test.ts @@ -1,49 +1,42 @@ import { MysqlClientPool } from "./pool.ts"; -import { QUERIES, services } from "./utils/testing.ts"; -import { clientPoolTest } from "@halvardm/sqlx/testing"; +import { services, testTransactionable } from "./utils/testing.ts"; +import { + testClientPoolConnection, + testSqlClientPool, +} from "@stdext/sql/testing"; Deno.test("Pool Test", async (t) => { for (const service of services) { await t.step(`Testing ${service.name}`, async (t) => { + testSqlClientPool(new MysqlClientPool(service.url), { + connectionUrl: service.url, + options: {}, + }); + + async function connectionTest(t: Deno.TestContext, url: string) { + await testClientPoolConnection(t, MysqlClientPool, [url, {}]); + await using clientPool = new MysqlClientPool(url); + await clientPool.connect(); + await using client = await clientPool.acquire(); + await client.execute("DROP TABLE IF EXISTS sqltesttable"); + await client.execute( + "CREATE TABLE IF NOT EXISTS sqltesttable (testcol TEXT)", + ); + try { + await testTransactionable(client); + } finally { + await client.execute("DROP TABLE IF EXISTS sqltesttable"); + } + } + await t.step(`TCP`, async (t) => { - await clientPoolTest({ - t, - Client: MysqlClientPool, - connectionUrl: service.url, - connectionOptions: {}, - queries: QUERIES, - }); + await connectionTest(t, service.url); }); // Enable once socket connection issue is fixed // // await t.step(`UNIX Socket`, async (t) => { - // await implementationTest({ - // t, - // Client: MysqlClient, - // // deno-lint-ignore no-explicit-any - // PoolClient: MysqlClientPool as any, - // connectionUrl: service.urlSocket, - // connectionOptions: {}, - // queries: { - // createTable: - // "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", - // dropTable: "DROP TABLE IF EXISTS sqlxtesttable", - // insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", - // insertManyToTable: - // "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", - // selectOneFromTable: - // "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", - // selectByMatchFromTable: - // "SELECT * FROM sqlxtesttable WHERE testcol = ?", - // selectManyFromTable: "SELECT * FROM sqlxtesttable", - // select1AsString: "SELECT '1' as result", - // select1Plus1AsNumber: "SELECT 1+1 as result", - // deleteByMatchFromTable: - // "DELETE FROM sqlxtesttable WHERE testcol = ?", - // deleteAllFromTable: "DELETE FROM sqlxtesttable", - // }, - // }); + // await connectionTest(t,service.urlSocket) // }); }); } diff --git a/lib/pool.ts b/lib/pool.ts index 0228ccf..0fef674 100644 --- a/lib/pool.ts +++ b/lib/pool.ts @@ -1,73 +1,86 @@ +import type { + SqlClientPool, + SqlClientPoolOptions, + SqlPoolClient, + SqlPoolClientOptions, +} from "@stdext/sql"; +import { DeferredStack } from "@stdext/collections"; import { - SqlxBase, - type SqlxClientPool, - type SqlxClientPoolOptions, - SqlxDeferredStack, - SqlxError, - type SqlxPoolClient, -} from "@halvardm/sqlx"; -import { - type MysqlPrepared, + type MysqlPreparedStatement, type MysqlQueryOptions, - type MySqlTransaction, + type MysqlTransaction, MysqlTransactionable, type MysqlTransactionOptions, -} from "./sqlx.ts"; +} from "./core.ts"; import { MysqlConnection, type MysqlConnectionOptions } from "./connection.ts"; import type { MysqlParameterType } from "./packets/parsers/result.ts"; import { - MysqlPoolAcquireEvent, - MysqlPoolCloseEvent, - MysqlPoolConnectEvent, - MysqlPoolReleaseEvent, + MysqlAcquireEvent, + MysqlCloseEvent, + MysqlConnectEvent, + MysqlPoolClientEventTarget, + MysqlReleaseEvent, } from "./utils/events.ts"; -import { MysqlClientEventTarget } from "./utils/events.ts"; - -export interface MysqlClientPoolOptions - extends MysqlConnectionOptions, SqlxClientPoolOptions { -} export class MysqlPoolClient extends MysqlTransactionable implements - SqlxPoolClient< + SqlPoolClient< MysqlConnectionOptions, MysqlConnection, MysqlParameterType, MysqlQueryOptions, - MysqlPrepared, + MysqlPreparedStatement, MysqlTransactionOptions, - MySqlTransaction + MysqlTransaction > { - /** - * Must be set by the client pool on creation - * @inheritdoc - */ - release(): Promise { - throw new Error("Method not implemented."); + declare readonly options: + & MysqlConnectionOptions + & MysqlQueryOptions + & SqlPoolClientOptions; + #releaseFn?: () => Promise; + + #disposed: boolean = false; + get disposed(): boolean { + return this.#disposed; + } + constructor( + connection: MysqlPoolClient["connection"], + options: MysqlPoolClient["options"] = {}, + ) { + super(connection, options); + if (this.options?.releaseFn) { + this.#releaseFn = this.options.releaseFn; + } + } + async release() { + this.#disposed = true; + await this.#releaseFn?.(); } - async [Symbol.asyncDispose](): Promise { - await this.release(); + [Symbol.asyncDispose](): Promise { + return this.release(); } } -export class MysqlClientPool extends SqlxBase implements - SqlxClientPool< +export class MysqlClientPool implements + SqlClientPool< MysqlConnectionOptions, - MysqlConnection, MysqlParameterType, MysqlQueryOptions, - MysqlPrepared, + MysqlConnection, + MysqlPreparedStatement, MysqlTransactionOptions, - MySqlTransaction, + MysqlTransaction, MysqlPoolClient, - SqlxDeferredStack + MysqlPoolClientEventTarget > { + declare readonly options: + & MysqlConnectionOptions + & MysqlQueryOptions + & SqlClientPoolOptions; readonly connectionUrl: string; - readonly connectionOptions: MysqlClientPoolOptions; - readonly eventTarget: EventTarget; - readonly deferredStack: SqlxDeferredStack; - readonly queryOptions: MysqlQueryOptions; + readonly eventTarget: MysqlPoolClientEventTarget; + readonly deferredStack: DeferredStack; #connected: boolean = false; @@ -77,38 +90,34 @@ export class MysqlClientPool extends SqlxBase implements constructor( connectionUrl: string | URL, - connectionOptions: MysqlClientPoolOptions = {}, + options: MysqlClientPool["options"] = {}, ) { - super(); this.connectionUrl = connectionUrl.toString(); - this.connectionOptions = connectionOptions; - this.queryOptions = connectionOptions; - this.eventTarget = new MysqlClientEventTarget(); - this.deferredStack = new SqlxDeferredStack( - connectionOptions, - ); + this.options = options; + this.eventTarget = new MysqlPoolClientEventTarget(); + this.deferredStack = new DeferredStack({ + ...options, + removeFn: async (element) => { + await element._value.close(); + }, + }); } async connect(): Promise { for (let i = 0; i < this.deferredStack.maxSize; i++) { const conn = new MysqlConnection( this.connectionUrl, - this.connectionOptions, - ); - const client = new MysqlPoolClient( - conn, - this.queryOptions, + this.options, ); - client.release = () => this.release(client); - if (!this.connectionOptions.lazyInitialization) { - await client.connection.connect(); + if (!this.options.lazyInitialization) { + await conn.connect(); this.eventTarget.dispatchEvent( - new MysqlPoolConnectEvent({ connectable: client }), + new MysqlConnectEvent({ connection: conn }), ); } - this.deferredStack.push(client); + this.deferredStack.add(conn); } this.#connected = true; @@ -117,40 +126,39 @@ export class MysqlClientPool extends SqlxBase implements async close(): Promise { this.#connected = false; - for (const client of this.deferredStack.elements) { + for (const el of this.deferredStack.elements) { this.eventTarget.dispatchEvent( - new MysqlPoolCloseEvent({ connectable: client }), + new MysqlCloseEvent({ connection: el._value }), ); - await client.connection.close(); + await el.remove(); } } async acquire(): Promise { - const client = await this.deferredStack.pop(); - if (!client.connected) { - await client.connection.connect(); + const el = await this.deferredStack.pop(); + + if (!el.value.connected) { + await el.value.connect(); } this.eventTarget.dispatchEvent( - new MysqlPoolAcquireEvent({ connectable: client }), + new MysqlAcquireEvent({ connection: el.value }), ); - return client; - } - async release(client: MysqlPoolClient): Promise { - this.eventTarget.dispatchEvent( - new MysqlPoolReleaseEvent({ connectable: client }), + const c = new MysqlPoolClient( + el.value, + { + ...this.options, + releaseFn: async () => { + this.eventTarget.dispatchEvent( + new MysqlReleaseEvent({ connection: el._value }), + ); + await el.release(); + }, + }, ); - try { - this.deferredStack.push(client); - } catch (e) { - if (e instanceof SqlxError && e.message === "Max pool size reached") { - await client.connection.close(); - throw e; - } else { - throw e; - } - } + + return c; } async [Symbol.asyncDispose](): Promise { diff --git a/lib/utils/errors.ts b/lib/utils/errors.ts index be73e89..56ad876 100644 --- a/lib/utils/errors.ts +++ b/lib/utils/errors.ts @@ -1,6 +1,6 @@ -import { isSqlxError, SqlxError } from "@halvardm/sqlx"; +import { isSqlError, SqlError } from "@stdext/sql"; -export class MysqlError extends SqlxError { +export class MysqlError extends SqlError { constructor(msg: string) { super(msg); } @@ -46,5 +46,5 @@ export class MysqlTransactionError extends MysqlError { * Check if an error is a MysqlError */ export function isMysqlError(err: unknown): err is MysqlError { - return isSqlxError(err) && err instanceof MysqlError; + return isSqlError(err) && err instanceof MysqlError; } diff --git a/lib/utils/events.test.ts b/lib/utils/events.test.ts new file mode 100644 index 0000000..4efae8d --- /dev/null +++ b/lib/utils/events.test.ts @@ -0,0 +1,10 @@ +import { testSqlEventTarget } from "@stdext/sql/testing"; +import { + MysqlClientEventTarget, + MysqlPoolClientEventTarget, +} from "./events.ts"; + +Deno.test(`events`, () => { + testSqlEventTarget(new MysqlClientEventTarget()); + testSqlEventTarget(new MysqlPoolClientEventTarget()); +}); diff --git a/lib/utils/events.ts b/lib/utils/events.ts index ba89720..9dd027d 100644 --- a/lib/utils/events.ts +++ b/lib/utils/events.ts @@ -1,68 +1,53 @@ import { - type SqlxClientEventType, - SqlxConnectableCloseEvent, - SqlxConnectableConnectEvent, - type SqlxConnectableEventInit, - SqlxEventTarget, - SqlxPoolConnectableAcquireEvent, - SqlxPoolConnectableReleaseEvent, - type SqlxPoolConnectionEventType, -} from "@halvardm/sqlx"; -import type { MysqlConnectionOptions } from "../connection.ts"; -import type { MysqlConnection } from "../connection.ts"; -import type { MysqlClient } from "../client.ts"; -import type { MysqlPoolClient } from "../pool.ts"; - -export class MysqlClientEventTarget extends SqlxEventTarget< + SqlAcquireEvent, + type SqlClientEventType, + SqlCloseEvent, + SqlConnectEvent, + type SqlConnectionEventInit, + SqlEventTarget, + type SqlPoolConnectionEventType, + SqlReleaseEvent, +} from "@stdext/sql"; +import type { MysqlConnection, MysqlConnectionOptions } from "../connection.ts"; + +export class MysqlClientEventTarget extends SqlEventTarget< MysqlConnectionOptions, MysqlConnection, - SqlxClientEventType, - MysqlClientEventInit, + SqlClientEventType, + MysqlConnectionEventInit, MysqlClientEvents > { } -export class MysqlPoolClientEventTarget extends SqlxEventTarget< +export class MysqlPoolClientEventTarget extends SqlEventTarget< MysqlConnectionOptions, MysqlConnection, - SqlxPoolConnectionEventType, - MysqlPoolEventInit, + SqlPoolConnectionEventType, + MysqlConnectionEventInit, MysqlPoolEvents > { } -export type MysqlClientEventInit = SqlxConnectableEventInit< - MysqlClient ->; - -export type MysqlPoolEventInit = SqlxConnectableEventInit< - MysqlPoolClient +export type MysqlConnectionEventInit = SqlConnectionEventInit< + MysqlConnection >; -export class MysqlClientConnectEvent - extends SqlxConnectableConnectEvent {} +export class MysqlConnectEvent + extends SqlConnectEvent {} -export class MysqlClientCloseEvent - extends SqlxConnectableCloseEvent {} -export class MysqlPoolConnectEvent - extends SqlxConnectableConnectEvent {} +export class MysqlCloseEvent extends SqlCloseEvent {} -export class MysqlPoolCloseEvent - extends SqlxConnectableCloseEvent {} - -export class MysqlPoolAcquireEvent - extends SqlxPoolConnectableAcquireEvent { -} +export class MysqlAcquireEvent + extends SqlAcquireEvent {} -export class MysqlPoolReleaseEvent - extends SqlxPoolConnectableReleaseEvent { +export class MysqlReleaseEvent + extends SqlReleaseEvent { } export type MysqlClientEvents = - | MysqlClientConnectEvent - | MysqlClientCloseEvent; + | MysqlConnectEvent + | MysqlCloseEvent; export type MysqlPoolEvents = - | MysqlClientConnectEvent - | MysqlClientCloseEvent - | MysqlPoolAcquireEvent - | MysqlPoolReleaseEvent; + | MysqlClientEvents + | MysqlAcquireEvent + | MysqlReleaseEvent; diff --git a/lib/utils/testing.ts b/lib/utils/testing.ts index 013ceaf..406d80d 100644 --- a/lib/utils/testing.ts +++ b/lib/utils/testing.ts @@ -2,7 +2,19 @@ import { resolve } from "@std/path"; import { ConsoleHandler, setup } from "@std/log"; import { MODULE_NAME } from "./meta.ts"; import { parse } from "@std/yaml"; -import type { BaseQueriableTestOptions } from "@halvardm/sqlx/testing"; +import type { + MysqlPreparable, + MysqlPreparedStatement, + MysqlQueriable, + MysqlTransaction, + MysqlTransactionable, +} from "../core.ts"; +import { assertEquals } from "@std/assert"; +import { + isSqlPreparable, + isSqlTransaction, + isSqlTransactionable, +} from "@stdext/sql"; type DockerCompose = { services: { @@ -72,16 +84,167 @@ export const services: ServiceParsed[] = Object.entries(composeParsed.services) export const URL_TEST_CONNECTION = services.find((s) => s.name === "mysql") ?.url as string; -export const QUERIES: BaseQueriableTestOptions["queries"] = { - createTable: "CREATE TABLE IF NOT EXISTS sqlxtesttable (testcol TEXT)", - dropTable: "DROP TABLE IF EXISTS sqlxtesttable", - insertOneToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?)", - insertManyToTable: "INSERT INTO sqlxtesttable (testcol) VALUES (?),(?),(?)", - selectOneFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ? LIMIT 1", - selectByMatchFromTable: "SELECT * FROM sqlxtesttable WHERE testcol = ?", - selectManyFromTable: "SELECT * FROM sqlxtesttable", - select1AsString: "SELECT '1' as result", - select1Plus1AsNumber: "SELECT 1+1 as result", - deleteByMatchFromTable: "DELETE FROM sqlxtesttable WHERE testcol = ?", - deleteAllFromTable: "DELETE FROM sqlxtesttable", -}; +export async function testQueriable( + queriable: MysqlQueriable, +) { + await queriable.execute("DELETE FROM sqltesttable"); + + const resultExecute = await queriable.execute( + "INSERT INTO sqltesttable (testcol) VALUES (?),(?),(?)", + ["queriable 1", "queriable 2", "queriable 3"], + ); + assertEquals(resultExecute, 3); + + const resultQuery = await queriable.query("SELECT * FROM sqltesttable"); + assertEquals(resultQuery, [ + { testcol: "queriable 1" }, + { testcol: "queriable 2" }, + { testcol: "queriable 3" }, + ]); + + const resultQueryOne = await queriable.queryOne( + "SELECT * FROM sqltesttable WHERE testcol LIKE ?", + ["queriable%"], + ); + assertEquals(resultQueryOne, { testcol: "queriable 1" }); + + const resultQueryMany = await Array.fromAsync( + queriable.queryMany("SELECT * FROM sqltesttable WHERE testcol LIKE ?", [ + "queriable%", + ]), + ); + assertEquals(resultQueryMany, [ + { testcol: "queriable 1" }, + { testcol: "queriable 2" }, + { testcol: "queriable 3" }, + ]); + + const resultQueryArray = await queriable.queryArray( + "SELECT * FROM sqltesttable WHERE testcol LIKE ?", + ["queriable%"], + ); + assertEquals(resultQueryArray, [ + ["queriable 1"], + ["queriable 2"], + ["queriable 3"], + ]); + + const resultQueryOneArray = await queriable.queryOneArray( + "SELECT * FROM sqltesttable WHERE testcol LIKE ?", + ["queriable%"], + ); + assertEquals(resultQueryOneArray, ["queriable 1"]); + + const resultQueryManyArray = await Array.fromAsync( + queriable.queryManyArray( + "SELECT * FROM sqltesttable WHERE testcol LIKE ?", + ["queriable%"], + ), + ); + assertEquals(resultQueryManyArray, [ + ["queriable 1"], + ["queriable 2"], + ["queriable 3"], + ]); + + const resultSql = await queriable + .sql`SELECT * FROM sqltesttable WHERE testcol LIKE ${"queriable%"}`; + assertEquals(resultSql, [ + { testcol: "queriable 1" }, + { testcol: "queriable 2" }, + { testcol: "queriable 3" }, + ]); + + const resultSqlArray = await queriable + .sqlArray`SELECT * FROM sqltesttable WHERE testcol LIKE ${"queriable%"}`; + assertEquals(resultSqlArray, [ + ["queriable 1"], + ["queriable 2"], + ["queriable 3"], + ]); +} + +export async function testPreparedStatement( + preparedStatement: MysqlPreparedStatement, +) { + const resultExecute = await preparedStatement.execute(["queriable%"]); + assertEquals(resultExecute, undefined); + + const resultQuery = await preparedStatement.query(["queriable%"]); + assertEquals(resultQuery, [ + { testcol: "queriable 1" }, + { testcol: "queriable 2" }, + { testcol: "queriable 3" }, + ]); + + const resultQueryOne = await preparedStatement.queryOne(["queriable%"]); + assertEquals(resultQueryOne, { testcol: "queriable 1" }); + + const resultQueryMany = await Array.fromAsync( + preparedStatement.queryMany(["queriable%"]), + ); + assertEquals(resultQueryMany, [ + { testcol: "queriable 1" }, + { testcol: "queriable 2" }, + { testcol: "queriable 3" }, + ]); + + const resultQueryArray = await preparedStatement.queryArray(["queriable%"]); + assertEquals(resultQueryArray, [ + ["queriable 1"], + ["queriable 2"], + ["queriable 3"], + ]); + + const resultQueryOneArray = await preparedStatement.queryOneArray([ + "queriable%", + ]); + assertEquals(resultQueryOneArray, ["queriable 1"]); + + const resultQueryManyArray = await Array.fromAsync( + preparedStatement.queryManyArray(["queriable%"]), + ); + assertEquals(resultQueryManyArray, [ + ["queriable 1"], + ["queriable 2"], + ["queriable 3"], + ]); +} + +export async function testPreparable( + preparable: MysqlPreparable, +) { + // Testing properties + isSqlPreparable(preparable); + + // Testing inherited classes + await testQueriable(preparable); + + // Testing methods + const prepared = preparable.prepare( + "SELECT * FROM sqltesttable WHERE testcol LIKE ?", + ); + await testPreparedStatement(prepared); +} +export async function testTransaction( + transaction: MysqlTransaction, +) { + // Testing properties + isSqlTransaction(transaction); + + // Testing inherited classes + await testPreparable(transaction); +} +export async function testTransactionable( + transactionable: MysqlTransactionable, +) { + // Testing properties + isSqlTransactionable(transactionable); + + // Testing inherited classes + await testPreparable(transactionable); + + // Testing methods + const transaction = await transactionable.beginTransaction(); + await testTransaction(transaction); +} From 13199dc0ff6f2be2bbd2cc7ba83eea1cfad828ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halvard=20M=C3=B8rstad?= Date: Wed, 26 Jun 2024 17:24:26 +0200 Subject: [PATCH 38/38] Updated readme --- README.md | 263 ++++++++++++++++++++++++++++++++++++------------------ deno.json | 2 +- 2 files changed, 177 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 6c718ac..035eafa 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -# deno_mysql +# @db/mysql -[![Build Status](https://github.com/manyuanrong/deno_mysql/workflows/ci/badge.svg?branch=master)](https://github.com/manyuanrong/deno_mysql/actions) -![GitHub](https://img.shields.io/github/license/manyuanrong/deno_mysql.svg) -![GitHub release](https://img.shields.io/github/release/manyuanrong/deno_mysql.svg) -![(Deno)](https://img.shields.io/badge/deno-1.0.0-green.svg) +[![Build Status](https://github.com/denodrivers/mysql/actions/workflows/ci.yml/badge.svg)](https://github.com/denodrivers/mysql/actions/workflows/ci.yml) +[![JSR](https://jsr.io/badges/@db/mysql)](https://jsr.io/@db/mysql) +[![JSR Score](https://jsr.io/badges/@db/mysql/score)](https://jsr.io/@db/mysql) MySQL and MariaDB database driver for Deno. @@ -12,45 +11,55 @@ On this basis, there is also an ORM library: 欢迎国内的小伙伴加我专门建的 Deno QQ 交流群:698469316 -## API +## Installation -### connect +This package is published on [JSR](https://jsr.io/@db/mysql) -```ts -import { Client } from "https://deno.land/x/mysql/mod.ts"; -const client = await new Client().connect({ - hostname: "127.0.0.1", - username: "root", - db: "dbname", - password: "password", -}); +``` +deno add @db/mysql ``` -### connect pool +## Usage -Create client with connection pool. +See [Deno Standard Library Extended SQL](https://jsr.io/@stdext/sql) for general +API interfaces and examples -pool size is auto increment from 0 to `poolSize` +### Client ```ts -import { Client } from "https://deno.land/x/mysql/mod.ts"; -const client = await new Client().connect({ - hostname: "127.0.0.1", - username: "root", - db: "dbname", - poolSize: 3, // connection limit - password: "password", -}); +import { MysqlClient } from "jsr:@db/mysql@3.0.0"; +await using client = new MysqlClient( + "mysql://root:password@0.0.0.0:3306/dbname", +); +await client.connect(); +await client.execute("CREATE TABLE test (testcol TEXT)"); +await client.query("SELECT * FROM test"); +``` + +### Client Pool + +```ts +import { MysqlClientPool } from "jsr:@db/mysql@3.0.0"; +await using clientPool = new MysqlClientPool( + "mysql://root:password@0.0.0.0:3306/dbname", + { maxSize: 3 }, +); +await clientPool.connect(); +const client = await clientPool.aquire(); +await client.query("SELECT * FROM test"); +clientPool.release(); ``` -### create database +### Queries + +#### create database ```ts await client.execute(`CREATE DATABASE IF NOT EXISTS enok`); await client.execute(`USE enok`); ``` -### create table +#### create table ```ts await client.execute(`DROP TABLE IF EXISTS users`); @@ -64,30 +73,30 @@ await client.execute(` `); ``` -### insert +#### insert ```ts let result = await client.execute(`INSERT INTO users(name) values(?)`, [ "manyuanrong", ]); console.log(result); -// { affectedRows: 1, lastInsertId: 1 } +// 1 ``` -### update +#### update ```ts let result = await client.execute(`update users set ?? = ?`, ["name", "MYR"]); console.log(result); -// { affectedRows: 1, lastInsertId: 0 } +// 1 ``` -### delete +#### delete ```ts let result = await client.execute(`delete from users where ?? = ?`, ["id", 1]); console.log(result); -// { affectedRows: 1, lastInsertId: 0 } +// 1 ``` ### query @@ -100,39 +109,9 @@ const queryWithParams = await client.query( ["id", "users", 1], ); console.log(users, queryWithParams); +// [{ id: 1, name: "enok" }] ``` -### execute - -There are two ways to execute an SQL statement. - -First and default one will return you an `rows` key containing an array of rows: - -```ts -const { rows: users } = await client.execute(`select * from users`); -console.log(users); -``` - -The second one will return you an `iterator` key containing an -`[Symbol.asyncIterator]` property: - -```ts -await client.useConnection(async (conn) => { - // note the third parameter of execute() method. - const { iterator: users } = await conn.execute( - `select * from users`, - /* params: */ [], - /* iterator: */ true, - ); - for await (const user of users) { - console.log(user); - } -}); -``` - -The second method is recommended only for SELECT queries that might contain many -results (e.g. 100k rows). - ### transaction ```ts @@ -157,47 +136,157 @@ You usually need not specify the caCert, unless the certificate is not included in the default root certificates. ```ts -import { Client, TLSConfig, TLSMode } from "https://deno.land/x/mysql/mod.ts"; -const tlsConfig: TLSConfig = { - mode: TLSMode.VERIFY_IDENTITY, - caCerts: [ - await Deno.readTextFile("capath"), - ], -}; +const client = new MysqlClient("mysql://root:password@0.0.0.0:3306/dbname", { + tls: { + mode: TLSMode.VERIFY_IDENTITY, + caCerts: [await Deno.readTextFile("capath")], + }, +}); + +await client.connect(); +``` + +### close + +If async dispose is not used, you have to manually close the connection at the +end of your script. + +```ts +// Async dispose +await using client = new MysqlClient( + "mysql://root:password@0.0.0.0:3306/dbname", +); +await client.connect(); +// no need to close the client + +// Normal creation of class +const client = new MysqlClient("mysql://root:password@0.0.0.0:3306/dbname"); +await client.connect(); +// manual closing of connection needed. +await client.close(); +``` + +## Logging + +Logging is set up using [std/log](https://jsr.io/@std/log) + +To change logging, add this in the entrypoint of your script: + +```ts +import { ConsoleHandler, setup } from "@std/log"; +import { MODULE_NAME } from "jsr:@db/mysql@3.0.0"; + +setup({ + handlers: { + console: new ConsoleHandler("DEBUG"), + }, + loggers: { + // configure default logger available via short-hand methods above + default: { + level: "WARN", + handlers: ["console"], + }, + [MODULE_NAME]: { + level: "WARN", + handlers: ["console"], + }, + }, +}); +``` + +## Test + +To run the tests, Docker and Docker Compose is required. + +Run using + +``` +deno task test +``` + +## Upgrade from v2 to v3 + +From version `3` onwards, this package will only be published to +[JSR](https://jsr.io/@db/mysql). Version `3` will also be adapted to use the +standard interfaces from [stdext/sql](https://jsr.io/@stdext/sql), thus this is +a breaking change where you will have to adjust your usage accordingly. + +### Client + +V2: + +```ts +import { Client } from "https://deno.land/x/mysql/mod.ts"; const client = await new Client().connect({ hostname: "127.0.0.1", username: "root", db: "dbname", password: "password", - tls: tlsConfig, }); ``` -### close +V3: ```ts -await client.close(); +import { MysqlClient } from "jsr:@db/mysql@3.0.0"; +await using client = new MysqlClient( + "mysql://root:password@0.0.0.0:3306/dbname", +); +await client.connect(); ``` -## Logging +### ClientPool -The driver logs to the console by default. +V2: -To disable logging: +```ts +import { Client } from "https://deno.land/x/mysql/mod.ts"; +const client = await new Client().connect({ + hostname: "127.0.0.1", + username: "root", + db: "dbname", + poolSize: 3, // connection limit + password: "password", +}); +await client.query("SELECT * FROM test"); +``` + +V3: ```ts -import { configLogger } from "https://deno.land/x/mysql/mod.ts"; -await configLogger({ enable: false }); +import { MysqlClientPool } from "jsr:@db/mysql@3.0.0"; +await using clientPool = new MysqlClientPool( + "mysql://root:password@0.0.0.0:3306/dbname", + { maxSize: 3 }, +); +await clientPool.connect(); +const client = await clientPool.aquire(); +await client.query("SELECT * FROM test"); +clientPool.release(); ``` -## Test +### Iterators -The tests require a database to run against. +V2: -```bash -docker container run --rm -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true docker.io/mariadb:latest -deno test --allow-env --allow-net=127.0.0.1:3306 ./test.ts +```ts +await client.useConnection(async (conn) => { + // note the third parameter of execute() method. + const { iterator: users } = await conn.execute( + `select * from users`, + /* params: */ [], + /* iterator: */ true, + ); + for await (const user of users) { + console.log(user); + } +}); ``` -Use different docker images to test against different versions of MySQL and -MariaDB. Please see [ci.yml](./.github/workflows/ci.yml) for examples. +V3: + +```ts +for await (const user of client.queryMany("SELECT * FROM users")) { + console.log(user); +} +``` diff --git a/deno.json b/deno.json index cd94249..6c86fa6 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@db/mysql", - "version": "2.12.2", + "version": "3.0.0-rc.1", "exports": "./mod.ts", "lock": false, "tasks": {