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.
data/app/sqlui.rb CHANGED
@@ -1,230 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'uri'
5
- require 'set'
3
+ require_relative 'sqlui_config'
4
+ require_relative 'server'
6
5
 
7
- # Main SQLUI class responsible for producing web content. This class needs to die.
8
- class SQLUI
6
+ # Main entry point.
7
+ class Sqlui
9
8
  MAX_ROWS = 1_000
10
9
 
11
- def initialize(client:, name:, saved_path:, table_schema: nil, max_rows: MAX_ROWS)
12
- @client = client
13
- @table_schema = table_schema
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
- sql = find_query_at_cursor(params[:sql], Integer(params[:cursor]))
65
- raise "can't find query at cursor" unless sql
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
- sql = File.read("#{@saved_path}/#{params['file']}")
74
- execute_query(sql).tap { |r| r[:file] = params[:file] }
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 metadata
78
- load_metadata
21
+ def run
22
+ Server.init_and_run(@config, @resources_dir)
79
23
  end
80
24
 
81
- def load_metadata
82
- result = {
83
- server: @name,
84
- schemas: {},
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
- where_clause = if @table_schema
145
- "where table_schema = '#{@table_schema}'"
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
- def execute_query(sql)
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
@@ -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
@@ -50,15 +50,15 @@
50
50
  </head>
51
51
 
52
52
  <body>
53
- <h1>Databases</h1>
54
- <% databases.values.each do |database| %>
55
- <div class="database" onclick="window.location='<%= "/db/#{database['url_path']}/app" %>'">
56
- <h2 class='name'><%= database['name'] %></h2>
57
- <a class='query-link' href="/db/<%= database['url_path'] %>/app">query</a>
58
- <a class='saved-link' href="/db/<%= database['url_path'] %>/app?tab=saved">saved</a>
59
- <a class='structure-link' href="/db/<%= database['url_path'] %>/app?tab=structure">structure</a>
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
- <%= database['description'] %>
61
+ <%= database_config.description %>
62
62
  </p>
63
63
  </div>
64
64
  <% end %>
data/bin/sqlui CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../app/server'
4
+ require_relative '../app/sqlui'
5
+
6
+ Sqlui.from_command_line(ARGV).run