question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Firebird queries do not work with CHAR(x) CHARACTER SET OCTETS type

See original GitHub issue

I am trying to store some hex data in a database. There are numerous fields, but for my simple test case let’s look at a MAC_ADDRESS field.

CREATE TABLE FOO (
    MAC_ADDRESS  CHAR(6) CHARACTER SET OCTETS NOT NULL
);

A human readable MAC is hex formatted - something like “FF:FF:FF:FF:FF:FF”. This can be stored as 6 bytes, each a value of 255. I am storing this data.

No combination of field definitions work for both selecting and insert/update on the table. I’ve tried char[], byte[], string, everything.

In the attached example, I am using a string, however as stated I have tried byte[] and other options. Either selection fails or update/insert fails, or both. Never do they all work. Code.zip

Environment details

linq2db version: 1.8.2
Database Server: Firebird 2.5.2
Database Provider: Firebird .Net Provider 5.9.1.0
Operating system: * Windows 10*
Framework version: .NET Framework 4.5.2

Issue Analytics

  • State:open
  • Created 6 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
cantakcommented, Jul 3, 2017

Yes. I have a table with the following fields:

MAC_ADDRESS       CHAR(6) CHARACTER SET OCTETS NOT NULL
IP_V4_ADDRESS     CHAR(4) CHARACTER SET OCTETS
IP_V6_ADDRESS     CHAR(16) CHARACTER SET OCTETS
PRODUCT_VERSION   CHAR(1) CHARACTER SET OCTETS NOT NULL

As you can see, I store byte values and not human readable values. I handle this using LinqToDb with a class and some type converters. But I have also performed a test against all these fields using the Firebird Provider and I will post all results here.

First off, here’s one of my classes:

    public class IpV4Address {
      private static readonly byte[] EMPTY_ADDRESS = new byte[] { 0, 0, 0, 0 };

      /// <summary>
      /// Stores the IP address as a string - not human readable. Needed for LinqToDb's data reads.
      /// </summary>
      public string Field {
         get { return (Value.Length == 4 ? Encoding.ASCII.GetString(Value) : null); }
         set { Value = (value?.Length == 4 ? Encoding.ASCII.GetBytes(value) : EMPTY_ADDRESS); }
      }

      /// <summary>
      /// IP address stored as a byte array. Needed for LinqToDb's inserts and updates.
      /// </summary>
      public byte[] Value { get; set; }

      /// <summary>
      /// IP Address of the machine in a human readable format.
      /// </summary>
      public string Display {
         get { return Value?.ToIpV4Format(); }
         set { Value = value?.ToIpV4Storage() ?? EMPTY_ADDRESS; }
      }

      public override string ToString() {
         return Display;
      }

      public override bool Equals(object obj) {
         return (obj as IpV4Address)?.Field == Field;
      }

      public override int GetHashCode() {
         return Field.GetHashCode();
      }
    }

To use this, I have some extension methods:

      /// <summary>
      /// Formats an IP4 address from bytes to readable display
      /// </summary>
      public static string ToIpV4Format(this byte[] bytes) {

         if (bytes?.Length == 4) {
            return string.Join(".", bytes.Select(o => Convert.ToInt16(o)));
         }

         return null;
      }

      /// <summary>
      /// Takes an IP V4 address from standard display and formats it for database storage.
      /// </summary>
      /// <param name="s"></param>
      /// <returns></returns>
      public static byte[] ToIpV4Storage(this string s) {

         // Ensure we have a value or we cannot split
         if (!string.IsNullOrWhiteSpace(s)) {
            var chunks = s.Split('.');

            // We expect 4 here
            if (chunks.Length == 4) {

               // Start up a final result. If anything here fails to convert, we will bail.
               var result = new byte[] { 0, 0, 0, 0 };

               // Loop and assign
               for (var counter = 0; counter < 4; counter++) {
                  int i;
                  if ((int.TryParse(chunks[counter], out i)) && (i >= 0) && (i <= 255)) {
                     result[counter] = Convert.ToByte(i);
                  }
                  else {
                     return null;
                  }
               }

               return result;
            }
         }

         return null;
      }

And here are my mappings:

         // LinqToDB default mappints - IpV4Address. Not all of these may be needed.
         MappingSchema.Default.SetConverter<IpV4Address, DataParameter>(o => new DataParameter { DataType = LinqToDB.DataType.Char, Value = o?.Value });
         MappingSchema.Default.SetConverter<DataParameter, IpV4Address>(o => new IpV4Address { Display = o.Value?.ToString() as string });
         MappingSchema.Default.SetConverter<string, IpV4Address>(o => new IpV4Address { Display = o });
         MappingSchema.Default.SetConverter<IpV4Address, string>(o => o.Display);
         MappingSchema.Default.SetConverter<byte[], IpV4Address>(o => new IpV4Address { Value = o });
         MappingSchema.Default.SetConverter<IpV4Address, byte[]>(o => o.Value);

Next, here’s what happens when I use the Firebird .Net provider. I’m current to 5.9.1.0.

         using (var conn = new FbConnection(cs)) {
            conn.Open();
            using (var cmd = new FbCommand("select mac_address, " +             // char(6)  character set octets
                                                  "ip_v4_address, " +           // char(4)  character set octets
                                                  "ip_v6_address, " +           // char(16) character set octets
                                                  "product_version " +          // char(1)  character set octets
                                           "from profitnet_computers", conn)) {

               using (var reader = cmd.ExecuteReader()) {
                  if (reader.Read()) {

                     // Mac address - 6 bytes
                     byte[] macBytes = new byte[6];
                     reader.GetBytes(0, 0, macBytes, 0, 6);                            // Returns a 6 byte array as expected
                     var macType = reader.GetFieldType(0);                             // {Name = "String" FullName = "System.String"}
                     var macTypeFromProvider = reader.GetProviderSpecificFieldType(0); // {Name = "String" FullName = "System.String"}
                     var macObjValue = reader.GetProviderSpecificValue(0);             // Returns a 6 byte array (same as GetBytes)
                     var macString = reader.GetString(0);                              // String of 6 characters with values matching byte array
                     var macValue = reader.GetValue(0);                                // Returns a 6 byte array (same as GetBytes)

                     using (var sr = new StreamReader(reader.GetStream(0))) {
                        var macStreamString = sr.ReadToEnd();                          // DO NOT USE - 5 character string using Unicode
                     }

                     // IP V4 address - 4 bytes
                     byte[] ip4Bytes = new byte[4];
                     reader.GetBytes(1, 0, ip4Bytes, 0, 4);                            // Returns a 6 byte array as expected
                     var ip4Type = reader.GetFieldType(1);                             // {Name = "String" FullName = "System.String"}
                     var ip4TypeFromProvider = reader.GetProviderSpecificFieldType(1); // {Name = "String" FullName = "System.String"}
                     var ip4ObjValue = reader.GetProviderSpecificValue(1);             // 4 byte array with correct values
                     var ip4String = reader.GetString(1);                              // String of 4 characters with values matching correct bytes
                     var ip4Value = reader.GetValue(1);                                // 4 byte array with correct values

                     using (var sr = new StreamReader(reader.GetStream(1))) {
                        var ip4StreamString = sr.ReadToEnd();                          // 4 character string that works in this case. Still would not use.
                     }

                     // IP V6 - 16 bytes
                     //byte[] ip6Bytes = new byte[16];
                     //reader.GetBytes(2, 0, ip6Bytes, 0, 16);                         // DO NOT USE - Cannot convert GUID to byte array
                     var ip6Type = reader.GetFieldType(2);                             // {Name = "Guid" FullName = "System.Guid"}
                     var ip6TypeFromProvider = reader.GetProviderSpecificFieldType(2); // {Name = "Guid" FullName = "System.Guid"}
                     var ip6ObjValue = reader.GetProviderSpecificValue(2);             // GUID with the 16 bytes in it
                     var ip6String = reader.GetString(2);                              // string represnting the guid
                     var ip6Value = reader.GetValue(2);                                // GUID with the 16 bytes in it
                     var ipv6Guid = reader.GetGuid(2);                                 // GUID with the 16 bytes in it

                     //using (var sr = new StreamReader(reader.GetStream(2))) {        // DO NOT USE - Cannot convert GUID to
                     //   var ip6StreamString = sr.ReadToEnd();                        // a byte array.
                     //}

                     // Product Version - 1 byte
                     byte[] pvBytes = new byte[1];
                     reader.GetBytes(3, 0, pvBytes, 0, 1);                             // Returns a byte array with 1 byte as expected
                     var pvType = reader.GetFieldType(3);                              // {Name = "String" FullName = "System.String"}
                     var pvTypeFromProvider = reader.GetProviderSpecificFieldType(3);  // {Name = "String" FullName = "System.String"}
                     var pvObjValue = reader.GetProviderSpecificValue(3);              // Single byte sized array matching GetBytes
                     var pvString = reader.GetString(3);                               // 1 character string as expected
                     var pvValue = reader.GetValue(3);                                 // Single byte sized array matching GetBytes
                     // var pvByte = reader.GetByte(3);                                // DO NOT USE - Even though this is only 1 byte this fails.

                     using (var sr = new StreamReader(reader.GetStream(3))) {
                        var pvStreamString = sr.ReadToEnd();                           // Works as expected however my value was under 127 so may fail for others
                     }

                  }
               }
            }
         }

I hope this helps some. I will add that I had to add extra mappings for my IP V6 class to convert to and from a GUID, etc.

0reactions
ilicommented, Jul 3, 2017

@cantak have @jack128’s solution solved your issue?

I’v took a closer look in to this case and faced with some problems: firebird client reads such field as string, I tried to insert ascii_char(50) || ascii_char(100) || ascii_char(150) || ascii_char(200) and then read it, and there are problems with byte converting.

How should I read such data directly (without using linq2db only with Firebird .Net Provider)?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Character Sets and Unicode in Firebird
After a short introduction to the world of Character Sets and Unicode, this session will show you how to bring it all to...
Read more >
3.5 Character Data Types
3.5 Character Data Types. For working with character data, Firebird has the fixed-length CHAR and the variable-length VARCHAR data types.
Read more >
How to use non-ascii character string literals in firebird sql ...
Firebird uses a connection character set to know how string values need to be encoded. The default is NONE which means that no...
Read more >
Firebird Character Sets and Collations
Character Set Languages Collation NONE All NONE OCTETS | BINARY All OCTETS ASCII English ASCII
Read more >
Expressions - Firebird 2.5 Language Reference
COLLATE clause, Clause applied to CHAR and VARCHAR types to specify the character-set-specific collation sequence to use in string comparisons.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found