Skip to content

Commit 3f3265e

Browse files
committed
Fix Oracle adapter cursor leak
1 parent 2ee6d0b commit 3f3265e

File tree

7 files changed

+326
-120
lines changed

7 files changed

+326
-120
lines changed

sqlit/core/keymap.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,6 @@ def _build_action_keys(self) -> list[ActionKeyDef]:
376376
ActionKeyDef("z", "toggle_result_section", "results"),
377377
ActionKeyDef("escape", "results_filter_close", "results_filter"),
378378
ActionKeyDef("enter", "results_filter_accept", "results_filter"),
379-
ActionKeyDef("n", "results_filter_next", "results_filter"),
380-
ActionKeyDef("N", "results_filter_prev", "results_filter"),
381379
# Value view
382380
ActionKeyDef("q", "close_value_view", "value_view"),
383381
ActionKeyDef("escape", "close_value_view", "value_view"),

sqlit/domains/connections/providers/oracle/adapter.py

Lines changed: 161 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -100,110 +100,144 @@ def get_databases(self, conn: Any) -> list[str]:
100100
def get_tables(self, conn: Any, database: str | None = None) -> list[TableInfo]:
101101
"""Get list of tables from Oracle. Returns (schema, name) with empty schema."""
102102
cursor = conn.cursor()
103-
cursor.execute("SELECT table_name FROM user_tables ORDER BY table_name")
104-
# user_tables returns only current user's tables, so no schema prefix needed
105-
return [("", row[0]) for row in cursor.fetchall()]
103+
try:
104+
cursor.execute("SELECT table_name FROM user_tables ORDER BY table_name")
105+
# user_tables returns only current user's tables, so no schema prefix needed
106+
return [("", row[0]) for row in cursor.fetchall()]
107+
finally:
108+
cursor.close()
106109

107110
def get_views(self, conn: Any, database: str | None = None) -> list[TableInfo]:
108111
"""Get list of views from Oracle. Returns (schema, name) with empty schema."""
109112
cursor = conn.cursor()
110-
cursor.execute("SELECT view_name FROM user_views ORDER BY view_name")
111-
return [("", row[0]) for row in cursor.fetchall()]
113+
try:
114+
cursor.execute("SELECT view_name FROM user_views ORDER BY view_name")
115+
return [("", row[0]) for row in cursor.fetchall()]
116+
finally:
117+
cursor.close()
112118

113119
def get_columns(
114120
self, conn: Any, table: str, database: str | None = None, schema: str | None = None
115121
) -> list[ColumnInfo]:
116122
"""Get columns for a table from Oracle. Schema parameter is ignored."""
117-
cursor = conn.cursor()
118-
119123
# Get primary key columns
120-
cursor.execute(
121-
"SELECT cols.column_name "
122-
"FROM user_constraints cons "
123-
"JOIN user_cons_columns cols ON cons.constraint_name = cols.constraint_name "
124-
"WHERE cons.constraint_type = 'P' AND cons.table_name = :1",
125-
(table.upper(),),
126-
)
127-
pk_columns = {row[0] for row in cursor.fetchall()}
124+
pk_cursor = conn.cursor()
125+
try:
126+
pk_cursor.execute(
127+
"SELECT cols.column_name "
128+
"FROM user_constraints cons "
129+
"JOIN user_cons_columns cols ON cons.constraint_name = cols.constraint_name "
130+
"WHERE cons.constraint_type = 'P' AND cons.table_name = :1",
131+
(table.upper(),),
132+
)
133+
pk_columns = {row[0] for row in pk_cursor.fetchall()}
134+
finally:
135+
pk_cursor.close()
128136

129137
# Get all columns
130-
cursor.execute(
131-
"SELECT column_name, data_type FROM user_tab_columns " "WHERE table_name = :1 ORDER BY column_id",
132-
(table.upper(),),
133-
)
134-
return [ColumnInfo(name=row[0], data_type=row[1], is_primary_key=row[0] in pk_columns) for row in cursor.fetchall()]
138+
cursor = conn.cursor()
139+
try:
140+
cursor.execute(
141+
"SELECT column_name, data_type FROM user_tab_columns " "WHERE table_name = :1 ORDER BY column_id",
142+
(table.upper(),),
143+
)
144+
return [ColumnInfo(name=row[0], data_type=row[1], is_primary_key=row[0] in pk_columns) for row in cursor.fetchall()]
145+
finally:
146+
cursor.close()
135147

136148
def get_procedures(self, conn: Any, database: str | None = None) -> list[str]:
137149
"""Get stored procedures from Oracle."""
138150
cursor = conn.cursor()
139-
cursor.execute(
140-
"SELECT object_name FROM user_procedures " "WHERE object_type = 'PROCEDURE' ORDER BY object_name"
141-
)
142-
return [row[0] for row in cursor.fetchall()]
151+
try:
152+
cursor.execute(
153+
"SELECT object_name FROM user_procedures " "WHERE object_type = 'PROCEDURE' ORDER BY object_name"
154+
)
155+
return [row[0] for row in cursor.fetchall()]
156+
finally:
157+
cursor.close()
143158

144159
def get_indexes(self, conn: Any, database: str | None = None) -> list[IndexInfo]:
145160
"""Get indexes from Oracle."""
146161
cursor = conn.cursor()
147-
cursor.execute(
148-
"SELECT index_name, table_name, uniqueness "
149-
"FROM user_indexes "
150-
"WHERE index_type != 'LOB' "
151-
"ORDER BY table_name, index_name"
152-
)
153-
return [
154-
IndexInfo(name=row[0], table_name=row[1], is_unique=row[2] == "UNIQUE")
155-
for row in cursor.fetchall()
156-
]
162+
try:
163+
cursor.execute(
164+
"SELECT index_name, table_name, uniqueness "
165+
"FROM user_indexes "
166+
"WHERE index_type != 'LOB' "
167+
"ORDER BY table_name, index_name"
168+
)
169+
return [
170+
IndexInfo(name=row[0], table_name=row[1], is_unique=row[2] == "UNIQUE")
171+
for row in cursor.fetchall()
172+
]
173+
finally:
174+
cursor.close()
157175

158176
def get_triggers(self, conn: Any, database: str | None = None) -> list[TriggerInfo]:
159177
"""Get triggers from Oracle."""
160178
cursor = conn.cursor()
161-
cursor.execute(
162-
"SELECT trigger_name, table_name "
163-
"FROM user_triggers "
164-
"WHERE base_object_type = 'TABLE' "
165-
"ORDER BY table_name, trigger_name"
166-
)
167-
return [TriggerInfo(name=row[0], table_name=row[1] or "") for row in cursor.fetchall()]
179+
try:
180+
cursor.execute(
181+
"SELECT trigger_name, table_name "
182+
"FROM user_triggers "
183+
"WHERE base_object_type = 'TABLE' "
184+
"ORDER BY table_name, trigger_name"
185+
)
186+
return [TriggerInfo(name=row[0], table_name=row[1] or "") for row in cursor.fetchall()]
187+
finally:
188+
cursor.close()
168189

169190
def get_sequences(self, conn: Any, database: str | None = None) -> list[SequenceInfo]:
170191
"""Get sequences from Oracle."""
171192
cursor = conn.cursor()
172-
cursor.execute("SELECT sequence_name FROM user_sequences ORDER BY sequence_name")
173-
return [SequenceInfo(name=row[0]) for row in cursor.fetchall()]
193+
try:
194+
cursor.execute("SELECT sequence_name FROM user_sequences ORDER BY sequence_name")
195+
return [SequenceInfo(name=row[0]) for row in cursor.fetchall()]
196+
finally:
197+
cursor.close()
174198

175199
def get_index_definition(
176200
self, conn: Any, index_name: str, table_name: str, database: str | None = None
177201
) -> dict[str, Any]:
178202
"""Get detailed information about an Oracle index."""
179-
cursor = conn.cursor()
180203
# Get index info
181-
cursor.execute(
182-
"SELECT uniqueness, index_type FROM user_indexes WHERE index_name = :1",
183-
(index_name.upper(),),
184-
)
185-
row = cursor.fetchone()
186-
is_unique = row[0] == "UNIQUE" if row else False
187-
index_type = row[1] if row else "NORMAL"
204+
cursor = conn.cursor()
205+
try:
206+
cursor.execute(
207+
"SELECT uniqueness, index_type FROM user_indexes WHERE index_name = :1",
208+
(index_name.upper(),),
209+
)
210+
row = cursor.fetchone()
211+
is_unique = row[0] == "UNIQUE" if row else False
212+
index_type = row[1] if row else "NORMAL"
213+
finally:
214+
cursor.close()
188215

189216
# Get index columns
190-
cursor.execute(
191-
"SELECT column_name FROM user_ind_columns "
192-
"WHERE index_name = :1 ORDER BY column_position",
193-
(index_name.upper(),),
194-
)
195-
columns = [row[0] for row in cursor.fetchall()]
217+
col_cursor = conn.cursor()
218+
try:
219+
col_cursor.execute(
220+
"SELECT column_name FROM user_ind_columns "
221+
"WHERE index_name = :1 ORDER BY column_position",
222+
(index_name.upper(),),
223+
)
224+
columns = [row[0] for row in col_cursor.fetchall()]
225+
finally:
226+
col_cursor.close()
196227

197228
# Try to get DDL
229+
ddl_cursor = conn.cursor()
198230
try:
199-
cursor.execute(
231+
ddl_cursor.execute(
200232
"SELECT DBMS_METADATA.GET_DDL('INDEX', :1) FROM dual",
201233
(index_name.upper(),),
202234
)
203-
ddl_row = cursor.fetchone()
235+
ddl_row = ddl_cursor.fetchone()
204236
definition = str(ddl_row[0]) if ddl_row else None
205237
except Exception:
206238
definition = f"CREATE {'UNIQUE ' if is_unique else ''}INDEX {index_name} ON {table_name} ({', '.join(columns)})"
239+
finally:
240+
ddl_cursor.close()
207241

208242
return {
209243
"name": index_name,
@@ -219,58 +253,64 @@ def get_trigger_definition(
219253
) -> dict[str, Any]:
220254
"""Get detailed information about an Oracle trigger."""
221255
cursor = conn.cursor()
222-
cursor.execute(
223-
"SELECT trigger_type, triggering_event, trigger_body "
224-
"FROM user_triggers WHERE trigger_name = :1",
225-
(trigger_name.upper(),),
226-
)
227-
row = cursor.fetchone()
228-
if row:
229-
# trigger_type is like "BEFORE EACH ROW" or "AFTER STATEMENT"
230-
timing = row[0].split()[0] if row[0] else None
256+
try:
257+
cursor.execute(
258+
"SELECT trigger_type, triggering_event, trigger_body "
259+
"FROM user_triggers WHERE trigger_name = :1",
260+
(trigger_name.upper(),),
261+
)
262+
row = cursor.fetchone()
263+
if row:
264+
# trigger_type is like "BEFORE EACH ROW" or "AFTER STATEMENT"
265+
timing = row[0].split()[0] if row[0] else None
266+
return {
267+
"name": trigger_name,
268+
"table_name": table_name,
269+
"timing": timing,
270+
"event": row[1],
271+
"definition": row[2],
272+
}
231273
return {
232274
"name": trigger_name,
233275
"table_name": table_name,
234-
"timing": timing,
235-
"event": row[1],
236-
"definition": row[2],
276+
"timing": None,
277+
"event": None,
278+
"definition": None,
237279
}
238-
return {
239-
"name": trigger_name,
240-
"table_name": table_name,
241-
"timing": None,
242-
"event": None,
243-
"definition": None,
244-
}
280+
finally:
281+
cursor.close()
245282

246283
def get_sequence_definition(
247284
self, conn: Any, sequence_name: str, database: str | None = None
248285
) -> dict[str, Any]:
249286
"""Get detailed information about an Oracle sequence."""
250287
cursor = conn.cursor()
251-
cursor.execute(
252-
"SELECT min_value, max_value, increment_by, cycle_flag, last_number "
253-
"FROM user_sequences WHERE sequence_name = :1",
254-
(sequence_name.upper(),),
255-
)
256-
row = cursor.fetchone()
257-
if row:
288+
try:
289+
cursor.execute(
290+
"SELECT min_value, max_value, increment_by, cycle_flag, last_number "
291+
"FROM user_sequences WHERE sequence_name = :1",
292+
(sequence_name.upper(),),
293+
)
294+
row = cursor.fetchone()
295+
if row:
296+
return {
297+
"name": sequence_name,
298+
"start_value": row[4], # last_number approximates current position
299+
"increment": row[2],
300+
"min_value": row[0],
301+
"max_value": row[1],
302+
"cycle": row[3] == "Y",
303+
}
258304
return {
259305
"name": sequence_name,
260-
"start_value": row[4], # last_number approximates current position
261-
"increment": row[2],
262-
"min_value": row[0],
263-
"max_value": row[1],
264-
"cycle": row[3] == "Y",
306+
"start_value": None,
307+
"increment": None,
308+
"min_value": None,
309+
"max_value": None,
310+
"cycle": None,
265311
}
266-
return {
267-
"name": sequence_name,
268-
"start_value": None,
269-
"increment": None,
270-
"min_value": None,
271-
"max_value": None,
272-
"cycle": None,
273-
}
312+
finally:
313+
cursor.close()
274314

275315
def quote_identifier(self, name: str) -> str:
276316
"""Quote identifier using double quotes for Oracle.
@@ -287,24 +327,30 @@ def build_select_query(self, table: str, limit: int, database: str | None = None
287327
def execute_query(self, conn: Any, query: str, max_rows: int | None = None) -> tuple[list[str], list[tuple], bool]:
288328
"""Execute a query on Oracle with optional row limit."""
289329
cursor = conn.cursor()
290-
cursor.execute(query)
291-
if cursor.description:
292-
columns = [col[0] for col in cursor.description]
293-
if max_rows is not None:
294-
rows = cursor.fetchmany(max_rows + 1)
295-
truncated = len(rows) > max_rows
296-
if truncated:
297-
rows = rows[:max_rows]
298-
else:
299-
rows = cursor.fetchall()
300-
truncated = False
301-
return columns, [tuple(row) for row in rows], truncated
302-
return [], [], False
330+
try:
331+
cursor.execute(query)
332+
if cursor.description:
333+
columns = [col[0] for col in cursor.description]
334+
if max_rows is not None:
335+
rows = cursor.fetchmany(max_rows + 1)
336+
truncated = len(rows) > max_rows
337+
if truncated:
338+
rows = rows[:max_rows]
339+
else:
340+
rows = cursor.fetchall()
341+
truncated = False
342+
return columns, [tuple(row) for row in rows], truncated
343+
return [], [], False
344+
finally:
345+
cursor.close()
303346

304347
def execute_non_query(self, conn: Any, query: str) -> int:
305348
"""Execute a non-query on Oracle."""
306349
cursor = conn.cursor()
307-
cursor.execute(query)
308-
rowcount = int(cursor.rowcount)
309-
conn.commit()
310-
return rowcount
350+
try:
351+
cursor.execute(query)
352+
rowcount = int(cursor.rowcount)
353+
conn.commit()
354+
return rowcount
355+
finally:
356+
cursor.close()

sqlit/domains/results/state/results_filter_active.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ class ResultsFilterActiveState(BlockingState):
1414
def _setup_actions(self) -> None:
1515
self.allows("results_filter_close", help="Close filter", help_key="esc")
1616
self.allows("results_filter_accept", help="Select row", help_key="enter")
17-
self.allows("results_filter_next", help="Next match", help_key="n/j")
18-
self.allows("results_filter_prev", help="Previous match", help_key="N/k")
1917
self.allows("quit")
2018

2119
def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding], list[DisplayBinding]]:

sqlit/domains/shell/state/machine.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ def binding(key: str, desc: str, indent: int = 4) -> str:
252252
lines.append(binding("<enter>", "Apply filter"))
253253
lines.append(binding("<esc>", "Close filter"))
254254
lines.append(binding("~prefix", "Fuzzy match mode"))
255-
lines.append(binding("n/N", "Next/prev match (Results)"))
256255
lines.append("")
257256

258257
# ═══════════════════════════════════════════════════════════════════

tests/connections/providers/__init__.py

Whitespace-only changes.

tests/connections/providers/oracle/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)