From d278f1e9de1eca361094483ca1c3eb50b2245b2e Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sat, 10 Oct 2026 17:30:14 +0530 Subject: [PATCH 01/12] Say "graph databases" instead of "engines" in the `qeval` help --- src/qeval/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qeval/config.py b/src/qeval/config.py index 79678a77..348398a2 100644 --- a/src/qeval/config.py +++ b/src/qeval/config.py @@ -63,7 +63,7 @@ def parse_command_line() -> argparse.Namespace: tool_description = ( f"{SCRIPT_NAME} sets up, indexes, queries, and benchmarks " "graph databases in a uniform way. " - f"Supported engines: {supported_engines}" + f"Supported graph databases: {supported_engines}" ) parser = argparse.ArgumentParser( description=colored(tool_description, attrs=["bold"]) From 8e8502cb5251d4db5c8e81b46399806dd81a9787 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sat, 10 Oct 2026 17:43:38 +0530 Subject: [PATCH 02/12] Add shared `status` and `stop` commands for the graph databases in `qeval` --- src/qeval/commands/__init__.py | 0 src/qeval/commands/status.py | 28 ++++++++++++ src/qeval/commands/stop.py | 82 ++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/qeval/commands/__init__.py create mode 100644 src/qeval/commands/status.py create mode 100644 src/qeval/commands/stop.py diff --git a/src/qeval/commands/__init__.py b/src/qeval/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/qeval/commands/status.py b/src/qeval/commands/status.py new file mode 100644 index 00000000..24b74d7d --- /dev/null +++ b/src/qeval/commands/status.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from abc import abstractmethod + +from qlever.commands.status import StatusCommand as QleverStatusCommand + + +class StatusCommand(QleverStatusCommand): + """ + Show the processes of an engine. + """ + + @abstractmethod + def default_regex(self) -> str: + """Regex for the command lines of the engine's processes.""" + + def description(self) -> str: + return ( + "Show the processes of this graph database running on this machine" + ) + + def additional_arguments(self, subparser) -> None: + subparser.add_argument( + "--cmdline-regex", + default=self.default_regex(), + help="Show only processes where the command " + "line matches this regex", + ) diff --git a/src/qeval/commands/stop.py b/src/qeval/commands/stop.py new file mode 100644 index 00000000..fde8bca2 --- /dev/null +++ b/src/qeval/commands/stop.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from abc import abstractmethod + +from qeval.commands.status import StatusCommand +from qlever.command import QleverCommand +from qlever.commands.stop import stop_container +from qlever.containerize import Containerize +from qlever.log import log +from qlever.util import stop_process_with_regex + + +class StopCommand(QleverCommand): + """ + Stop the server of an engine for a given dataset: natively by killing + the processes that match a regex, in a container by stopping and + removing the server container. + """ + + def __init__(self): + pass + + @abstractmethod + def default_regex(self) -> str: + """ + Regex for the command line of the engine's server, where `%%NAME%%` + stands for the dataset name. + """ + + @abstractmethod + def status_command(self) -> StatusCommand: + """The engine's `status` command, shown when no process matches.""" + + def description(self) -> str: + return "Stop the server of this graph database for a given dataset" + + def should_have_qleverfile(self) -> bool: + return True + + def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: + return { + "data": ["name"], + "runtime": ["system", "server_container"], + } + + def additional_arguments(self, subparser) -> None: + subparser.add_argument( + "--cmdline-regex", + default=self.default_regex(), + help="Show only processes where the command " + "line matches this regex", + ) + + def execute(self, args) -> bool: + # Only match the server of this dataset. + cmdline_regex = args.cmdline_regex.replace("%%NAME%%", args.name) + is_containerized = args.system in Containerize.supported_systems() + description = ( + f"Checking for container with name {args.server_container}" + if is_containerized + else f'Checking for processes matching "{cmdline_regex}"' + ) + self.show(description, only_show=args.show) + if args.show: + return True + + if is_containerized: + return stop_container(args.server_container) + + stop_process_results = stop_process_with_regex(cmdline_regex) + if stop_process_results is None: + return False + if len(stop_process_results) > 0: + return all(stop_process_results) + + # Nothing matched, so show what is running instead. + log.error("No matching process found") + status_command = self.status_command() + args.cmdline_regex = status_command.default_regex() + log.info("") + status_command.execute(args) + return True From 4df3ef93b3c82aab6311b4275fa610a3c18bfba4 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sat, 10 Oct 2026 17:49:08 +0530 Subject: [PATCH 03/12] Add shared `query` and `benchmark-queries` commands and an endpoint path per graph database in `qeval` --- src/qeval/commands/benchmark_queries.py | 18 ++++++++ src/qeval/commands/query.py | 59 +++++++++++++++++++++++++ src/qeval/config.py | 1 + src/qeval/engines.py | 9 ++-- 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/qeval/commands/benchmark_queries.py create mode 100644 src/qeval/commands/query.py diff --git a/src/qeval/commands/benchmark_queries.py b/src/qeval/commands/benchmark_queries.py new file mode 100644 index 00000000..0466be2e --- /dev/null +++ b/src/qeval/commands/benchmark_queries.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from qlever.commands.benchmark_queries import ( + BenchmarkQueriesCommand as QleverBenchmarkQueriesCommand, +) + + +class BenchmarkQueriesCommand(QleverBenchmarkQueriesCommand): + """ + Run benchmark queries against the SPARQL endpoint of an engine. + """ + + def execute(self, args) -> bool: + args.sparql_endpoint = ( + args.sparql_endpoint + or f"{args.host_name}:{args.port}{args.endpoint_path}" + ) + return super().execute(args) diff --git a/src/qeval/commands/query.py b/src/qeval/commands/query.py new file mode 100644 index 00000000..b022923e --- /dev/null +++ b/src/qeval/commands/query.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from qlever.commands.query import QueryCommand as QleverQueryCommand + + +class QueryCommand(QleverQueryCommand): + """ + Send a query to the SPARQL endpoint of an engine. QLever's option to pin + the result to its cache, and the access token that this needs, are left + out. + """ + + def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: + return {"server": ["host_name", "port"]} + + def additional_arguments(self, subparser) -> None: + subparser.add_argument( + "query", + type=str, + nargs="?", + default="SELECT * WHERE { ?s ?p ?o } LIMIT 10", + help="SPARQL query to send", + ) + subparser.add_argument( + "--predefined-query", + type=str, + choices=self.predefined_queries.keys(), + help="Use a predefined query", + ) + subparser.add_argument( + "--sparql-endpoint", type=str, help="URL of the SPARQL endpoint" + ) + subparser.add_argument( + "--accept", + type=str, + choices=[ + "text/tab-separated-values", + "text/csv", + "application/sparql-results+json", + "application/sparql-results+xml", + ], + default="text/tab-separated-values", + help="Accept header for the SPARQL query", + ) + subparser.add_argument( + "--no-time", + action="store_true", + default=False, + help="Do not print the (end-to-end) time taken", + ) + + def execute(self, args) -> bool: + args.sparql_endpoint = ( + args.sparql_endpoint + or f"{args.host_name}:{args.port}{args.endpoint_path}" + ) + # QLever's `execute` reads this option, which is left out above. + args.pin_to_cache = False + return super().execute(args) diff --git a/src/qeval/config.py b/src/qeval/config.py index 348398a2..215139ba 100644 --- a/src/qeval/config.py +++ b/src/qeval/config.py @@ -99,6 +99,7 @@ def parse_command_line() -> argparse.Namespace: script_name=SCRIPT_NAME, engine_short_name=engine, engine_display_name=engine_info.display_name, + endpoint_path=engine_info.endpoint_path, main_command_name=main_command_name, ) subparsers = engine_parser.add_subparsers( diff --git a/src/qeval/engines.py b/src/qeval/engines.py index d9d39cca..ab623b1c 100644 --- a/src/qeval/engines.py +++ b/src/qeval/engines.py @@ -6,16 +6,19 @@ class Engine(NamedTuple): """ - The package with the commands of an engine, and the name of the engine as - shown to the user. + The package with the commands of an engine, the name of the engine as + shown to the user, and the path of its SPARQL endpoint on the server. """ package: str display_name: str + endpoint_path: str ENGINES = { - "qlever": Engine(package="qlever", display_name="QLever"), + "qlever": Engine( + package="qlever", display_name="QLever", endpoint_path="" + ), } From bee679b8c8c8c6e6b6bf4a4748b4f70f60028afd Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sat, 10 Oct 2026 18:01:20 +0530 Subject: [PATCH 04/12] Add a shared `setup-config` command for the graph databases in `qeval` --- src/qeval/commands/setup_config.py | 82 ++++++++++++++++++++++++++++++ src/qlever/qleverfile.py | 13 ++--- 2 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/qeval/commands/setup_config.py diff --git a/src/qeval/commands/setup_config.py b/src/qeval/commands/setup_config.py new file mode 100644 index 00000000..a75f9545 --- /dev/null +++ b/src/qeval/commands/setup_config.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from abc import abstractmethod +from pathlib import Path + +from qlever.commands.setup_config import ( + SetupConfigCommand as QleverSetupConfigCommand, +) +from qlever.commands.setup_config import check_qleverfile_exists +from qlever.log import log +from qlever.qleverfile import Qleverfile + + +class SetupConfigCommand(QleverSetupConfigCommand): + """ + Create a Qleverfile for an engine from one of QLever's pre-configured + Qleverfiles: keep only the parts that every engine needs, then add the + engine's image and its own settings. + """ + + def filter_criteria(self) -> dict[str, list[str] | None]: + """ + The sections and options of a pre-configured Qleverfile to keep, by + default those that every engine needs. `None` keeps every option of + the section. An engine that needs more can add to what this returns. + """ + return { + "data": None, + "index": ["INPUT_FILES"], + "server": ["PORT"], + "runtime": ["SYSTEM", "IMAGE"], + "ui": ["UI_CONFIG"], + } + + @abstractmethod + def image(self) -> str: + """The container image of the engine.""" + + @abstractmethod + def engine_settings(self, args) -> dict[str, dict[str, str]]: + """The engine's own Qleverfile values, as {section: {option: value}}.""" + + def execute(self, args) -> bool: + template_path = ( + self.qleverfiles_path / f"Qleverfile.{args.config_name}" + ) + self.show( + f"Create a Qleverfile from {template_path}, keeping only the " + "parts that apply to this graph database", + only_show=args.show, + ) + if args.show: + return True + + if check_qleverfile_exists(args.main_command_name): + return False + + # Each step can override the values of the steps before it. + try: + qleverfile = Qleverfile.filter( + template_path, self.filter_criteria() + ) + qleverfile.set("runtime", "IMAGE", self.image()) + for section, options in self.engine_settings(args).items(): + # Skip sections that the pre-configured Qleverfile lacks. + if qleverfile.has_section(section): + for option, value in options.items(): + qleverfile.set(section, option, value) + for section, arg_name in self.override_args: + if arg_value := getattr(args, arg_name): + qleverfile.set(section, arg_name.upper(), str(arg_value)) + with Path("Qleverfile").open("w") as qleverfile_file: + qleverfile.write(qleverfile_file) + except Exception as e: + log.error(f"Could not create the Qleverfile: {e}") + return False + + log.info( + f'Created Qleverfile for config "{args.config_name}" in the ' + "current directory" + ) + return True diff --git a/src/qlever/qleverfile.py b/src/qlever/qleverfile.py index 60780cf4..7bbb19e4 100644 --- a/src/qlever/qleverfile.py +++ b/src/qlever/qleverfile.py @@ -761,14 +761,15 @@ def read(qleverfile_path: Path, engine_short_name: str) -> ConfigParser: @staticmethod def filter( - qleverfile_path: Path, options_included: dict[str, list[str]] + qleverfile_path: Path, options_included: dict[str, list[str] | None] ) -> RawConfigParser: """ Given a filter criteria (key: section_header, value: list[options]), return a RawConfigParser object to create a new filtered Qleverfile - with only the specified sections and options (selects all options if - list[options] is empty). Mainly to be used by `qeval` for the - `setup-config` command of non-qlever engines. + with only the specified sections and options (a value of `None` keeps + every option of the section, an empty list keeps the section without + options). Mainly to be used by `qeval` for the `setup-config` command + of non-qlever engines. """ # Read the Qleverfile. config = RawConfigParser() @@ -782,8 +783,8 @@ def filter( if config.has_section(section): filtered_config.add_section(section) - # If the list is empty, copy all fields - if not desired_fields: + # Without a list of fields, copy all of them. + if desired_fields is None: for field, value in config.items(section): filtered_config.set(section, field, value) else: From da603b40767f84803656df5c5a5993da7fb65ea2 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sat, 10 Oct 2026 18:14:45 +0530 Subject: [PATCH 05/12] Add a shared `index-stats` command for the graph databases in `qeval` --- src/qeval/commands/index_stats.py | 80 +++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/qeval/commands/index_stats.py diff --git a/src/qeval/commands/index_stats.py b/src/qeval/commands/index_stats.py new file mode 100644 index 00000000..7f2d0ad5 --- /dev/null +++ b/src/qeval/commands/index_stats.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from abc import abstractmethod +from pathlib import Path + +from qlever.commands.index_stats import ( + IndexStatsCommand as QleverIndexStatsCommand, +) +from qlever.commands.index_stats import ( + get_size_unit, + get_size_unit_factor, + get_time_unit, + get_time_unit_factor, +) +from qlever.log import log +from qlever.util import get_total_file_size + + +def read_index_log(log_file_name: str | Path) -> str | None: + """ + The text of the index log, or None if it cannot be read, which is + reported as an error. + """ + try: + return Path(log_file_name).read_text(errors="replace") + except OSError as e: + log.error(f"Problem reading index log file {log_file_name}: {e}") + return None + + +class IndexStatsCommand(QleverIndexStatsCommand): + """ + Show how long the index build of an engine took and how much space its + index uses, from the durations in its index log and the size of its + index files. + """ + + @abstractmethod + def index_size_patterns(self, args) -> list[str]: + """The glob patterns of the index files, whose sizes add up.""" + + @abstractmethod + def parse_index_durations(self, log_text: str) -> dict[str, float]: + """ + How long each phase of the build took in seconds, from the text of + the index log, in the order to show them. + """ + + def index_durations(self, log_file_name: str | Path) -> dict[str, float]: + """ + Read the index log and parse it. Empty if the log cannot be read. + """ + log_text = read_index_log(log_file_name) + if log_text is None: + return {} + return self.parse_index_durations(log_text) + + def execute_time( + self, args, log_file_name: str + ) -> dict[str, tuple[float | None, str]]: + """ + The duration of each phase, all in the same unit, picked to suit + the longest one. + """ + durations = self.index_durations(log_file_name) + if not durations: + return {} + time_unit = get_time_unit(args.time_unit, max(durations.values())) + unit_factor = get_time_unit_factor(time_unit) + return { + heading: (seconds / unit_factor, time_unit) + for heading, seconds in durations.items() + } + + def execute_space(self, args) -> dict[str, tuple[float, str]]: + """The total size of the index files, in a unit that suits it.""" + index_size = get_total_file_size(self.index_size_patterns(args)) + size_unit = get_size_unit(args.size_unit, index_size) + unit_factor = get_size_unit_factor(size_unit) + return {"TOTAL size": (index_size / unit_factor, size_unit)} From 39fcda0fcc0c9146e6ae1f4eac0fd436a2ba58a6 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sat, 10 Oct 2026 18:20:44 +0530 Subject: [PATCH 06/12] Rename the shared `qeval` commands to `Base...Command` in `qeval/base_commands` --- src/qeval/{commands => base_commands}/__init__.py | 0 src/qeval/{commands => base_commands}/benchmark_queries.py | 2 +- src/qeval/{commands => base_commands}/index_stats.py | 2 +- src/qeval/{commands => base_commands}/query.py | 2 +- src/qeval/{commands => base_commands}/setup_config.py | 2 +- src/qeval/{commands => base_commands}/status.py | 2 +- src/qeval/{commands => base_commands}/stop.py | 6 +++--- 7 files changed, 8 insertions(+), 8 deletions(-) rename src/qeval/{commands => base_commands}/__init__.py (100%) rename src/qeval/{commands => base_commands}/benchmark_queries.py (87%) rename src/qeval/{commands => base_commands}/index_stats.py (98%) rename src/qeval/{commands => base_commands}/query.py (97%) rename src/qeval/{commands => base_commands}/setup_config.py (98%) rename src/qeval/{commands => base_commands}/status.py (94%) rename src/qeval/{commands => base_commands}/stop.py (94%) diff --git a/src/qeval/commands/__init__.py b/src/qeval/base_commands/__init__.py similarity index 100% rename from src/qeval/commands/__init__.py rename to src/qeval/base_commands/__init__.py diff --git a/src/qeval/commands/benchmark_queries.py b/src/qeval/base_commands/benchmark_queries.py similarity index 87% rename from src/qeval/commands/benchmark_queries.py rename to src/qeval/base_commands/benchmark_queries.py index 0466be2e..856f1343 100644 --- a/src/qeval/commands/benchmark_queries.py +++ b/src/qeval/base_commands/benchmark_queries.py @@ -5,7 +5,7 @@ ) -class BenchmarkQueriesCommand(QleverBenchmarkQueriesCommand): +class BaseBenchmarkQueriesCommand(QleverBenchmarkQueriesCommand): """ Run benchmark queries against the SPARQL endpoint of an engine. """ diff --git a/src/qeval/commands/index_stats.py b/src/qeval/base_commands/index_stats.py similarity index 98% rename from src/qeval/commands/index_stats.py rename to src/qeval/base_commands/index_stats.py index 7f2d0ad5..08f69062 100644 --- a/src/qeval/commands/index_stats.py +++ b/src/qeval/base_commands/index_stats.py @@ -28,7 +28,7 @@ def read_index_log(log_file_name: str | Path) -> str | None: return None -class IndexStatsCommand(QleverIndexStatsCommand): +class BaseIndexStatsCommand(QleverIndexStatsCommand): """ Show how long the index build of an engine took and how much space its index uses, from the durations in its index log and the size of its diff --git a/src/qeval/commands/query.py b/src/qeval/base_commands/query.py similarity index 97% rename from src/qeval/commands/query.py rename to src/qeval/base_commands/query.py index b022923e..3a9f785f 100644 --- a/src/qeval/commands/query.py +++ b/src/qeval/base_commands/query.py @@ -3,7 +3,7 @@ from qlever.commands.query import QueryCommand as QleverQueryCommand -class QueryCommand(QleverQueryCommand): +class BaseQueryCommand(QleverQueryCommand): """ Send a query to the SPARQL endpoint of an engine. QLever's option to pin the result to its cache, and the access token that this needs, are left diff --git a/src/qeval/commands/setup_config.py b/src/qeval/base_commands/setup_config.py similarity index 98% rename from src/qeval/commands/setup_config.py rename to src/qeval/base_commands/setup_config.py index a75f9545..91575387 100644 --- a/src/qeval/commands/setup_config.py +++ b/src/qeval/base_commands/setup_config.py @@ -11,7 +11,7 @@ from qlever.qleverfile import Qleverfile -class SetupConfigCommand(QleverSetupConfigCommand): +class BaseSetupConfigCommand(QleverSetupConfigCommand): """ Create a Qleverfile for an engine from one of QLever's pre-configured Qleverfiles: keep only the parts that every engine needs, then add the diff --git a/src/qeval/commands/status.py b/src/qeval/base_commands/status.py similarity index 94% rename from src/qeval/commands/status.py rename to src/qeval/base_commands/status.py index 24b74d7d..bfa76f84 100644 --- a/src/qeval/commands/status.py +++ b/src/qeval/base_commands/status.py @@ -5,7 +5,7 @@ from qlever.commands.status import StatusCommand as QleverStatusCommand -class StatusCommand(QleverStatusCommand): +class BaseStatusCommand(QleverStatusCommand): """ Show the processes of an engine. """ diff --git a/src/qeval/commands/stop.py b/src/qeval/base_commands/stop.py similarity index 94% rename from src/qeval/commands/stop.py rename to src/qeval/base_commands/stop.py index fde8bca2..91aabb88 100644 --- a/src/qeval/commands/stop.py +++ b/src/qeval/base_commands/stop.py @@ -2,7 +2,7 @@ from abc import abstractmethod -from qeval.commands.status import StatusCommand +from qeval.base_commands.status import BaseStatusCommand from qlever.command import QleverCommand from qlever.commands.stop import stop_container from qlever.containerize import Containerize @@ -10,7 +10,7 @@ from qlever.util import stop_process_with_regex -class StopCommand(QleverCommand): +class BaseStopCommand(QleverCommand): """ Stop the server of an engine for a given dataset: natively by killing the processes that match a regex, in a container by stopping and @@ -28,7 +28,7 @@ def default_regex(self) -> str: """ @abstractmethod - def status_command(self) -> StatusCommand: + def status_command(self) -> BaseStatusCommand: """The engine's `status` command, shown when no process matches.""" def description(self) -> str: From 8e70388330dc510516f80d0dc711963df896c0c5 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sun, 11 Oct 2026 00:29:07 +0530 Subject: [PATCH 07/12] Set `pin_to_cache` as a parser default in the shared `query` command --- src/qeval/base_commands/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qeval/base_commands/query.py b/src/qeval/base_commands/query.py index 3a9f785f..90b4fda9 100644 --- a/src/qeval/base_commands/query.py +++ b/src/qeval/base_commands/query.py @@ -48,12 +48,12 @@ def additional_arguments(self, subparser) -> None: default=False, help="Do not print the (end-to-end) time taken", ) + # QLever's `execute` reads this option, which is left out above. + subparser.set_defaults(pin_to_cache=False) def execute(self, args) -> bool: args.sparql_endpoint = ( args.sparql_endpoint or f"{args.host_name}:{args.port}{args.endpoint_path}" ) - # QLever's `execute` reads this option, which is left out above. - args.pin_to_cache = False return super().execute(args) From b59743329180a24df4ef8099aaee5ab80824d69d Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Sun, 11 Oct 2026 00:33:05 +0530 Subject: [PATCH 08/12] Let engines choose the index-stats command and cache clearing in `benchmark-queries` --- src/qeval/base_commands/benchmark_queries.py | 11 ++++++++ src/qlever/commands/benchmark_queries.py | 29 ++++++++++++++------ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/qeval/base_commands/benchmark_queries.py b/src/qeval/base_commands/benchmark_queries.py index 856f1343..4a2c9866 100644 --- a/src/qeval/base_commands/benchmark_queries.py +++ b/src/qeval/base_commands/benchmark_queries.py @@ -1,5 +1,8 @@ from __future__ import annotations +from abc import abstractmethod + +from qeval.base_commands.index_stats import BaseIndexStatsCommand from qlever.commands.benchmark_queries import ( BenchmarkQueriesCommand as QleverBenchmarkQueriesCommand, ) @@ -10,6 +13,14 @@ class BaseBenchmarkQueriesCommand(QleverBenchmarkQueriesCommand): Run benchmark queries against the SPARQL endpoint of an engine. """ + @abstractmethod + def index_stats_command(self) -> BaseIndexStatsCommand: + """The command that reads the index time and size of the engine.""" + + def can_clear_cache(self, args, engine: str | None) -> bool: + # Clearing the cache before each query only works for QLever. + return False + def execute(self, args) -> bool: args.sparql_endpoint = ( args.sparql_endpoint diff --git a/src/qlever/commands/benchmark_queries.py b/src/qlever/commands/benchmark_queries.py index eb3f2c0b..e0f046c4 100644 --- a/src/qlever/commands/benchmark_queries.py +++ b/src/qlever/commands/benchmark_queries.py @@ -722,6 +722,23 @@ def additional_arguments(self, subparser) -> None: ), ) + def index_stats_command(self) -> IndexStatsCommand: + """The command that reads the index time and size of the engine.""" + return IndexStatsCommand() + + def can_clear_cache(self, args, engine: str | None) -> bool: + """ + Whether the cache can be cleared before each query. Only QLever can + do that, so guess from the endpoint and the result file name. + """ + is_qlever = ( + not args.sparql_endpoint + or args.sparql_endpoint.startswith("https://qlever") + ) + if engine is not None: + is_qlever = is_qlever or "qlever" in engine.lower() + return is_qlever + def execute(self, args) -> bool: # We can't have both `--remove-offset-and-limit` and `--limit`. if args.remove_offset_and_limit and args.limit: @@ -806,14 +823,8 @@ def execute(self, args) -> bool: return False # Clear cache only works for QLever. - is_qlever = ( - not args.sparql_endpoint - or args.sparql_endpoint.startswith("https://qlever") - ) - if engine is not None: - is_qlever = is_qlever or "qlever" in engine.lower() if args.clear_cache == "yes": - if is_qlever: + if self.can_clear_cache(args, engine): log.warning( "Clearing the cache before each query" " (only works for QLever)" @@ -898,7 +909,9 @@ def execute(self, args) -> bool: if timeout: result_yml_query_records["timeout"] = timeout - index_time, index_size = compute_index_stats(IndexStatsCommand()) + index_time, index_size = compute_index_stats( + self.index_stats_command() + ) result_yml_query_records["index_time"] = index_time result_yml_query_records["index_size"] = index_size From d8b8681cfae31053f6ea53949d7ca5efdd0e4466 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Tue, 13 Oct 2026 19:08:26 +0530 Subject: [PATCH 09/12] Read the SPARQL endpoint path and the `status` regex from hooks instead of copying QLever's code in `qeval` and use clearer description texts --- src/qeval/base_commands/benchmark_queries.py | 7 ------- src/qeval/base_commands/query.py | 7 ------- src/qeval/base_commands/status.py | 8 -------- src/qeval/base_commands/stop.py | 3 +-- src/qlever/commands/benchmark_queries.py | 3 ++- src/qlever/commands/index_stats.py | 5 ++--- src/qlever/commands/query.py | 2 +- src/qlever/commands/status.py | 14 +++++++++----- src/qlever/config.py | 9 +++++---- 9 files changed, 20 insertions(+), 38 deletions(-) diff --git a/src/qeval/base_commands/benchmark_queries.py b/src/qeval/base_commands/benchmark_queries.py index 4a2c9866..adeff742 100644 --- a/src/qeval/base_commands/benchmark_queries.py +++ b/src/qeval/base_commands/benchmark_queries.py @@ -20,10 +20,3 @@ def index_stats_command(self) -> BaseIndexStatsCommand: def can_clear_cache(self, args, engine: str | None) -> bool: # Clearing the cache before each query only works for QLever. return False - - def execute(self, args) -> bool: - args.sparql_endpoint = ( - args.sparql_endpoint - or f"{args.host_name}:{args.port}{args.endpoint_path}" - ) - return super().execute(args) diff --git a/src/qeval/base_commands/query.py b/src/qeval/base_commands/query.py index 90b4fda9..9bc3d07f 100644 --- a/src/qeval/base_commands/query.py +++ b/src/qeval/base_commands/query.py @@ -50,10 +50,3 @@ def additional_arguments(self, subparser) -> None: ) # QLever's `execute` reads this option, which is left out above. subparser.set_defaults(pin_to_cache=False) - - def execute(self, args) -> bool: - args.sparql_endpoint = ( - args.sparql_endpoint - or f"{args.host_name}:{args.port}{args.endpoint_path}" - ) - return super().execute(args) diff --git a/src/qeval/base_commands/status.py b/src/qeval/base_commands/status.py index bfa76f84..a4d6eca5 100644 --- a/src/qeval/base_commands/status.py +++ b/src/qeval/base_commands/status.py @@ -18,11 +18,3 @@ def description(self) -> str: return ( "Show the processes of this graph database running on this machine" ) - - def additional_arguments(self, subparser) -> None: - subparser.add_argument( - "--cmdline-regex", - default=self.default_regex(), - help="Show only processes where the command " - "line matches this regex", - ) diff --git a/src/qeval/base_commands/stop.py b/src/qeval/base_commands/stop.py index 91aabb88..d406b7c6 100644 --- a/src/qeval/base_commands/stop.py +++ b/src/qeval/base_commands/stop.py @@ -47,8 +47,7 @@ def additional_arguments(self, subparser) -> None: subparser.add_argument( "--cmdline-regex", default=self.default_regex(), - help="Show only processes where the command " - "line matches this regex", + help="Stop the processes whose command line matches this regex", ) def execute(self, args) -> bool: diff --git a/src/qlever/commands/benchmark_queries.py b/src/qlever/commands/benchmark_queries.py index e0f046c4..9caef5b2 100644 --- a/src/qlever/commands/benchmark_queries.py +++ b/src/qlever/commands/benchmark_queries.py @@ -841,7 +841,8 @@ def execute(self, args) -> bool: f"curl -sv https://qlever.dev/api/examples/{args.ui_config}" ) sparql_endpoint = ( - args.sparql_endpoint or f"{args.host_name}:{args.port}" + args.sparql_endpoint + or f"{args.host_name}:{args.port}{args.endpoint_path}" ) self.show( diff --git a/src/qlever/commands/index_stats.py b/src/qlever/commands/index_stats.py index 28aa334c..831289cc 100644 --- a/src/qlever/commands/index_stats.py +++ b/src/qlever/commands/index_stats.py @@ -291,9 +291,8 @@ def execute(self, args) -> bool: if not args.only_space: log_file_name = f"{args.name}.index-log.txt" self.show( - f"Breakdown of the time used for " - f"building the index, based on the timestamps for key " - f'lines in "{log_file_name}"', + "Breakdown of the time used for building the index, " + f'based on key lines in "{log_file_name}"', only_show=args.show, ) if not args.show: diff --git a/src/qlever/commands/query.py b/src/qlever/commands/query.py index a2005ad1..33520549 100644 --- a/src/qlever/commands/query.py +++ b/src/qlever/commands/query.py @@ -110,7 +110,7 @@ def execute(self, args) -> bool: sparql_endpoint = ( args.sparql_endpoint if args.sparql_endpoint - else f"{args.host_name}:{args.port}" + else f"{args.host_name}:{args.port}{args.endpoint_path}" ) curl_cmd = ( f"curl -s {sparql_endpoint}" diff --git a/src/qlever/commands/status.py b/src/qlever/commands/status.py index e9a89a2c..72b1dd34 100644 --- a/src/qlever/commands/status.py +++ b/src/qlever/commands/status.py @@ -23,14 +23,18 @@ def should_have_qleverfile(self) -> bool: def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: return {} + def default_regex(self) -> str: + """Regex for the command lines of QLever's processes.""" + # A server that runs as a systemd unit (see `start`) has the + # absolute path of its binary in its command line, because + # `systemd-run` resolves it; a server started with `nohup` has + # the name as given in `SERVER_BINARY`. + return r"^(\S*/)?(qlever-server|qlever-index)" + def additional_arguments(self, subparser) -> None: subparser.add_argument( "--cmdline-regex", - # A server that runs as a systemd unit (see `start`) has the - # absolute path of its binary in its command line, because - # `systemd-run` resolves it; a server started with `nohup` has - # the name as given in `SERVER_BINARY`. - default=r"^(\S*/)?(qlever-server|qlever-index)", + default=self.default_regex(), help="Show only processes where the command " "line matches this regex", ) diff --git a/src/qlever/config.py b/src/qlever/config.py index 802925de..c57ad0b0 100644 --- a/src/qlever/config.py +++ b/src/qlever/config.py @@ -278,14 +278,15 @@ def parse_command_line() -> argparse.Namespace: # The name of the script (the console script from `pyproject.toml`), the # short name of the engine (used for the container names), the name of the - # engine as shown to the user, and the name of the main command (what the - # user types before a command, which for a script that supports several - # engines may consist of more than one word). These are provided to all - # commands via `args`. + # engine as shown to the user, the path of its SPARQL endpoint on the + # server, and the name of the main command (what the user types before a + # command, which for a script that supports several engines may consist + # of more than one word). These are provided to all commands via `args`. parser.set_defaults( script_name="qlever", engine_short_name="qlever", engine_display_name="QLever", + endpoint_path="", main_command_name="qlever", ) From 25052fa7df4d63ca29ff41439a895003f885b667 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Tue, 13 Oct 2026 19:22:10 +0530 Subject: [PATCH 10/12] Use `/` as the SPARQL endpoint path of QLever, so that every endpoint path starts with a slash --- src/qeval/engines.py | 2 +- src/qlever/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qeval/engines.py b/src/qeval/engines.py index ab623b1c..83e67a9b 100644 --- a/src/qeval/engines.py +++ b/src/qeval/engines.py @@ -17,7 +17,7 @@ class Engine(NamedTuple): ENGINES = { "qlever": Engine( - package="qlever", display_name="QLever", endpoint_path="" + package="qlever", display_name="QLever", endpoint_path="/" ), } diff --git a/src/qlever/config.py b/src/qlever/config.py index c57ad0b0..e8f05c2b 100644 --- a/src/qlever/config.py +++ b/src/qlever/config.py @@ -286,7 +286,7 @@ def parse_command_line() -> argparse.Namespace: script_name="qlever", engine_short_name="qlever", engine_display_name="QLever", - endpoint_path="", + endpoint_path="/", main_command_name="qlever", ) From e15c7e7e104fff799fc4356674d88df946e512ec Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Tue, 13 Oct 2026 20:02:49 +0530 Subject: [PATCH 11/12] Let engines choose the `--accept` choices and whether `--pin-to-cache` is offered in the shared `query` command --- src/qeval/base_commands/query.py | 47 +++++++------------------------- src/qlever/commands/query.py | 40 +++++++++++++++++---------- 2 files changed, 36 insertions(+), 51 deletions(-) diff --git a/src/qeval/base_commands/query.py b/src/qeval/base_commands/query.py index 9bc3d07f..305b2b0c 100644 --- a/src/qeval/base_commands/query.py +++ b/src/qeval/base_commands/query.py @@ -13,40 +13,13 @@ class BaseQueryCommand(QleverQueryCommand): def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: return {"server": ["host_name", "port"]} - def additional_arguments(self, subparser) -> None: - subparser.add_argument( - "query", - type=str, - nargs="?", - default="SELECT * WHERE { ?s ?p ?o } LIMIT 10", - help="SPARQL query to send", - ) - subparser.add_argument( - "--predefined-query", - type=str, - choices=self.predefined_queries.keys(), - help="Use a predefined query", - ) - subparser.add_argument( - "--sparql-endpoint", type=str, help="URL of the SPARQL endpoint" - ) - subparser.add_argument( - "--accept", - type=str, - choices=[ - "text/tab-separated-values", - "text/csv", - "application/sparql-results+json", - "application/sparql-results+xml", - ], - default="text/tab-separated-values", - help="Accept header for the SPARQL query", - ) - subparser.add_argument( - "--no-time", - action="store_true", - default=False, - help="Do not print the (end-to-end) time taken", - ) - # QLever's `execute` reads this option, which is left out above. - subparser.set_defaults(pin_to_cache=False) + def accept_header_choices(self) -> list[str]: + return [ + "text/tab-separated-values", + "text/csv", + "application/sparql-results+json", + "application/sparql-results+xml", + ] + + def can_pin_to_cache(self) -> bool: + return False diff --git a/src/qlever/commands/query.py b/src/qlever/commands/query.py index 33520549..3367c22d 100644 --- a/src/qlever/commands/query.py +++ b/src/qlever/commands/query.py @@ -40,6 +40,21 @@ def should_have_qleverfile(self) -> bool: def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: return {"server": ["host_name", "port", "access_token"]} + def accept_header_choices(self) -> list[str]: + """The media types that `--accept` offers.""" + return [ + "text/tab-separated-values", + "text/csv", + "application/sparql-results+json", + "application/sparql-results+xml", + "application/qlever-results+json", + "application/octet-stream", + ] + + def can_pin_to_cache(self) -> bool: + """Whether `--pin-to-cache` is offered, which needs an access token.""" + return True + def additional_arguments(self, subparser) -> None: subparser.add_argument( "query", @@ -54,26 +69,23 @@ def additional_arguments(self, subparser) -> None: choices=self.predefined_queries.keys(), help="Use a predefined query", ) - subparser.add_argument( - "--pin-to-cache", - action="store_true", - default=False, - help="Pin the query to the cache", - ) + if self.can_pin_to_cache(): + subparser.add_argument( + "--pin-to-cache", + action="store_true", + default=False, + help="Pin the query to the cache", + ) + else: + # `execute` reads this option, so it needs a value. + subparser.set_defaults(pin_to_cache=False) subparser.add_argument( "--sparql-endpoint", type=str, help="URL of the SPARQL endpoint" ) subparser.add_argument( "--accept", type=str, - choices=[ - "text/tab-separated-values", - "text/csv", - "application/sparql-results+json", - "application/sparql-results+xml", - "application/qlever-results+json", - "application/octet-stream", - ], + choices=self.accept_header_choices(), default="text/tab-separated-values", help="Accept header for the SPARQL query", ) From 3238bdbaf7e5e07093cfb297b0549cba3dcf3c04 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Thu, 22 Oct 2026 15:27:40 +0200 Subject: [PATCH 12/12] Report a missing container in the shared `stop` command So far, `BaseStopCommand` returned `False` without a message when the Qleverfile says that the server runs in a container and no container with the configured name exists, so `qeval stop` exited with status 1 after the line "Checking for container with name ..." and nothing else. Now it reports the missing container, like QLever's `stop` reports a missing process or container. --- src/qeval/base_commands/stop.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/qeval/base_commands/stop.py b/src/qeval/base_commands/stop.py index d406b7c6..2ce2a7db 100644 --- a/src/qeval/base_commands/stop.py +++ b/src/qeval/base_commands/stop.py @@ -64,7 +64,12 @@ def execute(self, args) -> bool: return True if is_containerized: - return stop_container(args.server_container) + if stop_container(args.server_container): + return True + log.error( + f'No container with name "{args.server_container}" found' + ) + return False stop_process_results = stop_process_with_regex(cmdline_regex) if stop_process_results is None: