Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9a935eb
Expose vector column base type and dimensions
apoorvdeshmukh Aug 4, 2026
8fc82cb
Add an IEEE 754 binary16 codec
apoorvdeshmukh Aug 4, 2026
dad19a3
Add vector(float16) support
apoorvdeshmukh Aug 4, 2026
734a90a
Document float16 vector support
apoorvdeshmukh Aug 4, 2026
37e871d
Transfer vectors between vector columns as their raw payload
apoorvdeshmukh Aug 12, 2026
a5de733
Cover nulls in cross base type vector bulk copy
apoorvdeshmukh Aug 12, 2026
3090c13
Address review feedback on vector(float16) support
apoorvdeshmukh Aug 17, 2026
ae88d35
Negotiate the vector feature extension version by connection string
apoorvdeshmukh Aug 25, 2026
39d84b3
Document the cost of float16 on .NET Framework
apoorvdeshmukh Aug 25, 2026
ba86fe0
Read the bulk copy source column type through IDataReader
apoorvdeshmukh Aug 25, 2026
0acf1e5
Make the vector tests portable to Azure SQL
apoorvdeshmukh Aug 25, 2026
31dc266
Report the vector type from what the connection negotiated
apoorvdeshmukh Aug 25, 2026
ee6a4b0
Accept VectorTypeSupport as a connection string keyword
apoorvdeshmukh Aug 25, 2026
45f4fd7
Describe what a textual bulk copy source actually does
apoorvdeshmukh Aug 25, 2026
00c14bd
Merge branch 'main' into dev/ad/vector-float16-support
apoorvdeshmukh Sep 18, 2026
29028cb
Address review feedback
apoorvdeshmukh Sep 18, 2026
df8166a
Let a wide float16 column be read and written
apoorvdeshmukh Sep 18, 2026
0b70737
Hold the vector feature extension to what the connection asked for
apoorvdeshmukh Sep 18, 2026
c7ece85
Merge branch 'main' into dev/ad/vector-float16-support
apoorvdeshmukh Oct 2, 2026
92d712a
Describe the codec's binary32 coverage accurately
apoorvdeshmukh Oct 2, 2026
a833488
Reject a vector base type the connection did not negotiate
apoorvdeshmukh Oct 6, 2026
ca9e7f7
Check the element count against the base type a value is sent as
apoorvdeshmukh Oct 6, 2026
f9bece3
Accept a JSON string as a vector parameter value
apoorvdeshmukh Oct 6, 2026
2592c22
Say where the vector column metadata is unavailable
apoorvdeshmukh Oct 6, 2026
c428dcd
Send a JSON vector parameter as text
apoorvdeshmukh Oct 6, 2026
de32fbf
Document the remaining vector test methods
apoorvdeshmukh Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .github/instructions/features.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ This is a comprehensive reference of supported connection string keywords.
| `Column Encryption Setting` | Disabled | Always Encrypted mode |
| `Enclave Attestation Url` | | Enclave attestation URL |
| `Type System Version` | Latest | Type system version |
| `Vector Type Support` | v1 | Vector base types exchanged in binary form: `off`, `v1`, `v2` |
| `Replication` | False | Replication support |
| `User Instance` | False | SQL Express user instance |
| `ConnectRetryCount` | 1 | Connection retry count |
Expand Down Expand Up @@ -123,6 +124,117 @@ This is a comprehensive reference of supported connection string keywords.
| `Json` | `String` | SQL Server 2025+ |
| `Vector` | `ISqlVector` | SQL Server 2025+ |

### Vector Base Types

A `vector` column has a base type, which determines how its elements are stored and
transported. The base type is selected by the type parameter of `SqlVector<T>`.

| SQL Server base type | `SqlVector<T>` | Element size | Max dimensions | Availability |
|----------------------|----------------|--------------|----------------|--------------|
| `float32` (default) | `SqlVector<float>` | 4 bytes | 1998 | SQL Server 2025+ |
| `float16` | `SqlVector<Half>` | 2 bytes | 3996 | SQL Server 2025+ (preview), .NET only |

Notes:

- The `float16` base type is only exchanged in its binary form when the connection asks for
it through the `Vector Type Support` keyword. See below.
- `System.Half` does not exist on .NET Framework, so `SqlVector<Half>` cannot be used there.
A `float16` column is instead reported as a string, and can be read either as a JSON array
through `GetString`/`GetSqlString`/`GetFieldValue<string>`, or as a `SqlVector<float>` via
`GetSqlVector<float>`, which widens the elements. Widening from `float16` is exact.
- `float16` requires `ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON` while
it is in preview.
- Whether SQL Server converts between base types for a parameter depends on the server:
some builds block both implicit and explicit conversion and report error 42238, others
allow it (verified against SQL Server vNext CTP 1.0, 18.0.258.0). A JSON string is
accepted by every server, so prefer it when the target server is not known. This matters
most on .NET Framework, where `System.Half` is unavailable and a `SqlVector<float>` would
otherwise be the only strongly typed way to write a `float16` column.
- A bulk copy is different: the `INSERT BULK` statement states the destination's base type,
and the server then requires a binary payload of exactly that width, so it performs no
conversion within the data stream. Any in-memory value — a JSON string or a
`SqlVector<T>` whose element type differs from the column's — is therefore converted to
the destination's base type by the driver, which does not depend on the server, and
narrowing to `float16` throws an `OverflowException` for a value outside its range. A
payload read from another vector column keeps its own base type, so copying between
columns of different base types is reported by the server.
- A vector base type can only be used once the connection has negotiated the feature
extension version which covers it, so writing a `float16` value needs
`Vector Type Support=v2`. The driver reports this itself rather than letting the server
see a value it did not negotiate.
- A column's base type and number of dimensions are available from the column schema:
`reader.GetColumnSchema()[i]["VectorBaseType"]` and `["VectorDimensions"]`. Both are `null`
for columns which are not vectors, **including a vector column the server returned as
`varchar(max)` because the connection did not negotiate its base type**. So at `v1` a
`float16` column reports `null` for both, and at `off` so does every vector column; query
`sys.columns.vector_base_type` and `sys.columns.vector_dimensions` instead when the
connection has not negotiated the column's base type. Where the column *is* negotiated,
these properties are the only way to tell the two base types apart when a `float16` column
is surfaced as a JSON string, because `GetFieldType` reports `string` for such a column
just as it does for a `varchar` one:

```csharp
DbColumn column = reader.GetColumnSchema()[0];

string baseType = (string)column["VectorBaseType"]; // "float16" | "float32"
int dimensions = (int) column["VectorDimensions"]; // element count

// The dimension count is known before any row is read, so a buffer can be sized once.
float[] buffer = new float[dimensions];

while (reader.Read())
{
// Widening from float16 is exact, so single precision reads either base type
// losslessly, and is the only strongly typed option on .NET Framework.
reader.GetSqlVector<float>(0).Memory.Span.CopyTo(buffer);
}
```

> [!NOTE]
> On .NET Framework, `float16` workloads do more work per value than they do on .NET.
> Because `System.Half` isn't available, the driver widens each element to single
> precision when it reads a `float16` column, and renders the result as a JSON array for
> the string read paths. On .NET, a `SqlVector<Half>` wraps the payload the server sent,
> so no per-element conversion takes place. Writes differ in the same way: .NET can send a
> `SqlVector<Half>` unchanged, while .NET Framework sends a JSON string or a
> `SqlVector<float>`, which the driver converts to the column's base type.
>
> If you read `float16` columns in bulk on .NET Framework, prefer
> `GetSqlVector<float>` over the string read paths. Both widen the elements, but only the
> string paths also serialize the result.

#### Vector Feature Extension Versions

The vector base types available on a connection are negotiated through the `VECTORSUPPORT`
feature extension (`0x0E`):

| Version | Meaning |
|---------|---------|
| `0` | The server does not support vectors. Vector columns are returned as `varchar(max)`. |
| `1` | `float32` is supported. Columns with any other base type are returned as `varchar(max)`. |
| `2` | `float16` is supported in addition to `float32`. |

The version requested at login is chosen by the `Vector Type Support` connection string
keyword, which may also be written `VectorTypeSupport`, and the server acknowledges the
highest version they have in common:

| Keyword value | Requested version |
|---------------|-------------------|
| `off` | The feature extension is not requested at all. |
| `v1` | `1` — this is the **default**. |
| `v2` | `2` |

The default is `v1`, so an application opts in to the `float16` representation rather than
receiving it when it upgrades the driver. The equivalent property is
`SqlConnectionStringBuilder.VectorTypeSupport`, of type `SqlVectorTypeSupport`.

```csharp
var builder = new SqlConnectionStringBuilder(connectionString)
{
VectorTypeSupport = SqlVectorTypeSupport.V2
};
```

## SqlCommand Execution Modes

### ExecuteNonQuery
Expand Down
217 changes: 217 additions & 0 deletions doc/samples/SqlVectorFloat16Example.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
namespace SqlVectorFloat16Example;

// VectorFloat16ConsoleApp: Demonstrates working with the float16 base type of the
// SQL Server vector datatype via Microsoft.Data.SqlClient
//
// Highlights:
// - Creates a table with a vector(3, float16) column
// - Inserts vectors using SqlVector<Half> on .NET
// - Inserts vectors from .NET Framework, where System.Half is unavailable
// - Reads float16 vectors as SqlVector<Half>, as widened SqlVector<float>, and as JSON
// - Inspects a column's base type and number of dimensions
// - Converts between the float16 and float32 base types, where the server permits it
//
// Requirements:
// - SQL Server 2025 and above, with PREVIEW_FEATURES enabled for the database
// - Microsoft.Data.SqlClient (8.0.0 and above)
//<Snippet1>
using Microsoft.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlTypes;
using System;
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;

class VectorFloat16ConsoleApp
{
// It is recommended to use a secure connection string in production code with valid cert.
//
// "Vector Type Support=v2" opts in to the float16 base type. It defaults to v1, under
// which a float16 column is returned as a varchar(max) containing a JSON array.
private const string ConnectionString =
"Server=localhost;Database=Demo2;Integrated Security=true;Encrypt=true;TrustServerCertificate=true;Vector Type Support=v2;";

private const string TableName = "[dbo].[VectorFloat16Demo]";

static async Task Main()
{
try
{
using var conn = new SqlConnection(ConnectionString);
await conn.OpenAsync();

await CreateObjectsAsync(conn);

await InsertVectorsAsync(conn);
await ReadVectorsAsync(conn);
await ReadColumnMetadataAsync(conn);
await ConvertBetweenBaseTypesAsync(conn);
}
catch (SqlException ex)
{
Console.Error.WriteLine($"SQL ERROR: {ex.Message}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"ERROR: {ex}");
}
}

private static async Task CreateObjectsAsync(SqlConnection conn)
{
// The float16 base type is in preview, so it has to be enabled for the database.
string setup = $@"
ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON;
IF OBJECT_ID(N'{TableName}', N'U') IS NOT NULL DROP TABLE {TableName};
CREATE TABLE {TableName}
(
Id INT IDENTITY(1,1) PRIMARY KEY,
VectorData vector(3, float16) NULL
);";
using var cmd = new SqlCommand(setup, conn);
await cmd.ExecuteNonQueryAsync();
}

#region InsertFloat16Vectors
private static async Task InsertVectorsAsync(SqlConnection conn)
{
string insertSql = $@"INSERT INTO {TableName}(VectorData) VALUES(@VectorData);";
using var cmd = new SqlCommand(insertSql, conn);
var p = new SqlParameter("@VectorData", SqlDbTypeExtensions.Vector);
cmd.Parameters.Add(p);

#if NET
// On .NET, a float16 vector is represented by SqlVector<Half>.
p.Value = new SqlVector<Half>(new Half[] { (Half)1.5f, (Half)2.5f, (Half)3.5f });
await cmd.ExecuteNonQueryAsync();
#endif

// System.Half is unavailable on .NET Framework, so a float16 vector cannot be
// represented directly there. A vector of single precision values can be used
// instead: SQL Server converts it to the column's base type. The conversion loses
// precision for values which float16 cannot represent exactly, and fails for values
// outside its range, in the same way as inserting a JSON literal does.
p.Value = new SqlVector<float>(new float[] { 4.5f, 5.5f, 6.5f });
await cmd.ExecuteNonQueryAsync();

// A JSON array can also be used, which SQL Server parses directly into the column's
// base type.
cmd.Parameters.Clear();
cmd.Parameters.Add(new SqlParameter("@VectorData", SqlDbType.VarChar, -1) { Value = "[7.5,8.5,9.5]" });
await cmd.ExecuteNonQueryAsync();

Console.WriteLine("Inserted float16 vectors.");
}
#endregion

#region ReadFloat16Vectors
private static async Task ReadVectorsAsync(SqlConnection conn)
{
string selectSql = $@"SELECT Id, VectorData FROM {TableName} ORDER BY Id;";
using var cmd = new SqlCommand(selectSql, conn);
using var reader = await cmd.ExecuteReaderAsync();

Console.WriteLine("\nReading rows...");
while (await reader.ReadAsync())
{
int id = reader.GetInt32(0);

#if NET
// On .NET, the column's own base type is available directly. A SqlVector<Half>
// wraps the payload the server sent, so no per-element conversion takes place.
SqlVector<Half> exact = reader.GetSqlVector<Half>(1);
Console.WriteLine($" Id={id} as Half: [{string.Join(", ", exact.Memory.ToArray())}]");
#endif

// On any framework, the elements can be widened to single precision. Widening
// from float16 is exact, so no information is lost. On .NET Framework this is
// the cheapest strongly typed read: the string read paths widen the elements
// as well, and then serialize the result as a JSON array.
SqlVector<float> widened = reader.GetSqlVector<float>(1);
Console.WriteLine($" Id={id} as float: [{string.Join(", ", widened.Memory.ToArray())}]");

// The value can also be read as a JSON array.
Console.WriteLine($" Id={id} as JSON: {reader.GetString(1)}");
}
}
#endregion

#region ReadVectorColumnMetadata
private static async Task ReadColumnMetadataAsync(SqlConnection conn)
{
using var cmd = new SqlCommand($@"SELECT VectorData FROM {TableName};", conn);
using var reader = await cmd.ExecuteReaderAsync();

DbColumn column = reader.GetColumnSchema()[0];

// A vector column reports its base type and number of dimensions, which is how an
// application can discover them without querying the server's catalog views. Both
// are null for columns which are not vectors.
Console.WriteLine($"\nColumn base type: {column["VectorBaseType"]}");
Console.WriteLine($"Column dimensions: {column["VectorDimensions"]}");

// The base type is what a caller which does not know the schema in advance needs in
// order to choose a read path. GetFieldType is not enough on its own: it reports
// string for a float16 column on .NET Framework, which is also what a plain
// varchar column reports, and it cannot distinguish the two base types at all when
// the caller wants to read both as SqlVector<float>.
string baseType = (string)column["VectorBaseType"];
int dimensions = (int)column["VectorDimensions"];

// Allocate once, knowing the length before reading any row.
float[] buffer = new float[dimensions];

while (await reader.ReadAsync())
{
if (reader.IsDBNull(0))
{
continue;
}

switch (baseType)
{
case "float32":
reader.GetSqlVector<float>(0).Memory.Span.CopyTo(buffer);
break;

case "float16":
// Widening from float16 is exact, so single precision is a lossless
// representation for a caller which wants one array type throughout.
reader.GetSqlVector<float>(0).Memory.Span.CopyTo(buffer);
break;

default:
throw new NotSupportedException($"Unknown vector base type {baseType}.");
}

Console.WriteLine($"Read {dimensions} {baseType} elements: [{string.Join(",", buffer)}]");
}
}
#endregion

#region ConvertBetweenBaseTypes
private static async Task ConvertBetweenBaseTypesAsync(SqlConnection conn)
{
// Whether SQL Server converts between the two base types depends on the server: some
// builds block both implicit and explicit conversion and report error 42238. Where it
// is permitted, a vector read from a column of one base type can be written to a
// column of the other. Where it is not, write a JSON string instead, which every
// server accepts and which the examples above use.
try
{
using var cmd = new SqlCommand(
"SELECT CAST(CAST('[1.5,2.5,3.5]' AS vector(3, float16)) AS vector(3, float32));", conn);
using var reader = await cmd.ExecuteReaderAsync();

await reader.ReadAsync();
Console.WriteLine($"\nfloat16 converted to float32: {reader.GetString(0)}");
}
catch (SqlException ex)
{
Console.WriteLine($"\nThis server does not convert between vector base types: {ex.Message}");
}
}
#endregion
}
//</Snippet1>
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,23 @@ The following example converts an existing connection string from using SQL Serv
</para>
</remarks>
</PoolBlockingPeriod>
<VectorTypeSupport>
<summary>
Gets or sets the level of support for the <c>vector</c> data type which the driver
negotiates with the server.
</summary>
<value>
A <see cref="T:Microsoft.Data.SqlClient.SqlVectorTypeSupport" />. The default is
<see cref="F:Microsoft.Data.SqlClient.SqlVectorTypeSupport.V1" />.
</value>
<remarks>
Corresponds to the <c>Vector Type Support</c> and <c>VectorTypeSupport</c> keys
within the connection string, whose values are <c>off</c>, <c>v1</c> and <c>v2</c>.
</remarks>
<exception cref="T:System.ArgumentOutOfRangeException">
The value is not a member of <see cref="T:Microsoft.Data.SqlClient.SqlVectorTypeSupport" />.
</exception>
</VectorTypeSupport>
<Pooling>
<summary>
Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly opened every time that the connection is requested.
Expand Down
17 changes: 16 additions & 1 deletion doc/snippets/Microsoft.Data.SqlClient/SqlDataReader.xml
Original file line number Diff line number Diff line change
Expand Up @@ -981,8 +981,23 @@ The <xref:Microsoft.Data.SqlClient.SqlDataReader.GetSchemaTable%2A> method retur
<exception cref="T:System.InvalidCastException">
The retrieved data is not compatible with the <see cref="T:Microsoft.Data.SqlTypes.SqlVector`1" /> type.
</exception>
<exception cref="T:System.NotSupportedException">
The column's base type cannot be represented by <typeparamref name="T" /> without losing precision.
</exception>
<remarks>
No conversions are performed; therefore, the data retrieved must already be a vector value, or an exception is generated.
<format type="text/markdown">
<![CDATA[
The data retrieved must already be a vector value, or an exception is generated.

A column whose base type matches `T` is returned as it was sent, without conversion. A base
type which widens to `T` without losing precision is converted: a `float16` column can be read
as `SqlVector<float>`, which is the only strongly typed way to read one on .NET Framework,
where `System.Half` does not exist. Narrowing is rejected with
<xref:System.NotSupportedException> rather than performed silently, so a `float32` column
cannot be read as `SqlVector<Half>`.

]]>
</format>
</remarks>
</GetSqlVector>
<GetSqlMoney>
Expand Down
Loading
Loading