Skip to content

Commit b1d65e0

Browse files
committed
SQL: Limit tables in dropdown
1 parent eb4d8ed commit b1d65e0

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:
@@ -204,14 +218,16 @@ def select_table(self):
204218

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

213228
if self.data_source == self.TABLE:
214-
self.table = self.tables[curIdx]
229+
self.table = self.tables[curIdx] if curIdx > 0 else \
230+
self.tabletext.text()
215231
self.database_desc["Table"] = self.table
216232
if "Query" in self.database_desc:
217233
del self.database_desc["Query"]
@@ -291,7 +307,8 @@ def get_table(self):
291307
confirm.setIcon(QMessageBox.Warning)
292308
confirm.setText("Data appears to be big. Do you really "
293309
"want to download it to local memory?\n"
294-
"Table length: {:,}. Limit {:,}".format(table.approx_len(), MAX_DL_LIMIT))
310+
"Table length: {:,}. Limit {:,}".format(
311+
table.approx_len(), MAX_DL_LIMIT))
295312

296313
if table.approx_len() <= MAX_DL_LIMIT:
297314
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

@@ -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)