Skip to content

Commit 9e35194

Browse files
authored
Merge pull request #7214 from VesnaT/owsql_count
[ENH] SQL: Limit tables in dropdown
2 parents 23e0ad6 + 7ae2042 commit 9e35194

File tree

6 files changed

+239
-122
lines changed

6 files changed

+239
-122
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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Psycopg2Backend(Backend):
2121

2222
display_name = "PostgreSQL"
2323
connection_pool = None
24-
auto_create_extensions = True
24+
auto_create_extensions = False
2525

2626
def __init__(self, connection_params):
2727
super().__init__(connection_params)
@@ -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: 88 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from AnyQt.QtWidgets import QComboBox, QTextEdit, QMessageBox, QApplication
1+
from AnyQt.QtWidgets import QComboBox, QTextEdit, QMessageBox, QApplication, \
2+
QGridLayout, QLineEdit
23
from AnyQt.QtGui import QCursor
34
from AnyQt.QtCore import Qt
45

6+
from orangewidget.utils.combobox import ComboBoxSearch
7+
58
from Orange.data import Table
69
from Orange.data.sql.backend import Backend
710
from Orange.data.sql.backend.base import BackendError
@@ -14,6 +17,7 @@
1417
from Orange.widgets.widget import Output, Msg
1518

1619
MAX_DL_LIMIT = 1000000
20+
MAX_TABLES = 1000
1721

1822

1923
def is_postgres(backend):
@@ -52,21 +56,19 @@ class Outputs:
5256

5357
buttons_area_orientation = None
5458

59+
TABLE, CUSTOM_SQL = range(2)
5560
selected_backend = Setting(None)
61+
data_source = Setting(TABLE)
5662
table = Setting(None)
5763
sql = Setting("")
5864
guess_values = Setting(True)
59-
download = Setting(False)
6065

6166
materialize = Setting(False)
6267
materialize_table_name = Setting("")
6368

6469
class Information(OWBaseSql.Information):
6570
data_sampled = Msg("Data description was generated from a sample.")
6671

67-
class Warning(OWBaseSql.Warning):
68-
missing_extension = Msg("Database is missing extensions: {}")
69-
7072
class Error(OWBaseSql.Error):
7173
no_backends = Msg("Please install a backend to use this widget.")
7274

@@ -76,9 +78,9 @@ def __init__(self):
7678
self.backendcombo = None
7779
self.tables = None
7880
self.tablecombo = None
81+
self.tabletext = None
7982
self.sqltext = None
8083
self.custom_sql = None
81-
self.downloadcb = None
8284
super().__init__()
8385

8486
def _setup_gui(self):
@@ -106,21 +108,33 @@ def __backend_changed(self):
106108
self.selected_backend = backend.display_name if backend else None
107109

108110
def _add_tables_controls(self):
109-
vbox = gui.vBox(self.controlArea, "Tables")
110-
box = gui.vBox(vbox)
111+
box = gui.vBox(self.controlArea, 'Data Selection')
112+
form = QGridLayout()
113+
radio_buttons = gui.radioButtons(
114+
box, self, 'data_source', orientation=form,
115+
callback=self.__on_data_source_changed)
116+
radio_table = gui.appendRadioButton(
117+
radio_buttons, 'Table:', addToLayout=False)
118+
radio_custom_sql = gui.appendRadioButton(
119+
radio_buttons, 'Custom SQL:', addToLayout=False)
120+
111121
self.tables = TableModel()
112122

113-
self.tablecombo = QComboBox(
123+
self.tablecombo = ComboBoxSearch(
114124
minimumContentsLength=35,
115125
sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon
116126
)
117127
self.tablecombo.setModel(self.tables)
118128
self.tablecombo.setToolTip('table')
119129
self.tablecombo.activated[int].connect(self.select_table)
120-
box.layout().addWidget(self.tablecombo)
130+
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)
121135

122136
self.custom_sql = gui.vBox(box)
123-
self.custom_sql.setVisible(False)
137+
self.custom_sql.setVisible(self.data_source == self.CUSTOM_SQL)
124138
self.sqltext = QTextEdit(self.custom_sql)
125139
self.sqltext.setPlainText(self.sql)
126140
self.custom_sql.layout().addWidget(self.sqltext)
@@ -133,15 +147,18 @@ def _add_tables_controls(self):
133147

134148
gui.button(self.custom_sql, self, 'Execute', callback=self.open_table)
135149

136-
box.layout().addWidget(self.custom_sql)
150+
form.addWidget(radio_table, 1, 0, Qt.AlignLeft)
151+
form.addWidget(self.tablecombo, 1, 1)
152+
form.addWidget(self.tabletext, 1, 1)
153+
form.addWidget(radio_custom_sql, 2, 0, Qt.AlignLeft)
137154

138155
gui.checkBox(box, self, "guess_values",
139156
"Auto-discover categorical variables",
140157
callback=self.open_table)
141158

142-
self.downloadcb = gui.checkBox(box, self, "download",
143-
"Download data to local memory",
144-
callback=self.open_table)
159+
def __on_data_source_changed(self):
160+
self.custom_sql.setVisible(self.data_source == self.CUSTOM_SQL)
161+
self.select_table()
145162

146163
def highlight_error(self, text=""):
147164
err = ['', 'QLineEdit {border: 2px solid red;}']
@@ -155,14 +172,6 @@ def get_backend(self):
155172
return self.backends[self.backendcombo.currentIndex()]
156173

157174
def on_connection_success(self):
158-
if getattr(self.backend, 'missing_extension', False):
159-
self.Warning.missing_extension(
160-
", ".join(self.backend.missing_extension))
161-
self.download = True
162-
self.downloadcb.setEnabled(False)
163-
if not is_postgres(self.backend):
164-
self.download = True
165-
self.downloadcb.setEnabled(False)
166175
super().on_connection_success()
167176
self.refresh_tables()
168177
self.select_table()
@@ -173,8 +182,6 @@ def on_connection_error(self, err):
173182

174183
def clear(self):
175184
super().clear()
176-
self.Warning.missing_extension.clear()
177-
self.downloadcb.setEnabled(True)
178185
self.highlight_error()
179186
self.tablecombo.clear()
180187
self.tablecombo.repaint()
@@ -186,37 +193,44 @@ def refresh_tables(self):
186193
return
187194

188195
self.tables.append("Select a table")
189-
self.tables.append("Custom SQL")
190-
self.tables.extend(self.backend.list_tables(self.schema))
191-
index = self.tablecombo.findText(str(self.table))
192-
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)
193205
self.tablecombo.repaint()
194206

195207
# Called on tablecombo selection change:
196208
def select_table(self):
197-
curIdx = self.tablecombo.currentIndex()
198-
if self.tablecombo.itemText(curIdx) != "Custom SQL":
199-
self.custom_sql.setVisible(False)
209+
if self.data_source == self.TABLE:
200210
return self.open_table()
201211
else:
202-
self.custom_sql.setVisible(True)
203212
self.data_desc_table = None
204-
self.database_desc["Table"] = "(None)"
213+
if self.database_desc:
214+
self.database_desc["Table"] = "(None)"
205215
self.table = None
206216
if len(str(self.sql)) > 14:
207217
return self.open_table()
208218
return None
209219

210220
def get_table(self):
221+
if self.backend is None:
222+
return None
211223
curIdx = self.tablecombo.currentIndex()
212-
if curIdx <= 0:
224+
if self.data_source == self.TABLE and curIdx <= 0 and \
225+
self.tabletext.text() == "":
213226
if self.database_desc:
214227
self.database_desc["Table"] = "(None)"
215228
self.data_desc_table = None
216229
return None
217230

218-
if self.tablecombo.itemText(curIdx) != "Custom SQL":
219-
self.table = self.tables[self.tablecombo.currentIndex()]
231+
if self.data_source == self.TABLE:
232+
self.table = self.tables[curIdx] if curIdx > 0 else \
233+
self.tabletext.text()
220234
self.database_desc["Table"] = self.table
221235
if "Query" in self.database_desc:
222236
del self.database_desc["Query"]
@@ -290,45 +304,45 @@ def get_table(self):
290304
QApplication.restoreOverrideCursor()
291305
table.domain = domain
292306

293-
if self.download:
294-
if table.approx_len() > AUTO_DL_LIMIT:
295-
if is_postgres(self.backend):
296-
confirm = QMessageBox(self)
297-
confirm.setIcon(QMessageBox.Warning)
298-
confirm.setText("Data appears to be big. Do you really "
299-
"want to download it to local memory?\n"
300-
"Table length: {:,}. Limit {:,}".format(table.approx_len(), MAX_DL_LIMIT))
301-
302-
if table.approx_len() <= MAX_DL_LIMIT:
303-
confirm.addButton("Yes", QMessageBox.YesRole)
304-
no_button = confirm.addButton("No", QMessageBox.NoRole)
305-
sample_button = confirm.addButton("Yes, a sample",
306-
QMessageBox.YesRole)
307-
confirm.exec()
308-
if confirm.clickedButton() == no_button:
309-
return None
310-
elif confirm.clickedButton() == sample_button:
311-
table = table.sample_percentage(
312-
AUTO_DL_LIMIT / table.approx_len() * 100)
307+
if table.approx_len() > AUTO_DL_LIMIT:
308+
if is_postgres(self.backend):
309+
confirm = QMessageBox(self)
310+
confirm.setIcon(QMessageBox.Warning)
311+
confirm.setText("Data appears to be big. Do you really "
312+
"want to download it to local memory?\n"
313+
"Table length: {:,}. Limit {:,}".format(
314+
table.approx_len(), MAX_DL_LIMIT))
315+
316+
if table.approx_len() <= MAX_DL_LIMIT:
317+
confirm.addButton("Yes", QMessageBox.YesRole)
318+
no_button = confirm.addButton("No", QMessageBox.NoRole)
319+
sample_button = confirm.addButton("Yes, a sample",
320+
QMessageBox.YesRole)
321+
confirm.exec()
322+
if confirm.clickedButton() == no_button:
323+
return None
324+
elif confirm.clickedButton() == sample_button:
325+
table = table.sample_percentage(
326+
AUTO_DL_LIMIT / table.approx_len() * 100)
327+
else:
328+
if table.approx_len() > MAX_DL_LIMIT:
329+
QMessageBox.warning(
330+
self, 'Warning',
331+
"Data is too big to download.\n"
332+
"Table length: {:,}. Limit {:,}".format(table.approx_len(), MAX_DL_LIMIT)
333+
)
334+
return None
313335
else:
314-
if table.approx_len() > MAX_DL_LIMIT:
315-
QMessageBox.warning(
316-
self, 'Warning',
317-
"Data is too big to download.\n"
318-
"Table length: {:,}. Limit {:,}".format(table.approx_len(), MAX_DL_LIMIT)
319-
)
336+
confirm = QMessageBox.question(
337+
self, 'Question',
338+
"Data appears to be big. Do you really "
339+
"want to download it to local memory?",
340+
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
341+
if confirm == QMessageBox.No:
320342
return None
321-
else:
322-
confirm = QMessageBox.question(
323-
self, 'Question',
324-
"Data appears to be big. Do you really "
325-
"want to download it to local memory?",
326-
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
327-
if confirm == QMessageBox.No:
328-
return None
329-
330-
table.download_data(MAX_DL_LIMIT)
331-
table = Table(table)
343+
344+
table.download_data(MAX_DL_LIMIT)
345+
table = Table(table)
332346

333347
return table
334348

0 commit comments

Comments
 (0)