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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 666536fd481e1b7dbd4a1425eac6da54aa29a67a9e3cc6247b63de0752accf67
4
- data.tar.gz: 4c06d6e51d8b60d10be5f2f126c65bd0d2cba9a815192c1c0ab7a37edd148974
3
+ metadata.gz: d21e960cc4220b3bf67a98d0ce5ef7f367710545173ff09cfcee580cc3895e64
4
+ data.tar.gz: dba6d148d06ea37444017539b0bb23fd43cb96c9deeedf6235fc8e5cfe3999b7
5
5
  SHA512:
6
- metadata.gz: 14bdd91ff681914a3158dea8798e0a70c8ecc947e9a4be4172bdefb89996dcf3e658127d3d2251481b6f1ae6eaf749a60d152e524c2f4cccaee8135fd12eb68f
7
- data.tar.gz: 28f3d5e64a8bec859a00bba960f5772289f9511d00c7106889b54dfd411428400b06ef3dcc4dce494c0216bc0a4fa929ae266c01239768a0e4e7c1bbc1d9e77c
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.connection_count && detector.connection_limit
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
@@ -1,3 +1,3 @@
1
1
  module Detector
2
- VERSION = "0.5.0"
2
+ VERSION = "0.8.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: detector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel