Skip to content

Commit 7d4e27e

Browse files
RubenCerna2079souvikghosh04Copilot
authored
Add Support for Vector Data Type in SQL for GraphQL (#3714)
## Why make this change? - Solves issue #3682 ## What is this change? - In order to allow graphql to read array types that are not `[string]` we moved the section that parses the scalar values to types hot chocolate can use in order output them to the user. This new `CoerceJsonLeafValueToRuntimeType` method is then used in a loop to allow support for arrays inside the `ExecutionHelper.cs` file. - In multiple places throughout the code, DAB looks at the SyntaxKind of the values it is working with in order to know if the value can be directly used or if it has a relationship with another entity. In order to allow graphql to write to the data base, we now add a new function `TryGetUnderlyingFieldKind` in `ISqlMetadataProvider.cs` that checks if the value is a list and it comes from a row that has the `IsArrayType`. If that condition is met, then it means the list value we have should be processed as a regular ScalarType and not as a List/Object that is a relationship to a different entity. This method is used in the following files: `MultipleMutationInputValidator.cs`, `MultipleCreateOrderHelper.cs`, `SqlMutationEnginge.cs`. - Lastly, as part to allow graphql to write to the data base, we add a new `GetStringifiedValue` function that checks if the value that we are going to parse to the data base is a list or a regular value and transforms it into a string accordingly. Since we set all values as strings, this is something that cannot be directly done with a list as the `.ToString()` function will return a useless value. This value is then parsed before being used as a parameter in the data base query. ## How was this tested? - [x] Integration Tests - [ ] Unit Tests Added tests that query (read) from the data base as well as mutation (change) to the data base such as `create`, `update`, and `delete`. ## Sample Request(s) Query ``` query { dbo_normalvectors { items { Embedding ProductID } } } ``` <img width="407" height="368" alt="image" src="https://github.com/user-attachments/assets/6e49c2c7-10e6-41af-abe9-aae10bc31604" /> Create ``` mutation { createdbo_normalvector( item: { ProductID: 10000, Embedding: [125, 1.22222, 0.75] } ) { ProductID Embedding } } ``` <img width="434" height="358" alt="image" src="https://github.com/user-attachments/assets/613a9c2a-c95c-49bd-bf85-ede9e74030d9" /> Update ``` mutation { updatedbo_normalvector( ProductID: 10000, item: { Embedding: [0.532, 200, 100.152] } ) { ProductID Embedding } } ``` <img width="461" height="322" alt="image" src="https://github.com/user-attachments/assets/a6514f4d-6f27-41d5-812f-8e4825582a7f" /> Delete ``` mutation { deletedbo_normalvector( ProductID: 10000 ) { ProductID Embedding } } ``` <img width="411" height="335" alt="image" src="https://github.com/user-attachments/assets/0fe839f8-2732-481e-9d26-0fa74cd972b0" /> --------- Co-authored-by: souvikghosh04 <souvikofficial04@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 90ccf42 commit 7d4e27e

18 files changed

Lines changed: 909 additions & 80 deletions

config-generators/mssql-commands.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ 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"
19+
add VectorOwner --config "dab-config.MsSql.json" --source vector_owners --rest true --graphql true --permissions "anonymous:create,read,delete,update"
20+
update VectorOwner --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update"
21+
add VectorType --config "dab-config.MsSql.json" --source vector_type_table --rest true --graphql true --permissions "anonymous:create,read,delete,update"
2022
update VectorType --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update"
23+
update VectorOwner --config "dab-config.MsSql.json" --relationship vectors --target.entity VectorType --cardinality many --relationship.fields "id:owner_id"
24+
update VectorType --config "dab-config.MsSql.json" --relationship owner --target.entity VectorOwner --cardinality one --relationship.fields "owner_id:id"
2125
add Profile --config "dab-config.MsSql.json" --source profiles --rest true --graphql true --permissions "anonymous:create,read,delete,update"
2226
update Profile --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update"
2327
add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permissions "authenticated:create,read,update,delete"

src/Core/Resolvers/MultipleCreateOrderHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ private static RelationshipFields GetRelationshipFieldsInSourceAndTarget(
385385
foreach (ObjectFieldNode field in fieldNodes)
386386
{
387387
Tuple<IValueNode?, SyntaxKind> fieldDetails = GraphQLUtils.GetFieldDetails(field.Value, context.Variables);
388-
SyntaxKind fieldKind = fieldDetails.Item2;
388+
SyntaxKind fieldKind = metadataProvider.TryGetArrayElementSyntaxKind(entityName, field.Name.Value, out SyntaxKind arrayFieldKind) ? arrayFieldKind : fieldDetails.Item2;
389389
if (GraphQLUtils.IsScalarField(fieldKind) && metadataProvider.TryGetBackingColumn(entityName, field.Name.Value, out string? backingColumnName))
390390
{
391391
backingColumnData.Add(backingColumnName, fieldDetails.Item1);

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,5 +724,22 @@ protected object GetParamAsSystemType(string fieldValue, string fieldName, Type
724724

725725
}
726726
}
727+
728+
/// <summary>
729+
/// Transforms the value of an object as a string.
730+
/// Array/vector columns (e.g. SQL Server 'vector') arrive as a List<IValueNode>.
731+
/// Calling ToString() on a list/array only yields the CLR type name, so instead extract
732+
/// the underlying element values and serialize them into a JSON array string (e.g. "[1.5,2.5,3.5]").
733+
/// </summary>
734+
/// <param name="value">The value to be transformed into a string.</param>
735+
/// <returns>A string representation of the value.</returns>
736+
protected static string GetStringifiedValue(object value)
737+
{
738+
return value switch
739+
{
740+
IEnumerable<IValueNode> valueNodes => JsonSerializer.Serialize(valueNodes.Select(TypeHelper.GetValue)),
741+
_ => value.ToString()!
742+
};
743+
}
727744
}
728745
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ private void PopulateColumnsAndParams(string columnName, object? value)
111111

112112
if (value is not null)
113113
{
114+
string stringValue = GetStringifiedValue(value);
114115
paramName = MakeDbConnectionParam(
115-
GetParamAsSystemType(value.ToString()!, columnName, GetColumnSystemType(columnName)), columnName);
116+
GetParamAsSystemType(stringValue, columnName, GetColumnSystemType(columnName)), columnName);
116117
}
117118
else
118119
{

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,12 @@ private Predicate CreatePredicateForParam(KeyValuePair<string, object?> param)
183183
}
184184
else
185185
{
186+
string stringValue = GetStringifiedValue(param.Value);
186187
predicate = new(
187188
new PredicateOperand(
188189
new Column(tableSchema: DatabaseObject.SchemaName, tableName: DatabaseObject.Name, backingColumn)),
189190
PredicateOperation.Equal,
190-
new PredicateOperand($"{MakeDbConnectionParam(GetParamAsSystemType(param.Value.ToString()!, backingColumn, GetColumnSystemType(backingColumn)), backingColumn)}"));
191+
new PredicateOperand($"{MakeDbConnectionParam(GetParamAsSystemType(stringValue, backingColumn, GetColumnSystemType(backingColumn)), backingColumn)}"));
191192
}
192193

193194
return predicate;

src/Core/Resolvers/SqlMutationEngine.cs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ await PerformDeleteOperation(
190190
context,
191191
parameters,
192192
sqlMetadataProvider,
193+
_runtimeConfigProvider.GetConfig(),
193194
!isPointMutation);
194195

195196
// For point create multiple mutation operation, a single item is created in the
@@ -1066,14 +1067,15 @@ await queryExecutor.ExecuteQueryAsync(
10661067
IMiddlewareContext context,
10671068
IDictionary<string, object?> mutationInputParamsFromGQLContext,
10681069
ISqlMetadataProvider sqlMetadataProvider,
1070+
RuntimeConfig runtimeConfig,
10691071
bool isMultipleInputType = false)
10701072
{
10711073
// rootFieldName can be either "item" or "items" depending on whether the operation
10721074
// is point multiple create or many-type multiple create.
10731075
string rootFieldName = isMultipleInputType ? MULTIPLE_INPUT_ARGUEMENT_NAME : SINGLE_INPUT_ARGUEMENT_NAME;
10741076

10751077
// Parse the hotchocolate input parameters into .net object types
1076-
object? parsedInputParams = GQLMultipleCreateArgumentToDictParams(context, rootFieldName, mutationInputParamsFromGQLContext);
1078+
object? parsedInputParams = GQLMultipleCreateArgumentToDictParams(context, rootFieldName, mutationInputParamsFromGQLContext, sqlMetadataProvider, entityName, runtimeConfig);
10771079

10781080
if (parsedInputParams is null)
10791081
{
@@ -1743,14 +1745,17 @@ private static void PopulateCurrentAndLinkingEntityParams(
17431745
internal static object? GQLMultipleCreateArgumentToDictParams(
17441746
IMiddlewareContext context,
17451747
string rootFieldName,
1746-
IDictionary<string, object?> mutationParameters)
1748+
IDictionary<string, object?> mutationParameters,
1749+
ISqlMetadataProvider metadataProvider,
1750+
string entityName,
1751+
RuntimeConfig runtimeConfig)
17471752
{
17481753
if (mutationParameters.TryGetValue(rootFieldName, out object? inputParameters))
17491754
{
17501755
ObjectField fieldSchema = context.Selection.Field;
17511756
IInputValueDefinition itemsArgumentSchema = fieldSchema.Arguments[rootFieldName];
17521757
InputObjectType inputObjectType = ExecutionHelper.InputObjectTypeFromIInputField(itemsArgumentSchema);
1753-
return GQLMultipleCreateArgumentToDictParamsHelper(context, inputObjectType, inputParameters);
1758+
return GQLMultipleCreateArgumentToDictParamsHelper(context, inputObjectType, inputParameters, metadataProvider, entityName, runtimeConfig);
17541759
}
17551760
else
17561761
{
@@ -1798,7 +1803,10 @@ private static void PopulateCurrentAndLinkingEntityParams(
17981803
internal static object? GQLMultipleCreateArgumentToDictParamsHelper(
17991804
IMiddlewareContext context,
18001805
InputObjectType inputObjectType,
1801-
object? inputParameters)
1806+
object? inputParameters,
1807+
ISqlMetadataProvider metadataProvider,
1808+
string entityName,
1809+
RuntimeConfig runtimeConfig)
18021810
{
18031811
// This condition is met for input types that accept an array of values
18041812
// where the mutation input field is 'items' such as
@@ -1820,7 +1828,10 @@ private static void PopulateCurrentAndLinkingEntityParams(
18201828
object? parsedInputFieldItem = GQLMultipleCreateArgumentToDictParamsHelper(
18211829
context: context,
18221830
inputObjectType: inputObjectType,
1823-
inputParameters: inputField.Value);
1831+
inputParameters: inputField.Value,
1832+
metadataProvider: metadataProvider,
1833+
entityName: entityName,
1834+
runtimeConfig: runtimeConfig);
18241835
if (parsedInputFieldItem is not null)
18251836
{
18261837
parsedInputFieldItems.Add((IDictionary<string, object?>)parsedInputFieldItem);
@@ -1847,33 +1858,49 @@ private static void PopulateCurrentAndLinkingEntityParams(
18471858
foreach (ObjectFieldNode inputFieldNode in inputFieldNodes)
18481859
{
18491860
string fieldName = inputFieldNode.Name.Value;
1861+
string targetEntityName = entityName;
1862+
Dictionary<string, EntityRelationship>? entityRelationships = runtimeConfig.Entities![entityName].Relationships;
1863+
if (entityRelationships is not null && entityRelationships.ContainsKey(fieldName))
1864+
{
1865+
targetEntityName = entityRelationships[fieldName].TargetEntity;
1866+
}
1867+
1868+
SyntaxKind fieldKind = metadataProvider.TryGetArrayElementSyntaxKind(targetEntityName, fieldName, out SyntaxKind arrayFieldKind)
1869+
? arrayFieldKind : inputFieldNode.Value.Kind;
1870+
18501871
// For the mutation pointMultipleCreateExample (outlined in the method summary),
18511872
// the following condition will evaluate to true for fields 'authors' and 'reviews'.
18521873
// Fields 'authors'/'reviews' can again consist of combination of scalar and relationship fields.
18531874
// So, the input object type for 'authors'/'reviews' is fetched and the same function is
18541875
// invoked with the fetched input object type again to parse the input fields of 'authors'/'reviews'.
1855-
if (inputFieldNode.Value.Kind == SyntaxKind.ListValue)
1876+
if (fieldKind == SyntaxKind.ListValue)
18561877
{
18571878
parsedInputFields.Add(
18581879
fieldName,
18591880
GQLMultipleCreateArgumentToDictParamsHelper(
18601881
context,
18611882
GetInputObjectTypeForAField(fieldName, inputObjectType.Fields),
1862-
inputFieldNode.Value.Value));
1883+
inputFieldNode.Value.Value,
1884+
metadataProvider,
1885+
targetEntityName,
1886+
runtimeConfig));
18631887
}
18641888
// For the mutation pointMultipleCreateExample (outlined in the method summary),
18651889
// the following condition will evaluate to true for fields 'publishers'.
18661890
// Field 'publishers' can again consist of combination of scalar and relationship fields.
18671891
// So, the input object type for 'publishers' is fetched and the same function is
18681892
// invoked with the fetched input object type again to parse the input fields of 'publishers'.
1869-
else if (inputFieldNode.Value.Kind == SyntaxKind.ObjectValue)
1893+
else if (fieldKind == SyntaxKind.ObjectValue)
18701894
{
18711895
parsedInputFields.Add(
18721896
fieldName,
18731897
GQLMultipleCreateArgumentToDictParamsHelper(
18741898
context,
18751899
GetInputObjectTypeForAField(fieldName, inputObjectType.Fields),
1876-
inputFieldNode.Value.Value));
1900+
inputFieldNode.Value.Value,
1901+
metadataProvider,
1902+
targetEntityName,
1903+
runtimeConfig));
18771904
}
18781905
// The flow enters this block for all scalar input fields.
18791906
else
@@ -2351,7 +2378,7 @@ private void ProcessObjectFieldNodesForAuthZ(
23512378
foreach (ObjectFieldNode field in fieldNodes)
23522379
{
23532380
Tuple<IValueNode?, SyntaxKind> fieldDetails = GraphQLUtils.GetFieldDetails(field.Value, context.Variables);
2354-
SyntaxKind underlyingFieldKind = fieldDetails.Item2;
2381+
SyntaxKind underlyingFieldKind = metadataProvider.TryGetArrayElementSyntaxKind(entityName, field.Name.Value, out SyntaxKind arrayFieldKind) ? arrayFieldKind : fieldDetails.Item2;
23552382

23562383
// For a column field, we do not have to recurse to process fields in the value - which is required for relationship fields.
23572384
if (GraphQLUtils.IsScalarField(underlyingFieldKind) || underlyingFieldKind is SyntaxKind.NullValue)

0 commit comments

Comments
 (0)