@deterministic-code/datasources-mysql 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MysqlBulkUpdateStatementTextRenderer.d.ts +4 -0
- package/dist/MysqlCreateTableStatementRenderer.d.ts +5 -0
- package/dist/MysqlDriver.d.ts +12 -0
- package/dist/MysqlStatementRenderer.d.ts +7 -0
- package/dist/bind.d.ts +5 -0
- package/dist/columnTypes.d.ts +1 -0
- package/dist/command.d.ts +16 -0
- package/dist/connection-options.d.ts +5 -0
- package/dist/connection.d.ts +19 -0
- package/dist/converters/BinaryConverter.d.ts +5 -0
- package/dist/converters/BoolConverter.d.ts +5 -0
- package/dist/converters/DateTimeConverter.d.ts +5 -0
- package/dist/converters/DecimalConverter.d.ts +5 -0
- package/dist/converters/MysqlRowTypeConverters.d.ts +3 -0
- package/dist/converters/UuidConverter.d.ts +5 -0
- package/dist/i-mysql-engine.d.ts +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/scalar.d.ts +2 -0
- package/dist/transaction.d.ts +9 -0
- package/package.json +23 -4
- package/src/MysqlBulkUpdateStatementTextRenderer.ts +10 -0
- package/src/MysqlCreateTableStatementRenderer.ts +12 -0
- package/src/MysqlDriver.ts +54 -0
- package/src/MysqlStatementRenderer.test.ts +64 -0
- package/src/MysqlStatementRenderer.ts +34 -0
- package/src/bind.ts +31 -0
- package/src/columnTypes.ts +10 -0
- package/src/command.ts +197 -0
- package/src/connection-options.ts +6 -0
- package/src/connection.ts +91 -0
- package/src/converters/BinaryConverter.test.ts +12 -0
- package/src/converters/BinaryConverter.ts +9 -0
- package/src/converters/BoolConverter.test.ts +14 -0
- package/src/converters/BoolConverter.ts +6 -0
- package/src/converters/DateTimeConverter.test.ts +14 -0
- package/src/converters/DateTimeConverter.ts +15 -0
- package/src/converters/DecimalConverter.test.ts +11 -0
- package/src/converters/DecimalConverter.ts +7 -0
- package/src/converters/MysqlRowTypeConverters.test.ts +19 -0
- package/src/converters/MysqlRowTypeConverters.ts +25 -0
- package/src/converters/UuidConverter.test.ts +11 -0
- package/src/converters/UuidConverter.ts +6 -0
- package/src/i-mysql-engine.ts +58 -0
- package/src/index.ts +1 -0
- package/src/mysql.test.ts +342 -0
- package/src/scalar.ts +2 -0
- package/src/transaction.ts +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CreateTableStatementTextRenderer, type Column } from "@datasources/core";
|
|
2
|
+
export declare class MysqlCreateTableStatementRenderer extends CreateTableStatementTextRenderer {
|
|
3
|
+
protected typeName(column: Column): string;
|
|
4
|
+
protected identity(column: Column): string;
|
|
5
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StatementTextRenderer, type Cell, type DatasourceOptions, type ICommand, type IConnection, type IDriver, type Information, type ITransaction, type IsolationLevel, type TypeConverter } from "@datasources/core";
|
|
2
|
+
import { columnTypes } from "./columnTypes.js";
|
|
3
|
+
export { columnTypes };
|
|
4
|
+
export declare class MysqlDriver implements IDriver {
|
|
5
|
+
readonly statementRenderer: StatementTextRenderer;
|
|
6
|
+
readonly information: Information;
|
|
7
|
+
rowTypeConverters: Record<string, TypeConverter<Cell, Cell>>;
|
|
8
|
+
columnType: (name: string) => string;
|
|
9
|
+
connection: (options?: DatasourceOptions) => IConnection;
|
|
10
|
+
command: (connection: IConnection) => ICommand;
|
|
11
|
+
transaction: (connection: IConnection, isolation?: IsolationLevel) => ITransaction;
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BulkUpdateStatement, CreateTableStatement, SelectInsertedIdStatement, StatementRenderer, type StatementTextRenderer } from "@datasources/core";
|
|
2
|
+
export declare class MysqlStatementRenderer extends StatementRenderer {
|
|
3
|
+
constructor(text: StatementTextRenderer);
|
|
4
|
+
protected visitSelectInsertedId(inserted: SelectInsertedIdStatement): string;
|
|
5
|
+
protected visitCreateTable(create: CreateTableStatement): string;
|
|
6
|
+
protected visitBulkUpdate(update: BulkUpdateStatement): string;
|
|
7
|
+
}
|
package/dist/bind.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Parameter } from "@datasources/core";
|
|
2
|
+
import type { Scalar } from "./scalar.js";
|
|
3
|
+
export declare const withoutReturning: (sql: string) => string;
|
|
4
|
+
export declare const toMysql: (sql: string) => string;
|
|
5
|
+
export declare const bindParameters: (parameters?: Parameter[] | null) => Record<string, Scalar>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const columnTypes: Record<string, string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CommandMiddlewareHost, Native, StatementTextRenderer, Response, type Cells, type ICommand, type IReader, type IStatement, type Parameter } from "@datasources/core";
|
|
2
|
+
import type { MysqlConnection } from "./connection.js";
|
|
3
|
+
export declare class MysqlCommand extends CommandMiddlewareHost implements ICommand {
|
|
4
|
+
readonly connection: MysqlConnection;
|
|
5
|
+
private readonly statements;
|
|
6
|
+
constructor(connection: MysqlConnection, statements?: StatementTextRenderer);
|
|
7
|
+
executeReaderNative(text: Native, parameters?: Parameter[]): Promise<IReader<Cells>>;
|
|
8
|
+
executeReaderStatement(statement: IStatement, parameters?: Parameter[]): Promise<IReader<Cells>>;
|
|
9
|
+
executeStatement(statement: IStatement, parameters?: Parameter[]): Promise<Response>;
|
|
10
|
+
private firstCell;
|
|
11
|
+
executeNative(text: Native, parameters?: Parameter[]): Promise<Response>;
|
|
12
|
+
private executeProcedure;
|
|
13
|
+
private runSql;
|
|
14
|
+
private query;
|
|
15
|
+
private callResults;
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DatasourceOptions, type ICommand, type IConnection, type IsolationLevelName, type ITransaction } from "@datasources/core";
|
|
2
|
+
import type { MysqlConnectionOptions } from "./connection-options.js";
|
|
3
|
+
import { type IMysqlClient } from "./i-mysql-engine.js";
|
|
4
|
+
export declare class MysqlConnection implements IConnection {
|
|
5
|
+
readonly provider = "mysql";
|
|
6
|
+
url: string;
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
private client?;
|
|
9
|
+
private readonly connect?;
|
|
10
|
+
static from: (options?: DatasourceOptions) => MysqlConnection;
|
|
11
|
+
constructor(options?: MysqlConnectionOptions);
|
|
12
|
+
createCommand: () => ICommand;
|
|
13
|
+
open(): Promise<void>;
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
16
|
+
startTransaction(isolation?: IsolationLevelName): Promise<ITransaction>;
|
|
17
|
+
ensureOpen(): void;
|
|
18
|
+
require(): IMysqlClient;
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Parameter } from "@datasources/core";
|
|
2
|
+
import type { Cells, Scalar } from "./scalar.js";
|
|
3
|
+
export interface MysqlExecuteResult {
|
|
4
|
+
rowsAffected: number;
|
|
5
|
+
result?: Scalar;
|
|
6
|
+
parameters?: Parameter[];
|
|
7
|
+
}
|
|
8
|
+
export interface MysqlQueryHeader {
|
|
9
|
+
affectedRows?: number;
|
|
10
|
+
insertId?: Scalar;
|
|
11
|
+
}
|
|
12
|
+
export interface IMysqlClient {
|
|
13
|
+
query(sql: string, params?: Record<string, Scalar>): Promise<[MysqlQueryHeader | Cells[]]>;
|
|
14
|
+
queryRows(sql: string, params?: Record<string, Scalar>): AsyncIterable<Cells>;
|
|
15
|
+
beginTransaction(): Promise<void>;
|
|
16
|
+
commit(): Promise<void>;
|
|
17
|
+
rollback(): Promise<void>;
|
|
18
|
+
end(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export type MysqlCreateConnection = (url: string) => Promise<IMysqlClient>;
|
|
21
|
+
export type Mysql2Module = {
|
|
22
|
+
createConnection: (options: {
|
|
23
|
+
uri: string;
|
|
24
|
+
namedPlaceholders: boolean;
|
|
25
|
+
}) => Promise<IMysqlClient>;
|
|
26
|
+
};
|
|
27
|
+
export declare const mysql2Connect: (mysql: Mysql2Module) => MysqlCreateConnection;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MysqlDriver } from "./MysqlDriver.js";
|
package/dist/scalar.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ITransaction, type IsolationLevelName } from "@datasources/core";
|
|
2
|
+
import type { MysqlConnection } from "./connection.js";
|
|
3
|
+
export declare class MysqlTransaction implements ITransaction {
|
|
4
|
+
readonly connection: MysqlConnection;
|
|
5
|
+
readonly isolation: IsolationLevelName;
|
|
6
|
+
constructor(connection: MysqlConnection, isolation?: IsolationLevelName);
|
|
7
|
+
commit(): Promise<void>;
|
|
8
|
+
abort(): Promise<void>;
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deterministic-code/datasources-mysql",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"mysql2": "^3.14.0",
|
|
23
|
+
"@deterministic-code/datasources": "^0.1.1"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BulkUpdateStatementTextRenderer, type BulkUpdateStatement } from "@datasources/core";
|
|
2
|
+
|
|
3
|
+
export class MysqlBulkUpdateStatementTextRenderer extends BulkUpdateStatementTextRenderer {
|
|
4
|
+
override render = (statement: BulkUpdateStatement): string => {
|
|
5
|
+
const assigns = statement.set
|
|
6
|
+
.map((column) => `${statement.table}.${column} = data.${column}`)
|
|
7
|
+
.join(", ");
|
|
8
|
+
return `UPDATE ${statement.table} JOIN ${this.source(statement)} ON ${this.on(statement)} SET ${assigns}`;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CreateTableStatementTextRenderer, type Column } from "@datasources/core";
|
|
2
|
+
import { columnTypes } from "./columnTypes.ts";
|
|
3
|
+
|
|
4
|
+
export class MysqlCreateTableStatementRenderer extends CreateTableStatementTextRenderer {
|
|
5
|
+
protected typeName(column: Column): string {
|
|
6
|
+
return columnTypes[column.type] ?? column.type;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
protected identity(column: Column): string {
|
|
10
|
+
return column.identity ? " AUTO_INCREMENT" : "";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DatasourceError,
|
|
3
|
+
StatementTextRenderer,
|
|
4
|
+
type Cell,
|
|
5
|
+
type DatasourceOptions,
|
|
6
|
+
type ICommand,
|
|
7
|
+
type IConnection,
|
|
8
|
+
type IDriver,
|
|
9
|
+
type Information,
|
|
10
|
+
type ITransaction,
|
|
11
|
+
type IsolationLevel,
|
|
12
|
+
type TypeConverter,
|
|
13
|
+
} from "@datasources/core";
|
|
14
|
+
import { MysqlConnection } from "./connection.ts";
|
|
15
|
+
import { MysqlCommand } from "./command.ts";
|
|
16
|
+
import { MysqlTransaction } from "./transaction.ts";
|
|
17
|
+
import { MysqlRowTypeConverters } from "./converters/MysqlRowTypeConverters.ts";
|
|
18
|
+
import { MysqlStatementRenderer } from "./MysqlStatementRenderer.ts";
|
|
19
|
+
import { columnTypes } from "./columnTypes.ts";
|
|
20
|
+
|
|
21
|
+
export { columnTypes };
|
|
22
|
+
|
|
23
|
+
const statements = new StatementTextRenderer(t => new MysqlStatementRenderer(t));
|
|
24
|
+
|
|
25
|
+
export class MysqlDriver implements IDriver {
|
|
26
|
+
readonly statementRenderer = statements;
|
|
27
|
+
readonly information: Information = {
|
|
28
|
+
descriptions: [
|
|
29
|
+
"MySQL / MariaDB server driver.",
|
|
30
|
+
"Full connection, command, transaction, and repository support.",
|
|
31
|
+
],
|
|
32
|
+
overrides: [
|
|
33
|
+
"Overrides SelectInsertedIdStatement to SELECT LAST_INSERT_ID() AS {column}",
|
|
34
|
+
"Overrides CreateTableStatement identity to AUTO_INCREMENT",
|
|
35
|
+
"Overrides CreateTableStatement type mapping (int→INT, uuid→CHAR(36), string/datetime→TEXT, bool→TINYINT, decimal→DECIMAL(18,4), float→DOUBLE, binary→BLOB)",
|
|
36
|
+
],
|
|
37
|
+
changeLogs: [],
|
|
38
|
+
};
|
|
39
|
+
rowTypeConverters: Record<string, TypeConverter<Cell, Cell>> = MysqlRowTypeConverters;
|
|
40
|
+
columnType = (name: string): string =>
|
|
41
|
+
columnTypes[name] ??
|
|
42
|
+
(() => {
|
|
43
|
+
throw new DatasourceError(`Unknown mysql type "${name}"`);
|
|
44
|
+
})();
|
|
45
|
+
connection = (options?: DatasourceOptions): IConnection =>
|
|
46
|
+
MysqlConnection.from(options);
|
|
47
|
+
command = (connection: IConnection): ICommand =>
|
|
48
|
+
new MysqlCommand(connection as MysqlConnection, statements);
|
|
49
|
+
transaction = (
|
|
50
|
+
connection: IConnection,
|
|
51
|
+
isolation: IsolationLevel = "readCommitted",
|
|
52
|
+
): ITransaction =>
|
|
53
|
+
new MysqlTransaction(connection as MysqlConnection, isolation);
|
|
54
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BulkUpdateStatement,
|
|
3
|
+
ColumnDefault,
|
|
4
|
+
CreateTableStatement,
|
|
5
|
+
DefaultStatementTextRenderer,
|
|
6
|
+
ForeignKey,
|
|
7
|
+
SelectInsertedIdStatement,
|
|
8
|
+
NotNull,
|
|
9
|
+
PrimaryKey,
|
|
10
|
+
ReferentialAction,
|
|
11
|
+
SelectStatement,
|
|
12
|
+
StatementTextRenderer,
|
|
13
|
+
} from "@datasources/core";
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
import { MysqlCreateTableStatementRenderer } from "./MysqlCreateTableStatementRenderer.ts";
|
|
16
|
+
import { MysqlStatementRenderer } from "./MysqlStatementRenderer.ts";
|
|
17
|
+
|
|
18
|
+
describe("MysqlStatementRenderer", () => {
|
|
19
|
+
const renderer = new StatementTextRenderer(t => new MysqlStatementRenderer(t));
|
|
20
|
+
|
|
21
|
+
it("renders inserted id and create table", () => {
|
|
22
|
+
expect(renderer.render(new SelectInsertedIdStatement("users", "id"))).toBe(
|
|
23
|
+
"SELECT LAST_INSERT_ID() AS id",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const create = new CreateTableStatement(
|
|
27
|
+
"orders",
|
|
28
|
+
[
|
|
29
|
+
{ name: "id", type: "INTEGER", default: new ColumnDefault("0", "df_id"), notNull: new NotNull("nn_id") },
|
|
30
|
+
{ name: "user_id", type: "INTEGER" },
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
new PrimaryKey("pk_orders", ["id"]),
|
|
34
|
+
new ForeignKey("fk_orders_user", ["user_id"], "users", ["id"], ReferentialAction.Cascade),
|
|
35
|
+
],
|
|
36
|
+
);
|
|
37
|
+
expect(renderer.render(create)).toBe(new MysqlCreateTableStatementRenderer().render(create));
|
|
38
|
+
expect(renderer.render(new SelectStatement({ table: "users", fields: ["id"] }))).toBe(
|
|
39
|
+
"SELECT id FROM users",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("renders type default and identity", () => {
|
|
44
|
+
const create = new CreateTableStatement("users", [
|
|
45
|
+
{ name: "id", type: "int", default: new ColumnDefault("0"), notNull: new NotNull(), identity: true },
|
|
46
|
+
]);
|
|
47
|
+
expect(renderer.render(create)).toBe(
|
|
48
|
+
"CREATE TABLE users (id INT AUTO_INCREMENT DEFAULT 0 NOT NULL)",
|
|
49
|
+
);
|
|
50
|
+
expect(DefaultStatementTextRenderer.instance.render(create)).toBe(
|
|
51
|
+
"CREATE TABLE users (id int DEFAULT 0 NOT NULL)",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders bulk update as join", () => {
|
|
56
|
+
const update = new BulkUpdateStatement("parents", ["label"], ["id"], [
|
|
57
|
+
{ fields: [{ name: "label", value: "a" }], ids: [{ name: "id", value: 1 }] },
|
|
58
|
+
{ fields: [{ name: "label", value: "b" }], ids: [{ name: "id", value: 2 }] },
|
|
59
|
+
]);
|
|
60
|
+
expect(renderer.render(update)).toBe(
|
|
61
|
+
"UPDATE parents JOIN (SELECT $label__0 AS label, $id__0 AS id UNION ALL SELECT $label__1 AS label, $id__1 AS id) AS data ON parents.id = data.id SET parents.label = data.label",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BulkUpdateStatement,
|
|
3
|
+
CreateTableStatement,
|
|
4
|
+
SelectInsertedIdStatement,
|
|
5
|
+
SelectInsertedIdStatementTextRenderer,
|
|
6
|
+
StatementRenderer,
|
|
7
|
+
type StatementTextRenderer,
|
|
8
|
+
} from "@datasources/core";
|
|
9
|
+
import { MysqlBulkUpdateStatementTextRenderer } from "./MysqlBulkUpdateStatementTextRenderer.ts";
|
|
10
|
+
import { MysqlCreateTableStatementRenderer } from "./MysqlCreateTableStatementRenderer.ts";
|
|
11
|
+
|
|
12
|
+
class MysqlSelectInsertedIdStatementTextRenderer extends SelectInsertedIdStatementTextRenderer {
|
|
13
|
+
override render(statement: SelectInsertedIdStatement): string {
|
|
14
|
+
return `SELECT LAST_INSERT_ID() AS ${statement.column}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MysqlStatementRenderer extends StatementRenderer {
|
|
19
|
+
constructor(text: StatementTextRenderer) {
|
|
20
|
+
super(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected override visitSelectInsertedId(inserted: SelectInsertedIdStatement): string {
|
|
24
|
+
return new MysqlSelectInsertedIdStatementTextRenderer().render(inserted);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected override visitCreateTable(create: CreateTableStatement): string {
|
|
28
|
+
return new MysqlCreateTableStatementRenderer().render(create);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected override visitBulkUpdate(update: BulkUpdateStatement): string {
|
|
32
|
+
return new MysqlBulkUpdateStatementTextRenderer().render(update);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/bind.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parametersToRecord, type Cell, type Parameter } from "@datasources/core";
|
|
2
|
+
import { reverse } from "./converters/MysqlRowTypeConverters.ts";
|
|
3
|
+
import type { Scalar } from "./scalar.ts";
|
|
4
|
+
|
|
5
|
+
const NAME = /\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
6
|
+
const TOKEN = /\$[A-Za-z_][A-Za-z0-9_]*|'[^']*'|"[^"]*"|`[^`]*`|\b[A-Za-z_][A-Za-z0-9_]*\b/g;
|
|
7
|
+
|
|
8
|
+
export const withoutReturning = (sql: string): string =>
|
|
9
|
+
sql.replace(/\s+RETURNING\b[^;]*$/i, "");
|
|
10
|
+
|
|
11
|
+
const quoteReserved = (sql: string): string =>
|
|
12
|
+
sql.replace(TOKEN, (token, offset: number) => {
|
|
13
|
+
const skip = token.startsWith("$") || token.startsWith("'") || token.startsWith("\"") || token.startsWith("`");
|
|
14
|
+
const reserved = !skip && token.toLowerCase() === "key"
|
|
15
|
+
&& !/\b(?:PRIMARY|FOREIGN|UNIQUE)\s+$/i.test(sql.slice(0, offset));
|
|
16
|
+
const mixed = !skip && /[A-Z]/.test(token) && /[a-z]/.test(token);
|
|
17
|
+
return reserved || mixed ? `\`${token}\`` : token;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const toMysql = (sql: string): string =>
|
|
21
|
+
quoteReserved(withoutReturning(sql)).replace(NAME, (_match, name: string) => `:${name}`);
|
|
22
|
+
|
|
23
|
+
export const bindParameters = (
|
|
24
|
+
parameters?: Parameter[] | null,
|
|
25
|
+
): Record<string, Scalar> => {
|
|
26
|
+
const record = parametersToRecord(parameters);
|
|
27
|
+
const bound: Record<string, Scalar> = {};
|
|
28
|
+
for (const [name, value] of Object.entries(record))
|
|
29
|
+
bound[name] = reverse(value as Cell) as Scalar;
|
|
30
|
+
return bound;
|
|
31
|
+
};
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayReader,
|
|
3
|
+
CommandMiddlewareHost,
|
|
4
|
+
FirstResultReader,
|
|
5
|
+
DatasourceError,
|
|
6
|
+
ExecuteStoredProcedureStatement,
|
|
7
|
+
Native,
|
|
8
|
+
ParameterDirection,
|
|
9
|
+
ReturnStatement,
|
|
10
|
+
SelectStatement,
|
|
11
|
+
StatementTextRenderer,
|
|
12
|
+
Response,
|
|
13
|
+
splitStatements,
|
|
14
|
+
statementParameters,
|
|
15
|
+
statementReader,
|
|
16
|
+
StreamingReader,
|
|
17
|
+
type Cells,
|
|
18
|
+
type ICommand,
|
|
19
|
+
type IReader,
|
|
20
|
+
type IStatement,
|
|
21
|
+
type Parameter,
|
|
22
|
+
} from "@datasources/core";
|
|
23
|
+
import { bindParameters, toMysql } from "./bind.ts";
|
|
24
|
+
import type { MysqlConnection } from "./connection.ts";
|
|
25
|
+
import type { MysqlExecuteResult } from "./i-mysql-engine.ts";
|
|
26
|
+
import type { Scalar } from "./scalar.ts";
|
|
27
|
+
import { MysqlStatementRenderer } from "./MysqlStatementRenderer.ts";
|
|
28
|
+
|
|
29
|
+
const outputs = (parameters?: Parameter[]) =>
|
|
30
|
+
parameters?.filter(
|
|
31
|
+
(parameter) =>
|
|
32
|
+
parameter.direction && parameter.direction !== ParameterDirection.Input,
|
|
33
|
+
) ?? [];
|
|
34
|
+
|
|
35
|
+
export class MysqlCommand extends CommandMiddlewareHost implements ICommand {
|
|
36
|
+
private readonly statements: StatementTextRenderer;
|
|
37
|
+
|
|
38
|
+
constructor(readonly connection: MysqlConnection, statements?: StatementTextRenderer) {
|
|
39
|
+
super();
|
|
40
|
+
this.statements = statements ?? new StatementTextRenderer(t => new MysqlStatementRenderer(t));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async executeReaderNative(
|
|
44
|
+
text: Native,
|
|
45
|
+
parameters?: Parameter[],
|
|
46
|
+
): Promise<IReader<Cells>> {
|
|
47
|
+
return this.run(text.text, parameters, async (query, params) => {
|
|
48
|
+
if (!query.trim()) return { value: new ArrayReader([]), rows: [] };
|
|
49
|
+
this.connection.ensureOpen();
|
|
50
|
+
const statements = splitStatements(query);
|
|
51
|
+
if (statements.length === 0) return { value: new ArrayReader([]), rows: [] };
|
|
52
|
+
const bound = bindParameters(params);
|
|
53
|
+
if (statements.some((sql) => /^\s*call\b/i.test(sql))) {
|
|
54
|
+
const sets: Cells[][] = [];
|
|
55
|
+
for (const sql of statements) sets.push(...(await this.callResults(sql, bound)));
|
|
56
|
+
return FirstResultReader.capture(resultSetsReader(sets));
|
|
57
|
+
}
|
|
58
|
+
return FirstResultReader.capture(statementReader(statements, (sql) => this.query(sql, bound)));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async executeReaderStatement(
|
|
63
|
+
statement: IStatement,
|
|
64
|
+
parameters?: Parameter[],
|
|
65
|
+
): Promise<IReader<Cells>> {
|
|
66
|
+
return this.executeReaderNative(new Native(this.statements.render(statement)), parameters);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async executeStatement(statement: IStatement, parameters?: Parameter[]): Promise<Response> {
|
|
70
|
+
if (statement instanceof SelectStatement || statement instanceof ReturnStatement)
|
|
71
|
+
return this.firstCell(statement, parameters);
|
|
72
|
+
if (statement instanceof ExecuteStoredProcedureStatement && statement.name && outputs(parameters).length)
|
|
73
|
+
return this.executeProcedure(this.statements.render(statement), parameters);
|
|
74
|
+
return this.executeNative(new Native(this.statements.render(statement)), parameters);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async firstCell(statement: IStatement, parameters?: Parameter[]): Promise<Response> {
|
|
78
|
+
const reader = await this.executeReaderStatement(statement, parameters);
|
|
79
|
+
try {
|
|
80
|
+
if (!(await reader.next())) return new Response(null, 0, parameters);
|
|
81
|
+
const row = reader.current;
|
|
82
|
+
/* v8 ignore next */
|
|
83
|
+
if (!row) throw new DatasourceError("Reader current must be a row");
|
|
84
|
+
const values = Object.values(row);
|
|
85
|
+
return new Response(values.length === 0 ? null : values[0], 1, parameters);
|
|
86
|
+
} finally {
|
|
87
|
+
await reader.dispose();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async executeNative(text: Native, parameters?: Parameter[]): Promise<Response> {
|
|
92
|
+
return this.run(text.text, parameters, async (query, params) => {
|
|
93
|
+
if (!query.trim()) return { value: new Response(undefined, 0, params), rows: [] };
|
|
94
|
+
this.connection.ensureOpen();
|
|
95
|
+
const named = bindParameters(params);
|
|
96
|
+
let rowsAffected = 0;
|
|
97
|
+
let parameters = params;
|
|
98
|
+
let result: MysqlExecuteResult["result"];
|
|
99
|
+
for (const part of splitStatements(query)) {
|
|
100
|
+
const names = new Set(statementParameters(part));
|
|
101
|
+
const used = Object.fromEntries(
|
|
102
|
+
Object.entries(named).filter(([name]) => names.has(name.replace(/^[$:@]/, ""))),
|
|
103
|
+
);
|
|
104
|
+
const executed = await this.runSql(part, used, params);
|
|
105
|
+
rowsAffected += executed.rowsAffected;
|
|
106
|
+
parameters = executed.parameters ?? params;
|
|
107
|
+
result = executed.result;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
value: new Response(result, rowsAffected, parameters),
|
|
111
|
+
rows: [],
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async executeProcedure(sql: string, parameters?: Parameter[]): Promise<Response> {
|
|
117
|
+
return this.run(sql, parameters, async (query, params) => {
|
|
118
|
+
this.connection.ensureOpen();
|
|
119
|
+
const outs = outputs(params);
|
|
120
|
+
const client = this.connection.require();
|
|
121
|
+
for (const parameter of params!) {
|
|
122
|
+
if (
|
|
123
|
+
!parameter.direction ||
|
|
124
|
+
parameter.direction === ParameterDirection.Input ||
|
|
125
|
+
parameter.direction === ParameterDirection.InputOutput
|
|
126
|
+
)
|
|
127
|
+
await client.query(`SET @${parameter.name} = :${parameter.name}`, {
|
|
128
|
+
[parameter.name]: parameter.value ?? null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
await client.query(toMysql(query).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "@$1"));
|
|
132
|
+
const [rows] = await client.query(
|
|
133
|
+
`SELECT ${outs.map((parameter) => `@${parameter.name} AS \`${parameter.name}\``).join(", ")}`,
|
|
134
|
+
);
|
|
135
|
+
const row = Array.isArray(rows) ? (rows[0] as Cells | undefined) : undefined;
|
|
136
|
+
return {
|
|
137
|
+
value: new Response(
|
|
138
|
+
undefined,
|
|
139
|
+
0,
|
|
140
|
+
params?.map((parameter) =>
|
|
141
|
+
row && parameter.direction && parameter.direction !== ParameterDirection.Input
|
|
142
|
+
? { ...parameter, value: cellValue(row[parameter.name]) }
|
|
143
|
+
: parameter,
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
rows: [],
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async runSql(
|
|
152
|
+
sql: string,
|
|
153
|
+
params?: Record<string, Scalar>,
|
|
154
|
+
parameters?: Parameter[],
|
|
155
|
+
): Promise<MysqlExecuteResult> {
|
|
156
|
+
const [result] = await this.connection.require().query(toMysql(sql), params);
|
|
157
|
+
return Array.isArray(result)
|
|
158
|
+
? { rowsAffected: result.length, parameters }
|
|
159
|
+
: { rowsAffected: result.affectedRows ?? 0, result: null, parameters };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private query(sql: string, params?: Record<string, Scalar>): AsyncIterable<Cells> {
|
|
163
|
+
return this.connection.require().queryRows(toMysql(sql), params);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async callResults(sql: string, params?: Record<string, Scalar>): Promise<Cells[][]> {
|
|
167
|
+
const [result] = await this.connection.require().query(toMysql(sql), params);
|
|
168
|
+
if (!Array.isArray(result)) return [[]];
|
|
169
|
+
return result.filter((item): item is Cells[] => Array.isArray(item));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cellValue = (cell: Scalar | object | undefined): Scalar => {
|
|
174
|
+
if (cell === null || cell === undefined) return null;
|
|
175
|
+
if (typeof cell === "string" || typeof cell === "number" || typeof cell === "boolean" || typeof cell === "bigint")
|
|
176
|
+
return cell;
|
|
177
|
+
if (cell instanceof Uint8Array) return new TextDecoder().decode(cell);
|
|
178
|
+
return JSON.stringify(cell);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const resultSetsReader = (sets: Cells[][]): StreamingReader => {
|
|
182
|
+
let set = 0;
|
|
183
|
+
let row = 0;
|
|
184
|
+
return new StreamingReader(
|
|
185
|
+
async () => {
|
|
186
|
+
const current = sets[set]?.[row];
|
|
187
|
+
if (current === undefined) return undefined;
|
|
188
|
+
row += 1;
|
|
189
|
+
return current;
|
|
190
|
+
},
|
|
191
|
+
async () => {
|
|
192
|
+
set += 1;
|
|
193
|
+
row = 0;
|
|
194
|
+
return set < sets.length;
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
};
|