sqlui 0.1.65 → 0.1.67

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dccd56c50fbd55f848e0af58c735e04a2359f752d83bc5733011de736d88e47c
4
- data.tar.gz: 2d1399f6aa1ac923101aad2d409c7488fec210107426ec2480475626fa57dbb2
3
+ metadata.gz: 7b91f535db1da5166bfebd7b9d9097db156b429aa283c6084e1f79bb348f56c4
4
+ data.tar.gz: bec7b344b15ee7730f88e77ad88910a8e316ac135aa9f66a4ea96aafecf763ac
5
5
  SHA512:
6
- metadata.gz: a60671998954c01b16253548b4ed9217a931cd9fded5d0af3262c506950665993d0261d4b0e6ecb942acd970e98b2d166cf2a8ee4f2f1cb1917ce50ed3054c55
7
- data.tar.gz: 22b4e49d4b7ab49298f80c2e11a6da30c418d49b4a9f212c5441cf3e43136bcf460f6b399916ebd4e607f6c4090e74d9038885eae2ad750f26c274ed153336fb
6
+ metadata.gz: e470fbc8f9ea18c9de08eb05ce67a491f6a0b42a9e0895033566f97953da3e630963f431192411af6471dc678979e45e67aa845fbde3b07093abc4c95c119e3d
7
+ data.tar.gz: bd4030d8b1e3095873b887c36dad1303b63aa1e020851ce5fa3336b8b5f055b6b33b20b21db28a203350c3cc7746cd1dc422a221a08412903a39b8d0f6a79afa
data/.release-version CHANGED
@@ -1 +1 @@
1
- 0.1.65
1
+ 0.1.67
@@ -10,6 +10,7 @@ class DatabaseMetadata
10
10
  def lookup(client, database_config)
11
11
  result = load_columns(client, database_config)
12
12
  load_stats(result, client, database_config)
13
+ load_tables(result, client, database_config)
13
14
 
14
15
  result
15
16
  end
@@ -91,7 +92,7 @@ class DatabaseMetadata
91
92
  from information_schema.statistics
92
93
  #{where_clause}
93
94
  order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
94
- SQL
95
+ SQL
95
96
  )
96
97
  stats_result.each do |row|
97
98
  table_schema = row.shift
@@ -110,5 +111,81 @@ class DatabaseMetadata
110
111
  column[:column_name] = column_name
111
112
  end
112
113
  end
114
+
115
+ def load_tables(result, client, _database_config)
116
+ tables_result = client.query(
117
+ <<~SQL
118
+ SELECT
119
+ TABLE_SCHEMA,
120
+ TABLE_NAME,
121
+ CREATE_TIME,
122
+ UPDATE_TIME,
123
+ ENGINE,
124
+ TABLE_ROWS,
125
+ AVG_ROW_LENGTH,
126
+ DATA_LENGTH,
127
+ INDEX_LENGTH,
128
+ TABLE_COLLATION,
129
+ AUTO_INCREMENT
130
+ FROM information_schema.TABLES;
131
+ SQL
132
+ )
133
+ tables_hash = {}
134
+ tables_result.each do |row|
135
+ table_schema = row.shift
136
+ table_name = row.shift
137
+ tables_hash[table_schema] ||= {}
138
+ tables_hash[table_schema][table_name] = {
139
+ created_at: row.shift,
140
+ updated_at: row.shift,
141
+ engine: row.shift,
142
+ rows: add_thousands_separator(row.shift),
143
+ average_row_size: pretty_data_size(row.shift),
144
+ data_size: pretty_data_size(row.shift),
145
+ index_size: pretty_data_size(row.shift),
146
+ encoding: row.shift,
147
+ auto_increment: add_thousands_separator(row.shift)
148
+ }
149
+ end
150
+ result.each do |table_schema, schema_hash|
151
+ schema_hash[:tables].each do |table_name, table|
152
+ table[:info] = tables_hash[table_schema][table_name]
153
+ end
154
+ end
155
+ end
156
+
157
+ def pretty_data_size(size)
158
+ if size.nil?
159
+ return nil
160
+ elsif size < 1024
161
+ unit = "byte#{size == 1 ? '' : 's'}"
162
+ converted = size
163
+ elsif size < 1024 * 1024
164
+ unit = 'KiB'
165
+ converted = (size.to_f / 1024).round(2)
166
+ elsif size < 1024 * 1024 * 1024
167
+ unit = 'MiB'
168
+ converted = (size.to_f / (1024 * 1024)).round(2)
169
+ elsif size < 1024 * 1024 * 1024 * 1024
170
+ unit = 'GiB'
171
+ converted = (size.to_f / (1024 * 1024 * 1024)).round(2)
172
+ end
173
+
174
+ "#{add_thousands_separator(converted)} #{unit}"
175
+ end
176
+
177
+ def add_thousands_separator(value)
178
+ return nil if value.nil?
179
+
180
+ integer = value.round(1).floor
181
+ fractional = ((value.round(1) - integer) * 10).floor
182
+
183
+ with_commas = integer.to_s.reverse.scan(/.{1,3}/).join(',').reverse
184
+ if fractional.zero?
185
+ with_commas
186
+ else
187
+ "#{with_commas}.#{fractional}"
188
+ end
189
+ end
113
190
  end
114
191
  end
data/app/sql_parser.rb CHANGED
@@ -40,10 +40,10 @@ class SqlParser
40
40
  multi_commented = true
41
41
  elsif multi_commented && char == '*' && scanner.peek(1) == '/'
42
42
  multi_commented = false
43
- elsif multi_commented
43
+ elsif multi_commented || (single_commented && char != "\n")
44
44
  next
45
45
  elsif !single_quoted && !double_quoted && !escaped && !multi_commented &&
46
- char == '-' && scanner.peek(1).match?(/-[ \n\t]/)
46
+ char == '-' && scanner.peek(2).match?(/-[ \n\t]/)
47
47
  current += scanner.getch
48
48
  single_commented = true
49
49
  elsif !single_quoted && !double_quoted && !escaped && !multi_commented && char == '#'
@@ -51,7 +51,7 @@ class SqlParser
51
51
  elsif single_commented && char == "\n"
52
52
  single_commented = false
53
53
  elsif single_commented
54
- single_quoted = false if char == "\n"
54
+ single_commented = false if char == "\n"
55
55
  elsif char == '\\'
56
56
  escaped = true
57
57
  elsif char == "'"
@@ -11,18 +11,6 @@
11
11
  margin: 0;
12
12
  }
13
13
 
14
- h1 {
15
- font-size: 22px;
16
- }
17
-
18
- h2 {
19
- font-size: 20px;
20
- }
21
-
22
- p {
23
- font-size: 18px;
24
- }
25
-
26
14
  .header-box {
27
15
  display: flex;
28
16
  flex-direction: row;
@@ -37,6 +25,12 @@
37
25
  align-items: center;
38
26
  justify-content: start;
39
27
  color: #333;
28
+ font-size: 22px;
29
+ }
30
+
31
+ .header {
32
+ font-weight: bold;
33
+ padding-left: 5px;
40
34
  }
41
35
 
42
36
  .server-name {
@@ -45,30 +39,28 @@
45
39
  font-weight: normal;
46
40
  }
47
41
 
48
- .name-and-links {
42
+ .links {
49
43
  display: flex;
50
44
  flex-direction: row;
51
45
  align-items: center;
46
+ margin-top: 5px;
52
47
  }
53
48
 
54
- .header {
55
- font-weight: bold;
56
- padding-left: 5px;
57
- }
58
-
59
- .database a {
60
- margin-right: 5px;
49
+ .link {
50
+ margin-right: 10px;
61
51
  color: darkblue;
62
- font-size: 16px;
52
+ font-size: 17px;
63
53
  text-decoration: none;
64
54
  }
65
55
 
66
- .database h2 {
67
- margin: 0 10px 0 0;
56
+ .name {
57
+ margin: 0 200px 0 0;
58
+ font-size: 20px;
68
59
  }
69
60
 
70
- .database p {
71
- margin: 10px 0 0;
61
+ .description {
62
+ margin: 20px 0 0;
63
+ font-size: 18px;
72
64
  }
73
65
 
74
66
  .database {
@@ -90,11 +82,11 @@
90
82
  </div>
91
83
  <% config.database_configs.each do |database_config| %>
92
84
  <div class="database">
93
- <div class="name-and-links">
94
- <h2 class='name'><%= database_config.display_name %></h2>
95
- <a class='query-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/query" %>">query</a>
96
- <a class='saved-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/saved" %>">saved</a>
97
- <a class='structure-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/structure" %>">structure</a>
85
+ <h2 class='name'><%= database_config.display_name %></h2>
86
+ <div class="links">
87
+ <a class='link query-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/query" %>">query</a>
88
+ <a class='link saved-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/saved" %>">saved</a>
89
+ <a class='link structure-link' href="<%= "#{config.base_url_path}/#{database_config.url_path}/structure" %>">structure</a>
98
90
  </div>
99
91
  <p class='description'>
100
92
  <%= database_config.description %>
data/app/views/sqlui.erb CHANGED
@@ -84,10 +84,16 @@
84
84
 
85
85
  <div id="structure-box" class="tab-content-element structure-element" style="display: none;">
86
86
  <div class="structure-wrapper">
87
- <select id="schemas" size="4">
88
- </select>
89
- <select id="tables" size="4">
90
- </select>
87
+ <div id="schemas-tables-and-stats">
88
+ <div id="schemas-and-tables">
89
+ <select id="schemas" size="4">
90
+ </select>
91
+ <select id="tables" size="4">
92
+ </select>
93
+ </div>
94
+ <div id="stats">
95
+ </div>
96
+ </div>
91
97
  <div id="table-info">
92
98
  <div id="columns">
93
99
  </div>
@@ -8,18 +8,6 @@ body {
8
8
  overflow: hidden;
9
9
  }
10
10
 
11
- h1 {
12
- font-size: 22px;
13
- }
14
-
15
- h2 {
16
- font-size: 20px;
17
- }
18
-
19
- p {
20
- font-size: 18px;
21
- }
22
-
23
11
  #loading-box {
24
12
  font-family: monospace;
25
13
  display: flex;
@@ -48,6 +36,7 @@ p {
48
36
  align-items: center;
49
37
  justify-content: start;
50
38
  color: #333;
39
+ font-size: 22px;
51
40
  }
52
41
 
53
42
  #header {
@@ -126,6 +115,7 @@ p {
126
115
  cursor: row-resize;
127
116
  flex-direction: column;
128
117
  align-items: center;
118
+ padding-left: 220px; /* To center the resizer icon. Ok, it's a hack. Get over it. */
129
119
  }
130
120
 
131
121
  #cancel-button {
@@ -135,7 +125,7 @@ p {
135
125
  border: 1px solid #888;
136
126
  height: 32px;
137
127
  font-size: 18px;
138
- margin: 0 220px 0 0; /* To center the resizer icon. Ok, it's a hack. Get over it. */
128
+ margin: 0;
139
129
  }
140
130
 
141
131
  #cancel-button-spacer {
@@ -258,6 +248,7 @@ p {
258
248
  display: flex;
259
249
  flex-direction: column;
260
250
  }
251
+
261
252
  table tbody tr td {
262
253
  height: 21px;
263
254
  }
@@ -384,16 +375,28 @@ thead {
384
375
  font-family: Helvetica, sans-serif;
385
376
  }
386
377
 
387
- #saved-box h2 {
388
- font-weight: bold;
378
+ .links {
379
+ display: flex;
380
+ flex-direction: row;
381
+ align-items: center;
382
+ margin-top: 5px;
389
383
  }
390
384
 
391
- .saved-list-item:last-child {
392
- border-top: none !important;
385
+ .link {
386
+ margin-right: 10px;
387
+ color: darkblue;
388
+ font-size: 17px;
389
+ text-decoration: none;
393
390
  }
394
391
 
395
- #saved-box p {
396
- margin: 0;
392
+ .name {
393
+ margin: 0 200px 0 0;
394
+ font-size: 20px;
395
+ }
396
+
397
+ .description {
398
+ margin: 20px 0 0;
399
+ font-size: 18px;
397
400
  }
398
401
 
399
402
  .saved-list-item {
@@ -402,25 +405,8 @@ thead {
402
405
  padding: 10px;
403
406
  }
404
407
 
405
- .saved-list-item h2 {
406
- margin: 0 10px 0 0;
407
- }
408
-
409
- .saved-list-item p {
410
- margin: 10px 0 0;
411
- }
412
-
413
- .name-and-links {
414
- display: flex;
415
- flex-direction: row;
416
- align-items: center;
417
- }
418
-
419
- .name-and-links a {
420
- margin-right: 5px;
421
- color: darkblue;
422
- font-size: 16px;
423
- text-decoration: none;
408
+ .saved-list-item:last-child {
409
+ border-bottom: none;
424
410
  }
425
411
 
426
412
  .cm-editor.cm-focused {
@@ -435,10 +421,49 @@ thead {
435
421
  height: 100%;
436
422
  }
437
423
 
424
+ #schemas-and-tables {
425
+ display: flex;
426
+ flex-direction: row;
427
+ flex: 1;
428
+ }
429
+
430
+ #schemas-tables-and-stats {
431
+ display: flex;
432
+ flex-direction: column;
433
+ border-right: 1px solid #ddd;
434
+ min-width: 400px;
435
+ }
436
+
437
+ #stats {
438
+ border-top: 1px solid #ddd;
439
+ padding: 5px 0;
440
+ }
441
+
442
+ #stats table {
443
+ width: 100%;
444
+ }
445
+
446
+ #stats table td {
447
+ padding: 2px 5px;
448
+ font-size: 18px;
449
+ color: #333;
450
+ }
451
+
452
+ table td:nth-child(1) {
453
+ text-align: left;
454
+ font-family: Helvetica, sans-serif;
455
+ }
456
+
457
+ table td:nth-child(2) {
458
+ text-align: right;
459
+ font-family: monospace;
460
+ }
461
+
438
462
  #schemas, #tables {
439
463
  border: none;
440
464
  display: flex;
441
- min-width: 200px;
465
+ flex: 1;
466
+ padding: 5px
442
467
  }
443
468
 
444
469
  #table-info {
@@ -21908,6 +21908,7 @@
21908
21908
  }
21909
21909
 
21910
21910
  function copyTextToClipboard (text) {
21911
+ if (text === null) text = '';
21911
21912
  const type = 'text/plain';
21912
21913
  const blob = new Blob([text], { type });
21913
21914
  navigator.clipboard.write([new window.ClipboardItem({ [type]: blob })]);
@@ -24744,9 +24745,17 @@
24744
24745
  popupElement.appendChild(contentElement);
24745
24746
 
24746
24747
  let renderedText = text;
24747
- if (typeof text === 'string' && text.match(/^\s*(?:\{.*\}|\[.*\])\s*$/)) {
24748
+ let clipboardText = text;
24749
+ let formatted = false;
24750
+ if (text === null) {
24751
+ renderedText = 'null';
24752
+ contentElement.style.color = '#888';
24753
+ clipboardText = '';
24754
+ } else if (typeof text === 'string' && text.match(/^\s*(?:\{.*\}|\[.*\])\s*$/)) {
24748
24755
  try {
24749
24756
  renderedText = JSON.stringify(JSON.parse(text), null, 2);
24757
+ clipboardText = renderedText;
24758
+ formatted = renderedText !== text;
24750
24759
  } catch (_) { }
24751
24760
  }
24752
24761
  contentElement.innerText = renderedText;
@@ -24762,11 +24771,11 @@
24762
24771
  buttonBarElement.appendChild(copyElement);
24763
24772
 
24764
24773
  copyElement.addEventListener('click', (event) => {
24765
- copyTextToClipboard(renderedText);
24774
+ copyTextToClipboard(clipboardText);
24766
24775
  toast('Text copied to clipboard.');
24767
24776
  });
24768
24777
 
24769
- if (renderedText !== text) {
24778
+ if (formatted) {
24770
24779
  const copyOriginalElement = document.createElement('input');
24771
24780
  copyOriginalElement.classList.add(styles$1.button);
24772
24781
  copyOriginalElement.type = 'button';
@@ -25118,6 +25127,23 @@
25118
25127
  }
25119
25128
  }
25120
25129
 
25130
+ function statsHtml (info) {
25131
+ const hidden = info == null;
25132
+ info ||= {};
25133
+ return `
25134
+ <table ${hidden ? 'style="visibility: hidden;"' : ''}>
25135
+ <tr><td>created:</td><td>${valueOrNullHtml(info.created_at)}</td></tr>
25136
+ <tr><td>updated:</td><td>${valueOrNullHtml(info.updated_at)}</td></tr>
25137
+ <tr><td>data size:</td><td>${valueOrNullHtml(info.data_size)}</td></tr>
25138
+ <tr><td>index size:</td><td>${valueOrNullHtml(info.index_size)}</td></tr>
25139
+ <tr><td>rows:</td><td>${valueOrNullHtml(info.rows)}</td></tr>
25140
+ <tr><td>row size:</td><td>${valueOrNullHtml(info.average_row_size)}</td></tr>
25141
+ <tr><td>encoding:</td><td>${valueOrNullHtml(info.encoding)}</td></tr>
25142
+ <tr><td>auto increment:</td><td>${valueOrNullHtml(info.auto_increment)}</td></tr>
25143
+ </table>
25144
+ `
25145
+ }
25146
+
25121
25147
  function selectStructureTab () {
25122
25148
  Array.prototype.forEach.call(document.getElementsByClassName('structure-element'), function (selected) {
25123
25149
  selected.style.display = 'flex';
@@ -25129,6 +25155,9 @@
25129
25155
 
25130
25156
  const schemasElement = document.getElementById('schemas');
25131
25157
  const tablesElement = document.getElementById('tables');
25158
+ const statsElement = document.getElementById('stats');
25159
+ statsElement.innerHTML = statsHtml(null);
25160
+
25132
25161
  const columnsElement = document.getElementById('columns');
25133
25162
  const indexesElement = document.getElementById('indexes');
25134
25163
 
@@ -25157,6 +25186,10 @@
25157
25186
  schemasElement.appendChild(optionElement);
25158
25187
  });
25159
25188
  schemasElement.addEventListener('change', function () {
25189
+ while (statsElement.firstChild) {
25190
+ statsElement.removeChild(statsElement.firstChild);
25191
+ }
25192
+ statsElement.innerHTML = statsHtml(null);
25160
25193
  while (tablesElement.firstChild) {
25161
25194
  tablesElement.removeChild(tablesElement.firstChild);
25162
25195
  }
@@ -25178,6 +25211,9 @@
25178
25211
  });
25179
25212
  }
25180
25213
  tablesElement.addEventListener('change', function () {
25214
+ while (statsElement.firstChild) {
25215
+ statsElement.removeChild(statsElement.firstChild);
25216
+ }
25181
25217
  while (columnsElement.firstChild) {
25182
25218
  columnsElement.removeChild(columnsElement.firstChild);
25183
25219
  }
@@ -25187,7 +25223,9 @@
25187
25223
  const schemaName = schemaNames.length === 1 ? schemaNames[0] : schemasElement.value;
25188
25224
  const tableName = tablesElement.value;
25189
25225
  const table = window.metadata.schemas[schemaName].tables[tableName];
25226
+ const info = table.info;
25190
25227
 
25228
+ statsElement.innerHTML = statsHtml(info);
25191
25229
  const columnEntries = Object.entries(table.columns);
25192
25230
  if (columnEntries.length > 0) {
25193
25231
  const columns = Object.keys(columnEntries[0][1]);
@@ -25275,7 +25313,7 @@
25275
25313
  viewUrl.searchParams.set('file', file.filename);
25276
25314
 
25277
25315
  const viewLinkElement = document.createElement('a');
25278
- viewLinkElement.classList.add('view-link');
25316
+ viewLinkElement.classList.add('link', 'view-link');
25279
25317
  viewLinkElement.innerText = 'view';
25280
25318
  viewLinkElement.href = viewUrl.pathname + viewUrl.search;
25281
25319
  addEventListener(viewLinkElement, 'click', (event) => {
@@ -25289,7 +25327,7 @@
25289
25327
  runUrl.searchParams.set('run', 'true');
25290
25328
 
25291
25329
  const runLinkElement = document.createElement('a');
25292
- runLinkElement.classList.add('run-link');
25330
+ runLinkElement.classList.add('link', 'run-link');
25293
25331
  runLinkElement.innerText = 'run';
25294
25332
  runLinkElement.href = runUrl.pathname + runUrl.search;
25295
25333
  addEventListener(runLinkElement, 'click', (event) => {
@@ -25300,25 +25338,31 @@
25300
25338
 
25301
25339
  const nameElement = document.createElement('h2');
25302
25340
  nameElement.innerText = file.filename;
25341
+ nameElement.classList.add('name');
25303
25342
 
25304
- const nameAndLinksElement = document.createElement('div');
25305
- nameAndLinksElement.classList.add('name-and-links');
25306
- nameAndLinksElement.appendChild(nameElement);
25307
- nameAndLinksElement.appendChild(viewLinkElement);
25308
- nameAndLinksElement.appendChild(runLinkElement);
25343
+ const linksElement = document.createElement('div');
25344
+ linksElement.classList.add('links');
25345
+ linksElement.appendChild(viewLinkElement);
25346
+ linksElement.appendChild(runLinkElement);
25309
25347
 
25310
25348
  const descriptionElement = document.createElement('p');
25311
25349
  descriptionElement.innerText = file.description;
25350
+ descriptionElement.classList.add('description');
25312
25351
 
25313
25352
  const divElement = document.createElement('div');
25314
25353
  divElement.classList.add('saved-list-item');
25315
- divElement.appendChild(nameAndLinksElement);
25354
+ divElement.appendChild(nameElement);
25355
+ divElement.appendChild(linksElement);
25316
25356
  divElement.appendChild(descriptionElement);
25317
25357
 
25318
25358
  savedElement.appendChild(divElement);
25319
25359
  });
25320
25360
  }
25321
25361
 
25362
+ function valueOrNullHtml (value) {
25363
+ return value == null ? '<span style="color: #888">null</span>' : value
25364
+ }
25365
+
25322
25366
  function submitAll (target, event) {
25323
25367
  submit(target, event);
25324
25368
  }
@@ -25651,6 +25695,9 @@
25651
25695
  return wrapperElement
25652
25696
  } else {
25653
25697
  cellElement.style.textAlign = columnType === 'string' ? 'left' : 'right';
25698
+ if (value === null) {
25699
+ cellElement.style.color = '#888';
25700
+ }
25654
25701
  return document.createTextNode(value)
25655
25702
  }
25656
25703
  };
@@ -25961,6 +26008,12 @@
25961
26008
  }
25962
26009
  });
25963
26010
 
26011
+ document.addEventListener('keydown', (event) => {
26012
+ if (event.code === 'Escape') {
26013
+ focus();
26014
+ }
26015
+ });
26016
+
25964
26017
  window.onload = function () {
25965
26018
  Promise.all([
25966
26019
  google.charts.load('current', { packages: ['corechart', 'line'] }),
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqlui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.65
4
+ version: 0.1.67
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Dower
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-08 00:00:00.000000000 Z
11
+ date: 2023-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: airbrake