Timestamps, numbers, and names
Three rules determine whether a value arrives the way it was meant: values are bound by the type of
their column, not as text; a timestamp without a time zone appears over OData with a Z; and names
containing characters that OData doesn't allow are encoded. This page explains all three in one
place, and the reference pages link here.
Key features
- Dates, timestamps, amounts, and floating-point values arrive exactly, regardless of the language setting.
- Over OData, reading and writing timestamps are symmetric.
- Every name that SQL Server accepts can be reached, as long as its encoded form is no longer than 128 characters.
Core concepts
Values by column type
Values are bound by column type, not as text. A date in the request body reaches SQL Server as a
date. If it were sent as text, SQL Server would interpret it according to the connection's language
setting, and with German settings 2023-06-01 would become January 6—and be stored that way for
good. This leads to the following rules:
- A date is accepted only in ISO format;
01.06.2023is ambiguous. - A timestamp with a time zone for a column without one is refused on the native route and converted over OData (see Timestamps without a time zone).
- An amount is bound exactly, including in exponent notation: for a decimal column, a JSON value such as
1.5E2is parsed from its digits and keeps every one of them. This holds for up to 38 digits, as many asdecimal(38, s)holds, when writing and when reading; a longer number is refused for a decimal column. - A Boolean value for a text column is refused.
- A floating-point value arrives bit for bit. It's parsed from its digits, not converted from
decimal, a conversion that doesn't round correctly.
A timestamp is bound as the type of its column, both when writing and in a filter: as
datetime2 for a datetime2 column, as date for a date column. If the type were inferred from
the value alone, every timestamp would be a datetime—rounded to 1/300 of a second and limited to
the year 1753 and later. Because of this rule, a filter matches a stored value with seven fractional
digits, and a written value keeps all seven digits, even in the year 1.
A null for a binary column is bound as a binary value. Without a type, null would reach SQL
Server as nvarchar, and SQL Server doesn't implicitly convert nvarchar to binary, varbinary,
or image—not even a null. An insert would then fail with error 257. Access leaves out fields that
weren't set, but the ADO.NET CommandBuilder lists every column, including a varbinary column set
to null.
Put a date in a filter as a timestamp, not as a string. On the OData interface,
ge 2023-06-01T00:00:00Z(the OData 4 literal) andge datetime'2023-06-01T00:00:00'(the OData 3 literal that Access and Power Query write) bind a timestamp;ge '2023-06-01'binds text. For adatetimeorsmalldatetimecolumn, SQL Server interprets that text according to the connection's language setting: against a database with German settings,'2023-06-01'becomes January 6—no error, no warning, just a different set of rows.
A decimal value is bound exactly, not as a floating-point number. SQL Server gives floating point a higher precedence than decimal (Microsoft: Data type precedence). A floating-point parameter compared with a
decimalormoneycolumn therefore converts the column: its index can no longer be used, and the comparison runs against an approximation of the stored value. Two different decimal values can map to the same floating-point value:DecimalField eq 123456789012.345678matches one row, while the same filter with…678dmatches two. Floating point is used only where the literal calls for it—anfordsuffix, exponent notation, or a value outside the exact range.
A refused literal costs more than it seems. A spreadsheet or reporting client doesn't report the
400; it drops the condition and reads the whole object instead. A targeted query turns into a full table read, which can hit the time limit on a large table. That's why the protocol's literals are part of the service contract, not a convenience.
Timestamps without a time zone
A timestamp without a time zone appears with a
Z. OData v4 has no type for a timestamp without a time zone. The service writes the stored wall-clock time and appendsZ. TheZis dictated by the protocol; it says nothing about the stored time, because the database doesn't know which time zone its values came from. To match, a filter value with a time zone is converted to Coordinated Universal Time (UTC) for such a column:ge 2023-01-01T00:00:00+01:00compares against December 31, 2022, 11:00 PM. The two rules only ever change together.
Over OData, a timestamp with a time zone is converted to UTC when it's written. The read side appends a
Zfor a column without a time zone, so a client that reads a row, changes it, and sends it back includes thatZ; the service strips it off again. Any other offset is converted the same way:17:00+02:00is stored as 15:00—the same point in time that a filter with a time zone refers to. The native route (/api/v1/cf/…), by contrast, refuses a timestamp with a time zone for such a column, because it has no such convention.
Types
| Column type | Edm type | Notes |
|---|---|---|
bit, tinyint, smallint, int, bigint | Boolean, Byte, Int16, Int32, Int64 | Int64 is written as a JSON number; a client that reads numbers as IEEE 754 floating point (JavaScript, for example) loses digits beyond 2^53 |
decimal, numeric, money, smallmoney | Decimal | precision and scale in the schema; written exactly as a JSON number—with the same caveat for floating-point readers |
real, float | Single, Double | |
date, time | Date, TimeOfDay | |
datetime, datetime2, smalldatetime | DateTimeOffset | wall-clock time with Z; see Timestamps without a time zone |
datetimeoffset | DateTimeOffset | with the stored time zone |
Text types and xml | String | length: in characters for nchar/nvarchar, in bytes of storage for char/varchar (more than the number of characters under UTF-8 collations) |
uniqueidentifier | Guid | |
| Binary types, row version | Binary | |
hierarchyid, geography, geometry | Binary | the stored bytes, the same ones ODBC Driver 18 returns; a written value is passed as bytes and converted back by SQL Server |
sql_variant | String | the stored value as text in the form of its base type, as ODBC Driver 18 returns it: 42, 12.345, 2026-10-29 08:00:00.000; a written value is stored as text |
vector, json, CLR types of an application | — | not offered: not over OData, not when reading without a field list, and not when reading back after a write on the native route |
Not every type can be compared or sorted. SQL Server doesn't compare or sort text, ntext,
image, xml, geography, and geometry. It compares a sql_variant by its base type: a value
holding the number 42 isn't equal to the text '42', and the filter would silently find nothing. Over
OData, the service therefore rejects a comparison in $filter and a sort in $orderby on such a
column with 400 and names the column; the check for a missing value (eq null) and text search
stay allowed. hierarchyid is ordered by its bytes and can be compared and sorted, with a binary
value in hexadecimal notation: binary'5B40'. The native route passes a comparison to SQL Server
unchanged; a comparison on sql_variant there finds a value only if it's stored in the same base
type.
vector (SQL Server 2025) reports varbinary as its system type but refuses conversion to
varbinary(max). The catalog therefore lists it under its own name. Listed as varbinary, it would
be offered as a binary value without being one, and a row stamp over the table would make every read
fail.
Names with special characters
Inside square brackets, SQL Server accepts almost any character in a name: [F&E], [Ein/Aus],
[Prüf-Kennung], [Pos. Nr.]. OData allows only letters, digits, and the underscore in a name, up
to 128 characters (SimpleIdentifier in
OData CSDL XML 4.01,
section „SimpleIdentifier“). The service therefore encodes such names the same way SQL Server does in
FOR XML and SharePoint does for field names: a character that isn't allowed in its position becomes
_x, followed by its code as four uppercase hexadecimal digits and _; outside Unicode's Basic
Multilingual Plane, the code has eight digits.
| Stored | Over the wire |
|---|---|
F&E | F_x0026_E |
Ein/Aus | Ein_x002F_Aus |
Prüf-Kennung | Prüf_x002D_Kennung |
Pos. Nr. | Pos_x002E__x0020_Nr_x002E_ |
Geräte Liste | Geräte_x0020_Liste |
1Nummer | _x0031_Nummer |
Nr# | Nr_x0023_ |
GeändertAm | GeändertAm (unchanged) |
A_x0020_B (literal name) | A_x005F_x0020_B |
| Rule | Reason |
|---|---|
| Letters (including accented letters), the underscore, and digits after the first character stay unchanged | A name without special characters stays as it is |
@, #, and $ are encoded too | SQL Server allows them in a regular identifier, but OData doesn't; left unencoded, they would make the schema document invalid |
An underscore that looks like the start of an encoding is itself encoded (_x005F_) | Otherwise A B and a column literally named A_x0020_B would end up with the same name |
Applies to schemas, tables, views, and columns: in $metadata, in the path, in $select, $filter, and $orderby, in the key, and in the body | A caller refers to an object the same way everywhere |
The configuration refers to objects the same way (RowStampTables, KeylessWriteTables, limits) | It names objects the way a request does |
On the native route (/api/v1/cf/…), the request uses the encoded name, and the response keeps the stored name | Responses on this route use names as the database stores them; see Representations |
| The only objects or columns left out because of their names are those whose encoded name would exceed 128 characters (apart from that, columns of unsupported types and objects without a supported column are missing) | An identifier can't be longer; a long name with many spaces can go over the limit |
Only the catalog's spelling ever goes into a statement: the service looks up the encoded name in the
catalog and replaces it with the stored name before writing it into the statement—in brackets, with
any ] doubled. The ODBC driver translates names back for Access, so F&E shows up as F&E there.
How a name gets into a statement safely. Names with umlauts and accents are allowed, even though
databases compare identifiers under collations that treat accents differently; otherwise nobody
could write to a table with a required column such as GeändertAm. Two steps protect this boundary:
the name is looked up in the catalog that the service read from the database, and only the
catalog's spelling goes into the statement, in brackets and with the closing bracket doubled. The
comparison is ordinal—case is folded, accents aren't. An encoded name is compared with the catalog's
encoded names, never with the stored ones; otherwise a column literally named F_x0026_E could be
mistaken for F&E.