Skip to content

Commit f40f541

Browse files
committed
SQL: Limit tables in dropdown
1 parent 4845830 commit f40f541

File tree

5 files changed

+101
-8
lines changed

5 files changed

+101
-8
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+
if schema:
119+
query += f" WHERE table_schema = '{schema}'"
120+
return query
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: 24 additions & 7 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):
@@ -77,6 +78,7 @@ def __init__(self):
7778
self.backendcombo = None
7879
self.tables = None
7980
self.tablecombo = None
81+
self.tabletext = None
8082
self.sqltext = None
8183
self.custom_sql = None
8284
super().__init__()
@@ -126,6 +128,11 @@ def _add_tables_controls(self):
126128
self.tablecombo.setToolTip('table')
127129
self.tablecombo.activated[int].connect(self.select_table)
128130

131+
self.tabletext = QLineEdit(placeholderText='TABLE_NAME')
132+
self.tabletext.setToolTip('table')
133+
self.tabletext.editingFinished.connect(self.select_table)
134+
self.tabletext.setVisible(False)
135+
129136
self.custom_sql = gui.vBox(box)
130137
self.custom_sql.setVisible(self.data_source == self.CUSTOM_SQL)
131138
self.sqltext = QTextEdit(self.custom_sql)
@@ -142,6 +149,7 @@ def _add_tables_controls(self):
142149

143150
form.addWidget(radio_table, 1, 0, Qt.AlignLeft)
144151
form.addWidget(self.tablecombo, 1, 1)
152+
form.addWidget(self.tabletext, 1, 1)
145153
form.addWidget(radio_custom_sql, 2, 0, Qt.AlignLeft)
146154

147155
gui.checkBox(box, self, "guess_values",
@@ -185,9 +193,15 @@ def refresh_tables(self):
185193
return
186194

187195
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)
196+
if self.backend.n_tables(self.schema) <= MAX_TABLES:
197+
self.tables.extend(self.backend.list_tables(self.schema))
198+
index = self.tablecombo.findText(str(self.table))
199+
self.tablecombo.setCurrentIndex(index if index != -1 else 0)
200+
self.tablecombo.setVisible(True)
201+
self.tabletext.setVisible(False)
202+
else:
203+
self.tablecombo.setVisible(False)
204+
self.tabletext.setVisible(True)
191205
self.tablecombo.repaint()
192206

193207
# Called on tablecombo selection change:
@@ -205,14 +219,16 @@ def select_table(self):
205219

206220
def get_table(self):
207221
curIdx = self.tablecombo.currentIndex()
208-
if self.data_source == self.TABLE and curIdx <= 0:
222+
if self.data_source == self.TABLE and curIdx <= 0 and \
223+
self.tabletext.text() == "":
209224
if self.database_desc:
210225
self.database_desc["Table"] = "(None)"
211226
self.data_desc_table = None
212227
return None
213228

214229
if self.data_source == self.TABLE:
215-
self.table = self.tables[curIdx]
230+
self.table = self.tables[curIdx] if curIdx > 0 else \
231+
self.tabletext.text()
216232
self.database_desc["Table"] = self.table
217233
if "Query" in self.database_desc:
218234
del self.database_desc["Query"]
@@ -292,7 +308,8 @@ def get_table(self):
292308
confirm.setIcon(QMessageBox.Warning)
293309
confirm.setText("Data appears to be big. Do you really "
294310
"want to download it to local memory?\n"
295-
"Table length: {:,}. Limit {:,}".format(table.approx_len(), MAX_DL_LIMIT))
311+
"Table length: {:,}. Limit {:,}".format(
312+
table.approx_len(), MAX_DL_LIMIT))
296313

297314
if table.approx_len() <= MAX_DL_LIMIT:
298315
confirm.addButton("Yes", QMessageBox.YesRole)

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

@@ -116,27 +124,55 @@ def test_data_source(self, mocked_backends: mock.Mock):
116124
backend = mock.Mock()
117125
backend().display_name = "Dummy Backend"
118126
backend().list_tables.return_value = ["a", "b", "c"]
127+
backend().n_tables.return_value = 3
119128
mocked_backends.available_backends.return_value = [backend]
120129

121130
settings = {"selected_backend": "Dummy Backend",
122131
"host": "host", "port": "port", "database": "DB",
123132
"schema": "", "username": "username",
124-
"password": "password", "data_source": OWSql.TABLE}
133+
"password": "password"}
125134
widget: OWSql = self.create_widget(OWSql, stored_settings=settings)
126135
self.assertEqual(widget.tablecombo.currentText(), "Select a table")
127136
self.assertFalse(widget.tablecombo.isHidden())
137+
self.assertTrue(widget.tabletext.isHidden())
128138
self.assertTrue(widget.custom_sql.isHidden())
129139

130140
widget.controls.data_source.buttons[OWSql.CUSTOM_SQL].click()
131141
self.assertEqual(widget.tablecombo.currentText(), "Select a table")
132142
self.assertFalse(widget.tablecombo.isHidden())
143+
self.assertTrue(widget.tabletext.isHidden())
133144
self.assertFalse(widget.custom_sql.isHidden())
134145

135146
widget.controls.data_source.buttons[OWSql.TABLE].click()
136147
self.assertEqual(widget.tablecombo.currentText(), "Select a table")
137148
self.assertFalse(widget.tablecombo.isHidden())
149+
self.assertTrue(widget.tabletext.isHidden())
138150
self.assertTrue(widget.custom_sql.isHidden())
139151

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

141177
if __name__ == "__main__":
142178
unittest.main()

0 commit comments

Comments
 (0)