sqlui 0.1.18 → 0.1.19

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: f951e672b477e0fd4882c1aa4e42472975ac07fa6a2955eb4430bc1018ea6165
4
- data.tar.gz: d8e7da6fe289ed3e94322a3ea3d5ce461356cbfa73e2e2732bab9001a8d3af24
3
+ metadata.gz: 3e42ae906ab341a224ba0904cf74be9224a13e6284e2ead198a382926e84db34
4
+ data.tar.gz: bd8f33a32f30179057914b9f291ff4af084f6917d8123f2bba550d5974df9b6f
5
5
  SHA512:
6
- metadata.gz: 6769e3ee3c0b5330ca0b1ac1433370756838cec3187f0bd9822308704d31ee1608592199acb9b622edba8d45146c557e413d42c0a060bb9ace9b7653b43ad54c
7
- data.tar.gz: 4ef70417125cdb931c5f6edba2ef7986be7bda22f0f27c362d7bea97df81752133493e9507b941cbfe51261fa4e77aca85f4838354805a97d88e64f0240e452d
6
+ metadata.gz: c1f4f3c0f0e180b6aea398da508f7bc551edd19fd77e24f60825bfb33c7f4130e1299770a160947d27854cc7dbd26d76fda6436528561b82720b0652815208dc
7
+ data.tar.gz: 1e275114bf9e63d2182c1a4a2a2390ebe97a14ad5b15d1b21ac0e276fbadce766203e42546cf09b6bbdaf9e0bc4c8abc451aa2756dc02334b2dc89f403ae6447
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.18
1
+ 0.1.19
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mysql2'
4
+
5
+ # Config for a single database.
6
+ class DatabaseConfig
7
+ attr_reader :display_name, :description, :url_path, :saved_path, :client_params, :database
8
+
9
+ def initialize(hash)
10
+ @display_name = hash[:display_name].strip
11
+ @description = hash[:description].strip
12
+ @url_path = hash[:url_path].strip
13
+ raise ArgumentError, 'url_path should start with a /' unless @url_path.start_with?('/')
14
+ raise ArgumentError, 'url_path should not end with a /' if @url_path.length > 1 && @url_path.end_with?('/')
15
+
16
+ @saved_path = hash[:saved_path].strip
17
+ @client_params = hash[:client_params]
18
+ @database = @client_params[:database].strip
19
+ end
20
+
21
+ def with_client(&block)
22
+ client = Mysql2::Client.new(@client_params)
23
+ result = block.call(client)
24
+ client.close
25
+ client = nil
26
+ result
27
+ ensure
28
+ client&.close
29
+ end
30
+ end
data/app/server.rb CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  require 'erb'
4
4
  require 'json'
5
- require 'mysql2'
6
5
  require 'sinatra/base'
7
- require_relative 'sqlui'
8
- require 'yaml'
6
+ require 'uri'
9
7
  require_relative 'environment'
8
+ require_relative 'sql_parser'
9
+ require_relative 'sqlui_config'
10
10
 
11
11
  if ARGV.include?('-v') || ARGV.include?('--version')
12
12
  puts File.read('.version')
@@ -18,78 +18,249 @@ raise 'configuration file does not exist' unless File.exist?(ARGV[0])
18
18
 
19
19
  # SQLUI Sinatra server.
20
20
  class Server < Sinatra::Base
21
+ MAX_ROWS = 1_000
22
+
23
+ CONFIG = SqluiConfig.new(ARGV[0])
24
+
25
+ def initialize(app = nil, **_kwargs)
26
+ super
27
+ @config = Server::CONFIG
28
+ @resources_dir = File.join(File.expand_path('..', File.dirname(__FILE__)), 'client', 'resources')
29
+ end
30
+
21
31
  set :logging, true
22
32
  set :bind, '0.0.0.0'
23
33
  set :port, Environment.server_port
24
34
  set :env, Environment.server_env
25
35
 
26
- # A MySQL client. This needs to go away.
27
- class Client
28
- def initialize(params)
29
- @params = params
36
+ get '/-/health' do
37
+ status 200
38
+ body 'OK'
39
+ end
40
+
41
+ get "#{CONFIG.list_url_path}/?" do
42
+ erb :databases, locals: { config: @config }
43
+ end
44
+
45
+ CONFIG.database_configs.each do |database|
46
+ get database.url_path.to_s do
47
+ redirect "#{params[:database]}/app", 301
30
48
  end
31
49
 
32
- def query(sql)
33
- client = Thread.current.thread_variable_get(:client)
34
- unless client
35
- client = Mysql2::Client.new(@params)
36
- Thread.current.thread_variable_set(:client, client)
50
+ get "#{database.url_path}/app" do
51
+ @html ||= File.read(File.join(@resources_dir, 'sqlui.html'))
52
+ status 200
53
+ headers 'Content-Type': 'text/html'
54
+ body @html
55
+ end
56
+
57
+ get "#{database.url_path}/sqlui.css" do
58
+ @css ||= File.read(File.join(@resources_dir, 'sqlui.css'))
59
+ status 200
60
+ headers 'Content-Type': 'text/css'
61
+ body @css
62
+ end
63
+
64
+ get "#{database.url_path}/sqlui.js" do
65
+ @js ||= File.read(File.join(@resources_dir, 'sqlui.js'))
66
+ status 200
67
+ headers 'Content-Type': 'text/javascript'
68
+ body @js
69
+ end
70
+
71
+ get "#{database.url_path}/metadata" do
72
+ database_config = @config.database_config_for(url_path: database.url_path)
73
+ metadata = database_config.with_client do |client|
74
+ load_metadata(client, database_config.database, database_config.saved_path)
37
75
  end
38
- client.query(sql)
76
+ status 200
77
+ headers 'Content-Type': 'application/json'
78
+ body metadata.to_json
39
79
  end
40
- end
41
80
 
42
- config = YAML.safe_load(ERB.new(File.read(ARGV[0])).result)
43
- saved_path_root = config['saved_path']
44
- client_map = config['databases'].values.to_h do |database_config|
45
- client_params = {
46
- host: database_config['db_host'],
47
- port: database_config['db_port'] || 3306,
48
- username: database_config['db_username'],
49
- password: database_config['db_password'],
50
- database: database_config['db_database'],
51
- read_timeout: 10, # seconds
52
- write_timeout: 0, # seconds
53
- connect_timeout: 5 # seconds
54
- }
55
- client = Client.new(client_params)
56
- [
57
- database_config['url_path'],
58
- ::SQLUI.new(
59
- client: client,
60
- table_schema: database_config['db_database'],
61
- name: database_config['name'],
62
- saved_path: File.join(saved_path_root, database_config['saved_path'])
63
- )
64
- ]
65
- end
81
+ get "#{database.url_path}/query_file" do
82
+ return client_error('missing file param') unless params[:file]
83
+ return client_error('no such file') unless File.exist?(params[:file])
66
84
 
67
- get '/-/health' do
68
- status 200
69
- body 'OK'
70
- end
85
+ database_config = @config.database_config_for(url_path: database.url_path)
86
+ sql = File.read(File.join(database_config.saved_path, params[:file]))
87
+ result = database_config.with_client do |client|
88
+ execute_query(client, sql).tap { |r| r[:file] = params[:file] }
89
+ end
90
+
91
+ status 200
92
+ headers 'Content-Type': 'application/json'
93
+ body result.to_json
94
+ end
95
+
96
+ post "#{database.url_path}/query" do
97
+ params.merge!(JSON.parse(request.body.read, symbolize_names: true))
98
+ return client_error('missing sql') unless params[:sql]
99
+ return client_error('missing cursor') unless params[:cursor]
100
+
101
+ sql = SqlParser.find_statement_at_cursor(params[:sql], Integer(params[:cursor]))
102
+ raise "can't find query at cursor" unless sql
71
103
 
72
- get '/db/?' do
73
- erb :databases, locals: { databases: config['databases'] }
104
+ database_config = @config.database_config_for(url_path: database.url_path)
105
+ result = database_config.with_client do |client|
106
+ execute_query(client, sql)
107
+ end
108
+
109
+ status 200
110
+ headers 'Content-Type': 'application/json'
111
+ body result.to_json
112
+ end
74
113
  end
75
114
 
76
- get '/db/:database' do
77
- redirect "/db/#{params[:database]}/app", 301
115
+ private
116
+
117
+ def client_error(message, stacktrace: nil)
118
+ status(400)
119
+ headers('Content-Type': 'application/json')
120
+ body({ message: message, stacktrace: stacktrace }.compact.to_json)
78
121
  end
79
122
 
80
- get '/db/:database/:route' do
81
- response = client_map[params[:database]].get(params)
82
- status response[:status]
83
- headers 'Content-Type': response[:content_type]
84
- body response[:body]
123
+ def load_metadata(client, database, saved_path)
124
+ result = {
125
+ server: @config.name,
126
+ schemas: {},
127
+ saved: Dir.glob("#{saved_path}/*.sql").map do |path|
128
+ {
129
+ filename: File.basename(path),
130
+ description: File.readlines(path).take_while { |l| l.start_with?('--') }.map { |l| l.sub(/^-- */, '') }.join
131
+ }
132
+ end
133
+ }
134
+
135
+ where_clause = if database
136
+ "where table_schema = '#{database}'"
137
+ else
138
+ "where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
139
+ end
140
+ column_result = client.query(
141
+ <<~SQL
142
+ select
143
+ table_schema,
144
+ table_name,
145
+ column_name,
146
+ data_type,
147
+ character_maximum_length,
148
+ is_nullable,
149
+ column_key,
150
+ column_default,
151
+ extra
152
+ from information_schema.columns
153
+ #{where_clause}
154
+ order by table_schema, table_name, column_name, ordinal_position;
155
+ SQL
156
+ )
157
+ column_result.each do |row|
158
+ row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
159
+ table_schema = row[:table_schema]
160
+ unless result[:schemas][table_schema]
161
+ result[:schemas][table_schema] = {
162
+ tables: {}
163
+ }
164
+ end
165
+ table_name = row[:table_name]
166
+ tables = result[:schemas][table_schema][:tables]
167
+ unless tables[table_name]
168
+ tables[table_name] = {
169
+ indexes: {},
170
+ columns: {}
171
+ }
172
+ end
173
+ columns = result[:schemas][table_schema][:tables][table_name][:columns]
174
+ column_name = row[:column_name]
175
+ columns[column_name] = {} unless columns[column_name]
176
+ column = columns[column_name]
177
+ column[:name] = column_name
178
+ column[:data_type] = row[:data_type]
179
+ column[:length] = row[:character_maximum_length]
180
+ column[:allow_null] = row[:is_nullable]
181
+ column[:key] = row[:column_key]
182
+ column[:default] = row[:column_default]
183
+ column[:extra] = row[:extra]
184
+ end
185
+
186
+ where_clause = if database
187
+ "where table_schema = '#{database}'"
188
+ else
189
+ "where table_schema not in('mysql', 'sys', 'information_schema', 'performance_schema')"
190
+ end
191
+ stats_result = client.query(
192
+ <<~SQL
193
+ select
194
+ table_schema,
195
+ table_name,
196
+ index_name,
197
+ seq_in_index,
198
+ non_unique,
199
+ column_name
200
+ from information_schema.statistics
201
+ #{where_clause}
202
+ order by table_schema, table_name, if(index_name = "PRIMARY", 0, index_name), seq_in_index;
203
+ SQL
204
+ )
205
+ stats_result.each do |row|
206
+ row = row.transform_keys(&:downcase).transform_keys(&:to_sym)
207
+ table_schema = row[:table_schema]
208
+ tables = result[:schemas][table_schema][:tables]
209
+ table_name = row[:table_name]
210
+ indexes = tables[table_name][:indexes]
211
+ index_name = row[:index_name]
212
+ indexes[index_name] = {} unless indexes[index_name]
213
+ index = indexes[index_name]
214
+ column_name = row[:column_name]
215
+ index[column_name] = {}
216
+ column = index[column_name]
217
+ column[:name] = index_name
218
+ column[:seq_in_index] = row[:seq_in_index]
219
+ column[:non_unique] = row[:non_unique]
220
+ column[:column_name] = row[:column_name]
221
+ end
222
+
223
+ result
85
224
  end
86
225
 
87
- post '/db/:database/:route' do
88
- post_body = JSON.parse(request.body.read)
89
- response = client_map[params[:database]].post(params.merge(post_body))
90
- status response[:status]
91
- headers 'Content-Type': response[:content_type]
92
- body response[:body]
226
+ def execute_query(client, sql)
227
+ result = client.query(sql, cast: false)
228
+ rows = result.map(&:values)
229
+ columns = result.first&.keys || []
230
+ # TODO: use field_types
231
+ column_types = columns.map { |_| 'string' }
232
+ unless rows.empty?
233
+ maybe_non_null_column_value_exemplars = columns.each_with_index.map do |_, index|
234
+ row = rows.find do |current|
235
+ !current[index].nil?
236
+ end
237
+ row.nil? ? nil : row[index]
238
+ end
239
+ column_types = maybe_non_null_column_value_exemplars.map do |value|
240
+ case value
241
+ when String, NilClass
242
+ 'string'
243
+ when Integer, Float
244
+ 'number'
245
+ when Date
246
+ 'date'
247
+ when Time
248
+ 'datetime'
249
+ when TrueClass, FalseClass
250
+ 'boolean'
251
+ else
252
+ # TODO: report an error
253
+ value.class.to_s
254
+ end
255
+ end
256
+ end
257
+ {
258
+ query: sql,
259
+ columns: columns,
260
+ column_types: column_types,
261
+ total_rows: rows.size,
262
+ rows: rows.take(MAX_ROWS)
263
+ }
93
264
  end
94
265
 
95
266
  run!
data/app/sql_parser.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Used to parse strings containing one or more SQL statements.
4
+ class SqlParser
5
+ def self.find_statement_at_cursor(sql, cursor)
6
+ parts_with_ranges = []
7
+ sql.scan(/[^;]*;[ \n]*/) { |part| parts_with_ranges << [part, 0, part.size] }
8
+ parts_with_ranges.inject(0) do |pos, current|
9
+ current[1] += pos
10
+ current[2] += pos
11
+ end
12
+ part_with_range = parts_with_ranges.find do |current|
13
+ cursor >= current[1] && cursor < current[2]
14
+ end || parts_with_ranges[-1]
15
+ part_with_range[0].strip
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative 'database_config'
5
+
6
+ # App config including database configs.
7
+ class SqluiConfig
8
+ attr_reader :name, :list_url_path, :database_configs
9
+
10
+ def initialize(filename)
11
+ config = YAML.safe_load(ERB.new(File.read(filename)).result)
12
+ deep_symbolize!(config)
13
+ @name = fetch_non_empty_string(config, :name).strip
14
+ @list_url_path = fetch_non_empty_string(config, :list_url_path).strip
15
+ raise ArgumentError, 'list_url_path should start with a /' unless @list_url_path.start_with?('/')
16
+ if @list_url_path.length > 1 && @list_url_path.end_with?('/')
17
+ raise ArgumentError, 'list_url_path should not end with a /'
18
+ end
19
+
20
+ databases = config[:databases]
21
+ if databases.nil? || !databases.is_a?(Hash) || databases.empty?
22
+ raise ArgumentError, 'required parameter databases missing'
23
+ end
24
+
25
+ @database_configs = databases.map do |_, current|
26
+ DatabaseConfig.new(current)
27
+ end
28
+ end
29
+
30
+ def database_config_for(url_path:)
31
+ @database_configs.find { |database| database.url_path == url_path } || raise("no config found for path #{url_path}")
32
+ end
33
+
34
+ private
35
+
36
+ def fetch_non_empty_string(hash, key)
37
+ value = hash[key]
38
+ raise ArgumentError, "required parameter #{key} missing" if value.nil? || !value.is_a?(String) || value.strip.empty?
39
+
40
+ value.strip
41
+ end
42
+
43
+ def deep_symbolize!(object)
44
+ return unless object.is_a? Hash
45
+
46
+ object.transform_keys!(&:to_sym)
47
+ object.each_value { |child| deep_symbolize!(child) }
48
+ end
49
+ 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 %>
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.18
4
+ version: 0.1.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Dower
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-24 00:00:00.000000000 Z
11
+ date: 2022-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mysql2
@@ -158,9 +158,11 @@ extensions: []
158
158
  extra_rdoc_files: []
159
159
  files:
160
160
  - ".version"
161
+ - app/database_config.rb
161
162
  - app/environment.rb
162
163
  - app/server.rb
163
- - app/sqlui.rb
164
+ - app/sql_parser.rb
165
+ - app/sqlui_config.rb
164
166
  - app/views/databases.erb
165
167
  - bin/sqlui
166
168
  - client/resources/sqlui.css
data/app/sqlui.rb DELETED
@@ -1,230 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'uri'
5
- require 'set'
6
-
7
- # Main SQLUI class responsible for producing web content. This class needs to die.
8
- class SQLUI
9
- MAX_ROWS = 1_000
10
-
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]
63
-
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']
72
-
73
- sql = File.read("#{@saved_path}/#{params['file']}")
74
- execute_query(sql).tap { |r| r[:file] = params[:file] }
75
- end
76
-
77
- def metadata
78
- load_metadata
79
- end
80
-
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]
142
- end
143
-
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
183
-
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]
229
- end
230
- end