sqlui 0.1.18 → 0.1.20
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 +4 -4
- data/.version +1 -1
- data/app/args.rb +33 -0
- data/app/database_config.rb +30 -0
- data/app/database_metadata.rb +116 -0
- data/app/deep.rb +58 -0
- data/app/mysql_types.rb +33 -0
- data/app/server.rb +151 -74
- data/app/sql_parser.rb +17 -0
- data/app/sqlui.rb +20 -214
- data/app/sqlui_config.rb +44 -0
- data/app/views/databases.erb +8 -8
- data/bin/sqlui +3 -1
- data/client/resources/sqlui.css +263 -242
- data/client/resources/sqlui.html +8 -5
- data/client/resources/sqlui.js +166 -81
- metadata +9 -2
data/app/sqlui.rb
CHANGED
@@ -1,230 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require 'set'
|
3
|
+
require_relative 'sqlui_config'
|
4
|
+
require_relative 'server'
|
6
5
|
|
7
|
-
# Main
|
8
|
-
class
|
6
|
+
# Main entry point.
|
7
|
+
class Sqlui
|
9
8
|
MAX_ROWS = 1_000
|
10
9
|
|
11
|
-
def initialize(
|
12
|
-
|
13
|
-
|
14
|
-
@name = name
|
15
|
-
@saved_path = saved_path
|
16
|
-
@max_rows = max_rows
|
17
|
-
@resources_dir = File.join(File.expand_path('..', File.dirname(__FILE__)), 'client', 'resources')
|
18
|
-
end
|
19
|
-
|
20
|
-
def get(params)
|
21
|
-
case params[:route]
|
22
|
-
when 'app'
|
23
|
-
{ body: html, status: 200, content_type: 'text/html' }
|
24
|
-
when 'sqlui.css'
|
25
|
-
{ body: css, status: 200, content_type: 'text/css' }
|
26
|
-
when 'sqlui.js'
|
27
|
-
{ body: javascript, status: 200, content_type: 'text/javascript' }
|
28
|
-
when 'metadata'
|
29
|
-
{ body: metadata.to_json, status: 200, content_type: 'application/json' }
|
30
|
-
when 'query_file'
|
31
|
-
{ body: query_file(params).to_json, status: 200, content_type: 'application/json' }
|
32
|
-
else
|
33
|
-
{ body: "unknown route: #{params[:route]}", status: 404, content_type: 'text/plain' }
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def post(params)
|
38
|
-
case params[:route]
|
39
|
-
when 'query'
|
40
|
-
{ body: query(params).to_json, status: 200, content_type: 'application/json' }
|
41
|
-
else
|
42
|
-
{ body: "unknown route: #{params[:route]}", status: 404, content_type: 'text/plain' }
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def html
|
49
|
-
@html ||= File.read(File.join(@resources_dir, 'sqlui.html'))
|
50
|
-
end
|
51
|
-
|
52
|
-
def css
|
53
|
-
@css ||= File.read(File.join(@resources_dir, 'sqlui.css'))
|
54
|
-
end
|
55
|
-
|
56
|
-
def javascript
|
57
|
-
@javascript ||= File.read(File.join(@resources_dir, 'sqlui.js'))
|
58
|
-
end
|
59
|
-
|
60
|
-
def query(params)
|
61
|
-
raise 'missing sql' unless params[:sql]
|
62
|
-
raise 'missing cursor' unless params[:cursor]
|
10
|
+
def initialize(config_file)
|
11
|
+
raise 'you must specify a configuration file' unless config_file
|
12
|
+
raise 'configuration file does not exist' unless File.exist?(config_file)
|
63
13
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
execute_query(sql)
|
68
|
-
end
|
69
|
-
|
70
|
-
def query_file(params)
|
71
|
-
raise 'missing file param' unless params['file']
|
14
|
+
@config = SqluiConfig.new(config_file)
|
15
|
+
@resources_dir = File.join(File.expand_path('..', File.dirname(__FILE__)), 'client', 'resources')
|
72
16
|
|
73
|
-
|
74
|
-
|
17
|
+
# Connect to each database to verify each can be connected to.
|
18
|
+
@config.database_configs.each { |database| database.with_client { |client| client } }
|
75
19
|
end
|
76
20
|
|
77
|
-
def
|
78
|
-
|
21
|
+
def run
|
22
|
+
Server.init_and_run(@config, @resources_dir)
|
79
23
|
end
|
80
24
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
saved: Dir.glob("#{@saved_path}/*.sql").map do |path|
|
86
|
-
{
|
87
|
-
filename: File.basename(path),
|
88
|
-
description: File.readlines(path).take_while { |l| l.start_with?('--') }.map { |l| l.sub(/^-- */, '') }.join
|
89
|
-
}
|
90
|
-
end
|
91
|
-
}
|
92
|
-
|
93
|
-
where_clause = if @table_schema
|
94
|
-
"where table_schema = '#{@table_schema}'"
|
95
|
-
else
|
96
|
-
"where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
|
97
|
-
end
|
98
|
-
column_result = @client.query(
|
99
|
-
<<~SQL
|
100
|
-
select
|
101
|
-
table_schema,
|
102
|
-
table_name,
|
103
|
-
column_name,
|
104
|
-
data_type,
|
105
|
-
character_maximum_length,
|
106
|
-
is_nullable,
|
107
|
-
column_key,
|
108
|
-
column_default,
|
109
|
-
extra
|
110
|
-
from information_schema.columns
|
111
|
-
#{where_clause}
|
112
|
-
order by table_schema, table_name, column_name, ordinal_position;
|
113
|
-
SQL
|
114
|
-
)
|
115
|
-
column_result.each do |row|
|
116
|
-
row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
|
117
|
-
table_schema = row[:table_schema]
|
118
|
-
unless result[:schemas][table_schema]
|
119
|
-
result[:schemas][table_schema] = {
|
120
|
-
tables: {}
|
121
|
-
}
|
122
|
-
end
|
123
|
-
table_name = row[:table_name]
|
124
|
-
tables = result[:schemas][table_schema][:tables]
|
125
|
-
unless tables[table_name]
|
126
|
-
tables[table_name] = {
|
127
|
-
indexes: {},
|
128
|
-
columns: {}
|
129
|
-
}
|
130
|
-
end
|
131
|
-
columns = result[:schemas][table_schema][:tables][table_name][:columns]
|
132
|
-
column_name = row[:column_name]
|
133
|
-
columns[column_name] = {} unless columns[column_name]
|
134
|
-
column = columns[column_name]
|
135
|
-
column[:name] = column_name
|
136
|
-
column[:data_type] = row[:data_type]
|
137
|
-
column[:length] = row[:character_maximum_length]
|
138
|
-
column[:allow_null] = row[:is_nullable]
|
139
|
-
column[:key] = row[:column_key]
|
140
|
-
column[:default] = row[:column_default]
|
141
|
-
column[:extra] = row[:extra]
|
25
|
+
def self.from_command_line(args)
|
26
|
+
if args.include?('-v') || args.include?('--version')
|
27
|
+
puts File.read('.version')
|
28
|
+
exit
|
142
29
|
end
|
143
30
|
|
144
|
-
|
145
|
-
|
146
|
-
else
|
147
|
-
"where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
|
148
|
-
end
|
149
|
-
stats_result = @client.query(
|
150
|
-
<<~SQL
|
151
|
-
select
|
152
|
-
table_schema,
|
153
|
-
table_name,
|
154
|
-
index_name,
|
155
|
-
seq_in_index,
|
156
|
-
non_unique,
|
157
|
-
column_name
|
158
|
-
from information_schema.statistics
|
159
|
-
#{where_clause}
|
160
|
-
order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
|
161
|
-
SQL
|
162
|
-
)
|
163
|
-
stats_result.each do |row|
|
164
|
-
row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
|
165
|
-
table_schema = row[:table_schema]
|
166
|
-
tables = result[:schemas][table_schema][:tables]
|
167
|
-
table_name = row[:table_name]
|
168
|
-
indexes = tables[table_name][:indexes]
|
169
|
-
index_name = row[:index_name]
|
170
|
-
indexes[index_name] = {} unless indexes[index_name]
|
171
|
-
index = indexes[index_name]
|
172
|
-
column_name = row[:column_name]
|
173
|
-
index[column_name] = {}
|
174
|
-
column = index[column_name]
|
175
|
-
column[:name] = index_name
|
176
|
-
column[:seq_in_index] = row[:seq_in_index]
|
177
|
-
column[:non_unique] = row[:non_unique]
|
178
|
-
column[:column_name] = row[:column_name]
|
179
|
-
end
|
180
|
-
|
181
|
-
result
|
182
|
-
end
|
31
|
+
raise 'you must specify a configuration file' unless args.size == 1
|
32
|
+
raise 'configuration file does not exist' unless File.exist?(args[0])
|
183
33
|
|
184
|
-
|
185
|
-
result = @client.query(sql)
|
186
|
-
rows = result.map(&:values)
|
187
|
-
columns = result.first&.keys || []
|
188
|
-
column_types = columns.map { |_| 'string' }
|
189
|
-
unless rows.empty?
|
190
|
-
maybe_non_null_column_value_exemplars = columns.each_with_index.map do |_, index|
|
191
|
-
row = rows.find do |current|
|
192
|
-
!current[index].nil?
|
193
|
-
end
|
194
|
-
row.nil? ? nil : row[index]
|
195
|
-
end
|
196
|
-
column_types = maybe_non_null_column_value_exemplars.map do |value|
|
197
|
-
case value
|
198
|
-
when String, NilClass
|
199
|
-
'string'
|
200
|
-
when Integer
|
201
|
-
'number'
|
202
|
-
when Date, Time
|
203
|
-
'date'
|
204
|
-
else
|
205
|
-
value.class.to_s
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
{
|
210
|
-
query: sql,
|
211
|
-
columns: columns,
|
212
|
-
column_types: column_types,
|
213
|
-
total_rows: rows.size,
|
214
|
-
rows: rows.take(@max_rows)
|
215
|
-
}
|
216
|
-
end
|
217
|
-
|
218
|
-
def find_query_at_cursor(sql, cursor)
|
219
|
-
parts_with_ranges = []
|
220
|
-
sql.scan(/[^;]*;[ \n]*/) { |part| parts_with_ranges << [part, 0, part.size] }
|
221
|
-
parts_with_ranges.inject(0) do |pos, current|
|
222
|
-
current[1] += pos
|
223
|
-
current[2] += pos
|
224
|
-
end
|
225
|
-
part_with_range = parts_with_ranges.find do |current|
|
226
|
-
cursor >= current[1] && cursor < current[2]
|
227
|
-
end || parts_with_ranges[-1]
|
228
|
-
part_with_range[0]
|
34
|
+
Sqlui.new(args[0])
|
229
35
|
end
|
230
36
|
end
|
data/app/sqlui_config.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require_relative 'database_config'
|
5
|
+
require_relative 'args'
|
6
|
+
|
7
|
+
# App config including database configs.
|
8
|
+
class SqluiConfig
|
9
|
+
attr_reader :name, :list_url_path, :database_configs
|
10
|
+
|
11
|
+
def initialize(filename)
|
12
|
+
config = YAML.safe_load(ERB.new(File.read(filename)).result)
|
13
|
+
deep_symbolize!(config)
|
14
|
+
@name = Args.fetch_non_empty_string(config, :name).strip
|
15
|
+
@list_url_path = Args.fetch_non_empty_string(config, :list_url_path).strip
|
16
|
+
raise ArgumentError, 'list_url_path should start with a /' unless @list_url_path.start_with?('/')
|
17
|
+
if @list_url_path.length > 1 && @list_url_path.end_with?('/')
|
18
|
+
raise ArgumentError, 'list_url_path should not end with a /'
|
19
|
+
end
|
20
|
+
|
21
|
+
databases = Args.fetch_non_empty_hash(config, :databases)
|
22
|
+
@database_configs = databases.map do |_, current|
|
23
|
+
DatabaseConfig.new(current)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def database_config_for(url_path:)
|
28
|
+
config = @database_configs.find { |database| database.url_path == url_path }
|
29
|
+
raise ArgumentError, "no config found for path #{url_path}" unless config
|
30
|
+
|
31
|
+
config
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def deep_symbolize!(object)
|
37
|
+
return object unless object.is_a? Hash
|
38
|
+
|
39
|
+
object.transform_keys!(&:to_sym)
|
40
|
+
object.each_value { |child| deep_symbolize!(child) }
|
41
|
+
|
42
|
+
object
|
43
|
+
end
|
44
|
+
end
|
data/app/views/databases.erb
CHANGED
@@ -50,15 +50,15 @@
|
|
50
50
|
</head>
|
51
51
|
|
52
52
|
<body>
|
53
|
-
<h1
|
54
|
-
<%
|
55
|
-
<div class="database" onclick="window.location='<%= "
|
56
|
-
<h2 class='name'><%=
|
57
|
-
<a class='query-link' href="
|
58
|
-
<a class='saved-link' href="
|
59
|
-
<a class='structure-link' href="
|
53
|
+
<h1><% config.name %> Databases</h1>
|
54
|
+
<% config.database_configs.each do |database_config| %>
|
55
|
+
<div class="database" onclick="window.location='<%= "#{database_config.url_path}/app" %>'">
|
56
|
+
<h2 class='name'><%= database_config.display_name %></h2>
|
57
|
+
<a class='query-link' href="<%= database_config.url_path %>/app">query</a>
|
58
|
+
<a class='saved-link' href="<%= database_config.url_path %>/app?tab=saved">saved</a>
|
59
|
+
<a class='structure-link' href="<%= database_config.url_path %>/app?tab=structure">structure</a>
|
60
60
|
<p class='description'>
|
61
|
-
<%=
|
61
|
+
<%= database_config.description %>
|
62
62
|
</p>
|
63
63
|
</div>
|
64
64
|
<% end %>
|