feat: MySQL, SQL Server and SQLite dialects; writes without RETURNING; one null order on every database

This commit is contained in:
2026-11-02 01:45:55 +01:00
parent 6801f257fb
commit 3c78bd97c1
43 changed files with 2776 additions and 25 deletions
+67
View File
@@ -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));
}
}