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