Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Npgsql: Use EnableDynamicJson to unlock better container type mappings #777

Merged
merged 4 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions by-language/csharp-npgsql/BasicPoco.cs
Original file line number Diff line number Diff line change
@@ -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)

Check warning on line 9 in by-language/csharp-npgsql/BasicPoco.cs

View workflow job for this annotation

GitHub Actions / .NET: 8.0.x Npgsql: 8.0.6 CrateDB: nightly on ubuntu-22.04

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).

Check warning on line 9 in by-language/csharp-npgsql/BasicPoco.cs

View workflow job for this annotation

GitHub Actions / .NET: 8.0.x Npgsql: 9.0.2 CrateDB: nightly on ubuntu-22.04

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).

Check warning on line 9 in by-language/csharp-npgsql/BasicPoco.cs

View workflow job for this annotation

GitHub Actions / .NET: 9.0.x Npgsql: 8.0.6 CrateDB: nightly on ubuntu-22.04

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).

Check warning on line 9 in by-language/csharp-npgsql/BasicPoco.cs

View workflow job for this annotation

GitHub Actions / .NET: 9.0.x Npgsql: 9.0.2 CrateDB: nightly on ubuntu-22.04

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).
{
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;

}
16 changes: 13 additions & 3 deletions by-language/csharp-npgsql/DemoProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@ await Parser.Default.ParseArguments<Options>(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();
});

Expand Down
169 changes: 130 additions & 39 deletions by-language/csharp-npgsql/DemoTypes.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -54,12 +53,19 @@ public class AllTypesRecord
public IList<string>? FloatVector { get; set; }
}

public class DatabaseWorkloadsMore
public class DatabaseWorkloadsTypes
{

public static async Task<DataTable> 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))
Expand Down Expand Up @@ -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,
Expand All @@ -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("""
Expand Down Expand Up @@ -154,23 +166,35 @@ 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<string>{"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<string, string>(){{"foo", "bar"}});
cmd.Parameters.AddWithValue("object", """{"foo": "bar"}""");
cmd.Parameters.AddWithValue("array", NpgsqlDbType.Json, new List<string>{"foo", "bar"});
cmd.Parameters.AddWithValue("object", NpgsqlDbType.Json, new Dictionary<string, string>{{"foo", "bar"}});
cmd.Parameters.AddWithValue("geopoint", new List<double>{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))");
cmd.Parameters.AddWithValue("float_vector", new List<double> {1.1, 2.2, 3.3});
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<DataTable> 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))
Expand All @@ -185,64 +209,131 @@ INSERT INTO testdrive.example (

}

public static async Task<DataTable> ContainerTypesExample(NpgsqlConnection conn)
public async Task<JsonDocument> 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<JsonDocument>("object");
Console.WriteLine(obj);
return obj;
}
}

public async Task<List<JsonDocument>> 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<List<JsonDocument>>("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,
@object
);
""", 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<string>{"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<BasicPoco>
{
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<BasicPoco> 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<BasicPoco>("object");
Console.WriteLine(obj);
return obj;
}
}

public async Task<List<BasicPoco>> 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<List<BasicPoco>>("array_object");
Console.WriteLine(obj[0]);
Console.WriteLine(obj[1]);
return obj;
}

}

}
Expand Down
25 changes: 13 additions & 12 deletions by-language/csharp-npgsql/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
kneth marked this conversation as resolved.
Show resolved Hide resolved
using Npgsql 8.x and 9.x.


*****
Expand All @@ -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
Expand All @@ -82,7 +83,7 @@ If you observe an error like this when invoking the program or test case::

please adjust ``demo.csproj`` like that::

- <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
- <TargetFramework>net$(NETCoreAppMaximumVersion)</TargetFramework>
+ <TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>


Expand Down
Loading