You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This PR adds native support for SQL Server 2025's VECTOR data type, enabling efficient storage and retrieval of vector embeddings for AI/ML workloads in Go applications.
The VECTOR type is designed for similarity search scenarios, storing fixed-dimensional arrays of floating-point numbers optimized for operations like VECTOR_DISTANCE.
Features
Core Types
Vector - Vector type implementing driver.Valuer and sql.Scanner interfaces
NullVector - Nullable wrapper for database columns that allow NULL values
VectorElementType - Enum for element precision (float32/float16)
Element Type Support
Type
Constant
Bytes
Max Dimensions
Notes
float32
VectorElementFloat32
4
1998
Default, fully supported
float16
VectorElementFloat16
2
3996
Preview feature, requires PREVIEW_FEATURES = ON
Wire Format Support
Binary TDS format: Native encoding/decoding matching SQL Server's internal format
JSON format: Backward-compatible parameter transmission for older drivers/servers
Automatic format selection: Uses binary when supported, falls back to JSON
Framework Compatibility
Direct support for []float32 and []float64 parameter binding (no wrapper needed)
Vector and NullVector types implement sql.Scanner for decoding binary data
Native protocol version 1 preserves float32 and float16 result metadata
Float16 parameters use JSON; native float16 results use the binary format
JSON fallback has no element-type metadata: arrays through 1998 dimensions decode as float32, including short float16 columns
Connection String Option
vectortypesupport=v1 # Enable native binary TDS results and float32 parameters
vectortypesupport=off # Default: JSON format for backward compatibility
API Summary
Creating Vectors
// From float32 slice (default)v, err:=mssql.NewVector([]float32{1.0, 2.0, 3.0})
// From float64 slice (converted to float32)v, err:=mssql.NewVectorFromFloat64([]float64{1.0, 2.0, 3.0})
// With explicit element type (for float16)v, err:=mssql.NewVectorWithType(mssql.VectorElementFloat16, values)
Inserting Vectors
// Using Vector type_, err=db.Exec("INSERT INTO embeddings (v) VALUES (@p1)", v)
// Using []float32 directly (convenient for frameworks)_, err=db.Exec("INSERT INTO embeddings (v) VALUES (@p1)", []float32{1.0, 2.0, 3.0})
// Using []float64 (auto-converted to float32)_, err=db.Exec("INSERT INTO embeddings (v) VALUES (@p1)", []float64{1.0, 2.0, 3.0})
Reading Vectors
// To Vector type (recommended)varv mssql.Vectorerr:=row.Scan(&v)
fmt.Println(v.Dimensions(), v.Values())
// Nullable columnsvarnv mssql.NullVectorerr:=row.Scan(&nv)
ifnv.Valid {
// Use nv.Vector
}
Similarity Search
queryVec, _:=mssql.NewVector([]float32{1.0, 0.0, 0.0})
rows, _:=db.Query(` SELECT name, VECTOR_DISTANCE('cosine', embedding, CAST(@p1 AS VECTOR(3))) as distance FROM documents ORDER BY distance`, queryVec)
Header structure: magic (0xA9), version (0x01), dimensions (2 bytes), element type, reserved (3 bytes)
NULL Handling
NullVector{Valid: false} sends as NVARCHAR(1) NULL for dimension-agnostic NULL insertion
This workaround avoids SQL Server's requirement for matching dimensions on NULL vector parameters
Special Value Handling
NaN and Infinity: Rejected by JSON parameter paths because JSON cannot represent them as numbers
Precision warnings: Optional callback for float64→float32 precision loss detection
Thread Safety
SetVectorPrecisionLossHandler() uses mutex for thread-safe handler updates
Precision warnings fire once per vector (first loss only) for performance
Driver Value Types
The readVectorType function returns []byte (raw binary vector payload), which is a standard database/sql/driver.Value type. This follows Go database driver conventions where drivers return primitive types and custom types handle decoding via sql.Scanner.
Applications should scan to Vector or NullVector types, which implement sql.Scanner and decode the binary representation automatically.
Testing
Unit Tests (go test ./...)
Vector encoding/decoding (float32 and float16)
Float16 conversion accuracy
JSON serialization/deserialization
NULL handling
Error cases (invalid data, unsupported types)
Maximum dimension limits
Type metadata functions
Integration Tests (requires SQL Server 2025)
Insert/select round-trips
NULL vector handling
Different dimension counts (1D to 500D)
Special floating-point values
VECTOR_DISTANCE similarity search
Column metadata verification
Batch operations
Native and JSON-fallback bulk copy, including direct slices and NULL vectors
Float16 with PREVIEW_FEATURES
Test Coverage
New vector-specific tests: 50+ test functions
All existing tests continue to pass
Graceful skip on pre-2025 SQL Server instances
Requirements
SQL Server 2025 or later for VECTOR type support
go-mssqldb 1.9.7 or later
For float16: ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON
Migration Notes
Runtime backward compatible: Default vectortypesupport=off uses JSON format
Source compatible: The option is stored in the existing msdsn.Config.Parameters map, so the exported struct layout is unchanged
Opt-in optimization: Set vectortypesupport=v1 for native binary vector results and float32 parameters with SQL Server 2025 and later versions
We asked whether this feature needs a connection string flag, and whether "breaking changes aren't
something to worry about in Go" holds. Summary of what the primary sources say.
Go's compatibility rules cover API shape and exported struct layout, not behavior. Keeping Your Modules Compatible gives the rule
"add, don't change or remove". The implementation keeps the exported msdsn.Config layout
unchanged by storing vectortypesupport in the existing Parameters map and exposing an accessor
method. Existing keyed and unkeyed literals continue to compile. The same page is explicit that
source compatibility is not the whole question:
However, behavior changes can also break users, even if user code continues to compile.
Its worked example is json.Decoder and unknown fields: rather than change behaviour for everyone,
the Go team added Decoder.DisallowUnknownFields so that "calling this method opts a user in to the
new behavior, but not doing so preserves the old behavior for existing users."
Go 1 and the Future of Go Programs does not extend to us. It is
scoped to "the language and ... the standard packages", is explicitly "at the source level", and
only expresses a hope that third-party modules behave similarly. No tooling detects a behavioural
regression in a v1.x minor release; gorelease checks API shape.
A Go driver cannot hand back an arbitrary type. driver.Value is a closed set: int64, float64, bool, []byte, string, time.Time, nil. Richer types are reconstructed on the
destination side via sql.Scanner. Rows.Scan converts within documented rules and errors
outside them. So the caller's scan destination, not the driver, decides the Go type. This is a real
difference from JDBC, where getObject() makes the driver's choice the caller's type.
Two places a driver's type choice does reach callers unfiltered:
*interface{}, where "Scan copies the value provided by the underlying driver without conversion"
ColumnTypeScanType(), which tooling reads
The principle we settled on:
A connection string flag is warranted when the feature changes the bytes on the wire or carries a
performance/compatibility trade-off the caller must choose. It is not warranted when the change is
confined to metadata that database/sql already insulates callers from.
Applied here: vector clears that bar, so vectortypesupport stays. Negotiating the binary
protocol changes what the server sends. Native results preserve float32 and float16 metadata,
while float16 parameters use JSON and float32 parameters use the binary path. JDBC/ODBC already
expose vectorTypeSupport, so callers moving between drivers expect the same knob. Default off
preserves existing behaviour byte for byte.
❌ Patch coverage is 85.31140% with 125 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.03%. Comparing base (60674a5) to head (f88ff22).
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds native support for SQL Server 2025's VECTOR data type, enabling efficient storage and retrieval of vector embeddings for AI/ML workloads. The implementation includes Vector and NullVector types that implement standard database interfaces, support for both float32 and float16 element types, binary format encoding/decoding, JSON format transmission for parameterized queries, and comprehensive test coverage with graceful degradation for pre-2025 servers.
Changes:
Added Vector and NullVector types with driver.Valuer and sql.Scanner interface implementations
Implemented binary format encoding/decoding matching SQL Server's native TDS format
Added float16 conversion functions with IEEE 754 half-precision support
Extended types.go with typeVectorN constant and read/write functions for vector data handling
Updated parameter binding in mssql.go to transmit vectors as JSON strings for backward compatibility
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
File
Description
vector.go
Core Vector type implementation with encoding/decoding, float16 conversion, and helper functions
vector_test.go
Comprehensive unit tests for Vector type including encoding, decoding, and edge cases
vector_db_test.go
Database integration tests with SQL Server 2025+ version detection and preview feature handling
types.go
Added typeVectorN constant and TDS stream read/write functions for vector data
mssql.go
Extended makeParam to handle Vector/NullVector types via JSON string transmission
doc/how-to-use-vectors.md
Complete usage documentation with examples and best practices
Previously missed (1) — in code that hasn't changed since the last review.
bulkcopy.go:768
This path always serializes a VECTOR column as the native binary payload, regardless of vectortypesupport=off or whether the session negotiated vectorSupported. Consequently CopyIn bypasses the documented JSON fallback and can send a wire format that the connection explicitly disabled. The fallback needs to be selected before the bulk column metadata is emitted (and the row writer must then send the JSON string).
doc/how-to-use-vectors.md:262
This note promises that native float16 results preserve the element type, but SQL Server 2025 currently transports float16 vectors as VARCHAR(MAX) because binary float16 transport is not available. Those results arrive as JSON/text and Vector.Scan cannot recover VectorElementFloat16 (for 1–1998 dimensions it reports float32), so the documentation currently guarantees behavior the server cannot provide. Please qualify this as a current limitation and reserve the preservation claim for servers that actually send the binary header.
> **Note:** The element type is determined by the SQL Server column definition (e.g., `VECTOR(3)` for float32, `VECTOR(3, float16)` for float16), not by the Go-side `Vector` struct. With `vectortypesupport=v1`, float32 parameters use the native binary vector format, while float16 parameters use JSON. Native float32 and float16 results preserve the element type from the binary header.
msdsn/conn_str.go:685
This connection option only negotiates vector protocol v1, but SQL Server's vector negotiation uses v1 for FLOAT32 and v2 for FLOAT16. As a result, the advertised native FLOAT16 result/bulk-copy support cannot be negotiated (and the FLOAT16 integration test opens a v1 connection). Add version-aware v2 negotiation, or restrict FLOAT16 to the JSON/fallback path and update the docs/tests.
if vectorSupport, ok := params[VectorTypeSupportParam]; ok {
switch strings.ToLower(vectorSupport) {
case "off", "0":
case "v1", "1":
default:
vector_db_test.go:817
This round-trip only checks dimensions, so it passes even when SQL Server returns the float16 column as VARCHAR(MAX)/JSON and Vector.Scan sets ElementType to float32. Because the PR documents native float16 metadata preservation, the test should distinguish the current JSON fallback from a binary result and assert the corresponding ElementType; otherwise it cannot catch the metadata behavior this feature claims to support.
The note read as an unconditional guarantee that float16 results preserve
their element type. Make the condition explicit and point at the JSON
fallback rule documented immediately below.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e2e8f0d6-7010-4642-a314-7700c265085e
The latest Copilot review generated no inline threads, but its summary lists four suppressed comments. Here is where each one landed after checking it against the code.
Refuted: the JSON fallback is already selected, and the server selects it.
makeBulkVectorParam opens with if col.ti.TypeId != typeVectorN { value, _ := vector.Value(); return b.makeParam(value, col) } (bulkcopy.go:747). That is the JSON fallback. col.ti is not built by the driver: getMetadata runs SET FMTONLY ON plus select * from <table> and assigns b.metadata = rows.(*Rows).cols (bulkcopy.go:353), so the type comes from the server's COLMETADATA token.
The server only describes a column as typeVectorN when the session negotiated vector support. The login packet adds the vector feature extension only under VectorTypeSupportV1 (tds.go:1108), and sess.vectorSupported is set only on the server's ack (tds.go:1428). On a connection with vectortypesupport=off the column comes back as varchar, the type check fails, and the JSON path runs. There is no reachable state where bulk copy emits native binary on a connection that disabled vector support.
2. doc/how-to-use-vectors.md:262: the note promises element-type preservation the JSON path cannot deliver.
Fixed in a4e1e4d. The sentence read as an unconditional guarantee. It now states the condition:
When a result arrives in the native binary vector format, the driver reads the element type from the binary header, so float32 and float16 are both preserved. When a result arrives as JSON, the element type is inferred instead, as described below.
The blockquote immediately after already gave the inference rule (1 through 1998 dimensions scan as float32, larger scan as float16), so this connects the two. I deliberately did not add any claim about what SQL Server 2025 puts on the wire for float16, because I have no server here to confirm it.
3. msdsn/conn_str.go:685: only v1 is negotiated, so native float16 is unreachable.
The observation is accurate about the code. featureExtVector.toBytes() returns {0x01} and its own comment says "Version 1 of vector support (float32 vectors)" (tds.go:1502).
The suggested remedy has two halves. Version-aware v2 negotiation is a new feature, and this is a merge run, so I have not added it. The other half, keeping float16 on the JSON path, is what the code already does: mssql.go:1120 gates native encoding on s.c.sess.vectorSupported && v.ElementType == VectorElementFloat32, so float16 parameters take the JSON path today. The documentation half is item 2 above.
4. vector_db_test.go:817: the float16 round trip would pass against a JSON result.
Left for you to decide, with two corrections to the observation.
The test asserts values as well as dimensions, not dimensions alone. But the substantive point holds: it never asserts ElementType, so a JSON result with three dimensions scans as float32 and the test still passes.
I did not add the assertion because the test cannot tell which path the server used. The comment asks to distinguish the JSON fallback from a binary result and assert accordingly, and the public API exposes no per-row signal for that. Asserting float16 unconditionally would fail whenever the JSON path is taken. This is also an integration test that skips without a connection string, and I have no SQL Server here, so I could not validate any version of it. Adding an assertion I cannot run, on a path whose behavior depends on the server build, is how CI breaks for you later.
On the red AppVeyor check.continuous-integration/appveyor/branch build 54710443 failed with fatal: Remote branch feature/vector-support not found in upstream origin. Earlier in this run I pushed this branch to microsoft/go-mssqldb by mistake and deleted it; that push queued a branch build which then had nothing to clone. It is not a code failure. continuous-integration/appveyor/pr build 54710444 ran the same commit and passed.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
bulkcopy.go ignores vectortypesupport=off for VECTOR bulk copies, and the broad protocol changes require human review.
Review details
Suppressed comments (1)
bulkcopy.go:766
When the destination column is VECTOR, this branch always encodes a native binary payload and never consults the connection's vectortypesupport setting. Thus vectortypesupport=off (and the TestVectorBulkCopyJSONFallback case) still sends typeVectorN binary data, contrary to the documented default-off/JSON fallback and the PR description. Either implement the off-mode bulk representation if SQL Server supports it, or explicitly make bulk VECTOR native-only and update the option contract and test so this behavior is not presented as a fallback.
Refuted, same finding as the first item in #307 (comment), now with the named test.
TestVectorBulkCopyJSONFallback opens its connection through setupVectorTestWithSupport(t, 3, false, msdsn.VectorTypeSupportOff) (vector_db_test.go:543). That session never sends the vector feature extension, so the server never acks it, and the SET FMTONLY ON metadata that getMetadata assigns at bulkcopy.go:353 describes the column as varchar rather than typeVectorN.
makeBulkVectorParam then takes its first branch at bulkcopy.go:747 and returns b.makeParam(vector.Value(), col), which is the JSON path. Line 766 is unreachable on a connection that disabled vector support, so the branch does not need to read the setting itself. The setting is already expressed in the server-supplied column type.
Use StreamError for malformed VECTOR metadata and payload lengths so bad responses invalidate the connection. Rename the bulk test to reflect native encoding without VECTOR negotiation.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e2e8f0d6-7010-4642-a314-7700c265085e
Require non-encrypted VECTOR values to match the declared column size and cover shorter payloads with a regression test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e2e8f0d6-7010-4642-a314-7700c265085e
Preserve vector pointers for native parameter handling
mssql_go19.go:102
convertInputParameter still sends *Vector and *NullVector through driver.DefaultParameterConverter, which calls their Value methods and turns non-nil pointers into JSON strings (and nil pointers into an untyped nil) before Stmt.makeParam can reach its pointer cases in mssql.go:1123-1132. As a result, pointer arguments never use native VECTOR metadata and nil pointers lose the nvarchar(1) NULL workaround. Preserve both pointer types here so the downstream pointer cases remain reachable.
CheckNamedValue runs convertInputParameter before Stmt.makeParam, but this switch preserves only the value forms. A non-nil *Vector or *NullVector therefore falls through to driver.DefaultParameterConverter, which calls Value() and turns it into an NVARCHAR JSON string; with vectortypesupport=v1 this silently bypasses native VECTOR encoding (and nil pointers cannot reach the typed NULL handling in mssql.go). Preserve the pointer forms here as well, and add a parameter-path test for them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SQL Server 2025 VECTOR Type Support
Overview
This PR adds native support for SQL Server 2025's VECTOR data type, enabling efficient storage and retrieval of vector embeddings for AI/ML workloads in Go applications.
The VECTOR type is designed for similarity search scenarios, storing fixed-dimensional arrays of floating-point numbers optimized for operations like
VECTOR_DISTANCE.Features
Core Types
Vector- Vector type implementingdriver.Valuerandsql.ScannerinterfacesNullVector- Nullable wrapper for database columns that allow NULL valuesVectorElementType- Enum for element precision (float32/float16)Element Type Support
Wire Format Support
Framework Compatibility
[]float32and[]float64parameter binding (no wrapper needed)VectorandNullVectortypes implementsql.Scannerfor decoding binary dataConnection String Option
API Summary
Creating Vectors
Inserting Vectors
Reading Vectors
Similarity Search
Files Changed
New Files
Modified Files
Implementation Details
TDS Protocol Integration
featExtVECTORSUPPORT(0x0E) for LOGIN7 negotiationNULL Handling
NullVector{Valid: false}sends asNVARCHAR(1) NULLfor dimension-agnostic NULL insertionSpecial Value Handling
Thread Safety
SetVectorPrecisionLossHandler()uses mutex for thread-safe handler updatesDriver Value Types
The
readVectorTypefunction returns[]byte(raw binary vector payload), which is a standarddatabase/sql/driver.Valuetype. This follows Go database driver conventions where drivers return primitive types and custom types handle decoding viasql.Scanner.Applications should scan to
VectororNullVectortypes, which implementsql.Scannerand decode the binary representation automatically.Testing
Unit Tests (
go test ./...)Integration Tests (requires SQL Server 2025)
VECTOR_DISTANCEsimilarity searchPREVIEW_FEATURESTest Coverage
Requirements
ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ONMigration Notes
vectortypesupport=offuses JSON formatmsdsn.Config.Parametersmap, so the exported struct layout is unchangedvectortypesupport=v1for native binary vector results and float32 parameters with SQL Server 2025 and later versionsRelated Documentation
Acknowledgments
Thanks to David Shiflet (@shueybubbles) for the review feedback that improved:
[]float32and[]float64parameter supportConnection string flag: research and decision
We asked whether this feature needs a connection string flag, and whether "breaking changes aren't
something to worry about in Go" holds. Summary of what the primary sources say.
Go's compatibility rules cover API shape and exported struct layout, not behavior.
Keeping Your Modules Compatible gives the rule
"add, don't change or remove". The implementation keeps the exported
msdsn.Configlayoutunchanged by storing
vectortypesupportin the existingParametersmap and exposing an accessormethod. Existing keyed and unkeyed literals continue to compile. The same page is explicit that
source compatibility is not the whole question:
Its worked example is
json.Decoderand unknown fields: rather than change behaviour for everyone,the Go team added
Decoder.DisallowUnknownFieldsso that "calling this method opts a user in to thenew behavior, but not doing so preserves the old behavior for existing users."
Go 1 and the Future of Go Programs does not extend to us. It is
scoped to "the language and ... the standard packages", is explicitly "at the source level", and
only expresses a hope that third-party modules behave similarly. No tooling detects a behavioural
regression in a v1.x minor release;
goreleasechecks API shape.A Go driver cannot hand back an arbitrary type.
driver.Valueis a closed set:int64,float64,bool,[]byte,string,time.Time, nil. Richer types are reconstructed on thedestination side via
sql.Scanner.Rows.Scanconverts within documented rules and errorsoutside them. So the caller's scan destination, not the driver, decides the Go type. This is a real
difference from JDBC, where
getObject()makes the driver's choice the caller's type.Two places a driver's type choice does reach callers unfiltered:
*interface{}, where "Scan copies the value provided by the underlying driver without conversion"ColumnTypeScanType(), which tooling readsThe principle we settled on:
Applied here: vector clears that bar, so
vectortypesupportstays. Negotiating the binaryprotocol changes what the server sends. Native results preserve float32 and float16 metadata,
while float16 parameters use JSON and float32 parameters use the binary path. JDBC/ODBC already
expose
vectorTypeSupport, so callers moving between drivers expect the same knob. Defaultoffpreserves existing behaviour byte for byte.