@deterministic-code/datasources-mysql 0.1.18 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ import type { DefaultValueOptions } from "./DefaultValueOptions.js";
2
+ export declare const EMPTY_UUID = "00000000-0000-0000-0000-000000000000";
3
+ export type SpecToken = {
4
+ spec: RegExp;
5
+ specText?: (arg: string) => string;
6
+ };
7
+ export type DefaultEntry = SpecToken & {
8
+ native: (arg: string) => string;
9
+ reverse: RegExp;
10
+ };
11
+ type DefaultToken = {
12
+ token: string;
13
+ arg?: string | boolean;
14
+ };
15
+ export declare const sqlStringLiteral: (value: string | number | boolean | null | undefined) => string;
16
+ export declare class DefaultValueConverter {
17
+ static readonly spec: Record<string, Record<string, SpecToken>>;
18
+ readonly dictionary: Record<string, Record<string, DefaultEntry>>;
19
+ constructor(options: DefaultValueOptions);
20
+ static parse(specType: string, value: unknown): DefaultToken;
21
+ convert: (specType: string, text: string) => string | null;
22
+ reverseConvert: (specType: string, native: string) => string;
23
+ private static dictionaryOf;
24
+ }
25
+ export {};
@@ -0,0 +1,172 @@
1
+ export const EMPTY_UUID = "00000000-0000-0000-0000-000000000000";
2
+ const NUMERIC_TYPES = [
3
+ "integer",
4
+ "unsignedinteger",
5
+ "smallinteger",
6
+ "unsignedsmallinteger",
7
+ "biginteger",
8
+ "unsignedbiginteger",
9
+ "float",
10
+ "decimal",
11
+ ];
12
+ const NUMERIC_LITERAL_TYPES = new Set([...NUMERIC_TYPES, "reference"]);
13
+ const CALL_FORM = /^([A-Za-z]+)\('(.*)'\)$/;
14
+ const quotedReverse = /^'(.*)'$/;
15
+ const identity = (arg) => arg;
16
+ const intLit = { spec: /^-?[0-9]+$/, specText: identity };
17
+ const uintLit = { spec: /^[0-9]+$/, specText: identity };
18
+ const floatLit = { spec: /^-?[0-9]+(\.[0-9]+)?$/, specText: identity };
19
+ export const sqlStringLiteral = (value) => `'${String(value).replace(/'/g, "''")}'`;
20
+ const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21
+ const exact = (text) => new RegExp(`^${escapeRe(text)}$`);
22
+ const quoted = (arg) => sqlStringLiteral(arg);
23
+ const callTokenName = (name) => (name === "uuid" ? "Uuid" : name);
24
+ const token = (specToken, native, reverse) => ({
25
+ ...specToken,
26
+ native,
27
+ reverse,
28
+ });
29
+ export class DefaultValueConverter {
30
+ static spec = {
31
+ string: { String: { spec: /^.*$/, specText: identity } },
32
+ character: { String: { spec: /^.+$/, specText: identity } },
33
+ integer: { Numeric: intLit },
34
+ unsignedinteger: { Numeric: uintLit },
35
+ smallinteger: { Numeric: intLit },
36
+ unsignedsmallinteger: { Numeric: uintLit },
37
+ biginteger: { Numeric: intLit },
38
+ unsignedbiginteger: { Numeric: uintLit },
39
+ float: { Numeric: floatLit },
40
+ decimal: { Numeric: floatLit, String: { spec: /^.*$/, specText: identity } },
41
+ boolean: { Boolean: { spec: /^(true|false)$/, specText: identity } },
42
+ datetime: {
43
+ Now: { spec: /^Now$/ },
44
+ UtcNow: { spec: /^UtcNow$/ },
45
+ DateTime: {
46
+ spec: /^DateTime\('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(Z|[+-][0-9]{2}:[0-9]{2})'\)$/,
47
+ specText: (arg) => `DateTime('${arg}')`,
48
+ },
49
+ },
50
+ binary: {
51
+ Hex: { spec: /^Hex\('([0-9a-fA-F]{2})*'\)$/, specText: (arg) => `Hex('${arg}')` },
52
+ },
53
+ uuid: {
54
+ NewId: { spec: /^NewId$/ },
55
+ Empty: { spec: /^Empty$/ },
56
+ Uuid: {
57
+ spec: /^uuid\('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'\)$/,
58
+ specText: (arg) => `uuid('${arg}')`,
59
+ },
60
+ },
61
+ reference: {},
62
+ };
63
+ dictionary;
64
+ constructor(options) {
65
+ this.dictionary = DefaultValueConverter.dictionaryOf(options);
66
+ }
67
+ static parse(specType, value) {
68
+ if (value === undefined || value === null)
69
+ return { token: "None" };
70
+ const v = String(value);
71
+ const call = CALL_FORM.exec(v);
72
+ if (call !== null)
73
+ return { token: callTokenName(call[1]), arg: call[2] };
74
+ if (specType === "boolean") {
75
+ return { token: "Boolean", arg: typeof value === "boolean" ? value : v === "true" };
76
+ }
77
+ if (NUMERIC_LITERAL_TYPES.has(specType))
78
+ return { token: "Numeric", arg: v };
79
+ if (specType === "datetime") {
80
+ if (v === "Now")
81
+ return { token: "Now" };
82
+ if (v === "UtcNow")
83
+ return { token: "UtcNow" };
84
+ return { token: "DateTime", arg: v };
85
+ }
86
+ if (specType === "uuid") {
87
+ if (v === "NewId")
88
+ return { token: "NewId" };
89
+ if (v === "Empty")
90
+ return { token: "Empty" };
91
+ return { token: "Uuid", arg: v };
92
+ }
93
+ if (specType === "binary")
94
+ return { token: "Hex", arg: v };
95
+ return { token: "String", arg: v };
96
+ }
97
+ convert = (specType, text) => {
98
+ const parsed = DefaultValueConverter.parse(specType, text);
99
+ if (parsed.token === "None")
100
+ return null;
101
+ const entries = this.dictionary[specType];
102
+ if (entries === undefined)
103
+ throw new Error(`Unknown spec field type: ${specType}`);
104
+ const entry = entries[parsed.token];
105
+ if (entry === undefined)
106
+ return null;
107
+ if (!entry.spec.test(text)) {
108
+ throw new Error(`default_value '${text}' is not a valid ${specType} ${parsed.token} default`);
109
+ }
110
+ const expr = entry.native(parsed.arg === undefined ? "" : String(parsed.arg));
111
+ return expr === "" ? null : expr;
112
+ };
113
+ reverseConvert = (specType, native) => {
114
+ const entries = this.dictionary[specType];
115
+ if (entries === undefined)
116
+ throw new Error(`Unknown spec field type: ${specType}`);
117
+ for (const [name, entry] of Object.entries(entries)) {
118
+ const match = entry.reverse.exec(native);
119
+ if (match === null)
120
+ continue;
121
+ const arg = match.slice(1).find((group) => group !== undefined) ?? "";
122
+ return entry.specText === undefined ? name : entry.specText(arg);
123
+ }
124
+ throw new Error(`Native default '${native}' is not a valid ${specType} default`);
125
+ };
126
+ static dictionaryOf = (opts) => {
127
+ const spec = DefaultValueConverter.spec;
128
+ const newId = opts.newId();
129
+ const now = opts.now();
130
+ const utcNow = opts.utcNow();
131
+ const numericEntries = (type) => {
132
+ const numeric = spec[type].Numeric;
133
+ return {
134
+ Numeric: token(numeric, (arg) => arg, numeric.spec),
135
+ String: token(numeric, (arg) => arg, numeric.spec),
136
+ };
137
+ };
138
+ const quote = opts.quote ?? quoted;
139
+ const quoteReverse = opts.quoteReverse ?? quotedReverse;
140
+ const quotedString = {
141
+ String: token(spec.string.String, quote, quoteReverse),
142
+ Numeric: token(spec.string.String, quote, quoteReverse),
143
+ };
144
+ const dictionary = {
145
+ string: quotedString,
146
+ character: {
147
+ String: token(spec.character.String, quote, quoteReverse),
148
+ Numeric: token(spec.character.String, quote, quoteReverse),
149
+ },
150
+ boolean: {
151
+ Boolean: token(spec.boolean.Boolean, (arg) => (arg === "true" ? opts.booleanOn : opts.booleanOff), new RegExp(`^(${escapeRe(opts.booleanOn)}|${escapeRe(opts.booleanOff)})$`)),
152
+ },
153
+ datetime: {
154
+ Now: token(spec.datetime.Now, () => now, exact(now)),
155
+ UtcNow: token(spec.datetime.UtcNow, () => utcNow, exact(utcNow)),
156
+ DateTime: token(spec.datetime.DateTime, quoted, quotedReverse),
157
+ },
158
+ binary: {
159
+ Hex: token(spec.binary.Hex, opts.hex, opts.hexReverse),
160
+ },
161
+ uuid: {
162
+ NewId: token(spec.uuid.NewId, () => newId, exact(newId)),
163
+ Empty: token(spec.uuid.Empty, () => quoted(EMPTY_UUID), exact(quoted(EMPTY_UUID))),
164
+ Uuid: token(spec.uuid.Uuid, quoted, quotedReverse),
165
+ },
166
+ reference: {},
167
+ };
168
+ for (const type of NUMERIC_TYPES)
169
+ dictionary[type] = numericEntries(type);
170
+ return dictionary;
171
+ };
172
+ }
@@ -0,0 +1,11 @@
1
+ export type DefaultValueOptions = {
2
+ newId: () => string;
3
+ now: () => string;
4
+ utcNow: () => string;
5
+ booleanOn: string;
6
+ booleanOff: string;
7
+ hex: (arg: string) => string;
8
+ hexReverse: RegExp;
9
+ quote?: (arg: string) => string;
10
+ quoteReverse?: RegExp;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { AlterTableStatementTextRenderer, type AlterAction, type Column, type NotNull } from "@deterministic-code/datasources";
2
2
  export declare class MysqlAlterTableStatementRenderer extends AlterTableStatementTextRenderer {
3
+ protected quote(name: string): string;
3
4
  protected typeName(column: Column): string;
4
5
  protected columnDefault(column: Column): string;
5
6
  protected notNullSql(value?: NotNull): string;
@@ -1,11 +1,14 @@
1
1
  import { AlterTableStatementTextRenderer, } from "@deterministic-code/datasources";
2
- import { columnTypes } from "./columnTypes.js";
2
+ import { MysqlDriver } from "./MysqlDriver.js";
3
3
  export class MysqlAlterTableStatementRenderer extends AlterTableStatementTextRenderer {
4
+ quote(name) {
5
+ return MysqlDriver.quote(name);
6
+ }
4
7
  typeName(column) {
5
- return columnTypes[column.type] ?? column.type;
8
+ return MysqlDriver.nativeType(column);
6
9
  }
7
10
  columnDefault(column) {
8
- return column.default == null ? "" : ` DEFAULT ${column.default.value}`;
11
+ return column.default == null ? "" : ` DEFAULT ${MysqlDriver.nativeDefault(column)}`;
9
12
  }
10
13
  notNullSql(value) {
11
14
  return value == null ? "" : " NOT NULL";
@@ -1,5 +1,6 @@
1
1
  import { CreateTableStatementTextRenderer, type Column, type NotNull } from "@deterministic-code/datasources";
2
2
  export declare class MysqlCreateTableStatementRenderer extends CreateTableStatementTextRenderer {
3
+ protected quote(name: string): string;
3
4
  protected typeName(column: Column): string;
4
5
  protected columnDefault(column: Column): string;
5
6
  protected notNullSql(value?: NotNull): string;
@@ -1,11 +1,14 @@
1
1
  import { CreateTableStatementTextRenderer } from "@deterministic-code/datasources";
2
- import { columnTypes } from "./columnTypes.js";
2
+ import { MysqlDriver } from "./MysqlDriver.js";
3
3
  export class MysqlCreateTableStatementRenderer extends CreateTableStatementTextRenderer {
4
+ quote(name) {
5
+ return MysqlDriver.quote(name);
6
+ }
4
7
  typeName(column) {
5
- return columnTypes[column.type] ?? column.type;
8
+ return MysqlDriver.nativeType(column);
6
9
  }
7
10
  columnDefault(column) {
8
- return column.default == null ? "" : ` DEFAULT ${column.default.value}`;
11
+ return column.default == null ? "" : ` DEFAULT ${MysqlDriver.nativeDefault(column)}`;
9
12
  }
10
13
  notNullSql(value) {
11
14
  return value == null ? "" : " NOT NULL";
@@ -1,7 +1,8 @@
1
1
  import { CreateTriggerStatementTextRenderer } from "@deterministic-code/datasources";
2
+ import { MysqlDriver } from "./MysqlDriver.js";
2
3
  export class MysqlCreateTriggerStatementRenderer extends CreateTriggerStatementTextRenderer {
3
4
  render(statement) {
4
- return `CREATE TRIGGER ${statement.name} BEFORE UPDATE ON ${statement.table}
5
- FOR EACH ROW SET NEW.${statement.updatedColumn} = ${statement.utcNow}`;
5
+ return `CREATE TRIGGER ${MysqlDriver.quote(statement.name)} BEFORE UPDATE ON ${MysqlDriver.quote(statement.table)}
6
+ FOR EACH ROW SET NEW.${MysqlDriver.quote(statement.updatedColumn)} = ${MysqlDriver.utcNow()}`;
6
7
  }
7
8
  }
@@ -1,13 +1,22 @@
1
- import { DefaultValueConverter, DeterministicTypeConverter, StatementTextRenderer, type Cell, type DatasourceOptions, type ICommand, type IConnection, type IDriver, type Information, type ITransaction, type IsolationLevel, type TypeConverter } from "@deterministic-code/datasources";
1
+ import { StatementTextRenderer, type Cell, type Column, type DatasourceOptions, type ICommand, type IConnection, type IDriver, type Information, type ITransaction, type IsolationLevel, type RoutineParameter, type TypeConverter } from "@deterministic-code/datasources";
2
2
  import { columnTypes } from "./columnTypes.js";
3
+ import { TypeMap } from "./TypeMap.js";
4
+ import { DefaultValueConverter } from "./DefaultValueConverter.js";
3
5
  export { columnTypes };
4
6
  export declare class MysqlDriver implements IDriver {
7
+ static readonly procedurePayloadType = "JSON";
8
+ static applyKeywords: (sql: string, keyword: (word: string) => string) => string;
9
+ static quote: (name: string) => string;
10
+ static nativeType: (column: Column) => string;
11
+ static nativeDefault: (column: Column) => string;
12
+ static utcNow: () => string;
13
+ static nativeParam: (parameter: RoutineParameter) => string;
5
14
  readonly statementRenderer: StatementTextRenderer;
6
15
  readonly information: Information;
7
16
  rowTypeConverters: Record<string, TypeConverter<Cell, Cell>>;
8
17
  readonly quoteLeft = "`";
9
18
  readonly quoteRight = "`";
10
- readonly typeConverter: DeterministicTypeConverter;
19
+ readonly typeConverter: TypeMap;
11
20
  readonly defaultValueConverter: DefaultValueConverter;
12
21
  columnType: (name: string) => string;
13
22
  connection: (options?: DatasourceOptions) => IConnection;
@@ -1,13 +1,59 @@
1
- import { DatasourceError, DefaultValueConverter, sqlStringLiteral, DeterministicTypeConverter, StatementTextRenderer, } from "@deterministic-code/datasources";
1
+ import { DatasourceError, StatementTextRenderer, } from "@deterministic-code/datasources";
2
2
  import { MysqlConnection } from "./connection.js";
3
3
  import { MysqlCommand } from "./command.js";
4
4
  import { MysqlTransaction } from "./transaction.js";
5
5
  import { MysqlRowTypeConverters } from "./converters/MysqlRowTypeConverters.js";
6
6
  import { MysqlStatementRenderer } from "./MysqlStatementRenderer.js";
7
7
  import { columnTypes } from "./columnTypes.js";
8
+ import { applyKeywords } from "./applyKeywords.js";
9
+ import { TypeMap } from "./TypeMap.js";
10
+ import { DefaultValueConverter, sqlStringLiteral } from "./DefaultValueConverter.js";
8
11
  export { columnTypes };
9
- const statements = new StatementTextRenderer(t => new MysqlStatementRenderer(t));
12
+ const statements = new StatementTextRenderer(t => new MysqlStatementRenderer(t), undefined, undefined, undefined, applyKeywords);
13
+ const types = new TypeMap({
14
+ string: "TEXT",
15
+ character: "CHAR(1)",
16
+ integer: "INT",
17
+ unsignedinteger: "INT UNSIGNED",
18
+ smallinteger: "SMALLINT",
19
+ unsignedsmallinteger: "SMALLINT UNSIGNED",
20
+ biginteger: "BIGINT",
21
+ unsignedbiginteger: "BIGINT UNSIGNED",
22
+ float: "DOUBLE",
23
+ decimal: "DECIMAL(10, 2)",
24
+ boolean: "TINYINT(1)",
25
+ datetime: "DATETIME(3)",
26
+ binary: "BLOB",
27
+ uuid: "CHAR(36)",
28
+ reference: "INT",
29
+ });
30
+ const defaults = new DefaultValueConverter({
31
+ newId: () => `(UUID())`,
32
+ now: () => `(NOW(3))`,
33
+ utcNow: () => `(UTC_TIMESTAMP(3))`,
34
+ booleanOn: "1",
35
+ booleanOff: "0",
36
+ hex: (arg) => `(X'${arg}')`,
37
+ hexReverse: /^(?:\(X'(.*)'\)|X'(.*)')$/,
38
+ quote: (arg) => `(${sqlStringLiteral(arg)})`,
39
+ quoteReverse: /^(?:\('(.*)'\)|'(.*)')$/,
40
+ });
10
41
  export class MysqlDriver {
42
+ static procedurePayloadType = "JSON";
43
+ static applyKeywords = applyKeywords;
44
+ static quote = (name) => `\`${name}\``;
45
+ static nativeType = (column) => column.field ? types.convertField(column.field) : columnTypes[column.type] ?? column.type;
46
+ static nativeDefault = (column) => column.default == null
47
+ ? ""
48
+ : column.field === undefined
49
+ ? column.default.value
50
+ : defaults.convert(column.field.type, column.default.value) ?? column.default.value;
51
+ static utcNow = () => defaults.convert("datetime", "UtcNow") ?? "";
52
+ static nativeParam = (parameter) => parameter.type === "payload"
53
+ ? MysqlDriver.procedurePayloadType
54
+ : parameter.field
55
+ ? types.convertField(parameter.field)
56
+ : types.dictionary[parameter.type] ?? parameter.type;
11
57
  statementRenderer = statements;
12
58
  information = {
13
59
  descriptions: [
@@ -31,42 +77,8 @@ export class MysqlDriver {
31
77
  rowTypeConverters = MysqlRowTypeConverters;
32
78
  quoteLeft = "`";
33
79
  quoteRight = "`";
34
- typeConverter = new DeterministicTypeConverter({
35
- string: "TEXT",
36
- character: "CHAR(1)",
37
- integer: "INT",
38
- unsignedinteger: "INT UNSIGNED",
39
- smallinteger: "SMALLINT",
40
- unsignedsmallinteger: "SMALLINT UNSIGNED",
41
- biginteger: "BIGINT",
42
- unsignedbiginteger: "BIGINT UNSIGNED",
43
- float: "DOUBLE",
44
- decimal: "DECIMAL(10, 2)",
45
- boolean: "TINYINT(1)",
46
- datetime: "DATETIME(3)",
47
- binary: "BLOB",
48
- uuid: "CHAR(36)",
49
- reference: "INT",
50
- }, {
51
- dialectName: "mysql",
52
- stringUnlimited: "TEXT",
53
- stringSized: (n) => (n <= 16383 ? `VARCHAR(${n})` : "LONGTEXT"),
54
- character: "CHAR",
55
- decimal: { kind: "optional", type: "DECIMAL", fallback: "DECIMAL(10, 2)" },
56
- binaryUnlimited: "BLOB",
57
- binarySized: (n) => (n <= 65535 ? `VARBINARY(${n})` : "LONGBLOB"),
58
- });
59
- defaultValueConverter = new DefaultValueConverter({
60
- newId: () => `(UUID())`,
61
- now: () => `(NOW(3))`,
62
- utcNow: () => `(UTC_TIMESTAMP(3))`,
63
- booleanOn: "1",
64
- booleanOff: "0",
65
- hex: (arg) => `(X'${arg}')`,
66
- hexReverse: /^(?:\(X'(.*)'\)|X'(.*)')$/,
67
- quote: (arg) => `(${sqlStringLiteral(arg)})`,
68
- quoteReverse: /^(?:\('(.*)'\)|'(.*)')$/,
69
- });
80
+ typeConverter = types;
81
+ defaultValueConverter = defaults;
70
82
  columnType = (name) => columnTypes[name] ??
71
83
  (() => {
72
84
  throw new DatasourceError(`Unknown mysql type "${name}"`);
@@ -1,6 +1,7 @@
1
1
  import { DropIndexStatementTextRenderer } from "@deterministic-code/datasources";
2
+ import { MysqlDriver } from "./MysqlDriver.js";
2
3
  export class MysqlDropIndexStatementRenderer extends DropIndexStatementTextRenderer {
3
4
  render(statement) {
4
- return `DROP INDEX ${statement.name} ON ${statement.table}`;
5
+ return `DROP INDEX ${MysqlDriver.quote(statement.name)} ON ${MysqlDriver.quote(statement.table)}`;
5
6
  }
6
7
  }
@@ -0,0 +1,10 @@
1
+ import { CreateIndexStatementTextRenderer, DropTableStatementTextRenderer, type ForeignKeyStatement } from "@deterministic-code/datasources";
2
+ export declare class MysqlDropTableStatementRenderer extends DropTableStatementTextRenderer {
3
+ protected quote(name: string): string;
4
+ }
5
+ export declare class MysqlCreateIndexStatementRenderer extends CreateIndexStatementTextRenderer {
6
+ protected quote(name: string): string;
7
+ }
8
+ export declare class MysqlForeignKeyStatementRenderer {
9
+ render(statement: ForeignKeyStatement): string;
10
+ }
@@ -0,0 +1,23 @@
1
+ import { CreateIndexStatementTextRenderer, DropTableStatementTextRenderer, } from "@deterministic-code/datasources";
2
+ import { MysqlDriver } from "./MysqlDriver.js";
3
+ export class MysqlDropTableStatementRenderer extends DropTableStatementTextRenderer {
4
+ quote(name) {
5
+ return MysqlDriver.quote(name);
6
+ }
7
+ }
8
+ export class MysqlCreateIndexStatementRenderer extends CreateIndexStatementTextRenderer {
9
+ quote(name) {
10
+ return MysqlDriver.quote(name);
11
+ }
12
+ }
13
+ export class MysqlForeignKeyStatementRenderer {
14
+ render(statement) {
15
+ const table = MysqlDriver.quote(statement.table);
16
+ const name = MysqlDriver.quote(statement.name);
17
+ if (statement.kind === "drop")
18
+ return `ALTER TABLE ${table} DROP FOREIGN KEY ${name}`;
19
+ const cols = statement.columns.map(MysqlDriver.quote).join(", ");
20
+ const refs = statement.referencesColumns.map(MysqlDriver.quote).join(", ");
21
+ return `ALTER TABLE ${table} ADD CONSTRAINT ${name} FOREIGN KEY (${cols}) REFERENCES ${MysqlDriver.quote(statement.referencesTable)} (${refs})`;
22
+ }
23
+ }
@@ -0,0 +1,4 @@
1
+ import type { SqlSeedStatement } from "@deterministic-code/datasources";
2
+ export declare class MysqlSqlSeedStatementRenderer {
3
+ render(statement: SqlSeedStatement): string;
4
+ }
@@ -0,0 +1,49 @@
1
+ const quote = (ident) => `\`${ident}\``;
2
+ const quoteString = (value) => `'${String(value).replace(/'/g, "''")}'`;
3
+ const truthy = (value) => value === true || value === "true" || value === "True" || value === 1;
4
+ const numeric = new Set([
5
+ "integer",
6
+ "unsignedinteger",
7
+ "smallinteger",
8
+ "unsignedsmallinteger",
9
+ "biginteger",
10
+ "unsignedbiginteger",
11
+ "reference",
12
+ "float",
13
+ "decimal",
14
+ ]);
15
+ const defaultSql = (type, def) => {
16
+ const text = String(def);
17
+ if (type === "datetime" && text === "UtcNow")
18
+ return "(UTC_TIMESTAMP(3))";
19
+ if (type === "datetime" && text === "Now")
20
+ return "(NOW(3))";
21
+ if (type === "uuid" && text === "NewId")
22
+ return "(UUID())";
23
+ if (type === "boolean")
24
+ return text === "true" || text === "True" ? "1" : "0";
25
+ return quoteString(text);
26
+ };
27
+ const literal = (column) => {
28
+ if (column.value == null)
29
+ return column.field.defaultValue != null ? defaultSql(column.field.type, column.field.defaultValue) : "NULL";
30
+ if (column.field.type === "boolean")
31
+ return truthy(column.value) ? "1" : "0";
32
+ if (numeric.has(column.field.type))
33
+ return String(column.value);
34
+ if (column.field.type === "binary")
35
+ return `(X'${column.value}')`;
36
+ return quoteString(column.value);
37
+ };
38
+ export class MysqlSqlSeedStatementRenderer {
39
+ render(statement) {
40
+ const table = quote(statement.table);
41
+ if (statement.kind === "delete") {
42
+ const key = statement.columns[0];
43
+ return `DELETE FROM ${table} WHERE ${quote(key.column)} = ${literal(key)}`;
44
+ }
45
+ const cols = statement.columns.map((c) => quote(c.column)).join(", ");
46
+ const values = statement.columns.map(literal).join(", ");
47
+ return `INSERT INTO ${table} (${cols}) VALUES (${values})`;
48
+ }
49
+ }
@@ -1,6 +1,7 @@
1
- import { AlterTableStatement, BulkUpdateStatement, CreateBulkCreateStoredProcedureStatement, CreateBulkDeleteStoredProcedureStatement, CreateBulkUpdateStoredProcedureStatement, CreateCreateStoredProcedureStatement, CreateDeleteStoredProcedureStatement, CreateNestedReadStoredProcedureStatement, CreateReadStoredProcedureStatement, CreateTableStatement, CreateTriggerStatement, CreateUpdateStoredProcedureStatement, DropIndexStatement, DropRoutineStatement, SelectInsertedIdStatement, StatementRenderer, type StatementTextRenderer } from "@deterministic-code/datasources";
1
+ import { AlterTableStatement, BulkUpdateStatement, CreateBulkCreateStoredProcedureStatement, CreateBulkDeleteStoredProcedureStatement, CreateBulkUpdateStoredProcedureStatement, CreateCreateStoredProcedureStatement, CreateDeleteStoredProcedureStatement, CreateNestedReadStoredProcedureStatement, CreateReadStoredProcedureStatement, CreateTableStatement, CreateTriggerStatement, CreateUpdateStoredProcedureStatement, CreateIndexStatement, DropIndexStatement, DropRoutineStatement, DropTableStatement, ForeignKeyStatement, type ProcedureSource, SelectInsertedIdStatement, SqlSeedStatement, StatementRenderer, type StatementTextRenderer } from "@deterministic-code/datasources";
2
2
  export declare class MysqlStatementRenderer extends StatementRenderer {
3
3
  constructor(text: StatementTextRenderer);
4
+ procedures(source: ProcedureSource): import("@deterministic-code/datasources").IStatement[];
4
5
  protected visitSelectInsertedId(inserted: SelectInsertedIdStatement): string;
5
6
  protected visitCreateTable(create: CreateTableStatement): string;
6
7
  protected visitAlterTable(alter: AlterTableStatement): string;
@@ -16,4 +17,8 @@ export declare class MysqlStatementRenderer extends StatementRenderer {
16
17
  protected visitCreateBulkCreateStoredProcedureStatement(statement: CreateBulkCreateStoredProcedureStatement): string;
17
18
  protected visitCreateBulkUpdateStoredProcedureStatement(statement: CreateBulkUpdateStoredProcedureStatement): string;
18
19
  protected visitCreateBulkDeleteStoredProcedureStatement(statement: CreateBulkDeleteStoredProcedureStatement): string;
20
+ protected visitSqlSeed(seed: SqlSeedStatement): string;
21
+ protected visitDropTable(drop: DropTableStatement): string;
22
+ protected visitCreateIndex(index: CreateIndexStatement): string;
23
+ protected visitForeignKey(key: ForeignKeyStatement): string;
19
24
  }
@@ -1,4 +1,4 @@
1
- import { SelectInsertedIdStatementTextRenderer, StatementRenderer, } from "@deterministic-code/datasources";
1
+ import { sqlProcedureStatementsOf, SelectInsertedIdStatementTextRenderer, StatementRenderer, } from "@deterministic-code/datasources";
2
2
  import { MysqlAlterTableStatementRenderer } from "./MysqlAlterTableStatementRenderer.js";
3
3
  import { MysqlBulkUpdateStatementTextRenderer } from "./MysqlBulkUpdateStatementTextRenderer.js";
4
4
  import { MysqlCreateTableStatementRenderer } from "./MysqlCreateTableStatementRenderer.js";
@@ -6,6 +6,8 @@ import { MysqlCreateTriggerStatementRenderer } from "./MysqlCreateTriggerStateme
6
6
  import { MysqlDropIndexStatementRenderer } from "./MysqlDropIndexStatementRenderer.js";
7
7
  import { MysqlDropRoutineStatementRenderer } from "./MysqlDropRoutineStatementRenderer.js";
8
8
  import { MysqlStoredProcedureStatementRenderer } from "./MysqlStoredProcedureStatementRenderer.js";
9
+ import { MysqlSqlSeedStatementRenderer } from "./MysqlSqlSeedStatementRenderer.js";
10
+ import { MysqlCreateIndexStatementRenderer, MysqlDropTableStatementRenderer, MysqlForeignKeyStatementRenderer, } from "./MysqlQuotedRenderers.js";
9
11
  class MysqlSelectInsertedIdStatementTextRenderer extends SelectInsertedIdStatementTextRenderer {
10
12
  render(statement) {
11
13
  return `SELECT LAST_INSERT_ID() AS ${statement.column}`;
@@ -15,6 +17,9 @@ export class MysqlStatementRenderer extends StatementRenderer {
15
17
  constructor(text) {
16
18
  super(text);
17
19
  }
20
+ procedures(source) {
21
+ return sqlProcedureStatementsOf(source);
22
+ }
18
23
  visitSelectInsertedId(inserted) {
19
24
  return new MysqlSelectInsertedIdStatementTextRenderer().render(inserted);
20
25
  }
@@ -60,4 +65,16 @@ export class MysqlStatementRenderer extends StatementRenderer {
60
65
  visitCreateBulkDeleteStoredProcedureStatement(statement) {
61
66
  return new MysqlStoredProcedureStatementRenderer().renderBulkDelete(statement);
62
67
  }
68
+ visitSqlSeed(seed) {
69
+ return new MysqlSqlSeedStatementRenderer().render(seed);
70
+ }
71
+ visitDropTable(drop) {
72
+ return new MysqlDropTableStatementRenderer().render(drop);
73
+ }
74
+ visitCreateIndex(index) {
75
+ return new MysqlCreateIndexStatementRenderer().render(index);
76
+ }
77
+ visitForeignKey(key) {
78
+ return new MysqlForeignKeyStatementRenderer().render(key);
79
+ }
63
80
  }
@@ -1,7 +1,9 @@
1
1
  import { DefaultStatementTextRenderer } from "@deterministic-code/datasources";
2
- const params = (list) => list.map((p) => ` IN ${p.name} ${p.type}`).join(",\n");
2
+ import { MysqlDriver } from "./MysqlDriver.js";
3
+ const typeOf = (parameter) => MysqlDriver.nativeParam(parameter);
4
+ const params = (list) => list.map((p) => ` IN ${p.name} ${typeOf(p)}`).join(",\n");
3
5
  const where = (columns) => columns.map((c) => `t.${c} = ${c}`).join(" AND ");
4
- const jsonColumns = (columns) => columns.map((c) => `${c.name} ${c.type} PATH '$.${c.name}'`).join(", ");
6
+ const jsonColumns = (columns) => columns.map((c) => `${c.name} ${typeOf(c)} PATH '$.${c.name}'`).join(", ");
5
7
  const procedure = (name, signature, body) => `DROP PROCEDURE IF EXISTS ${name};
6
8
  GO
7
9
  CREATE PROCEDURE ${name}(${signature})
@@ -19,10 +21,10 @@ export class MysqlStoredProcedureStatementRenderer {
19
21
  const body = generated
20
22
  ? ` DECLARE new_id VARCHAR(36);
21
23
  SET new_id = UUID();
22
- INSERT INTO ${statement.table} (${insertCols.join(", ")})
24
+ INSERT INTO ${MysqlDriver.quote(statement.table)} (${insertCols.join(", ")})
23
25
  VALUES (${vals.join(", ")});
24
26
  SELECT new_id AS ${statement.key};`
25
- : ` INSERT INTO ${statement.table} (${insertCols.join(", ")})
27
+ : ` INSERT INTO ${MysqlDriver.quote(statement.table)} (${insertCols.join(", ")})
26
28
  VALUES (${vals.join(", ")});
27
29
  SELECT LAST_INSERT_ID() AS ${statement.key};`;
28
30
  return procedure(statement.name, `\n${params(statement.parameters)}\n`, body);
@@ -30,22 +32,22 @@ export class MysqlStoredProcedureStatementRenderer {
30
32
  renderNestedRead(statement) {
31
33
  const select = DefaultStatementTextRenderer.instance.render(statement.select);
32
34
  const signature = statement.arguments
33
- .map((argument) => `IN ${argument.parameter.name} ${argument.parameter.type}`)
35
+ .map((argument) => `IN ${argument.parameter.name} ${typeOf(argument.parameter)}`)
34
36
  .join(", ");
35
37
  return procedure(statement.name, signature, ` ${select};`);
36
38
  }
37
39
  renderRead(statement) {
38
40
  const aliased = statement.columns.map((c) => `t.${c}`).join(", ");
39
41
  if (statement.byProperties.length === 0) {
40
- return procedure(statement.name, "", ` SELECT ${aliased} FROM ${statement.table} t ORDER BY t.${statement.key};`);
42
+ return procedure(statement.name, "", ` SELECT ${aliased} FROM ${MysqlDriver.quote(statement.table)} t ORDER BY t.${statement.key};`);
41
43
  }
42
44
  const signature = statement.byProperties
43
45
  .map((c) => {
44
- const type = statement.parameters.find((p) => p.name === c)?.type ?? statement.keyType;
46
+ const type = typeOf(statement.parameters.find((p) => p.name === c) ?? { name: c, type: statement.keyType });
45
47
  return `IN ${c} ${type}`;
46
48
  })
47
49
  .join(", ");
48
- return procedure(statement.name, signature, ` SELECT ${aliased} FROM ${statement.table} t WHERE ${where(statement.byProperties)};`);
50
+ return procedure(statement.name, signature, ` SELECT ${aliased} FROM ${MysqlDriver.quote(statement.table)} t WHERE ${where(statement.byProperties)};`);
49
51
  }
50
52
  renderUpdate(statement) {
51
53
  const sets = statement.writable.map((c) => ` t.${c} = ${c}`);
@@ -55,7 +57,7 @@ export class MysqlStoredProcedureStatementRenderer {
55
57
  sets.push(` t.${statement.auditUpdated} = ${auditParam.name}`);
56
58
  }
57
59
  const by = statement.byProperties.length > 0 ? statement.byProperties : [statement.key];
58
- return procedure(statement.name, `\n${params(statement.parameters)}\n`, ` UPDATE ${statement.table} t
60
+ return procedure(statement.name, `\n${params(statement.parameters)}\n`, ` UPDATE ${MysqlDriver.quote(statement.table)} t
59
61
  SET
60
62
  ${sets.join(",\n")}
61
63
  WHERE ${where(by)};
@@ -65,16 +67,16 @@ ${sets.join(",\n")}
65
67
  const by = statement.byProperties.length > 0 ? statement.byProperties : [statement.key];
66
68
  const signature = by
67
69
  .map((c) => {
68
- const type = statement.parameters.find((p) => p.name === c)?.type ?? statement.keyType;
70
+ const type = typeOf(statement.parameters.find((p) => p.name === c) ?? { name: c, type: statement.keyType });
69
71
  return `IN ${c} ${type}`;
70
72
  })
71
73
  .join(", ");
72
- return procedure(statement.name, signature, ` DELETE t FROM ${statement.table} t WHERE ${where(by)};
74
+ return procedure(statement.name, signature, ` DELETE t FROM ${MysqlDriver.quote(statement.table)} t WHERE ${where(by)};
73
75
  SELECT ROW_COUNT() AS affected;`);
74
76
  }
75
77
  renderBulkCreate(statement) {
76
78
  const cols = statement.columns.map((c) => c.name).join(", ");
77
- return procedure(statement.name, `IN ${statement.payload.name} ${statement.payload.type}, OUT ids JSON`, ` INSERT INTO ${statement.table} (${cols})
79
+ return procedure(statement.name, `IN ${statement.payload.name} ${typeOf(statement.payload)}, OUT ids JSON`, ` INSERT INTO ${MysqlDriver.quote(statement.table)} (${cols})
78
80
  SELECT ${cols}
79
81
  FROM JSON_TABLE(${statement.payload.name}, '$[*]' COLUMNS (
80
82
  ${jsonColumns(statement.columns)}
@@ -93,7 +95,7 @@ ${sets.join(",\n")}
93
95
  .filter((c) => c.name !== statement.key)
94
96
  .map((c) => `c.${c.name} = payload_rows.${c.name}`)
95
97
  .join(", ");
96
- return procedure(statement.name, `IN ${statement.payload.name} ${statement.payload.type}`, ` UPDATE ${statement.table} c
98
+ return procedure(statement.name, `IN ${statement.payload.name} ${typeOf(statement.payload)}`, ` UPDATE ${MysqlDriver.quote(statement.table)} c
97
99
  JOIN JSON_TABLE(${statement.payload.name}, '$[*]' COLUMNS (
98
100
  ${jsonColumns(statement.columns)}
99
101
  )) AS payload_rows ON c.${statement.key} = payload_rows.${statement.key}
@@ -104,6 +106,6 @@ ${sets.join(",\n")}
104
106
  name: statement.key,
105
107
  type: statement.keyType,
106
108
  };
107
- return procedure(statement.name, `IN ${statement.payload.name} ${statement.payload.type}`, ` DELETE c FROM ${statement.table} c JOIN JSON_TABLE(${statement.payload.name}, '$[*]' COLUMNS (${key.name} ${key.type} PATH '$.${key.name}')) AS payload_rows ON c.${statement.key} = payload_rows.${statement.key};`);
109
+ return procedure(statement.name, `IN ${statement.payload.name} ${typeOf(statement.payload)}`, ` DELETE c FROM ${MysqlDriver.quote(statement.table)} c JOIN JSON_TABLE(${statement.payload.name}, '$[*]' COLUMNS (${key.name} ${typeOf(key)} PATH '$.${key.name}')) AS payload_rows ON c.${statement.key} = payload_rows.${statement.key};`);
108
110
  }
109
111
  }
@@ -0,0 +1,9 @@
1
+ import type { IMapping, SpecField } from "@deterministic-code/datasources";
2
+ export declare class TypeMap implements IMapping<string, string> {
3
+ readonly dictionary: Record<string, string>;
4
+ private readonly reverse;
5
+ constructor(dictionary: Record<string, string>);
6
+ convert: (from: string) => string;
7
+ convertField: (field: SpecField) => string;
8
+ reverseConvert: (native: string) => string;
9
+ }
@@ -0,0 +1,43 @@
1
+ export class TypeMap {
2
+ dictionary;
3
+ reverse = new Map();
4
+ constructor(dictionary) {
5
+ this.dictionary = dictionary;
6
+ for (const [spec, native] of Object.entries(dictionary)) {
7
+ if (!this.reverse.has(native))
8
+ this.reverse.set(native, spec);
9
+ }
10
+ }
11
+ convert = (from) => this.dictionary[from] ??
12
+ (() => {
13
+ throw new Error(`Unknown spec field type: ${from}`);
14
+ })();
15
+ convertField = (field) => {
16
+ if (field.type === "string")
17
+ return field.size == null || field.size === "unlimited"
18
+ ? "TEXT"
19
+ : Number(field.size) <= 16383
20
+ ? `VARCHAR(${field.size})`
21
+ : "LONGTEXT";
22
+ if (field.type === "character")
23
+ return `CHAR(${field.size == null ? 1 : Number(field.size)})`;
24
+ if (field.type === "decimal") {
25
+ if (field.size == null)
26
+ return "DECIMAL(10, 2)";
27
+ if (Array.isArray(field.size) && field.size.length === 2)
28
+ return `DECIMAL(${field.size[0]}, ${field.size[1]})`;
29
+ throw new Error(`decimal field "${field.name}" size must be [precision, scale] tuple; got ${field.size}`);
30
+ }
31
+ if (field.type === "binary")
32
+ return field.size == null || field.size === "unlimited"
33
+ ? "BLOB"
34
+ : Number(field.size) <= 65535
35
+ ? `VARBINARY(${field.size})`
36
+ : "LONGBLOB";
37
+ return this.convert(field.type);
38
+ };
39
+ reverseConvert = (native) => this.reverse.get(native) ??
40
+ (() => {
41
+ throw new Error(`Unknown native field type: ${native}`);
42
+ })();
43
+ }
@@ -0,0 +1 @@
1
+ export declare const applyKeywords: (sql: string, keyword: (word: string) => string) => string;
@@ -0,0 +1,13 @@
1
+ const KEYWORDS = new Set([
2
+ "AFTER", "ALTER", "AND", "AS", "AUTO_INCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BIGINT",
3
+ "BLOB", "BOOLEAN", "BY", "CASCADE", "CAST", "CHAR", "CONSTRAINT", "CREATE", "DATETIME",
4
+ "DECIMAL", "DECLARE", "DEFAULT", "DELETE", "DOUBLE", "DROP", "EACH", "ELSE", "END",
5
+ "EXISTS", "FALSE", "FLOAT", "FOR", "FOREIGN", "FROM", "GENERATED", "IF", "IN", "INDEX",
6
+ "INNER", "INSERT", "INT", "INTEGER", "INTO", "JOIN", "KEY", "LAST_INSERT_ID", "LONGBLOB",
7
+ "LONGTEXT", "NOT", "NOW", "NULL", "NUMERIC", "ON", "OR", "ORDER", "PRIMARY", "PROCEDURE",
8
+ "REFERENCES", "REPLACE", "RETURN", "ROW", "ROW_COUNT", "SELECT", "SET", "SMALLINT", "TABLE",
9
+ "TEXT", "THEN", "TIMESTAMP", "TINYINT", "TRIGGER", "TRUE", "UNIQUE", "UNSIGNED", "UPDATE",
10
+ "UTC_TIMESTAMP", "VALUES", "VARBINARY", "VARCHAR", "WHEN", "WHERE",
11
+ ]);
12
+ const TOKEN = /('(?:''|[^'])*')|(`(?:``|[^`])*`)|("(?:""|[^"])*")|(--[^\n]*)|(\/\*[\s\S]*?\*\/)|(?<word>[A-Za-z_][A-Za-z0-9_]*)/g;
13
+ export const applyKeywords = (sql, keyword) => sql.replace(TOKEN, (match, _s, _b, _d, _lc, _bc, word) => word !== undefined && KEYWORDS.has(word.toUpperCase()) ? keyword(word) : match);
@@ -0,0 +1,10 @@
1
+ export type ProcOp = "create" | "read" | "update" | "delete" | "bulkCreate" | "bulkUpdate" | "bulkDelete";
2
+ export type ProcSpec = {
3
+ op: ProcOp;
4
+ name: string;
5
+ byProperties: string[];
6
+ };
7
+ export declare const procedureSpecs: (entityName: string, byGroups: string[][], casing: {
8
+ routineName: (stem: string) => string;
9
+ pluralStem: (entity: string) => string;
10
+ }) => ProcSpec[];
@@ -0,0 +1,17 @@
1
+ export const procedureSpecs = (entityName, byGroups, casing) => {
2
+ const name = (stem) => casing.routineName(stem);
3
+ const plural = casing.pluralStem(entityName);
4
+ const bySuffix = (props) => props.length === 0 ? "" : `_by_${props.join("_")}`;
5
+ return [
6
+ { op: "create", name: name(`create_${plural}`), byProperties: [] },
7
+ { op: "read", name: name(`read_${plural}`), byProperties: [] },
8
+ ...byGroups.flatMap((props) => [
9
+ { op: "read", name: name(`read_${plural}${bySuffix(props)}`), byProperties: props },
10
+ { op: "update", name: name(`update_${plural}${bySuffix(props)}`), byProperties: props },
11
+ { op: "delete", name: name(`delete_${plural}${bySuffix(props)}`), byProperties: props },
12
+ ]),
13
+ { op: "bulkCreate", name: name(`bulk_create_${plural}`), byProperties: [] },
14
+ { op: "bulkUpdate", name: name(`bulk_update_${plural}`), byProperties: [] },
15
+ { op: "bulkDelete", name: name(`bulk_delete_${plural}`), byProperties: [] },
16
+ ];
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deterministic-code/datasources-mysql",
3
- "version": "0.1.18",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.js",
@@ -18,7 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@deterministic-code/datasources": "^0.1.18",
21
+ "@deterministic-code/datasources": "^0.1.30",
22
22
  "mysql2": "^3.14.0"
23
23
  }
24
24
  }