Skip to content

Commit f78f752

Browse files
authored
Update PredicateQuery string serialization (OrchardCMS#17365)
1 parent 049d775 commit f78f752

File tree

2 files changed

+187
-27
lines changed

2 files changed

+187
-27
lines changed

src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/Predicates/PredicateQuery.cs

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
using YesSql;
2+
using YesSql.Provider.MySql;
3+
using YesSql.Provider.PostgreSql;
4+
using YesSql.Provider.Sqlite;
5+
using YesSql.Provider.SqlServer;
26

37
namespace OrchardCore.ContentManagement.GraphQL.Queries.Predicates;
48

@@ -22,7 +26,6 @@ public PredicateQuery(
2226

2327
public IDictionary<string, object> Parameters { get; } = new Dictionary<string, object>();
2428

25-
2629
public string NewQueryParameter(object value)
2730
{
2831
var count = Parameters.Count;
@@ -41,6 +44,7 @@ public void CreateAlias(string path, string alias)
4144

4245
_aliases[path] = alias;
4346
}
47+
4448
public void CreateTableAlias(string path, string tableAlias)
4549
{
4650
ArgumentNullException.ThrowIfNull(path);
@@ -50,7 +54,6 @@ public void CreateTableAlias(string path, string tableAlias)
5054
_tableAliases[path] = tableAlias;
5155
}
5256

53-
5457
public void SearchUsedAlias(string propertyPath)
5558
{
5659
ArgumentNullException.ThrowIfNull(propertyPath);
@@ -63,10 +66,10 @@ public void SearchUsedAlias(string propertyPath)
6366
return;
6467
}
6568

66-
var values = propertyPath.Split('.', 2);
69+
var index = IndexOfUnquoted(propertyPath, '.');
6770

6871
// if empty prefix, use default (empty alias)
69-
var aliasPath = values.Length == 1 ? string.Empty : values[0];
72+
var aliasPath = index == -1 ? string.Empty : propertyPath[..index];
7073

7174
// get the actual index from the alias
7275
if (_aliases.TryGetValue(aliasPath, out alias))
@@ -76,21 +79,17 @@ public void SearchUsedAlias(string propertyPath)
7679

7780
if (propertyProvider != null)
7881
{
79-
if (propertyProvider.TryGetValue(values.Last(), out var columnName))
82+
if (propertyProvider.TryGetValue(propertyPath[(index + 1)..], out var columnName))
8083
{
8184
_usedAliases.Add(alias);
82-
return;
8385
}
8486
}
8587
else
8688
{
8789
_usedAliases.Add(alias);
88-
return;
8990
}
9091
}
91-
92-
// No aliases registered for this path, return the formatted path.
93-
return;
92+
// else: No aliases registered for this path, return the formatted path.
9493
}
9594

9695
public string GetColumnName(string propertyPath)
@@ -101,45 +100,130 @@ public string GetColumnName(string propertyPath)
101100
// aliasPart.Alias -> AliasFieldIndex.Alias
102101
if (_aliases.TryGetValue(propertyPath, out var alias))
103102
{
104-
return Dialect.QuoteForColumnName(alias);
103+
return EnsureQuotes(alias);
105104
}
106105

107-
var values = propertyPath.Split('.', 2);
106+
var index = IndexOfUnquoted(propertyPath, '.');
108107

109108
// if empty prefix, use default (empty alias)
110-
var aliasPath = values.Length == 1 ? string.Empty : values[0];
109+
var aliasPath = index == -1 ? string.Empty : propertyPath[..index];
111110

112111
// get the actual index from the alias
113112
if (_aliases.TryGetValue(aliasPath, out alias))
114113
{
115-
var tableAlias = _tableAliases[alias];
114+
if (!_tableAliases.TryGetValue(alias, out var tableAlias))
115+
{
116+
throw new InvalidOperationException($"Missing table alias for path {alias}.");
117+
}
118+
116119
// get the index property provider fore the alias
117120
var propertyProvider = _propertyProviders.FirstOrDefault(x => x.IndexName.Equals(alias, StringComparison.OrdinalIgnoreCase));
118121

119122
if (propertyProvider != null)
120123
{
121-
if (propertyProvider.TryGetValue(values.Last(), out var columnName))
124+
if (propertyProvider.TryGetValue(propertyPath[(index + 1)..], out var columnName))
122125
{
123126
// Switch the given alias in the path with the mapped alias.
124127
// aliasPart.alias -> AliasPartIndex.Alias
125-
return $"{Dialect.QuoteForAliasName(tableAlias)}.{Dialect.QuoteForColumnName(columnName)}";
128+
return EnsureQuotes(tableAlias, columnName);
126129
}
127130
}
128131
else
129132
{
130133
// no property provider exists; hope sql is case-insensitive (will break postgres; property providers must be supplied for postgres)
131134
// Switch the given alias in the path with the mapped alias.
132135
// aliasPart.Alias -> AliasPartIndex.alias
133-
return $"{Dialect.QuoteForAliasName(tableAlias)}.{Dialect.QuoteForColumnName(values.Last())}";
136+
return EnsureQuotes(tableAlias, propertyPath[(index + 1)..]);
134137
}
135138
}
136139

137140
// No aliases registered for this path, return the formatted path.
138-
return Dialect.QuoteForColumnName(propertyPath);
141+
return EnsureQuotes(propertyPath);
139142
}
140143

141144
public IEnumerable<string> GetUsedAliases()
142145
{
143146
return _usedAliases;
144147
}
148+
149+
private string EnsureQuotes(string alias)
150+
{
151+
var index = IndexOfUnquoted(alias, '.');
152+
return index == -1
153+
? (IsQuoted(alias) ? alias : Dialect.QuoteForColumnName(alias))
154+
: EnsureQuotes(alias[..index], alias[(index + 1)..]);
155+
}
156+
157+
private string EnsureQuotes(string tableAlias, string columnName)
158+
{
159+
if (!IsQuoted(tableAlias))
160+
{
161+
tableAlias = Dialect.QuoteForAliasName(tableAlias);
162+
}
163+
164+
if (!IsQuoted(columnName))
165+
{
166+
columnName = Dialect.QuoteForColumnName(columnName);
167+
}
168+
169+
return $"{tableAlias}.{columnName}";
170+
}
171+
172+
private bool IsQuoted(string value)
173+
{
174+
if (value.Length >= 2)
175+
{
176+
var (startQuote, endQuote) = GetQuoteChars(Dialect);
177+
return value[0] == startQuote && value[^1] == endQuote;
178+
}
179+
180+
return false;
181+
}
182+
183+
private int IndexOfUnquoted(string value, char c)
184+
{
185+
var startIndex = 0;
186+
187+
while (true)
188+
{
189+
var index = value.IndexOf(c, startIndex);
190+
191+
if (index < 0)
192+
{
193+
return -1;
194+
}
195+
196+
var (startQuote, endQuote) = GetQuoteChars(Dialect);
197+
var startQuoteIndex = value.IndexOf(startQuote, startIndex);
198+
199+
if (startQuoteIndex >= 0 && startQuoteIndex < index)
200+
{
201+
var endQuoteIndex = value.IndexOf(endQuote, startQuoteIndex + 1);
202+
203+
if (endQuoteIndex >= index)
204+
{
205+
startIndex = endQuoteIndex + 1;
206+
continue;
207+
}
208+
}
209+
210+
return index;
211+
}
212+
}
213+
214+
private static (char startQuote, char endQuote) GetQuoteChars(ISqlDialect dialect)
215+
=> dialect switch
216+
{
217+
MySqlDialect => ('`', '`'),
218+
PostgreSqlDialect => ('"', '"'),
219+
SqliteDialect or
220+
SqlServerDialect => ('[', ']'),
221+
_ => ExtractQuoteChars(dialect)
222+
};
223+
224+
private static (char startQuote, char endQuote) ExtractQuoteChars(ISqlDialect dialect)
225+
{
226+
var quoted = dialect.QuoteForColumnName("alias");
227+
return (quoted[0], quoted[^1]);
228+
}
145229
}

test/OrchardCore.Tests/Apis/GraphQL/Queries/PredicateQueryTests.cs

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using OrchardCore.ContentManagement.GraphQL.Queries;
22
using OrchardCore.ContentManagement.GraphQL.Queries.Predicates;
33
using YesSql.Indexes;
4+
using YesSql.Provider.MySql;
5+
using YesSql.Provider.PostgreSql;
6+
using YesSql.Provider.Sqlite;
47

58
namespace OrchardCore.Tests.Apis.GraphQL;
69

@@ -15,31 +18,104 @@ public PredicateQueryTests()
1518
.SetTablePrefix("Tenant1");
1619
}
1720

18-
[Fact]
19-
public void ShouldReturnQuotedColumnNameWhenAliasNotExists()
21+
[Theory]
22+
[InlineData("Value", "[Value]")]
23+
[InlineData("ListItemIndex.Value", "[ListItemIndex].[Value]")]
24+
[InlineData("[ListItemIndex.Value]", "[ListItemIndex.Value]")]
25+
[InlineData("[ListItemIndex].[Value]", "[ListItemIndex].[Value]")]
26+
public void ShouldReturnQuotedColumnNameWhenAliasNotExists(string propertyPath, string expectedColumnName)
2027
{
2128
// Arrange
2229
var predicateQuery = new PredicateQuery(_configuration, []);
2330

2431
// Act
25-
var columnName = predicateQuery.GetColumnName("ListItemIndex.Value");
32+
var columnName = predicateQuery.GetColumnName(propertyPath);
2633

2734
// Assert
28-
Assert.Equal("[ListItemIndex.Value]", columnName);
35+
Assert.Equal(expectedColumnName, columnName);
2936
}
3037

31-
[Fact]
32-
public void ShouldReturnQuotedAliasColumnNameWhenAliasExists()
38+
[Theory]
39+
[InlineData("Path", "Alias", "[Alias]")]
40+
[InlineData("ListItemIndexPath.ValuePath", "ListItemIndexAlias.ValueAlias", "[ListItemIndexAlias].[ValueAlias]")]
41+
[InlineData("ListItemIndexPath.ValuePath", "[ListItemIndexAlias.ValueAlias]", "[ListItemIndexAlias.ValueAlias]")]
42+
[InlineData("ListItemIndexPath.ValuePath", "[ListItemIndexAlias].[ValueAlias]", "[ListItemIndexAlias].[ValueAlias]")]
43+
public void ShouldReturnQuotedAliasColumnNameWhenAliasExists(string propertyPath, string alias, string expectedColumnName)
3344
{
3445
// Arrange
3546
var predicateQuery = new PredicateQuery(_configuration, []);
36-
predicateQuery.CreateAlias("ListItemIndexPath.ValuePath", "ListItemIndexAlias.ValueAlias");
47+
predicateQuery.CreateAlias(propertyPath, alias);
48+
49+
// Act
50+
var columnName = predicateQuery.GetColumnName(propertyPath);
51+
52+
// Assert
53+
Assert.Equal(expectedColumnName, columnName);
54+
}
55+
56+
[Theory]
57+
[InlineData("[ListItemIndex.Value]", "[ListItemIndex.Value]")]
58+
[InlineData("[ListItemIndex].[Value]", "[ListItemIndex].[Value]")]
59+
[InlineData("ListItemIndex.[Value]", "[ListItemIndex].[Value]")]
60+
[InlineData("[ListItemIndex].Value", "[ListItemIndex].[Value]")]
61+
public void DoesNotQuoteWhenPathIsQuoted(string propertyPath, string expectedColumnName)
62+
{
63+
// Arrange
64+
var predicateQuery = new PredicateQuery(_configuration, []);
65+
66+
// Act
67+
var columnName = predicateQuery.GetColumnName(propertyPath);
68+
69+
// Assert
70+
Assert.Equal(expectedColumnName, columnName);
71+
}
72+
73+
[Theory]
74+
[InlineData("`ListItemIndex.Value`", "`ListItemIndex.Value`", "MySql")]
75+
[InlineData("[ListItemIndex.Value]", "[ListItemIndex.Value]", "Sqlite")]
76+
[InlineData("[ListItemIndex.Value]", "[ListItemIndex.Value]", "SqlServer")]
77+
[InlineData("\"ListItemIndex.Value\"", "\"ListItemIndex.Value\"", "Postgre")]
78+
public void DetectsProviderDependentQuoteChars(string propertyPath, string expectedColumnName, string dialect)
79+
{
80+
// Arrange
81+
var configuration = new Configuration();
82+
83+
if (dialect == "MySql")
84+
{
85+
configuration
86+
.UseMySql("Fake database connection string for testing;", "TenantSchema")
87+
.SetTablePrefix("Tenant1");
88+
}
89+
else if (dialect == "Sqlite")
90+
{
91+
configuration
92+
.UseSqLite("Fake database connection string for testing;")
93+
.SetTablePrefix("Tenant1");
94+
}
95+
else if (dialect == "SqlServer")
96+
{
97+
configuration
98+
.UseSqlServer("Fake database connection string for testing;", "TenantSchema")
99+
.SetTablePrefix("Tenant1");
100+
}
101+
else if (dialect == "Postgre")
102+
{
103+
configuration
104+
.UsePostgreSql("Fake database connection string for testing;", "TenantSchema")
105+
.SetTablePrefix("Tenant1");
106+
}
107+
else
108+
{
109+
throw new ArgumentException("Unknown dialect", nameof(dialect));
110+
}
111+
112+
var predicateQuery = new PredicateQuery(configuration, []);
37113

38114
// Act
39-
var columnName = predicateQuery.GetColumnName("ListItemIndexPath.ValuePath");
115+
var columnName = predicateQuery.GetColumnName(propertyPath);
40116

41117
// Assert
42-
Assert.Equal("[ListItemIndexAlias.ValueAlias]", columnName);
118+
Assert.Equal(expectedColumnName, columnName);
43119
}
44120

45121
[Fact]

0 commit comments

Comments
 (0)