Skip to content

Support tinyint and time columns in synchronization - #20

Merged
adospace merged 2 commits into
mainfrom
feature/byte-timespan-support
Sep 16, 2026
Merged

adospace merged 2 commits into
mainfrom
feature/byte-timespan-support

Conversation

@adospace

Copy link
Copy Markdown
Owner

Why

Synchronizing a SQL Server tinyint or time column fails on the server with
NotSupportedException: Type of value ('System.Byte') is not supported for synchronization
(and the same for System.TimeSpan). MySQL TIME and PostgreSQL time/interval columns hit the same limitation.

What changes

Nothing on the wire: no new SyncItemValueType members, so peers on older versions keep working.

  • Value normalization (SyncItemValue): byte, sbyte and ushort travel as Int32, uint as Int64, char as a one-character String, and TimeSpan as an invariant "c" formatted String, which is also how Microsoft.Data.Sqlite and EF Core store a TimeSpan.
  • SQL Server / SqlServerCT: apply side converts the string back to TimeSpan for time and narrows to byte for tinyint. A time outside 00:00:00 to 23:59:59.9999999 is rejected with a clear message instead of an opaque SqlClient error or silent truncation.
  • PostgreSQL: time and interval columns are read as TimeSpan (Npgsql defaults time to TimeOnly, which cannot be synchronized) and applied back from the string form. timetz and intervals carrying months remain unsupported.
  • MySQL: string back to TimeSpan for TIME columns; every TINYINT flavour maps to a carryable type.
  • SQLite: nullable columns are read through the typed getter of their underlying type, so a byte? or TimeSpan? uploads the same shape the server sends down. If a legacy stored value cannot be parsed, the reader falls back to the raw value and logs a warning.

Tests

New ColumnTypeMappingTests (30 tests):

  • SQL Server, SqlServerCT, MySQL and PostgreSQL round trips against SQLite, direct and through HTTP in JSON and binary modes, with null and non-null rows and sub-second times.
  • Old-client payloads accepted by every server provider, and new payloads decodable by the old client conversion.
  • Time range edges and rejection, rounding into a lower-precision time column, MySQL tinyint flavours, PostgreSQL unsupported types, SQLite nullable and legacy reads, and a guard that SyncItemValueType gains no members.

Full suite run locally against SQL Server 2022, MySQL 8.4 and PostgreSQL 16 (Docker): all 155 existing tests still pass, and all 30 column-mapping tests pass.

A table holding a SQL Server tinyint or time column could not be synchronized at
all: SyncItemValue.DetectTypeOfObject threw NotSupportedException as soon as a
provider read one, because System.Byte and System.TimeSpan have no
SyncItemValueType.

Rather than add enum members - which an older peer would reject on both the
SyncProviderHttpClient and the SyncAgentController side - normalize the value to
a type that already has one, so the wire format is unchanged:

- byte, sbyte and ushort become Int32, uint becomes Int64;
- TimeSpan becomes an invariant "c" formatted string, the same representation
  Microsoft.Data.Sqlite and EF Core use to store a TimeSpan (ticks must never be
  used: Microsoft.Data.Sqlite reads an integer into a TimeSpan as days);
- char becomes a single character string, which the SQLite provider could
  already produce but nothing could carry.

Apply side:

- SqlServer/SqlServerCT ConvertToSqlType narrows to byte for SqlDbType.TinyInt
  (also covering the boolean a MySql TINYINT(1) yields) and parses a string back
  into a TimeSpan for SqlDbType.Time;
- PostgreSQL and MySql convert a string back into a TimeSpan for their time and
  interval columns;
- SqliteSyncProvider.GetValueFromRecord reads a nullable property through its
  underlying type and reads a TimeSpan explicitly, so a byte?/TimeSpan?/DateTime?
  column no longer falls through to GetValue() and is sent with the same
  SyncItemValueType the server sends down for the very same column.
- SqlServer and SqlServerCT: reject a time outside 00:00:00 to 23:59:59.9999999
  with a clear error, instead of an opaque SqlClient failure or silent truncation.
- PostgreSQL: read time and interval columns as TimeSpan, since Npgsql defaults
  time to TimeOnly, which cannot be synchronized. timetz and intervals carrying
  months remain unsupported and are documented as such.
- SQLite: when a typed getter cannot parse a legacy value in a nullable column,
  fall back to the raw stored value and log a warning.
- Tests: SQL Server, SqlServerCT, MySQL and PostgreSQL round trips against SQLite
  (direct, HTTP JSON, HTTP binary); old-client payloads accepted by every server
  provider; time range edges and rounding; MySQL tinyint flavours; SQLite
  nullable and legacy reads.
@adospace
adospace merged commit a8fcc45 into main Sep 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant