Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ internal abstract class SqlFilterTranslator
private readonly LambdaExpression _lambdaExpression;
private readonly ParameterExpression _recordParameter;
protected readonly StringBuilder _sql;
protected int _parameterIndex;

internal SqlFilterTranslator(
VectorStoreRecordModel model,
LambdaExpression lambdaExpression,
int startParamIndex,
StringBuilder? sql = null)
{
this._model = model;
this._lambdaExpression = lambdaExpression;
Debug.Assert(lambdaExpression.Parameters.Count == 1);
this._recordParameter = lambdaExpression.Parameters[0];
this._parameterIndex = startParamIndex;
this._sql = sql ?? new();
}

Expand Down Expand Up @@ -182,15 +185,16 @@ private void TranslateMember(MemberExpression memberExpression, bool isSearchCon
{
if (this.TryBindProperty(memberExpression, out var property))
{
this.GenerateColumn(property.StorageName, isSearchCondition);
this.GenerateColumn(property, isSearchCondition);
return;
}

throw new NotSupportedException($"Member access for '{memberExpression.Member.Name}' is unsupported - only member access over the filter parameter are supported");
}

protected virtual void GenerateColumn(string column, bool isSearchCondition = false)
=> this._sql.Append('"').Append(column.Replace("\"", "\"\"")).Append('"');
protected virtual void GenerateColumn(VectorStoreRecordPropertyModel column, bool isSearchCondition = false)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed the argument from string to VectorStoreRecordPropertyModel just to make sure all the derived types deal with StorageName in explicit way (they know this value is always escaped, while some random string may or may not be escaped).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

// all SQL-based connectors are required to escape values returned by StorageName property
=> this._sql.Append('"').Append(column.StorageName).Append('"');

protected abstract void TranslateQueryParameter(string name, object? value);

Expand All @@ -200,7 +204,7 @@ private void TranslateMethodCall(MethodCallExpression methodCall, bool isSearchC
{
// Dictionary access for dynamic mapping (r => r["SomeString"] == "foo")
case MethodCallExpression when this.TryBindProperty(methodCall, out var property):
this.GenerateColumn(property.StorageName, isSearchCondition);
this.GenerateColumn(property, isSearchCondition);
return;

// Enumerable.Contains()
Expand Down Expand Up @@ -297,7 +301,7 @@ private void TranslateUnary(UnaryExpression unary, bool isSearchCondition)

// Handle convert over member access, for dynamic dictionary access (r => (int)r["SomeInt"] == 8)
case ExpressionType.Convert when this.TryBindProperty(unary.Operand, out var property) && unary.Type == property.Type:
this.GenerateColumn(property.StorageName, isSearchCondition);
this.GenerateColumn(property, isSearchCondition);
return;

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ internal static class PostgresConstants
[
typeof(ReadOnlyMemory<float>),
typeof(ReadOnlyMemory<float>?)
]
],

EscapeIdentifier = Escape,
};

/// <summary>A <see cref="HashSet{T}"/> of types that vector properties on the provided model may have.</summary>
Expand Down Expand Up @@ -91,4 +93,8 @@ internal static class PostgresConstants
{
{ IndexKind.Hnsw, 2000 },
};

internal static string Escape(string input) => input.Replace("\"", "\"\"");

internal static string Unescape(string input) => input.Replace("\"\"", "\"");
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ namespace Microsoft.SemanticKernel.Connectors.Postgres;
internal sealed class PostgresFilterTranslator : SqlFilterTranslator
{
private readonly List<object> _parameterValues = new();
private int _parameterIndex;

internal PostgresFilterTranslator(
VectorStoreRecordModel model,
LambdaExpression lambdaExpression,
int startParamIndex,
StringBuilder? sql = null) : base(model, lambdaExpression, sql)
StringBuilder? sql = null) : base(model, lambdaExpression, startParamIndex, sql)
{
this._parameterIndex = startParamIndex;
}

internal List<object> ParameterValues => this._parameterValues;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ ReadOnlyMemory<float> floatMemory

public static object? GetPropertyValue(NpgsqlDataReader reader, string propertyName, Type propertyType)
{
int propertyIndex = reader.GetOrdinal(propertyName);
int propertyIndex = reader.GetOrdinal(PostgresConstants.Unescape(propertyName));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure unescaping is actually needed here (and in fact, is correct)? Escaping should only be needed when the column name is integrated in SQL (i.e. in a larger string containing a query), but this is an API specifically accepting the column name (not a SQL string). I don't think this is right (in fact, if the coiumn actually contains a double-column, this would cause a failure).

Regardless, for future improvement, a better approach here would simply to read back columns from NpgsqlDataReader based on their position, instead of based on their name - we're the ones generating the SQL that selects the columns, so we should know the order in which they were sent and can read it back... This is a bit more efficient and cleaner, etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed, when I revert the change, the tests start to fail:

    Microsoft.Extensions.VectorData.VectorStoreOperationException : Call to vector store failed.
    ---- System.IndexOutOfRangeException : Field not found in row: embed""ding

In general, I've extended the SimpleModelFixture storage names with all kind of weird characters. Because of that, we have coverage for collection, single record and batch CRUD. Initially I've quoted only the obvious things, but after studying the failing tests I was able to find all the weird and unexpected places.

a better approach here would simply to read back columns from NpgsqlDataReader based on their position

I agree, but it would require more non-trivial changes (build a dictionary name-position for every query), as the order may vary (we don't always include vector fields etc). If that is not a problem, I would prefer not to do it in this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed, when I revert the change, the tests start to fail:

I see, that's because you've changed model building so that PropertyModel.StorageName is escaped - so now it has to be un-escaped (code):

property.StorageName = this.Options.EscapeIdentifier is not null
    ? this.Options.EscapeIdentifier(storageName)
    : storageName;

I'm not sure I'm a fan of the approach... I think the model storage name itself shouldn't be escaped, and then we apply escaping to it when needed, and don't apply when not needed (like here, when interacting with DbDataReader). I guess you're trying to defensively ensure that identifiers are escaped by default (in case we forget to do it somewhere); but remember that this isn't a security problem - both property names and the names of captured variables are part of the application code, and so are trusted (any security concerns are around the contents of captured variables, since that's what typically comes from untrusted user inputs).

So if we forget to escape an identifier somewhere, that's just a bug we should fix, and since we have some contexts where we want unescaped names (DbDataReader.GetOrdinal(), possibly logging....), I think the model should probably just contain the raw, unescaped name.

In any case, I agree that reading back columns based on their position is something that belong out of this PR and can be done later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thought on this...

IIUC the property's StorageName is escaped, but it isn't quoted... That means that wherever the name needs to be integrated in SQL, we still need to remember to quote it - which I think removes the "safety-by-default" that having the escaped name in the model was supposed to provide in the first place...

To go all the way with that approach, we'd have the model storage name be both escaped and quoted; we'd then need to unquote and unescape for certain contexts (DbDataReader.GetOrdinal(), combining table/column names into an index name).

Or we go the other way, and just keep the raw, unquoted and unescaped name in the model, and simply call DelimitIdentifier wherever needed (again, we currently need to do this anyway for quoting). Building the index name would just be Delimit($"{tableName}_{columnName}") or whatever.


if (reader.IsDBNull(propertyIndex))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public virtual Task<UpdateResult> CreatePayloadIndexAsync(
string fieldName,
PayloadSchemaType schemaType = PayloadSchemaType.Keyword,
CancellationToken cancellationToken = default)
=> this._qdrantClient.CreatePayloadIndexAsync(collectionName, fieldName, schemaType, cancellationToken: cancellationToken);
=> this._qdrantClient.CreatePayloadIndexAsync(collectionName, $"\"{fieldName}\"", schemaType, cancellationToken: cancellationToken);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW do we also need to go over other non-SQL connectors to make sure they're safe etc.?

Shay Rojansky (@roji) I went through the non-SQL connectors (except of cosmos) and this was the only line of code I needed to change to get the tests passing. We are usually using strongly typed .NET APIs for these and/or JSON serializer that does all the escaping for us.


/// <summary>
/// Drop a collection and all its associated data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ internal sealed class SqlDataReaderDictionary(SqlDataReader sqlDataReader, IRead
// This is the only method used by the default mapper.
public object? this[string key]
{
get => this.Unwrap(key, sqlDataReader[key]);
get => this.Unwrap(key, sqlDataReader[SqlServerConstants.Unescape(key)]);
set => throw new InvalidOperationException();
}

Expand Down Expand Up @@ -114,7 +114,7 @@ public bool TryGetValue(string key, out object? value)
{
try
{
value = this.Unwrap(key, sqlDataReader[key]);
value = this.Unwrap(key, sqlDataReader[SqlServerConstants.Unescape(key)]);
return true;
}
catch (IndexOutOfRangeException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ internal static SqlCommand SelectVector<TRecord>(
sb.AppendFormat("SELECT ");
sb.AppendColumnNames(model.Properties, includeVectors: options.IncludeVectors);
sb.AppendLine(",");
sb.AppendFormat("VECTOR_DISTANCE('{0}', {1}, CAST(@vector AS VECTOR({2}))) AS [score]",
sb.AppendFormat("VECTOR_DISTANCE('{0}', [{1}], CAST(@vector AS VECTOR({2}))) AS [score]",
distanceMetric, vectorProperty.StorageName, vector.Length);
sb.AppendLine();
sb.Append("FROM ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ internal static class SqlServerConstants

SupportedEnumerableDataPropertyElementTypes = [],

SupportedVectorPropertyTypes = SupportedVectorTypes
SupportedVectorPropertyTypes = SupportedVectorTypes,

EscapeIdentifier = Escape
};

internal static string Escape(string input) => input.Replace("]", "]]");

internal static string Unescape(string input) => input.Replace("]]", "]");
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ namespace Microsoft.SemanticKernel.Connectors.SqlServer;
internal sealed class SqlServerFilterTranslator : SqlFilterTranslator
{
private readonly List<object> _parameterValues = new();
private int _parameterIndex;

internal SqlServerFilterTranslator(
VectorStoreRecordModel model,
LambdaExpression lambdaExpression,
StringBuilder sql,
int startParamIndex)
: base(model, lambdaExpression, sql)
: base(model, lambdaExpression, startParamIndex, sql)
{
this._parameterIndex = startParamIndex;
}

internal List<object> ParameterValues => this._parameterValues;
Expand All @@ -45,9 +43,10 @@ protected override void TranslateConstant(object? value)
}
}

protected override void GenerateColumn(string column, bool isSearchCondition = false)
protected override void GenerateColumn(VectorStoreRecordPropertyModel column, bool isSearchCondition = false)
{
this._sql.Append('[').Append(column).Append(']');
// all SQL-based connectors are required to escape values returned by StorageName property
this._sql.Append('[').Append(column.StorageName).Append(']');

// "SELECT * FROM MyTable WHERE BooleanColumn;" is not supported.
// "SELECT * FROM MyTable WHERE BooleanColumn = 1;" is supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ internal static class SqliteConstants
SupportedEnumerableDataPropertyElementTypes = [],
SupportedVectorPropertyTypes = SupportedVectorTypes,

EscapeIdentifier = SqliteVectorStoreCollectionCommandBuilder.EscapeIdentifier
EscapeIdentifier = Escape
};

internal static string Escape(string value) => value.Replace("\"", "\"\"");

internal static string Unescape(string value) => value.Replace("\"\"", "\"");
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal sealed class SqliteFilterTranslator : SqlFilterTranslator
private readonly Dictionary<string, object> _parameters = new();

internal SqliteFilterTranslator(VectorStoreRecordModel model, LambdaExpression lambdaExpression)
: base(model, lambdaExpression, sql: null)
: base(model, lambdaExpression, startParamIndex: 0, sql: null)
{
}

Expand Down Expand Up @@ -61,20 +61,10 @@ protected override void TranslateQueryParameter(string name, object? value)
}
else
{
// Duplicate parameter name, create a new parameter with a different name
// TODO: Share the same parameter when it references the same captured value
if (this._parameters.ContainsKey(name))
{
var baseName = name;
var i = 0;
do
{
name = baseName + (i++);
} while (this._parameters.ContainsKey(name));
}

this._parameters.Add(name, value);
this._sql.Append('@').Append(name);
string paramName = $"@{this._parameterIndex++}";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure why we're always removing the name... It's really useful for reading the SQL, 99% of the cases aren't going to be problematic, and the 1% (where there are weird chars) can be handled separately, no?

this._parameters.Add(paramName, value);
this._sql.Append(paramName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ internal static class SqliteVectorStoreCollectionCommandBuilder
{
internal const string DistancePropertyName = "distance";

internal static string EscapeIdentifier(this string value) => value.Replace("'", "''").Replace("\"", "\"\"");

public static DbCommand BuildTableCountCommand(SqliteConnection connection, string tableName)
{
Verify.NotNullOrWhiteSpace(tableName);
Expand Down Expand Up @@ -114,8 +112,6 @@ public static DbCommand BuildInsertCommand(

for (var recordIndex = 0; recordIndex < records.Count; recordIndex++)
{
var rowIdentifierParameterName = GetParameterName(rowIdentifier, recordIndex);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was not being used


var (columns, parameters, values) = GetQueryParts(
properties,
records[recordIndex],
Expand Down Expand Up @@ -410,7 +406,23 @@ private static (List<string> Columns, List<string> ParameterNames, List<object?>

private static string GetParameterName(string propertyName, int index)
{
return $"@{propertyName}{index}";
StringBuilder builder = new();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a bug here that this fixes? Or is this about slightly better perf?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input value (propertyName) can be column name with a character that is not valid for parameter names:

string propertyName = property.StorageName;
if (include && record.TryGetValue(propertyName, out var value))
{
columns.Add($"\"{propertyName}\"");
parameterNames.Add(GetParameterName(propertyName, index));

The tests I've provided cover it (when I revert this change, they fail)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the same problem discussed above for parameter names inside the filter translator (i.e. TranslateQueryParameter).

For one thing, whatever logic we choose, we should probably not have two different implementations for handling parameter names (once in the filter translator, one here).

BTW if I'm very pedantic, just appending an index at the end isn't a 100% solution, since two parameters could still conflict (imagine having one parameter x that becomes x12 (because it's the 12th parameter), and x1 that also becomes x12 (because it's the second parameter). To fully ensure uniquenes, we need to simply record the parameters that have already been added in some set, and check if the new parameter is already in the set. This would also allow us only append the numbers when needed, rather than always appending them as the code currently does.

But I don't want us to get too bogged down in this. I'm also OK with merging for now and then revisiting later.

// In SQLite, parameter names must follow these rules:
// 1. They must start with a prefix like '@', ':' or '$'.
builder.Append('@');
// 2. After the prefix, the parameter name can include alphanumeric characters (a-z, A-Z, 0-9) and underscores (_)
foreach (char character in propertyName)
{
if ((character is >= 'a' and <= 'z') || (character is >= 'A' and <= 'Z')
|| (character is >= '0' and <= '9')
|| character is '_')
{
builder.Append(character);
}
}
// 3. Each parameter name in a query must be unique.
builder.Append(index);
return builder.ToString();
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ public SqliteVectorStoreRecordCollection(
this._vectorSearchExtensionName = this._options.VectorSearchExtensionName ?? SqliteConstants.VectorSearchExtensionName;

// Escape both table names before exposing them to anything that may build SQL commands.
this._dataTableName = name.EscapeIdentifier();
this._vectorTableName = GetVectorTableName(name, this._options).EscapeIdentifier();
this._dataTableName = SqliteConstants.Escape(name);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can use using static to not have to do SqliteConstants every time, e.g.:

Suggested change
this._dataTableName = SqliteConstants.Escape(name);
this._dataTableName = EscapeIdentifier(name);

BTW it's slightly odd for me that we're caching the escaped names (this code isn't that high-perf). But if we do want to do that, may as well also add the quotes around it?

In general, rather than treating escaping and quoting as two different concerns, we can have just a single API that accepts a table or column name and outputs the escaped, quoted string representation (in EF we call this DelimitIdentifier).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW it's slightly odd for me that we're caching the escaped names (this code isn't that high-perf).

The goal I had here was to ensure nobody is using unescaped values anywhere (doing it once was just perf side effect).

In general, rather than treating escaping and quoting as two different concerns, we can have just a single API that accepts a table or column name and outputs the escaped

This is a great idea, however I am afraid we need both quoted and not:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for creating index names, we use table name and column name, if they were only quoted it would not work:

For this, we can interpolate the completely raw (unquoted and unescaped) table and column names, and then run Delimit on the result. This way, we always start from raw identifiers and perform both escaping and quoting at the very end, after concatenation etc.

the SQLite vector extension does not support quoting for column names

Ah, that is very shitty indeed... But it's a very special exception (that will hopefully be fixed), so I wouldn't design our code around this one flaw (we can specifically do escaping without quoting at that one place for now).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal I had here was to ensure nobody is using unescaped values anywhere (doing it once was just perf side effect).

OK I think I missed this before.

See https://github.com/microsoft/semantic-kernel/pull/11676/files#r2064348530 for some thoughts on this.

this._vectorTableName = SqliteConstants.Escape(GetVectorTableName(name, this._options));

this._model = new VectorStoreRecordModelBuilder(SqliteConstants.ModelBuildingOptions)
.Build(typeof(TRecord), this._options.VectorStoreRecordDefinition, this._options.EmbeddingGenerator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public static List<SqliteColumn> GetColumns(IReadOnlyList<VectorStoreRecordPrope

public static object? GetPropertyValue(DbDataReader reader, string propertyName, Type propertyType)
{
int propertyIndex = reader.GetOrdinal(propertyName);
int propertyIndex = reader.GetOrdinal(SqliteConstants.Unescape(propertyName));

if (reader.IsDBNull(propertyIndex))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.VectorData;
using VectorDataSpecificationTests.Models;
using VectorDataSpecificationTests.Support;

namespace RedisIntegrationTests.Support;

public class RedisHashSetSimpleModelFixture : SimpleModelFixture<string>
{
public override TestStore TestStore => RedisTestStore.HashSetInstance;

public override VectorStoreRecordDefinition GetRecordDefinition()
=> new()
{
Properties =
[
new VectorStoreRecordKeyProperty(nameof(SimpleRecord<string>.Id), typeof(string))
{
StoragePropertyName = "i d" // intentionally with a space
},
new VectorStoreRecordVectorProperty(nameof(SimpleRecord<string>.Floats), typeof(ReadOnlyMemory<float>?), SimpleRecord<string>.DimensionCount)
{
DistanceFunction = this.DistanceFunction,
IndexKind = this.IndexKind,
// Redis HashSet does not support escaping column names used in the Query (Json does).
},
new VectorStoreRecordDataProperty(nameof(SimpleRecord<string>.Number), typeof(int))
{
IsIndexed = true,
StoragePropertyName = "num'b\"er" // intentionally with both quotes
},
new VectorStoreRecordDataProperty(nameof(SimpleRecord<string>.Text), typeof(string))
{
IsIndexed = true,
StoragePropertyName = "t e]xt" // intentionally with a space and ]
},
]
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.VectorData;
using VectorDataSpecificationTests.Models;
using VectorDataSpecificationTests.Support;

namespace SqliteIntegrationTests.Support;
Expand All @@ -8,4 +10,30 @@ public class SqliteSimpleModelFixture<TKey> : SimpleModelFixture<TKey>
where TKey : notnull
{
public override TestStore TestStore => SqliteTestStore.Instance;

public override VectorStoreRecordDefinition GetRecordDefinition()
=> new()
{
Properties =
[
// The Id and Floats properties are stored in the vec_ table and this extension currently does not support
// quoting column names, so don't customize their storage property names.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vec extension simply does not support quoting. Example:

CREATE VIRTUAL TABLE "vec_6a64a46d-6f95-4c8b-a8ad-4c4b009f0a68" USING vec0(
"id" TEXT PRIMARY KEY,
"embedding" FLOAT[3] distance_metric=cosine
);
SqliteException: SQLite Error 1: 'vec0 constructor error: Could not parse '"id" TEXT PRIMARY KEY''.

new VectorStoreRecordKeyProperty(nameof(SimpleRecord<TKey>.Id), typeof(TKey)),
new VectorStoreRecordVectorProperty(nameof(SimpleRecord<TKey>.Floats), typeof(ReadOnlyMemory<float>?), SimpleRecord<TKey>.DimensionCount)
{
DistanceFunction = this.DistanceFunction,
IndexKind = this.IndexKind,
},
new VectorStoreRecordDataProperty(nameof(SimpleRecord<TKey>.Number), typeof(int))
{
IsIndexed = true,
StoragePropertyName = "num'b\"er" // intentionally with both quotes
},
new VectorStoreRecordDataProperty(nameof(SimpleRecord<TKey>.Text), typeof(string))
{
IsIndexed = true,
StoragePropertyName = "t e]xt" // intentionally with a space and ]
},
]
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ public virtual Task Equal_with_string()
r => r.String == "foo",
r => r["String"] == "foo");

[ConditionalFact]
public virtual Task Equal_with_string_sql_injection()
{
string sqlInjection = $"foo; DROP TABLE {fixture.Collection.Name};";

return this.TestFilterAsync(
r => r.String == sqlInjection,
r => r["String"] == sqlInjection,
expectZeroResults: true);
}

[ConditionalFact]
public virtual Task Equal_with_string_containing_special_characters()
=> this.TestFilterAsync(
Expand Down
Loading