detector 0.5.0 → 0.8.0
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/bin/detector +33 -2
- data/lib/detector/addons/mariadb.rb +26 -0
- data/lib/detector/addons/mysql.rb +47 -0
- data/lib/detector/addons/postgres.rb +62 -0
- data/lib/detector/addons/redis.rb +63 -0
- data/lib/detector/base.rb +18 -0
- data/lib/detector/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d21e960cc4220b3bf67a98d0ce5ef7f367710545173ff09cfcee580cc3895e64
|
4
|
+
data.tar.gz: dba6d148d06ea37444017539b0bb23fd43cb96c9deeedf6235fc8e5cfe3999b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19732b310f2f739e03c478103ebb1de34979145fd867d5beab4c82d8ef14c6405c7b4d9bac18849b79adba3dfc9e0e25c4dd0fcada4d6321e95a0822a9cc3efc
|
7
|
+
data.tar.gz: 318344d8e1f23196e5fe2eef8435aec32729fe488d41b0277bcf567117df1f3dbe105269777310a51344aba2453565adc1506c014d1c972a33b02c8f32d1afe5
|
data/bin/detector
CHANGED
@@ -7,10 +7,23 @@ if ARGV.empty?
|
|
7
7
|
puts "Detector v#{Detector::VERSION}"
|
8
8
|
puts "Usage: detector <URI>"
|
9
9
|
puts "Example: detector \"postgres://user:pass@host:port/dbname\""
|
10
|
+
puts "Additional options:"
|
11
|
+
puts " --table=TABLE_NAME [--database=DB_NAME] : Estimate row count for a specific table"
|
10
12
|
exit 1
|
11
13
|
end
|
12
14
|
|
13
15
|
uri = ARGV[0]
|
16
|
+
options = {}
|
17
|
+
|
18
|
+
# Parse command-line options
|
19
|
+
ARGV[1..-1].each do |arg|
|
20
|
+
if arg.start_with?('--table=')
|
21
|
+
options[:table] = arg.split('=', 2)[1]
|
22
|
+
elsif arg.start_with?('--database=')
|
23
|
+
options[:database] = arg.split('=', 2)[1]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
14
27
|
detector = Detector.detect(uri)
|
15
28
|
|
16
29
|
if detector.nil?
|
@@ -18,12 +31,27 @@ if detector.nil?
|
|
18
31
|
exit 1
|
19
32
|
end
|
20
33
|
|
34
|
+
# If table is specified, show row count estimate and exit
|
35
|
+
if options[:table]
|
36
|
+
count = detector.estimated_row_count(table: options[:table], database: options[:database])
|
37
|
+
if count
|
38
|
+
puts "Estimated row count for #{options[:table]}: #{count}"
|
39
|
+
else
|
40
|
+
puts "Unable to estimate row count for #{options[:table]}"
|
41
|
+
end
|
42
|
+
detector.close
|
43
|
+
exit 0
|
44
|
+
end
|
45
|
+
|
21
46
|
puts "Detector v#{Detector::VERSION}"
|
22
47
|
puts "Detected: #{detector.kind}"
|
23
48
|
puts "Version: #{detector.version}"
|
24
49
|
puts "Host: #{detector.host}:#{detector.port}"
|
25
50
|
|
26
|
-
if detector.
|
51
|
+
if detector.respond_to?(:connection_info) && detector.connection_info
|
52
|
+
conn_info = detector.connection_info
|
53
|
+
puts "Connections: global #{conn_info[:connection_count][:global]}/#{conn_info[:connection_limits][:global]} (user #{conn_info[:connection_count][:user]}/#{conn_info[:connection_limits][:user]})"
|
54
|
+
elsif detector.connection_count && detector.connection_limit
|
27
55
|
usage = detector.connection_usage_percentage
|
28
56
|
puts "Connections: #{detector.connection_count}/#{detector.connection_limit} (#{usage}%)"
|
29
57
|
end
|
@@ -76,4 +104,7 @@ if detector.databases?
|
|
76
104
|
end
|
77
105
|
end
|
78
106
|
end
|
79
|
-
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Make sure to close the connection
|
110
|
+
detector.close
|
@@ -53,6 +53,27 @@ module Detector
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
def connection_info
|
57
|
+
return nil unless connection
|
58
|
+
begin
|
59
|
+
user_limit = connection.query("SELECT @@max_user_connections AS `limit`").first['limit'].to_i
|
60
|
+
user_count = connection.query("SELECT COUNT(*) AS count FROM information_schema.PROCESSLIST WHERE user = USER()").first['count'].to_i
|
61
|
+
global_limit = connection.query("SELECT @@max_connections AS `limit`").first['limit'].to_i
|
62
|
+
global_count = connection.query("SELECT COUNT(*) AS count FROM information_schema.PROCESSLIST").first['count'].to_i
|
63
|
+
|
64
|
+
# If user limit is 0, it means no specific per-user limit (use global)
|
65
|
+
user_limit = global_limit if user_limit == 0
|
66
|
+
|
67
|
+
{
|
68
|
+
connection_count: { user: user_count, global: global_count },
|
69
|
+
connection_limits: { user: user_limit, global: global_limit }
|
70
|
+
}
|
71
|
+
rescue => e
|
72
|
+
puts "Error getting connection info: #{e.message}"
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
56
77
|
def tables(database_name)
|
57
78
|
return [] unless connection
|
58
79
|
|
@@ -108,6 +129,11 @@ module Detector
|
|
108
129
|
|
109
130
|
access_level
|
110
131
|
end
|
132
|
+
|
133
|
+
# MariaDB inherits the estimated_row_count method from MySQL, but we might want to override
|
134
|
+
# with MariaDB-specific optimizations or different statistics approaches in the future
|
135
|
+
|
136
|
+
# MariaDB inherits the close method from MySQL
|
111
137
|
end
|
112
138
|
end
|
113
139
|
|
@@ -93,6 +93,26 @@ module Detector
|
|
93
93
|
connection.query("SHOW VARIABLES LIKE 'max_connections'").first['Value'].to_i
|
94
94
|
end
|
95
95
|
|
96
|
+
def connection_info
|
97
|
+
return nil unless connection
|
98
|
+
begin
|
99
|
+
user_limit = connection.query("SELECT @@max_user_connections AS `limit`").first['limit'].to_i
|
100
|
+
user_count = connection.query("SELECT COUNT(*) AS count FROM information_schema.PROCESSLIST WHERE user = USER()").first['count'].to_i
|
101
|
+
global_limit = connection.query("SELECT @@max_connections AS `limit`").first['limit'].to_i
|
102
|
+
global_count = connection.query("SELECT COUNT(*) AS count FROM information_schema.PROCESSLIST").first['count'].to_i
|
103
|
+
|
104
|
+
# If user limit is 0, it means no specific per-user limit (use global)
|
105
|
+
user_limit = global_limit if user_limit == 0
|
106
|
+
|
107
|
+
{
|
108
|
+
connection_count: { user: user_count, global: global_count },
|
109
|
+
connection_limits: { user: user_limit, global: global_limit }
|
110
|
+
}
|
111
|
+
rescue => e
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
96
116
|
def cli_name
|
97
117
|
"mysql"
|
98
118
|
end
|
@@ -187,6 +207,33 @@ module Detector
|
|
187
207
|
nil
|
188
208
|
end
|
189
209
|
end
|
210
|
+
|
211
|
+
def estimated_row_count(table:, database: nil)
|
212
|
+
return nil unless connection
|
213
|
+
|
214
|
+
# Use current database if none specified
|
215
|
+
db_name = database || info['database']
|
216
|
+
return nil unless db_name
|
217
|
+
|
218
|
+
begin
|
219
|
+
# Query information_schema.tables for the statistics-based row estimate
|
220
|
+
result = connection.query("SELECT table_rows AS estimate
|
221
|
+
FROM information_schema.tables
|
222
|
+
WHERE table_schema = '#{db_name}'
|
223
|
+
AND table_name = '#{table}'").first
|
224
|
+
|
225
|
+
result ? result['estimate'].to_i : nil
|
226
|
+
rescue => e
|
227
|
+
nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def close
|
232
|
+
if @conn
|
233
|
+
@conn.close rescue nil
|
234
|
+
@conn = nil
|
235
|
+
end
|
236
|
+
end
|
190
237
|
end
|
191
238
|
end
|
192
239
|
|
@@ -146,6 +146,28 @@ module Detector
|
|
146
146
|
connection.exec("SELECT current_setting('max_connections')").first['current_setting'].to_i
|
147
147
|
end
|
148
148
|
|
149
|
+
def connection_info
|
150
|
+
return nil unless connection
|
151
|
+
begin
|
152
|
+
global_limit = connection.exec("SELECT current_setting('max_connections')").first['current_setting'].to_i
|
153
|
+
global_count = connection.exec("SELECT count(*) FROM pg_stat_activity").first['count'].to_i
|
154
|
+
|
155
|
+
# For PostgreSQL user connections - depends on per-user limits if set
|
156
|
+
user_limit_result = connection.exec("SELECT rolconnlimit FROM pg_roles WHERE rolname = current_user").first
|
157
|
+
user_limit = user_limit_result['rolconnlimit'].to_i
|
158
|
+
user_limit = global_limit if user_limit <= 0 # If unlimited, use global limit
|
159
|
+
|
160
|
+
user_count = connection.exec("SELECT count(*) FROM pg_stat_activity WHERE usename = current_user").first['count'].to_i
|
161
|
+
|
162
|
+
{
|
163
|
+
connection_count: { user: user_count, global: global_count },
|
164
|
+
connection_limits: { user: user_limit, global: global_limit }
|
165
|
+
}
|
166
|
+
rescue => e
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
149
171
|
def cli_name
|
150
172
|
"psql"
|
151
173
|
end
|
@@ -204,6 +226,46 @@ module Detector
|
|
204
226
|
nil
|
205
227
|
end
|
206
228
|
end
|
229
|
+
|
230
|
+
def estimated_row_count(table:, database: nil)
|
231
|
+
return nil unless connection
|
232
|
+
|
233
|
+
# Use the current database if none is specified
|
234
|
+
db_name = database || current_database
|
235
|
+
|
236
|
+
begin
|
237
|
+
# If we need to query a different database, temporarily connect to it
|
238
|
+
if db_name != current_database
|
239
|
+
# Create a temporary connection to the specified database
|
240
|
+
temp_conn = PG::Connection.new(host: host, port: port, user: uri.user,
|
241
|
+
password: uri.password, dbname: db_name) rescue nil
|
242
|
+
return nil unless temp_conn
|
243
|
+
|
244
|
+
# Use pg_class.reltuples for a fast, statistics-based row estimate
|
245
|
+
count = temp_conn.exec("SELECT reltuples::bigint AS estimate
|
246
|
+
FROM pg_class
|
247
|
+
WHERE relname = '#{table}'").first
|
248
|
+
temp_conn.close
|
249
|
+
return count ? count['estimate'].to_i : nil
|
250
|
+
end
|
251
|
+
|
252
|
+
# Query the current database using pg_class.reltuples
|
253
|
+
count = connection.exec("SELECT reltuples::bigint AS estimate
|
254
|
+
FROM pg_class
|
255
|
+
WHERE relname = '#{table}'").first
|
256
|
+
|
257
|
+
count ? count['estimate'].to_i : nil
|
258
|
+
rescue => e
|
259
|
+
nil
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def close
|
264
|
+
if @conn
|
265
|
+
@conn.close rescue nil
|
266
|
+
@conn = nil
|
267
|
+
end
|
268
|
+
end
|
207
269
|
end
|
208
270
|
end
|
209
271
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'redis'
|
2
|
+
require 'timeout'
|
2
3
|
|
3
4
|
module Detector
|
4
5
|
module Addons
|
@@ -68,6 +69,22 @@ module Detector
|
|
68
69
|
info['maxclients'].to_i rescue 0
|
69
70
|
end
|
70
71
|
|
72
|
+
def connection_info
|
73
|
+
return nil unless info
|
74
|
+
begin
|
75
|
+
# Redis doesn't have per-user connection limits, so user = global
|
76
|
+
global_count = info['connected_clients'].to_i rescue 0
|
77
|
+
global_limit = info['maxclients'].to_i rescue 0
|
78
|
+
|
79
|
+
{
|
80
|
+
connection_count: { user: global_count, global: global_count },
|
81
|
+
connection_limits: { user: global_limit, global: global_limit }
|
82
|
+
}
|
83
|
+
rescue => e
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
71
88
|
def cli_name
|
72
89
|
"redis-cli"
|
73
90
|
end
|
@@ -171,6 +188,52 @@ module Detector
|
|
171
188
|
nil
|
172
189
|
end
|
173
190
|
end
|
191
|
+
|
192
|
+
def estimated_row_count(table:, database: nil)
|
193
|
+
return nil unless connection
|
194
|
+
|
195
|
+
# In Redis, the database is a number (0-15 typically) and "table" concept is closest to key patterns
|
196
|
+
# We'll interpret table parameter as a key pattern
|
197
|
+
|
198
|
+
begin
|
199
|
+
# Set the database if specified
|
200
|
+
if database
|
201
|
+
# Redis db numbers are integers
|
202
|
+
db_num = database.to_s.gsub(/[^0-9]/, '').to_i
|
203
|
+
connection.select(db_num) rescue nil
|
204
|
+
end
|
205
|
+
|
206
|
+
# Count keys matching the pattern (consider this a heuristic approximation)
|
207
|
+
# Use SCAN for larger datasets, as it doesn't block the server
|
208
|
+
count = 0
|
209
|
+
cursor = "0"
|
210
|
+
|
211
|
+
begin
|
212
|
+
# Timeout after a reasonable time to prevent long-running operations
|
213
|
+
Timeout.timeout(5) do
|
214
|
+
loop do
|
215
|
+
cursor, keys = connection.scan(cursor, match: table, count: 1000)
|
216
|
+
count += keys.size
|
217
|
+
break if cursor == "0"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
rescue Timeout::Error
|
221
|
+
# If we time out, return the partial count with a note
|
222
|
+
return count
|
223
|
+
end
|
224
|
+
|
225
|
+
count
|
226
|
+
rescue => e
|
227
|
+
nil
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def close
|
232
|
+
if @conn
|
233
|
+
@conn.quit rescue nil
|
234
|
+
@conn = nil
|
235
|
+
end
|
236
|
+
end
|
174
237
|
end
|
175
238
|
end
|
176
239
|
|
data/lib/detector/base.rb
CHANGED
@@ -232,5 +232,23 @@ module Detector
|
|
232
232
|
return nil unless connection_count && connection_limit && connection_limit > 0
|
233
233
|
(connection_count.to_f / connection_limit.to_f * 100).round(1)
|
234
234
|
end
|
235
|
+
|
236
|
+
def connection_info
|
237
|
+
# Default implementation for databases without user-specific limits
|
238
|
+
return nil unless connection_count && connection_limit
|
239
|
+
{
|
240
|
+
connection_count: { user: connection_count, global: connection_count },
|
241
|
+
connection_limits: { user: connection_limit, global: connection_limit }
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
def estimated_row_count(table:, database: nil)
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
|
249
|
+
def close
|
250
|
+
# Default implementation does nothing
|
251
|
+
# Subclasses should override to close any open connections
|
252
|
+
end
|
235
253
|
end
|
236
254
|
end
|
data/lib/detector/version.rb
CHANGED