Kusto Query Language (KQL) Implementation

repository·master·Indexed 20 days ago

https://github.com/microsoft/kusto-query-language

Core implementation of the Kusto Query Language (KQL), featuring a C# parser, semantic analyzer, and translator for JavaScript. It provides tools to convert KQL queries into syntax trees via KustoCode.Parse and KustoCode.ParseAndAnalyze, allowing for the identification of referenced database tables, columns, and symbols. The library is available via the Microsoft.Azure.Kusto.Language NuGet package and powers query analysis and authoring experiences for Kusto-based data services.

Tokens
3.7K
Snippets
9
Records
12
Agent score
22%

What's inside microsoft-kusto-query-language

  1. Overview of Kusto Query Language (KQL)

    master

    Kusto Query Language (KQL) is a language designed for querying structured, semi-structured, and unstructured data using a relational model of tables and columns. It is optimized for telemetry, metrics, and logs, providing specialized support for:

    • Text search and parsing
    • Time-series operators and functions
    • Analytics and aggregation
    • Geospatial data
    • Vector similarity searches

    For comprehensive language documentation, refer to the Kusto Query Language (KQL) overview.

  2. Add built-in functions and aggregates to GlobalState

    master

    You can extend the parser's knowledge of functions and aggregates (even if they don't exist on the server) by modifying the GlobalState before parsing. This is useful for simulating specific environments or adding custom logic.

    • Use WithFunctions to add FunctionSymbol instances.
    • Use WithAggregates to add aggregate functions.

    Note that removing functions or aggregates from the GlobalState will cause the parser to produce errors when they are encountered in a query.

    // Adding a fake function
    var fnFake = new FunctionSymbol("fake", ScalarTypes.Real, new Parameter("x", ScalarTypes.Long), new Parameter("y", ScalarTypes.Long));
    var globals = GlobalState.Default.WithFunctions(globals.Functions.Concat(new [] {fnFake}).ToArray());
    var code = KustoCode.ParseAndAnalyze("print fake(10)", globals);
    
    // Adding a fake aggregate
    var fnMinMax = new FunctionSymbol("minmax", ScalarTypes.Real, new Parameter("x", ScalarTypes.Real));
    var globals = GlobalState.Default.WithAggregates(globals.Aggregates.Concat(new [] {fnMinMax}).ToArray());
    var code = KustoCode.ParseAndAnalyze("T | summarize minmax(c)", globals);
  3. Find all database tables referenced in a query

    master

    To identify which tables are used in a Kusto query, you can walk the syntax tree of a KustoCode object. A basic approach checks for TableSymbol references or expressions where the ResultType is a TableSymbol.

    Note: A basic walk may miss tables referenced inside the bodies of called functions. To capture these, you must use a recursive technique that calls GetCalledFunctionBody() on nodes to examine the function's internal syntax.

    For a production-ready implementation, it is recommended to use the Kusto.Toolkit NuGet package.

        public static HashSet<TableSymbol> GetDatabaseTables(KustoCode code)
        {
            var tables = new HashSet<TableSymbol>();
            GatherTables(code.Syntax);
            return tables;
    
            void GatherTables(SyntaxNode root)
            {
                SyntaxElement.WalkNodes(root,
                    fnBefore: n =>
                    {
                        if (n.ReferencedSymbol is TableSymbol t
                            && code.Globals.IsDatabaseTable(t))
                        {
                            tables.Add(t);
                        }
                        else if (n is Expression e
                            && e.ResultType is TableSymbol ts
                            && code.Globals.IsDatabaseTable(ts))
                        {
                            tables.Add(ts);
                        }
                        else if (n.GetCalledFunctionBody() is SyntaxNode body)
                        {
                            GatherTables(body);
                        }
                    },
                    fnDescend: n =>
                        !(n is FunctionDeclaration)
                    );
            }
        }
  4. Declare database schemas manually

    master

    To enable semantic analysis for specific tables and functions, you must declare them in the GlobalState before calling ParseAndAnalyze.

    1. Define Symbols

    • Tables: Create a TableSymbol with a name and a schema string.
    • Functions: Create a FunctionSymbol with a name and a body. Functions can have parameters or be parameterless.
    • Databases: Create a DatabaseSymbol by passing the name and the collection of tables and functions.

    2. Register in GlobalState

    Since GlobalState is immutable, use the With methods to add your definitions. You can add a database to the default scope or add a ClusterSymbol containing multiple databases.

    Accessing non-default entities:

    • If a database is not the default, use the database() function in your query.
    • If a cluster is not the default, use the cluster() function in your query.
    // 1. Declare symbols
    var shapes = new TableSymbol("Shapes", "(id: string, width: real, height: real)");
    var tallshapes = new FunctionSymbol("TallShapes", "{ Shapes | width < height; }");
    var shortshapes = new FunctionSymbol("ShortShapes", "(maxHeight: real)", "{ Shapes | height < maxHeight; }");
    
    // 2. Create database
    var mydb = new DatabaseSymbol("mydb", shapes, tallshapes, shortshapes);
    
    // 3. Add to GlobalState
    var globalsWithMyDb = GlobalState.Default.WithDatabase(mydb);
    
    // Alternatively, add via a cluster
    var mycluster = new ClusterSymbol("mycluster.kusto.windows.net", mydb);
    var globalsWithMyClusterAdded = GlobalState.Globals.AddOrReplaceCluster(mycluster);
  5. Find all database table columns referenced in a query

    master

    To identify which columns in a query originate from the provided database schema, you must traverse the syntax tree and check the ReferencedSymbol of nodes.

    Because columns can be renamed or introduced via operators (like union), a ColumnSymbol might not be directly found in the GlobalState. In such cases, you should check the OriginalColumns property of the ColumnSymbol to find the underlying schema columns.

    Additionally, if a query calls a database function, the columns referenced inside that function's body will not appear in the main query's syntax tree. To find these, use n.GetCalledFunctionBody() to retrieve the syntax tree of the function and traverse it recursively.

    public static HashSet<ColumnSymbol> GetDatabaseTableColumns(KustoCode code)
    {
        var columns = new HashSet<ColumnSymbol>();
        GatherColumns(code.Syntax);
        return columns;
    
        void GatherColumns(SyntaxNode root)
        {
            SyntaxElement.WalkNodes(root,
                fnBefore: n =>
                {
                    if (n.ReferencedSymbol is ColumnSymbol c)
                    {
                        AddDatabaseTableColumns(c, code.Globals, columns);
                    }
                    else if (n.GetCalledFunctionBody() is SyntaxNode body)
                    {
                        GatherColumns(body);
                    }
                },
                fnDescend: n =>
                    // skip descending into function declarations since their bodies will be examined by the code above
                    !(n is FunctionDeclaration)
                );
        }
    }
  6. Discover parsing and semantic errors

    master

    You can retrieve syntactic and semantic errors by calling code.GetDiagnostics().

    • If called on a query parsed only with KustoCode.Parse, it returns only syntax errors.
    • If called on a query parsed with KustoCode.ParseAndAnalyze, it returns both syntax and semantic errors.

    Check the Severity property of each diagnostic to distinguish between errors, warnings, and other diagnostic types.

    var diagnostics = code.GetDiagnostics();
    if (diagnostics.Count > 0) { /* handle diagnostics */ }
  7. Parse a query with semantic analysis

    master

    To distinguish between different entities with the same name (like two different columns named a) and to check for semantic errors, use KustoCode.ParseAndAnalyze.

    This method requires a GlobalState instance, which acts as a provider for symbols (definitions of database tables, functions, etc.). When semantic analysis is performed, syntax nodes gain ReferencedSymbol and ResultType properties, allowing you to identify exactly which column, variable, or table is being referenced.

    var globals = GlobalState.Default.WithDatabase(
        new DatabaseSymbol("db",
            new TableSymbol("T", "(a: real, b: real)")));
    
    var query = "T | project a = a + b | where a > 10.0";
    var code = KustoCode.ParseAndAnalyze(query, globals);
    
    // Accessing the specific column symbol via the syntax tree
    var columnA = globals.Database.Tables.First(t => t.Name == "db").GetColumn("a");
    var referencesToA = code.Syntax.GetDescendants<NameReference>(n => n.ReferencedSymbol == columnA);
  8. Parse a Kusto query

    master

    Use KustoCode.Parse to convert a Kusto query or control command string into a KustoCode instance containing a syntax tree. This allows you to navigate the tree using methods like GetDescendants, GetAncestors, GetChild, Parent, WalkNodes, GetTokenAt, or GetNodeAt.

    Note that simple parsing only provides syntactic information. It cannot distinguish between different columns that share the same name (e.g., a column from a table vs. a column declared via a project operator) because it lacks semantic context.

    // parse only
    var query = "T | project a = a + b | where a > 10.0";
    var code = KustoCode.Parse(query);
    
    // search syntax for a name reference of "a"
    var referencesToA = code.Syntax.GetDescendants<NameReference>(n => n.SimpleName == "a");
  9. Discover the hierarchy of symbols (Table, Database, Cluster)

    master

    If you have a KustoCode object with a populated GlobalState, you can traverse the hierarchy from columns up to clusters using the following methods on code.Globals:

    1. Find the table for a column: Use GetTable(column).
    2. Find the database for a table: Use GetDatabase(table).
    3. Find the cluster for a database: Use GetCluster(database).

    These methods require the GlobalState to contain the relevant definitions for the symbols being queried.

    // Find table from column
    var table = code.Globals.GetTable(column);
    
    // Find database from table
    var database = code.Globals.GetDatabase(table);
    
    // Find cluster from database
    var cluster = code.Globals.GetCluster(database);