Skip to content

Commit e88bbc5

Browse files
committed
Npgsql: Use EnableDynamicJson to unlock better container type mappings
Vanilla Npgsql provides good enough support to handle CrateDB's ARRAY and OBJECT types better than just plain strings. This patch demonstrates type mappings to native .NET `List` and `Dictionary` types, as well as type mappings to custom .NET POCO types.
1 parent de1d365 commit e88bbc5

File tree

4 files changed

+140
-53
lines changed

4 files changed

+140
-53
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace demo;
2+
3+
public class BasicPoco
4+
{
5+
6+
public string? name { get; set; }
7+
public int? age { get; set; }
8+
9+
public override bool Equals(object obj)
10+
{
11+
var other = (BasicPoco) obj;
12+
return name == other.name && age == other.age;
13+
}
14+
15+
public override int GetHashCode()
16+
{
17+
return base.GetHashCode();
18+
}
19+
20+
public override string ToString() => "Name: " + name + " Age: " + age;
21+
22+
}

by-language/csharp-npgsql/DemoProgram.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@ await Parser.Default.ParseArguments<Options>(args)
1818
var connString = $"Host={options.Host};Port={options.Port};SSL Mode={options.SslMode};" +
1919
$"Username={options.Username};Password={options.Password};Database={options.Database}";
2020
Console.WriteLine($"Connecting to {connString}\n");
21-
await using var conn = new NpgsqlConnection(connString);
22-
conn.Open();
21+
22+
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
23+
dataSourceBuilder.EnableDynamicJson();
24+
await using var dataSource = dataSourceBuilder.Build();
25+
await using var conn = dataSource.OpenConnection();
26+
2327
await DatabaseWorkloads.SystemQueryExample(conn);
2428
await DatabaseWorkloads.BasicConversationExample(conn);
2529
await DatabaseWorkloads.UnnestExample(conn);
26-
await DatabaseWorkloadsMore.AllTypesExample(conn);
30+
await DatabaseWorkloadsMore.AllTypesNativeExample(conn);
31+
await DatabaseWorkloadsMore.ObjectPocoExample(conn);
32+
await DatabaseWorkloadsMore.ObjectPocoArrayExample(conn);
2733
conn.Close();
2834
});
2935

by-language/csharp-npgsql/DemoTypes.cs

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ public class AllTypesRecord
5757
public class DatabaseWorkloadsMore
5858
{
5959

60-
public static async Task<DataTable> AllTypesExample(NpgsqlConnection conn)
60+
public static async Task<DataTable> AllTypesNativeExample(NpgsqlConnection conn)
6161
{
62-
Console.WriteLine("Running AllTypesExample");
62+
Console.WriteLine("Running AllTypesNativeExample");
6363

6464
// Submit DDL, create database schema.
6565
await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS testdrive.example", conn))
@@ -154,11 +154,8 @@ INSERT INTO testdrive.example (
154154
cmd.Parameters.AddWithValue("timestamp_tz", "1970-01-02T00:00:00+01:00");
155155
cmd.Parameters.AddWithValue("timestamp_notz", "1970-01-02T00:00:00");
156156
cmd.Parameters.AddWithValue("ip", "127.0.0.1");
157-
cmd.Parameters.AddWithValue("array", new List<string>{"foo", "bar"});
158-
// FIXME: System.NotSupportedException: Cannot resolve 'hstore' to a fully qualified datatype name. The datatype was not found in the current database info.
159-
// https://github.com/crate/zk/issues/26
160-
// cmd.Parameters.AddWithValue("object", new Dictionary<string, string>(){{"foo", "bar"}});
161-
cmd.Parameters.AddWithValue("object", """{"foo": "bar"}""");
157+
cmd.Parameters.AddWithValue("array", NpgsqlDbType.Json, new List<string>{"foo", "bar"});
158+
cmd.Parameters.AddWithValue("object", NpgsqlDbType.Json, new Dictionary<string, string>{{"foo", "bar"}});
162159
cmd.Parameters.AddWithValue("geopoint", new List<double>{85.43, 66.23});
163160
// TODO: Check if `GEO_SHAPE` types can be represented by real .NET or Npgsql data types.
164161
cmd.Parameters.AddWithValue("geoshape", "POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))");
@@ -185,20 +182,22 @@ INSERT INTO testdrive.example (
185182

186183
}
187184

188-
public static async Task<DataTable> ContainerTypesExample(NpgsqlConnection conn)
185+
public static async Task ProvisionPoco(NpgsqlConnection conn)
189186
{
190-
Console.WriteLine("Running AllTypesExample");
187+
/***
188+
* Verify Npgsql POCO mapping with CrateDB.
189+
* https://www.npgsql.org/doc/types/json.html#poco-mapping
190+
*/
191191

192192
// Submit DDL, create database schema.
193-
await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS testdrive.container", conn))
193+
await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS testdrive.poco", conn))
194194
{
195195
cmd.ExecuteNonQuery();
196196
}
197197

198198
await using (var cmd = new NpgsqlCommand("""
199-
CREATE TABLE testdrive.container (
200-
-- Container types
201-
"array" ARRAY(STRING),
199+
CREATE TABLE testdrive.poco (
200+
"array" ARRAY(OBJECT(DYNAMIC)),
202201
"object" OBJECT(DYNAMIC)
203202
);
204203
""", conn))
@@ -208,7 +207,7 @@ CREATE TABLE testdrive.container (
208207

209208
// Insert single data point.
210209
await using (var cmd = new NpgsqlCommand("""
211-
INSERT INTO testdrive.container (
210+
INSERT INTO testdrive.poco (
212211
"array",
213212
"object"
214213
) VALUES (
@@ -217,32 +216,58 @@ INSERT INTO testdrive.container (
217216
);
218217
""", conn))
219218
{
220-
Console.WriteLine(cmd);
221-
// FIXME: While doing conversations with ARRAY types works natively,
222-
// it doesn't work for OBJECT types.
223-
// Yet, they can be submitted as STRING in JSON format.
224-
cmd.Parameters.AddWithValue("array", new List<string>{"foo", "bar"});
225-
cmd.Parameters.AddWithValue("object", """{"foo": "bar"}""");
219+
cmd.Parameters.AddWithValue("object", NpgsqlDbType.Json, new BasicPoco { name = "Hotzenplotz" });
220+
cmd.Parameters.AddWithValue("array", NpgsqlDbType.Json, new List<BasicPoco>
221+
{
222+
new BasicPoco { name = "Hotzenplotz" },
223+
new BasicPoco { name = "Petrosilius", age = 42 },
224+
});
226225
cmd.ExecuteNonQuery();
227226
}
228227

229228
// Flush data.
230-
await using (var cmd = new NpgsqlCommand("REFRESH TABLE testdrive.container", conn))
229+
await using (var cmd = new NpgsqlCommand("REFRESH TABLE testdrive.poco", conn))
231230
{
232231
cmd.ExecuteNonQuery();
233232
}
234233

234+
}
235+
236+
public static async Task<BasicPoco> ObjectPocoExample(NpgsqlConnection conn)
237+
{
238+
Console.WriteLine("Running ObjectPocoExample");
239+
240+
// Provision data.
241+
await ProvisionPoco(conn);
242+
235243
// Query back data.
236-
await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.container", conn))
244+
await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.poco", conn))
237245
await using (var reader = cmd.ExecuteReader())
238246
{
239-
var dataTable = new DataTable();
240-
dataTable.Load(reader);
241-
var payload = JsonConvert.SerializeObject(dataTable);
242-
Console.WriteLine(payload);
243-
return (DataTable) dataTable;
247+
reader.Read();
248+
var obj = reader.GetFieldValue<BasicPoco>("object");
249+
Console.WriteLine(obj);
250+
return obj;
244251
}
252+
}
253+
254+
public static async Task<List<BasicPoco>> ObjectPocoArrayExample(NpgsqlConnection conn)
255+
{
256+
Console.WriteLine("Running ObjectPocoArrayExample");
245257

258+
// Provision data.
259+
await ProvisionPoco(conn);
260+
261+
// Query back data.
262+
await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.poco", conn))
263+
await using (var reader = cmd.ExecuteReader())
264+
{
265+
reader.Read();
266+
var obj = reader.GetFieldValue<List<BasicPoco>>("array");
267+
Console.WriteLine(obj[0]);
268+
Console.WriteLine(obj[1]);
269+
return obj;
270+
}
246271
}
247272

248273
}

by-language/csharp-npgsql/tests/DemoProgramTest.cs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ public DatabaseFixture()
2121
CRATEDB_DSN = $"Host=localhost;Port=5432;Username=crate;Password=;Database=testdrive";
2222
}
2323
Console.WriteLine($"Connecting to {CRATEDB_DSN}\n");
24-
Db = new NpgsqlConnection(CRATEDB_DSN);
25-
Db.Open();
24+
25+
var dataSourceBuilder = new NpgsqlDataSourceBuilder(CRATEDB_DSN);
26+
dataSourceBuilder.EnableDynamicJson();
27+
using var dataSource = dataSourceBuilder.Build();
28+
Db = dataSource.OpenConnection();
2629
}
2730

2831
public void Dispose()
@@ -83,12 +86,12 @@ public async Task TestUnnestExample()
8386
}
8487

8588
[Fact]
86-
public async Task TestAllTypesExample()
89+
public async Task TestAllTypesNativeExample()
8790
{
8891
var conn = fixture.Db;
8992

90-
// Invoke database workload.
91-
var task = DatabaseWorkloadsMore.AllTypesExample(conn);
93+
// Provision data.
94+
var task = DatabaseWorkloadsMore.AllTypesNativeExample(conn);
9295
var dt = await task.WaitAsync(TimeSpan.FromSeconds(0.5));
9396

9497
// Check results.
@@ -112,12 +115,18 @@ public async Task TestAllTypesExample()
112115
Assert.Equal("127.0.0.1", row["ip"]);
113116

114117
// Container types
115-
// FIXME: While doing conversations with ARRAY types works natively,
116-
// it doesn't work for OBJECT types.
117-
// Yet, they can be submitted as STRING in JSON format.
118118
Assert.Equal(new List<string>{"foo", "bar"}, row["array"]);
119119
Assert.Equal("""{"foo":"bar"}""", row["object"]);
120120

121+
// Note: While it works on the ingress side to communicate `Dictionary` types,
122+
// this kind of equality check does not work on the egress side,
123+
// presenting an error that indicates a different internal representation,
124+
// or a programming error ;].
125+
//
126+
// Expected: [["foo"] = "bar"]
127+
// Actual: {"foo":"bar"}
128+
// Assert.Equal(new Dictionary<string, string>{{"foo", "bar"}}, row["object"]);
129+
121130
// Geospatial types
122131
// TODO: Unlock native data types?
123132
// GEO_POINT and GEO_SHAPE types can be marshalled back and forth using STRING.
@@ -135,29 +144,21 @@ public async Task TestContainerTypesExample()
135144
{
136145
var conn = fixture.Db;
137146

138-
// Invoke database workload.
139-
var task = DatabaseWorkloadsMore.ContainerTypesExample(conn);
140-
var dt = await task.WaitAsync(TimeSpan.FromSeconds(0.5));
141-
142-
// Check results.
143-
var row = dt.Rows[0];
144-
// FIXME: While doing conversations with ARRAY types works natively,
145-
// it doesn't work for OBJECT types.
146-
// Yet, they can be submitted as STRING in JSON format.
147-
Assert.Equal(new List<string>{"foo", "bar"}, row["array"]);
148-
Assert.Equal("""{"foo":"bar"}""", row["object"]);
147+
// Provision data.
148+
var task = DatabaseWorkloadsMore.AllTypesNativeExample(conn);
149+
await task.WaitAsync(TimeSpan.FromSeconds(0.5));
149150

150-
// Run a special query indexing into ARRAY types.
151-
await using (var cmd = new NpgsqlCommand("""SELECT "array[2]" AS foo FROM testdrive.container""", conn))
151+
// Run an SQL query indexing into ARRAY types.
152+
await using (var cmd = new NpgsqlCommand("""SELECT "array[2]" AS foo FROM testdrive.example""", conn))
152153
await using (var reader = cmd.ExecuteReader())
153154
{
154155
var dataTable = new DataTable();
155156
dataTable.Load(reader);
156157
Assert.Equal("bar", dataTable.Rows[0]["foo"]);
157158
}
158159

159-
// Run a special query indexing into OBJECT types.
160-
await using (var cmd = new NpgsqlCommand("""SELECT "object['foo']" AS foo FROM testdrive.container""", conn))
160+
// Run an SQL query indexing into OBJECT types.
161+
await using (var cmd = new NpgsqlCommand("""SELECT "object['foo']" AS foo FROM testdrive.example""", conn))
161162
await using (var reader = cmd.ExecuteReader())
162163
{
163164
var dataTable = new DataTable();
@@ -167,5 +168,38 @@ public async Task TestContainerTypesExample()
167168

168169
}
169170

171+
[Fact]
172+
public async Task TestObjectPocoExample()
173+
{
174+
var conn = fixture.Db;
175+
176+
// Invoke database workload.
177+
var task = DatabaseWorkloadsMore.ObjectPocoExample(conn);
178+
var obj = await task.WaitAsync(TimeSpan.FromSeconds(0.5));
179+
180+
// Validate the outcome.
181+
Assert.Equal(new BasicPoco { name = "Hotzenplotz" }, obj);
182+
183+
}
184+
185+
[Fact]
186+
public async Task TestObjectPocoArrayExample()
187+
{
188+
var conn = fixture.Db;
189+
190+
// Invoke database workload.
191+
var task = DatabaseWorkloadsMore.ObjectPocoArrayExample(conn);
192+
var obj = await task.WaitAsync(TimeSpan.FromSeconds(0.5));
193+
194+
// Validate the outcome.
195+
var reference = new List<BasicPoco>
196+
{
197+
new BasicPoco { name = "Hotzenplotz" },
198+
new BasicPoco { name = "Petrosilius", age = 42 },
199+
};
200+
Assert.Equal(reference, obj);
201+
202+
}
203+
170204
}
171205
}

0 commit comments

Comments
 (0)