sqlshell 0.2.2__py3-none-any.whl → 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlshell might be problematic. Click here for more details.
- sqlshell/README.md +5 -1
- sqlshell/__init__.py +1 -1
- sqlshell/create_test_data.py +29 -0
- sqlshell/main.py +214 -3
- sqlshell/table_list.py +90 -1
- sqlshell/ui/filter_header.py +14 -0
- sqlshell/utils/profile_column.py +1099 -0
- sqlshell/utils/profile_distributions.py +613 -0
- sqlshell/utils/profile_foreign_keys.py +455 -0
- sqlshell-0.2.3.dist-info/METADATA +281 -0
- {sqlshell-0.2.2.dist-info → sqlshell-0.2.3.dist-info}/RECORD +14 -11
- {sqlshell-0.2.2.dist-info → sqlshell-0.2.3.dist-info}/WHEEL +1 -1
- sqlshell-0.2.2.dist-info/METADATA +0 -198
- {sqlshell-0.2.2.dist-info → sqlshell-0.2.3.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.2.2.dist-info → sqlshell-0.2.3.dist-info}/top_level.txt +0 -0
sqlshell/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
# SQLShell
|
|
1
|
+
# SQLShell - DEPRECATED README
|
|
2
|
+
|
|
3
|
+
**NOTE: This README is deprecated. Please refer to the main README.md file in the root directory of the repository for the most up-to-date information.**
|
|
2
4
|
|
|
3
5
|
A powerful SQL shell with GUI interface for data analysis. SQLShell provides an intuitive interface for working with various data formats (CSV, Excel, Parquet) using SQL queries powered by DuckDB.
|
|
4
6
|
|
|
@@ -12,6 +14,7 @@ A powerful SQL shell with GUI interface for data analysis. SQLShell provides an
|
|
|
12
14
|
- Table preview functionality
|
|
13
15
|
- Built-in test data generation
|
|
14
16
|
- Support for multiple concurrent table views
|
|
17
|
+
- "Explain Column" feature for analyzing relationships between data columns
|
|
15
18
|
|
|
16
19
|
## Installation
|
|
17
20
|
|
|
@@ -45,6 +48,7 @@ This will open the GUI interface where you can:
|
|
|
45
48
|
3. Execute queries using the "Execute" button or Ctrl+Enter
|
|
46
49
|
4. View results in the table view below
|
|
47
50
|
5. Load sample test data using the "Test" button
|
|
51
|
+
6. Right-click on column headers in the results to access features like sorting, filtering, and the "Explain Column" analysis tool
|
|
48
52
|
|
|
49
53
|
## Requirements
|
|
50
54
|
|
sqlshell/__init__.py
CHANGED
sqlshell/create_test_data.py
CHANGED
|
@@ -10,6 +10,35 @@ np.random.seed(42)
|
|
|
10
10
|
OUTPUT_DIR = 'test_data'
|
|
11
11
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
12
12
|
|
|
13
|
+
def create_california_housing_data(output_file='california_housing_data.parquet'):
|
|
14
|
+
"""Use the real world california housing dataset"""
|
|
15
|
+
# Load the dataset
|
|
16
|
+
df = pd.read_csv('https://raw.githubusercontent.com/ageron/handson-ml/master/datasets/housing/housing.csv')
|
|
17
|
+
|
|
18
|
+
# Save to Parquet
|
|
19
|
+
df.to_parquet(output_file)
|
|
20
|
+
return df
|
|
21
|
+
|
|
22
|
+
def create_large_customer_data(num_customers=1_000_000, chunk_size=100_000, output_file='large_customer_data.parquet'):
|
|
23
|
+
"""Create a large customer dataset """
|
|
24
|
+
# Generate customer data
|
|
25
|
+
data = {
|
|
26
|
+
'CustomerID': range(1, num_customers + 1),
|
|
27
|
+
'FirstName': [f'Customer{i}' for i in range(1, num_customers + 1)],
|
|
28
|
+
'LastName': [f'Lastname{i}' for i in range(1, num_customers + 1)],
|
|
29
|
+
'Email': [f'customer{i}@example.com' for i in range(1, num_customers + 1)],
|
|
30
|
+
'JoinDate': [datetime.now() - timedelta(days=np.random.randint(1, 1000))
|
|
31
|
+
for _ in range(num_customers)],
|
|
32
|
+
'CustomerType': np.random.choice(['Regular', 'Premium', 'VIP'], num_customers),
|
|
33
|
+
'CreditScore': np.random.randint(300, 851, num_customers)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Create DataFrame
|
|
37
|
+
df = pd.DataFrame(data)
|
|
38
|
+
|
|
39
|
+
return df
|
|
40
|
+
|
|
41
|
+
|
|
13
42
|
def create_sales_data(num_records=1000):
|
|
14
43
|
# Generate dates for the last 365 days
|
|
15
44
|
end_date = datetime.now()
|
sqlshell/main.py
CHANGED
|
@@ -188,6 +188,12 @@ class SQLShell(QMainWindow):
|
|
|
188
188
|
tables_header.setStyleSheet(get_tables_header_stylesheet())
|
|
189
189
|
left_layout.addWidget(tables_header)
|
|
190
190
|
|
|
191
|
+
# Tables info label
|
|
192
|
+
tables_info = QLabel("Right-click on tables to profile columns, analyze structure, and discover distributions. Select multiple tables to analyze foreign key relationships.")
|
|
193
|
+
tables_info.setWordWrap(True)
|
|
194
|
+
tables_info.setStyleSheet("color: #7FB3D5; font-size: 11px; margin-top: 2px; margin-bottom: 5px;")
|
|
195
|
+
left_layout.addWidget(tables_info)
|
|
196
|
+
|
|
191
197
|
# Tables list with custom styling
|
|
192
198
|
self.tables_list = DraggableTablesList(self)
|
|
193
199
|
self.tables_list.itemClicked.connect(self.show_table_preview)
|
|
@@ -808,25 +814,32 @@ class SQLShell(QMainWindow):
|
|
|
808
814
|
# Generate test data
|
|
809
815
|
sales_df = create_test_data.create_sales_data()
|
|
810
816
|
customer_df = create_test_data.create_customer_data()
|
|
817
|
+
large_customer_df = create_test_data.create_large_customer_data()
|
|
811
818
|
product_df = create_test_data.create_product_data()
|
|
812
819
|
large_numbers_df = create_test_data.create_large_numbers_data()
|
|
820
|
+
california_housing_df = create_test_data.create_california_housing_data()
|
|
813
821
|
|
|
814
822
|
# Save test data to temporary directory
|
|
815
823
|
sales_path = os.path.join(temp_dir, 'sample_sales_data.xlsx')
|
|
816
824
|
customer_path = os.path.join(temp_dir, 'customer_data.parquet')
|
|
817
825
|
product_path = os.path.join(temp_dir, 'product_catalog.xlsx')
|
|
818
826
|
large_numbers_path = os.path.join(temp_dir, 'large_numbers.xlsx')
|
|
819
|
-
|
|
827
|
+
large_customer_path = os.path.join(temp_dir, 'large_customer_data.parquet')
|
|
828
|
+
california_housing_path = os.path.join(temp_dir, 'california_housing_data.parquet')
|
|
820
829
|
sales_df.to_excel(sales_path, index=False)
|
|
821
830
|
customer_df.to_parquet(customer_path, index=False)
|
|
822
831
|
product_df.to_excel(product_path, index=False)
|
|
823
832
|
large_numbers_df.to_excel(large_numbers_path, index=False)
|
|
824
|
-
|
|
833
|
+
large_customer_df.to_parquet(large_customer_path, index=False)
|
|
834
|
+
california_housing_df.to_parquet(california_housing_path, index=False)
|
|
835
|
+
|
|
825
836
|
# Register the tables in the database manager
|
|
826
837
|
self.db_manager.register_dataframe(sales_df, 'sample_sales_data', sales_path)
|
|
827
838
|
self.db_manager.register_dataframe(product_df, 'product_catalog', product_path)
|
|
828
839
|
self.db_manager.register_dataframe(customer_df, 'customer_data', customer_path)
|
|
829
840
|
self.db_manager.register_dataframe(large_numbers_df, 'large_numbers', large_numbers_path)
|
|
841
|
+
self.db_manager.register_dataframe(large_customer_df, 'large_customer_data', large_customer_path)
|
|
842
|
+
self.db_manager.register_dataframe(california_housing_df, 'california_housing_data', california_housing_path)
|
|
830
843
|
|
|
831
844
|
# Update UI
|
|
832
845
|
self.tables_list.clear()
|
|
@@ -1203,6 +1216,30 @@ LIMIT 10
|
|
|
1203
1216
|
|
|
1204
1217
|
def show_tables_context_menu(self, position):
|
|
1205
1218
|
"""Show context menu for tables list"""
|
|
1219
|
+
# Check if we have multiple selected items
|
|
1220
|
+
selected_items = self.tables_list.selectedItems()
|
|
1221
|
+
if len(selected_items) > 1:
|
|
1222
|
+
# Filter out any folder items from selection
|
|
1223
|
+
table_items = [item for item in selected_items if not self.tables_list.is_folder_item(item)]
|
|
1224
|
+
|
|
1225
|
+
if len(table_items) > 1:
|
|
1226
|
+
# Create context menu for multiple table selection
|
|
1227
|
+
context_menu = QMenu(self)
|
|
1228
|
+
context_menu.setStyleSheet(get_context_menu_stylesheet())
|
|
1229
|
+
|
|
1230
|
+
# Add foreign key analysis option
|
|
1231
|
+
analyze_fk_action = context_menu.addAction(f"Analyze Foreign Keys Between {len(table_items)} Tables")
|
|
1232
|
+
analyze_fk_action.setIcon(QIcon.fromTheme("system-search"))
|
|
1233
|
+
|
|
1234
|
+
# Show menu and get selected action
|
|
1235
|
+
action = context_menu.exec(self.tables_list.mapToGlobal(position))
|
|
1236
|
+
|
|
1237
|
+
if action == analyze_fk_action:
|
|
1238
|
+
self.analyze_foreign_keys_between_tables(table_items)
|
|
1239
|
+
|
|
1240
|
+
return
|
|
1241
|
+
|
|
1242
|
+
# Single item selection (original functionality)
|
|
1206
1243
|
item = self.tables_list.itemAt(position)
|
|
1207
1244
|
|
|
1208
1245
|
# If no item or it's a folder, let the tree widget handle it
|
|
@@ -1236,6 +1273,10 @@ LIMIT 10
|
|
|
1236
1273
|
profile_table_action = context_menu.addAction("Profile Table Structure")
|
|
1237
1274
|
profile_table_action.setIcon(QIcon.fromTheme("edit-find"))
|
|
1238
1275
|
|
|
1276
|
+
# Add distributions profiler action
|
|
1277
|
+
profile_distributions_action = context_menu.addAction("Analyze Column Distributions")
|
|
1278
|
+
profile_distributions_action.setIcon(QIcon.fromTheme("accessories-calculator"))
|
|
1279
|
+
|
|
1239
1280
|
# Check if table needs reloading and add appropriate action
|
|
1240
1281
|
if table_name in self.tables_list.tables_needing_reload:
|
|
1241
1282
|
reload_action = context_menu.addAction("Reload Table")
|
|
@@ -1294,6 +1335,9 @@ LIMIT 10
|
|
|
1294
1335
|
elif action == profile_table_action:
|
|
1295
1336
|
# Call the table profile method
|
|
1296
1337
|
self.profile_table_structure(table_name)
|
|
1338
|
+
elif action == profile_distributions_action:
|
|
1339
|
+
# Call the distributions profile method
|
|
1340
|
+
self.profile_distributions(table_name)
|
|
1297
1341
|
elif action == rename_action:
|
|
1298
1342
|
# Show rename dialog
|
|
1299
1343
|
new_name, ok = QInputDialog.getText(
|
|
@@ -1349,6 +1393,73 @@ LIMIT 10
|
|
|
1349
1393
|
if target_folder:
|
|
1350
1394
|
self.tables_list.move_item_to_folder(item, target_folder)
|
|
1351
1395
|
self.statusBar().showMessage(f'Moved table "{table_name}" to folder "{target_folder.text(0)}"')
|
|
1396
|
+
|
|
1397
|
+
def analyze_foreign_keys_between_tables(self, table_items):
|
|
1398
|
+
"""Analyze foreign key relationships between selected tables"""
|
|
1399
|
+
try:
|
|
1400
|
+
# Show a loading indicator
|
|
1401
|
+
table_count = len(table_items)
|
|
1402
|
+
self.statusBar().showMessage(f'Analyzing foreign key relationships between {table_count} tables...')
|
|
1403
|
+
|
|
1404
|
+
# Extract table names from selected items
|
|
1405
|
+
table_names = []
|
|
1406
|
+
for item in table_items:
|
|
1407
|
+
table_name = self.tables_list.get_table_name_from_item(item)
|
|
1408
|
+
if table_name:
|
|
1409
|
+
table_names.append(table_name)
|
|
1410
|
+
|
|
1411
|
+
if len(table_names) < 2:
|
|
1412
|
+
QMessageBox.warning(self, "Not Enough Tables",
|
|
1413
|
+
"At least two tables are required for foreign key analysis.")
|
|
1414
|
+
return
|
|
1415
|
+
|
|
1416
|
+
# Check if any tables need to be reloaded
|
|
1417
|
+
tables_to_reload = [tn for tn in table_names if tn in self.tables_list.tables_needing_reload]
|
|
1418
|
+
for table_name in tables_to_reload:
|
|
1419
|
+
# Reload the table immediately
|
|
1420
|
+
self.reload_selected_table(table_name)
|
|
1421
|
+
|
|
1422
|
+
# Fetch data for each table
|
|
1423
|
+
dfs = []
|
|
1424
|
+
for table_name in table_names:
|
|
1425
|
+
try:
|
|
1426
|
+
# Get the data as a dataframe
|
|
1427
|
+
query = f'SELECT * FROM "{table_name}"'
|
|
1428
|
+
df = self.db_manager.execute_query(query)
|
|
1429
|
+
|
|
1430
|
+
if df is not None and not df.empty:
|
|
1431
|
+
# Sample large tables to improve performance
|
|
1432
|
+
if len(df) > 10000:
|
|
1433
|
+
self.statusBar().showMessage(f'Sampling {table_name} (using 10,000 rows from {len(df)} total)...')
|
|
1434
|
+
df = df.sample(n=10000, random_state=42)
|
|
1435
|
+
dfs.append(df)
|
|
1436
|
+
else:
|
|
1437
|
+
QMessageBox.warning(self, "Empty Table",
|
|
1438
|
+
f"Table '{table_name}' has no data and will be skipped.")
|
|
1439
|
+
except Exception as e:
|
|
1440
|
+
QMessageBox.warning(self, "Table Error",
|
|
1441
|
+
f"Error loading table '{table_name}': {str(e)}\nThis table will be skipped.")
|
|
1442
|
+
|
|
1443
|
+
if len(dfs) < 2:
|
|
1444
|
+
QMessageBox.warning(self, "Not Enough Tables",
|
|
1445
|
+
"At least two tables with data are required for foreign key analysis.")
|
|
1446
|
+
return
|
|
1447
|
+
|
|
1448
|
+
# Import the foreign key analyzer
|
|
1449
|
+
from sqlshell.utils.profile_foreign_keys import visualize_foreign_keys
|
|
1450
|
+
|
|
1451
|
+
# Create and show the visualization
|
|
1452
|
+
self.statusBar().showMessage(f'Analyzing foreign key relationships between {len(dfs)} tables...')
|
|
1453
|
+
vis = visualize_foreign_keys(dfs, table_names)
|
|
1454
|
+
|
|
1455
|
+
# Store a reference to prevent garbage collection
|
|
1456
|
+
self._fk_analysis_window = vis
|
|
1457
|
+
|
|
1458
|
+
self.statusBar().showMessage(f'Foreign key analysis complete for {len(dfs)} tables')
|
|
1459
|
+
|
|
1460
|
+
except Exception as e:
|
|
1461
|
+
QMessageBox.critical(self, "Analysis Error", f"Error analyzing foreign keys:\n\n{str(e)}")
|
|
1462
|
+
self.statusBar().showMessage(f'Error analyzing foreign keys: {str(e)}')
|
|
1352
1463
|
|
|
1353
1464
|
def reload_selected_table(self, table_name=None):
|
|
1354
1465
|
"""Reload the data for a table from its source file"""
|
|
@@ -3195,6 +3306,12 @@ LIMIT 10
|
|
|
3195
3306
|
df = self.db_manager.execute_query(query)
|
|
3196
3307
|
|
|
3197
3308
|
if df is not None and not df.empty:
|
|
3309
|
+
# Sample the data if it's larger than 10,000 rows
|
|
3310
|
+
row_count = len(df)
|
|
3311
|
+
if row_count > 10000:
|
|
3312
|
+
self.statusBar().showMessage(f'Sampling {table_name} (using 10,000 rows from {row_count} total)...')
|
|
3313
|
+
df = df.sample(n=10000, random_state=42)
|
|
3314
|
+
|
|
3198
3315
|
# Import the key profiler
|
|
3199
3316
|
from sqlshell.utils.profile_keys import visualize_profile
|
|
3200
3317
|
|
|
@@ -3205,7 +3322,10 @@ LIMIT 10
|
|
|
3205
3322
|
# Store a reference to prevent garbage collection
|
|
3206
3323
|
self._keys_profile_window = vis
|
|
3207
3324
|
|
|
3208
|
-
|
|
3325
|
+
if row_count > 10000:
|
|
3326
|
+
self.statusBar().showMessage(f'Table structure profile generated for "{table_name}" (sampled 10,000 rows from {row_count})')
|
|
3327
|
+
else:
|
|
3328
|
+
self.statusBar().showMessage(f'Table structure profile generated for "{table_name}"')
|
|
3209
3329
|
else:
|
|
3210
3330
|
QMessageBox.warning(self, "Empty Table", f"Table '{table_name}' has no data to analyze.")
|
|
3211
3331
|
self.statusBar().showMessage(f'Table "{table_name}" is empty - cannot analyze')
|
|
@@ -3216,6 +3336,97 @@ LIMIT 10
|
|
|
3216
3336
|
except Exception as e:
|
|
3217
3337
|
QMessageBox.critical(self, "Profile Error", f"Error profiling table structure:\n\n{str(e)}")
|
|
3218
3338
|
self.statusBar().showMessage(f'Error profiling table: {str(e)}')
|
|
3339
|
+
|
|
3340
|
+
def profile_distributions(self, table_name):
|
|
3341
|
+
"""Analyze a table's column distributions to understand data patterns"""
|
|
3342
|
+
try:
|
|
3343
|
+
# Show a loading indicator
|
|
3344
|
+
self.statusBar().showMessage(f'Analyzing column distributions for "{table_name}"...')
|
|
3345
|
+
|
|
3346
|
+
# Get the table data
|
|
3347
|
+
if table_name in self.db_manager.loaded_tables:
|
|
3348
|
+
# Check if table needs reloading first
|
|
3349
|
+
if table_name in self.tables_list.tables_needing_reload:
|
|
3350
|
+
# Reload the table immediately
|
|
3351
|
+
self.reload_selected_table(table_name)
|
|
3352
|
+
|
|
3353
|
+
# Get the data as a dataframe
|
|
3354
|
+
query = f'SELECT * FROM "{table_name}"'
|
|
3355
|
+
df = self.db_manager.execute_query(query)
|
|
3356
|
+
|
|
3357
|
+
if df is not None and not df.empty:
|
|
3358
|
+
# Sample the data if it's larger than 10,000 rows
|
|
3359
|
+
row_count = len(df)
|
|
3360
|
+
if row_count > 10000:
|
|
3361
|
+
self.statusBar().showMessage(f'Sampling {table_name} (using 10,000 rows from {row_count} total)...')
|
|
3362
|
+
df = df.sample(n=10000, random_state=42)
|
|
3363
|
+
|
|
3364
|
+
# Import the distribution profiler
|
|
3365
|
+
from sqlshell.utils.profile_distributions import visualize_profile
|
|
3366
|
+
|
|
3367
|
+
# Create and show the visualization
|
|
3368
|
+
self.statusBar().showMessage(f'Generating distribution profile for "{table_name}"...')
|
|
3369
|
+
vis = visualize_profile(df)
|
|
3370
|
+
|
|
3371
|
+
# Store a reference to prevent garbage collection
|
|
3372
|
+
self._distributions_window = vis
|
|
3373
|
+
|
|
3374
|
+
if row_count > 10000:
|
|
3375
|
+
self.statusBar().showMessage(f'Distribution profile generated for "{table_name}" (sampled 10,000 rows from {row_count})')
|
|
3376
|
+
else:
|
|
3377
|
+
self.statusBar().showMessage(f'Distribution profile generated for "{table_name}"')
|
|
3378
|
+
else:
|
|
3379
|
+
QMessageBox.warning(self, "Empty Table", f"Table '{table_name}' has no data to analyze.")
|
|
3380
|
+
self.statusBar().showMessage(f'Table "{table_name}" is empty - cannot analyze')
|
|
3381
|
+
else:
|
|
3382
|
+
QMessageBox.warning(self, "Table Not Found", f"Table '{table_name}' not found.")
|
|
3383
|
+
self.statusBar().showMessage(f'Table "{table_name}" not found')
|
|
3384
|
+
|
|
3385
|
+
except Exception as e:
|
|
3386
|
+
QMessageBox.critical(self, "Profile Error", f"Error analyzing distributions:\n\n{str(e)}")
|
|
3387
|
+
self.statusBar().showMessage(f'Error analyzing distributions: {str(e)}')
|
|
3388
|
+
|
|
3389
|
+
def explain_column(self, column_name):
|
|
3390
|
+
"""Analyze a column to explain its relationship with other columns"""
|
|
3391
|
+
try:
|
|
3392
|
+
# Get the current tab
|
|
3393
|
+
current_tab = self.get_current_tab()
|
|
3394
|
+
if not current_tab or current_tab.current_df is None:
|
|
3395
|
+
return
|
|
3396
|
+
|
|
3397
|
+
# Show a loading indicator
|
|
3398
|
+
self.statusBar().showMessage(f'Analyzing column "{column_name}"...')
|
|
3399
|
+
|
|
3400
|
+
# Get the dataframe from the current tab
|
|
3401
|
+
df = current_tab.current_df
|
|
3402
|
+
|
|
3403
|
+
if df is not None and not df.empty:
|
|
3404
|
+
# Sample the data if it's larger than 100 rows for ultra-fast performance
|
|
3405
|
+
row_count = len(df)
|
|
3406
|
+
if row_count > 100:
|
|
3407
|
+
self.statusBar().showMessage(f'Sampling data (using 100 rows from {row_count} total)...')
|
|
3408
|
+
df = df.sample(n=100, random_state=42)
|
|
3409
|
+
|
|
3410
|
+
# Import the column profiler
|
|
3411
|
+
from sqlshell.utils.profile_column import visualize_profile
|
|
3412
|
+
|
|
3413
|
+
# Create and show the visualization
|
|
3414
|
+
self.statusBar().showMessage(f'Generating column profile for "{column_name}"...')
|
|
3415
|
+
visualize_profile(df, column_name)
|
|
3416
|
+
|
|
3417
|
+
# We don't need to store a reference since the UI keeps itself alive
|
|
3418
|
+
|
|
3419
|
+
if row_count > 100:
|
|
3420
|
+
self.statusBar().showMessage(f'Column profile generated for "{column_name}" (sampled 100 rows from {row_count})')
|
|
3421
|
+
else:
|
|
3422
|
+
self.statusBar().showMessage(f'Column profile generated for "{column_name}"')
|
|
3423
|
+
else:
|
|
3424
|
+
QMessageBox.warning(self, "Empty Data", "No data available to analyze.")
|
|
3425
|
+
self.statusBar().showMessage(f'No data to analyze')
|
|
3426
|
+
|
|
3427
|
+
except Exception as e:
|
|
3428
|
+
QMessageBox.critical(self, "Analysis Error", f"Error analyzing column:\n\n{str(e)}")
|
|
3429
|
+
self.statusBar().showMessage(f'Error analyzing column: {str(e)}')
|
|
3219
3430
|
|
|
3220
3431
|
def main():
|
|
3221
3432
|
# Parse command line arguments
|
sqlshell/table_list.py
CHANGED
|
@@ -37,7 +37,7 @@ class DraggableTablesList(QTreeWidget):
|
|
|
37
37
|
self.setHeaderHidden(True)
|
|
38
38
|
self.setColumnCount(1)
|
|
39
39
|
self.setIndentation(15) # Smaller indentation for a cleaner look
|
|
40
|
-
self.setSelectionMode(QTreeWidget.SelectionMode.
|
|
40
|
+
self.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
|
|
41
41
|
self.setExpandsOnDoubleClick(False) # Handle double-clicks manually
|
|
42
42
|
|
|
43
43
|
# Apply custom styling
|
|
@@ -120,6 +120,95 @@ class DraggableTablesList(QTreeWidget):
|
|
|
120
120
|
|
|
121
121
|
def startDrag(self, supportedActions):
|
|
122
122
|
"""Override startDrag to customize the drag data."""
|
|
123
|
+
# Check for multiple selected items
|
|
124
|
+
selected_items = self.selectedItems()
|
|
125
|
+
if len(selected_items) > 1:
|
|
126
|
+
# Only support dragging multiple items to the editor (not for folder management)
|
|
127
|
+
# Filter out folder items
|
|
128
|
+
table_items = [item for item in selected_items if not self.is_folder_item(item)]
|
|
129
|
+
|
|
130
|
+
if not table_items:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Extract table names
|
|
134
|
+
table_names = [self.get_table_name_from_item(item) for item in table_items]
|
|
135
|
+
table_names = [name for name in table_names if name] # Remove None values
|
|
136
|
+
|
|
137
|
+
if not table_names:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Create mime data with comma-separated table names
|
|
141
|
+
mime_data = QMimeData()
|
|
142
|
+
mime_data.setText(", ".join(table_names))
|
|
143
|
+
|
|
144
|
+
# Create drag object
|
|
145
|
+
drag = QDrag(self)
|
|
146
|
+
drag.setMimeData(mime_data)
|
|
147
|
+
|
|
148
|
+
# Create a visually appealing drag pixmap
|
|
149
|
+
font = self.font()
|
|
150
|
+
font.setBold(True)
|
|
151
|
+
metrics = self.fontMetrics()
|
|
152
|
+
|
|
153
|
+
# Build a preview label with limited number of tables
|
|
154
|
+
display_names = table_names[:3]
|
|
155
|
+
if len(table_names) > 3:
|
|
156
|
+
display_text = f"{', '.join(display_names)} (+{len(table_names) - 3} more)"
|
|
157
|
+
else:
|
|
158
|
+
display_text = ", ".join(display_names)
|
|
159
|
+
|
|
160
|
+
text_width = metrics.horizontalAdvance(display_text)
|
|
161
|
+
text_height = metrics.height()
|
|
162
|
+
|
|
163
|
+
# Make the pixmap large enough for the text plus padding and a small icon
|
|
164
|
+
padding = 10
|
|
165
|
+
pixmap = QPixmap(text_width + padding * 2 + 16, text_height + padding)
|
|
166
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
167
|
+
|
|
168
|
+
# Begin painting
|
|
169
|
+
painter = QPainter(pixmap)
|
|
170
|
+
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
171
|
+
|
|
172
|
+
# Draw a nice rounded rectangle background
|
|
173
|
+
bg_color = QColor(44, 62, 80, 220) # Dark blue with transparency
|
|
174
|
+
painter.setBrush(QBrush(bg_color))
|
|
175
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
176
|
+
painter.drawRoundedRect(0, 0, pixmap.width(), pixmap.height(), 5, 5)
|
|
177
|
+
|
|
178
|
+
# Draw text
|
|
179
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
180
|
+
painter.setFont(font)
|
|
181
|
+
painter.drawText(int(padding + 16), int(text_height + (padding / 2) - 2), display_text)
|
|
182
|
+
|
|
183
|
+
# Draw a small database icon (simulated)
|
|
184
|
+
icon_x = padding / 2
|
|
185
|
+
icon_y = (pixmap.height() - 12) / 2
|
|
186
|
+
|
|
187
|
+
# Draw a simple database icon as a blue circle with lines
|
|
188
|
+
table_icon_color = QColor("#3498DB")
|
|
189
|
+
painter.setBrush(QBrush(table_icon_color))
|
|
190
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
191
|
+
painter.drawEllipse(int(icon_x), int(icon_y), 12, 12)
|
|
192
|
+
|
|
193
|
+
# Draw "table" lines inside the circle
|
|
194
|
+
painter.setPen(Qt.GlobalColor.white)
|
|
195
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 4), int(icon_x + 9), int(icon_y + 4))
|
|
196
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 6), int(icon_x + 9), int(icon_y + 6))
|
|
197
|
+
painter.drawLine(int(icon_x + 3), int(icon_y + 8), int(icon_x + 9), int(icon_y + 8))
|
|
198
|
+
|
|
199
|
+
painter.end()
|
|
200
|
+
|
|
201
|
+
# Set the drag pixmap
|
|
202
|
+
drag.setPixmap(pixmap)
|
|
203
|
+
|
|
204
|
+
# Set hotspot to be at the top-left corner of the text
|
|
205
|
+
drag.setHotSpot(QPoint(padding, pixmap.height() // 2))
|
|
206
|
+
|
|
207
|
+
# Execute drag operation - only allow copy action for multiple tables
|
|
208
|
+
drag.exec(Qt.DropAction.CopyAction)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Single item drag (original functionality)
|
|
123
212
|
item = self.currentItem()
|
|
124
213
|
if not item:
|
|
125
214
|
return
|
sqlshell/ui/filter_header.py
CHANGED
|
@@ -88,6 +88,11 @@ class FilterHeader(QHeaderView):
|
|
|
88
88
|
# Add sort actions
|
|
89
89
|
sort_asc_action = context_menu.addAction("Sort Ascending")
|
|
90
90
|
sort_desc_action = context_menu.addAction("Sort Descending")
|
|
91
|
+
context_menu.addSeparator()
|
|
92
|
+
|
|
93
|
+
# Add explain column action
|
|
94
|
+
explain_action = context_menu.addAction("Explain Column")
|
|
95
|
+
|
|
91
96
|
context_menu.addSeparator()
|
|
92
97
|
filter_action = context_menu.addAction("Filter...")
|
|
93
98
|
|
|
@@ -127,6 +132,15 @@ class FilterHeader(QHeaderView):
|
|
|
127
132
|
self.show_filter_menu(logical_index)
|
|
128
133
|
elif action == toggle_bar_action:
|
|
129
134
|
self.toggle_bar_chart(logical_index)
|
|
135
|
+
elif action == explain_action:
|
|
136
|
+
# Call the explain_column method on the main window
|
|
137
|
+
if self.main_window and hasattr(self.main_window, "explain_column"):
|
|
138
|
+
# Get the column name from the table (if it has a current dataframe)
|
|
139
|
+
current_tab = self.main_window.get_current_tab()
|
|
140
|
+
if current_tab and hasattr(current_tab, "current_df") and current_tab.current_df is not None:
|
|
141
|
+
if logical_index < len(current_tab.current_df.columns):
|
|
142
|
+
column_name = current_tab.current_df.columns[logical_index]
|
|
143
|
+
self.main_window.explain_column(column_name)
|
|
130
144
|
|
|
131
145
|
def set_main_window(self, window):
|
|
132
146
|
"""Set the reference to the main window"""
|