|
| 1 | +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; |
| 2 | + |
| 3 | +namespace SqlServerSimulator.Parser; |
| 4 | + |
| 5 | +/// <summary> |
| 6 | +/// Pins the <see cref="Keyword"/> enum against the authoritative T-SQL |
| 7 | +/// reserved keywords list at |
| 8 | +/// <c>https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql</c>. |
| 9 | +/// Any change to <see cref="Keyword"/> must be verified against that page — |
| 10 | +/// adding a contextual keyword (e.g. <c>THROW</c>, <c>TRY</c>, <c>CATCH</c>) |
| 11 | +/// to this enum is a bug because the tokenizer relies on the enum to decide |
| 12 | +/// what surfaces as <c>ReservedKeyword</c> vs. <c>UnquotedString</c>, and a |
| 13 | +/// non-reserved word being treated as reserved breaks valid SQL (e.g. |
| 14 | +/// blocking <c>select 1 as throw</c>). |
| 15 | +/// </summary> |
| 16 | +/// <remarks> |
| 17 | +/// The test cross-checks both directions: every entry in <see cref="Keyword"/> |
| 18 | +/// must appear in the canonical list, and every canonical entry must appear |
| 19 | +/// in <see cref="Keyword"/>. The single exception is <c>WITHIN GROUP</c>, |
| 20 | +/// which appears in the canonical list as a two-word entry but is omitted |
| 21 | +/// from the enum — <see cref="Keyword"/>'s trailing comment notes that |
| 22 | +/// <c>WITHIN</c> alone isn't enforced as a reserved word in real SQL Server |
| 23 | +/// and <c>GROUP</c> is already covered, so a multi-word enum entry would be |
| 24 | +/// functionally irrelevant. The omission is hardcoded into |
| 25 | +/// <see cref="DocumentedOmissions"/> rather than parameterized — the |
| 26 | +/// "reserved keyword list is frozen" rule has no anticipated exceptions |
| 27 | +/// beyond this one structural quirk. |
| 28 | +/// </remarks> |
| 29 | +[TestClass] |
| 30 | +public sealed class ReservedKeywordsTests |
| 31 | +{ |
| 32 | + /// <summary> |
| 33 | + /// The canonical reserved-keyword set, transcribed verbatim from |
| 34 | + /// <c>https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql</c>. |
| 35 | + /// Updates here must be verified against that page; do not edit to |
| 36 | + /// match the enum without re-checking the source. |
| 37 | + /// </summary> |
| 38 | + private static readonly HashSet<string> CanonicalReservedKeywords = |
| 39 | + [ |
| 40 | + "ADD", "ALL", "ALTER", "AND", "ANY", "AS", "ASC", "AUTHORIZATION", |
| 41 | + "BACKUP", "BEGIN", "BETWEEN", "BREAK", "BROWSE", "BULK", "BY", |
| 42 | + "CASCADE", "CASE", "CHECK", "CHECKPOINT", "CLOSE", "CLUSTERED", "COALESCE", |
| 43 | + "COLLATE", "COLUMN", "COMMIT", "COMPUTE", "CONSTRAINT", "CONTAINS", |
| 44 | + "CONTAINSTABLE", "CONTINUE", "CONVERT", "CREATE", "CROSS", "CURRENT", |
| 45 | + "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "CURSOR", |
| 46 | + "DATABASE", "DBCC", "DEALLOCATE", "DECLARE", "DEFAULT", "DELETE", "DENY", |
| 47 | + "DESC", "DISK", "DISTINCT", "DISTRIBUTED", "DOUBLE", "DROP", "DUMP", |
| 48 | + "ELSE", "END", "ERRLVL", "ESCAPE", "EXCEPT", "EXEC", "EXECUTE", "EXISTS", |
| 49 | + "EXIT", "EXTERNAL", |
| 50 | + "FETCH", "FILE", "FILLFACTOR", "FOR", "FOREIGN", "FREETEXT", "FREETEXTTABLE", |
| 51 | + "FROM", "FULL", "FUNCTION", |
| 52 | + "GOTO", "GRANT", "GROUP", |
| 53 | + "HAVING", "HOLDLOCK", |
| 54 | + "IDENTITY", "IDENTITY_INSERT", "IDENTITYCOL", "IF", "IN", "INDEX", "INNER", |
| 55 | + "INSERT", "INTERSECT", "INTO", "IS", |
| 56 | + "JOIN", |
| 57 | + "KEY", "KILL", |
| 58 | + "LEFT", "LIKE", "LINENO", "LOAD", |
| 59 | + "MERGE", |
| 60 | + "NATIONAL", "NOCHECK", "NONCLUSTERED", "NOT", "NULL", "NULLIF", |
| 61 | + "OF", "OFF", "OFFSETS", "ON", "OPEN", "OPENDATASOURCE", "OPENQUERY", |
| 62 | + "OPENROWSET", "OPENXML", "OPTION", "OR", "ORDER", "OUTER", "OVER", |
| 63 | + "PERCENT", "PIVOT", "PLAN", "PRECISION", "PRIMARY", "PRINT", "PROC", |
| 64 | + "PROCEDURE", "PUBLIC", |
| 65 | + "RAISERROR", "READ", "READTEXT", "RECONFIGURE", "REFERENCES", "REPLICATION", |
| 66 | + "RESTORE", "RESTRICT", "RETURN", "REVERT", "REVOKE", "RIGHT", "ROLLBACK", |
| 67 | + "ROWCOUNT", "ROWGUIDCOL", "RULE", |
| 68 | + "SAVE", "SCHEMA", "SECURITYAUDIT", "SELECT", "SEMANTICKEYPHRASETABLE", |
| 69 | + "SEMANTICSIMILARITYDETAILSTABLE", "SEMANTICSIMILARITYTABLE", "SESSION_USER", |
| 70 | + "SET", "SETUSER", "SHUTDOWN", "SOME", "STATISTICS", "SYSTEM_USER", |
| 71 | + "TABLE", "TABLESAMPLE", "TEXTSIZE", "THEN", "TO", "TOP", "TRAN", |
| 72 | + "TRANSACTION", "TRIGGER", "TRUNCATE", "TRY_CONVERT", "TSEQUAL", |
| 73 | + "UNION", "UNIQUE", "UNPIVOT", "UPDATE", "UPDATETEXT", "USE", "USER", |
| 74 | + "VALUES", "VARYING", "VIEW", |
| 75 | + "WAITFOR", "WHEN", "WHERE", "WHILE", "WITH", "WITHIN GROUP", "WRITETEXT", |
| 76 | + ]; |
| 77 | + |
| 78 | + /// <summary> |
| 79 | + /// The only canonical entry the enum intentionally omits — a multi-word |
| 80 | + /// reserved keyword whose component words don't independently behave as |
| 81 | + /// reserved in real SQL Server (see class doc). |
| 82 | + /// </summary> |
| 83 | + private static readonly HashSet<string> DocumentedOmissions = ["WITHIN GROUP"]; |
| 84 | + |
| 85 | + [TestMethod] |
| 86 | + public void Keyword_Enum_MatchesCanonicalReservedList() |
| 87 | + { |
| 88 | + var enumNames = Enum.GetNames<Keyword>() |
| 89 | + .Where(n => n != "_") |
| 90 | + .Select(n => n.ToUpperInvariant()) |
| 91 | + .ToHashSet(); |
| 92 | + |
| 93 | + var unexpectedExtras = enumNames.Except(CanonicalReservedKeywords).Order().ToList(); |
| 94 | + var unexpectedOmissions = CanonicalReservedKeywords.Except(enumNames).Except(DocumentedOmissions).Order().ToList(); |
| 95 | + |
| 96 | + IsEmpty( |
| 97 | + unexpectedExtras, |
| 98 | + $"Keyword enum has entries not in the canonical reserved list at {SourceUrl}: {string.Join(", ", unexpectedExtras)}. The reserved-keyword list is frozen — remove the entry from the enum (and route any contextual-keyword usage through UnquotedString, matching the TRY / CATCH / THROW pattern)."); |
| 99 | + |
| 100 | + IsEmpty( |
| 101 | + unexpectedOmissions, |
| 102 | + $"Keyword enum is missing canonical reserved keywords from {SourceUrl}: {string.Join(", ", unexpectedOmissions)}. Add them to the enum."); |
| 103 | + } |
| 104 | + |
| 105 | + private const string SourceUrl = "https://learn.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql"; |
| 106 | +} |
0 commit comments