diff --git a/by-language/csharp-npgsql/BasicPoco.cs b/by-language/csharp-npgsql/BasicPoco.cs new file mode 100644 index 00000000..bde7ff96 --- /dev/null +++ b/by-language/csharp-npgsql/BasicPoco.cs @@ -0,0 +1,22 @@ +namespace demo; + +public class BasicPoco +{ + + public string? name { get; set; } + public int? age { get; set; } + + public override bool Equals(object obj) + { + var other = (BasicPoco) obj; + return name == other.name && age == other.age; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override string ToString() => "Name: " + name + " Age: " + age; + +} diff --git a/by-language/csharp-npgsql/DemoProgram.cs b/by-language/csharp-npgsql/DemoProgram.cs index b0670c27..ec12a076 100644 --- a/by-language/csharp-npgsql/DemoProgram.cs +++ b/by-language/csharp-npgsql/DemoProgram.cs @@ -18,12 +18,22 @@ await Parser.Default.ParseArguments(args) var connString = $"Host={options.Host};Port={options.Port};SSL Mode={options.SslMode};" + $"Username={options.Username};Password={options.Password};Database={options.Database}"; Console.WriteLine($"Connecting to {connString}\n"); - await using var conn = new NpgsqlConnection(connString); - conn.Open(); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); + dataSourceBuilder.EnableDynamicJson(); + await using var dataSource = dataSourceBuilder.Build(); + await using var conn = dataSource.OpenConnection(); + await DatabaseWorkloads.SystemQueryExample(conn); await DatabaseWorkloads.BasicConversationExample(conn); await DatabaseWorkloads.UnnestExample(conn); - await DatabaseWorkloadsMore.AllTypesExample(conn); + + var dwt = new DatabaseWorkloadsTypes(conn); + await dwt.AllTypesNativeExample(); + await dwt.ObjectJsonDocumentExample(); + // await dwt.ArrayJsonDocumentExample(); + await dwt.ObjectPocoExample(); + await dwt.ArrayPocoExample(); conn.Close(); }); diff --git a/by-language/csharp-npgsql/DemoTypes.cs b/by-language/csharp-npgsql/DemoTypes.cs index 6f2ad062..641a3521 100644 --- a/by-language/csharp-npgsql/DemoTypes.cs +++ b/by-language/csharp-npgsql/DemoTypes.cs @@ -1,9 +1,8 @@ #nullable enable using System; -using System.Collections; using System.Collections.Generic; using System.Data; -using System.Reflection; +using System.Text.Json; using System.Threading.Tasks; using Newtonsoft.Json; using Npgsql; @@ -54,12 +53,19 @@ public class AllTypesRecord public IList? FloatVector { get; set; } } - public class DatabaseWorkloadsMore + public class DatabaseWorkloadsTypes { - public static async Task AllTypesExample(NpgsqlConnection conn) + public DatabaseWorkloadsTypes(NpgsqlConnection conn) { - Console.WriteLine("Running AllTypesExample"); + this.conn = conn; + } + + private NpgsqlConnection conn; + + public async Task CreateTable() + { + Console.WriteLine("Running CreateTable"); // Submit DDL, create database schema. await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS testdrive.example", conn)) @@ -87,6 +93,7 @@ char CHARACTER(5), -- Container types "array" ARRAY(STRING), "object" OBJECT(DYNAMIC), + "array_object" ARRAY(OBJECT(DYNAMIC)), -- Geospatial types geopoint GEO_POINT, geoshape GEO_SHAPE, @@ -97,6 +104,11 @@ float_vector FLOAT_VECTOR(3) { cmd.ExecuteNonQuery(); } + } + + public async Task InsertRecord() + { + Console.WriteLine("Running InsertRecord"); // Insert single data point. await using (var cmd = new NpgsqlCommand(""" @@ -154,11 +166,8 @@ INSERT INTO testdrive.example ( cmd.Parameters.AddWithValue("timestamp_tz", "1970-01-02T00:00:00+01:00"); cmd.Parameters.AddWithValue("timestamp_notz", "1970-01-02T00:00:00"); cmd.Parameters.AddWithValue("ip", "127.0.0.1"); - cmd.Parameters.AddWithValue("array", new List{"foo", "bar"}); - // FIXME: System.NotSupportedException: Cannot resolve 'hstore' to a fully qualified datatype name. The datatype was not found in the current database info. - // https://github.com/crate/zk/issues/26 - // cmd.Parameters.AddWithValue("object", new Dictionary(){{"foo", "bar"}}); - cmd.Parameters.AddWithValue("object", """{"foo": "bar"}"""); + cmd.Parameters.AddWithValue("array", NpgsqlDbType.Json, new List{"foo", "bar"}); + cmd.Parameters.AddWithValue("object", NpgsqlDbType.Json, new Dictionary{{"foo", "bar"}}); cmd.Parameters.AddWithValue("geopoint", new List{85.43, 66.23}); // TODO: Check if `GEO_SHAPE` types can be represented by real .NET or Npgsql data types. cmd.Parameters.AddWithValue("geoshape", "POLYGON ((5 5, 10 5, 10 10, 5 10, 5 5))"); @@ -166,11 +175,26 @@ INSERT INTO testdrive.example ( cmd.ExecuteNonQuery(); } + await RefreshTable(); + + } + + public async Task RefreshTable() + { // Flush data. await using (var cmd = new NpgsqlCommand("REFRESH TABLE testdrive.example", conn)) { cmd.ExecuteNonQuery(); } + } + + public async Task AllTypesNativeExample() + { + Console.WriteLine("Running AllTypesNativeExample"); + + // Provision data. + await CreateTable(); + await InsertRecord(); // Query back data. await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.example", conn)) @@ -185,31 +209,73 @@ INSERT INTO testdrive.example ( } - public static async Task ContainerTypesExample(NpgsqlConnection conn) + public async Task ObjectJsonDocumentExample() { - Console.WriteLine("Running AllTypesExample"); + Console.WriteLine("Running ObjectJsonDocumentExample"); - // Submit DDL, create database schema. - await using (var cmd = new NpgsqlCommand("DROP TABLE IF EXISTS testdrive.container", conn)) - { - cmd.ExecuteNonQuery(); - } + // Provision data. + await CreateTable(); await using (var cmd = new NpgsqlCommand(""" - CREATE TABLE testdrive.container ( - -- Container types - "array" ARRAY(STRING), - "object" OBJECT(DYNAMIC) - ); + INSERT INTO testdrive.example ( + "object" + ) VALUES ( + @object + ) """, conn)) { + cmd.Parameters.AddWithValue("object", NpgsqlDbType.Json, JsonDocument.Parse("""{"foo":"bar"}""")); cmd.ExecuteNonQuery(); } + // Flush data. + await RefreshTable(); + + // Query back data. + await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.example", conn)) + await using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + var obj = reader.GetFieldValue("object"); + Console.WriteLine(obj); + return obj; + } + } + + public async Task> ArrayJsonDocumentExample() + { + Console.WriteLine("Running ArrayJsonDocumentExample"); + + // Provision data. + await CreateTable(); + await InsertRecord(); + + // Query back data. + await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.example", conn)) + await using (var reader = cmd.ExecuteReader()) + { + reader.Read(); + // TODO: System.InvalidCastException: Reading as 'System.Text.Json.JsonDocument' or [1] + // is not supported for fields having DataTypeName 'character varying[]'. + // [1] `System.Collections.Generic.List`1[[System.Text.Json.JsonDocument]` + var obj = reader.GetFieldValue>("array"); + Console.WriteLine(obj); + return obj; + } + } + + public async Task InsertPoco() + { + /*** + * Verify Npgsql POCO mapping with CrateDB. + * https://www.npgsql.org/doc/types/json.html#poco-mapping + */ + Console.WriteLine("Running InsertPoco"); + // Insert single data point. await using (var cmd = new NpgsqlCommand(""" - INSERT INTO testdrive.container ( - "array", + INSERT INTO testdrive.example ( + "array_object", "object" ) VALUES ( @array, @@ -217,32 +283,57 @@ INSERT INTO testdrive.container ( ); """, conn)) { - Console.WriteLine(cmd); - // FIXME: While doing conversations with ARRAY types works natively, - // it doesn't work for OBJECT types. - // Yet, they can be submitted as STRING in JSON format. - cmd.Parameters.AddWithValue("array", new List{"foo", "bar"}); - cmd.Parameters.AddWithValue("object", """{"foo": "bar"}"""); + cmd.Parameters.AddWithValue("object", NpgsqlDbType.Json, new BasicPoco { name = "Hotzenplotz" }); + cmd.Parameters.AddWithValue("array", NpgsqlDbType.Json, new List + { + new BasicPoco { name = "Hotzenplotz" }, + new BasicPoco { name = "Petrosilius", age = 42 }, + }); cmd.ExecuteNonQuery(); } // Flush data. - await using (var cmd = new NpgsqlCommand("REFRESH TABLE testdrive.container", conn)) + await RefreshTable(); + + } + + public async Task ObjectPocoExample() + { + Console.WriteLine("Running ObjectPocoExample"); + + // Provision data. + await CreateTable(); + await InsertPoco(); + + // Query back data. + await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.example", conn)) + await using (var reader = cmd.ExecuteReader()) { - cmd.ExecuteNonQuery(); + reader.Read(); + var obj = reader.GetFieldValue("object"); + Console.WriteLine(obj); + return obj; } + } + + public async Task> ArrayPocoExample() + { + Console.WriteLine("Running ArrayPocoExample"); + + // Provision data. + await CreateTable(); + await InsertPoco(); // Query back data. - await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.container", conn)) + await using (var cmd = new NpgsqlCommand("SELECT * FROM testdrive.example", conn)) await using (var reader = cmd.ExecuteReader()) { - var dataTable = new DataTable(); - dataTable.Load(reader); - var payload = JsonConvert.SerializeObject(dataTable); - Console.WriteLine(payload); - return (DataTable) dataTable; + reader.Read(); + var obj = reader.GetFieldValue>("array_object"); + Console.WriteLine(obj[0]); + Console.WriteLine(obj[1]); + return obj; } - } } diff --git a/by-language/csharp-npgsql/README.rst b/by-language/csharp-npgsql/README.rst index ee47ee1b..82def11d 100644 --- a/by-language/csharp-npgsql/README.rst +++ b/by-language/csharp-npgsql/README.rst @@ -21,10 +21,8 @@ data provider for PostgreSQL, `crate-npgsql`_. CrateDB versions 4.2 and later work with the vanilla `Npgsql - .NET Access to PostgreSQL`_ driver without the need for a plugin. -Please note that Npgsql 5 is not supported starting with CrateDB 4.8.4, you -will need Npgsql 6 or newer. - -.NET 7, 8, and 9 are supported, .NET 3.1, 4.6, 5.0, and 6.0 may still work. +The example program in this folder is validated on .NET 8 and 9, +using Npgsql 8.x and 9.x. ***** @@ -37,38 +35,41 @@ To invoke a CrateDB instance for evaluation purposes, run:: Invoke example program:: - dotnet run --framework=net8.0 + dotnet run To connect to CrateDB Cloud, use a command like:: - dotnet run --framework=net8.0 -- \ + dotnet run -- \ --host=clustername.aks1.westeurope.azure.cratedb.net --ssl-mode=Require \ --username=foobar --password='X8F%Shn)TESTvF5ac7%eW4NM' Explore all available connection options:: - dotnet run --framework=net8.0 -- --help + dotnet run -- --help + +.. note:: + Use the ``--framework=net8.0`` option to target a specific .NET framework version. Tests ===== For running the test scenarios wrapped into a xUnit test suite, invoke:: - dotnet test --framework=net8.0 + dotnet test To generate a Cobertura code coverage report, run:: - dotnet test --framework=net8.0 --collect:"XPlat Code Coverage" + dotnet test --collect:"XPlat Code Coverage" For running the tests against a remote database, use, for example:: export CRATEDB_DSN='Host=clustername.aks1.westeurope.azure.cratedb.net;Port=5432;SSL Mode=Require;Username=foobar;Password=X8F%Shn)TESTvF5ac7%eW4NM;Database=testdrive' - dotnet test --framework=net8.0 + dotnet test For running tests selectively, use:: - dotnet test --framework=net8.0 --filter SystemQueryExample + dotnet test --filter SystemQueryExample Troubleshooting @@ -82,7 +83,7 @@ If you observe an error like this when invoking the program or test case:: please adjust ``demo.csproj`` like that:: -- net6.0;net8.0 +- net$(NETCoreAppMaximumVersion) + net6.0;net8.0;net9.0 diff --git a/by-language/csharp-npgsql/tests/DemoProgramTest.cs b/by-language/csharp-npgsql/tests/DemoProgramTest.cs index c0bfa08c..a84528e4 100644 --- a/by-language/csharp-npgsql/tests/DemoProgramTest.cs +++ b/by-language/csharp-npgsql/tests/DemoProgramTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Npgsql; using Xunit; @@ -21,8 +22,11 @@ public DatabaseFixture() CRATEDB_DSN = $"Host=localhost;Port=5432;Username=crate;Password=;Database=testdrive"; } Console.WriteLine($"Connecting to {CRATEDB_DSN}\n"); - Db = new NpgsqlConnection(CRATEDB_DSN); - Db.Open(); + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(CRATEDB_DSN); + dataSourceBuilder.EnableDynamicJson(); + using var dataSource = dataSourceBuilder.Build(); + Db = dataSource.OpenConnection(); } public void Dispose() @@ -83,12 +87,12 @@ public async Task TestUnnestExample() } [Fact] - public async Task TestAllTypesExample() + public async Task TestAllTypesNativeExample() { var conn = fixture.Db; - // Invoke database workload. - var task = DatabaseWorkloadsMore.AllTypesExample(conn); + // Provision data. + var task = new DatabaseWorkloadsTypes(conn).AllTypesNativeExample(); var dt = await task.WaitAsync(TimeSpan.FromSeconds(0.5)); // Check results. @@ -112,12 +116,18 @@ public async Task TestAllTypesExample() Assert.Equal("127.0.0.1", row["ip"]); // Container types - // FIXME: While doing conversations with ARRAY types works natively, - // it doesn't work for OBJECT types. - // Yet, they can be submitted as STRING in JSON format. Assert.Equal(new List{"foo", "bar"}, row["array"]); Assert.Equal("""{"foo":"bar"}""", row["object"]); + // Note: While it works on the ingress side to communicate `Dictionary` types, + // this kind of equality check does not work on the egress side, + // presenting an error that indicates a different internal representation, + // or a programming error ;]. + // + // Expected: [["foo"] = "bar"] + // Actual: {"foo":"bar"} + // Assert.Equal(new Dictionary{{"foo", "bar"}}, row["object"]); + // Geospatial types // TODO: Unlock native data types? // GEO_POINT and GEO_SHAPE types can be marshalled back and forth using STRING. @@ -135,20 +145,12 @@ public async Task TestContainerTypesExample() { var conn = fixture.Db; - // Invoke database workload. - var task = DatabaseWorkloadsMore.ContainerTypesExample(conn); - var dt = await task.WaitAsync(TimeSpan.FromSeconds(0.5)); - - // Check results. - var row = dt.Rows[0]; - // FIXME: While doing conversations with ARRAY types works natively, - // it doesn't work for OBJECT types. - // Yet, they can be submitted as STRING in JSON format. - Assert.Equal(new List{"foo", "bar"}, row["array"]); - Assert.Equal("""{"foo":"bar"}""", row["object"]); + // Provision data. + var task = new DatabaseWorkloadsTypes(conn).AllTypesNativeExample(); + await task.WaitAsync(TimeSpan.FromSeconds(0.5)); - // Run a special query indexing into ARRAY types. - await using (var cmd = new NpgsqlCommand("""SELECT "array[2]" AS foo FROM testdrive.container""", conn)) + // Run an SQL query indexing into ARRAY types. + await using (var cmd = new NpgsqlCommand("""SELECT "array[2]" AS foo FROM testdrive.example""", conn)) await using (var reader = cmd.ExecuteReader()) { var dataTable = new DataTable(); @@ -156,8 +158,8 @@ public async Task TestContainerTypesExample() Assert.Equal("bar", dataTable.Rows[0]["foo"]); } - // Run a special query indexing into OBJECT types. - await using (var cmd = new NpgsqlCommand("""SELECT "object['foo']" AS foo FROM testdrive.container""", conn)) + // Run an SQL query indexing into OBJECT types. + await using (var cmd = new NpgsqlCommand("""SELECT "object['foo']" AS foo FROM testdrive.example""", conn)) await using (var reader = cmd.ExecuteReader()) { var dataTable = new DataTable(); @@ -167,5 +169,51 @@ public async Task TestContainerTypesExample() } + [Fact] + public async Task TestObjectJsonDocumentExample() + { + var conn = fixture.Db; + + // Invoke database workload. + var task = new DatabaseWorkloadsTypes(conn).ObjectJsonDocumentExample(); + var obj = await task.WaitAsync(TimeSpan.FromSeconds(0.5)); + + // Validate the outcome. + Assert.Equal("""{"foo":"bar"}""", JsonSerializer.Serialize(obj)); + } + + [Fact] + public async Task TestObjectPocoExample() + { + var conn = fixture.Db; + + // Invoke database workload. + var task = new DatabaseWorkloadsTypes(conn).ObjectPocoExample(); + var obj = await task.WaitAsync(TimeSpan.FromSeconds(0.5)); + + // Validate the outcome. + Assert.Equal(new BasicPoco { name = "Hotzenplotz" }, obj); + + } + + [Fact] + public async Task TestArrayPocoExample() + { + var conn = fixture.Db; + + // Invoke database workload. + var task = new DatabaseWorkloadsTypes(conn).ArrayPocoExample(); + var obj = await task.WaitAsync(TimeSpan.FromSeconds(0.5)); + + // Validate the outcome. + var reference = new List + { + new BasicPoco { name = "Hotzenplotz" }, + new BasicPoco { name = "Petrosilius", age = 42 }, + }; + Assert.Equal(reference, obj); + + } + } }