Skip to content

Commit 7e25090

Browse files
authored
Merge branch 'main' into gvkries/gql-17261
2 parents 55bb6ef + f78f752 commit 7e25090

File tree

2 files changed

+188
-29
lines changed

2 files changed

+188
-29
lines changed

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

Lines changed: 103 additions & 20 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;
@@ -54,7 +57,6 @@ public void CreateTableAlias(string path, string tableAlias)
5457
_tableAliases[path] = tableAlias;
5558
}
5659

57-
5860
public void SearchUsedAlias(string propertyPath)
5961
{
6062
ArgumentNullException.ThrowIfNull(propertyPath);
@@ -67,10 +69,10 @@ public void SearchUsedAlias(string propertyPath)
6769
return;
6870
}
6971

70-
var values = propertyPath.Split('.', 2);
72+
var index = IndexOfUnquoted(propertyPath, '.');
7173

7274
// if empty prefix, use default (empty alias)
73-
var aliasPath = values.Length == 1 ? string.Empty : values[0];
75+
var aliasPath = index == -1 ? string.Empty : propertyPath[..index];
7476

7577
// get the actual index from the alias
7678
if (_aliases.TryGetValue(aliasPath, out alias))
@@ -80,21 +82,17 @@ public void SearchUsedAlias(string propertyPath)
8082

8183
if (propertyProvider != null)
8284
{
83-
if (propertyProvider.TryGetValue(values.Last(), out var columnName))
85+
if (propertyProvider.TryGetValue(propertyPath[(index + 1)..], out var columnName))
8486
{
8587
_usedAliases.Add(alias.alias);
86-
return;
8788
}
8889
}
8990
else
9091
{
9192
_usedAliases.Add(alias.alias);
92-
return;
9393
}
9494
}
95-
96-
// No aliases registered for this path, return the formatted path.
97-
return;
95+
// else: No aliases registered for this path, return the formatted path.
9896
}
9997

10098
public string GetColumnName(string propertyPath)
@@ -116,52 +114,137 @@ public string GetColumnName(string propertyPath)
116114
{
117115
if (propertyProvider.TryGetValue(propertyPath, out var columnName))
118116
{
119-
return $"{Dialect.QuoteForAliasName(tableAlias)}.{Dialect.QuoteForColumnName(columnName)}";
117+
return EnsureQuotes(tableAlias, columnName);
120118
}
121119
}
122120

123-
return $"{Dialect.QuoteForAliasName(tableAlias)}.{Dialect.QuoteForColumnName(alias.alias)}";
121+
return EnsureQuotes(tableAlias, alias.alias);
124122
}
125123

126-
return Dialect.QuoteForColumnName(alias.alias);
124+
return EnsureQuotes(alias.alias);
127125
}
128126

129-
var values = propertyPath.Split('.', 2);
127+
var index = IndexOfUnquoted(propertyPath, '.');
130128

131129
// if empty prefix, use default (empty alias)
132-
var aliasPath = values.Length == 1 ? string.Empty : values[0];
130+
var aliasPath = index == -1 ? string.Empty : propertyPath[..index];
133131

134132
// get the actual index from the alias
135133
if (_aliases.TryGetValue(aliasPath, out alias))
136134
{
137-
var tableAlias = _tableAliases[alias.alias];
135+
if (!_tableAliases.TryGetValue(alias.alias, out var tableAlias))
136+
{
137+
throw new InvalidOperationException($"Missing table alias for path {alias.alias}.");
138+
}
139+
138140
// get the index property provider fore the alias
139141
var propertyProvider = _propertyProviders.FirstOrDefault(x => x.IndexName.Equals(alias.alias, StringComparison.OrdinalIgnoreCase));
140142

141143
if (propertyProvider != null)
142144
{
143-
if (propertyProvider.TryGetValue(values.Last(), out var columnName))
145+
if (propertyProvider.TryGetValue(propertyPath[(index + 1)..], out var columnName))
144146
{
145147
// Switch the given alias in the path with the mapped alias.
146148
// aliasPart.alias -> AliasPartIndex.Alias
147-
return $"{Dialect.QuoteForAliasName(tableAlias)}.{Dialect.QuoteForColumnName(columnName)}";
149+
return EnsureQuotes(tableAlias, columnName);
148150
}
149151
}
150152
else
151153
{
152154
// no property provider exists; hope sql is case-insensitive (will break postgres; property providers must be supplied for postgres)
153155
// Switch the given alias in the path with the mapped alias.
154156
// aliasPart.Alias -> AliasPartIndex.alias
155-
return $"{Dialect.QuoteForAliasName(tableAlias)}.{Dialect.QuoteForColumnName(values.Last())}";
157+
return EnsureQuotes(tableAlias, propertyPath[(index + 1)..]);
156158
}
157159
}
158160

159161
// No aliases registered for this path, return the formatted path.
160-
return Dialect.QuoteForColumnName(propertyPath);
162+
return EnsureQuotes(propertyPath);
161163
}
162164

163165
public IEnumerable<string> GetUsedAliases()
164166
{
165167
return _usedAliases;
166168
}
169+
170+
private string EnsureQuotes(string alias)
171+
{
172+
var index = IndexOfUnquoted(alias, '.');
173+
return index == -1
174+
? (IsQuoted(alias) ? alias : Dialect.QuoteForColumnName(alias))
175+
: EnsureQuotes(alias[..index], alias[(index + 1)..]);
176+
}
177+
178+
private string EnsureQuotes(string tableAlias, string columnName)
179+
{
180+
if (!IsQuoted(tableAlias))
181+
{
182+
tableAlias = Dialect.QuoteForAliasName(tableAlias);
183+
}
184+
185+
if (!IsQuoted(columnName))
186+
{
187+
columnName = Dialect.QuoteForColumnName(columnName);
188+
}
189+
190+
return $"{tableAlias}.{columnName}";
191+
}
192+
193+
private bool IsQuoted(string value)
194+
{
195+
if (value.Length >= 2)
196+
{
197+
var (startQuote, endQuote) = GetQuoteChars(Dialect);
198+
return value[0] == startQuote && value[^1] == endQuote;
199+
}
200+
201+
return false;
202+
}
203+
204+
private int IndexOfUnquoted(string value, char c)
205+
{
206+
var startIndex = 0;
207+
208+
while (true)
209+
{
210+
var index = value.IndexOf(c, startIndex);
211+
212+
if (index < 0)
213+
{
214+
return -1;
215+
}
216+
217+
var (startQuote, endQuote) = GetQuoteChars(Dialect);
218+
var startQuoteIndex = value.IndexOf(startQuote, startIndex);
219+
220+
if (startQuoteIndex >= 0 && startQuoteIndex < index)
221+
{
222+
var endQuoteIndex = value.IndexOf(endQuote, startQuoteIndex + 1);
223+
224+
if (endQuoteIndex >= index)
225+
{
226+
startIndex = endQuoteIndex + 1;
227+
continue;
228+
}
229+
}
230+
231+
return index;
232+
}
233+
}
234+
235+
private static (char startQuote, char endQuote) GetQuoteChars(ISqlDialect dialect)
236+
=> dialect switch
237+
{
238+
MySqlDialect => ('`', '`'),
239+
PostgreSqlDialect => ('"', '"'),
240+
SqliteDialect or
241+
SqlServerDialect => ('[', ']'),
242+
_ => ExtractQuoteChars(dialect)
243+
};
244+
245+
private static (char startQuote, char endQuote) ExtractQuoteChars(ISqlDialect dialect)
246+
{
247+
var quoted = dialect.QuoteForColumnName("alias");
248+
return (quoted[0], quoted[^1]);
249+
}
167250
}

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)