// lexer_sql.c -- SQL language tokenizer // // Same pattern as lexer_go.c: a get_next_token() loop that advances a cursor // through the source, producing Token structs. After each token we paint // the per-byte token-type array. A second pass marks identifiers before '(' // as functions. // // SQL keywords are case-insensitive, so comparisons use a case-insensitive // match. Covers standard SQL plus common extensions (MySQL, PostgreSQL, etc.). #include "lexer/lexer.h" #include #include //////////////////////////////// // SQL keyword tables typedef struct SQLKeywordEntry { const char *word; Token_Type type; } SQLKeywordEntry; static const SQLKeywordEntry sql_keywords[] = { // DML / DQL keywords {"SELECT", TOK_KEYWORD}, {"FROM", TOK_KEYWORD}, {"WHERE", TOK_KEYWORD}, {"INSERT", TOK_KEYWORD}, {"INTO", TOK_KEYWORD}, {"UPDATE", TOK_KEYWORD}, {"DELETE", TOK_KEYWORD}, {"SET", TOK_KEYWORD}, {"VALUES", TOK_KEYWORD}, {"AS", TOK_KEYWORD}, {"ON", TOK_KEYWORD}, {"JOIN", TOK_KEYWORD}, {"INNER", TOK_KEYWORD}, {"LEFT", TOK_KEYWORD}, {"RIGHT", TOK_KEYWORD}, {"OUTER", TOK_KEYWORD}, {"FULL", TOK_KEYWORD}, {"CROSS", TOK_KEYWORD}, {"NATURAL", TOK_KEYWORD}, {"USING", TOK_KEYWORD}, {"ORDER", TOK_KEYWORD}, {"BY", TOK_KEYWORD}, {"GROUP", TOK_KEYWORD}, {"HAVING", TOK_KEYWORD}, {"LIMIT", TOK_KEYWORD}, {"OFFSET", TOK_KEYWORD}, {"UNION", TOK_KEYWORD}, {"ALL", TOK_KEYWORD}, {"DISTINCT", TOK_KEYWORD}, {"TOP", TOK_KEYWORD}, {"FETCH", TOK_KEYWORD}, {"NEXT", TOK_KEYWORD}, {"ROWS", TOK_KEYWORD}, {"ONLY", TOK_KEYWORD}, {"FIRST", TOK_KEYWORD}, {"LAST", TOK_KEYWORD}, // DDL keywords {"CREATE", TOK_KEYWORD}, {"ALTER", TOK_KEYWORD}, {"DROP", TOK_KEYWORD}, {"TABLE", TOK_KEYWORD}, {"VIEW", TOK_KEYWORD}, {"INDEX", TOK_KEYWORD}, {"DATABASE", TOK_KEYWORD}, {"SCHEMA", TOK_KEYWORD}, {"COLUMN", TOK_KEYWORD}, {"ADD", TOK_KEYWORD}, {"RENAME", TOK_KEYWORD}, {"TRUNCATE", TOK_KEYWORD}, {"REPLACE", TOK_KEYWORD}, {"TEMPORARY", TOK_KEYWORD}, {"TEMP", TOK_KEYWORD}, {"IF", TOK_KEYWORD}, {"EXISTS", TOK_KEYWORD}, {"CASCADE", TOK_KEYWORD}, {"RESTRICT", TOK_KEYWORD}, // Constraints & keys {"PRIMARY", TOK_KEYWORD}, {"KEY", TOK_KEYWORD}, {"FOREIGN", TOK_KEYWORD}, {"REFERENCES", TOK_KEYWORD}, {"UNIQUE", TOK_KEYWORD}, {"CHECK", TOK_KEYWORD}, {"CONSTRAINT", TOK_KEYWORD}, {"DEFAULT", TOK_KEYWORD}, {"AUTOINCREMENT", TOK_KEYWORD}, {"AUTO_INCREMENT", TOK_KEYWORD}, {"IDENTITY", TOK_KEYWORD}, // Control flow / procedural {"BEGIN", TOK_KEYWORD}, {"END", TOK_KEYWORD}, {"COMMIT", TOK_KEYWORD}, {"ROLLBACK", TOK_KEYWORD}, {"SAVEPOINT", TOK_KEYWORD}, {"TRANSACTION", TOK_KEYWORD}, {"RETURN", TOK_KEYWORD}, {"RETURNS", TOK_KEYWORD}, {"DECLARE", TOK_KEYWORD}, {"CURSOR", TOK_KEYWORD}, {"OPEN", TOK_KEYWORD}, {"CLOSE", TOK_KEYWORD}, {"DEALLOCATE", TOK_KEYWORD}, {"EXEC", TOK_KEYWORD}, {"EXECUTE", TOK_KEYWORD}, {"CALL", TOK_KEYWORD}, {"PROCEDURE", TOK_KEYWORD}, {"FUNCTION", TOK_KEYWORD}, {"TRIGGER", TOK_KEYWORD}, {"CASE", TOK_KEYWORD}, {"WHEN", TOK_KEYWORD}, {"THEN", TOK_KEYWORD}, {"ELSE", TOK_KEYWORD}, {"WHILE", TOK_KEYWORD}, {"LOOP", TOK_KEYWORD}, {"FOR", TOK_KEYWORD}, {"EACH", TOK_KEYWORD}, {"ROW", TOK_KEYWORD}, {"AFTER", TOK_KEYWORD}, {"BEFORE", TOK_KEYWORD}, {"INSTEAD", TOK_KEYWORD}, {"OF", TOK_KEYWORD}, // Logical operators (keywords) {"AND", TOK_KEYWORD}, {"OR", TOK_KEYWORD}, {"NOT", TOK_KEYWORD}, {"IN", TOK_KEYWORD}, {"BETWEEN", TOK_KEYWORD}, {"LIKE", TOK_KEYWORD}, {"ILIKE", TOK_KEYWORD}, {"IS", TOK_KEYWORD}, {"ANY", TOK_KEYWORD}, {"SOME", TOK_KEYWORD}, {"EXCEPT", TOK_KEYWORD}, {"INTERSECT", TOK_KEYWORD}, {"WITH", TOK_KEYWORD}, {"RECURSIVE", TOK_KEYWORD}, {"OVER", TOK_KEYWORD}, {"PARTITION", TOK_KEYWORD}, {"WINDOW", TOK_KEYWORD}, {"LATERAL", TOK_KEYWORD}, // Misc {"GRANT", TOK_KEYWORD}, {"REVOKE", TOK_KEYWORD}, {"EXPLAIN", TOK_KEYWORD}, {"ANALYZE", TOK_KEYWORD}, {"VACUUM", TOK_KEYWORD}, {"PRAGMA", TOK_KEYWORD}, {"DESCRIBE", TOK_KEYWORD}, {"SHOW", TOK_KEYWORD}, {"USE", TOK_KEYWORD}, {"COPY", TOK_KEYWORD}, {"PERFORM", TOK_KEYWORD}, {"RAISE", TOK_KEYWORD}, // Modifiers {"ASC", TOK_MODIFIER}, {"DESC", TOK_MODIFIER}, {"NULLS", TOK_MODIFIER}, {"NOT", TOK_MODIFIER}, // Data types {"INT", TOK_TYPE}, {"INTEGER", TOK_TYPE}, {"SMALLINT", TOK_TYPE}, {"BIGINT", TOK_TYPE}, {"TINYINT", TOK_TYPE}, {"MEDIUMINT", TOK_TYPE}, {"SERIAL", TOK_TYPE}, {"BIGSERIAL", TOK_TYPE}, {"FLOAT", TOK_TYPE}, {"REAL", TOK_TYPE}, {"DOUBLE", TOK_TYPE}, {"DECIMAL", TOK_TYPE}, {"NUMERIC", TOK_TYPE}, {"PRECISION", TOK_TYPE}, {"CHAR", TOK_TYPE}, {"VARCHAR", TOK_TYPE}, {"TEXT", TOK_TYPE}, {"NCHAR", TOK_TYPE}, {"NVARCHAR", TOK_TYPE}, {"NTEXT", TOK_TYPE}, {"BLOB", TOK_TYPE}, {"CLOB", TOK_TYPE}, {"BYTEA", TOK_TYPE}, {"BOOLEAN", TOK_TYPE}, {"BOOL", TOK_TYPE}, {"DATE", TOK_TYPE}, {"TIME", TOK_TYPE}, {"DATETIME", TOK_TYPE}, {"TIMESTAMP", TOK_TYPE}, {"TIMESTAMPTZ", TOK_TYPE}, {"INTERVAL", TOK_TYPE}, {"UUID", TOK_TYPE}, {"JSON", TOK_TYPE}, {"JSONB", TOK_TYPE}, {"XML", TOK_TYPE}, {"ARRAY", TOK_TYPE}, {"ENUM", TOK_TYPE}, {"MONEY", TOK_TYPE}, {"BIT", TOK_TYPE}, {"VARBIT", TOK_TYPE}, {"INET", TOK_TYPE}, {"CIDR", TOK_TYPE}, {"MACADDR", TOK_TYPE}, {"POINT", TOK_TYPE}, {"LINE", TOK_TYPE}, {"POLYGON", TOK_TYPE}, {"GEOMETRY", TOK_TYPE}, {"GEOGRAPHY", TOK_TYPE}, // Values {"NULL", TOK_VALUE}, {"TRUE", TOK_VALUE}, {"FALSE", TOK_VALUE}, {"CURRENT_DATE", TOK_VALUE}, {"CURRENT_TIME", TOK_VALUE}, {"CURRENT_TIMESTAMP", TOK_VALUE}, {"CURRENT_USER", TOK_VALUE}, // Built-in aggregate / window / common functions (TOK_MODIFIER) {"COUNT", TOK_MODIFIER}, {"SUM", TOK_MODIFIER}, {"AVG", TOK_MODIFIER}, {"MIN", TOK_MODIFIER}, {"MAX", TOK_MODIFIER}, {"COALESCE", TOK_MODIFIER}, {"NULLIF", TOK_MODIFIER}, {"CAST", TOK_MODIFIER}, {"CONVERT", TOK_MODIFIER}, {"IFNULL", TOK_MODIFIER}, {"ISNULL", TOK_MODIFIER}, {"NVL", TOK_MODIFIER}, {"ROW_NUMBER", TOK_MODIFIER}, {"RANK", TOK_MODIFIER}, {"DENSE_RANK", TOK_MODIFIER}, {"NTILE", TOK_MODIFIER}, {"LAG", TOK_MODIFIER}, {"LEAD", TOK_MODIFIER}, {"FIRST_VALUE", TOK_MODIFIER}, {"LAST_VALUE", TOK_MODIFIER}, {"SUBSTR", TOK_MODIFIER}, {"SUBSTRING", TOK_MODIFIER}, {"TRIM", TOK_MODIFIER}, {"UPPER", TOK_MODIFIER}, {"LOWER", TOK_MODIFIER}, {"LENGTH", TOK_MODIFIER}, {"CONCAT", TOK_MODIFIER}, {"REPLACE", TOK_MODIFIER}, {"ABS", TOK_MODIFIER}, {"ROUND", TOK_MODIFIER}, {"CEIL", TOK_MODIFIER}, {"FLOOR", TOK_MODIFIER}, {"NOW", TOK_MODIFIER}, {"EXTRACT", TOK_MODIFIER}, {"STRING_AGG", TOK_MODIFIER}, {"GROUP_CONCAT", TOK_MODIFIER}, {"ARRAY_AGG", TOK_MODIFIER}, {"GREATEST", TOK_MODIFIER}, {"LEAST", TOK_MODIFIER}, }; #define SQL_KEYWORD_COUNT (S32)(sizeof(sql_keywords) / sizeof(sql_keywords[0])) // Case-insensitive keyword lookup static Token_Type sql_lookup_keyword(const char *word, S32 len) { for (S32 i = 0; i < SQL_KEYWORD_COUNT; i++) { const char *kw = sql_keywords[i].word; S32 kwlen = (S32)strlen(kw); if (kwlen != len) continue; B32 match = 1; for (S32 j = 0; j < len; j++) { if (toupper((unsigned char)word[j]) != (unsigned char)kw[j]) { match = 0; break; } } if (match) return sql_keywords[i].type; } return TOK_IDENTIFIER; } //////////////////////////////// // Character helpers static B32 sql_is_ident_start(char c) { return isalpha((unsigned char)c) || c == '_'; } static B32 sql_is_ident_char(char c) { return isalnum((unsigned char)c) || c == '_'; } //////////////////////////////// // Individual token parsers static Token sql_parse_identifier(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); const char *begin = tok->t; while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) tok->t++; S32 len = (S32)(tok->t - begin); token.type = sql_lookup_keyword(begin, len); token.len = (S32)(tok->t - tok->start_t); return token; } static Token sql_parse_number(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_NUMBER; while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; // Decimal part if (tok->t < tok->max_t && *tok->t == '.') { tok->t++; while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; } // Exponent if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { tok->t++; if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; } token.len = (S32)(tok->t - tok->start_t); return token; } static Token sql_parse_dot_number(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_NUMBER; tok->t++; // skip '.' while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; if (tok->t < tok->max_t && (*tok->t == 'e' || *tok->t == 'E')) { tok->t++; if (tok->t < tok->max_t && (*tok->t == '+' || *tok->t == '-')) tok->t++; while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; } token.len = (S32)(tok->t - tok->start_t); return token; } // Single-quoted string: 'text', with '' as escape for embedded quote static Token sql_parse_string(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_STRING_LITERAL; tok->t++; // skip opening ' while (tok->t < tok->max_t) { if (*tok->t == '\'') { tok->t++; // '' is an escaped quote inside a string if (tok->t < tok->max_t && *tok->t == '\'') { tok->t++; continue; } break; } tok->t++; } token.len = (S32)(tok->t - tok->start_t); return token; } // Double-quoted identifier: "column_name" static Token sql_parse_quoted_identifier(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_IDENTIFIER; tok->t++; // skip opening " while (tok->t < tok->max_t) { if (*tok->t == '"') { tok->t++; // "" is an escaped quote inside a quoted identifier if (tok->t < tok->max_t && *tok->t == '"') { tok->t++; continue; } break; } tok->t++; } token.len = (S32)(tok->t - tok->start_t); return token; } // Backtick-quoted identifier (MySQL): `column_name` static Token sql_parse_backtick_identifier(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_IDENTIFIER; tok->t++; // skip opening ` while (tok->t < tok->max_t) { if (*tok->t == '`') { tok->t++; break; } tok->t++; } token.len = (S32)(tok->t - tok->start_t); return token; } // Line comment: -- ... static Token sql_parse_line_comment(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_COMMENT; tok->t += 2; // skip '--' while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; token.len = (S32)(tok->t - tok->start_t); return token; } // Block comment: /* ... */ static Token sql_parse_block_comment(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_MULTILINE_COMMENT; tok->t += 2; // skip '/*' while (tok->t < tok->max_t) { if (*tok->t == '*' && (tok->t + 1) < tok->max_t && *(tok->t + 1) == '/') { tok->t += 2; break; } tok->t++; } token.len = (S32)(tok->t - tok->start_t); return token; } // MySQL # comment static Token sql_parse_hash_comment(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_COMMENT; tok->t++; // skip '#' while (tok->t < tok->max_t && *tok->t != '\n') tok->t++; token.len = (S32)(tok->t - tok->start_t); return token; } // Variable / parameter: @var, @@var, :param, $1 static Token sql_parse_variable(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_DIRECTIVE; char c = *tok->t; tok->t++; if (c == '@') { // @@ for system variables if (tok->t < tok->max_t && *tok->t == '@') tok->t++; while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) tok->t++; } else if (c == ':') { while (tok->t < tok->max_t && sql_is_ident_char(*tok->t)) tok->t++; } else if (c == '$') { // $1, $2, ... (PostgreSQL positional params) while (tok->t < tok->max_t && isdigit((unsigned char)*tok->t)) tok->t++; // Also handle $tag$ dollar-quoted strings if no digits follow if (tok->t == tok->start_t + 1) { // Just a lone $, treat as punctuation token.type = TOK_PUNCTUATION; } } token.len = (S32)(tok->t - tok->start_t); return token; } static Token sql_parse_operator(Tokenizer *tok) { Token token; token.start = (S32)(tok->start_t - tok->buf); token.type = TOK_OPERATION; char c = *tok->t; tok->t++; if (tok->t < tok->max_t) { char n = *tok->t; // Two-character operators if ((c == '<' && n == '=') || (c == '>' && n == '=') || (c == '<' && n == '>') || (c == '!' && n == '=') || (c == '|' && n == '|') || (c == ':' && n == ':')) { tok->t++; } } token.len = (S32)(tok->t - tok->start_t); return token; } //////////////////////////////// // Main get_next_token static Token sql_get_next_token(Tokenizer *tok) { tokenizer_eat_whitespace(tok); Token token = {0}; token.start = (S32)(tok->t - tok->buf); token.type = TOK_DEFAULT; if (tok->t >= tok->max_t) { token.len = 0; return token; } tok->start_t = tok->t; char c = *tok->t; if (sql_is_ident_start(c)) { return sql_parse_identifier(tok); } if (isdigit((unsigned char)c)) { return sql_parse_number(tok); } switch (c) { case '\'': return sql_parse_string(tok); case '"': return sql_parse_quoted_identifier(tok); case '`': return sql_parse_backtick_identifier(tok); case '-': if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '-') { return sql_parse_line_comment(tok); } token.type = TOK_OPERATION; tok->t++; token.len = 1; return token; case '/': if ((tok->t + 1) < tok->max_t && *(tok->t + 1) == '*') { return sql_parse_block_comment(tok); } token.type = TOK_OPERATION; tok->t++; token.len = 1; return token; case '#': return sql_parse_hash_comment(tok); case '.': if ((tok->t + 1) < tok->max_t && isdigit((unsigned char)*(tok->t + 1))) { return sql_parse_dot_number(tok); } token.type = TOK_PUNCTUATION; tok->t++; token.len = 1; return token; case '@': case '$': return sql_parse_variable(tok); case ':': // :param or :: cast operator if ((tok->t + 1) < tok->max_t) { char n = *(tok->t + 1); if (n == ':') return sql_parse_operator(tok); if (sql_is_ident_start(n)) return sql_parse_variable(tok); } token.type = TOK_PUNCTUATION; tok->t++; token.len = 1; return token; // Punctuation case ';': case ',': case '{': case '}': case '(': case ')': case '[': case ']': token.type = TOK_PUNCTUATION; tok->t++; token.len = 1; return token; // Operators case '=': case '<': case '>': case '!': case '+': case '*': case '%': case '&': case '|': case '^': case '~': return sql_parse_operator(tok); default: token.type = TOK_INVALID; tok->t++; token.len = 1; return token; } } //////////////////////////////// // tokenize_sql -- main entry point static void tokenize_sql(const char *data, S32 length, U8 *out_tokens) { memset(out_tokens, TOK_DEFAULT, length); Tokenizer tok; tokenizer_init(&tok, data, length); Token prev = {0}; prev.type = TOK_DEFAULT; while (tok.t < tok.max_t) { Token token = sql_get_next_token(&tok); if (token.len == 0) break; paint_token(out_tokens, token.start, token.len, token.type); // Retroactively mark identifier before '(' as a function if (token.type == TOK_PUNCTUATION && token.len == 1 && data[token.start] == '(' && prev.type == TOK_IDENTIFIER) { paint_token(out_tokens, prev.start, prev.len, TOK_FUNCTION); } prev = token; } }