// Lo unico que cambia entre motores. Todo lo demas (validacion, conversion, transaccion) es ADO.NET // generico en TablasRamaflex.cs. // // Compilado y probado con .NET 8 contra SQL Server, PostgreSQL y MySQL (ver LEEME.txt). using System.Data; using System.Data.Common; using Microsoft.Data.SqlClient; using MySqlConnector; using Npgsql; namespace Ramaflex; public abstract class Dialecto { /// DbType de los parametros de fecha (el valor es UTC, sin zona). public abstract DbType TipoFecha { get; } /// SELECT de existencia que bloquea la fila (o el hueco de la clave) hasta el fin de la transaccion. public abstract string SqlExiste(string tabla, string donde); /// Paginacion; desde y cuantas son marcadores de parametros. public abstract string Paginar(string desde, string cuantas); /// Choque con otra escritura simultanea (clave duplicada o victima de un interbloqueo): se puede reintentar. public abstract bool EsConflicto(Exception e); public static Dialecto De(string motor) => motor switch { "sqlserver" => new DialectoSqlServer(), "postgresql" => new DialectoPostgreSql(), "mysql" => new DialectoMySql(), _ => throw new ArgumentException("Motor desconocido: " + motor + " (sqlserver, postgresql o mysql).") }; /// Fabrica de conexiones sin abrir para el motor (cada driver mantiene su propio pool). public static Func Conexion(string motor, string cadena) => motor switch { "sqlserver" => () => new SqlConnection(cadena), "postgresql" => () => new NpgsqlConnection(cadena), "mysql" => () => new MySqlConnection(cadena), _ => throw new ArgumentException("Motor desconocido: " + motor + " (sqlserver, postgresql o mysql).") }; } sealed class DialectoSqlServer : Dialecto { // DATETIME2: con DbType.DateTime el driver mandaria DATETIME, que redondea a 1/300 de segundo. public override DbType TipoFecha => DbType.DateTime2; // UPDLOCK + HOLDLOCK bloquea tambien la clave que todavia no existe: un PUT simultaneo espera. public override string SqlExiste(string tabla, string donde) => $"SELECT 1 FROM {tabla} WITH (UPDLOCK, HOLDLOCK) WHERE {donde}"; public override string Paginar(string desde, string cuantas) => $"OFFSET {desde} ROWS FETCH NEXT {cuantas} ROWS ONLY"; // 2627/2601 clave duplicada, 1205 interbloqueo. public override bool EsConflicto(Exception e) => e is SqlException { Number: 2627 or 2601 or 1205 }; } sealed class DialectoPostgreSql : Dialecto { // TIMESTAMP sin zona. En Npgsql 6+ DbType.DateTime es "timestamp with time zone" (rechaza Kind // Unspecified) y DbType.DateTime2 es "timestamp": NO cambiar a DateTime. El valor va con Kind Unspecified // y al leer vuelve con Kind Unspecified, que se formatea tal cual (sin zona del servidor). public override DbType TipoFecha => DbType.DateTime2; public override string SqlExiste(string tabla, string donde) => $"SELECT 1 FROM {tabla} WHERE {donde} FOR UPDATE"; public override string Paginar(string desde, string cuantas) => $"LIMIT {cuantas} OFFSET {desde}"; // 23505 clave duplicada, 40P01 interbloqueo. public override bool EsConflicto(Exception e) => e is PostgresException { SqlState: "23505" or "40P01" }; } sealed class DialectoMySql : Dialecto { // DATETIME sin zona. MySqlConnector lo lee con Kind Unspecified (opcion DateTimeKind por defecto) y // TINYINT(1) como bool (TreatTinyAsBoolean, por defecto true); igual se convierte 0/1 por si acaso. public override DbType TipoFecha => DbType.DateTime; public override string SqlExiste(string tabla, string donde) => $"SELECT 1 FROM {tabla} WHERE {donde} FOR UPDATE"; public override string Paginar(string desde, string cuantas) => $"LIMIT {cuantas} OFFSET {desde}"; // 1062 clave duplicada, 1213 interbloqueo. public override bool EsConflicto(Exception e) => e is MySqlException { Number: 1062 or 1213 }; }