Skip to content

Commit ec6560b

Browse files
committed
SQL: Limit tables in dropdown
1 parent f9cda23 commit ec6560b

File tree

5 files changed

+98
-7
lines changed

5 files changed

+98
-7
lines changed

Orange/data/sql/backend/base.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,37 @@ def list_tables(self, schema=None):
6868
tables.append(TableDesc(name, schema, sql))
6969
return tables
7070

71+
def n_tables_query(self, schema=None) -> str:
72+
"""Return a query to count tables in database.
73+
74+
Parameters
75+
----------
76+
schema : Optional[str]
77+
If set, only tables from schema should be listed
78+
79+
Returns
80+
-------
81+
Query string.
82+
"""
83+
raise NotImplementedError
84+
85+
def n_tables(self, schema=None) -> int:
86+
"""Return number of tables in database.
87+
88+
Parameters
89+
----------
90+
schema : Optional[str]
91+
If set, only tables from given schema will be listed.
92+
93+
Returns
94+
-------
95+
Number of tables in the database.
96+
"""
97+
query = self.n_tables_query(schema)
98+
with self.execute_sql_query(query) as cur:
99+
res = cur.fetchone()
100+
return res[0]
101+
71102
def get_fields(self, table_name):
72103
"""Return a list of field names and metadata in the given table
73104

Orange/data/sql/backend/mssql.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def list_tables_query(self, schema=None):
4343
ORDER BY [TABLE_NAME]
4444
"""
4545

46+
def n_tables_query(self, _=None) -> str:
47+
return "SELECT COUNT(*) FROM information_schema.tables"
48+
4649
def quote_identifier(self, name):
4750
return "[{}]".format(name)
4851

Orange/data/sql/backend/postgres.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ def list_tables_query(self, schema=None):
113113
AND NOT c.relname LIKE '\\_\\_%'
114114
ORDER BY 1,2;""".format(schema_clause)
115115

116+
def n_tables_query(self, schema=None) -> str:
117+
query = "SELECT COUNT(*) FROM information_schema.tables"
118+
schema_clause = " WHERE table_schema = '{}'".format(schema) \
119+
if schema else ""
120+
return query + schema_clause
121+
116122
def create_variable(self, field_name, field_metadata,
117123
type_hints, inspect_table=None):
118124
if field_name in type_hints:

Orange/widgets/data/owsql.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from AnyQt.QtWidgets import QComboBox, QTextEdit, QMessageBox, QApplication, \
2-
QGridLayout
2+
QGridLayout, QLineEdit
33
from AnyQt.QtGui import QCursor
44
from AnyQt.QtCore import Qt
55

@@ -17,6 +17,7 @@
1717
from Orange.widgets.widget import Output, Msg
1818

1919
MAX_DL_LIMIT = 1000000
20+
MAX_TABLES = 1000
2021

2122

2223
def is_postgres(backend):
@@ -126,6 +127,11 @@ def _add_tables_controls(self):
126127
self.tablecombo.setToolTip('table')
127128
self.tablecombo.activated[int].connect(self.select_table)
128129

130+
self.tabletext = QLineEdit(placeholderText='TABLE_NAME')
131+
self.tabletext.setToolTip('table')
132+
self.tabletext.editingFinished.connect(self.select_table)
133+
self.tabletext.setVisible(False)
134+
129135
self.custom_sql = gui.vBox(box)
130136
self.custom_sql.setVisible(self.data_source == self.CUSTOM_SQL)
131137
self.sqltext = QTextEdit(self.custom_sql)
@@ -142,6 +148,7 @@ def _add_tables_controls(self):
142148

143149
form.addWidget(radio_table, 1, 0, Qt.AlignLeft)
144150
form.addWidget(self.tablecombo, 1, 1)
151+
form.addWidget(self.tabletext, 1, 1)
145152
form.addWidget(radio_custom_sql, 2, 0, Qt.AlignLeft)
146153

147154
gui.checkBox(box, self, "guess_values",
@@ -185,9 +192,15 @@ def refresh_tables(self):
185192
return
186193

187194
self.tables.append("Select a table")
188-
self.tables.extend(self.backend.list_tables(self.schema))
189-
index = self.tablecombo.findText(str(self.table))
190-
self.tablecombo.setCurrentIndex(index if index != -1 else 0)
195+
if self.backend.n_tables(self.schema) <= MAX_TABLES:
196+
self.tables.extend(self.backend.list_tables(self.schema))
197+
index = self.tablecombo.findText(str(self.table))
198+
self.tablecombo.setCurrentIndex(index if index != -1 else 0)
199+
self.tablecombo.setVisible(True)
200+
self.tabletext.setVisible(False)
201+
else:
202+
self.tablecombo.setVisible(False)
203+
self.tabletext.setVisible(True)
191204
self.tablecombo.repaint()
192205

193206
# Called on tablecombo selection change:
@@ -204,14 +217,16 @@ def select_table(self):
204217

205218
def get_table(self):
206219
curIdx = self.tablecombo.currentIndex()
207-
if self.data_source == self.TABLE and curIdx <= 0:
220+
if self.data_source == self.TABLE and curIdx <= 0 and \
221+
self.tabletext.text() == "":
208222
if self.database_desc:
209223
self.database_desc["Table"] = "(None)"
210224
self.data_desc_table = None
211225
return None
212226

213227
if self.data_source == self.TABLE:
214-
self.table = self.tables[curIdx]
228+
self.table = self.tables[curIdx] if curIdx > 0 else \
229+
self.tabletext.text()
215230
self.database_desc["Table"] = self.table
216231
if "Query" in self.database_desc:
217232
del self.database_desc["Query"]

Orange/widgets/data/tests/test_owsql.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
from Orange.tests.sql.base import DataBaseTest as dbt
1111

1212

13+
def mock_sqltable(*args, **_):
14+
table = Table(args[1])
15+
table.get_domain = lambda **_: table.domain
16+
table.download_data = lambda *_: 1
17+
return table
18+
19+
1320
class TestOWSqlConnected(WidgetTest, dbt):
1421
def setUpDB(self):
1522
# pylint: disable=attribute-defined-outside-init
@@ -72,6 +79,7 @@ def test_restore_table(self, mock_backends, mock_sqltable):
7279
backend().display_name = "database"
7380
del backend().missing_extension
7481
backend().list_tables.return_value = ["a", "b", "c"]
82+
backend().n_tables.return_value = 3
7583
mock_backends.available_backends.return_value = [backend]
7684
mock_sqltable().approx_len.return_value = 100
7785

@@ -113,27 +121,55 @@ def test_data_source(self, mocked_backends: mock.Mock):
113121
backend = mock.Mock()
114122
backend().display_name = "Dummy Backend"
115123
backend().list_tables.return_value = ["a", "b", "c"]
124+
backend().n_tables.return_value = 3
116125
mocked_backends.available_backends.return_value = [backend]
117126

118127
settings = {"selected_backend": "Dummy Backend",
119128
"host": "host", "port": "port", "database": "DB",
120129
"schema": "", "username": "username",
121-
"password": "password", "data_source": OWSql.TABLE}
130+
"password": "password"}
122131
widget: OWSql = self.create_widget(OWSql, stored_settings=settings)
123132
self.assertEqual(widget.tablecombo.currentText(), "Select a table")
124133
self.assertFalse(widget.tablecombo.isHidden())
134+
self.assertTrue(widget.tabletext.isHidden())
125135
self.assertTrue(widget.custom_sql.isHidden())
126136

127137
widget.controls.data_source.buttons[OWSql.CUSTOM_SQL].click()
128138
self.assertEqual(widget.tablecombo.currentText(), "Select a table")
129139
self.assertFalse(widget.tablecombo.isHidden())
140+
self.assertTrue(widget.tabletext.isHidden())
130141
self.assertFalse(widget.custom_sql.isHidden())
131142

132143
widget.controls.data_source.buttons[OWSql.TABLE].click()
133144
self.assertEqual(widget.tablecombo.currentText(), "Select a table")
134145
self.assertFalse(widget.tablecombo.isHidden())
146+
self.assertTrue(widget.tabletext.isHidden())
135147
self.assertTrue(widget.custom_sql.isHidden())
136148

149+
@mock.patch('Orange.widgets.data.owsql.MAX_TABLES', 2)
150+
@mock.patch('Orange.widgets.data.owsql.SqlTable',
151+
mock.Mock(side_effect=mock_sqltable))
152+
@mock.patch('Orange.widgets.data.owsql.Backend')
153+
def test_table_text(self, mocked_backends: mock.Mock):
154+
backend = mock.Mock()
155+
backend().display_name = "Dummy Backend"
156+
backend().list_tables.return_value = ["iris", "zoo", "titanic"]
157+
backend().n_tables.return_value = 3
158+
mocked_backends.available_backends.return_value = [backend]
159+
160+
settings = {"selected_backend": "Dummy Backend",
161+
"host": "host", "port": "port", "database": "DB",
162+
"schema": "", "username": "username",
163+
"password": "password"}
164+
widget: OWSql = self.create_widget(OWSql, stored_settings=settings)
165+
self.assertTrue(widget.tablecombo.isHidden())
166+
self.assertFalse(widget.tabletext.isHidden())
167+
widget.tabletext.setText("zoo")
168+
widget.select_table()
169+
output = self.get_output(widget.Outputs.data, widget=widget)
170+
self.assertIsInstance(output, Table)
171+
self.assertEqual(len(output), 101)
172+
137173

138174
if __name__ == "__main__":
139175
unittest.main()

0 commit comments

Comments
 (0)