Skip to content

Commit e9779c1

Browse files
ggeurtsoskarb
authored andcommitted
Added new SqlTokenizer class to support parsing of non-trivial SELECT statements. Modified SqlString.Parse() to ignore parameter references in SQL comments.
1 parent 01a8d31 commit e9779c1

File tree

8 files changed

+684
-26
lines changed

8 files changed

+684
-26
lines changed

src/NHibernate.Test/NHibernate.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,7 @@
11111111
<Compile Include="ReadOnly\TextHolder.cs" />
11121112
<Compile Include="ReadOnly\VersionedNode.cs" />
11131113
<Compile Include="RecordingInterceptor.cs" />
1114+
<Compile Include="SqlCommandTest\SqlTokenizerFixture.cs" />
11141115
<Compile Include="Stateless\Contact.cs" />
11151116
<Compile Include="Stateless\Country.cs" />
11161117
<Compile Include="Stateless\FetchingLazyCollections\LazyCollectionFetchTests.cs" />
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
using NUnit.Framework;
2+
using NHibernate.SqlCommand;
3+
using NHibernate.SqlCommand.Parser;
4+
5+
namespace NHibernate.Test.SqlCommandTest
6+
{
7+
[TestFixture]
8+
public class SqlTokenizerFixture
9+
{
10+
[Test]
11+
public void TokenizeSimpleSelectStatement()
12+
{
13+
VerifyTokenizer("SELECT * FROM some_table WHERE key = ? ORDER BY some_field DESC",
14+
Text("SELECT"), Whitespace(" "), Text("*"),
15+
Whitespace(" "),
16+
Text("FROM"), Whitespace(" "), Text("some_table"),
17+
Whitespace(" "),
18+
Text("WHERE"), Whitespace(" "), Text("key"), Whitespace(" "), Text("="), Whitespace(" "), Parameter(),
19+
Whitespace(" "),
20+
Text("ORDER"), Whitespace(" "), Text("BY"), Whitespace(" "), Text("some_field"), Whitespace(" "), Text("DESC"));
21+
}
22+
23+
[Test]
24+
public void TokenizeLineComments()
25+
{
26+
VerifyTokenizer("--", Comment("--"));
27+
VerifyTokenizer("---", Comment("---"));
28+
VerifyTokenizer("--\r", Comment("--"), Whitespace("\r"));
29+
VerifyTokenizer("-- Any comment will do",
30+
Comment("-- Any comment will do"));
31+
VerifyTokenizer("-- Two comments\n--will do too",
32+
Comment("-- Two comments"), Whitespace("\n"), Comment("--will do too"));
33+
}
34+
35+
[Test]
36+
public void TokenizeBlockComments()
37+
{
38+
VerifyTokenizer("/**/", Comment("/**/"));
39+
VerifyTokenizer("/***/", Comment("/***/"));
40+
VerifyTokenizer("/*/*/", Comment("/*/*/"));
41+
VerifyTokenizer("/****/", Comment("/****/"));
42+
VerifyTokenizer("//**//", Text("/"), Comment("/**/"), Text("/"));
43+
VerifyTokenizer("/*\n*/", Comment("/*\n*/"));
44+
VerifyTokenizer("/**/\n", Comment("/**/"), Whitespace("\n"));
45+
VerifyTokenizer("/**/*", Comment("/**/"), Text("*"));
46+
VerifyTokenizer("/**/*/", Comment("/**/"), Text("*/"));
47+
VerifyTokenizer("*//**/", Text("*/"), Comment("/**/"));
48+
VerifyTokenizer("SELECT/**/*", Text("SELECT"), Comment("/**/"), Text("*"));
49+
}
50+
51+
[Test]
52+
public void TokenizeBrackets()
53+
{
54+
VerifyTokenizer("()", BracketOpen(), BracketClose());
55+
VerifyTokenizer("(())", BracketOpen(), BracketOpen(), BracketClose(), BracketClose());
56+
VerifyTokenizer("()()", BracketOpen(), BracketClose(), BracketOpen(), BracketClose());
57+
VerifyTokenizer("(\n)", BracketOpen(), Whitespace("\n"), BracketClose());
58+
VerifyTokenizer("()--()", BracketOpen(), BracketClose(), Comment("--()"));
59+
VerifyTokenizer("(--\n)", BracketOpen(), Comment("--"), Whitespace("\n"), BracketClose());
60+
VerifyTokenizer("(SELECT)", BracketOpen(), Text("SELECT"), BracketClose());
61+
62+
VerifyTokenizer("SELECT (SELECT COUNT(*) FROM table), ?",
63+
Text("SELECT"), Whitespace(" "),
64+
BracketOpen(),
65+
Text("SELECT"), Whitespace(" "), Text("COUNT"),
66+
BracketOpen(),
67+
Text("*"),
68+
BracketClose(),
69+
Whitespace(" "), Text("FROM"), Whitespace(" "), Text("table"),
70+
BracketClose(),
71+
Comma(), Whitespace(" "), Parameter());
72+
}
73+
74+
[Test]
75+
public void TokenizeQuotedString()
76+
{
77+
VerifyTokenizer("''", DelimitedText("''"));
78+
VerifyTokenizer("''''", DelimitedText("''''"));
79+
VerifyTokenizer("'string literal'", DelimitedText("'string literal'"));
80+
VerifyTokenizer("'x''s value'", DelimitedText("'x''s value'"));
81+
}
82+
83+
[Test]
84+
public void TokenizeQuotedIdentifier()
85+
{
86+
VerifyTokenizer(@"""Identifier""", DelimitedText(@"""Identifier"""));
87+
VerifyTokenizer(@"""""""Identifier""""""", DelimitedText(@"""""""Identifier"""""""));
88+
VerifyTokenizer("[Identifier]", DelimitedText("[Identifier]"));
89+
VerifyTokenizer("[[Identifier]]]", DelimitedText("[[Identifier]]]"));
90+
}
91+
92+
[Test]
93+
public void TokenizeParameters()
94+
{
95+
VerifyTokenizer("?", Parameter());
96+
VerifyTokenizer("'?'", DelimitedText("'?'"));
97+
VerifyTokenizer(@"""?""", DelimitedText(@"""?"""));
98+
VerifyTokenizer("[?]", DelimitedText("[?]"));
99+
VerifyTokenizer("--?", Comment("--?"));
100+
VerifyTokenizer("/*?*/", Comment("/*?*/"));
101+
VerifyTokenizer("(?)", BracketOpen(), Parameter(), BracketClose());
102+
VerifyTokenizer("EXEC InsertSomething ?, ?",
103+
Text("EXEC"), Whitespace(" "), Text("InsertSomething"),
104+
Whitespace(" "), Parameter(), Comma(),
105+
Whitespace(" "), Parameter());
106+
}
107+
108+
private static void VerifyTokenizer(string sql, params ExpectedToken[] expectedTokens)
109+
{
110+
var sqlString = SqlString.Parse(sql);
111+
112+
int tokenIndex = 0;
113+
int sqlIndex = 0;
114+
foreach (var token in new SqlTokenizer(sqlString) { IgnoreComments = false, IgnoreWhitespace = false })
115+
{
116+
if (tokenIndex >= expectedTokens.Length)
117+
{
118+
Assert.Fail("Tokenizer returns more than expected '{0}' tokens. \nSQL: {1}\nLast Token: {2}({3})",
119+
expectedTokens.Length, sql, token.TokenType, token.Value);
120+
}
121+
122+
var expectedToken = expectedTokens[tokenIndex];
123+
Assert.That(token.TokenType, Is.EqualTo(expectedToken.TokenType), "[Token #{0} in '{1}']TokenType", tokenIndex, sql);
124+
Assert.That(token.Value, Is.EqualTo(expectedToken.Value), "[Token #{0} in {1}]Value", tokenIndex, sql);
125+
Assert.That(token.SqlIndex, Is.EqualTo(sqlIndex), "[Token #{0} in {1}]SqlIndex", tokenIndex, sql);
126+
Assert.That(token.Length, Is.EqualTo(expectedToken.Length), "[Token #{0} in {1}]Length", tokenIndex, sql);
127+
128+
tokenIndex++;
129+
sqlIndex += expectedToken.Length;
130+
}
131+
132+
if (tokenIndex < expectedTokens.Length)
133+
{
134+
Assert.Fail("Tokenizer returns less than expected '{0}' tokens.\nSQL: {1}", expectedTokens.Length, sql);
135+
}
136+
}
137+
138+
private static ExpectedToken Comma()
139+
{
140+
return new ExpectedToken(SqlTokenType.Comma, ",");
141+
}
142+
143+
private static ExpectedToken Parameter()
144+
{
145+
return new ExpectedToken(SqlTokenType.Parameter, "?");
146+
}
147+
148+
private static ExpectedToken BracketOpen()
149+
{
150+
return new ExpectedToken(SqlTokenType.BracketOpen, "(");
151+
}
152+
153+
private static ExpectedToken BracketClose()
154+
{
155+
return new ExpectedToken(SqlTokenType.BracketClose, ")");
156+
}
157+
158+
private static ExpectedToken DelimitedText(string text)
159+
{
160+
return new ExpectedToken(SqlTokenType.DelimitedText, text);
161+
}
162+
163+
private static ExpectedToken Text(string text)
164+
{
165+
return new ExpectedToken(SqlTokenType.Text, text);
166+
}
167+
168+
private static ExpectedToken Comment(string text)
169+
{
170+
return new ExpectedToken(SqlTokenType.Comment, text);
171+
}
172+
173+
private static ExpectedToken Whitespace(string text)
174+
{
175+
return new ExpectedToken(SqlTokenType.Whitespace, text);
176+
}
177+
178+
private class ExpectedToken
179+
{
180+
public SqlTokenType TokenType { get; private set; }
181+
public string Value { get; private set; }
182+
183+
public ExpectedToken(SqlTokenType tokenType, string value)
184+
{
185+
this.TokenType = tokenType;
186+
this.Value = value;
187+
}
188+
189+
public int Length
190+
{
191+
get { return this.Value != null ? this.Value.Length : 0; }
192+
}
193+
}
194+
}
195+
}

src/NHibernate/NHibernate.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,17 +609,21 @@
609609
<Compile Include="SqlCommand\JoinFragment.cs" />
610610
<Compile Include="SqlCommand\OracleJoinFragment.cs" />
611611
<Compile Include="SqlCommand\Parameter.cs" />
612+
<Compile Include="SqlCommand\Parser\SqlToken.cs" />
613+
<Compile Include="SqlCommand\Parser\SqlTokenType.cs" />
612614
<Compile Include="SqlCommand\QueryJoinFragment.cs" />
613615
<Compile Include="SqlCommand\QuerySelect.cs" />
614616
<Compile Include="SqlCommand\SelectFragment.cs" />
615617
<Compile Include="SqlCommand\SqlBaseBuilder.cs" />
616618
<Compile Include="SqlCommand\SqlDeleteBuilder.cs" />
617619
<Compile Include="SqlCommand\SqlInsertBuilder.cs" />
618620
<Compile Include="SqlCommand\SqlCommandImpl.cs" />
621+
<Compile Include="SqlCommand\Parser\SqlParserUtils.cs" />
619622
<Compile Include="SqlCommand\SqlSelectBuilder.cs" />
620623
<Compile Include="SqlCommand\SqlSimpleSelectBuilder.cs" />
621624
<Compile Include="SqlCommand\SqlString.cs" />
622625
<Compile Include="SqlCommand\SqlStringBuilder.cs" />
626+
<Compile Include="SqlCommand\Parser\SqlTokenizer.cs" />
623627
<Compile Include="SqlCommand\SqlUpdateBuilder.cs" />
624628
<Compile Include="SqlCommand\Template.cs" />
625629
<Compile Include="SqlCommand\WhereBuilder.cs" />
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using NHibernate.Exceptions;
2+
3+
namespace NHibernate.SqlCommand.Parser
4+
{
5+
internal static class SqlParserUtils
6+
{
7+
public static int ReadDelimitedText(string text, int maxOffset, int offset)
8+
{
9+
var startOffset = offset;
10+
char quoteEndChar;
11+
12+
// Determine end delimiter
13+
var quoteChar = text[offset++];
14+
switch (quoteChar)
15+
{
16+
case '\'':
17+
case '"':
18+
quoteEndChar = quoteChar;
19+
break;
20+
case '[':
21+
quoteEndChar = ']';
22+
break;
23+
default:
24+
throw new SqlParseException(string.Format("Quoted text cannot start with '{0}' character", text[offset]));
25+
}
26+
27+
// Find end delimiter, but ignore escaped end delimiters
28+
while (offset < maxOffset)
29+
{
30+
if (text[offset++] == quoteEndChar)
31+
{
32+
if (offset >= maxOffset || text[offset] != quoteEndChar)
33+
{
34+
// Non-escaped delimiter char
35+
return offset - startOffset;
36+
}
37+
38+
// Escaped delimiter char
39+
offset++;
40+
}
41+
}
42+
43+
throw new SqlParseException(string.Format("Cannot find terminating '{0}' character for quoted text.", quoteEndChar));
44+
}
45+
46+
public static int ReadLineComment(string text, int maxOffset, int offset)
47+
{
48+
var startOffset = offset;
49+
50+
offset += 2;
51+
for (; offset < maxOffset; offset++)
52+
{
53+
switch (text[offset])
54+
{
55+
case '\r':
56+
case '\n':
57+
return offset - startOffset;
58+
}
59+
}
60+
61+
return offset - startOffset;
62+
}
63+
64+
public static int ReadMultilineComment(string text, int maxOffset, int offset)
65+
{
66+
var startOffset = offset;
67+
offset += 2;
68+
69+
var prevChar = '\0';
70+
for (; offset < maxOffset; offset++)
71+
{
72+
var ch = text[offset];
73+
if (ch == '/' && prevChar == '*')
74+
{
75+
return offset + 1 - startOffset;
76+
}
77+
78+
prevChar = ch;
79+
}
80+
81+
throw new SqlParseException(string.Format("Cannot find terminating '*/' string for multiline comment."));
82+
}
83+
84+
public static int ReadWhitespace(string text, int maxOffset, int offset)
85+
{
86+
var startOffset = offset;
87+
88+
offset++;
89+
while (offset < maxOffset)
90+
{
91+
if (!char.IsWhiteSpace(text[offset])) break;
92+
offset++;
93+
}
94+
95+
var result = offset - startOffset;
96+
return result;
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)