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

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 

11 

12 

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) 

35 

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. 

45 

46 Exactly one of `sql_command` or `script_path` must be provided. 

47 

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. 

59 

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. 

65 

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.") 

71 

72 command_to_run: Union[list[str], str] = [] 

73 use_shell = False 

74 effective_isql_path_for_error = "" 

75 command_description = "" 

76 

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" 

84 

85 effective_isql_path_for_error = f"'{args.docker_isql_path}' inside container '{args.docker_container}' via '{args.docker_path}'" 

86 

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) 

104 

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 ] 

117 

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" 

122 

123 effective_isql_path_for_error = f"'{args.isql_path}' on host" 

124 

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}" 

139 

140 effective_isql_path_for_error += " using shell redirection" 

141 use_shell = True 

142 

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) 

151 

152 try: 

153 returncode, stdout, stderr = _run_subprocess(command_to_run, use_shell=use_shell) 

154 

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}" 

168 

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)