feat: MySQL, SQL Server and SQLite dialects; writes without RETURNING; one null order on every database
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user