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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +250 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/dbviewer/application.css +21 -0
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
- data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
- data/app/controllers/dbviewer/application_controller.rb +21 -0
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
- data/app/controllers/dbviewer/home_controller.rb +10 -0
- data/app/controllers/dbviewer/logs_controller.rb +39 -0
- data/app/controllers/dbviewer/tables_controller.rb +73 -0
- data/app/helpers/dbviewer/application_helper.rb +118 -0
- data/app/jobs/dbviewer/application_job.rb +4 -0
- data/app/mailers/dbviewer/application_mailer.rb +6 -0
- data/app/models/dbviewer/application_record.rb +5 -0
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +82 -0
- data/app/services/dbviewer/query_storage.rb +0 -0
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
- data/app/views/dbviewer/home/index.html.erb +237 -0
- data/app/views/dbviewer/logs/index.html.erb +614 -0
- data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
- data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
- data/app/views/dbviewer/tables/index.html.erb +128 -0
- data/app/views/dbviewer/tables/query.html.erb +600 -0
- data/app/views/dbviewer/tables/show.html.erb +271 -0
- data/app/views/layouts/dbviewer/application.html.erb +728 -0
- data/config/routes.rb +22 -0
- data/lib/dbviewer/configuration.rb +79 -0
- data/lib/dbviewer/database_manager.rb +450 -0
- data/lib/dbviewer/engine.rb +20 -0
- data/lib/dbviewer/initializer.rb +23 -0
- data/lib/dbviewer/logger.rb +102 -0
- data/lib/dbviewer/query_analyzer.rb +109 -0
- data/lib/dbviewer/query_collection.rb +41 -0
- data/lib/dbviewer/query_parser.rb +82 -0
- data/lib/dbviewer/sql_validator.rb +194 -0
- data/lib/dbviewer/storage/base.rb +31 -0
- data/lib/dbviewer/storage/file_storage.rb +96 -0
- data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
- data/lib/dbviewer/version.rb +3 -0
- data/lib/dbviewer.rb +65 -0
- data/lib/tasks/dbviewer_tasks.rake +4 -0
- 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>
|