unittest.sql.pp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. {
  2. This file is part of the Free Component Library (FCL)
  3. Copyright (c) 2025 by Michael Van Canneyt
  4. SQL highlighter unit test
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. unit unittest.sql;
  12. interface
  13. {$mode objfpc}{$H+}
  14. uses
  15. Classes, SysUtils, fpcunit, testregistry,
  16. syntax.highlighter, syntax.sql;
  17. type
  18. TTestSqlHighlighter = class(TTestCase)
  19. protected
  20. procedure SetUp; override;
  21. procedure TearDown; override;
  22. private
  23. function DoSqlHighlighting(const source: string): TSyntaxTokenArray;
  24. function DoSqlHighlightingWithMode(const source: string; mode: TSqlStringEscapeMode): TSyntaxTokenArray;
  25. published
  26. procedure TestSqlKeywords;
  27. procedure TestSqlStringsDoubledEscape;
  28. procedure TestSqlStringsBackslashEscape;
  29. procedure TestSqlStringEscapeModeProperty;
  30. procedure TestSqlNumbers;
  31. procedure TestSqlComments;
  32. procedure TestSqlOperators;
  33. procedure TestSqlSymbols;
  34. procedure TestComplexSqlQuery;
  35. procedure TestSqlDataTypes;
  36. procedure TestSqlFunctions;
  37. procedure TestSqlJoins;
  38. procedure TestHexNumbers;
  39. procedure TestScientificNotation;
  40. procedure TestMultiCharOperators;
  41. procedure TestNestedComments;
  42. end;
  43. implementation
  44. procedure TTestSqlHighlighter.SetUp;
  45. begin
  46. end;
  47. procedure TTestSqlHighlighter.TearDown;
  48. begin
  49. // Nothing to do
  50. end;
  51. function TTestSqlHighlighter.DoSqlHighlighting(const source: string): TSyntaxTokenArray;
  52. var
  53. highlighter: TSqlSyntaxHighlighter;
  54. begin
  55. highlighter := TSqlSyntaxHighlighter.Create;
  56. try
  57. Result := highlighter.Execute(source);
  58. finally
  59. highlighter.Free;
  60. end;
  61. end;
  62. function TTestSqlHighlighter.DoSqlHighlightingWithMode(const source: string; mode: TSqlStringEscapeMode): TSyntaxTokenArray;
  63. var
  64. highlighter: TSqlSyntaxHighlighter;
  65. begin
  66. highlighter := TSqlSyntaxHighlighter.Create;
  67. try
  68. highlighter.StringEscapeMode := mode;
  69. Result := highlighter.Execute(source);
  70. finally
  71. highlighter.Free;
  72. end;
  73. end;
  74. procedure TTestSqlHighlighter.TestSqlKeywords;
  75. const
  76. Keywords: array[0..9] of string = (
  77. 'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'TABLE', 'JOIN', 'ORDER'
  78. );
  79. var
  80. tokens: TSyntaxTokenArray;
  81. i: Integer;
  82. begin
  83. for i := 0 to High(Keywords) do
  84. begin
  85. tokens := DoSqlHighlighting(Keywords[i]);
  86. AssertEquals('Should have 1 token for ' + Keywords[i], 1, Length(tokens));
  87. AssertEquals('Token should be ' + Keywords[i], Keywords[i], tokens[0].Text);
  88. AssertEquals(Keywords[i] + ' should be keyword', Ord(shKeyword), Ord(tokens[0].Kind));
  89. // Test lowercase version
  90. tokens := DoSqlHighlighting(LowerCase(Keywords[i]));
  91. AssertEquals('Should have 1 token for ' + LowerCase(Keywords[i]), 1, Length(tokens));
  92. AssertEquals('Token should be ' + LowerCase(Keywords[i]), LowerCase(Keywords[i]), tokens[0].Text);
  93. AssertEquals(LowerCase(Keywords[i]) + ' should be keyword', Ord(shKeyword), Ord(tokens[0].Kind));
  94. end;
  95. end;
  96. procedure TTestSqlHighlighter.TestSqlStringsDoubledEscape;
  97. var
  98. tokens: TSyntaxTokenArray;
  99. begin
  100. // Test simple single-quoted string with doubled escaping (default mode)
  101. tokens := DoSqlHighlighting('''Hello World''');
  102. AssertEquals('Should have 1 token', 1, Length(tokens));
  103. AssertEquals('Token should be single-quoted string', '''Hello World''', tokens[0].Text);
  104. AssertEquals('Token should be string', Ord(shStrings), Ord(tokens[0].Kind));
  105. // Test string with escaped single quote (doubled)
  106. tokens := DoSqlHighlighting('''Can''''t do it''');
  107. AssertEquals('Should have 1 token', 1, Length(tokens));
  108. AssertEquals('Token should be escaped string', '''Can''''t do it''', tokens[0].Text);
  109. AssertEquals('Token should be string', Ord(shStrings), Ord(tokens[0].Kind));
  110. // Test double-quoted string with escaped double quote (doubled)
  111. tokens := DoSqlHighlighting('"Say ""Hello"""');
  112. AssertEquals('Should have 1 token', 1, Length(tokens));
  113. AssertEquals('Token should be escaped double-quoted string', '"Say ""Hello"""', tokens[0].Text);
  114. AssertEquals('Token should be string', Ord(shStrings), Ord(tokens[0].Kind));
  115. end;
  116. procedure TTestSqlHighlighter.TestSqlStringsBackslashEscape;
  117. var
  118. tokens: TSyntaxTokenArray;
  119. begin
  120. // Test simple single-quoted string with backslash escaping
  121. tokens := DoSqlHighlightingWithMode('''Hello World''', semBackslash);
  122. AssertEquals('Should have 1 token', 1, Length(tokens));
  123. AssertEquals('Token should be single-quoted string', '''Hello World''', tokens[0].Text);
  124. AssertEquals('Token should be string', Ord(shStrings), Ord(tokens[0].Kind));
  125. // Test string with escaped single quote (backslash)
  126. tokens := DoSqlHighlightingWithMode('''Can\''t do it''', semBackslash);
  127. AssertEquals('Should have 1 token', 1, Length(tokens));
  128. AssertEquals('Token should be escaped string', '''Can\''t do it''', tokens[0].Text);
  129. AssertEquals('Token should be string', Ord(shStrings), Ord(tokens[0].Kind));
  130. // Test string with escaped backslash
  131. tokens := DoSqlHighlightingWithMode('''Path\\to\\file''', semBackslash);
  132. AssertEquals('Should have 1 token', 1, Length(tokens));
  133. AssertEquals('Token should be escaped string', '''Path\\to\\file''', tokens[0].Text);
  134. AssertEquals('Token should be string', Ord(shStrings), Ord(tokens[0].Kind));
  135. end;
  136. procedure TTestSqlHighlighter.TestSqlStringEscapeModeProperty;
  137. var
  138. highlighter: TSqlSyntaxHighlighter;
  139. tokens: TSyntaxTokenArray;
  140. begin
  141. highlighter := TSqlSyntaxHighlighter.Create;
  142. try
  143. // Test default mode
  144. AssertEquals('Default should be doubled escaping', Ord(semDoubled), Ord(highlighter.StringEscapeMode));
  145. // Test setting backslash mode
  146. highlighter.StringEscapeMode := semBackslash;
  147. AssertEquals('Should be backslash escaping', Ord(semBackslash), Ord(highlighter.StringEscapeMode));
  148. // Test that mode affects string parsing
  149. tokens := highlighter.Execute('''Can\''t''');
  150. AssertTrue('Should have at least 1 token', Length(tokens) >= 1);
  151. AssertEquals('Should parse as single string token', '''Can\''t''', tokens[0].Text);
  152. AssertEquals('Should be string token', Ord(shStrings), Ord(tokens[0].Kind));
  153. finally
  154. highlighter.Free;
  155. end;
  156. end;
  157. procedure TTestSqlHighlighter.TestSqlNumbers;
  158. var
  159. tokens: TSyntaxTokenArray;
  160. begin
  161. // Test integer
  162. tokens := DoSqlHighlighting('123');
  163. AssertEquals('Should have 1 token', 1, Length(tokens));
  164. AssertEquals('Token should be integer', '123', tokens[0].Text);
  165. AssertEquals('Token should be number', Ord(shNumbers), Ord(tokens[0].Kind));
  166. // Test decimal
  167. tokens := DoSqlHighlighting('123.45');
  168. AssertEquals('Should have 1 token', 1, Length(tokens));
  169. AssertEquals('Token should be decimal', '123.45', tokens[0].Text);
  170. AssertEquals('Token should be number', Ord(shNumbers), Ord(tokens[0].Kind));
  171. // Test scientific notation
  172. tokens := DoSqlHighlighting('1.23E-4');
  173. AssertEquals('Should have 1 token', 1, Length(tokens));
  174. AssertEquals('Token should be scientific notation', '1.23E-4', tokens[0].Text);
  175. AssertEquals('Token should be number', Ord(shNumbers), Ord(tokens[0].Kind));
  176. end;
  177. procedure TTestSqlHighlighter.TestSqlComments;
  178. var
  179. tokens: TSyntaxTokenArray;
  180. i: Integer;
  181. foundComment: Boolean;
  182. begin
  183. // Test single-line comment
  184. tokens := DoSqlHighlighting('-- This is a comment');
  185. foundComment := False;
  186. for i := 0 to High(tokens) do
  187. begin
  188. if (tokens[i].Kind = shComment) and (Pos('--', tokens[i].Text) = 1) then
  189. begin
  190. foundComment := True;
  191. break;
  192. end;
  193. end;
  194. AssertTrue('Should find single-line comment', foundComment);
  195. // Test multi-line comment
  196. tokens := DoSqlHighlighting('/* Multi-line comment */');
  197. foundComment := False;
  198. for i := 0 to High(tokens) do
  199. begin
  200. if (tokens[i].Kind = shComment) and (Pos('/*', tokens[i].Text) = 1) then
  201. begin
  202. foundComment := True;
  203. break;
  204. end;
  205. end;
  206. AssertTrue('Should find multi-line comment', foundComment);
  207. end;
  208. procedure TTestSqlHighlighter.TestSqlOperators;
  209. var
  210. tokens: TSyntaxTokenArray;
  211. i: Integer;
  212. foundOperator: Boolean;
  213. begin
  214. // Test equals operator
  215. tokens := DoSqlHighlighting('=');
  216. AssertEquals('Should have 1 token', 1, Length(tokens));
  217. AssertEquals('Token should be equals', '=', tokens[0].Text);
  218. AssertEquals('Token should be operator', Ord(shOperator), Ord(tokens[0].Kind));
  219. // Test not equals operator
  220. tokens := DoSqlHighlighting('!=');
  221. foundOperator := False;
  222. for i := 0 to High(tokens) do
  223. begin
  224. if (tokens[i].Text = '!=') and (tokens[i].Kind = shOperator) then
  225. begin
  226. foundOperator := True;
  227. break;
  228. end;
  229. end;
  230. AssertTrue('Should find != operator', foundOperator);
  231. // Test less than or equal
  232. tokens := DoSqlHighlighting('<=');
  233. foundOperator := False;
  234. for i := 0 to High(tokens) do
  235. begin
  236. if (tokens[i].Text = '<=') and (tokens[i].Kind = shOperator) then
  237. begin
  238. foundOperator := True;
  239. break;
  240. end;
  241. end;
  242. AssertTrue('Should find <= operator', foundOperator);
  243. end;
  244. procedure TTestSqlHighlighter.TestSqlSymbols;
  245. var
  246. tokens: TSyntaxTokenArray;
  247. begin
  248. // Test parentheses
  249. tokens := DoSqlHighlighting('(');
  250. AssertEquals('Should have 1 token', 1, Length(tokens));
  251. AssertEquals('Token should be opening parenthesis', '(', tokens[0].Text);
  252. AssertEquals('Token should be symbol', Ord(shSymbol), Ord(tokens[0].Kind));
  253. // Test semicolon
  254. tokens := DoSqlHighlighting(';');
  255. AssertEquals('Should have 1 token', 1, Length(tokens));
  256. AssertEquals('Token should be semicolon', ';', tokens[0].Text);
  257. AssertEquals('Token should be symbol', Ord(shSymbol), Ord(tokens[0].Kind));
  258. end;
  259. procedure TTestSqlHighlighter.TestComplexSqlQuery;
  260. var
  261. tokens: TSyntaxTokenArray;
  262. sqlQuery: string;
  263. i: Integer;
  264. hasKeywords, hasStrings, hasSymbols, hasNumbers: Boolean;
  265. begin
  266. sqlQuery := 'SELECT name, age FROM users WHERE age > 18 AND name = ''John'';';
  267. tokens := DoSqlHighlighting(sqlQuery);
  268. AssertTrue('Should have multiple tokens', Length(tokens) > 10);
  269. // Check that we have different token types
  270. hasKeywords := False;
  271. hasStrings := False;
  272. hasSymbols := False;
  273. hasNumbers := False;
  274. for i := 0 to High(tokens) do
  275. begin
  276. case tokens[i].Kind of
  277. shKeyword: hasKeywords := True;
  278. shStrings: hasStrings := True;
  279. shSymbol: hasSymbols := True;
  280. shNumbers: hasNumbers := True;
  281. end;
  282. end;
  283. AssertTrue('Should contain keyword tokens', hasKeywords);
  284. AssertTrue('Should contain string tokens', hasStrings);
  285. AssertTrue('Should contain symbol tokens', hasSymbols);
  286. AssertTrue('Should contain number tokens', hasNumbers);
  287. end;
  288. procedure TTestSqlHighlighter.TestSqlDataTypes;
  289. const
  290. DataTypes: array[0..4] of string = ('INTEGER', 'VARCHAR', 'DATE', 'DECIMAL', 'BOOLEAN');
  291. var
  292. tokens: TSyntaxTokenArray;
  293. i: Integer;
  294. begin
  295. for i := 0 to High(DataTypes) do
  296. begin
  297. tokens := DoSqlHighlighting(DataTypes[i]);
  298. AssertEquals('Should have 1 token for ' + DataTypes[i], 1, Length(tokens));
  299. AssertEquals('Token should be ' + DataTypes[i], DataTypes[i], tokens[0].Text);
  300. AssertEquals(DataTypes[i] + ' should be keyword', Ord(shKeyword), Ord(tokens[0].Kind));
  301. end;
  302. end;
  303. procedure TTestSqlHighlighter.TestSqlFunctions;
  304. const
  305. Functions: array[0..4] of string = ('COUNT', 'SUM', 'MAX', 'MIN', 'AVG');
  306. var
  307. tokens: TSyntaxTokenArray;
  308. i: Integer;
  309. begin
  310. for i := 0 to High(Functions) do
  311. begin
  312. tokens := DoSqlHighlighting(Functions[i]);
  313. AssertEquals('Should have 1 token for ' + Functions[i], 1, Length(tokens));
  314. AssertEquals('Token should be ' + Functions[i], Functions[i], tokens[0].Text);
  315. AssertEquals(Functions[i] + ' should be keyword', Ord(shKeyword), Ord(tokens[0].Kind));
  316. end;
  317. end;
  318. procedure TTestSqlHighlighter.TestSqlJoins;
  319. const
  320. JoinKeywords: array[0..4] of string = ('JOIN', 'INNER', 'LEFT', 'RIGHT', 'OUTER');
  321. var
  322. tokens: TSyntaxTokenArray;
  323. i: Integer;
  324. begin
  325. for i := 0 to High(JoinKeywords) do
  326. begin
  327. tokens := DoSqlHighlighting(JoinKeywords[i]);
  328. AssertEquals('Should have 1 token for ' + JoinKeywords[i], 1, Length(tokens));
  329. AssertEquals('Token should be ' + JoinKeywords[i], JoinKeywords[i], tokens[0].Text);
  330. AssertEquals(JoinKeywords[i] + ' should be keyword', Ord(shKeyword), Ord(tokens[0].Kind));
  331. end;
  332. end;
  333. procedure TTestSqlHighlighter.TestHexNumbers;
  334. var
  335. tokens: TSyntaxTokenArray;
  336. begin
  337. // Test hexadecimal number (some SQL dialects support $-prefixed hex)
  338. tokens := DoSqlHighlighting('$DEADBEEF');
  339. AssertEquals('Should have 1 token', 1, Length(tokens));
  340. AssertEquals('Token should be hex number', '$DEADBEEF', tokens[0].Text);
  341. AssertEquals('Token should be number', Ord(shNumbers), Ord(tokens[0].Kind));
  342. // Test shorter hex number
  343. tokens := DoSqlHighlighting('$FF');
  344. AssertEquals('Should have 1 token', 1, Length(tokens));
  345. AssertEquals('Token should be hex number', '$FF', tokens[0].Text);
  346. AssertEquals('Token should be number', Ord(shNumbers), Ord(tokens[0].Kind));
  347. end;
  348. procedure TTestSqlHighlighter.TestScientificNotation;
  349. var
  350. tokens: TSyntaxTokenArray;
  351. begin
  352. // Test positive exponent
  353. tokens := DoSqlHighlighting('1.23E+10');
  354. AssertEquals('Should have 1 token', 1, Length(tokens));
  355. AssertEquals('Token should be scientific notation', '1.23E+10', tokens[0].Text);
  356. AssertEquals('Token should be number', Ord(shNumbers), Ord(tokens[0].Kind));
  357. // Test lowercase e
  358. tokens := DoSqlHighlighting('2.5e-3');
  359. AssertEquals('Should have 1 token', 1, Length(tokens));
  360. AssertEquals('Token should be scientific notation', '2.5e-3', tokens[0].Text);
  361. AssertEquals('Token should be number', Ord(shNumbers), Ord(tokens[0].Kind));
  362. end;
  363. procedure TTestSqlHighlighter.TestMultiCharOperators;
  364. var
  365. tokens: TSyntaxTokenArray;
  366. i: Integer;
  367. foundOperator: Boolean;
  368. begin
  369. // Test >= operator
  370. tokens := DoSqlHighlighting('>=');
  371. foundOperator := False;
  372. for i := 0 to High(tokens) do
  373. begin
  374. if (tokens[i].Text = '>=') and (tokens[i].Kind = shOperator) then
  375. begin
  376. foundOperator := True;
  377. break;
  378. end;
  379. end;
  380. AssertTrue('Should find >= operator', foundOperator);
  381. // Test <> operator (not equal in some SQL dialects)
  382. tokens := DoSqlHighlighting('<>');
  383. foundOperator := False;
  384. for i := 0 to High(tokens) do
  385. begin
  386. if (tokens[i].Text = '<>') and (tokens[i].Kind = shOperator) then
  387. begin
  388. foundOperator := True;
  389. break;
  390. end;
  391. end;
  392. AssertTrue('Should find <> operator', foundOperator);
  393. end;
  394. procedure TTestSqlHighlighter.TestNestedComments;
  395. var
  396. tokens: TSyntaxTokenArray;
  397. sqlWithComment: string;
  398. i: Integer;
  399. hasKeywords, hasComments: Boolean;
  400. begin
  401. sqlWithComment := 'SELECT * /* This is a comment */ FROM table1;';
  402. tokens := DoSqlHighlighting(sqlWithComment);
  403. AssertTrue('Should have multiple tokens', Length(tokens) > 5);
  404. hasKeywords := False;
  405. hasComments := False;
  406. for i := 0 to High(tokens) do
  407. begin
  408. case tokens[i].Kind of
  409. shKeyword: hasKeywords := True;
  410. shComment: hasComments := True;
  411. end;
  412. end;
  413. AssertTrue('Should contain keyword tokens', hasKeywords);
  414. AssertTrue('Should contain comment tokens', hasComments);
  415. end;
  416. initialization
  417. RegisterTest(TTestSqlHighlighter);
  418. end.