""" SQL syntax highlighter for QPlainTextEdit. Highlights keywords, types, functions, strings, comments, and numbers. """ import re from PyQt6.QtCore import QRegularExpression, Qt from PyQt6.QtGui import ( QSyntaxHighlighter, QTextCharFormat, QColor, QFont ) def _fmt(color: str, bold: bool = False, italic: bool = False) -> QTextCharFormat: f = QTextCharFormat() f.setForeground(QColor(color)) if bold: f.setFontWeight(QFont.Weight.Bold) if italic: f.setFontItalic(True) return f # ── Token categories (Catppuccin Frappé palette) ───────────────────────────── _KEYWORD_FMT = _fmt("#8caaee", bold=True) # blue — DDL/DML/control _TYPE_FMT = _fmt("#ef9f76") # peach — data types _FUNCTION_FMT = _fmt("#a6d189") # green — functions _STRING_FMT = _fmt("#a6d189") # green — string literals _NUMBER_FMT = _fmt("#ef9f76") # orange — numeric literals _COMMENT_FMT = _fmt("#737994", italic=True) # grey — comments _OPERATOR_FMT = _fmt("#ca9ee6") # mauve — operators/special _STAR_FMT = _fmt("#ca9ee6", bold=True) # mauve — * wildcard # ── Keyword lists ───────────────────────────────────────────────────────────── _KEYWORDS = [ "SELECT", "FROM", "WHERE", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "FULL", "CROSS", "ON", "AS", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN", "IS", "NULL", "EXISTS", "CASE", "WHEN", "THEN", "ELSE", "END", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "MERGE", "CREATE", "TABLE", "VIEW", "INDEX", "DATABASE", "SCHEMA", "SEQUENCE", "DROP", "ALTER", "ADD", "COLUMN", "PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "DEFAULT", "CONSTRAINT", "CHECK", "IF", "EXISTS", "NOT", "TRUNCATE", "RENAME", "MODIFY", "CHANGE", "GRANT", "REVOKE", "COMMIT", "ROLLBACK", "SAVEPOINT", "BEGIN", "TRANSACTION", "START", "END", "LOCK", "UNLOCK", "EXPLAIN", "ANALYZE", "VACUUM", "SHOW", "DESCRIBE", "DESC", "ASC", "LIMIT", "OFFSET", "ORDER", "BY", "GROUP", "HAVING", "DISTINCT", "UNION", "ALL", "INTERSECT", "EXCEPT", "PARTITION", "OVER", "WITH", "RECURSIVE", "USE", "CALL", "EXEC", "EXECUTE", "PROCEDURE", "FUNCTION", "TRIGGER", "EVENT", "REPLACE", "TRUE", "FALSE", "RETURNING", ] _TYPES = [ "INT", "INTEGER", "BIGINT", "SMALLINT", "TINYINT", "MEDIUMINT", "FLOAT", "DOUBLE", "DECIMAL", "NUMERIC", "REAL", "MONEY", "VARCHAR", "CHAR", "TEXT", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT", "BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", "BINARY", "VARBINARY", "DATE", "DATETIME", "TIMESTAMP", "TIME", "YEAR", "BOOLEAN", "BOOL", "BIT", "ENUM", "SET", "JSON", "UUID", "SERIAL", "BYTEA", "NVARCHAR", "NCHAR", "IMAGE", "XML", "CURSOR", "ROWVERSION", "UNIQUEIDENTIFIER", ] _FUNCTIONS = [ "COUNT", "SUM", "AVG", "MIN", "MAX", "COALESCE", "IFNULL", "NULLIF", "NOW", "CURDATE", "CURTIME", "DATE", "YEAR", "MONTH", "DAY", "CONCAT", "SUBSTRING", "LENGTH", "TRIM", "UPPER", "LOWER", "REPLACE", "ROUND", "FLOOR", "CEIL", "ABS", "MOD", "POWER", "CAST", "CONVERT", "ISNULL", "ISNUMERIC", "ROW_NUMBER", "RANK", "DENSE_RANK", "LAG", "LEAD", "NTILE", "FIRST_VALUE", "LAST_VALUE", "NTH_VALUE", "STRING_AGG", "GROUP_CONCAT", "ARRAY_AGG", "TO_CHAR", "TO_DATE", "TO_NUMBER", "EXTRACT", "DATEDIFF", "DATEADD", "DATE_FORMAT", "DATE_TRUNC", "IF", "IIF", "DECODE", "GREATEST", "LEAST", ] class SQLHighlighter(QSyntaxHighlighter): """Applies syntax colouring to a SQL document.""" def __init__(self, document): super().__init__(document) self._rules: list[tuple] = [] def kw_pattern(words: list[str]) -> str: return r"\b(?:" + "|".join(words) + r")\b" self._rules = [ # Keywords (case-insensitive handled via flag) (QRegularExpression(kw_pattern(_KEYWORDS), QRegularExpression.PatternOption.CaseInsensitiveOption), _KEYWORD_FMT), # Data types (QRegularExpression(kw_pattern(_TYPES), QRegularExpression.PatternOption.CaseInsensitiveOption), _TYPE_FMT), # Functions (QRegularExpression(kw_pattern(_FUNCTIONS), QRegularExpression.PatternOption.CaseInsensitiveOption), _FUNCTION_FMT), # Numbers (QRegularExpression(r"\b\d+(\.\d+)?\b"), _NUMBER_FMT), # Single-quoted strings (QRegularExpression(r"'[^'\\]*(?:\\.[^'\\]*)*'"), _STRING_FMT), # Double-quoted identifiers (QRegularExpression(r'"[^"]*"'), _fmt("#99d1db")), # Backtick identifiers (MySQL) (QRegularExpression(r"`[^`]*`"), _fmt("#99d1db")), # * wildcard (QRegularExpression(r"\bSELECT\s+\*|\*(?=\s*FROM)", QRegularExpression.PatternOption.CaseInsensitiveOption), _STAR_FMT), # Operators (QRegularExpression(r"[=<>!%&|^~+\-*/]"), _OPERATOR_FMT), # Single-line comment -- (QRegularExpression(r"--[^\n]*"), _COMMENT_FMT), # Single-line comment # (QRegularExpression(r"#[^\n]*"), _COMMENT_FMT), ] # Multi-line block comment /* ... */ self._block_comment_start = QRegularExpression(r"/\*") self._block_comment_end = QRegularExpression(r"\*/") def highlightBlock(self, text: str) -> None: # Single-line rules for pattern, fmt in self._rules: it = pattern.globalMatch(text) while it.hasNext(): m = it.next() self.setFormat(m.capturedStart(), m.capturedLength(), fmt) # Multi-line block comments self.setCurrentBlockState(0) start_idx = 0 if self.previousBlockState() != 1: m = self._block_comment_start.match(text) start_idx = m.capturedStart() if m.hasMatch() else -1 while start_idx >= 0: end_m = self._block_comment_end.match(text, start_idx) if end_m.hasMatch(): end_idx = end_m.capturedStart() + end_m.capturedLength() self.setFormat(start_idx, end_idx - start_idx, _COMMENT_FMT) m = self._block_comment_start.match(text, end_idx) start_idx = m.capturedStart() if m.hasMatch() else -1 else: self.setCurrentBlockState(1) self.setFormat(start_idx, len(text) - start_idx, _COMMENT_FMT) break