Skip to content

Converting .NET Date Time Objects to MySQL‐Compliant SQL

adriancs edited this page Jul 13, 2025 · 6 revisions

Converting .NET Date/Time Objects to MySQL-Compliant SQL

Overview

Handling MySQL date and time data types in .NET applications involves complex conversions between MySQL's temporal types and .NET objects. This guide explains how MySqlBackup.NET handles these conversions correctly while preserving data integrity and precision.

The Core Challenge: Data Representation vs. Storage Format

When working with MySQL date/time data in .NET, there's a fundamental challenge that MySqlBackup.NET must address: the disconnect between how data is represented in .NET objects versus how it should be stored in MySQL.

The Translation Problem

When MySqlBackup.NET receives date/time data from MySQL through the .NET connector, it arrives as converted .NET objects (such as TimeSpan or DateTime). However, these objects don't always preserve the exact original MySQL format. For example, a MySQL TIME column might originally contain:

-- Original MySQL storage formats:
'00:00'           -- 2-digit format
'00:00:00'        -- 6-digit format  
'00:00:00.000'    -- With 3-digit fractional seconds
'00:00:00.000000' -- With 6-digit fractional seconds

But in .NET, all of these become the same TimeSpan object: TimeSpan.Zero. The original storage precision is lost during the MySQL → .NET conversion.

The ConvertZeroDateTime Complication

This problem is amplified when using ConvertZeroDateTime=true. MySQL's invalid dates like '0000-00-00' get automatically converted to DateTime.MinValue (0001-01-01), completely losing the original representation. While this makes .NET programming easier, it creates a data fidelity issue for backup tools.

Why This Matters for .NET Developers

If you're doing regular .NET programming (C#, VB.NET, etc.), this translation loss typically doesn't affect you because:

  • The .NET ecosystem doesn't use MySQL's zero dates anyway
  • We naturally use DateTime.MinValue in C# culture
  • Standard .NET applications don't need to preserve exact MySQL storage formats
  • The converted values work perfectly for business logic

Why This Matters for MySqlBackup.NET

However, since we're building a MySQL backup tool, it's crucial to maintain 100% MySQL compliance. Our goal is complete compatibility with mysqldump and MySQL command-line tools. This means:

  • ✅ Preserving exact fractional seconds precision (TIME(3) vs TIME(6))
  • ✅ Maintaining original zero date representations ('0000-00-00')
  • ✅ Ensuring restored data is byte-for-byte identical to the original
  • ✅ Supporting all MySQL temporal edge cases and special values

The Solution: Secondary Column Metadata Reference

To achieve this level of fidelity, MySqlBackup.NET performs a secondary reference to MySQL column metadata after receiving the .NET objects. This two-step process:

  1. First: Gets the data as .NET objects from MySqlConnector
  2. Second: Consults the original MySQL column definition (INFORMATION_SCHEMA) to determine:
    • Exact fractional seconds precision (TIME(6), DATETIME(3), etc.)
    • Column data type (DATE, TIME, DATETIME, TIMESTAMP)
    • Whether zero values should be preserved as MySQL zero dates

This approach allows MySqlBackup.NET to reconstruct the precise MySQL-compliant SQL format, ensuring that backup files are indistinguishable from those created by mysqldump.


Basic Concepts

Understanding the Challenge

MySQL and .NET represent time differently:

  • MySQL: Supports microsecond precision (6 decimal places) and special values like '0000-00-00'
  • .NET: Uses 100-nanosecond ticks (7 decimal places) and doesn't support invalid dates

Example of the precision difference:

// MySQL TIME(6): '14:30:45.123456' (6 decimal places)
// .NET TimeSpan: 522451234560 ticks (equivalent to 14:30:45.1234560)

Type Mapping Quick Reference

MySQL Type .NET Type Example Notes
DATE DateTime '2024-01-15' → new DateTime(2024,1,15) Time portion is 00:00:00
TIME TimeSpan '14:30:45' → TimeSpan.FromHours(14.5125) Can exceed 24 hours
TIME(6) TimeSpan '14:30:45.123456' Microsecond precision
DATETIME DateTime '2024-01-15 14:30:45' No timezone
DATETIME(6) DateTime '2024-01-15 14:30:45.123456' With microseconds
TIMESTAMP DateTime UTC-based, timezone-aware Converted to local time
YEAR Int32 2024 → 2024 Simple integer

The ConvertZeroDateTime Setting

What You Need to Know

MySQL allows invalid dates like '0000-00-00' that .NET cannot represent. The ConvertZeroDateTime connection string option controls how these are handled:

// Option 1: Convert zero dates to DateTime.MinValue (easier to use)
string connStr = "Server=localhost;Database=test;ConvertZeroDateTime=true;";

// Option 2: Keep as MySqlDateTime objects (preserves original data)
string connStr = "Server=localhost;Database=test;ConvertZeroDateTime=false;";

Impact on Your Data

Setting Zero Date Result Data Fidelity Ease of Use
true DateTime.MinValue (0001-01-01) ❌ Data changed ✅ Simple
false MySqlDateTime.IsValidDateTime = false ✅ Preserved ⚠️ Requires checking

For backup/restore operations, we recommend ConvertZeroDateTime=false to preserve exact data.

Code Examples

With ConvertZeroDateTime=true:

DateTime date = reader.GetDateTime("date_col");
// Zero dates become DateTime.MinValue automatically
if (date == DateTime.MinValue) 
{
    // This might be a converted zero date
}

With ConvertZeroDateTime=false:

object value = reader.GetValue("date_col");
if (value is MySqlDateTime mdt)
{
    if (mdt.IsValidDateTime)
    {
        DateTime date = mdt.GetDateTime(); // Safe conversion
    }
    else
    {
        // Handle zero date case - original MySQL '0000-00-00'
        Console.WriteLine("Invalid date found");
    }
}

How MySqlBackup.NET Preserves Precision

The MySqlColumn Detection System

MySqlBackup.NET automatically detects the precision of each column to ensure accurate data conversion:

// Column definitions MySqlBackup.NET can detect:
// "time"         → TimeFractionLength = 0 (no fractional seconds)
// "time(3)"      → TimeFractionLength = 3 (milliseconds)
// "time(6)"      → TimeFractionLength = 6 (microseconds)
// "datetime(4)"  → TimeFractionLength = 4 (0.1 milliseconds)

Why this matters:

  • Without precision detection: '14:30:45.123456' becomes '14:30:45' (data loss!)
  • With precision detection: '14:30:45.123456' stays '14:30:45.123456' (preserved!)

Real-World Example

Consider this MySQL table:

CREATE TABLE events (
    id INT PRIMARY KEY,
    quick_time TIME,              -- No fractional seconds
    precise_time TIME(6),         -- Microsecond precision
    event_datetime DATETIME(3)    -- Millisecond precision
);

INSERT INTO events VALUES 
(1, '14:30:45', '14:30:45.123456', '2024-01-15 14:30:45.123');

MySqlBackup.NET processes this as:

  • quick_time: Outputs '14:30:45' (no decimals)
  • precise_time: Outputs '14:30:45.123456' (6 decimals)
  • event_datetime: Outputs '2024-01-15 14:30:45.123' (3 decimals)

Complete Reference Tables

MySQL Date/Time Types

MySQL Type Range Precision Storage Description
DATE 1000-01-01 to 9999-12-31 Day 3 bytes Date only
TIME -838:59:59 to 838:59:59 Second 3 bytes Time only (can exceed 24 hours)
TIME(n) Same as TIME n fractional digits (0-6) 3-6 bytes Time with subsecond precision
DATETIME 1000-01-01 00:00:00 to 9999-12-31 23:59:59 Second 8 bytes Date and time
DATETIME(n) Same as DATETIME n fractional digits (0-6) 5-8 bytes DateTime with subsecond precision
TIMESTAMP 1970-01-01 00:00:01 UTC to 2038-01-19 03:14:07 UTC Second 4 bytes Timezone-aware timestamp
TIMESTAMP(n) Same as TIMESTAMP n fractional digits (0-6) 4-7 bytes Timestamp with subsecond precision
YEAR 1901 to 2155 Year 1 byte Year only

.NET Date/Time Types

.NET Type Range Precision Description
DateTime 0001-01-01 00:00:00 to 9999-12-31 23:59:59 100 nanoseconds (Ticks) Date and time
TimeSpan ±10675199.02:48:05.4775808 100 nanoseconds (Ticks) Duration/Time interval
MySqlDateTime MySQL-compatible Microseconds MySQL Connector specific type

Advanced: How the Conversion Works

Precision Detection Algorithm

MySqlBackup.NET uses this logic to detect fractional seconds precision:

public class MySqlColumn
{
    private int _timeFractionLength = 0;
    
    public MySqlColumn(string name, Type type, string mySqlDataType, /*...*/)
    {
        // Detect temporal types
        if (_dataType == typeof(DateTime) || _dataType == typeof(TimeSpan) ||
            _mySqlDataType.StartsWith("time") || _mySqlDataType.StartsWith("datetime") || 
            _mySqlDataType.StartsWith("timestamp"))
        {
            // Look for precision specifier: "time(6)" → length 7 > 4
            if (_mySqlDataType.Length > 4)
            {
                // Extract numeric characters: "time(6)" → "6"
                string fractionLength = string.Empty;
                foreach (char c in _mySqlDataType)
                {
                    if (char.IsNumber(c))
                        fractionLength += c;
                }

                // Parse precision: "6" → _timeFractionLength = 6
                if (fractionLength.Length > 0)
                {
                    int.TryParse(fractionLength, out _timeFractionLength);
                }
            }
        }
    }
}

SQL Format Conversion

The detected precision drives accurate SQL generation:

// TimeSpan (for TIME columns) - Actual implementation
if (ob is TimeSpan ts)
{
    // Format: HHH:MM:SS (preserves negative values, can exceed 24 hours)
    sb.Append(((int)ts.TotalHours).ToString().PadLeft(2, '0')); // Preserves sign
    sb.Append(":");
    sb.Append(ts.Duration().Minutes.ToString().PadLeft(2, '0')); // Absolute value
    sb.Append(":");
    sb.Append(ts.Duration().Seconds.ToString().PadLeft(2, '0')); // Absolute value

    // Add fractional seconds based on detected precision
    if (col?.TimeFractionLength > 0)
    {
        sb.Append(".");
        long totalMicroseconds = ts.Ticks / 10;
        long microsecondPart = totalMicroseconds % 1000000;
        sb.Append(microsecondPart.ToString().PadLeft(col.TimeFractionLength, '0'));
    }
}

// DateTime (for DATETIME/TIMESTAMP columns) - Actual implementation
else if (ob is DateTime dt)
{
    sb.Append(dt.ToString("yyyy-MM-dd HH:mm:ss"));

    // Add fractional seconds using format string generation
    if (col?.TimeFractionLength > 0)
    {
        sb.Append(".");
        // Builds format string like "ffffff" for 6-digit precision
        string formatString = "".PadLeft(col.TimeFractionLength, 'f');
        string microseconds = dt.ToString(formatString);
        sb.Append(microseconds);
    }
}

Testing Date/Time Handling

Create Test Data

-- Create test table with various precision levels
CREATE TABLE datetime_test (
    id INT PRIMARY KEY,
    date_col DATE,
    time_col TIME,
    time_3_col TIME(3),
    time_6_col TIME(6),
    datetime_col DATETIME,
    datetime_6_col DATETIME(6),
    timestamp_col TIMESTAMP,
    zero_date DATE,
    zero_datetime DATETIME
);

-- Insert test data including edge cases
INSERT INTO datetime_test VALUES 
(1, '2024-01-15', '14:30:45', '14:30:45.123', '14:30:45.123456',
 '2024-01-15 14:30:45', '2024-01-15 14:30:45.123456',
 '2024-01-15 14:30:45', '0000-00-00', '0000-00-00 00:00:00'),
(2, '2024-12-31', '838:59:59', '838:59:59.999', '838:59:59.999999',
 '9999-12-31 23:59:59', '9999-12-31 23:59:59.999999',
 '2038-01-19 03:14:07', '0000-00-00', '0000-00-00 00:00:00'),
(3, '1000-01-01', '-838:59:59', '-838:59:59.999', '-838:59:59.999999',
 '1000-01-01 00:00:00', '1000-01-01 00:00:00.000001',
 '1970-01-01 00:00:01', '0000-00-00', '0000-00-00 00:00:00');

Verification Script

public void VerifyDateTimeIntegrity()
{
    // Test with both ConvertZeroDateTime settings
    TestWithSetting(true);
    TestWithSetting(false);
}

private void TestWithSetting(bool convertZeroDateTime)
{
    string connStr = $"Server=localhost;Database=test;ConvertZeroDateTime={convertZeroDateTime};";
    
    using var connection = new MySqlConnection(connStr);
    connection.Open();
    
    using var command = connection.CreateCommand();
    command.CommandText = "SELECT * FROM datetime_test";
    
    using var reader = command.ExecuteReader();
    while (reader.Read())
    {
        // Verify each column maintains its precision and value
        VerifyColumn(reader, "time_6_col", 6); // Should have 6 decimal places
        VerifyColumn(reader, "datetime_6_col", 6); // Should have 6 decimal places
        VerifyZeroDates(reader, convertZeroDateTime);
    }
}

The Handling of MySQL Timestamp Values

Read more: Understanding MySQL TIMESTAMP Timezone Handling in MySqlBackup.NET

Clone this wiki locally