Coverage for virtuoso_utilities / isql_helpers.py: 48%
83 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-15 14:45 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-15 14:45 +0000
1"""
2Helper functions for executing ISQL commands and scripts against Virtuoso,
3handling both direct execution and execution via Docker.
4"""
5import argparse
6import os
7import shlex
8import subprocess
9import sys
10from typing import Union
13def _run_subprocess(
14 command: Union[list[str], str],
15 use_shell: bool = False,
16 encoding: str = 'utf-8'
17) -> tuple[int, str, str]:
18 """Internal helper to run a subprocess command. Always captures output."""
19 try:
20 process = subprocess.run(
21 command,
22 shell=use_shell,
23 capture_output=True,
24 text=True,
25 check=False,
26 encoding=encoding
27 )
28 stdout = process.stdout.strip() if process.stdout else ""
29 stderr = process.stderr.strip() if process.stderr else ""
30 return process.returncode, stdout, stderr
31 except Exception as e:
32 print(f"Subprocess execution failed: {e}", file=sys.stderr)
33 print(f"Command: {command}", file=sys.stderr)
34 return -1, "", str(e)
36def run_isql_command(
37 args: argparse.Namespace,
38 sql_command: Union[str, None] = None,
39 script_path: Union[str, None] = None,
40 ignore_errors: bool = False,
41) -> tuple[bool, str, str]:
42 """
43 Executes a SQL command or script using the 'isql' utility, either directly
44 or via 'docker exec'. Output is always captured and only shown on error.
46 Exactly one of `sql_command` or `script_path` must be provided.
48 Args:
49 args (argparse.Namespace): Parsed command-line arguments containing
50 connection details and paths. Must have
51 attributes: docker_container, host, port,
52 user, password. If docker_container
53 is set, must also have docker_path and
54 docker_isql_path. Otherwise, must have
55 isql_path.
56 sql_command (Union[str, None]): The SQL command string to execute.
57 script_path (Union[str, None]): The path to the SQL script file to execute.
58 ignore_errors (bool): If True, print errors but return True anyway.
60 Returns:
61 tuple: (success_status, stdout, stderr)
62 success_status is True if the command/script ran without error
63 (exit code 0) or if ignore_errors is True.
64 stdout and stderr contain the respective outputs.
66 Raises:
67 ValueError: If neither or both sql_command and script_path are provided.
68 """
69 if not ((sql_command is None) ^ (script_path is None)):
70 raise ValueError("Exactly one of sql_command or script_path must be provided.")
72 command_to_run: Union[list[str], str] = []
73 use_shell = False
74 effective_isql_path_for_error = ""
75 command_description = ""
77 if args.docker_container:
78 if not hasattr(args, 'docker_path') or not args.docker_path:
79 print("Error: 'docker_path' argument missing for Docker execution.", file=sys.stderr)
80 return False, "", "'docker_path' argument missing"
81 if not hasattr(args, 'docker_isql_path') or not args.docker_isql_path:
82 print("Error: 'docker_isql_path' argument missing for Docker execution.", file=sys.stderr)
83 return False, "", "'docker_isql_path' argument missing"
85 effective_isql_path_for_error = f"'{args.docker_isql_path}' inside container '{args.docker_container}' via '{args.docker_path}'"
87 exec_content = ""
88 if sql_command:
89 exec_content = sql_command
90 command_description = "ISQL command (Docker)"
91 else:
92 command_description = "ISQL script (Docker)"
93 if not os.path.exists(script_path):
94 print(f"Error: Script file not found at '{script_path}'", file=sys.stderr)
95 return False, "", f"Script file not found: {script_path}"
96 print("Reading script content for docker exec...", file=sys.stderr)
97 try:
98 with open(script_path, 'r', encoding='utf-8') as f:
99 sql_content = f.read()
100 exec_content = sql_content.replace('\n', ' ').strip()
101 except Exception as e:
102 print(f"Error reading SQL script file '{script_path}': {e}", file=sys.stderr)
103 return False, "", str(e)
105 docker_internal_host = "localhost"
106 docker_internal_port = 1111
107 command_to_run = [
108 args.docker_path,
109 'exec',
110 args.docker_container,
111 args.docker_isql_path,
112 f"{docker_internal_host}:{docker_internal_port}",
113 args.user,
114 args.password,
115 f"exec={exec_content}"
116 ]
118 else:
119 if not hasattr(args, 'isql_path') or not args.isql_path:
120 print("Error: 'isql_path' argument missing for non-Docker execution.", file=sys.stderr)
121 return False, "", "'isql_path' argument missing"
123 effective_isql_path_for_error = f"'{args.isql_path}' on host"
125 if sql_command:
126 command_description = "ISQL command (Local)"
127 command_to_run = [
128 args.isql_path,
129 f"{args.host}:{args.port}",
130 args.user,
131 args.password,
132 f"exec={sql_command}"
133 ]
134 else:
135 command_description = "ISQL script (Local)"
136 if not os.path.exists(script_path):
137 print(f"Error: Script file not found at '{script_path}'", file=sys.stderr)
138 return False, "", f"Script file not found: {script_path}"
140 effective_isql_path_for_error += " using shell redirection"
141 use_shell = True
143 base_command_list = [
144 args.isql_path,
145 f"{args.host}:{args.port}",
146 args.user,
147 args.password,
148 f"< {shlex.quote(script_path)}" # Use shlex.quote for safety
149 ]
150 command_to_run = " ".join(base_command_list)
152 try:
153 returncode, stdout, stderr = _run_subprocess(command_to_run, use_shell=use_shell)
155 if returncode != 0:
156 # Handle specific FileNotFoundError after subprocess call
157 if ("No such file or directory" in stderr or "not found" in stderr or returncode == 127):
158 # Distinguish between primary executable not found vs other issues
159 missing_cmd = args.docker_path if args.docker_container else args.isql_path
160 print(f"Error: Command '{missing_cmd}' or related component not found.", file=sys.stderr)
161 if args.docker_container:
162 print(f"Make sure '{args.docker_path}' is installed and in your PATH, and the container/isql path is correct.", file=sys.stderr)
163 else:
164 print(f"Make sure Virtuoso client tools (containing '{args.isql_path}') are installed and in your PATH.", file=sys.stderr)
165 if use_shell:
166 print(f"Check shell environment if using local script execution.", file=sys.stderr)
167 return False, stdout, f"Executable or shell component not found: {missing_cmd}"
169 return ignore_errors, stdout, stderr
170 return True, stdout, stderr
171 except Exception as e:
172 # Catch unexpected errors *around* the subprocess call if any
173 print(f"An unexpected error occurred preparing or handling {command_description}: {e}", file=sys.stderr)
174 print(f"Command context: {command_to_run}", file=sys.stderr)
175 return False, "", str(e)