68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import { SqlRenderer, type CatalogColumn, type CatalogFunction, type RenderContext, type Statement } from '@crudy/restloom';
|
|
|
|
/**
|
|
* MySQL 8: backquoted names, positional placeholders, no RETURNING. Arrays are
|
|
* built with GROUP_CONCAT, which keeps an order where JSON_ARRAYAGG does not.
|
|
*/
|
|
export class MysqlRenderer extends SqlRenderer {
|
|
quote(identifier: string): string {
|
|
return `\`${identifier.replace(/`/g, '``')}\``;
|
|
}
|
|
|
|
literal(text: string): string {
|
|
return `'${text.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
}
|
|
|
|
placeholder(_index: number): string {
|
|
return '?';
|
|
}
|
|
|
|
jsonObject(pairs: [string, string][]): string {
|
|
return `JSON_OBJECT(${pairs.map(([k, v]) => `${this.literal(k)}, ${v}`).join(', ')})`;
|
|
}
|
|
|
|
jsonArray(rowExpr: string, orderExpr: string): string {
|
|
return `COALESCE(CAST(CONCAT('[', GROUP_CONCAT(${rowExpr} ORDER BY ${orderExpr} SEPARATOR ','), ']') AS JSON), JSON_ARRAY())`;
|
|
}
|
|
|
|
nested(expr: string): string {
|
|
return `CAST(${expr} AS JSON)`;
|
|
}
|
|
|
|
jsonValue(expr: string, column: CatalogColumn): string {
|
|
if (column.type === 'boolean') return `IF(${expr} IS NULL, NULL, CAST(IF(${expr}, 'true', 'false') AS JSON))`;
|
|
if (column.type === 'timestamp') return `DATE_FORMAT(${expr}, '%Y-%m-%dT%H:%i:%s${/^timestamp/i.test(column.nativeType) ? '+00:00' : ''}')`;
|
|
return expr;
|
|
}
|
|
|
|
like(expr: string, pattern: string, insensitive: boolean): string {
|
|
return insensitive
|
|
? `LOWER(${expr}) LIKE LOWER(${pattern}) ESCAPE '\\\\'`
|
|
: `${expr} COLLATE utf8mb4_bin LIKE ${pattern} ESCAPE '\\\\'`;
|
|
}
|
|
|
|
isBoolean(expr: string, value: boolean): string {
|
|
return `${expr} = ${value ? 1 : 0}`;
|
|
}
|
|
|
|
/** MySQL sorts nulls first ascending; this keeps the Postgres order: last ascending, first descending. */
|
|
orderBy(expr: string, desc: boolean, nulls?: 'first' | 'last'): string {
|
|
const first = (nulls ?? (desc ? 'first' : 'last')) === 'first';
|
|
return `(${expr} IS NULL) ${first ? 'DESC' : 'ASC'}, ${expr} ${desc ? 'DESC' : 'ASC'}`;
|
|
}
|
|
|
|
page(limit: number | undefined, offset: number | undefined): string {
|
|
return limit === undefined && !offset ? '' : ` LIMIT ${limit ?? '18446744073709551615'}${offset ? ` OFFSET ${offset}` : ''}`;
|
|
}
|
|
|
|
returning(): string {
|
|
return '';
|
|
}
|
|
|
|
call(fn: CatalogFunction, args: [string, unknown][], ctx: RenderContext): Statement {
|
|
const byName = new Map(args);
|
|
const call = `${this.table(fn.schema, fn.name)}(${fn.args.map((a) => ctx.params.add(byName.get(a.name))).join(', ')})`;
|
|
return ctx.params.finalize(`SELECT JSON_EXTRACT(JSON_ARRAY(${call}), '$[0]') AS r`, (i) => this.placeholder(i));
|
|
}
|
|
}
|