-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(krl): DB-6 Phase A — explain keyword + structured query plan (#34) #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
6ea708c
fix: post-Bareiss follow-up — conway marker removed, polynomial bound…
hyperpolymath dac9b3e
docs(test): record the measured cause of the BR-5 quandle_key residual
hyperpolymath 68fe9e1
fix(ci): symlink siblings one level up — #83 moved [sources] and brok…
hyperpolymath a7b553a
feat(krl): DB-6 Phase A — `explain` keyword and structured query plan
hyperpolymath 29642b9
Merge branch 'main' into feat/db6-krl-explain
hyperpolymath File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| # SPDX-License-Identifier: MPL-2.0 | ||
| # Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> | ||
| # | ||
| # DB-6 Phase A — structured query plan for `explain from … | …`. | ||
| # | ||
| # Produces the shape the DB-6 acceptance criteria specify: an ordered list of | ||
| # operations, each tagged with its kind and, for filters, the column, the | ||
| # comparator, the literal value, whether the column is indexed, and a | ||
| # selectivity estimate. | ||
| # | ||
| # HONESTY NOTE ON SELECTIVITY | ||
| # --------------------------- | ||
| # There are no column histograms yet — that is DB-3 (#33). The criteria | ||
| # explicitly permit a stub until DB-3 lands, so every estimate here is | ||
| # `SELECTIVITY_STUB` and is reported with `"selectivity_source" => "stub"`. | ||
| # Nothing downstream may treat these as measured: `plan_is_costed` returns | ||
| # false while any estimate is a stub, so a future DB-7 cost-based reorder | ||
| # cannot silently optimise against made-up numbers. | ||
|
|
||
| # Included directly into the `KRL` module namespace (as Ast.jl / Parser.jl / | ||
| # Evaluator.jl are) — not a submodule, so AST types resolve without imports. | ||
|
|
||
| """Placeholder selectivity used until DB-3 (#33) provides real histograms.""" | ||
| const SELECTIVITY_STUB = 0.5 | ||
|
|
||
| """ | ||
| Columns carrying a secondary index today. Kept deliberately small and | ||
| explicit: an over-claiming list would make `indexed` a fake signal, which is | ||
| worse than reporting `false`. Extend as DB-3 lands real indexes. | ||
| """ | ||
| const INDEXED_COLUMNS = Set([ | ||
| "colouring_count_3", | ||
| "colouring_count_5", | ||
| "presentation_hash", | ||
| "quandle_key", | ||
| ]) | ||
|
|
||
| _cmp_symbol(k::Symbol) = get(Dict( | ||
| :eq => "=", :neq => "!=", :lt => "<", :lte => "<=", | ||
| :gt => ">", :gte => ">=", :iso => "≅", :path => "~>", :in => "in", | ||
| ), k, string(k)) | ||
|
|
||
| _source_name(s::KRLSource) = | ||
| s isa KRLSourceKnots ? "knots" : | ||
| s isa KRLSourceDiagrams ? "diagrams" : | ||
| s isa KRLSourceInvariants ? "invariants" : | ||
| s isa KRLSourceNamed ? s.name : | ||
| s isa KRLSourceSubquery ? "<subquery>" : "<unknown>" | ||
|
|
||
| # Pull (column, comparator, value) out of a comparison predicate when the | ||
| # shape is the simple `column <cmp> literal` the plan can describe. Anything | ||
| # else (nested boolean, function call, column-to-column) yields `nothing` and | ||
| # is reported as an opaque predicate rather than being mis-described. | ||
| _literal_value(e::KRLExpr) = | ||
| e isa KRLInt ? e.n : | ||
| e isa KRLFloat ? e.x : | ||
| e isa KRLString ? e.s : | ||
| e isa KRLBool ? e.b : | ||
| e isa KRLKnotName ? e.name : nothing | ||
|
|
||
| function _simple_comparison(e::KRLExpr) | ||
| e isa KRLCompare || return nothing | ||
| e.left isa KRLVar || return nothing | ||
| val = _literal_value(e.right) | ||
| val === nothing && return nothing | ||
| (e.left.name, _cmp_symbol(e.op), val) | ||
| end | ||
|
|
||
| # Split a conjunction into its conjuncts so `filter a = 1 and b < 2` plans as | ||
| # two filter operations, which is what makes a future reorder meaningful. | ||
| function _conjuncts(e::KRLExpr) | ||
| e isa KRLAnd && return vcat(_conjuncts(e.left), _conjuncts(e.right)) | ||
| [e] | ||
| end | ||
|
|
||
| function _filter_ops(pred::KRLExpr) | ||
| ops = Dict{String, Any}[] | ||
| for c in _conjuncts(pred) | ||
| simple = _simple_comparison(c) | ||
| if simple === nothing | ||
| push!(ops, Dict{String, Any}( | ||
| "op" => "filter", | ||
| "predicate" => "<expression>", | ||
| "indexed" => false, | ||
| "selectivity_estimate" => SELECTIVITY_STUB, | ||
| "selectivity_source" => "stub", | ||
| )) | ||
| else | ||
| col, cmp, val = simple | ||
| push!(ops, Dict{String, Any}( | ||
| "op" => "filter", | ||
| "column" => col, | ||
| "comparator" => cmp, | ||
| "value" => val, | ||
| "indexed" => col in INDEXED_COLUMNS, | ||
| "selectivity_estimate" => SELECTIVITY_STUB, | ||
| "selectivity_source" => "stub", | ||
| )) | ||
| end | ||
| end | ||
| ops | ||
| end | ||
|
|
||
| """ | ||
| explain_plan(q::KRLQuery) -> Vector{Dict{String, Any}} | ||
|
|
||
| Ordered plan for `q`: a `scan` of the source followed by one entry per | ||
| pipeline stage, with conjoined filters split into separate `filter` | ||
| operations. | ||
|
|
||
| The order returned is **execution order as written** — no reordering is | ||
| performed. Cost-based reordering is DB-7 and requires real selectivity | ||
| (DB-3), which is why every estimate here is flagged `"stub"`. | ||
| """ | ||
| function explain_plan(q::KRLQuery)::Vector{Dict{String, Any}} | ||
| plan = Dict{String, Any}[Dict{String, Any}( | ||
| "op" => "scan", | ||
| "table" => _source_name(q.source), | ||
| )] | ||
| for stage in q.stages | ||
| if stage isa KRLFilterStage | ||
| append!(plan, _filter_ops(stage.pred)) | ||
| elseif stage isa KRLSortStage | ||
| push!(plan, Dict{String, Any}( | ||
| "op" => "sort", | ||
| "keys" => length(stage.items), | ||
| )) | ||
| elseif stage isa KRLTakeStage | ||
| push!(plan, Dict{String, Any}("op" => "take", "n" => stage.n)) | ||
| elseif stage isa KRLSkipStage | ||
| push!(plan, Dict{String, Any}("op" => "skip", "n" => stage.n)) | ||
| else | ||
| push!(plan, Dict{String, Any}("op" => _stage_op_name(stage))) | ||
| end | ||
| end | ||
| plan | ||
| end | ||
|
|
||
| _stage_op_name(s::KRLPipeStage) = | ||
| s isa KRLReturnStage ? "return" : | ||
| s isa KRLGroupByStage ? "group_by" : | ||
| lowercase(replace(string(nameof(typeof(s))), "KRL" => "", "Stage" => "")) | ||
|
|
||
| """ | ||
| plan_is_costed(plan) -> Bool | ||
|
|
||
| `true` only when every selectivity estimate in `plan` came from real | ||
| statistics. While DB-3 (#33) is outstanding this is always `false` — the | ||
| guard that stops DB-7 optimising against stub numbers. | ||
| """ | ||
| plan_is_costed(plan::Vector{Dict{String, Any}}) = | ||
| !any(get(op, "selectivity_source", "stub") == "stub" for op in plan) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # SPDX-License-Identifier: MPL-2.0 | ||
| # Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> | ||
| # | ||
| # DB-6 Phase A — `explain from … | …` produces a structured plan. | ||
|
|
||
| using Test | ||
| include(joinpath(@__DIR__, "..", "KRL.jl")) | ||
| using .KRL: parse_krl, explain_plan, plan_is_costed, SELECTIVITY_STUB, | ||
| KRLExplainStmt, KRLQueryStmt, KRLParseError | ||
|
|
||
| @testset "DB-6: explain is a distinct statement, not a query" begin | ||
| prog = parse_krl("explain from knots | filter crossing < 8") | ||
| @test length(prog.statements) == 1 | ||
| @test prog.statements[1] isa KRLExplainStmt | ||
| # A plain query must NOT become an explain — the two are distinguishable | ||
| # so no existing consumer can be handed a plan where it expected rows. | ||
| @test parse_krl("from knots | filter crossing < 8").statements[1] isa KRLQueryStmt | ||
| end | ||
|
|
||
| @testset "DB-6: explain requires a query after it" begin | ||
| @test_throws KRLParseError parse_krl("explain") | ||
| @test_throws KRLParseError parse_krl("explain take 5") | ||
| end | ||
|
|
||
| @testset "DB-6: plan shape matches the acceptance criteria" begin | ||
| prog = parse_krl( | ||
| "explain from knots | filter colouring_count_3 = 9 and crossing < 8 | take 5") | ||
| plan = explain_plan(prog.statements[1].query) | ||
|
|
||
| @test plan[1]["op"] == "scan" | ||
| @test plan[1]["table"] == "knots" | ||
|
|
||
| # A conjunction becomes SEPARATE filter operations — that is what makes a | ||
| # future cost-based reorder (DB-7) meaningful. | ||
| @test plan[2]["op"] == "filter" | ||
| @test plan[2]["column"] == "colouring_count_3" | ||
| @test plan[2]["comparator"] == "=" | ||
| @test plan[2]["value"] == 9 | ||
| @test plan[2]["indexed"] == true # carries a secondary index | ||
|
|
||
| @test plan[3]["column"] == "crossing" | ||
| @test plan[3]["comparator"] == "<" | ||
| @test plan[3]["value"] == 8 | ||
| @test plan[3]["indexed"] == false # does not | ||
|
|
||
| @test plan[4]["op"] == "take" | ||
| @test plan[4]["n"] == 5 | ||
| end | ||
|
|
||
| @testset "DB-6: selectivity is honestly labelled a stub until DB-3" begin | ||
| plan = explain_plan( | ||
| parse_krl("explain from knots | filter crossing < 8").statements[1].query) | ||
| filt = plan[2] | ||
| @test filt["selectivity_estimate"] == SELECTIVITY_STUB | ||
| @test filt["selectivity_source"] == "stub" | ||
| # The guard that stops DB-7 optimising against invented numbers. This must | ||
| # stay false until real histograms land (#33) — if it ever returns true | ||
| # while estimates are stubs, the cost model is lying. | ||
| @test plan_is_costed(plan) == false | ||
| end | ||
|
|
||
| @testset "DB-6: a predicate the plan cannot describe is not mis-described" begin | ||
| # Column-to-column comparison is not a simple `column <cmp> literal`, so | ||
| # it must be reported opaquely rather than given a fabricated column. | ||
| plan = explain_plan( | ||
| parse_krl("explain from knots | filter crossing < genus").statements[1].query) | ||
| @test plan[2]["op"] == "filter" | ||
| @test plan[2]["predicate"] == "<expression>" | ||
| @test !haskey(plan[2], "column") | ||
| @test plan[2]["indexed"] == false | ||
| end | ||
|
|
||
| println("krl-explain-tests-ok") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.