Skip to content

Commit 8b4ed96

Browse files
RubenCerna2079souvikghosh04Copilotaaronburtle
authored
Add Support for Vector Data Type in SQL for REST (#3677)
## Why make this change? - Solves issue #3654 ## What is this change? These changes add the base to support the Vector data type for REST, GraphQL, and MCP. This PR focuses mainly on the REST endpoint, ensuring that it is able to both read and write to the vector columns through it. Lastly, it also adds support to ensure that OpenAPI is able to show columns with the Vector data type as `array: [numeric]`. The following files were changed to show that DAB is able to support the Vector data type and also change the type internally to system types it can work with. Such as the `float` type for `DbType`, the `Numeric` type for `JsonDataType`, and `SINGLE_TYPE` for GraphQL. - SqlTypeConstants - TypeHelper.cs - SqlSchemaConverter.cs The following file was changed to allow DAB to save the vector columns as an array type, which is necessary for GraphQL to be able to know if the column of an entity is a simple type or an array. - MsSqlMetadataProvider.cs The following files were changed to parse the information from the user to a format that can be used by DAB to create the queries for SQL. - BaseSqlQueryStructure.cs: Parses the array information by creating a new function that checks for the simple type of the array (e.g If it is an array of floats it will use the type float) and parse each value inside the array. - MsSqlQueryExecutor.cs: Parses the values given by the user and adds them to an SqlVector object which allows for an easier way to query the values. The following file was changed to allow DAB's OpenAPI to support array types by checking if the main type is `array` and then taking the subtype and creating the appropriate item with it. - OpenApiDocumentor.cs Note: All of these changes are only applicable to Vectors of Float32, support for Vectors of Float16 will have to wait as support for it in .NET is still missing. ## How was this tested? - [x] Integration Tests - [ ] Unit Tests Added tests that ensure the following scenarios on REST: - Read regular vector - Read vector with max number of dimensions possible - Read vector with null value - Read vector with `$first` argument - Read vector with `$after` argument - Read vector with `$filter` argument - fails - Read vector with `$orderby` argument - fails - Insert regular vector - Insert vector with null value - Insert vector with number values in scientific format - Insert vector with number values in string format - Insert vector with more dimensions than allowed - fails - Insert vector with non-valid values - fails - Update vector with new values ## Sample Request(s) Note: Currently the values will be outputted in scientific notation, this will be changed in the future #3680 GET https://localhost:5001/api/dbo_normalvector/ <img width="429" height="609" alt="image" src="https://github.com/user-attachments/assets/f0c16c4a-4e6a-4199-8d26-33469af2feab" /> POST https://localhost:5001/api/dbo_normalvector/ ``` { "ProductID": 2100, "Embedding": [ 1.15202468461012559910005, 0.00005, 5.111111111 ] } ``` <img width="303" height="319" alt="image" src="https://github.com/user-attachments/assets/3b660ddd-dbe8-4f24-9025-fc526bbd64e4" /> PATCH https://localhost:5001/api/dbo_normalvector/ (Adding new row) ``` { "ProductID": 3100, "Embedding": [ 1.15202468461012559910005, 0.00005, 5.111111111 ] } ``` <img width="472" height="318" alt="image" src="https://github.com/user-attachments/assets/c22cf689-bf60-4a1f-934b-91bbad2de3ae" /> PATCH https://localhost:5001/api/dbo_normalvector/ (Update existing row) ``` { "ProductID": 2100, "Embedding": [ 25, 1.2, 510.35 ] } ``` <img width="363" height="315" alt="image" src="https://github.com/user-attachments/assets/7af1259b-834a-4881-a80f-ab011ee1123d" /> PUT https://localhost:5001/api/dbo_normalvector/ (Adding new row) ``` { "ProductID": 4100, "Embedding": [ 0, 1, 37 ] } ``` <img width="310" height="289" alt="image" src="https://github.com/user-attachments/assets/1aba2a11-5efe-443d-9f89-62b510fe4a42" /> PUT https://localhost:5001/api/dbo_normalvector/ (Edit existing row) ``` { "ProductID": 2100, "Embedding": [ 0.220224962, 0.66666667, 37 ] } ``` <img width="386" height="303" alt="image" src="https://github.com/user-attachments/assets/abf79182-716c-407f-af2a-0396e26a7c26" /> https://localhost:5001/swagger <img width="348" height="142" alt="image" src="https://github.com/user-attachments/assets/5d65470b-7df2-4725-a70d-ec9eeb69e7d0" /> <img width="302" height="277" alt="image" src="https://github.com/user-attachments/assets/3b5c08b2-39ed-4bd8-a287-7b6fba3a7684" /> --------- Co-authored-by: souvikghosh04 <souvikofficial04@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
1 parent 0f80a0e commit 8b4ed96

13 files changed

Lines changed: 549 additions & 12 deletions

File tree

config-generators/mssql-commands.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ add Broker --config "dab-config.MsSql.json" --source brokers --permissions "anon
1616
add WebsiteUser --config "dab-config.MsSql.json" --source website_users --permissions "anonymous:create,read,delete,update"
1717
add WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm --graphql "websiteuser_mm:websiteusers_mm" --permissions "anonymous:*"
1818
add SupportedType --config "dab-config.MsSql.json" --source type_table --permissions "anonymous:create,read,delete,update"
19+
add VectorType --config "dab-config.MsSql.json" --source vector_type_table --rest true --graphql false --permissions "anonymous:create,read,delete,update"
20+
update VectorType --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update"
1921
add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permissions "authenticated:create,read,update,delete"
2022
update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read"
2123
update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read" --fields.exclude "price"

src/Core/Models/SqlTypeConstants.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public static class SqlTypeConstants
4848
{ "datetime2", true }, // SqlDbType.DateTime2
4949
{ "datetimeoffset", true }, // SqlDbType.DateTimeOffset
5050
{ "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported)
51-
{ "numeric", true} // Not present in SqlDbType, however can be returned by sql functions like LAG and should map to decimal.
51+
{ "numeric", true}, // Not present in SqlDbType, however can be returned by sql functions like LAG and should map to decimal.
52+
{ "vector", true } // SqlDbType.Vector
5253
};
5354
}

src/Core/Resolvers/MsSqlQueryBuilder.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,8 +666,7 @@ AND ty.name IN
666666
N'hierarchyid',
667667
N'sql_variant',
668668
N'xml',
669-
N'rowversion',
670-
N'vector'
669+
N'rowversion'
671670
)
672671
) THEN 1
673672
ELSE 0
@@ -712,8 +711,7 @@ AND ty.name IN
712711
N'hierarchyid',
713712
N'sql_variant',
714713
N'xml',
715-
N'rowversion',
716-
N'vector'
714+
N'rowversion'
717715
)
718716
)
719717
)

src/Core/Resolvers/MsSqlQueryExecutor.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Azure.Identity;
1919
using Microsoft.AspNetCore.Http;
2020
using Microsoft.Data.SqlClient;
21+
using Microsoft.Data.SqlTypes;
2122
using Microsoft.Extensions.Logging;
2223

2324
namespace Azure.DataApiBuilder.Core.Resolvers
@@ -695,6 +696,19 @@ public override SqlCommand PrepareDbCommand(
695696
parameter.Size = parameterEntry.Value.Length.Value;
696697
}
697698

699+
// if sqldbtype is vector then set the value as an SqlVector object
700+
if (parameter.SqlDbType is SqlDbType.Vector)
701+
{
702+
List<float> values = new();
703+
foreach (float val in (Array)parameter.Value)
704+
{
705+
values.Add(val);
706+
}
707+
708+
SqlVector<float> value = new(values.ToArray());
709+
parameter.Value = value;
710+
}
711+
698712
cmd.Parameters.Add(parameter);
699713
}
700714
}

src/Core/Resolvers/QueryExecutor.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Azure.DataApiBuilder.Core.Models;
1414
using Azure.DataApiBuilder.Service.Exceptions;
1515
using Microsoft.AspNetCore.Http;
16+
using Microsoft.Data.SqlTypes;
1617
using Microsoft.Extensions.Logging;
1718
using Polly;
1819
using Polly.Retry;
@@ -502,7 +503,7 @@ public async Task<DbResultSet>
502503
{
503504
if (!ConfigProvider.GetConfig().MaxResponseSizeLogicEnabled())
504505
{
505-
dbResultSetRow.Columns.Add(columnName, dbDataReader[columnName]);
506+
dbResultSetRow.Columns.Add(columnName, GetColumnInformation(dbDataReader, columnName));
506507
}
507508
else
508509
{
@@ -554,7 +555,7 @@ public DbResultSet
554555
{
555556
if (!ConfigProvider.GetConfig().MaxResponseSizeLogicEnabled())
556557
{
557-
dbResultSetRow.Columns.Add(columnName, dbDataReader[columnName]);
558+
dbResultSetRow.Columns.Add(columnName, GetColumnInformation(dbDataReader, columnName));
558559
}
559560
else
560561
{
@@ -822,7 +823,7 @@ internal int StreamDataIntoDbResultSetRow(DbDataReader dbDataReader, DbResultSet
822823
{
823824
dataRead = columnSize;
824825
ValidateSize(availableBytes, dataRead);
825-
dbResultSetRow.Columns.Add(columnName, dbDataReader[columnName]);
826+
dbResultSetRow.Columns.Add(columnName, GetColumnInformation(dbDataReader, columnName));
826827
}
827828

828829
return dataRead;
@@ -885,6 +886,18 @@ private void ValidateSize(long availableSizeBytes, long sizeToBeReadBytes)
885886
}
886887
}
887888

889+
/// <summary>
890+
/// Helper function to get column information from the DbDataReader and handle special cases like SqlVector<float>.
891+
/// </summary>
892+
/// <param name="dbDataReader"></param>
893+
/// <param name="columnName"></param>
894+
/// <returns></returns>
895+
private static object GetColumnInformation(DbDataReader dbDataReader, string columnName)
896+
{
897+
object value = dbDataReader[columnName];
898+
return value is SqlVector<float> vector ? vector.Memory : value;
899+
}
900+
888901
internal virtual void AddDbExecutionTimeToMiddlewareContext(long time)
889902
{
890903
HttpContext? httpContext = HttpContextAccessor?.HttpContext;

src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Data;
55
using System.Globalization;
66
using System.Net;
7+
using System.Text.Json;
78
using Azure.DataApiBuilder.Auth;
89
using Azure.DataApiBuilder.Config.DatabasePrimitives;
910
using Azure.DataApiBuilder.Config.ObjectModel;
@@ -452,10 +453,49 @@ protected static object ParseParamAsSystemType(string param, Type systemType)
452453
"Guid" => Guid.Parse(param),
453454
"TimeOnly" => TimeOnly.Parse(param),
454455
"TimeSpan" => TimeOnly.Parse(param),
456+
"Single[]" => ParseArrayIntoSystemType(param, systemType),
455457
_ => throw new NotSupportedException($"{systemType.Name} is not supported")
456458
};
457459
}
458460

461+
/// <summary>
462+
/// Takes the array of the parameter we are going to parse and converts each element to the specified system type.
463+
/// </summary>
464+
/// <param name="param"></param>
465+
/// <param name="systemType"></param>
466+
/// <returns></returns>
467+
/// <exception cref="NotSupportedException"></exception>
468+
/// <exception cref="FormatException"></exception>
469+
private static object ParseArrayIntoSystemType(string param, Type systemType)
470+
{
471+
Type typeOfArray;
472+
switch (systemType.Name)
473+
{
474+
case "Single[]":
475+
typeOfArray = typeof(Single);
476+
break;
477+
478+
default:
479+
throw new NotSupportedException($"{systemType.Name} is not supported");
480+
}
481+
482+
try
483+
{
484+
object[] values = JsonSerializer.Deserialize<object[]>(param) ?? Array.Empty<object>();
485+
for (int i = 0; i < values.Length; i++)
486+
{
487+
string stringValue = values[i]?.ToString() ?? string.Empty;
488+
values[i] = ParseParamAsSystemType(stringValue, typeOfArray);
489+
}
490+
491+
return values;
492+
}
493+
catch
494+
{
495+
throw new FormatException($"Expected an array for {systemType.Name} but got an unexpected value");
496+
}
497+
}
498+
459499
/// <summary>
460500
/// Very similar to GQLArgumentToDictParams but only extracts the argument names from
461501
/// the specified field which means that the method does not need a middleware context

src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Azure.DataApiBuilder.Service.Exceptions;
1616
using Azure.DataApiBuilder.Service.GraphQLBuilder;
1717
using Microsoft.Data.SqlClient;
18+
using Microsoft.Data.SqlTypes;
1819
using Microsoft.Extensions.Logging;
1920
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming;
2021

@@ -120,6 +121,15 @@ protected override void PopulateColumnDefinitionWithHasDefaultAndDbType(
120121
columnDefinition.DbType = TypeHelper.GetDbTypeFromSystemType(columnDefinition.SystemType);
121122

122123
string sqlDbTypeName = (string)columnInfo["DATA_TYPE"];
124+
125+
if (columnDefinition.SystemType == typeof(SqlVector<Single>))
126+
{
127+
sqlDbTypeName = "vector"; // Currently the "DATA_TYPE" column returns "varbinary" for vector type columns. This is a known issue https://learn.microsoft.com/en-us/sql/t-sql/data-types/vector-data-type?view=sql-server-ver17&tabs=csharp#known-issues
128+
columnDefinition.IsArrayType = true;
129+
columnDefinition.ElementSystemType = typeof(Single);
130+
columnDefinition.SystemType = columnDefinition.ElementSystemType.MakeArrayType();
131+
}
132+
123133
if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType))
124134
{
125135
// The DbType enum in .NET does not distinguish between VarChar and NVarChar. Both are mapped to DbType.String.

src/Core/Services/OpenAPI/OpenApiDocumentor.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1484,12 +1484,18 @@ private static OpenApiSchema CreateComponentSchema(
14841484
if (metadataProvider.TryGetBackingColumn(entityName, field, out string? backingColumnValue) && !string.IsNullOrEmpty(backingColumnValue))
14851485
{
14861486
string typeMetadata = string.Empty;
1487+
string subTypeMetadata = string.Empty;
14871488
string formatMetadata = string.Empty;
14881489
string? fieldDescription = null;
14891490

14901491
if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef))
14911492
{
14921493
typeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower();
1494+
1495+
if (string.Equals(typeMetadata, JsonDataType.Array.ToString().ToLower(), StringComparison.OrdinalIgnoreCase))
1496+
{
1497+
subTypeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.ElementSystemType!).ToString().ToLower();
1498+
}
14931499
}
14941500

14951501
if (entityConfig?.Fields != null)
@@ -1502,7 +1508,8 @@ private static OpenApiSchema CreateComponentSchema(
15021508
{
15031509
Type = typeMetadata,
15041510
Format = formatMetadata,
1505-
Description = fieldDescription
1511+
Description = fieldDescription,
1512+
Items = !string.IsNullOrWhiteSpace(subTypeMetadata) ? new OpenApiSchema() { Type = subTypeMetadata } : null
15061513
});
15071514
}
15081515
}

src/Core/Services/TypeHelper.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Azure.DataApiBuilder.Core.Services.OpenAPI;
99
using Azure.DataApiBuilder.Service.Exceptions;
1010
using HotChocolate.Language;
11+
using Microsoft.Data.SqlTypes;
1112
using Microsoft.OData.Edm;
1213

1314
namespace Azure.DataApiBuilder.Core.Services
@@ -46,7 +47,8 @@ public static class TypeHelper
4647
[typeof(byte[])] = DbType.Binary,
4748
[typeof(TimeOnly)] = DbType.Time,
4849
[typeof(TimeSpan)] = DbType.Time,
49-
[typeof(object)] = DbType.Object
50+
[typeof(object)] = DbType.Object,
51+
[typeof(SqlVector<Single>)] = DbType.Single
5052
};
5153

5254
/// <summary>
@@ -77,7 +79,8 @@ public static class TypeHelper
7779
[typeof(TimeOnly)] = JsonDataType.String,
7880
[typeof(object)] = JsonDataType.Object,
7981
[typeof(DateTime)] = JsonDataType.String,
80-
[typeof(DateTimeOffset)] = JsonDataType.String
82+
[typeof(DateTimeOffset)] = JsonDataType.String,
83+
[typeof(Single[])] = JsonDataType.Array
8184
};
8285

8386
/// <summary>
@@ -111,7 +114,8 @@ public static class TypeHelper
111114
[SqlDbType.TinyInt] = typeof(byte),
112115
[SqlDbType.UniqueIdentifier] = typeof(Guid),
113116
[SqlDbType.VarBinary] = typeof(byte[]),
114-
[SqlDbType.VarChar] = typeof(string)
117+
[SqlDbType.VarChar] = typeof(string),
118+
[SqlDbType.Vector] = typeof(float)
115119
};
116120

117121
private static Dictionary<SqlDbType, DbType> _sqlDbDateTimeTypeToDbType = new()

src/Service.Tests/DatabaseSchema-MsSql.sql

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ DROP TABLE IF EXISTS stocks;
4040
DROP TABLE IF EXISTS comics;
4141
DROP TABLE IF EXISTS brokers;
4242
DROP TABLE IF EXISTS type_table;
43+
DROP TABLE IF EXISTS vector_type_table;
4344
DROP TABLE IF EXISTS trees;
4445
DROP TABLE IF EXISTS fungi;
4546
DROP TABLE IF EXISTS empty_table;
@@ -234,6 +235,12 @@ CREATE TABLE type_table(
234235
uuid_types uniqueidentifier DEFAULT newid()
235236
);
236237

238+
CREATE TABLE vector_type_table(
239+
id int IDENTITY(5001, 1) PRIMARY KEY,
240+
vector_data vector(3),
241+
vector_data_max vector(1998)
242+
);
243+
237244
CREATE TABLE trees (
238245
treeId int PRIMARY KEY,
239246
species varchar(max),
@@ -616,6 +623,23 @@ VALUES
616623
INSERT INTO type_table(id, uuid_types) values(10, 'D1D021A8-47B4-4AE4-B718-98E89C41A161');
617624
SET IDENTITY_INSERT type_table OFF
618625

626+
SET IDENTITY_INSERT vector_type_table ON
627+
INSERT INTO vector_type_table(id, vector_data)
628+
VALUES
629+
(1, '[0.5, 0.25, 0.75]'),
630+
(2, '[1.5, -2.5, 3.5]'),
631+
(3, NULL),
632+
(4, '[1.0, 2.0, 3.0]'),
633+
(5, '[4.0, 5.0, 6.0]'),
634+
(6, '[7.0, 8.0, 9.0]');
635+
636+
INSERT INTO vector_type_table(id, vector_data_max)
637+
VALUES (7, CAST('[' + (
638+
SELECT STRING_AGG(CAST(value AS NVARCHAR(MAX)), ',') WITHIN GROUP (ORDER BY value)
639+
FROM GENERATE_SERIES(1, 1998)
640+
) + ']' AS vector(1998)));
641+
SET IDENTITY_INSERT vector_type_table OFF
642+
619643
SET IDENTITY_INSERT sales ON
620644
INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12);
621645
SET IDENTITY_INSERT sales OFF

0 commit comments

Comments
 (0)