using System.Globalization;
using System.Runtime.CompilerServices;
using NpgsqlTypes;
namespace NpgsqlRest;
///
/// Pre-computed delegate array for type-specific parameter parsing.
/// Replaces sequential if-chain with O(1) delegate lookup.
///
public static class ParameterParsers
{
///
/// Delegate signature for type-specific parsing.
///
/// The string value to parse
/// The parsed result if successful
/// True if parsing succeeded, false otherwise
public delegate bool TryParseDelegate(string? value, out object? result);
private static readonly TryParseDelegate?[] _parsers;
static ParameterParsers()
{
_parsers = new TryParseDelegate?[128];
// Numeric types
_parsers[(int)NpgsqlDbType.Smallint] = TryParseSmallint;
_parsers[(int)NpgsqlDbType.Integer] = TryParseInteger;
_parsers[(int)NpgsqlDbType.Bigint] = TryParseBigint;
_parsers[(int)NpgsqlDbType.Double] = TryParseDouble;
_parsers[(int)NpgsqlDbType.Real] = TryParseReal;
_parsers[(int)NpgsqlDbType.Numeric] = TryParseDecimal;
_parsers[(int)NpgsqlDbType.Money] = TryParseDecimal;
// Boolean
_parsers[(int)NpgsqlDbType.Boolean] = TryParseBoolean;
// DateTime types
_parsers[(int)NpgsqlDbType.Timestamp] = TryParseTimestamp;
_parsers[(int)NpgsqlDbType.TimestampTz] = TryParseTimestampTz;
_parsers[(int)NpgsqlDbType.Date] = TryParseDate;
_parsers[(int)NpgsqlDbType.Time] = TryParseTime;
_parsers[(int)NpgsqlDbType.TimeTz] = TryParseTimeTz;
// UUID
_parsers[(int)NpgsqlDbType.Uuid] = TryParseUuid;
// Text types - handled separately (just return string as-is)
// Not added to lookup - will fall through to text handling
}
// NpgsqlDbType flags for Range and Multirange
private const int RangeFlag = 0x40000000; // NpgsqlDbType.Range
private const int MultirangeFlag = 0x20000000; // NpgsqlDbType.Multirange
///
/// Gets the parser delegate for the specified NpgsqlDbType.
/// Returns null if the type should be handled as text.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TryParseDelegate? GetParser(NpgsqlDbType dbType)
{
int rawValue = (int)dbType;
// Range and Multirange types are handled as text
if ((rawValue & (RangeFlag | MultirangeFlag)) != 0)
{
return null;
}
// Strip Array flag and look up in table
int index = rawValue & ~(int)NpgsqlDbType.Array;
return (uint)index < (uint)_parsers.Length ? _parsers[index] : null;
}
private static bool TryParseSmallint(string? value, out object? result)
{
if (short.TryParse(value, CultureInfo.InvariantCulture.NumberFormat, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
private static bool TryParseInteger(string? value, out object? result)
{
if (int.TryParse(value, CultureInfo.InvariantCulture.NumberFormat, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
private static bool TryParseBigint(string? value, out object? result)
{
if (long.TryParse(value, CultureInfo.InvariantCulture.NumberFormat, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
private static bool TryParseDouble(string? value, out object? result)
{
if (double.TryParse(value, CultureInfo.InvariantCulture.NumberFormat, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
private static bool TryParseReal(string? value, out object? result)
{
if (float.TryParse(value, CultureInfo.InvariantCulture.NumberFormat, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
private static bool TryParseDecimal(string? value, out object? result)
{
if (decimal.TryParse(value, CultureInfo.InvariantCulture.NumberFormat, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
private static bool TryParseBoolean(string? value, out object? result)
{
if (bool.TryParse(value, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
// AssumeUniversal: naive ISO strings (no offset, no Z) are treated as UTC -
// the canonical JSON-over-HTTP convention. AdjustToUniversal: Z- and offset-
// bearing values are converted to UTC. Together they yield Kind=Utc holding
// the true UTC instant regardless of the host's TZ. Without these styles,
// DateTime.TryParse silently shifts to host-local time and tags Kind=Local.
private const DateTimeStyles UtcStyles =
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal;
///
/// Process-wide toggle wired from . When true (default), the
/// datetime parsers convert any input to UTC and assume naive strings are UTC. When false, the legacy pre-3.16.0
/// host-local-interpretation behavior is restored.
///
public static bool JsonTimestampsAreUtc = true;
private static bool TryParseTimestamp(string? value, out object? result)
{
if (JsonTimestampsAreUtc)
{
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, UtcStyles, out var v))
{
// Strip Kind so Npgsql sends the UTC clock-time verbatim as the
// naive wall-clock value for `timestamp without time zone`.
result = DateTime.SpecifyKind(v, DateTimeKind.Unspecified);
return true;
}
}
else if (DateTime.TryParse(value, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
private static bool TryParseTimestampTz(string? value, out object? result)
{
if (JsonTimestampsAreUtc)
{
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, UtcStyles, out var v))
{
result = v;
return true;
}
}
else if (DateTime.TryParse(value, out var v))
{
result = DateTime.SpecifyKind(v, DateTimeKind.Utc);
return true;
}
result = null;
return false;
}
private static bool TryParseDate(string? value, out object? result)
{
// Fast path: bare date or naive date-time string. DateOnly.TryParse handles
// "2026-05-20" and "2026-05-20T06:00:00" but rejects any Z- or offset-bearing
// input outright.
if (DateOnly.TryParse(value, out var v))
{
result = v;
return true;
}
// Fallback for Z- and offset-bearing strings (e.g. "2026-05-20T03:00:00Z"). The
// legacy parser returned false here, forcing callers to strip the time portion
// client-side. The fallback parses as DateTime and extracts the date portion.
// The UTC-vs-host-local semantic mirrors JsonTimestampsAreUtc so that the
// chosen date is deterministic on UTC hosts (true) or matches the host's
// calendar day (false).
if (JsonTimestampsAreUtc)
{
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, UtcStyles, out var dt))
{
result = DateOnly.FromDateTime(dt);
return true;
}
}
else if (DateTime.TryParse(value, out var dt))
{
result = DateOnly.FromDateTime(dt);
return true;
}
result = null;
return false;
}
private static bool TryParseTime(string? value, out object? result)
{
if (JsonTimestampsAreUtc)
{
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, UtcStyles, out var v))
{
result = TimeOnly.FromDateTime(v);
return true;
}
}
else if (DateTime.TryParse(value, out var v))
{
result = TimeOnly.FromDateTime(v);
return true;
}
result = null;
return false;
}
private static bool TryParseTimeTz(string? value, out object? result)
{
if (JsonTimestampsAreUtc)
{
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, UtcStyles, out var v))
{
result = new DateTimeOffset(v, TimeSpan.Zero);
return true;
}
}
else if (DateTime.TryParse(value, out var v))
{
result = new DateTimeOffset(DateTime.SpecifyKind(v, DateTimeKind.Utc));
return true;
}
result = null;
return false;
}
private static bool TryParseUuid(string? value, out object? result)
{
if (Guid.TryParse(value, out var v))
{
result = v;
return true;
}
result = null;
return false;
}
}