Skip to content

Commit a07ff48

Browse files
authored
Merge pull request #22 from servicetitan/sqlhelper-quote
Speedup SqlHelper.Quote* methods.
2 parents dfad886 + 89bff9c commit a07ff48

File tree

2 files changed

+253
-27
lines changed

2 files changed

+253
-27
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (C) 2020 Xtensive LLC.
2+
// All rights reserved.
3+
// For conditions of distribution and use, see license.
4+
// Created by: Alexey Kulakov
5+
// Created: 2020.04.23
6+
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Text;
10+
using NUnit.Framework;
11+
using Xtensive.Sql;
12+
13+
namespace Xtensive.Orm.Tests.Sql
14+
{
15+
[TestFixture]
16+
public sealed class SqlHelperQuotationTest
17+
{
18+
public static IEnumerable<(string[] testNames, string expectedResult)> QuotesRegularNames()
19+
{
20+
yield return (new string[] { "name1" }, "\"name1\"");
21+
yield return (new string[] { "n\"\"\"1" }, "\"n\"\"\"\"\"\"1\"");
22+
yield return (new string[] { "name1", "name2" }, "\"name1\".\"name2\"");
23+
yield return (new string[] { "n\"\"\"1", "n\"\"\"2" }, "\"n\"\"\"\"\"\"1\".\"n\"\"\"\"\"\"2\"");
24+
yield return (new string[] { "name1", "name2", "name3" }, "\"name1\".\"name2\".\"name3\"");
25+
yield return (new string[] { "n\"\"\"1", "n\"\"\"2", "n\"\"\"3" }, "\"n\"\"\"\"\"\"1\".\"n\"\"\"\"\"\"2\".\"n\"\"\"\"\"\"3\"");
26+
}
27+
28+
public static IEnumerable<(string[] testNames, string expectedResult)> QuotesEmptyNames()
29+
{
30+
yield return (new string[] {"name1", "name2", ""}, "\"name1\".\"name2\"");
31+
yield return (new string[] { "name1", "", "name3" }, "\"name1\".\"name3\"");
32+
yield return (new string[] { "", "name2", "name3" }, "\"name2\".\"name3\"");
33+
yield return (new string[] { "name1", "", "" } , "\"name1\"");
34+
yield return (new string[] { "", "", "name3" }, "\"name3\"");
35+
yield return (new string[] { "", "name2", "" }, "\"name2\"");
36+
yield return (new string[] { "", "", "" }, string.Empty);
37+
}
38+
39+
public static IEnumerable<(string[] testNames, string expectedResult)> QuotesNullNames()
40+
{
41+
yield return (new string[] { "name1", "name2", null }, "\"name1\".\"name2\"");
42+
yield return (new string[] { "name1", null, "name3" }, "\"name1\".\"name3\"");
43+
yield return (new string[] { null, "name2", "name3" }, "\"name2\".\"name3\"");
44+
yield return (new string[] { "name1", null, null }, "\"name1\"");
45+
yield return (new string[] { null, null, "name3" }, "\"name3\"");
46+
yield return (new string[] { null, "name2", null }, "\"name2\"");
47+
yield return (new string[] { null, null, null }, string.Empty);
48+
}
49+
50+
public static IEnumerable<(string[] testNames, string expectedResult)> BracketsRegularNames()
51+
{
52+
yield return (new string[] { "name1" }, "[name1]");
53+
yield return (new string[] { "n[[[1" }, "[n[[[1]");
54+
yield return (new string[] { "n]]]1" }, "[n]]]]]]1]");
55+
yield return (new string[] { "name1", "name2" }, "[name1].[name2]");
56+
yield return (new string[] { "n[[[1", "n[[[2" }, "[n[[[1].[n[[[2]");
57+
yield return (new string[] { "n]]]1", "n]]]2" }, "[n]]]]]]1].[n]]]]]]2]");
58+
yield return (new string[] { "name1", "name2", "name3" }, "[name1].[name2].[name3]");
59+
yield return (new string[] { "n[[[1", "n[[[2", "n[[[3" }, "[n[[[1].[n[[[2].[n[[[3]");
60+
yield return (new string[] { "n]]]1", "n]]]2", "n]]]3" }, "[n]]]]]]1].[n]]]]]]2].[n]]]]]]3]");
61+
}
62+
63+
public static IEnumerable<(string[] testNames, string expectedResult)> BracketsEmptyNames()
64+
{
65+
yield return (new string[] { "name1", "name2", "" }, "[name1].[name2]");
66+
yield return (new string[] { "name1", "", "name3" }, "[name1].[name3]");
67+
yield return (new string[] { "", "name2", "name3" }, "[name2].[name3]");
68+
yield return (new string[] { "name1", "", "" }, "[name1]");
69+
yield return (new string[] { "", "", "name3" }, "[name3]");
70+
yield return (new string[] { "", "name2", "" }, "[name2]");
71+
yield return (new string[] { "", "", "" }, string.Empty);
72+
}
73+
74+
public static IEnumerable<(string[] testNames, string expectedResult)> BracketsNullNames()
75+
{
76+
yield return (new string[] { "name1", "name2", null }, "[name1].[name2]");
77+
yield return (new string[] { "name1", null, "name3" }, "[name1].[name3]");
78+
yield return (new string[] { null, "name2", "name3" }, "[name2].[name3]");
79+
yield return (new string[] { "name1", null, null }, "[name1]");
80+
yield return (new string[] { null, null, "name3" }, "[name3]");
81+
yield return (new string[] { null, "name2", null }, "[name2]");
82+
yield return (new string[] { null, null, null }, string.Empty);
83+
}
84+
85+
public static IEnumerable<(string[] testNames, string expectedResult)> BackTickRegularNames()
86+
{
87+
yield return (new string[] { "name1" }, "`name1`");
88+
yield return (new string[] { "n```1" }, "`n``````1`");
89+
yield return (new string[] { "name1", "name2" }, "`name1`.`name2`");
90+
yield return (new string[] { "n```1", "n```2" }, "`n``````1`.`n``````2`");
91+
yield return (new string[] { "name1", "name2", "name3" }, "`name1`.`name2`.`name3`");
92+
yield return (new string[] { "n```1", "n```2", "n```3" }, "`n``````1`.`n``````2`.`n``````3`");
93+
}
94+
95+
public static IEnumerable<(string[] testNames, string expectedResult)> BackTickEmptyNames()
96+
{
97+
yield return (new string[] { "name1", "name2", "" }, "`name1`.`name2`");
98+
yield return (new string[] { "name1", "", "name3" }, "`name1`.`name3`");
99+
yield return (new string[] { "", "name2", "name3" }, "`name2`.`name3`");
100+
yield return (new string[] { "name1", "", "" }, "`name1`");
101+
yield return (new string[] { "", "", "name3" }, "`name3`");
102+
yield return (new string[] { "", "name2", "" }, "`name2`");
103+
yield return (new string[] { "", "", "" }, string.Empty);
104+
}
105+
106+
public static IEnumerable<(string[] testNames, string expectedResult)> BackTickNullNames()
107+
{
108+
yield return (new string[] { "name1", "name2", null }, "`name1`.`name2`");
109+
yield return (new string[] { "name1", null, "name3" }, "`name1`.`name3`");
110+
yield return (new string[] { null, "name2", "name3" }, "`name2`.`name3`");
111+
yield return (new string[] { "name1", null, null }, "`name1`");
112+
yield return (new string[] { null, null, "name3" }, "`name3`");
113+
yield return (new string[] { null, "name2", null }, "`name2`");
114+
yield return (new string[] { null, null, null }, string.Empty);
115+
}
116+
117+
[Test]
118+
[TestCaseSource(nameof(SqlHelperQuotationTest.QuotesRegularNames))]
119+
public void QuoteIndentifierWithQuotesWithRegularNames((string[] testNames, string expectedResult) testData)
120+
=> Assert.That(SqlHelper.QuoteIdentifierWithQuotes(testData.testNames),
121+
Is.EqualTo(testData.expectedResult));
122+
123+
[Test]
124+
[TestCaseSource(nameof(SqlHelperQuotationTest.QuotesEmptyNames))]
125+
public void QuoteIndentifierWithQuotesWithEmptyNames((string[] testNames, string expectedResult) testData)
126+
=> Assert.That(SqlHelper.QuoteIdentifierWithQuotes(testData.testNames),
127+
Is.EqualTo(testData.expectedResult));
128+
129+
[Test]
130+
[TestCaseSource(nameof(SqlHelperQuotationTest.QuotesNullNames))]
131+
public void QuoteIndentifierWithQuotesWithNullNames((string[] testNames, string expectedResult) testData)
132+
=> Assert.That(SqlHelper.QuoteIdentifierWithQuotes(testData.testNames),
133+
Is.EqualTo(testData.expectedResult));
134+
135+
[Test]
136+
[TestCaseSource(nameof(SqlHelperQuotationTest.BracketsRegularNames))]
137+
public void QuoteIdentifierWithBracketsWithRegularNames((string[] testNames, string expectedResult) testData)
138+
=> Assert.That(SqlHelper.QuoteIdentifierWithBrackets(testData.testNames),
139+
Is.EqualTo(testData.expectedResult));
140+
141+
[Test]
142+
[TestCaseSource(nameof(SqlHelperQuotationTest.BracketsEmptyNames))]
143+
public void QuoteIdentifierWithBracketsWithEmptyNames((string[] testNames, string expectedResult) testData)
144+
=> Assert.That(SqlHelper.QuoteIdentifierWithBrackets(testData.testNames),
145+
Is.EqualTo(testData.expectedResult));
146+
147+
[Test]
148+
[TestCaseSource(nameof(SqlHelperQuotationTest.BracketsNullNames))]
149+
public void QuoteIdentifierWithBracketsWithNullNames((string[] testNames, string expectedResult) testData)
150+
=> Assert.That(SqlHelper.QuoteIdentifierWithBrackets(testData.testNames),
151+
Is.EqualTo(testData.expectedResult));
152+
153+
[Test]
154+
[TestCaseSource(nameof(SqlHelperQuotationTest.BackTickRegularNames))]
155+
public void QuoteIdentifierWithBackTicksWithRegularNames((string[] testNames, string expectedResult) testData)
156+
=> Assert.That(SqlHelper.QuoteIdentifierWithBackTick(testData.testNames),
157+
Is.EqualTo(testData.expectedResult));
158+
159+
[Test]
160+
[TestCaseSource(nameof(SqlHelperQuotationTest.BackTickEmptyNames))]
161+
public void QuoteIdentifierWithBackTicksWithEmptyNames((string[] testNames, string expectedResult) testData)
162+
=> Assert.That(SqlHelper.QuoteIdentifierWithBackTick(testData.testNames),
163+
Is.EqualTo(testData.expectedResult));
164+
165+
[Test]
166+
[TestCaseSource(nameof(SqlHelperQuotationTest.BackTickNullNames))]
167+
public void QuoteIdentifierWithBackTicksWithNullNames((string[] testNames, string expectedResult) testData)
168+
=> Assert.That(SqlHelper.QuoteIdentifierWithBackTick(testData.testNames),
169+
Is.EqualTo(testData.expectedResult));
170+
}
171+
}

Orm/Xtensive.Orm/Sql/SqlHelper.cs

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Data.Common;
1111
using System.Data.SqlClient;
1212
using System.Linq;
13+
using System.Runtime.CompilerServices;
1314
using System.Text;
1415
using JetBrains.Annotations;
1516
using Xtensive.Collections;
@@ -25,6 +26,31 @@ namespace Xtensive.Sql
2526
/// </summary>
2627
public static class SqlHelper
2728
{
29+
public readonly struct EscapeSetup
30+
{
31+
public static readonly EscapeSetup WithQuotes =
32+
new EscapeSetup('.', '\"', '\"', '\"', '\"');
33+
public static readonly EscapeSetup WithBrackets =
34+
new EscapeSetup('.', '[', ']', ']', ']');
35+
public static readonly EscapeSetup WithBackTick =
36+
new EscapeSetup('.', '`', '`', '`', '`');
37+
38+
public readonly char Delimiter;
39+
public readonly char Opener;
40+
public readonly char Closer;
41+
public readonly char EscapeCloser1;
42+
public readonly char EscapeCloser2;
43+
44+
public EscapeSetup(char delimiter, char opener, char closer, char escapeCloser1, char escapeCloser2)
45+
{
46+
Delimiter = delimiter;
47+
Opener = opener;
48+
Closer = closer;
49+
EscapeCloser1 = escapeCloser1;
50+
EscapeCloser2 = escapeCloser2;
51+
}
52+
}
53+
2854
/// <summary>
2955
/// Validates the specified URL againts charactes that usually forbidden inside connection strings.
3056
/// </summary>
@@ -45,44 +71,73 @@ public static void ValidateConnectionUrl(UrlInfo url)
4571
/// Quotes the specified identifier with quotes (i.e. "").
4672
/// </summary>
4773
/// <returns>Quoted identifier.</returns>
48-
public static string QuoteIdentifierWithQuotes(string[] names)
49-
{
50-
return Quote("\"", "\"", ".", "\"\"", names);
51-
}
74+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
75+
public static string QuoteIdentifierWithQuotes(string[] names)
76+
=> Quote(EscapeSetup.WithQuotes, names);
5277

5378
/// <summary>
5479
/// Quotes the specified identifier with square brackets (i.e. []).
5580
/// </summary>
5681
/// <returns>Quoted indentifier.</returns>
57-
public static string QuoteIdentifierWithBrackets(string[] names)
58-
{
59-
return Quote("[", "]", ".", "]]", names);
60-
}
82+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
83+
public static string QuoteIdentifierWithBrackets(string[] names)
84+
=> Quote(EscapeSetup.WithBrackets, names);
6185

6286
/// <summary>
6387
/// Quotes the specified identifier with square brackets (i.e. ``).
6488
/// </summary>
65-
/// <returns>Quoted indentifier.</returns>
66-
public static string QuoteIdentifierWithBackTick(string[] names)
67-
{
68-
return Quote("`", "`", ".", "``", names);
69-
}
89+
/// <returns>Quoted identifier.</returns>
90+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
91+
public static string QuoteIdentifierWithBackTick(string[] names)
92+
=> Quote(EscapeSetup.WithBackTick, names);
7093

71-
private static string Quote(string openingBracket, string closingBracket, string delimiter,
72-
string escapedClosingBracket, IEnumerable<string> names)
94+
private static unsafe string Quote(in EscapeSetup setup, string[] names)
7395
{
74-
var tokens = names.Where(name => !string.IsNullOrEmpty(name)).ToArray();
75-
var builder = new StringBuilder();
76-
for (int i = 0; i < tokens.Length - 1; i++) {
77-
builder.Append(openingBracket);
78-
builder.Append(tokens[i].Replace(closingBracket, escapedClosingBracket));
79-
builder.Append(closingBracket);
80-
builder.Append(delimiter);
96+
// That's one of frequently called methods, so it's optimized for speed.
97+
98+
// 1. Find resultLength
99+
var resultLength = 0;
100+
foreach (var name in names) {
101+
if (string.IsNullOrEmpty(name))
102+
continue;
103+
if (resultLength != 0)
104+
resultLength++;
105+
resultLength += 2 + name.Length;
106+
var start = 0;
107+
while (true) {
108+
start = 1 + name.IndexOf(setup.Closer, start);
109+
if (start == 0)
110+
break;
111+
resultLength++;
112+
}
81113
}
82-
builder.Append(openingBracket);
83-
builder.Append(tokens[tokens.Length - 1].Replace(closingBracket, escapedClosingBracket));
84-
builder.Append(closingBracket);
85-
return builder.ToString();
114+
115+
// 2. Create the resulting string at once
116+
var result = new string(setup.Delimiter, resultLength);
117+
fixed (char* pResult = result) {
118+
var p = pResult;
119+
foreach (var name in names) {
120+
if (string.IsNullOrEmpty(name))
121+
continue;
122+
if (p != pResult)
123+
p++; // Skip the delimiter (initial result value is a repeating delimiter)
124+
*p++ = setup.Opener;
125+
fixed (char* pName = name) {
126+
var pNameEnd = pName + name.Length;
127+
for (var pn = pName; pn < pNameEnd; pn++) {
128+
var c = *pn;
129+
if (c == setup.Closer) {
130+
*p++ = setup.EscapeCloser1;
131+
*p++ = setup.EscapeCloser2;
132+
continue;
133+
}
134+
*p++ = c;
135+
}
136+
}
137+
*p++ = setup.Closer;
138+
}
139+
}
140+
return result;
86141
}
87142

88143
/// <summary>
@@ -379,4 +434,4 @@ public static NotSupportedException NotSupported(ServerFeatures feature)
379434
return NotSupported(feature.ToString());
380435
}
381436
}
382-
}
437+
}

0 commit comments

Comments
 (0)