// 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 };
}