0

I'm trying to build an IN clause for array of Guids for MySql query. Guid columns are represented as binary(16) in the DB. According to docs and answers here I should be able to do something like

var arrayOfGuidsFromDb = ...;

await dbconn.ExecuteAsync<T>("UPDATE ...
SET ...
WHERE SomeGuidField IN @Ids",
new { Ids = arrayOfGuidsFromDb }

I'm also using this Guid converter

class MySqlGuidTypeHandler : SqlMapper.TypeHandler<Guid>
{
    public override void SetValue(IDbDataParameter parameter, Guid guid) => parameter.Value = guid.ToByteArray();

    public override Guid Parse(object value) => new Guid((byte[])value);
}

The issue with MySql though is that it tries (by default) to optimize GUID layout in the DB by rearranging the timestamp part of the GUID value. I decided not to change this behavior, it is working fine for reads/writes and conditions like WHERE SomeGuidField = @SomeGuid but for the IN statement in the question it matches 0 results. I was able to write this hack instead

guids.Select(guid => $"uuid_to_bin('{RotateToMatchInDbGuid(guid).ToString()}')")

where I convert each guid to a string and then string.Join(','... them for the IN clause, the helper method:

static Guid RotateToMatchInDbGuid(Guid source)
    {
        Span<byte> result = stackalloc byte[16];
        source.TryWriteBytes(result);
        Swap(result, 0, 3);
        Swap(result, 1, 2);
        Swap(result, 4, 5);
        Swap(result, 6, 7);
        return new Guid(result);
    }

This obviously doesn't look and feel right. Am I doing something wrong or is there some setting missing that I should enable to make Dapper behavior consistent for both = and IN GUID conditions?

Full code:

Guid[] guids = await dbConn.QueryAsync("SELECT Id FROM SomeTable"); //returns 1 row

// query using IN clause and array param:
var usingIn = await dbConn.QueryAsync("SELECT * From SomeTable WHERE Id IN @Ids", new { Ids = guids}); // returns 0 rows, should be 1

// now query using the `=` operator and same param but as a single value
var usingEquals = await dbConn.QueryAsync("SELECT * From SomeTable WHERE Id = @Id", new { Id = guids.First() }); // returns 1 row as expected

// query using array as CSV and no params
var usingCSV = await dbConn.QueryAsync($"SELECT * From SomeTable WHERE Id IN ({BuildCsv(guids)})"); // also returns 1 row as expected
5
  • I suggest switching to nuget.org/packages/MySqlConnector and using its GuidFormat=TimeSwapBinary16 connection string option (mysqlconnector.net/connection-options). That way, you shouldn't need the custom MySqlGuidTypeHandler but the MySqlConnector ADO.NET library will serialize them (efficiently) on the wire whenever you use a Guid as a MySqlCommand parameter value (either directly, or generated by Dapper). Commented May 26, 2020 at 23:40
  • thanks but I already have quite a lot of code written using Dapper and it's fine, the only issue I'm having is this one when I started adding IN clauses Commented May 27, 2020 at 8:52
  • Dapper works great with MySqlConnector; there would be no need to change any of your existing code. Just swap out the MySQL ADO.NET library and let it handle the serialization of Guid objects on the wire. Commented May 27, 2020 at 11:37
  • note: guids as bytes are super awkward in databases because there are multiple mutually exclusive layouts; no-one agrees what it should be Commented May 27, 2020 at 16:51
  • @MarcGravell I understand, my concern is that the same guid param is treated differently by Dapper depending on whether it's used in IN (not rotated) or in = (rotated) clause if I'm not mistaken Commented May 28, 2020 at 10:26

1 Answer 1

0

Parameterization of an IN clause requires one parameter per IN'd value; you do not pass a CSV string to a single parameter IN and hope it will figure it out

//no
Query("SELECT * FROM person WHERE name IN @names", new { names = "John,Jane,Mary" })

//yes
Query("SELECT * FROM person WHERE name IN (@name1,@name2,@name3)", new { name1 = "John", name2="Jane", name3="Mary" })

The first form will only find someone whose name literally is "John,Jane,Mary"

You might be able to use FIND IN SET:

Query("SELECT * FROM person WHERE FIND_IN_SET(name, @names) > 0", new { names = "John,Jane,Mary" })

But it's a bit lame..

Sign up to request clarification or add additional context in comments.

5 Comments

the arrayOfGuidsFromDb variable in my questions is actually an array of Guids Guid[] fetched from DB using Dapper and it should(?) work stackoverflow.com/questions/17150542/… , the CSV hack was added because it did not
I think you might be confusing SQL IN with dapper's iterating an array and repeatedly running the same query, once for each array value? That is to say, if you made the query ("SELECT * FROM t WHERE x IN (@p)", arrayOfX) with 5 elements in the array, Dapper will run the query 5 times, one element per execution (and IN works with a list of 1, i.e. this is valid SQL :SELECT * FROM Person WHERE name IN ('Jon') which is the same as SELECT * FROM Person WHERE Name = 'Jon') - Dapper doesnt do some funky thing to make the query into x IN (@p1,@p2,@p3,@p4,@p5) and put one element into each @pX
oh, that's unfortunate, I was sure it would be doing IN (@p1,@p2,@p3,@p4,@p5) so this means my CSV hack is actually a best solution if I want to do a single query instead of N for N elements?
Actually, I recant that last comment: stackoverflow.com/questions/8388093/… seems to indicate that if you use IN without brackets then Dapper will convert Query("... id IN @someList", myArrayOf3Ids) into the equivalent of Query("... id IN (@p1,@p2,@p3)", new { p1 = myArrayOf3Ids[0], p2 = myArrayOf3Ids[1], p3 = myArrayOf3Ids[2] } ), the number of @pX generated being equal to the number of elements in the array
Be careful that any CSV hack you come up woth doesn'y concatenate values into an SQL string. Always concat parameters and then give the parameters the values. I can't currently explain why the advice given in stackoverflow.com/questions/8388093/… indicates that your second query should work, but doesn't. It also gives an advice for using a table valued parameter - maybe give it a try and see if it works out?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.