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

IDocumentOperations.QueueSqlCommand to insert into jsonb-column #2422

Closed
gfoidl opened this issue Dec 10, 2022 · 6 comments · Fixed by #2423
Closed

IDocumentOperations.QueueSqlCommand to insert into jsonb-column #2422

gfoidl opened this issue Dec 10, 2022 · 6 comments · Fixed by #2423

Comments

@gfoidl
Copy link
Contributor

gfoidl commented Dec 10, 2022

E.g.

// Setup
Table myTable = new("my_table");
myTable.AddColumn("data", "jsonb");

options.Storage.ExtendedSchemaObjects.Add(myTable);

Creates a table with a jsonb-column.

internal class MyProjection : IProjection
{
    public void Apply(IDocumentOperations operations, IReadOnlyList<StreamAction> streams)
    {
        foreach (IEvent @event in streams.SelectMany(static s => s.Events))
        {
            const string Sql = "insert into my_table (data) values (?);";

            // Use the object as parameter directly
            operations.QueueSqlCommand(Sql, @event.Data);
        }
    }
}

fails with

System.InvalidCastException: Can't write CLR type XYZ with handler type TextHandler
         at Npgsql.Internal.TypeHandlers.TextHandler.ValidateObjectAndGetLength(Object value, NpgsqlLengthCache& lengthCache, NpgsqlParameter parameter)
         at Npgsql.NpgsqlParameter.ValidateAndGetLength()
         at Npgsql.NpgsqlParameterCollection.ProcessParameters(TypeMapper typeMapper, Boolean validateValues, CommandType commandType)
         at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
         at Npgsql.NpgsqlCommand.ExecuteReader(CommandBehavior behavior, Boolean async, CancellationToken cancellationToken)
         at Marten.Internal.Sessions.QuerySession.ExecuteReaderAsync(NpgsqlCommand command, CancellationToken token)

Trying to insert the JSON as string like

internal class MyProjection : IProjection
{
    public void Apply(IDocumentOperations operations, IReadOnlyList<StreamAction> streams)
    {
        foreach (IEvent @event in streams.SelectMany(static s => s.Events))
        {
            const string Sql = "insert into my_table (data) values (?);";

            string json = JsonSerializer.Serialize(@event.Data);
            operations.QueueSqlCommand(Sql, json);
        }
    }
}

fails with

 Npgsql.PostgresException (0x80004005): 42804: column "data" is of type jsonb but expression is of type text

Is there any way to insert JSON into a jsonb-column via IDocumentOperations?

If not, then I believe the problem results from

var dbType = PostgresqlProvider.Instance.TryGetDbType(_parameterValues[i].GetType());
(at for the "insert json as string"-case), as the type is string, so it tries to insert via TextHandler.
If this gets changed to

                var dbType = PostgresqlProvider.Instance.TryGetDbType(_parameterValues[i].GetType());

                parameters[i].NpgsqlDbType = dbType != null
                    ? dbType.Value
                    : NpgsqlTypes.NpgsqlDbType.Jsonb;

then the object could be used a parameter directly, and NpgSql would care about serialization and the insert to the jsonb-column.

Could this have any downsides? Is there possibly a better way by getting the actual underlying db-column-type and set the NpgsqlDbType based on that?

A different approach could be to pass the DB-types as arguments to QueueSqlCommand (additional overload).

@mysticmind
Copy link
Member

mysticmind commented Dec 10, 2022

Use const string Sql = "insert into my_table (data) values (?::jsonb);"; to convert the json string value to jsonb using ::jsonb

Your code changes as below:

const string Sql = "insert into my_table (data) values (?::jsonb);";
string json = JsonSerializer.Serialize(@event.Data);
operations.QueueSqlCommand(Sql, json);

@gfoidl
Copy link
Contributor Author

gfoidl commented Dec 10, 2022

Fantastic 👍🏻 Thank you!

In Execute custom SQL in session a hint could be added that Postgres casting operator can be appended to parameters and that these casts aren't changed / removed by Marten. Then it would be ideal.

Something like

"?" placeholders can be used to denote parameter values. Postgres type casts :: can applied to the parameter if needed.

If that sounds good, I can do a PR for this.

@mysticmind
Copy link
Member

@gfoidl Sure, go ahead, we are happy to take a PR!

@mysticmind
Copy link
Member

mysticmind commented Dec 10, 2022

@gfoidl Quick note, the use case which you outlined is not a typical one which users would use especially a custom table with jsonb column (we haven't seen such usage so far).

@gfoidl
Copy link
Contributor Author

gfoidl commented Dec 10, 2022

Yep, I know. Normally the document store features are sufficient. I came accross this issue while playing around with the outbox pattern (and wanted to have the jsonb column, not a plain string / text column).

@mysticmind
Copy link
Member

Got you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants