Coverage for mindsdb / integrations / handlers / firebird_handler / firebird_handler.py: 0%

85 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-21 00:36 +0000

1from typing import Optional 

2 

3import pandas as pd 

4import fdb 

5 

6from mindsdb_sql_parser import parse_sql 

7from sqlalchemy_firebird.base import FBDialect 

8from mindsdb.utilities.render.sqlalchemy_render import SqlalchemyRender 

9from mindsdb.integrations.libs.base import DatabaseHandler 

10 

11from mindsdb_sql_parser.ast.base import ASTNode 

12 

13from mindsdb.utilities import log 

14from mindsdb.integrations.libs.response import ( 

15 HandlerStatusResponse as StatusResponse, 

16 HandlerResponse as Response, 

17 RESPONSE_TYPE 

18) 

19 

20logger = log.getLogger(__name__) 

21 

22 

23class FirebirdHandler(DatabaseHandler): 

24 """ 

25 This handler handles connection and execution of the Firebird statements. 

26 """ 

27 

28 name = 'firebird' 

29 

30 def __init__(self, name: str, connection_data: Optional[dict], **kwargs): 

31 """ 

32 Initialize the handler. 

33 Args: 

34 name (str): name of particular handler instance 

35 connection_data (dict): parameters for connecting to the database 

36 **kwargs: arbitrary keyword arguments. 

37 """ 

38 super().__init__(name) 

39 self.parser = parse_sql 

40 self.dialect = 'firebird' 

41 self.connection_data = connection_data 

42 self.kwargs = kwargs 

43 

44 self.connection = None 

45 self.is_connected = False 

46 

47 def __del__(self): 

48 if self.is_connected is True: 

49 self.disconnect() 

50 

51 def connect(self) -> StatusResponse: 

52 """ 

53 Set up the connection required by the handler. 

54 Returns: 

55 HandlerStatusResponse 

56 """ 

57 

58 if self.is_connected is True: 

59 return self.connection 

60 

61 self.connection = fdb.connect( 

62 host=self.connection_data['host'], 

63 database=self.connection_data['database'], 

64 user=self.connection_data['user'], 

65 password=self.connection_data['password'], 

66 ) 

67 self.is_connected = True 

68 

69 return self.connection 

70 

71 def disconnect(self): 

72 """ 

73 Close any existing connections. 

74 """ 

75 

76 if self.is_connected is False: 

77 return 

78 

79 self.connection.close() 

80 self.is_connected = False 

81 return self.is_connected 

82 

83 def check_connection(self) -> StatusResponse: 

84 """ 

85 Check connection to the handler. 

86 Returns: 

87 HandlerStatusResponse 

88 """ 

89 

90 response = StatusResponse(False) 

91 need_to_close = self.is_connected is False 

92 

93 try: 

94 self.connect() 

95 response.success = True 

96 except Exception as e: 

97 logger.error(f'Error connecting to Firebird {self.connection_data["database"]}, {e}!') 

98 response.error_message = str(e) 

99 finally: 

100 if response.success is True and need_to_close: 

101 self.disconnect() 

102 if response.success is False and self.is_connected is True: 

103 self.is_connected = False 

104 

105 return response 

106 

107 def native_query(self, query: str) -> StatusResponse: 

108 """ 

109 Receive raw query and act upon it somehow. 

110 Args: 

111 query (str): query in native format 

112 Returns: 

113 HandlerResponse 

114 """ 

115 

116 need_to_close = self.is_connected is False 

117 

118 connection = self.connect() 

119 cursor = connection.cursor() 

120 

121 try: 

122 cursor.execute(query) 

123 result = cursor.fetchall() 

124 if result: 

125 response = Response( 

126 RESPONSE_TYPE.TABLE, 

127 data_frame=pd.DataFrame( 

128 result, 

129 columns=[x[0] for x in cursor.description] 

130 ) 

131 ) 

132 else: 

133 connection.commit() 

134 response = Response(RESPONSE_TYPE.OK) 

135 except Exception as e: 

136 logger.error(f'Error running query: {query} on {self.connection_data["database"]}!') 

137 response = Response( 

138 RESPONSE_TYPE.ERROR, 

139 error_message=str(e) 

140 ) 

141 

142 cursor.close() 

143 if need_to_close is True: 

144 self.disconnect() 

145 

146 return response 

147 

148 def query(self, query: ASTNode) -> StatusResponse: 

149 """ 

150 Receive query as AST (abstract syntax tree) and act upon it somehow. 

151 Args: 

152 query (ASTNode): sql query represented as AST. May be any kind 

153 of query: SELECT, INTSERT, DELETE, etc 

154 Returns: 

155 HandlerResponse 

156 """ 

157 renderer = SqlalchemyRender(FBDialect) 

158 query_str = renderer.get_string(query, with_failback=True) 

159 return self.native_query(query_str) 

160 

161 def get_tables(self) -> StatusResponse: 

162 """ 

163 Return list of entities that will be accessible as tables. 

164 Returns: 

165 HandlerResponse 

166 """ 

167 

168 query = """ 

169 SELECT RDB$RELATION_NAME 

170 FROM RDB$RELATIONS 

171 WHERE (RDB$SYSTEM_FLAG <> 1 OR RDB$SYSTEM_FLAG IS NULL) AND RDB$VIEW_BLR IS NULL 

172 ORDER BY RDB$RELATION_NAME; 

173 """ 

174 result = self.native_query(query) 

175 df = result.data_frame 

176 df[df.columns[0]] = df[df.columns[0]].apply(lambda row: row.strip()) 

177 result.data_frame = df.rename(columns={df.columns[0]: 'table_name'}) 

178 return result 

179 

180 def get_columns(self, table_name: str) -> StatusResponse: 

181 """ 

182 Returns a list of entity columns. 

183 Args: 

184 table_name (str): name of one of tables returned by self.get_tables() 

185 Returns: 

186 HandlerResponse 

187 """ 

188 

189 query = f""" 

190 SELECT 

191 RF.RDB$FIELD_NAME FIELD_NAME, 

192 CASE F.RDB$FIELD_TYPE 

193 WHEN 7 THEN 

194 CASE F.RDB$FIELD_SUB_TYPE 

195 WHEN 0 THEN 'SMALLINT' 

196 WHEN 1 THEN 'NUMERIC(' || F.RDB$FIELD_PRECISION || ', ' || (-F.RDB$FIELD_SCALE) || ')' 

197 WHEN 2 THEN 'DECIMAL' 

198 END 

199 WHEN 8 THEN 

200 CASE F.RDB$FIELD_SUB_TYPE 

201 WHEN 0 THEN 'INTEGER' 

202 WHEN 1 THEN 'NUMERIC(' || F.RDB$FIELD_PRECISION || ', ' || (-F.RDB$FIELD_SCALE) || ')' 

203 WHEN 2 THEN 'DECIMAL' 

204 END 

205 WHEN 9 THEN 'QUAD' 

206 WHEN 10 THEN 'FLOAT' 

207 WHEN 12 THEN 'DATE' 

208 WHEN 13 THEN 'TIME' 

209 WHEN 14 THEN 'CHAR(' || (TRUNC(F.RDB$FIELD_LENGTH / CH.RDB$BYTES_PER_CHARACTER)) || ') ' 

210 WHEN 16 THEN 

211 CASE F.RDB$FIELD_SUB_TYPE 

212 WHEN 0 THEN 'BIGINT' 

213 WHEN 1 THEN 'NUMERIC(' || F.RDB$FIELD_PRECISION || ', ' || (-F.RDB$FIELD_SCALE) || ')' 

214 WHEN 2 THEN 'DECIMAL' 

215 END 

216 WHEN 27 THEN 'DOUBLE' 

217 WHEN 35 THEN 'TIMESTAMP' 

218 WHEN 37 THEN 'VARCHAR(' || (TRUNC(F.RDB$FIELD_LENGTH / CH.RDB$BYTES_PER_CHARACTER)) || ')' 

219 WHEN 40 THEN 'CSTRING' || (TRUNC(F.RDB$FIELD_LENGTH / CH.RDB$BYTES_PER_CHARACTER)) || ')' 

220 WHEN 45 THEN 'BLOB_ID' 

221 WHEN 261 THEN 'BLOB SUB_TYPE ' || F.RDB$FIELD_SUB_TYPE 

222 ELSE 'RDB$FIELD_TYPE: ' || F.RDB$FIELD_TYPE || '?' 

223 END FIELD_TYPE, 

224 IIF(COALESCE(RF.RDB$NULL_FLAG, 0) = 0, NULL, 'NOT NULL') FIELD_NULL, 

225 CH.RDB$CHARACTER_SET_NAME FIELD_CHARSET, 

226 DCO.RDB$COLLATION_NAME FIELD_COLLATION, 

227 COALESCE(RF.RDB$DEFAULT_SOURCE, F.RDB$DEFAULT_SOURCE) FIELD_DEFAULT, 

228 F.RDB$VALIDATION_SOURCE FIELD_CHECK, 

229 RF.RDB$DESCRIPTION FIELD_DESCRIPTION 

230 FROM RDB$RELATION_FIELDS RF 

231 JOIN RDB$FIELDS F ON (F.RDB$FIELD_NAME = RF.RDB$FIELD_SOURCE) 

232 LEFT OUTER JOIN RDB$CHARACTER_SETS CH ON (CH.RDB$CHARACTER_SET_ID = F.RDB$CHARACTER_SET_ID) 

233 LEFT OUTER JOIN RDB$COLLATIONS DCO ON ((DCO.RDB$COLLATION_ID = F.RDB$COLLATION_ID) AND (DCO.RDB$CHARACTER_SET_ID = F.RDB$CHARACTER_SET_ID)) 

234 WHERE (RF.RDB$RELATION_NAME = '{table_name.upper()}') AND (COALESCE(RF.RDB$SYSTEM_FLAG, 0) = 0) 

235 ORDER BY RF.RDB$FIELD_POSITION; 

236 """ 

237 result = self.native_query(query) 

238 df = result.data_frame 

239 result.data_frame = df.rename(columns={'FIELD_NAME': 'column_name', 'FIELD_TYPE': 'data_type'}) 

240 return result