Skip to content

Commit b25fb8b

Browse files
committed
fix(Postgres): decode int/bool/float/date columns by typeName
Wire up PgDecoder.ParseValue so values are returned with their proper SqlValueKind instead of falling through as Text. Without this, every non-string column came back as Kind=Text and SqlValue.AsInt() returned null, which caused downstream callers (e.g. positional row mappers) to default numeric and boolean fields to 0/false. Also fix a netstandard2.0 build break in ParseBytea where byte.TryParse(ReadOnlySpan<char>, ...) is unavailable on that target. Bump version to 1.9.48.
1 parent 4a7b3b0 commit b25fb8b

2 files changed

Lines changed: 74 additions & 4 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
<Authors>vkuttyp</Authors>
88
<PackageLicenseExpression>MIT</PackageLicenseExpression>
99
<RepositoryUrl>https://github.com/vkuttyp/CosmoSQLClient-Dotnet</RepositoryUrl>
10-
<Version>1.9.47</Version>
10+
<Version>1.9.48</Version>
1111
</PropertyGroup>
1212
</Project>

src/CosmoSQLClient.Postgres/Proto/PgDecoder.cs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Buffers;
2+
using System.Globalization;
23
using System.Text;
34
using Cosmo.Transport.Pipelines;
45
using CosmoSQLClient.Core;
@@ -43,16 +44,17 @@ public static SqlRow ParseDataRow(ReadOnlySequence<byte> body, IReadOnlyList<Sql
4344
{
4445
var valSeq = body.Slice(reader.Consumed, len);
4546
reader.Advance(len);
47+
var typeName = i < columns.Count ? columns[i].TypeName : string.Empty;
4648
#if NETSTANDARD2_0
47-
values[i] = SqlValue.From(Encoding.UTF8.GetString(valSeq.ToArray()));
49+
values[i] = ParseValue(typeName, Encoding.UTF8.GetString(valSeq.ToArray()));
4850
#else
4951
if (valSeq.IsSingleSegment)
5052
{
51-
values[i] = SqlValue.From(Encoding.UTF8.GetString(valSeq.First.Span));
53+
values[i] = ParseValue(typeName, Encoding.UTF8.GetString(valSeq.First.Span));
5254
}
5355
else
5456
{
55-
values[i] = SqlValue.From(Encoding.UTF8.GetString(valSeq.ToArray()));
57+
values[i] = ParseValue(typeName, Encoding.UTF8.GetString(valSeq.ToArray()));
5658
}
5759
#endif
5860
}
@@ -107,6 +109,74 @@ private static string ReadNullTerminatedString(ref SequenceReader<byte> reader)
107109
1082 => "date", 1114 => "timestamp", 1184 => "timestamptz",
108110
1700 => "numeric", 2950 => "uuid", _ => oid.ToString()
109111
};
112+
113+
private static SqlValue ParseValue(string typeName, string text)
114+
{
115+
switch (typeName.ToLowerInvariant())
116+
{
117+
case "bool":
118+
return bool.TryParse(text, out var b) ? SqlValue.From(b) : SqlValue.From(text == "1" || text.Equals("t", StringComparison.OrdinalIgnoreCase));
119+
case "int2":
120+
case "int4":
121+
case "int8":
122+
return long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l)
123+
? SqlValue.From(l)
124+
: SqlValue.From(text);
125+
case "float4":
126+
case "float8":
127+
return double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var d)
128+
? SqlValue.From(d)
129+
: SqlValue.From(text);
130+
case "numeric":
131+
return decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out var dec)
132+
? SqlValue.From(dec)
133+
: SqlValue.From(text);
134+
case "uuid":
135+
return Guid.TryParse(text, out var g) ? SqlValue.From(g) : SqlValue.From(text);
136+
case "date":
137+
case "timestamp":
138+
return DateTime.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt)
139+
? SqlValue.From(dt)
140+
: SqlValue.From(text);
141+
case "timestamptz":
142+
return DateTimeOffset.TryParse(text, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dto)
143+
? SqlValue.From(dto)
144+
: SqlValue.From(text);
145+
case "bytea":
146+
return ParseBytea(text);
147+
default:
148+
return SqlValue.From(text);
149+
}
150+
}
151+
152+
private static SqlValue ParseBytea(string text)
153+
{
154+
if (string.IsNullOrEmpty(text))
155+
return SqlValue.EmptyString_;
156+
157+
if (text.StartsWith("\\x", StringComparison.OrdinalIgnoreCase))
158+
{
159+
int hexLen = text.Length - 2;
160+
if (hexLen % 2 == 0)
161+
{
162+
var bytes = new byte[hexLen / 2];
163+
for (int i = 0; i < bytes.Length; i++)
164+
{
165+
#if NETSTANDARD2_0
166+
if (!byte.TryParse(text.Substring(2 + i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b))
167+
return SqlValue.From(text);
168+
#else
169+
if (!byte.TryParse(text.AsSpan(2 + i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b))
170+
return SqlValue.From(text);
171+
#endif
172+
bytes[i] = b;
173+
}
174+
return SqlValue.From(bytes);
175+
}
176+
}
177+
178+
return SqlValue.From(text);
179+
}
110180
}
111181

112182
internal interface IPostgresTokenHandler

0 commit comments

Comments
 (0)