sqlui 0.1.5

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c1cecf33df22e86f955f8485d16c8a4add25d5598d4336f8b93f25116275a704
4
+ data.tar.gz: 502cb4a146fe34ad3255e66cf016d8b7dc7ab2d1558ebe313f45624e0e8eb4c3
5
+ SHA512:
6
+ metadata.gz: aa5faf1265ade6457bd26d5fce342ea3ebd09902dbdb4d359b4e50a989f82e43d68e20a263dd359598ff2994ee792c9475a15011ba1ebe4f879b28dbc311e384
7
+ data.tar.gz: 0d5ef75a2ca38abde8955e9b3650e082e8fffa5c3abfa17b6a10ddb25b364f4c9c1c66681d3e8ea5efc5149fe81c97e2c250b84e3d372686c99b2bc71f601500
data/lib/sqlui.rb ADDED
@@ -0,0 +1,217 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'set'
4
+
5
+ class SQLUI
6
+ MAX_ROWS = 1_000
7
+
8
+ def initialize(name:, saved_path:, max_rows: MAX_ROWS, &block)
9
+ @name = name
10
+ @saved_path = saved_path
11
+ @max_rows = max_rows
12
+ @queryer = block
13
+ @resources_dir = File.join(File.expand_path('..', File.dirname(__FILE__)), 'resources')
14
+ end
15
+
16
+ def get(params)
17
+ case params[:route]
18
+ when 'app'
19
+ { body: html.html_safe, status: 200, content_type: 'text/html' }
20
+ when 'sqlui.css'
21
+ { body: css, status: 200, content_type: 'text/css' }
22
+ when 'sqlui.js'
23
+ { body: javascript, status: 200, content_type: 'text/javascript' }
24
+ when 'metadata'
25
+ { body: metadata.to_json, status: 200, content_type: 'application/json' }
26
+ when 'query_file'
27
+ { body: query_file(params).to_json, status: 200, content_type: 'application/json' }
28
+ else
29
+ { body: "unknown route: #{params[:route]}", status: 404, content_type: 'text/plain' }
30
+ end
31
+ end
32
+
33
+ def post(params)
34
+ case params[:route]
35
+ when 'query'
36
+ { body: query(params).to_json, status: 200, content_type: 'text/html' }
37
+ else
38
+ { body: "unknown route: #{params[:route]}", status: 404, content_type: 'text/plain' }
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def html
45
+ @html ||= File.read(File.join(@resources_dir, 'sqlui.html'))
46
+ end
47
+
48
+ def css
49
+ @css ||= File.read(File.join(@resources_dir, 'sqlui.css'))
50
+ end
51
+
52
+ def javascript
53
+ @javascript ||= File.read(File.join(@resources_dir, 'sqlui.js'))
54
+ end
55
+
56
+ def query(params)
57
+ raise 'missing sql' unless params[:sql]
58
+ raise 'missing cursor' unless params[:cursor]
59
+
60
+ sql = find_query_at_cursor(params[:sql], Integer(params[:cursor]))
61
+ raise "can't find query at cursor" unless sql
62
+
63
+ execute_query(sql)
64
+ end
65
+
66
+ def query_file(params)
67
+ if params['file']
68
+ sql = File.read("#{@saved_path}/#{params['file']}")
69
+ execute_query(sql).tap { |r| r[:file] = params[:file] }
70
+ else
71
+ raise 'missing file param'
72
+ end
73
+ end
74
+
75
+ def metadata
76
+ @metadata ||= load_metadata
77
+ end
78
+
79
+ def load_metadata
80
+ stats_result = @queryer.call(
81
+ <<~SQL.squish
82
+ select
83
+ table_schema,
84
+ table_name,
85
+ index_name,
86
+ seq_in_index,
87
+ non_unique,
88
+ column_name
89
+ from information_schema.statistics
90
+ where table_schema not in('mysql', 'sys')
91
+ order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
92
+ SQL
93
+ )
94
+ result = {
95
+ server: @name,
96
+ schemas: {},
97
+ saved: Dir.glob("#{@saved_path}/*.sql").sort.map do |path|
98
+ {
99
+ filename: File.basename(path),
100
+ description: File.readlines(path).take_while { |l| l.start_with?('--') }.map { |l| l.sub(/^-- */, '') }.join
101
+ }
102
+ end
103
+ }
104
+ stats_result.each do |row|
105
+ table_schema = row['table_schema']
106
+ unless result[:schemas][table_schema]
107
+ result[:schemas][table_schema] = {
108
+ tables: {}
109
+ }
110
+ end
111
+ tables = result[:schemas][table_schema][:tables]
112
+ table_name = row['table_name']
113
+ unless tables[table_name]
114
+ tables[table_name] = {
115
+ indexes: {},
116
+ columns: {}
117
+ }
118
+ end
119
+ indexes = tables[table_name][:indexes]
120
+ index_name = row['index_name']
121
+ unless indexes[index_name]
122
+ indexes[index_name] = {}
123
+ end
124
+ index = indexes[index_name]
125
+ column_name = row['column_name']
126
+ index[column_name] = {}
127
+ column = index[column_name]
128
+ column[:name] = index_name
129
+ column[:seq_in_index] = row['seq_in_index']
130
+ column[:non_unique] = row['non_unique']
131
+ column[:column_name] = row['column_name']
132
+ end
133
+
134
+ column_result = @queryer.call(
135
+ <<~SQL.squish
136
+ select
137
+ table_schema,
138
+ table_name,
139
+ column_name,
140
+ data_type,
141
+ character_maximum_length,
142
+ is_nullable,
143
+ column_key,
144
+ column_default,
145
+ extra
146
+ from information_schema.columns
147
+ where table_schema not in('information_schema' 'mysql', 'performance_schema', 'sys')
148
+ order by table_schema, table_name, column_name, ordinal_position;
149
+ SQL
150
+ )
151
+ column_result.each do |row|
152
+ table_schema = row['table_schema']
153
+ table_name = row['table_name']
154
+ column_name = row['column_name']
155
+ next unless result[:schemas][table_schema]
156
+ next unless result[:schemas][table_schema][:tables][table_name]
157
+
158
+ columns = result[:schemas][table_schema][:tables][table_name][:columns]
159
+ unless columns[column_name]
160
+ columns[column_name] = {}
161
+ end
162
+ column = columns[column_name]
163
+ column[:name] = column_name
164
+ column[:data_type] = row['data_type']
165
+ column[:length] = row['character_maximum_length']
166
+ column[:allow_null] = row['is_nullable']
167
+ column[:key] = row['column_key']
168
+ column[:default] = row['column_default']
169
+ column[:extra] = row['extra']
170
+ end
171
+
172
+ result
173
+ end
174
+
175
+ def execute_query(sql)
176
+ result = @queryer.call(sql)
177
+ rows = result.rows
178
+ column_types = result.columns.map { |_| 'string' }
179
+ unless rows.empty?
180
+ maybe_non_null_column_value_exemplars = result.columns.each_with_index.map do |_, index|
181
+ rows.find do |row|
182
+ !row[index].nil?
183
+ end.try(:[], index)
184
+ end
185
+ column_types = maybe_non_null_column_value_exemplars.map do |value|
186
+ case value
187
+ when String, NilClass
188
+ 'string'
189
+ when Integer
190
+ 'number'
191
+ when Date, Time
192
+ 'date'
193
+ else
194
+ value.class.to_s
195
+ end
196
+ end
197
+ end
198
+ {
199
+ query: sql,
200
+ columns: result.columns,
201
+ column_types: column_types,
202
+ total_rows: rows.size,
203
+ rows: rows.take(@max_rows)
204
+ }
205
+ end
206
+
207
+ def find_query_at_cursor(sql, cursor)
208
+ parts_with_ranges = []
209
+ sql.scan(/[^;]*;[ \n]*/) { |part| parts_with_ranges << [part, 0, part.size] }
210
+ parts_with_ranges.inject(0) do |pos, part_with_range|
211
+ part_with_range[1] += pos
212
+ part_with_range[2] += pos
213
+ end
214
+ part_with_range = parts_with_ranges.find { |part_with_range| cursor >= part_with_range[1] && cursor < part_with_range[2] } || parts_with_ranges[-1]
215
+ part_with_range[0]
216
+ end
217
+ end
@@ -0,0 +1,242 @@
1
+ body {
2
+ font-size: 16px;
3
+ margin: 0;
4
+ }
5
+
6
+ .main-box {
7
+ display: flex;
8
+ flex-direction: column;
9
+ flex: 1;
10
+ margin: 0;
11
+ height: 100%;
12
+ min-height: 100%;
13
+ }
14
+
15
+ .header {
16
+ display: flex;
17
+ flex: 1;
18
+ align-items: center;
19
+ justify-content: start;
20
+ padding-left: 5px;
21
+ color: #333;
22
+ font-weight: bold;
23
+ }
24
+
25
+ .tabs-box {
26
+ display: flex;
27
+ flex-direction: row;
28
+ border-bottom: 1px solid #ddd;
29
+ height: 36px;
30
+ font-size: 16px;
31
+ font-family: Helvetica;
32
+ }
33
+
34
+ .tab-button, .selected-tab-button {
35
+ border: none;
36
+ outline: none;
37
+ cursor: pointer;
38
+ width: 150px;
39
+ padding: 2px;
40
+ margin: 0px;
41
+ display: flex;
42
+ justify-content: center;
43
+ background-color: #fff;
44
+ }
45
+
46
+ .tab-button {
47
+ color: #888;
48
+ }
49
+
50
+ .tab-button:hover {
51
+ color: #333;
52
+ }
53
+
54
+ .selected-tab-button {
55
+ color: #333;
56
+ font-weight: bold;
57
+ }
58
+
59
+ .tab-content-element {
60
+ display: none;
61
+ }
62
+
63
+ .query-box {
64
+ display: flex;
65
+ flex-direction: column;
66
+ }
67
+
68
+ .query {
69
+ display: flex;
70
+ flex: 1;
71
+ }
72
+
73
+ .submit-box {
74
+ display: flex;
75
+ border-top: 1px solid #ddd;
76
+ height: 36px;
77
+ justify-content: right;
78
+ }
79
+
80
+ .status {
81
+ display: flex;
82
+ justify-content: center;
83
+ align-content: center;
84
+ flex-direction: column;
85
+ font-family: Helvetica;
86
+ }
87
+
88
+ .result-box, .saved-box, .graph-box, .structure-box {
89
+ flex: 1;
90
+ overflow: auto;
91
+ display: flex;
92
+ flex-direction: column;
93
+ }
94
+
95
+ .graph-box, .result-box {
96
+ border-top: 1px solid #ddd;
97
+ }
98
+
99
+ .graph-box {
100
+ padding: 20px;
101
+ }
102
+
103
+ table {
104
+ font-family: monospace;
105
+ flex: 1;
106
+ border-spacing: 0px;
107
+ display: flex;
108
+ width: 100%;
109
+ }
110
+
111
+ table td:last-child, table th:last-child {
112
+ width: 100%;
113
+ border-right: none !important;
114
+ }
115
+
116
+ td, th {
117
+ padding: 5px 20px 5px 5px;
118
+ font-weight: normal;
119
+ white-space: nowrap;
120
+ max-width: 500px;
121
+ overflow: hidden;
122
+ text-overflow: ellipsis;
123
+ }
124
+
125
+ td {
126
+ text-align: right;
127
+ }
128
+
129
+ th {
130
+ text-align: left;
131
+ font-weight: bold;
132
+ border-bottom: 1px solid #ddd;
133
+ border-right: 1px solid #ddd;
134
+ }
135
+
136
+ table {
137
+ display: block;
138
+ }
139
+
140
+ thead {
141
+ padding: 0px;
142
+ background-color: #fff;
143
+ position: -webkit-sticky;
144
+ position: sticky;
145
+ top: 0px;
146
+ z-index: 100;
147
+ table-layout:fixed;
148
+ }
149
+
150
+ .highlighted-row {
151
+ background: #eee;
152
+ }
153
+
154
+ .status-box {
155
+ padding: 5px;
156
+ display: flex;
157
+ flex-direction: rows;
158
+ justify-content: space-between;
159
+ border-top: 1px solid #ddd;
160
+ height: 30px;
161
+ }
162
+
163
+ .CodeMirror pre.CodeMirror-placeholder {
164
+ color: #999;
165
+ }
166
+
167
+ .tabs-box {
168
+ display: flex;
169
+ }
170
+
171
+ .saved-box {
172
+ font-family: Helvetica;
173
+ }
174
+
175
+ .saved-box h1 {
176
+ margin: 0px;
177
+ padding-left: 10px;
178
+ padding-right: 10px;
179
+ padding-top: 10px;
180
+ padding-bottom: 10px;
181
+ font-size: 16px;
182
+ font-weight: bold;
183
+ }
184
+
185
+ .saved-box div:first-child {
186
+ border-top: none !important;
187
+ }
188
+
189
+ .saved-box p {
190
+ margin: 0px;
191
+ padding-left: 20px;
192
+ padding-bottom: 20px;
193
+ }
194
+
195
+ .saved-box div {
196
+ cursor: pointer;
197
+ border-top: 1px solid #eeeeee;
198
+ }
199
+
200
+ .saved-box div:hover {
201
+ background: #eee;
202
+ }
203
+
204
+ .submit-button {
205
+ cursor: pointer;
206
+ margin-right: 10px;
207
+ }
208
+
209
+ .cm-editor.cm-focused {
210
+ outline: none !important;
211
+ }
212
+
213
+ .schemas, .tables {
214
+ border: none;
215
+ display: flex;
216
+ min-width: 200px;
217
+ }
218
+
219
+ .table-info {
220
+ display: grid;
221
+ grid-template-columns: 1;
222
+ grid-template-rows: 0.5fr 0.5fr;
223
+ justify-items: stretch;
224
+ flex: 1;
225
+ }
226
+
227
+ .columns {
228
+ border-bottom: 1px solid #ddd;
229
+ overflow: auto;
230
+ grid-column: 1;
231
+ grid-row: 1;
232
+ }
233
+
234
+ .indexes {
235
+ overflow: auto;
236
+ grid-column: 1;
237
+ grid-row: 2;
238
+ }
239
+
240
+ select {
241
+ outline: none;
242
+ }
@@ -0,0 +1,60 @@
1
+ <head>
2
+
3
+ <title>SQLUI</title>
4
+
5
+ <script src="sqlui.js"></script>
6
+ <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
7
+ <link rel="stylesheet" href="sqlui.css">
8
+ </head>
9
+
10
+ <body>
11
+ <div class="main-box">
12
+ <div class="tabs-box">
13
+ <div id="header" class="header">SQLUI</div>
14
+ <input id="query-tab-button" class="tab-button" type="button" value="Query" onclick="sqlui.selectTab('query')"></input>
15
+ <input id="graph-tab-button" class="tab-button" type="button" value="Graph" onclick="sqlui.selectTab('graph')"></input>
16
+ <input id="saved-tab-button" class="tab-button" type="button" value="Saved" onclick="sqlui.selectTab('saved')"></input>
17
+ <input id="structure-tab-button" class="tab-button" type="button" value="Structure" onclick="sqlui.selectTab('structure')"></input>
18
+ </div>
19
+
20
+ <div id="query-box" class="query-box tab-content-element graph-element query-element" style="display: none;">
21
+ <div id="query"></div>
22
+ </div>
23
+
24
+ <!--
25
+ <div id="submit-box" class="submit-box tab-content-element graph-element query-element" style="display: none;">
26
+ <input id="submit-button" class="submit-button" type="button" value="submit" onclick="submit();"></input>
27
+ </div>
28
+ -->
29
+
30
+ <div id="result-box" class="result-box tab-content-element query-element" style="display: none;">
31
+ </div>
32
+
33
+ <div id="graph-box" class="graph-box tab-content-element graph-element" style="display: none;">
34
+ </div>
35
+
36
+ <div id="saved-box" class="saved-box tab-content-element saved-element" style="display: none;">
37
+ </div>
38
+
39
+ <div id="structure-box" class="structure-box tab-content-element structure-element" style="display: none;">
40
+ <div style="display: flex; flex: 1; flex-direction: row; align-items: stretch;">
41
+ <select id="schemas" class="schemas" size="4">
42
+ </select>
43
+ <select id="tables" class="tables" size="4">
44
+ </select>
45
+ <div id="table-info" class="table-info">
46
+ <div id="columns" class="columns">
47
+ </div>
48
+ <div id="indexes" class="indexes">
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div id="status-box" class="status-box">
55
+ <div id="query-status" class="status tab-content-element query-element"></div>
56
+ <div id="graph-status" class="status tab-content-element graph-element"></div>
57
+ <div id="saved-status" class="status tab-content-element saved-element"></div>
58
+ </div>
59
+ </div>
60
+ </body>