syntax.sql.pp 12 KB

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