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 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
@@ -2,7 +2,7 @@
2
2
  SQLShell - A powerful SQL shell with GUI interface for data analysis
3
3
  """
4
4
 
5
- __version__ = "0.2.2"
5
+ __version__ = "0.2.3"
6
6
  __author__ = "SQLShell Team"
7
7
 
8
8
  from sqlshell.main import main
@@ -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
- self.statusBar().showMessage(f'Table structure profile generated for "{table_name}"')
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.SingleSelection)
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
@@ -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"""