Skip to content

Commit abbdd9f

Browse files
feat: integrate explain capabilities for main and semantic index (#81)
This PR integrates the EXPLAIN capabilities added to Skein.jl into the QuandleDB server. It replaces hardcoded index creation logic with Skein.create_index! and adds the /api/explain endpoint for both raw SQL and semantic index searches (resolving DB-3 and DB-6 Phase B).
1 parent f0f25af commit abbdd9f

1 file changed

Lines changed: 38 additions & 20 deletions

File tree

server/serve.jl

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ using SHA
2323
include("quandle_semantic.jl")
2424
using .QuandleSemantic
2525

26+
include("query_explain.jl")
27+
2628
include("Diagnostics.jl")
2729
include("krl/KRL.jl")
2830
using .KRL: parse_any, parse_krl, parse_sql, KRLParseError, KRLLexError,
@@ -63,23 +65,6 @@ CREATE TABLE IF NOT EXISTS semantic_schema_info (
6365
);
6466
"""
6567

66-
const SEMANTIC_INDEX_STATEMENTS = [
67-
"CREATE INDEX IF NOT EXISTS idx_semantic_hash ON quandle_semantic_index(descriptor_hash)",
68-
"CREATE INDEX IF NOT EXISTS idx_semantic_key ON quandle_semantic_index(quandle_key)",
69-
"CREATE INDEX IF NOT EXISTS idx_semantic_crossing ON quandle_semantic_index(crossing_number)",
70-
"CREATE INDEX IF NOT EXISTS idx_semantic_determinant ON quandle_semantic_index(determinant)",
71-
"CREATE INDEX IF NOT EXISTS idx_semantic_signature ON quandle_semantic_index(signature)",
72-
"CREATE INDEX IF NOT EXISTS idx_semantic_col3 ON quandle_semantic_index(colouring_count_3)",
73-
"CREATE INDEX IF NOT EXISTS idx_semantic_col5 ON quandle_semantic_index(colouring_count_5)",
74-
# DB-3 Phase A additions (see docs/db-3-index-strategy.md):
75-
# writhe / genus / quandle_generator_count are accepted as filter
76-
# parameters in GET /api/knots and GET /api/semantic but were not
77-
# indexed; queries forced O(n) scans. B-tree per the audit.
78-
"CREATE INDEX IF NOT EXISTS idx_semantic_writhe ON quandle_semantic_index(writhe)",
79-
"CREATE INDEX IF NOT EXISTS idx_semantic_genus ON quandle_semantic_index(genus)",
80-
"CREATE INDEX IF NOT EXISTS idx_semantic_gencount ON quandle_semantic_index(quandle_generator_count)",
81-
]
82-
8368
const REQUIRED_SEMANTIC_COLUMNS = [
8469
("descriptor_version", "TEXT"),
8570
("descriptor_hash", "TEXT"),
@@ -116,8 +101,12 @@ mutable struct SemanticIndexDB
116101
end
117102

118103
_ensure_semantic_columns!(conn)
119-
for stmt in SEMANTIC_INDEX_STATEMENTS
120-
DBInterface.execute(conn, stmt)
104+
105+
# Create standard indices via Skein.jl (DB-3 Phase B)
106+
for col in ["descriptor_hash", "quandle_key", "crossing_number", "determinant",
107+
"signature", "colouring_count_3", "colouring_count_5",
108+
"writhe", "genus", "quandle_generator_count"]
109+
Skein.create_index!(conn, "quandle_semantic_index", col)
121110
end
122111

123112
DBInterface.execute(conn,
@@ -756,7 +745,7 @@ function handle_semantic_equivalents(db::SkeinDB, sdb::SemanticIndexDB, name::St
756745
))
757746
end
758747

759-
function handle_semantic_index(sdb::SemanticIndexDB, params::Dict{String, String})
748+
function _build_semantic_query_sql(params::Dict{String, String})
760749
crossing_number = parse_int_param(params, "crossing_number")
761750
determinant_val = parse_int_param(params, "determinant")
762751
signature_val = parse_int_param(params, "signature")
@@ -809,10 +798,37 @@ function handle_semantic_index(sdb::SemanticIndexDB, params::Dict{String, String
809798
push!(args, limit)
810799
push!(args, offset)
811800

801+
return sql, args, limit, offset
802+
end
803+
804+
function handle_semantic_index(sdb::SemanticIndexDB, params::Dict{String, String})
805+
sql, args, limit, offset = _build_semantic_query_sql(params)
812806
rows = [semantic_to_dict(row) for row in DBInterface.execute(sdb.conn, sql, args)]
813807
json_response(Dict("semantic_index" => rows, "count" => length(rows), "limit" => limit, "offset" => offset))
814808
end
815809

810+
function handle_explain(db::SkeinDB, sdb::SemanticIndexDB, params::Dict{String, String})
811+
endpoint = get(params, "endpoint", nothing)
812+
if endpoint == "semantic"
813+
sql, args, limit, offset = _build_semantic_query_sql(params)
814+
plan = explain_query_plan(sdb.conn, sql, args)
815+
return json_response(Dict("query_plan" => plan, "plan_summary" => plan_summary(plan)))
816+
end
817+
818+
raw_sql = get(params, "sql", nothing)
819+
if !isnothing(raw_sql)
820+
if !is_select_only(raw_sql)
821+
return error_response("Mutation prohibited. EXPLAIN is restricted to SELECT/WITH queries."; status=403)
822+
end
823+
# Phase B: explain raw SQL using the DB connection (could be SkeinDB or SemanticIndexDB)
824+
# Assuming the raw SQL queries the main Skein database
825+
plan = explain_query_plan(db.conn, raw_sql, Any[])
826+
return json_response(Dict("query_plan" => plan, "plan_summary" => plan_summary(plan)))
827+
end
828+
829+
error_response("Missing 'endpoint=semantic' or 'sql=...' parameter"; status=400)
830+
end
831+
816832
function handle_krl_query(data::DataProvider, sem::SemProvider,
817833
metrics::QueryMetrics, req::HTTP.Request)
818834
t0 = time()
@@ -998,6 +1014,8 @@ function router(db::SkeinDB, sdb::SemanticIndexDB, static_dir::String,
9981014
!isnothing(m_sem) &&
9991015
return handle_semantic_detail(db, sdb, m_sem.captures[1])
10001016

1017+
path == "/api/explain" && return handle_explain(db, sdb, params)
1018+
10011019
m_knot = match(r"^/api/knots/(.+)$", path)
10021020
!isnothing(m_knot) &&
10031021
return handle_knot_detail(db, sdb, m_knot.captures[1])

0 commit comments

Comments
 (0)