Skip to content

Commit 814cea0

Browse files
committed
3.4.0 improve custom types in sets handling
1 parent 1b54086 commit 814cea0

18 files changed

Lines changed: 1721 additions & 35 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace NpgsqlRest.Defaults;
2+
3+
internal static partial class DefaultCommentParser
4+
{
5+
/// <summary>
6+
/// Annotation: nested | nested_json | nested_composite
7+
/// Syntax: nested
8+
///
9+
/// Description: Serialize composite type columns as nested JSON objects instead of flattening them.
10+
/// For example, a column "req" of type "my_request(id int, name text)" becomes {"req": {"id": 1, "name": "test"}}
11+
/// instead of the default flat structure {"id": 1, "name": "test"}.
12+
/// </summary>
13+
private static readonly string[] NestedJsonKey = [
14+
"nested",
15+
"nested_json",
16+
"nested_composite",
17+
];
18+
19+
private static void HandleNestedJson(
20+
RoutineEndpoint endpoint,
21+
string description)
22+
{
23+
Logger?.CommentSetNestedJson(description);
24+
endpoint.NestedJsonForCompositeTypes = true;
25+
}
26+
}

NpgsqlRest/Defaults/DefaultCommentParser.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,14 @@ internal static partial class DefaultCommentParser
368368
{
369369
HandleProxy(routine, routineEndpoint, wordsLower, words, len, description);
370370
}
371+
372+
// nested
373+
// nested_json
374+
// nested_composite
375+
else if (haveTag is true && StrEqualsToArray(wordsLower[0], NestedJsonKey))
376+
{
377+
HandleNestedJson(routineEndpoint, description);
378+
}
371379
}
372380
if (disabled)
373381
{

NpgsqlRest/Log.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,7 @@ public static partial class Log
265265

266266
[LoggerMessage(Level = LogLevel.Debug, Message = "{endpoint} validation failed for parameter '{paramName}': {message}")]
267267
public static partial void ValidationFailed(this ILogger logger, string endpoint, string paramName, string message);
268+
269+
[LoggerMessage(Level = LogLevel.Debug, Message = "{description} has set NESTED JSON FOR COMPOSITE TYPES by the comment annotation.")]
270+
public static partial void CommentSetNestedJson(this ILogger logger, string description);
268271
}

NpgsqlRest/NpgsqlRest.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,6 @@
6262

6363
<ItemGroup>
6464
<InternalsVisibleTo Include="NpgsqlRestTests" />
65+
<InternalsVisibleTo Include="NpgsqlRest.TsClient" />
6566
</ItemGroup>
6667
</Project>

NpgsqlRest/NpgsqlRestEndpoint.cs

Lines changed: 122 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,6 +1937,35 @@ await command.ExecuteNonQueryWithRetryAsync(
19371937
StringBuilder row = new();
19381938
ulong rowCount = 0;
19391939

1940+
// Precompute nested JSON composite column mapping
1941+
// Maps column index to: (compositeColumnName, fieldName, isFirstField, isLastField, fieldCount)
1942+
Dictionary<int, (string CompositeColumnName, string FieldName, bool IsFirstField, bool IsLastField, int FieldCount)>? nestedJsonColumnMap = null;
1943+
if (endpoint.NestedJsonForCompositeTypes == true && routine.CompositeColumnInfo is not null)
1944+
{
1945+
nestedJsonColumnMap = new();
1946+
foreach (var (_, compositeInfo) in routine.CompositeColumnInfo)
1947+
{
1948+
var indices = compositeInfo.ExpandedColumnIndices;
1949+
var fieldCount = indices.Length;
1950+
for (int fieldIdx = 0; fieldIdx < fieldCount; fieldIdx++)
1951+
{
1952+
var colIdx = indices[fieldIdx];
1953+
nestedJsonColumnMap[colIdx] = (
1954+
compositeInfo.ConvertedColumnName,
1955+
compositeInfo.FieldNames[fieldIdx],
1956+
fieldIdx == 0,
1957+
fieldIdx == indices.Length - 1,
1958+
fieldCount
1959+
);
1960+
}
1961+
}
1962+
}
1963+
1964+
// Buffer for composite fields to detect all-NULL composites
1965+
StringBuilder? compositeBuffer = nestedJsonColumnMap is not null ? new() : null;
1966+
bool compositeHasNonNullValue = false;
1967+
string? currentCompositeName = null;
1968+
19401969
if (endpoint.Raw is true && endpoint.RawColumnNames is true && binary is false)
19411970
{
19421971
StringBuilder columns = new();
@@ -2017,76 +2046,155 @@ await command.ExecuteNonQueryWithRetryAsync(
20172046
}
20182047
else
20192048
{
2049+
// Handle nested JSON composite types
2050+
(string CompositeColumnName, string FieldName, bool IsFirstField, bool IsLastField, int FieldCount) compositeMapping = default;
2051+
bool isInComposite = nestedJsonColumnMap is not null && nestedJsonColumnMap.TryGetValue(i, out compositeMapping);
2052+
2053+
// Determine which buffer to write to
2054+
StringBuilder outputBuffer = (isInComposite && compositeBuffer is not null) ? compositeBuffer : row;
2055+
20202056
if (routine.ReturnsUnnamedSet == false)
20212057
{
20222058
if (i == 0)
20232059
{
20242060
row.Append(Consts.OpenBrace);
20252061
}
2026-
row.Append(Consts.DoubleQuote);
2027-
row.Append(routine.ColumnNames[i]);
2028-
row.Append(Consts.DoubleQuoteColon);
2062+
2063+
if (isInComposite)
2064+
{
2065+
if (compositeMapping.IsFirstField)
2066+
{
2067+
// Start of composite: reset buffer and track column name
2068+
compositeBuffer!.Clear();
2069+
compositeHasNonNullValue = false;
2070+
currentCompositeName = compositeMapping.CompositeColumnName;
2071+
2072+
// Write field name/value to buffer
2073+
outputBuffer.Append(Consts.DoubleQuote);
2074+
outputBuffer.Append(compositeMapping.FieldName);
2075+
outputBuffer.Append(Consts.DoubleQuoteColon);
2076+
}
2077+
else
2078+
{
2079+
// Middle or end field in composite: just output field name
2080+
outputBuffer.Append(Consts.DoubleQuote);
2081+
outputBuffer.Append(compositeMapping.FieldName);
2082+
outputBuffer.Append(Consts.DoubleQuoteColon);
2083+
}
2084+
}
2085+
else
2086+
{
2087+
row.Append(Consts.DoubleQuote);
2088+
row.Append(routine.ColumnNames[i]);
2089+
row.Append(Consts.DoubleQuoteColon);
2090+
}
20292091
}
20302092

20312093
var descriptor = routine.ColumnsTypeDescriptor[i];
20322094
if (value == DBNull.Value)
20332095
{
2034-
row.Append(Consts.Null);
2096+
outputBuffer.Append(Consts.Null);
20352097
}
20362098
else if (descriptor.IsArray && value is not null)
20372099
{
2038-
row.Append(PgArrayToJsonArray(raw, descriptor));
2100+
if (isInComposite) compositeHasNonNullValue = true;
2101+
// Check if this is an array of composite types - always serialize as nested JSON objects
2102+
if (routine.ArrayCompositeColumnInfo is not null &&
2103+
routine.ArrayCompositeColumnInfo.TryGetValue(i, out var arrayCompositeInfo))
2104+
{
2105+
outputBuffer.Append(PgCompositeArrayToJsonArray(raw, arrayCompositeInfo.FieldNames, arrayCompositeInfo.FieldDescriptors));
2106+
}
2107+
else
2108+
{
2109+
outputBuffer.Append(PgArrayToJsonArray(raw, descriptor));
2110+
}
20392111
}
20402112
else if ((descriptor.IsNumeric || descriptor.IsBoolean || descriptor.IsJson) && value is not null)
20412113
{
2114+
if (isInComposite) compositeHasNonNullValue = true;
20422115
if (descriptor.IsBoolean)
20432116
{
20442117
if (raw.Length == 1 && raw[0] == 't')
20452118
{
2046-
row.Append(Consts.True);
2119+
outputBuffer.Append(Consts.True);
20472120
}
20482121
else if (raw.Length == 1 && raw[0] == 'f')
20492122
{
2050-
row.Append(Consts.False);
2123+
outputBuffer.Append(Consts.False);
20512124
}
20522125
else
20532126
{
2054-
row.Append(raw);
2127+
outputBuffer.Append(raw);
20552128
}
20562129
}
20572130
else
20582131
{
20592132
// numeric and json
2060-
row.Append(raw);
2133+
outputBuffer.Append(raw);
20612134
}
20622135
}
20632136
else
20642137
{
2138+
if (isInComposite) compositeHasNonNullValue = true;
20652139
if (descriptor.ActualDbType == NpgsqlDbType.Unknown)
20662140
{
2067-
row.Append(PgUnknownToJsonArray(ref raw));
2141+
outputBuffer.Append(PgUnknownToJsonArray(ref raw));
20682142
}
20692143
else if (descriptor.NeedsEscape)
20702144
{
2071-
row.Append(SerializeString(ref raw));
2145+
outputBuffer.Append(SerializeString(ref raw));
20722146
}
20732147
else
20742148
{
20752149
if (descriptor.IsDateTime)
20762150
{
2077-
row.Append(QuoteDateTime(ref raw));
2151+
outputBuffer.Append(QuoteDateTime(ref raw));
20782152
}
20792153
else
20802154
{
2081-
row.Append(Quote(ref raw));
2155+
outputBuffer.Append(Quote(ref raw));
20822156
}
20832157
}
20842158
}
2159+
2160+
// Handle closing braces and commas for nested JSON composite types
2161+
if (isInComposite && compositeMapping.IsLastField)
2162+
{
2163+
// End of composite: decide whether to output null or the buffered object
2164+
row.Append(Consts.DoubleQuote);
2165+
row.Append(currentCompositeName);
2166+
row.Append(Consts.DoubleQuoteColon);
2167+
2168+
if (compositeHasNonNullValue)
2169+
{
2170+
// At least one field has a value, output as object
2171+
row.Append(Consts.OpenBrace);
2172+
row.Append(compositeBuffer);
2173+
row.Append(Consts.CloseBrace);
2174+
}
2175+
else
2176+
{
2177+
// All fields are NULL, output null
2178+
row.Append(Consts.Null);
2179+
}
2180+
}
2181+
else if (isInComposite)
2182+
{
2183+
// Add comma between composite fields
2184+
outputBuffer.Append(Consts.Comma);
2185+
}
2186+
20852187
if (routine.ReturnsUnnamedSet == false && i == routine.ColumnCount - 1)
20862188
{
20872189
row.Append(Consts.CloseBrace);
20882190
}
2089-
if (i < routine.ColumnCount - 1)
2191+
2192+
// Add comma between columns (but not within composite fields which are handled above)
2193+
if (!isInComposite && i < routine.ColumnCount - 1)
2194+
{
2195+
row.Append(Consts.Comma);
2196+
}
2197+
else if (isInComposite && compositeMapping.IsLastField && i < routine.ColumnCount - 1)
20902198
{
20912199
row.Append(Consts.Comma);
20922200
}

0 commit comments

Comments
 (0)