diff --git a/Doxyfile b/Doxyfile index 3e5bb7209..0994f1097 100644 --- a/Doxyfile +++ b/Doxyfile @@ -45,7 +45,11 @@ XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- INPUT = src/ -FILE_PATTERNS = *.sql *.template +# Only the generated *.sql is documented. *.template files are build-time +# sources (they still carry $RELEASE_VERSION placeholders and duplicate their +# generated .sql), so documenting them produced a spurious second `version` +# memberdef with placeholder text. +FILE_PATTERNS = *.sql RECURSIVE = YES EXCLUDE_PATTERNS = *_test.sql diff --git a/src/v3/version.template b/src/v3/version.template index 0849b0b08..828635352 100644 --- a/src/v3/version.template +++ b/src/v3/version.template @@ -21,10 +21,8 @@ DROP FUNCTION IF EXISTS eql_v3.version(); --! --! @note Auto-generated during build from src/v3/version.template --! ---! @example ---! -- Check installed EQL version ---! SELECT eql_v3.version(); ---! -- Returns: '3.0.0' +--! Example: `SELECT eql_v3.version()` returns the installed version string, +--! e.g. `'3.0.0'` (or `'DEV'` for development builds). CREATE FUNCTION eql_v3.version() RETURNS text IMMUTABLE STRICT PARALLEL SAFE diff --git a/tasks/docs/doxygen-filter.sh b/tasks/docs/doxygen-filter.sh index ab196dd9c..b64bd806a 100755 --- a/tasks/docs/doxygen-filter.sh +++ b/tasks/docs/doxygen-filter.sh @@ -1,5 +1,50 @@ #!/usr/bin/env bash #MISE description="Doxygen input filter for SQL files" +set -euo pipefail -# Converts SQL-style comments (--!) to C++-style comments (//!) -sed 's/^--!/\/\/!/g' "$1" +# Doxygen always calls an INPUT_FILTER with exactly one filename. Guard it so a +# stray invocation fails fast instead of `awk` blocking on stdin and hanging the +# docs job. +if [ "$#" -ne 1 ]; then + echo "usage: $(basename "$0") " >&2 + exit 2 +fi + +# Prepares SQL for Doxygen's C++ parser. Two transforms: +# +# 1. `--!` doc comments -> `//!` so Doxygen sees them. +# 2. Strip dollar-quoted function bodies (`$$ ... $$`), leaving just the +# declaration and its trailing clauses. Doxygen parses SQL heuristically as +# C++, and body SQL derails it: a `::type` cast reads as C++ scope +# resolution and drops the whole enclosing CREATE FUNCTION memberdef (this +# silently lost jsonb_path_query and ~hundreds of generated extractor +# overloads), while keywords/calls in bodies (`SELECT`, `RETURN NEXT`, +# `array_length(...)`) get mis-parsed as spurious functions. Bodies carry no +# documentation, so removing them is lossless for the generated reference +# and leaves Doxygen only clean `CREATE FUNCTION name(args) RETURNS ...` +# declarations to read. Only bare `$$` quoting is used in this codebase. +awk ' + /^--!/ { print "//!" substr($0, 4); next } + { + out = "" + s = $0 + while (length(s) > 0) { + p = index(s, "$$") + if (inbody) { + if (p == 0) { s = ""; break } # whole remainder is body: drop + s = substr(s, p + 2) # resume after the closing $$ + inbody = 0 + } else { + if (p == 0) { out = out s; break } # no body marker: keep as-is + out = out substr(s, 1, p - 1) # keep code before opening $$ + s = substr(s, p + 2) + inbody = 1 + } + } + # Regular SQL `--` comments read as C++ code to Doxygen (e.g. + # `-- per-entry overloads (...)` mints a phantom `overloads(...)` function), + # so neutralize them to C++ line comments on the (body-stripped) code. + gsub(/--/, "//", out) + print out + } +' "$1" diff --git a/tasks/docs/generate/test_xml_to_markdown.py b/tasks/docs/generate/test_xml_to_markdown.py index 77fb6cd5a..c547c9703 100755 --- a/tasks/docs/generate/test_xml_to_markdown.py +++ b/tasks/docs/generate/test_xml_to_markdown.py @@ -155,6 +155,50 @@ def test_schema_qualified_type(): print("āœ“ Schema-qualified type test passed") +def _load_process_function(): + """Load process_function from the hyphenated module by path.""" + import importlib.util + + spec = importlib.util.spec_from_file_location( + "eql_xml_to_markdown", Path(__file__).parent / "xml-to-markdown.py" + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.process_function + +def test_internal_schema_is_private(): + """Functions in the eql_v3_internal schema are flagged private via . + + Doxygen puts the schema in the memberdef ("CREATE FUNCTION + eql_v3_internal"), not the , so visibility is a schema check. Guards + against regressing to the old leading-underscore-name heuristic, which + flagged none of the internal surface (reporting everything public). + """ + from xml.etree import ElementTree as ET + + process_function = _load_process_function() + + def memberdef(schema, fn): + return ET.fromstring(f''' + + {fn} + CREATE FUNCTION {schema} + (val jsonb) RETURNS bytea + Extract a term. + + + ''') + + internal = process_function(memberdef("eql_v3_internal", "eq_term")) + assert internal is not None, "internal function should be extracted" + assert internal["is_private"] is True, "eql_v3_internal.* must be private" + + public = process_function(memberdef("eql_v3", "jsonb_path_query")) + assert public is not None, "public function should be extracted" + assert public["is_private"] is False, "eql_v3.* must be public" + + print("āœ“ Internal-schema private detection test passed") + if __name__ == '__main__': print("Running xml-to-markdown tests...\n") @@ -163,6 +207,7 @@ def test_schema_qualified_type(): test_variants_no_self_reference() test_param_name_type_swap() test_schema_qualified_type() + test_internal_schema_is_private() print("\nāœ… All tests passed!") sys.exit(0) diff --git a/tasks/docs/generate/xml-to-markdown.py b/tasks/docs/generate/xml-to-markdown.py index 02ce371b5..f838547fc 100755 --- a/tasks/docs/generate/xml-to-markdown.py +++ b/tasks/docs/generate/xml-to-markdown.py @@ -203,8 +203,15 @@ def process_function(memberdef): if op_match: func_name = op_match.group(1) # Use operator as function name - # Check if this is a private/internal function - is_private = func_name.startswith('_') + # Check if this is a private/internal function. + # Internal functions live in the `eql_v3_internal` schema. Doxygen puts the + # schema in the memberdef (e.g. "CREATE FUNCTION eql_v3_internal"), + # not the , so a schema check is required — the older bare + # leading-underscore convention alone flags none of them (which left the + # whole surface reported as public). Keep the underscore check too. + type_elem = memberdef.find('type') + type_text = extract_para_text(type_elem) if type_elem is not None else '' + is_private = func_name.startswith('_') or 'eql_v3_internal' in type_text # Extract descriptions brief = extract_description(memberdef.find('briefdescription'))