Add vector(float16) support - #4501
Conversation
A vector column's base type and number of dimensions were already available from the column schema, but only as a numeric scale and a column size which the caller had to decode. They are now surfaced under their own names, so that applications inspecting result set metadata do not have to know that encoding. Also registers the vector type in the DataTypes schema collection, where it was missing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e17ed782-c576-4cb7-9b4b-7ad286d7a7d0
SQL Server transports vector(N, float16) elements as raw binary16 values. System.Half is only available on .NET, so the conversion is implemented manually for .NET Framework. The manual implementation is compiled for every target framework rather than only for .NET Framework, so that it can be validated exhaustively against System.Half on .NET while remaining the code path .NET Framework actually uses. It is verified against every binary16 bit pattern, a strided sweep of the single precision range, and the rounding, subnormal, overflow and underflow boundaries. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e17ed782-c576-4cb7-9b4b-7ad286d7a7d0
Advertises version 2 of the VECTORSUPPORT feature extension, so that a vector(N, float16) column is exchanged in its native binary form rather than as a varchar(max) JSON string. On .NET such a column is surfaced as SqlVector<Half>. .NET Framework has no System.Half, so it is reported as a string there, matching how it is already presented when the server does not negotiate float16 support. Callers on either framework can explicitly request a strongly typed value via GetSqlVector<float>, which widens the elements without loss. SqlVector<T> continues to derive the base type written to the wire from T alone. Conversion between base types is left to the server, which performs it for parameters. Bulk copy is the exception: it declares the destination's base type in the INSERT BULK statement, so a payload using a different base type is rejected as a column length error rather than converted, and is rewritten by the driver first. That conversion runs after coercion, because the payload coercion produces uses the source value's own base type: a JSON string always yields float32, which is how a float16 column reads back where System.Half is unavailable. SqlVector<T>.ToString() now returns the vector's values as a JSON array rather than the type name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e17ed782-c576-4cb7-9b4b-7ad286d7a7d0
Describes the base types a vector column can have, how they map to SqlVector<T>, and how a float16 column is read and written on .NET Framework, where System.Half does not exist. Also documents the vector feature extension versions and the column metadata properties. Adds a sample covering both frameworks, reading a float16 column as an exact, widened or JSON value, inspecting a column's base type and dimensions, and converting between base types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e17ed782-c576-4cb7-9b4b-7ad286d7a7d0
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for SQL Server vector(N, float16) by negotiating VECTORSUPPORT feature extension version 2, introducing an IEEE-754 binary16 codec, and wiring float16 handling through SqlVector<T> read/write paths (including bulk copy), with accompanying docs and tests.
Changes:
- Negotiate vector feature extension v2 and track negotiated vector capability version (float32/float16) on the connection.
- Add float16 vector support across
SqlVector<T>,SqlDataReader,SqlBuffer,SqlParameter,SqlCommand, andSqlBulkCopy, including payload conversion for bulk copy. - Add unit/manual tests plus docs/snippets/sample updates; expose vector base type + dimensions via
DbColumnindexer and registervectorin theDataTypesschema collection.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/ConnectionTests.cs | Updates simulated negotiation tests for vector feature extension v2. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs | Adds float16 construction/rendering tests and payload conversion tests. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/Float16ConverterTest.cs | New unit tests validating binary16 codec (incl. exhaustive bit-pattern validation on .NET). |
| src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorFloat16BehaviourTests.cs | Manual tests for float16-specific behaviors (read/write/cross-base-type/bulk copy). |
| src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorColumnMetadataTests.cs | Manual tests for vector column metadata + schema collection registration. |
| src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorFloat16Tests.cs | Manual typed tests for SqlVector<Half> on .NET. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs | Implements float16 support in SqlVector<T>, adds JSON ToString(), payload widening/conversion helpers. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsEnums.cs | Adds vector version constants and sets max supported vector version to float16 (v2). |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs | Handles float16 vector return/coercion paths (Half on .NET, widening on netfx). |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.DataTypes.cs | Registers vector in GetSchema("DataTypes"). |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs | Adds float16 vector element type and element-size mapping; meta type inference includes SqlVector<Half> on .NET. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDbColumn.cs | Exposes VectorBaseType/VectorDimensions via DbColumn indexer. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs | Adds float16 field-type mapping and broadens GetSqlVector<T> to allow Half on .NET. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs | Emits (N, float16) parameter declaration when needed (keeps float32 declaration unchanged). |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs | Emits float16 vector type in INSERT BULK declaration and converts payload base type to match destination. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBuffer.cs | Centralizes float16 vector rendering/value-shaping across frameworks. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs | Records negotiated vector feature version on FEATUREEXTACK. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/ConnectionCapabilities.cs | Replaces bool flag with VectorVersion and derived float32/float16 capability properties. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/Float16Converter.cs | New internal binary16<->binary32 conversion implementation. |
| src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlTypes.cs | Updates reference surface to include SqlVector<T>.ToString() override. |
| doc/snippets/Microsoft.Data.SqlTypes/SqlVector.xml | Documents float16 support, size constraints, and ToString() JSON rendering. |
| doc/samples/SqlVectorFloat16Example.cs | New sample demonstrating float16 vectors (insert/read/metadata). |
| .github/instructions/features.instructions.md | Updates repo feature reference docs for float16 vector base type and negotiation versions. |
Suppressed comments (1)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs:420
ConvertPayloadElementTypedoesn’t validate the vector header magic/version bytes before using the length and element type fields. This can cause non-vector payloads to be converted (or to fail later with less appropriate exceptions). ValidateVecHeaderMagicNo/VecVersionNoup front, consistent withGetCountsOrThrow.
if (tdsBytes.Length < TdsEnums.VECTOR_HEADER_SIZE)
{
throw ADP.InvalidVectorHeader();
}
| // The payload is converted directly rather than through a strongly typed vector, | ||
| // so that .NET Framework, which has no System.Half, can also write to float16 | ||
| // destinations. | ||
| return SqlTypes.SqlVector<float>.ConvertPayloadElementType(payload, destinationElementType); |
| if (tdsBytes.Length < TdsEnums.VECTOR_HEADER_SIZE) | ||
| { | ||
| throw ADP.InvalidVectorHeader(); | ||
| } |
Bulk copy read a vector column through the representation the reader surfaces, which is a JSON string on frameworks without System.Half. That round trip is both larger than the payload it encodes and unable to carry a negative zero, because System.Text.Json on .NET Framework serialises one as zero and parses a negative zero literal back as positive zero. Reading the payload directly avoids both. It is chosen once per column, when the source and destination are both vector columns, alongside the existing decimal and streaming decisions. Any difference in base type between the two is still resolved when the value is converted. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e17ed782-c576-4cb7-9b4b-7ad286d7a7d0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs:1755
SqlTypes.SqlVector<float>doesn’t resolve to any namespace/type in this file (there’s nousing SqlTypes = ...and noSqlTypesnamespace). This should be fully qualified toMicrosoft.Data.SqlTypes.SqlVector<float>(or add an alias) to avoid a compile error.
// The payload is converted directly rather than through a strongly typed vector,
// so that .NET Framework, which has no System.Half, can also write to float16
// destinations.
return SqlTypes.SqlVector<float>.ConvertPayloadElementType(payload, destinationElementType);
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs:154
FromTdsPayloadreads header fields (element type/length) without validating the vector magic/version bytes. This makes the widening path accept malformed payloads thatGetCountsOrThrowwould reject.
if (tdsBytes.Length < TdsEnums.VECTOR_HEADER_SIZE)
{
throw ADP.InvalidVectorHeader();
}
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs:420
ConvertPayloadElementTypeshould validate the vector header magic/version before interpreting element type and length; otherwise malformed byte[] values can be converted and sent on the wire rather than failing fast with InvalidVectorHeader.
if (tdsBytes.Length < TdsEnums.VECTOR_HEADER_SIZE)
{
throw ADP.InvalidVectorHeader();
}
doc/samples/SqlVectorFloat16Example.cs:146
- These interpolated strings won’t compile because the expression uses double quotes (e.g., column["VectorBaseType"]) inside a double-quoted string literal. Escape the quotes (or assign to a local variable) before interpolating.
Console.WriteLine($"\nColumn base type: {column["VectorBaseType"]}");
Console.WriteLine($"Column dimensions: {column["VectorDimensions"]}");
The existing suite covers nulls where the source and destination share a base type, but not where they differ, which is the path that converts the payload. Verified that nulls survive in every combination, interleaved with non-null rows so that a row's nullness cannot be satisfied by position alone. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e17ed782-c576-4cb7-9b4b-7ad286d7a7d0
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.
Suppressed comments (2)
doc/samples/SqlVectorFloat16Example.cs:146
- These interpolated strings won’t compile because the expression contains a string literal with double quotes (e.g., column["VectorBaseType"]) which terminates the outer interpolated string. Assign the indexer results to variables (or constants) first, then interpolate those variables.
Console.WriteLine($"\nColumn base type: {column["VectorBaseType"]}");
Console.WriteLine($"Column dimensions: {column["VectorDimensions"]}");
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs:2408
- This SqlVector special-case is redundant/unreachable because SqlVector implements ISqlVector (so it will already be handled by the earlier
value is ISqlVectorbranch). Keeping the extra branch increases maintenance burden and risks diverging behavior.
else if (currentType == typeof(SqlVector<Half>))
{
value = ((ISqlVector)value).VectorPayload;
}
#endif
| /// the connection did not negotiate support for its base type. | ||
| /// </para> | ||
| /// </remarks> | ||
| public override object this[string property] => |
There was a problem hiding this comment.
Can you write a sample/test case that uses this property?
I want to know the customer use case of why it is required to be exposed.
|
|
||
| #nullable enable | ||
|
|
||
| public sealed class VectorFloat16TestData : NativeVectorTestDataBase<Half> |
There was a problem hiding this comment.
Please write float16 tests for .NET Framework as well.
| /// Tests for the metadata a vector column reports. This applies to every base type, so the | ||
| /// tests here use float32 and run against any server which supports vectors. | ||
| /// </summary> | ||
| [Trait("Set", "3")] |
There was a problem hiding this comment.
Does this trait work at class level?
| { | ||
| throw SQL.VectorNotSupportedOnColumnType(metaData.column); | ||
| } | ||
| return (T)(object)data.GetSqlVector<Half>(); |
There was a problem hiding this comment.
How do we make it a non-breaking change for existing applications because this time we're not in a major release milestone.
GetFieldType/GetValue for a float16 column now returns SqlVector<Half> on .NET and string on .NET Framework — a behavior change for existing apps once VECTORSUPPORT v2 is negotiated (previously varchar/string everywhere). Worth calling out as potentially breaking.
@saurabh500 for awareness.
There was a problem hiding this comment.
Other type conversions within the driver use SqlConnectionStringBuilder.TypeSystemVersion as a decision point. This hasn't been used for a long time, but might be appropriate.
There was a problem hiding this comment.
@cheenamalhotra good catch.
I think this is a classic catch of AppContext Switch to allow customers to go to back to a compatible change. An AppContext to not negotiate feature extension, till customers can adapt the App, could be a viable solution
@edwardneal TypeSystemVersion could have been a good candidate if we only had SQL On-prem. But the API doesn't hold true in the world of ever evolving cloud offerings which are versionless.
saurabh500
left a comment
There was a problem hiding this comment.
Summary
This adds vector(N, float16) by advertising VECTORSUPPORT v2, introducing a hand-written binary16 codec, teaching SqlVector<T> about System.Half, and switching vector→vector bulk copy to a raw-payload transfer. The engineering is careful, and I want to call out specifically that the endianness and element-size arithmetic is correct throughout — I went looking for a missed (ColumnSize - 8) / 4 and there isn't one. The commit split is clean and the rationale in the description is unusually good.
I have one blocking correctness issue, plus a set of suggestions. Inline comments carry the detail and suggested diffs; this is the map.
Blocking
SqlBuffer.GetSqlVector<T>()succeeds or throws depending on the row's nullness. TheIsNullbranch builds a vector for anyTwithout consulting the column's base type, while the non-null branch validates.GetSqlVector<Half>()over afloat32column therefore returnsNullfor NULL rows and throwsNotSupportedExceptionfor non-NULL rows in the same result set. Data-dependent rather than schema-dependent, so it is hard to find in testing and impossible to guard against in caller code.
Suggestions
- The codec is not bit-exact against
System.Halffor NaN, contrary to the description, and the tests are written to step around exactly that case (continue/IsNaN-only).float.NaNcarries the sign bit, so widening flips the sign of every NaN relative toSystem.Half; narrowing canonicalises payloads that(Half)floatpreserves. Either matchSystem.Halfor pin the canonical form with explicit assertions and drop the "bit-exact" claim. - Bulk copy's vector case uses
metadata.scalerather than thescalelocal that the surrounding code establishes for encrypted columns. - The widening path skips the magic-number and version validation that the matching path gets via
GetCountsOrThrow. Capabilities.Float16VectorTypeis never read anywhere insrc/, so aSqlVector<Half>on a v1-negotiated connection fails server-side rather than client-side. (Float32VectorTypewas already dead inmain; this adds a second.)- The negotiation theory's
0x3case doesn't test what its comment claims — the simulated server caps the ack itself, so the client's own ceiling check atSqlConnectionInternal.cs:1660stays untested. - Docs say narrowing "fails for values outside its range"; the code saturates to ±Infinity and leaves it to the server.
MetaDatais dereferenced without a null check inCreateSourceColumnMetadata.
Things I checked and found correct
Worth recording, since they're the parts most likely to be wrong in a change like this:
- Subnormal widening, signed zero, overflow to infinity, the flush-to-zero boundary (2⁻²⁵ ties to even → 0, 2⁻²⁶ flushes), binary32 subnormal inputs, and the rounding carry into the exponent are all correct. Compiling
Manual*on every TFM so the netfx path is under test on .NET too is a good arrangement. - Bulk copy null handling is correct as-is —
GetValueFromSourceRowreturnsDBNull.ValuewithisNull = trueandConvertValuereturns beforeConvertVectorToBaseType. Notea5de733c3is test-only; it documents behaviour that already worked rather than fixing anything. - The raw-payload path can't be taken by
DataTable,DataRow[], or non-SqlClientDbDataReadersources (they keepValueMethod.GetValue), and reordering column mappings are safe becausesourceOrdinalis the mapped ordinal. - No unacknowledged breaking changes beyond the three listed:
GetDataTypeNameis unchanged,GetFieldType/GetProviderSpecificFieldTypeboth route through the single newGetVectorFieldTypeso they stay consistent,SqlMetaDataFactory.DataTypesonly adds a row (gated onMinimumVersionKey), thefloat32declaration is deliberately unchanged, andSqlDbColumn's new indexer falls through tobase[property]. - No shared mutable state:
Float16Converteris stateless andSqlVector<T>is areadonly structwith no static caches.
On testing
Taking as given that CI has no float16-capable server and no Azure SQL DB connectivity, so the manual suite is the only gate that will ever run — I looked at whether it is complete enough for a lab run rather than whether CI covers it. It is substantial: 11 behaviour tests plus the inherited NativeVectorTestsBase matrix, and the sample data is well chosen (Half.MaxValue, Half.Epsilon, -0.0f, exactly-representable eighths). Gaps I'd close:
- .NET Framework gets none of the
NativeVectorTestsBasematrix —NativeVectorFloat16Tests.csis entirely#if NET. That is where the hand-rolledManual*codec is the production path. - No async coverage for the new representation on any framework, and none at all for float16 on netfx.
DataTestUtility.CheckVectorFloat16Supportedfails open through the code under test. It reads the probe vector withGetString+JsonSerializer.Deserializeand catchesJsonException→false. A driver regression that produces malformed JSON silently skips the whole float16 suite green. Since the manual run is the only gate, that is the wrong failure mode. (Outside this diff, so no inline comment — but worth fixing alongside. It also leavesPREVIEW_FEATURES = ONon the shared test database as a side effect.)- Two range tests assert only that some
SqlExceptionwas thrown; they'd pass on an unrelated failure, and they can't distinguish "client rejects" from "client saturates and server rejects" — which is exactly the ambiguity in the doc wording above. - Nothing covers the blocking issue: reading a float32 column as
SqlVector<Half>, for a NULL and a non-NULL row. - Bulk copy with a dimension-count mismatch between source and destination is uncovered. Mitigating:
float32→float32now routes through the new raw-payload path too and is covered by the existingNativeVectorFloat32Tests, which runs against any vector-capable server — so regression risk to shipped functionality is covered.
Minor
SqlVector<T>.ToString()changing for existingSqlVector<float>callers is justified and correctly surfaced in the ref assembly and docs, but it is unrelated to float16 — it wants its own release-note entry as a behavioural break, not just an API-list line. Related:GetString()usesJsonSerializer.Serialize, which throws onNaN/Infinityby default, and on .NET Framework that is now the defaultGetValue()path.ConvertPayloadElementTypeisinternal staticonSqlVector<T>but never usesT, so callers writeSqlVector<float>.ConvertPayloadElementType(...), which reads as though it returns a float32 result.- On .NET Framework the reader surfaces a float16 column as
stringwhile an output parameter surfaces it asSqlVector<float>. Self-consistent, but worth documenting. - Preprocessor directives in the new code are indented to the surrounding block; the dominant style in these files is column 0.
ConnectionCapabilities.cs:179says vectors were "introduced in SQL Server 2022" — pre-existing, but the newFloat16VectorTypedoc sits right beside it.- Worth confirming the
doc/samplesbuild resolves the locally-packed driver:SqlVectorFloat16Example.csreferencesSqlVector<Half>andGetSqlVector<Half>, which exist in no released package.
Review assisted by GitHub Copilot; findings verified against the code at a5de733c3.
| // The payload's base type may differ from T: a float16 column can be | ||
| // read as a vector of single precision values, which is the only | ||
| // strongly typed form available on .NET Framework. | ||
| return SqlVector<T>.FromTdsPayload(SqlBinary.Value); |
There was a problem hiding this comment.
Blocking — this succeeds or throws depending on the row's nullness.
The IsNull branch above (981-984) calls SqlVector<T>.CreateNull for any T without consulting _value._vectorInfo._elementType, while this line delegates to FromTdsPayload, which throws SQL.VectorTypeNotSupported for any narrowing pair (SqlVector.cs:170-172).
Concretely, on .NET, reader.GetSqlVector<Half>(i) against a vector(N, float32) column returns a valid SqlVector<Half>.Null for NULL rows and throws NotSupportedException for non-NULL rows in the same result set. GetFieldValue<SqlVector<Half>> inherits this because it routes here. The outcome depends on the data rather than the schema, which makes it very hard to discover in testing and impossible for a caller to guard against.
There's a secondary problem in the same branch: a null float32 column read as SqlVector<Half> yields a Half vector of the same element count, which is a different wire size. Round-tripping that value back as a parameter declares vector(N, float16) against a float32 column.
Validating the pairing before the null check fixes both, and makes the behaviour depend on the column:
internal SqlVector<T> GetSqlVector<T>() where T : unmanaged
{
if (_type is StorageType.Vector)
{
// The payload's base type may differ from T: a float16 column can be read as a
// vector of single precision values, which is the only strongly typed form
// available on .NET Framework. Validate the pairing before considering
// nullness, so that the outcome depends on the column's base type rather than
// on whether a particular row happens to be null.
SqlVector<T>.ThrowIfNotConvertibleFrom(_value._vectorInfo._elementType);
if (IsNull)
{
return SqlVector<T>.CreateNull(_value._vectorInfo._elementCount);
}
return SqlVector<T>.FromTdsPayload(SqlBinary.Value);
}
return (SqlVector<T>)SqlValue;
}where ThrowIfNotConvertibleFrom factors out the source/target check FromTdsPayload already performs at SqlVector.cs:156-172, so the two cannot drift.
Please also add a test that reads both a NULL and a non-NULL row of a float32 column as SqlVector<Half> and asserts they behave the same way — that pairing isn't covered anywhere today.
| return mantissa == 0 | ||
| ? Int32BitsToSingle((sign << 31) | (Binary32MaxExponent << Binary32MantissaBits)) | ||
| : float.NaN; |
There was a problem hiding this comment.
The PR description says the codec is "validated bit-exactly against System.Half across all 65,536 patterns". That isn't true for NaN, and it's this line.
float.NaN is 0xFFC00000 — it has the sign bit set. (float)Half.NaN is 0x7FC00000. So every NaN widened here comes out with the opposite sign to what System.Half produces, and System.Half also preserves the payload bits rather than canonicalising them:
| input | #if NET path (production on .NET) |
this path (production on netfx) |
|---|---|---|
0x7E00 |
0x7FC00000 |
0xFFC00000 |
0xFE00 |
0xFFC00000 |
0xFFC00000 |
That's a real cross-framework difference in a conversion the driver ships, even if SQL Server never stores a NaN in a vector. Preserving sign and payload costs nothing and removes the divergence:
| return mantissa == 0 | |
| ? Int32BitsToSingle((sign << 31) | (Binary32MaxExponent << Binary32MantissaBits)) | |
| : float.NaN; | |
| return mantissa == 0 | |
| ? Int32BitsToSingle((sign << 31) | (Binary32MaxExponent << Binary32MantissaBits)) | |
| : Int32BitsToSingle( | |
| (sign << 31) | | |
| (Binary32MaxExponent << Binary32MantissaBits) | | |
| (mantissa << MantissaShift)); |
The comment on 85-86 needs updating either way, since it documents canonicalisation.
If the canonicalisation is deliberate, that's defensible — but then the claim should come out of the description and the tests should pin the canonical form explicitly rather than skipping NaN (see my comments on Float16ConverterTest.cs).
| // Infinity or NaN. NaN is canonicalised to a quiet NaN, matching the | ||
| // representation produced by System.Half. | ||
| int payload = mantissa == 0 ? 0x7C00 : 0x7E00; | ||
| return (ushort)((sign << 15) | payload); |
There was a problem hiding this comment.
Same divergence in the narrowing direction. (Half)float preserves the top payload bits and forces the quiet bit; this canonicalises every NaN to 0x7E00, so ManualFromSingle(0x7FFFFFFF) returns 0x7E00 where System.Half returns 0x7FFF.
To match:
| // Infinity or NaN. NaN is canonicalised to a quiet NaN, matching the | |
| // representation produced by System.Half. | |
| int payload = mantissa == 0 ? 0x7C00 : 0x7E00; | |
| return (ushort)((sign << 15) | payload); | |
| // Infinity or NaN. A NaN keeps its sign and the high bits of its payload, | |
| // with the quiet bit forced, which is what a System.Half conversion produces. | |
| int payload = mantissa == 0 | |
| ? 0x7C00 | |
| : 0x7C00 | 0x0200 | ((mantissa >> MantissaShift) & 0x1FF); | |
| return (ushort)((sign << 15) | payload); |
Whichever way you resolve this and the widening case, the two directions should agree with each other.
| // Too small to represent as a normal value. Values more than eleven | ||
| // binade below the smallest subnormal cannot round up to one, so they | ||
| // are flushed to zero rather than shifted by more than the mantissa width. |
There was a problem hiding this comment.
Nit: the prose doesn't match the guard. "More than eleven binade below the smallest subnormal" doesn't describe targetExponent < -Binary16MantissaBits, and "eleven" doesn't correspond to Binary16MantissaBits (10) either.
| // Too small to represent as a normal value. Values more than eleven | |
| // binade below the smallest subnormal cannot round up to one, so they | |
| // are flushed to zero rather than shifted by more than the mantissa width. | |
| // Too small to represent as a normal value. Values below half of the | |
| // smallest subnormal cannot round up to it, so they are flushed to zero | |
| // rather than shifted by more than the mantissa width. |
For the record, I verified the boundary itself is right: exactly 2⁻²⁵ ties to even and yields zero, and 2⁻²⁶ flushes. Binary32 subnormal inputs always land in this branch too, so the implicit-bit restore below is never misapplied to them.
| // uses the source value's own base type: a JSON string always yields | ||
| // float32, which is how a float16 column reads back on frameworks | ||
| // without System.Half. | ||
| value = ConvertVectorToBaseType(value, metadata.scale); |
There was a problem hiding this comment.
This should use the scale local rather than metadata.scale. Lines 1778-1788 establish it precisely so that encrypted columns read their base type info:
byte scale = metadata.scale;
...
if (metadata.isEncrypted)
{
type = metadata.baseTI.metaType;
scale = metadata.baseTI.scale;
...
}For an encrypted column, metadata.scale is the wrapping varbinary metadata's scale — 0, i.e. Float32 — not the vector's base type, which is in metadata.baseTI.scale. Whether Always Encrypted currently permits a vector column or not, every other conversion in this method reads the redirected value, and this one silently won't.
| value = ConvertVectorToBaseType(value, metadata.scale); | |
| value = ConvertVectorToBaseType(value, scale); |
updateBulkCommandText at 883-886 has the same shape (metadata.scale used for both the dimension count and the float16 test) — worth a look for consistency, though that path may legitimately want the outer metadata.
| if (float.IsNaN(expected)) | ||
| { | ||
| Assert.True(float.IsNaN(actual), $"0x{bits:X4} should convert to NaN."); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
This asserts only that the result is a NaN, not that it's the same NaN — which is what lets the sign divergence I flagged in Float16Converter.cs:87-89 through. ManualToSingle(0x7E00) returns 0xFFC00000 while (float)Half returns 0x7FC00000, and this passes.
Once the production behaviour is settled, this should assert the bit pattern like the non-NaN case below does. If you match System.Half, the special case disappears entirely and the existing bitwise assertion covers all 65,536 patterns — which is what the PR description claims today.
If canonicalisation stays deliberate, assert it explicitly rather than skipping:
| if (float.IsNaN(expected)) | |
| { | |
| Assert.True(float.IsNaN(actual), $"0x{bits:X4} should convert to NaN."); | |
| continue; | |
| } | |
| if (float.IsNaN(expected)) | |
| { | |
| // Deliberately canonicalised rather than payload preserving, so compare | |
| // against the documented canonical form rather than against Half. | |
| Assert.Equal( | |
| BitConverter.SingleToInt32Bits(float.NaN), | |
| BitConverter.SingleToInt32Bits(actual)); | |
| continue; | |
| } |
The rest of this test is excellent — exhaustive over the bit patterns, and comparing bitwise so signed zero is distinguished.
| if (Half.IsNaN(value)) | ||
| { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Same gap in the narrowing direction: skipping NaN here is what hides ManualFromSingle canonicalising payloads that (Half)float preserves. FromSingle_MatchesHalf_AcrossTheSinglePrecisionRange has the same continue at 82-85.
Whichever way Float16Converter.cs:137-140 is resolved, please replace both continues with an assertion, so the NaN behaviour is pinned rather than merely undefined. As it stands, the two codec implementations could drift arbitrarily on NaN and every test here would still pass.
| // System.Half, and therefore SqlVector<Half>, is only available on .NET. | ||
| #if NET | ||
|
|
There was a problem hiding this comment.
Understood why this file is #if NET — SqlVector<Half> doesn't exist on netfx. The consequence, though, is that .NET Framework gets none of the NativeVectorTestsBase matrix for float16: no sync/async parameter round trip, no stored proc params, no bulk copy source modes, no Prepare. On netfx those go entirely untested for this base type.
That's the framework where it matters most, because netfx is where the hand-rolled ManualToSingle/ManualFromSingle path is the production codec rather than a test double.
NativeVectorTestsBase is generic over TElement, so the documented netfx usage — write SqlVector<float>, read widened — can reuse the whole harness:
// Runs on every framework: exercises the float16 column through the single precision
// representation, which is the only strongly typed form available without System.Half.
public sealed class VectorFloat16AsSingleTestData : NativeVectorTestDataBase<float>
{
public override bool IsSupported => DataTestUtility.IsSqlVectorFloat16Supported;
public override string SqlServerTypeName => "float16";
// sample data restricted to values exactly representable in binary16
}
[Trait("Set", "3")]
public sealed class NativeVectorFloat16AsSingleTests
: NativeVectorTestsBase<float, VectorFloat16AsSingleTestData>
{
}That would also give netfx its only async float16 coverage, since VectorFloat16BehaviourTests is entirely synchronous.
| SqlException exception = Assert.Throws<SqlException>(() => | ||
| Insert(_float16Table, new SqlVector<float>(new float[] { 70000f, 1f, 2f }))); | ||
|
|
||
| Assert.NotEmpty(exception.Message); |
There was a problem hiding this comment.
This passes on any SqlException — including "invalid object name" if the table setup ever regresses — so it doesn't establish that the server rejected the value rather than something else going wrong. BulkCopyRejectsValuesOutsideTheFloat16Range at 271 has the same shape.
Worth asserting the error number instead, so the test fails if the failure mode changes:
Assert.Equal(/* the server's vector range error number */, exception.Number);This matters more than usual here because of the doc/behaviour mismatch I flagged on SqlVector.cs:494: the client saturates 70000 to +Infinity and the server rejects that, which is a different failure from the client rejecting an out-of-range value. As written the test passes either way, so it can't tell you which behaviour you shipped.
| public void ReportsUnsupportedElementTypes() | ||
| { | ||
| Insert(_float16Table, new SqlVector<float>(new float[] { 1.5f, 2.5f, 3.5f })); | ||
|
|
||
| using SqlDataReader reader = Select(_float16Table); | ||
| Assert.True(reader.Read()); | ||
|
|
||
| Assert.Throws<NotSupportedException>(() => reader.GetSqlVector<double>(0)); | ||
| Assert.Throws<NotSupportedException>(() => reader.GetSqlVector<int>(0)); |
There was a problem hiding this comment.
This covers double and int, but not the case that actually breaks: reading a float32 column as SqlVector<Half> (see my blocking comment on SqlBuffer.cs). That pairing throws for non-null rows and silently succeeds for null ones.
_float32Table is already available here, so it's a short addition:
[ConditionalFact(nameof(IsSupported))]
public void ReportsNarrowingReadsConsistentlyForNullAndNonNullRows()
{
// The base type pairing is a property of the column, so a null row must be
// rejected the same way a populated one is.
Insert(_float32Table, DBNull.Value);
Insert(_float32Table, new SqlVector<float>(new float[] { 1.5f, 2.5f, 3.5f }));
using SqlConnection connection = new(_connectionString);
connection.Open();
using SqlCommand command =
new($"SELECT {ColumnName} FROM {_float32Table.Name} ORDER BY Id", connection);
using SqlDataReader reader = command.ExecuteReader();
Assert.True(reader.Read());
Assert.Throws<NotSupportedException>(() => reader.GetSqlVector<Half>(0)); // null row
Assert.True(reader.Read());
Assert.Throws<NotSupportedException>(() => reader.GetSqlVector<Half>(0)); // populated row
}More generally: VectorFloat16BehaviourTests is entirely synchronous, and it's the suite that pins the new representation and the acknowledged breaking changes. GetFieldValueAsync<SqlVector<Half>> and GetFieldValueAsync<SqlVector<float>> over a float16 column aren't exercised anywhere. Given CI can never run any of this, the lab suite is the only gate and async is worth covering here.
|
@apoorvdeshmukh and @cheenamalhotra, I think we will need to call this API a known limitation for preventing backward migration from .Net Runtime to NetFx. I believe this is a known reasonable compromise. |
|
BTW, I saw changes to ref assembly. Should I expect two copies of changes, one for netcore and another for NetFx? I am curious about how the APIs will show up in contract assemblies targeting 2 different frameworks. |
Adds support for the
float16base type of thevectordata type, by advertising version 2 of theVECTORSUPPORTfeature extension. Avector(N, float16)column is now exchanged in its native binary form rather than as avarchar(max)JSON string.Opened as a draft: this is an integration branch. The five commits are independently buildable and can be split into separate PRs on request.
Representation
SqlVector<Half>string(JSON)GetSqlVector<Half>exact, orGetSqlVector<float>widenedGetSqlVector<float>widenedSqlVector<Half>SqlVector<float>, JSON string, orSqlBulkCopySystem.Halfdoes not exist on .NET Framework, so afloat16column is reported as a string there, matching how it is already presented when the server does not negotiatefloat16support.GetSqlVector<float>widens the elements, which is exact. .NET Framework decodes binary16 itself, as no BCL package suppliesSystem.Halffornet462; on .NET the same methods delegate toBitConverter.SqlVector<T>keeps its shipped contract:Talone determines the base type written to the wire. Conversion between base types is left to the server, except forSqlBulkCopy, which states the destination's base type in theINSERT BULKstatement and so must convert the payload itself.Behaviour changes
vector(N, float16)column no longer returnsvarchar(max). String read paths still work, but the text changes from the server's scientific notation ([1.0000000e+000,2.0000000e+000]) to a compact JSON array ([1,2]). Both parse to the same values. This is the same transitionfloat32columns made when vector support was added in 6.1, so the two base types now render identically.GetFieldType,GetValueand the column type of a filledDataTablechange fromstringtoSqlVector<Half>; castingGetValuedirectly tostringnow throwsInvalidCastException.SqlVector<T>.ToString()now returns the values as a JSON array rather than the type name, which also affects existingSqlVector<float>callers.float16is server-side preview-gated (PREVIEW_FEATURES = ON), so applications which have not enabled it are unaffected.Also included
Vector column metadata. A vector column's base type and dimension count are now available from the column schema as
["VectorBaseType"]and["VectorDimensions"], viaDbColumn's virtual indexer, so no new public API. This is a v1 gap affectingfloat32today: the dimension count previously required hardcoding(ColumnSize - 8) / 4. Thevectortype is also registered in theDataTypesschema collection, where it was missing.Bulk copy between vector columns now transfers the raw payload. It previously read the column through the representation the reader surfaces, which is a JSON string where
System.Halfis unavailable. That round trip is several times larger than the payload it encodes — a 1998-dimensionfloat16vector is about 4 KB as a payload and about 24 KB as text — and it silently dropped a negative zero, becauseSystem.Text.Jsonon .NET Framework serialises one as0and parses-0back as positive zero. The decision is made once per column, alongside the existing decimal and streaming decisions.Validation
Verified against SQL Server 18.0.258.0.
The binary16 codec is validated bit-exactly against
System.Halfacross all 65,536 patterns and a strided sweep of the single precision range. It is compiled on every framework so that the .NET Framework path is the one under test.Every
SqlBulkCopysource type was checked against afloat16destination on .NET Framework —SqlDataReaderover both base types,DataTable,DataRow[], and a non-SqlClientDbDataReader— and all now preserve values exactly. A caller-supplied JSON string still normalises a negative zero, which matches what the server does when parsing a vector literal.Existing
Float16VectorTypeBackwardCompatibilityTestspass unchanged. CI has nofloat16-capable server, so those tests will skip.Commits
9a935eb8fc82cbinternal)dad19a38fc82cb734a90adad19a337e871ddad19a3dad19a3cannot be split further: advertising version 2 without the reader, parameter and bulk copy wiring would leavefloat16columns arriving natively with nothing able to interpret them.Note for the TVP work
WriteSmiTypeInfohas noVectorcase inmain; TVP support for vectors is on the unmergeddev/ad/tvp-json-vector. This change neither breaks nor covers that path. When the TVP branch lands it must write the base type into the TVP column metadata scale byte, asWriteParameterMetadatadoes.Checklist