dbviewer 0.3.1

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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +250 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/dbviewer/application.css +21 -0
  6. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  7. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  8. data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
  9. data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
  10. data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
  11. data/app/controllers/dbviewer/application_controller.rb +21 -0
  12. data/app/controllers/dbviewer/databases_controller.rb +0 -0
  13. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
  14. data/app/controllers/dbviewer/home_controller.rb +10 -0
  15. data/app/controllers/dbviewer/logs_controller.rb +39 -0
  16. data/app/controllers/dbviewer/tables_controller.rb +73 -0
  17. data/app/helpers/dbviewer/application_helper.rb +118 -0
  18. data/app/jobs/dbviewer/application_job.rb +4 -0
  19. data/app/mailers/dbviewer/application_mailer.rb +6 -0
  20. data/app/models/dbviewer/application_record.rb +5 -0
  21. data/app/services/dbviewer/file_storage.rb +0 -0
  22. data/app/services/dbviewer/in_memory_storage.rb +0 -0
  23. data/app/services/dbviewer/query_analyzer.rb +0 -0
  24. data/app/services/dbviewer/query_collection.rb +0 -0
  25. data/app/services/dbviewer/query_logger.rb +0 -0
  26. data/app/services/dbviewer/query_parser.rb +82 -0
  27. data/app/services/dbviewer/query_storage.rb +0 -0
  28. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
  29. data/app/views/dbviewer/home/index.html.erb +237 -0
  30. data/app/views/dbviewer/logs/index.html.erb +614 -0
  31. data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
  32. data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
  33. data/app/views/dbviewer/tables/index.html.erb +128 -0
  34. data/app/views/dbviewer/tables/query.html.erb +600 -0
  35. data/app/views/dbviewer/tables/show.html.erb +271 -0
  36. data/app/views/layouts/dbviewer/application.html.erb +728 -0
  37. data/config/routes.rb +22 -0
  38. data/lib/dbviewer/configuration.rb +79 -0
  39. data/lib/dbviewer/database_manager.rb +450 -0
  40. data/lib/dbviewer/engine.rb +20 -0
  41. data/lib/dbviewer/initializer.rb +23 -0
  42. data/lib/dbviewer/logger.rb +102 -0
  43. data/lib/dbviewer/query_analyzer.rb +109 -0
  44. data/lib/dbviewer/query_collection.rb +41 -0
  45. data/lib/dbviewer/query_parser.rb +82 -0
  46. data/lib/dbviewer/sql_validator.rb +194 -0
  47. data/lib/dbviewer/storage/base.rb +31 -0
  48. data/lib/dbviewer/storage/file_storage.rb +96 -0
  49. data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
  50. data/lib/dbviewer/version.rb +3 -0
  51. data/lib/dbviewer.rb +65 -0
  52. data/lib/tasks/dbviewer_tasks.rake +4 -0
  53. metadata +126 -0
@@ -0,0 +1,600 @@
1
+ <% content_for :title do %>
2
+ Query: <%= @table_name %>
3
+ <% end %>
4
+
5
+ <% content_for :head do %>
6
+ <link href="https://cdn.jsdelivr.net/npm/vscode-codicons@0.0.17/dist/codicon.min.css" rel="stylesheet">
7
+ <style>
8
+ /* Monaco Editor styling */
9
+ #monaco-editor {
10
+ margin-bottom: 1rem;
11
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
12
+ }
13
+
14
+ .monaco-editor-container {
15
+ border: 1px solid #ced4da;
16
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
17
+ }
18
+
19
+ [data-bs-theme="dark"] .monaco-editor-container {
20
+ border: 1px solid #495057;
21
+ }
22
+
23
+ [data-bs-theme="dark"] #monaco-editor {
24
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
25
+ }
26
+
27
+ .example-queries {
28
+ display: flex;
29
+ flex-wrap: wrap;
30
+ gap: 5px;
31
+ margin-top: 8px;
32
+ }
33
+
34
+ .example-query {
35
+ display: inline-block;
36
+ transition: all 0.2s ease;
37
+ cursor: pointer;
38
+ font-size: 0.85rem;
39
+ white-space: nowrap;
40
+ overflow: hidden;
41
+ text-overflow: ellipsis;
42
+ max-width: 100%;
43
+ }
44
+
45
+ [data-bs-theme="light"] .example-query {
46
+ border-color: #ced4da;
47
+ }
48
+
49
+ [data-bs-theme="dark"] .example-query {
50
+ border-color: #495057;
51
+ color: #f8f9fa;
52
+ }
53
+
54
+ /* Result table styling */
55
+ .results-table {
56
+ border-collapse: collapse;
57
+ }
58
+
59
+ [data-bs-theme="dark"] .results-table {
60
+ border-color: #495057;
61
+ }
62
+
63
+ .example-query:hover {
64
+ background-color: #0d6efd;
65
+ color: white;
66
+ border-color: #0d6efd;
67
+ }
68
+
69
+ /* Keyboard shortcut helper */
70
+ .keyboard-hint {
71
+ font-size: 0.8rem;
72
+ margin-left: 8px;
73
+ opacity: 0.7;
74
+ }
75
+
76
+ [data-bs-theme="light"] .shortcut-hints {
77
+ color: #6c757d;
78
+ }
79
+
80
+ [data-bs-theme="dark"] .shortcut-hints {
81
+ color: #adb5bd;
82
+ }
83
+
84
+ /* Monaco status bar */
85
+ .monaco-status-bar {
86
+ display: flex;
87
+ justify-content: space-between;
88
+ padding: 3px 8px;
89
+ font-size: 0.75rem;
90
+ border-top: none;
91
+ border-bottom-left-radius: 4px;
92
+ border-bottom-right-radius: 4px;
93
+ }
94
+
95
+ [data-bs-theme="light"] .monaco-status-bar {
96
+ background-color: #f8f9fa;
97
+ border: 1px solid #ced4da;
98
+ color: #6c757d;
99
+ }
100
+
101
+ [data-bs-theme="dark"] .monaco-status-bar {
102
+ background-color: #343a40;
103
+ border: 1px solid #495057;
104
+ color: #adb5bd;
105
+ }
106
+
107
+ .monaco-status-bar .column-info {
108
+ font-weight: 500;
109
+ }
110
+
111
+ /* Table structure styles */
112
+ #tableStructureHeader .btn-link {
113
+ font-weight: 500;
114
+ display: flex;
115
+ align-items: center;
116
+ width: 100%;
117
+ text-align: left;
118
+ }
119
+
120
+ [data-bs-theme="light"] #tableStructureHeader .btn-link {
121
+ color: #212529;
122
+ }
123
+
124
+ [data-bs-theme="dark"] #tableStructureHeader .btn-link {
125
+ color: #f8f9fa;
126
+ }
127
+
128
+ [data-bs-theme="light"] .table-columns-count {
129
+ color: #6c757d;
130
+ }
131
+
132
+ [data-bs-theme="dark"] .table-columns-count {
133
+ color: #adb5bd;
134
+ }
135
+
136
+ #tableStructureHeader .btn-link:hover,
137
+ #tableStructureHeader .btn-link:focus {
138
+ text-decoration: none;
139
+ color: #0d6efd;
140
+ }
141
+
142
+ #tableStructureHeader .btn-link i {
143
+ transition: transform 0.2s ease-in-out;
144
+ }
145
+
146
+ /* Table style overrides for query page */
147
+ [data-bs-theme="dark"] .table-sm th,
148
+ [data-bs-theme="dark"] .table-sm td {
149
+ border-color: #495057;
150
+ }
151
+
152
+ /* Results card styling */
153
+ [data-bs-theme="dark"] .card-header h5 {
154
+ color: #f8f9fa;
155
+ }
156
+
157
+ /* Alert styling for dark mode */
158
+ [data-bs-theme="dark"] .alert-warning {
159
+ background-color: rgba(255, 193, 7, 0.15);
160
+ border-color: rgba(255, 193, 7, 0.4);
161
+ color: #ffc107;
162
+ }
163
+
164
+ [data-bs-theme="dark"] .alert-danger {
165
+ background-color: rgba(220, 53, 69, 0.15);
166
+ border-color: rgba(220, 53, 69, 0.4);
167
+ color: #f8d7da;
168
+ }
169
+
170
+ /* Make headings stand out in dark mode */
171
+ [data-bs-theme="dark"] h1,
172
+ [data-bs-theme="dark"] h2,
173
+ [data-bs-theme="dark"] h3,
174
+ [data-bs-theme="dark"] h4,
175
+ [data-bs-theme="dark"] h5,
176
+ [data-bs-theme="dark"] h6 {
177
+ color: #f8f9fa;
178
+ }
179
+ </style>
180
+ <% end %>
181
+
182
+ <% content_for :sidebar_active do %>active<% end %>
183
+
184
+ <% content_for :sidebar do %>
185
+ <%= render 'dbviewer/shared/sidebar' %>
186
+ <% end %>
187
+
188
+ <div class="d-flex justify-content-between align-items-center mb-4">
189
+ <h1>Query: <%= @table_name %></h1>
190
+ <div>
191
+ <%= link_to table_path(@table_name), class: "btn btn-outline-primary" do %>
192
+ <i class="bi bi-arrow-left me-1"></i> Back to Table
193
+ <% end %>
194
+ </div>
195
+ </div>
196
+
197
+ <% if flash[:warning].present? %>
198
+ <div class="alert alert-warning alert-dismissible fade show" role="alert">
199
+ <%= flash[:warning] %>
200
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
201
+ </div>
202
+ <% end %>
203
+
204
+ <div class="card mb-4">
205
+ <div class="card-header">
206
+ <h5>SQL Query (Read-Only)</h5>
207
+ </div>
208
+ <div class="card-body">
209
+ <%= form_with url: query_table_path(@table_name), method: :post, local: true, id: "sql-query-form" do |form| %>
210
+ <div class="mb-3">
211
+ <div id="monaco-editor" class="monaco-editor-container" style="min-height: 200px; border-radius: 4px; margin-bottom: 0rem;"
212
+ data-initial-query="<%= CGI.escapeHTML(@query.to_s) %>"></div>
213
+ <%= form.hidden_field :query, id: "query-input", value: @query.to_s %>
214
+ </div>
215
+
216
+ <div class="d-flex justify-content-between align-items-start">
217
+ <div class="form-text">
218
+ <strong>Examples:</strong><br>
219
+ <div class="example-queries">
220
+ <code class="example-query btn btn-sm btn-outline-secondary mb-1">SELECT * FROM <%= @table_name %> LIMIT 100</code>
221
+ <code class="example-query btn btn-sm btn-outline-secondary mb-1">SELECT
222
+ <%
223
+ # Display first 3 columns or all if less than 3
224
+ display_cols = @columns.present? ? @columns.first(3).map { |c| c[:name] }.join(", ") : "column1, column2"
225
+ # Get first non-ID column for WHERE example if available
226
+ where_col = @columns.present? ? (@columns.find { |c| !c[:name].to_s.downcase.include?("id") } || @columns.first)[:name] : "column_name"
227
+ # Get a numeric column for aggregation if available
228
+ num_col = @columns.present? ? (@columns.find { |c| c[:type].to_s.downcase.include?("int") || c[:type].to_s.downcase.include?("num") } || @columns.first)[:name] : "id"
229
+ %>
230
+ <%= display_cols %> FROM <%= @table_name %> WHERE <%= where_col %> = 'value'</code>
231
+ <code class="example-query btn btn-sm btn-outline-secondary mb-1">SELECT COUNT(*) FROM <%= @table_name %> GROUP BY <%= num_col %></code>
232
+ </div>
233
+ </div>
234
+ <div>
235
+ <%= form.submit "Run Query", class: "btn btn-primary" %>
236
+ <span class="keyboard-hint d-none d-md-inline">(or press Cmd+Enter / Ctrl+Enter)</span>
237
+ <div class="small mt-2 d-none d-md-block shortcut-hints">
238
+ <strong>Shortcuts:</strong>
239
+ <span class="me-2">Cmd+Alt+T: Toggle table structure</span>
240
+ <span class="me-2">Cmd+Alt+S: Insert SELECT</span>
241
+ <span>Cmd+Alt+W: Insert WHERE</span>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ <% end %>
246
+ </div>
247
+ </div>
248
+ <div class="card mb-3">
249
+ <div class="card-header" id="tableStructureHeader">
250
+ <h6 class="mb-0">
251
+ <button class="btn btn-link btn-sm text-decoration-none p-0 collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#tableStructureContent" aria-expanded="false" aria-controls="tableStructureContent">
252
+ <i class="bi bi-chevron-down me-1"></i>
253
+ Table Structure Reference
254
+ <small class="table-columns-count ms-2">(<%= @columns.present? ? @columns.size : 0 %> columns)</small>
255
+ </button>
256
+ </h6>
257
+ </div>
258
+ <div id="tableStructureContent" class="collapse" aria-labelledby="tableStructureHeader">
259
+ <div class="card-body p-2">
260
+ <% if @columns.present? %>
261
+ <div class="table-responsive">
262
+ <table class="table table-sm table-bordered mb-0">
263
+ <thead>
264
+ <tr>
265
+ <th>Column</th>
266
+ <th>Type</th>
267
+ </tr>
268
+ </thead>
269
+ <tbody>
270
+ <% @columns.each do |column| %>
271
+ <tr>
272
+ <td><code><%= column[:name] %><%= " (PK)" if column[:primary] %></code></td>
273
+ <td><span class="badge bg-secondary"><%= column[:type] %></span></td>
274
+ </tr>
275
+ <% end %>
276
+ </tbody>
277
+ </table>
278
+ </div>
279
+ <% else %>
280
+ <p class="mb-0">No column information available.</p>
281
+ <% end %>
282
+ </div>
283
+ </div>
284
+ </div>
285
+
286
+
287
+ <% if @error.present? %>
288
+ <div class="alert alert-danger" role="alert">
289
+ <strong>Error:</strong> <%= @error %>
290
+ </div>
291
+ <% end %>
292
+
293
+ <% if @records.present? %>
294
+ <div class="card">
295
+ <div class="card-header d-flex justify-content-between align-items-center">
296
+ <h5>Results</h5>
297
+ <span class="badge bg-info">Rows: <%= @records.rows.count %></span>
298
+ </div>
299
+ <div class="card-body">
300
+ <div class="table-responsive">
301
+ <table class="table table-bordered table-striped">
302
+ <% if @records.columns.any? %>
303
+ <thead>
304
+ <tr>
305
+ <% @records.columns.each do |column_name| %>
306
+ <th><%= column_name %></th>
307
+ <% end %>
308
+ </tr>
309
+ </thead>
310
+ <tbody>
311
+ <% if @records.rows.any? %>
312
+ <% @records.rows.each do |row| %>
313
+ <tr>
314
+ <% row.each do |cell| %>
315
+ <td><%= format_cell_value(cell) %></td>
316
+ <% end %>
317
+ </tr>
318
+ <% end %>
319
+ <% else %>
320
+ <tr>
321
+ <td colspan="<%= @records.columns.count %>">Query executed successfully, but returned no rows.</td>
322
+ </tr>
323
+ <% end %>
324
+ </tbody>
325
+ <% else %>
326
+ <tr>
327
+ <td>Query executed successfully, but returned no columns.</td>
328
+ </tr>
329
+ <% end %>
330
+ </table>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ <% end %>
335
+ </div>
336
+
337
+ <script type="module">
338
+ import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm';
339
+
340
+ // Helper function to decode HTML entities
341
+ function decodeHTMLEntities(text) {
342
+ const textarea = document.createElement('textarea');
343
+ textarea.innerHTML = text;
344
+ return textarea.value;
345
+ }
346
+
347
+ // Get initial query value from a data attribute to avoid string escaping issues
348
+ const initialQueryEncoded = document.getElementById('monaco-editor').getAttribute('data-initial-query');
349
+ const initialQuery = decodeHTMLEntities(initialQueryEncoded);
350
+
351
+ // Determine initial theme based on document theme
352
+ const initialTheme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'vs-dark' : 'vs';
353
+
354
+ // Initialize Monaco Editor with SQL syntax highlighting
355
+ const editor = monaco.editor.create(document.getElementById('monaco-editor'), {
356
+ value: initialQuery || '',
357
+ language: 'sql',
358
+ theme: initialTheme,
359
+ automaticLayout: true, // Resize automatically
360
+ minimap: { enabled: true },
361
+ scrollBeyondLastLine: false,
362
+ lineNumbers: 'on',
363
+ renderLineHighlight: 'all',
364
+ tabSize: 2,
365
+ wordWrap: 'on',
366
+ formatOnPaste: true,
367
+ formatOnType: true,
368
+ autoIndent: 'full',
369
+ folding: true,
370
+ glyphMargin: false,
371
+ suggestOnTriggerCharacters: true,
372
+ fixedOverflowWidgets: true,
373
+ quickSuggestions: {
374
+ other: true,
375
+ comments: true,
376
+ strings: true
377
+ },
378
+ suggest: {
379
+ showKeywords: true,
380
+ showSnippets: true,
381
+ preview: true,
382
+ showIcons: true,
383
+ maxVisibleSuggestions: 12
384
+ }
385
+ });
386
+
387
+ // Theme change listener
388
+ document.addEventListener('dbviewerThemeChanged', (event) => {
389
+ const newTheme = event.detail.theme === 'dark' ? 'vs-dark' : 'vs';
390
+ monaco.editor.setTheme(newTheme);
391
+
392
+ // Update editor container border color
393
+ const editorContainer = document.querySelector('.monaco-editor-container');
394
+ if (editorContainer) {
395
+ editorContainer.style.borderColor = event.detail.theme === 'dark' ? '#495057' : '#ced4da';
396
+ }
397
+
398
+ // Update status bar styling based on theme
399
+ updateStatusBarTheme(event.detail.theme);
400
+
401
+ // Update example query buttons
402
+ const exampleQueries = document.querySelectorAll('.example-query');
403
+ exampleQueries.forEach(query => {
404
+ if (event.detail.theme === 'dark') {
405
+ query.style.borderColor = '#495057';
406
+ if (!query.classList.contains('btn-primary')) {
407
+ query.style.color = '#f8f9fa';
408
+ }
409
+ } else {
410
+ query.style.borderColor = '#ced4da';
411
+ if (!query.classList.contains('btn-primary')) {
412
+ query.style.color = '';
413
+ }
414
+ }
415
+ });
416
+ });
417
+
418
+ function updateStatusBarTheme(theme) {
419
+ const statusBar = document.querySelector('.monaco-status-bar');
420
+ if (!statusBar) return;
421
+
422
+ if (theme === 'dark') {
423
+ statusBar.style.backgroundColor = '#343a40';
424
+ statusBar.style.borderColor = '#495057';
425
+ statusBar.style.color = '#adb5bd';
426
+ } else {
427
+ statusBar.style.backgroundColor = '#f8f9fa';
428
+ statusBar.style.borderColor = '#ced4da';
429
+ statusBar.style.color = '#6c757d';
430
+ }
431
+ }
432
+
433
+ // Set up SQL intellisense with table/column completions
434
+ const tableName = "<%= @table_name %>";
435
+ const columns = [
436
+ <% if @columns.present? %>
437
+ <% @columns.each do |column| %>
438
+ { name: "<%= column[:name] %>", type: "<%= column[:type] %>" },
439
+ <% end %>
440
+ <% end %>
441
+ ];
442
+
443
+ // Register SQL completion providers
444
+ monaco.languages.registerCompletionItemProvider('sql', {
445
+ provideCompletionItems: function(model, position) {
446
+ const textUntilPosition = model.getValueInRange({
447
+ startLineNumber: position.lineNumber,
448
+ startColumn: 1,
449
+ endLineNumber: position.lineNumber,
450
+ endColumn: position.column
451
+ });
452
+
453
+ const suggestions = [];
454
+
455
+ // Add table name suggestion
456
+ suggestions.push({
457
+ label: tableName,
458
+ kind: monaco.languages.CompletionItemKind.Class,
459
+ insertText: tableName,
460
+ detail: 'Table name'
461
+ });
462
+
463
+ // Add column name suggestions
464
+ columns.forEach(col => {
465
+ suggestions.push({
466
+ label: col.name,
467
+ kind: monaco.languages.CompletionItemKind.Field,
468
+ insertText: col.name,
469
+ detail: `Column (${col.type})`
470
+ });
471
+ });
472
+
473
+ // Add common SQL keywords
474
+ const keywords = [
475
+ { label: 'SELECT', insertText: 'SELECT ' },
476
+ { label: 'FROM', insertText: 'FROM ' },
477
+ { label: 'WHERE', insertText: 'WHERE ' },
478
+ { label: 'ORDER BY', insertText: 'ORDER BY ' },
479
+ { label: 'GROUP BY', insertText: 'GROUP BY ' },
480
+ { label: 'HAVING', insertText: 'HAVING ' },
481
+ { label: 'LIMIT', insertText: 'LIMIT ' },
482
+ { label: 'JOIN', insertText: 'JOIN ' },
483
+ { label: 'LEFT JOIN', insertText: 'LEFT JOIN ' },
484
+ { label: 'INNER JOIN', insertText: 'INNER JOIN ' }
485
+ ];
486
+
487
+ keywords.forEach(kw => {
488
+ suggestions.push({
489
+ label: kw.label,
490
+ kind: monaco.languages.CompletionItemKind.Keyword,
491
+ insertText: kw.insertText
492
+ });
493
+ });
494
+
495
+ return { suggestions };
496
+ }
497
+ });
498
+
499
+ // Handle form submission - transfer content to hidden input before submitting
500
+ document.getElementById('sql-query-form').addEventListener('submit', function(event) {
501
+ // Stop the form from submitting immediately
502
+ event.preventDefault();
503
+
504
+ // Get the query value from the editor and set it to the hidden input
505
+ const queryValue = editor.getValue();
506
+ document.getElementById('query-input').value = queryValue;
507
+
508
+ // Log for debugging
509
+ console.log('Submitting query:', queryValue);
510
+
511
+ // Now manually submit the form
512
+ this.submit();
513
+ });
514
+
515
+ // Make example queries clickable
516
+ document.querySelectorAll('.example-query').forEach(example => {
517
+ example.style.cursor = 'pointer';
518
+ example.addEventListener('click', () => {
519
+ const query = decodeHTMLEntities(example.textContent);
520
+ editor.setValue(query);
521
+ editor.focus();
522
+ });
523
+ });
524
+
525
+ // Setup editor keybindings
526
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, function() {
527
+ // Get the query value from the editor and set it to the hidden input
528
+ const queryValue = editor.getValue();
529
+ document.getElementById('query-input').value = queryValue;
530
+
531
+ // Log for debugging
532
+ console.log('Submitting query via keyboard shortcut:', queryValue);
533
+
534
+ // Submit the form
535
+ document.getElementById('sql-query-form').submit();
536
+ });
537
+
538
+ // Add keyboard shortcuts for common SQL statements
539
+ editor.addAction({
540
+ id: 'insert-select-all',
541
+ label: 'Insert SELECT * FROM statement',
542
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KeyS],
543
+ run: function() {
544
+ editor.trigger('keyboard', 'type', { text: `SELECT * FROM ${tableName} LIMIT 100` });
545
+ return null;
546
+ }
547
+ });
548
+
549
+ editor.addAction({
550
+ id: 'insert-where',
551
+ label: 'Insert WHERE clause',
552
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KeyW],
553
+ run: function() {
554
+ editor.trigger('keyboard', 'type', { text: ' WHERE ' });
555
+ return null;
556
+ }
557
+ });
558
+
559
+ editor.addAction({
560
+ id: 'toggle-table-structure',
561
+ label: 'Toggle Table Structure Reference',
562
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KeyT],
563
+ run: function() {
564
+ // Use Bootstrap's collapse API to toggle
565
+ bootstrap.Collapse.getOrCreateInstance(document.getElementById('tableStructureContent')).toggle();
566
+ return null;
567
+ }
568
+ });
569
+
570
+ // Create a status bar showing cursor position and columns info
571
+ const statusBarDiv = document.createElement('div');
572
+ statusBarDiv.className = 'monaco-status-bar';
573
+ statusBarDiv.innerHTML = `<div class="status-info">Ready</div>
574
+ <div class="column-info">Table: ${tableName} (${columns.length} columns)</div>`;
575
+ document.getElementById('monaco-editor').after(statusBarDiv);
576
+
577
+ // Apply initial theme to status bar
578
+ const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
579
+ updateStatusBarTheme(currentTheme);
580
+
581
+ // Update status bar with cursor position
582
+ editor.onDidChangeCursorPosition(e => {
583
+ const position = `Ln ${e.position.lineNumber}, Col ${e.position.column}`;
584
+ statusBarDiv.querySelector('.status-info').textContent = position;
585
+ });
586
+
587
+ // Focus the editor when page loads
588
+ window.addEventListener('load', () => {
589
+ editor.focus();
590
+ });
591
+
592
+ // Toggle icon when table structure collapses or expands
593
+ document.getElementById('tableStructureContent').addEventListener('show.bs.collapse', function() {
594
+ document.querySelector('#tableStructureHeader button i').classList.replace('bi-chevron-down', 'bi-chevron-up');
595
+ });
596
+
597
+ document.getElementById('tableStructureContent').addEventListener('hide.bs.collapse', function() {
598
+ document.querySelector('#tableStructureHeader button i').classList.replace('bi-chevron-up', 'bi-chevron-down');
599
+ });
600
+ </script>