sqlui 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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>