Skip to content

Commit 2e5d127

Browse files
committed
Automatically UNHEX binary data. Fixes #816
1 parent b73d723 commit 2e5d127

File tree

3 files changed

+47
-20
lines changed

3 files changed

+47
-20
lines changed

docs/content/api/mysql-bulk-copy.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ Source columns that don't have an entry in `MySqlBulkCopy.ColumnMappings` will b
121121
(unless the `ColumnMappings` collection is empty, in which case all columns will be mapped
122122
one-to-one).
123123

124-
Columns containing binary data must be mapped using an expression that uses the `UNHEX` function.
124+
MySqlConnector will transmit all binary data as hex, so any expression that operates on it
125+
must decode it with the `UNHEX` function first. (This will be performed automatically if no
126+
`Expression` is specified, but will be necessary to specify manually for more complex expressions.)
125127

126128
### Examples
127129

@@ -137,10 +139,4 @@ new MySqlBulkCopyColumnMapping
137139
DestinationColumn = "@tmp",
138140
Expression = "SET column_value = @tmp * 2",
139141
},
140-
new MySqlBulkCopyColumnMapping
141-
{
142-
SourceOrdinal = 1,
143-
DestinationColumn = "@tmp2",
144-
Expression = "SET binary_column = UNHEX(@tmp2)",
145-
},
146142
```

src/MySqlConnector/MySql.Data.MySqlClient/MySqlBulkCopy.cs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Threading.Tasks;
1010
using MySql.Data.Types;
1111
using MySqlConnector.Core;
12+
using MySqlConnector.Logging;
1213
using MySqlConnector.Protocol;
1314
using MySqlConnector.Protocol.Serialization;
1415
using MySqlConnector.Utilities;
@@ -150,17 +151,18 @@ private async ValueTask WriteToServerAsync(IOBehavior ioBehavior, CancellationTo
150151
closeConnection = true;
151152
}
152153

153-
// if no user-supplied column mappings, compute them from the destination schema
154-
if (ColumnMappings.Count == 0)
154+
// merge column mappings with the destination schema
155+
var columnMappings = new List<MySqlBulkCopyColumnMapping>(ColumnMappings);
156+
var addDefaultMappings = columnMappings.Count == 0;
157+
using (var cmd = new MySqlCommand("select * from " + tableName + ";", m_connection, m_transaction))
158+
using (var reader = (MySqlDataReader) await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly, ioBehavior, cancellationToken).ConfigureAwait(false))
155159
{
156-
using var cmd = new MySqlCommand("select * from " + tableName + ";", m_connection, m_transaction);
157-
using var reader = (MySqlDataReader) await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly, ioBehavior, cancellationToken).ConfigureAwait(false);
158160
var schema = reader.GetColumnSchema();
159161
for (var i = 0; i < schema.Count; i++)
160162
{
161163
if (schema[i].DataTypeName == "BIT")
162164
{
163-
ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i, $"@col{i}", $"`{reader.GetName(i)}` = CAST(@col{i} AS UNSIGNED)"));
165+
AddColumnMapping(columnMappings, addDefaultMappings, i, reader.GetName(i), $"@`\uE002\bcol{i}`", $"%COL% = CAST(%VAR% AS UNSIGNED)");
164166
}
165167
else if (schema[i].DataTypeName == "YEAR")
166168
{
@@ -172,11 +174,11 @@ private async ValueTask WriteToServerAsync(IOBehavior ioBehavior, CancellationTo
172174
var type = schema[i].DataType;
173175
if (type == typeof(byte[]) || (type == typeof(Guid) && (m_connection.GuidFormat == MySqlGuidFormat.Binary16 || m_connection.GuidFormat == MySqlGuidFormat.LittleEndianBinary16 || m_connection.GuidFormat == MySqlGuidFormat.TimeSwapBinary16)))
174176
{
175-
ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i, $"@col{i}", $"`{reader.GetName(i)}` = UNHEX(@col{i})"));
177+
AddColumnMapping(columnMappings, addDefaultMappings, i, reader.GetName(i), $"@`\uE002\bcol{i}`", $"%COL% = UNHEX(%VAR%)");
176178
}
177-
else
179+
else if (addDefaultMappings)
178180
{
179-
ColumnMappings.Add(new MySqlBulkCopyColumnMapping(i, reader.GetName(i)));
181+
columnMappings.Add(new MySqlBulkCopyColumnMapping(i, reader.GetName(i)));
180182
}
181183
}
182184
}
@@ -185,9 +187,10 @@ private async ValueTask WriteToServerAsync(IOBehavior ioBehavior, CancellationTo
185187
// set columns and expressions from the column mappings
186188
for (var i = 0; i < m_valuesEnumerator!.FieldCount; i++)
187189
{
188-
var columnMapping = ColumnMappings.FirstOrDefault(x => x.SourceOrdinal == i);
190+
var columnMapping = columnMappings.FirstOrDefault(x => x.SourceOrdinal == i);
189191
if (columnMapping is null)
190192
{
193+
Log.Info("Ignoring column with SourceOrdinal {0}", i);
191194
bulkLoader.Columns.Add("@`\uE002\bignore`");
192195
}
193196
else
@@ -213,6 +216,28 @@ private async ValueTask WriteToServerAsync(IOBehavior ioBehavior, CancellationTo
213216
#endif
214217

215218
static string QuoteIdentifier(string identifier) => "`" + identifier.Replace("`", "``") + "`";
219+
220+
static void AddColumnMapping(List<MySqlBulkCopyColumnMapping> columnMappings, bool addDefaultMappings, int destinationOrdinal, string destinationColumn, string variableName, string expression)
221+
{
222+
expression = expression.Replace("%COL%", "`" + destinationColumn + "`").Replace("%VAR%", variableName);
223+
var columnMapping = columnMappings.FirstOrDefault(x => destinationColumn.Equals(x.DestinationColumn, StringComparison.OrdinalIgnoreCase));
224+
if (columnMapping is object)
225+
{
226+
if (columnMapping.Expression is object)
227+
{
228+
Log.Warn("Column mapping for SourceOrdinal {0}, DestinationColumn {1} already has Expression {2}", columnMapping.SourceOrdinal, columnMapping.DestinationColumn, columnMapping.Expression);
229+
}
230+
else
231+
{
232+
columnMappings.Remove(columnMapping);
233+
columnMappings.Add(new MySqlBulkCopyColumnMapping(columnMapping.SourceOrdinal, variableName, expression));
234+
}
235+
}
236+
else if (addDefaultMappings)
237+
{
238+
columnMappings.Add(new MySqlBulkCopyColumnMapping(destinationOrdinal, variableName, expression));
239+
}
240+
}
216241
}
217242

218243
internal async Task SendDataReaderAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
@@ -515,6 +540,7 @@ static bool WriteBytes(ReadOnlySpan<byte> value, Span<byte> output, out int byte
515540

516541
private static ReadOnlySpan<byte> EscapedNull => new byte[] { 0x5C, 0x4E };
517542
private static readonly char[] s_specialCharacters = new char[] { '\t', '\\', '\n' };
543+
private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(MySqlBulkCopy));
518544

519545
readonly MySqlConnection m_connection;
520546
readonly MySqlTransaction? m_transaction;

tests/SideBySide/BulkLoaderSync.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ public void BulkCopyColumnMappings()
793793
using var connection = new MySqlConnection(GetLocalConnectionString());
794794
connection.Open();
795795
using (var cmd = new MySqlCommand(@"drop table if exists bulk_copy_column_mapping;
796-
create table bulk_copy_column_mapping(intvalue int, `text` text);", connection))
796+
create table bulk_copy_column_mapping(intvalue int, `text` text, data blob);", connection))
797797
{
798798
cmd.ExecuteNonQuery();
799799
}
@@ -805,6 +805,7 @@ public void BulkCopyColumnMappings()
805805
{
806806
new MySqlBulkCopyColumnMapping(1, "@val", "intvalue = @val + 1"),
807807
new MySqlBulkCopyColumnMapping(3, "text"),
808+
new MySqlBulkCopyColumnMapping(4, "data"),
808809
},
809810
};
810811

@@ -816,12 +817,13 @@ public void BulkCopyColumnMappings()
816817
new DataColumn("c2", typeof(int)),
817818
new DataColumn("c3", typeof(string)),
818819
new DataColumn("c4", typeof(string)),
820+
new DataColumn("c5", typeof(byte[])),
819821
},
820822
Rows =
821823
{
822-
new object[] { 1, 100, "a", "A" },
823-
new object[] { 2, 200, "bb", "BB" },
824-
new object[] { 3, 300, "ccc", "CCC" },
824+
new object[] { 1, 100, "a", "A", new byte[] { 0x33, 0x30 } },
825+
new object[] { 2, 200, "bb", "BB", new byte[] { 0x33, 0x31 } },
826+
new object[] { 3, 300, "ccc", "CCC", new byte[] { 0x33, 0x32 } },
825827
}
826828
};
827829

@@ -831,12 +833,15 @@ public void BulkCopyColumnMappings()
831833
Assert.True(reader.Read());
832834
Assert.Equal(101, reader.GetValue(0));
833835
Assert.Equal("A", reader.GetValue(1));
836+
Assert.Equal(new byte[] { 0x33, 0x30 }, reader.GetValue(2));
834837
Assert.True(reader.Read());
835838
Assert.Equal(201, reader.GetValue(0));
836839
Assert.Equal("BB", reader.GetValue(1));
840+
Assert.Equal(new byte[] { 0x33, 0x31 }, reader.GetValue(2));
837841
Assert.True(reader.Read());
838842
Assert.Equal(301, reader.GetValue(0));
839843
Assert.Equal("CCC", reader.GetValue(1));
844+
Assert.Equal(new byte[] { 0x33, 0x32 }, reader.GetValue(2));
840845
Assert.False(reader.Read());
841846
}
842847
#endif

0 commit comments

Comments
 (0)