syntax.sql.pp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. {
  2. This file is part of the Free Component Library (FCL)
  3. Copyright (c) 2025 by Michael Van Canneyt
  4. SQL syntax highlighter
  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. {$MODE objfpc}
  12. {$H+}
  13. unit syntax.sql;
  14. interface
  15. uses
  16. {$IFDEF FPC_DOTTEDUNITS}
  17. System.Types, System.SysUtils, syntax.highlighter;
  18. {$ELSE}
  19. types, sysutils, syntax.highlighter;
  20. {$ENDIF}
  21. type
  22. // String escaping modes for SQL
  23. TSqlStringEscapeMode = (
  24. semBackslash, // Backslash escaping: 'I\'m here'
  25. semDoubled // Doubled character escaping: 'I''m here' (Firebird, standard SQL)
  26. );
  27. { TSqlSyntaxHighlighter }
  28. TSqlSyntaxHighlighter = class(TSyntaxHighlighter)
  29. private
  30. FSource: string;
  31. FPos: integer;
  32. FStringEscapeMode: TSqlStringEscapeMode;
  33. protected
  34. procedure ProcessSingleQuoteString(var endPos: integer);
  35. procedure ProcessDoubleQuoteString(var endPos: integer);
  36. procedure ProcessSingleLineComment(var endPos: integer);
  37. procedure ProcessMultiLineComment(var endPos: integer);
  38. procedure ProcessNumber(var endPos: integer);
  39. function CheckForKeyword(var endPos: integer): boolean;
  40. function IsWordChar(ch: char): boolean;
  41. function IsHexChar(ch: char): boolean;
  42. class procedure CheckCategories;
  43. class procedure RegisterDefaultCategories; override;
  44. class function GetLanguages : TStringDynarray; override;
  45. public
  46. constructor Create; override;
  47. class var
  48. CategorySQL : Integer;
  49. function Execute(const Source: string): TSyntaxTokenArray; override;
  50. property StringEscapeMode: TSqlStringEscapeMode read FStringEscapeMode write FStringEscapeMode;
  51. end;
  52. const
  53. MaxKeywordLength = 20;
  54. MaxKeyword = 113;
  55. SqlKeywordTable: array[0..MaxKeyword] of string = (
  56. // Basic SQL keywords
  57. 'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER',
  58. 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'PROCEDURE', 'FUNCTION', 'TRIGGER',
  59. // Data types
  60. 'INTEGER', 'INT', 'BIGINT', 'SMALLINT', 'DECIMAL', 'NUMERIC', 'FLOAT', 'REAL', 'DOUBLE',
  61. 'VARCHAR', 'CHAR', 'TEXT', 'BLOB', 'CLOB', 'DATE', 'TIME', 'TIMESTAMP', 'BOOLEAN',
  62. // Constraints and modifiers
  63. 'PRIMARY', 'FOREIGN', 'KEY', 'REFERENCES', 'CONSTRAINT', 'UNIQUE', 'NOT', 'NULL',
  64. 'DEFAULT', 'CHECK', 'AUTO_INCREMENT', 'IDENTITY',
  65. // Joins and set operations
  66. 'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'CROSS', 'ON', 'USING',
  67. 'UNION', 'INTERSECT', 'EXCEPT', 'MINUS',
  68. // Clauses and operators
  69. 'AND', 'OR', 'IN', 'EXISTS', 'BETWEEN', 'LIKE', 'IS', 'AS', 'DISTINCT', 'ALL', 'ANY', 'SOME',
  70. 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'TOP',
  71. // Functions and aggregates
  72. 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
  73. 'CAST', 'CONVERT', 'COALESCE', 'NULLIF',
  74. // Transaction control
  75. 'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'SAVEPOINT',
  76. // Privileges and security
  77. 'GRANT', 'REVOKE', 'ROLE', 'USER', 'PRIVILEGES',
  78. // Conditional and flow control
  79. 'IF', 'ELSIF', 'ELSEIF', 'WHILE', 'FOR', 'LOOP', 'DECLARE', 'SET',
  80. // Schema operations
  81. 'SCHEMA', 'CATALOG', 'DOMAIN', 'SEQUENCE'
  82. );
  83. function DoSqlHighlighting(const Source: string): TSyntaxTokenArray;
  84. function DoSqlHighlighting(const Source: string; EscapeMode: TSqlStringEscapeMode): TSyntaxTokenArray;
  85. implementation
  86. { TSqlSyntaxHighlighter }
  87. procedure TSqlSyntaxHighlighter.ProcessSingleQuoteString(var endPos: integer);
  88. var
  89. startPos: integer;
  90. begin
  91. startPos := FPos;
  92. Inc(FPos); // Skip opening quote
  93. while FPos <= Length(FSource) do
  94. begin
  95. if FSource[FPos] = '''' then
  96. begin
  97. if FStringEscapeMode = semDoubled then
  98. begin
  99. // Standard SQL doubled quote escaping
  100. if (FPos < Length(FSource)) and (FSource[FPos + 1] = '''') then
  101. Inc(FPos, 2) // Skip escaped quote
  102. else
  103. begin
  104. Inc(FPos); // Skip closing quote
  105. break;
  106. end;
  107. end
  108. else
  109. begin
  110. // Single quote always ends the string in backslash mode
  111. Inc(FPos);
  112. break;
  113. end;
  114. end
  115. else if (FStringEscapeMode = semBackslash) and (FSource[FPos] = '\') then
  116. begin
  117. if FPos < Length(FSource) then
  118. Inc(FPos); // Skip escaped character
  119. Inc(FPos);
  120. end
  121. else
  122. Inc(FPos);
  123. end;
  124. endPos := FPos - 1;
  125. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shStrings);
  126. end;
  127. procedure TSqlSyntaxHighlighter.ProcessDoubleQuoteString(var endPos: integer);
  128. var
  129. startPos: integer;
  130. begin
  131. startPos := FPos;
  132. Inc(FPos); // Skip opening quote
  133. while FPos <= Length(FSource) do
  134. begin
  135. if FSource[FPos] = '"' then
  136. begin
  137. if FStringEscapeMode = semDoubled then
  138. begin
  139. // Standard SQL doubled quote escaping
  140. if (FPos < Length(FSource)) and (FSource[FPos + 1] = '"') then
  141. Inc(FPos, 2) // Skip escaped quote
  142. else
  143. begin
  144. Inc(FPos); // Skip closing quote
  145. break;
  146. end;
  147. end
  148. else
  149. begin
  150. // Double quote always ends the string in backslash mode
  151. Inc(FPos);
  152. break;
  153. end;
  154. end
  155. else if (FStringEscapeMode = semBackslash) and (FSource[FPos] = '\') then
  156. begin
  157. if FPos < Length(FSource) then
  158. Inc(FPos); // Skip escaped character
  159. Inc(FPos);
  160. end
  161. else
  162. Inc(FPos);
  163. end;
  164. endPos := FPos - 1;
  165. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shStrings);
  166. end;
  167. procedure TSqlSyntaxHighlighter.ProcessSingleLineComment(var endPos: integer);
  168. var
  169. startPos: integer;
  170. begin
  171. startPos := FPos;
  172. Inc(FPos, 2); // Skip '--'
  173. // Process until end of line
  174. while (FPos <= Length(FSource)) and (FSource[FPos] <> #10) and (FSource[FPos] <> #13) do
  175. Inc(FPos);
  176. endPos := FPos - 1;
  177. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shComment);
  178. end;
  179. procedure TSqlSyntaxHighlighter.ProcessMultiLineComment(var endPos: integer);
  180. var
  181. startPos: integer;
  182. begin
  183. startPos := FPos;
  184. Inc(FPos, 2); // Skip the opening /*
  185. while FPos < Length(FSource) do
  186. begin
  187. if (FSource[FPos] = '*') and (FSource[FPos + 1] = '/') then
  188. begin
  189. Inc(FPos, 2);
  190. break;
  191. end;
  192. Inc(FPos);
  193. end;
  194. endPos := FPos - 1;
  195. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shComment);
  196. end;
  197. procedure TSqlSyntaxHighlighter.ProcessNumber(var endPos: integer);
  198. var
  199. startPos: integer;
  200. hasDecimalPoint: boolean;
  201. begin
  202. startPos := FPos;
  203. hasDecimalPoint := False;
  204. // Handle numbers (including decimals and scientific notation)
  205. while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
  206. Inc(FPos);
  207. // Handle decimal point
  208. if (FPos <= Length(FSource)) and (FSource[FPos] = '.') and not hasDecimalPoint then
  209. begin
  210. hasDecimalPoint := True;
  211. Inc(FPos);
  212. while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
  213. Inc(FPos);
  214. end;
  215. // Handle scientific notation (E or e)
  216. if (FPos <= Length(FSource)) and (FSource[FPos] in ['E', 'e']) then
  217. begin
  218. Inc(FPos);
  219. if (FPos <= Length(FSource)) and (FSource[FPos] in ['+', '-']) then
  220. Inc(FPos);
  221. while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
  222. Inc(FPos);
  223. end;
  224. endPos := FPos - 1;
  225. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shNumbers);
  226. end;
  227. function TSqlSyntaxHighlighter.CheckForKeyword(var endPos: integer): boolean;
  228. var
  229. i, j: integer;
  230. keyword, ukeyword: string;
  231. begin
  232. Result := False;
  233. i := 0;
  234. while (FPos + i <= Length(FSource)) and (i < MaxKeywordLength) and
  235. IsWordChar(FSource[FPos + i]) do
  236. Inc(i);
  237. keyword := Copy(FSource, FPos, i);
  238. ukeyword := UpperCase(keyword);
  239. for j := 0 to MaxKeyword do
  240. if SqlKeywordTable[j] = ukeyword then
  241. begin
  242. Result := True;
  243. break;
  244. end;
  245. if Result then
  246. begin
  247. Inc(FPos, i);
  248. endPos := FPos - 1;
  249. AddToken(keyword, shKeyword);
  250. end;
  251. end;
  252. function TSqlSyntaxHighlighter.IsWordChar(ch: char): boolean;
  253. begin
  254. Result := ch in ['a'..'z', 'A'..'Z', '0'..'9', '_'];
  255. end;
  256. function TSqlSyntaxHighlighter.IsHexChar(ch: char): boolean;
  257. begin
  258. Result := ch in ['0'..'9', 'A'..'F', 'a'..'f'];
  259. end;
  260. class procedure TSqlSyntaxHighlighter.CheckCategories;
  261. begin
  262. if CategorySQL = 0 then
  263. RegisterDefaultCategories;
  264. end;
  265. class procedure TSqlSyntaxHighlighter.RegisterDefaultCategories;
  266. begin
  267. CategorySQL := RegisterCategory('SQL');
  268. end;
  269. class function TSqlSyntaxHighlighter.GetLanguages: TStringDynarray;
  270. begin
  271. Result := ['sql', 'mysql', 'postgresql', 'sqlite', 'firebird', 'oracle', 'mssql', 'tsql'];
  272. end;
  273. constructor TSqlSyntaxHighlighter.Create;
  274. begin
  275. inherited Create;
  276. CheckCategories;
  277. DefaultCategory := CategorySQL;
  278. FStringEscapeMode := semDoubled; // Default to standard SQL escaping
  279. end;
  280. function TSqlSyntaxHighlighter.Execute(const Source: string): TSyntaxTokenArray;
  281. var
  282. lLen, endPos, startPos: integer;
  283. ch: char;
  284. begin
  285. Result := Nil;
  286. CheckCategories;
  287. lLen := Length(Source);
  288. if lLen = 0 then
  289. Exit;
  290. FSource := Source;
  291. FTokens.Reset;
  292. FPos := 1;
  293. EndPos := 0;
  294. while FPos <= lLen do
  295. begin
  296. ch := FSource[FPos];
  297. case ch of
  298. '''':
  299. ProcessSingleQuoteString(endPos);
  300. '"':
  301. ProcessDoubleQuoteString(endPos);
  302. '-':
  303. begin
  304. if (FPos < Length(FSource)) and (FSource[FPos + 1] = '-') then
  305. ProcessSingleLineComment(endPos)
  306. else
  307. begin
  308. AddToken('-', shOperator);
  309. endPos := FPos;
  310. Inc(FPos);
  311. end;
  312. end;
  313. '/':
  314. begin
  315. if (FPos < Length(FSource)) and (FSource[FPos + 1] = '*') then
  316. ProcessMultiLineComment(endPos)
  317. else
  318. begin
  319. AddToken('/', shOperator);
  320. endPos := FPos;
  321. Inc(FPos);
  322. end;
  323. end;
  324. '0'..'9':
  325. ProcessNumber(endPos);
  326. '$': // Hexadecimal numbers (some SQL dialects)
  327. begin
  328. startPos := FPos;
  329. Inc(FPos);
  330. while (FPos <= Length(FSource)) and IsHexChar(FSource[FPos]) do
  331. Inc(FPos);
  332. endPos := FPos - 1;
  333. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shNumbers);
  334. end;
  335. 'a'..'z', 'A'..'Z', '_':
  336. begin
  337. if not CheckForKeyword(endPos) then
  338. begin
  339. startPos := FPos;
  340. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  341. Inc(FPos);
  342. endPos := FPos - 1;
  343. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  344. end;
  345. end;
  346. '(', ')', '[', ']', '{', '}', ';', ',':
  347. begin
  348. AddToken(ch, shSymbol);
  349. endPos := FPos;
  350. Inc(FPos);
  351. end;
  352. '=', '<', '>', '!', '+', '*', '%', '&', '|', '^', '~':
  353. begin
  354. startPos := FPos;
  355. // Handle multi-character operators
  356. if ch = '<' then
  357. begin
  358. if (FPos < Length(FSource)) and (FSource[FPos + 1] in ['=', '>', '<']) then
  359. Inc(FPos);
  360. end
  361. else if ch = '>' then
  362. begin
  363. if (FPos < Length(FSource)) and (FSource[FPos + 1] in ['=', '<']) then
  364. Inc(FPos);
  365. end
  366. else if ch = '!' then
  367. begin
  368. if (FPos < Length(FSource)) and (FSource[FPos + 1] = '=') then
  369. Inc(FPos);
  370. end;
  371. Inc(FPos);
  372. endPos := FPos - 1;
  373. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shOperator);
  374. end;
  375. ' ', #9, #10, #13:
  376. begin
  377. startPos := FPos;
  378. while (FPos <= Length(FSource)) and (FSource[FPos] in [' ', #9, #10, #13]) do
  379. Inc(FPos);
  380. endPos := FPos - 1;
  381. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  382. end;
  383. else
  384. AddToken(ch, shInvalid);
  385. endPos := FPos;
  386. Inc(FPos);
  387. end;
  388. if FPos = endPos then Inc(FPos);
  389. end;
  390. Result := FTokens.GetTokens;
  391. end;
  392. function DoSqlHighlighting(const Source: string): TSyntaxTokenArray;
  393. var
  394. highlighter: TSqlSyntaxHighlighter;
  395. begin
  396. highlighter := TSqlSyntaxHighlighter.Create;
  397. try
  398. Result := highlighter.Execute(Source);
  399. finally
  400. highlighter.Free;
  401. end;
  402. end;
  403. function DoSqlHighlighting(const Source: string; EscapeMode: TSqlStringEscapeMode): TSyntaxTokenArray;
  404. var
  405. highlighter: TSqlSyntaxHighlighter;
  406. begin
  407. highlighter := TSqlSyntaxHighlighter.Create;
  408. try
  409. highlighter.StringEscapeMode := EscapeMode;
  410. Result := highlighter.Execute(Source);
  411. finally
  412. highlighter.Free;
  413. end;
  414. end;
  415. initialization
  416. TSqlSyntaxHighlighter.Register;
  417. end.