dbviewer 0.3.1 → 0.3.2
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/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +1 -3
- data/app/controllers/dbviewer/home_controller.rb +6 -3
- data/app/controllers/dbviewer/logs_controller.rb +8 -4
- data/lib/dbviewer/cache_manager.rb +78 -0
- data/lib/dbviewer/configuration.rb +0 -28
- data/lib/dbviewer/database_manager.rb +80 -301
- data/lib/dbviewer/dynamic_model_factory.rb +60 -0
- data/lib/dbviewer/engine.rb +5 -3
- data/lib/dbviewer/error_handler.rb +18 -0
- data/lib/dbviewer/logger.rb +26 -53
- data/lib/dbviewer/query_collection.rb +0 -4
- data/lib/dbviewer/query_executor.rb +91 -0
- data/lib/dbviewer/table_metadata_manager.rb +104 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +10 -15
- metadata +6 -3
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/lib/dbviewer/initializer.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf5af06afd4454013148e7f43eaa347b3726383bd9094fbe74b5e3394482d6e6
|
4
|
+
data.tar.gz: 8a64dc26217a82eda9d7db0723279b24a7d8f0e6f877f97e149e2cb421e8f195
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d15ebd1b469850e457d3ebd5043d26df998b6921c51d83dc6d8fdb6b45bd6dc3f00f8179bb5d39542e26e202607af5c8bde45712ee33a59c2ffed0534d9171a6
|
7
|
+
data.tar.gz: e2addc067f7646dd9d30b80f68552db53a955f8fd9cbbb36e785dda08e3e5ce62b38e23321cdfc98df451f4e50a1ecd32b486ef4a970d28cf20ca36f940b465c
|
@@ -1,8 +1,6 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
class EntityRelationshipDiagramsController < ApplicationController
|
3
3
|
def index
|
4
|
-
@tables = fetch_tables_with_stats
|
5
|
-
|
6
4
|
if @tables.present?
|
7
5
|
@table_relationships = fetch_table_relationships
|
8
6
|
else
|
@@ -11,7 +9,7 @@ module Dbviewer
|
|
11
9
|
end
|
12
10
|
|
13
11
|
respond_to do |format|
|
14
|
-
format.html
|
12
|
+
format.html
|
15
13
|
format.json do
|
16
14
|
render json: {
|
17
15
|
tables: @tables,
|
@@ -1,10 +1,13 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
class HomeController < ApplicationController
|
3
|
-
skip_before_action :set_tables
|
4
|
-
|
5
3
|
def index
|
6
|
-
@tables = fetch_tables_with_stats(include_record_counts: true)
|
7
4
|
@analytics = fetch_database_analytics
|
8
5
|
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def set_tables
|
10
|
+
@tables = fetch_tables_with_stats(include_record_counts: true)
|
11
|
+
end
|
9
12
|
end
|
10
13
|
end
|
@@ -3,7 +3,7 @@ module Dbviewer
|
|
3
3
|
before_action :set_filters, only: [ :index ]
|
4
4
|
|
5
5
|
def index
|
6
|
-
@queries =
|
6
|
+
@queries = dbviewer_logger.recent_queries(
|
7
7
|
limit: @limit,
|
8
8
|
table_filter: @table_filter,
|
9
9
|
request_id: @request_id,
|
@@ -11,16 +11,16 @@ module Dbviewer
|
|
11
11
|
)
|
12
12
|
|
13
13
|
if @request_id.present? || @table_filter.present? || @min_duration.present?
|
14
|
-
@stats =
|
14
|
+
@stats = dbviewer_logger.stats_for_queries(@queries)
|
15
15
|
@filtered_stats = true
|
16
16
|
else
|
17
|
-
@stats =
|
17
|
+
@stats = dbviewer_logger.stats
|
18
18
|
@filtered_stats = false
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
def destroy_all
|
23
|
-
|
23
|
+
dbviewer_logger.clear
|
24
24
|
flash[:success] = "Query logs cleared successfully"
|
25
25
|
|
26
26
|
redirect_to logs_path
|
@@ -35,5 +35,9 @@ module Dbviewer
|
|
35
35
|
@limit = (params[:limit] || 100).to_i
|
36
36
|
@limit = 1000 if @limit > 1000
|
37
37
|
end
|
38
|
+
|
39
|
+
def dbviewer_logger
|
40
|
+
@dbviewer_logger ||= Dbviewer::Logger.instance
|
41
|
+
end
|
38
42
|
end
|
39
43
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
# CacheManager handles caching concerns for the DatabaseManager
|
3
|
+
# It provides an abstraction layer for managing caches efficiently
|
4
|
+
class CacheManager
|
5
|
+
# Initialize the cache manager
|
6
|
+
# @param config [Dbviewer::Configuration] Configuration object
|
7
|
+
def initialize(config = nil)
|
8
|
+
@config = config || Dbviewer.configuration
|
9
|
+
@dynamic_models = {}
|
10
|
+
@table_columns_cache = {}
|
11
|
+
@table_metadata_cache = {}
|
12
|
+
@cache_last_reset = Time.now
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get a model from cache or return nil
|
16
|
+
# @param table_name [String] Name of the table
|
17
|
+
# @return [Class, nil] The cached model or nil if not found
|
18
|
+
def get_model(table_name)
|
19
|
+
@dynamic_models[table_name]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Store a model in the cache
|
23
|
+
# @param table_name [String] Name of the table
|
24
|
+
# @param model [Class] ActiveRecord model class
|
25
|
+
def store_model(table_name, model)
|
26
|
+
@dynamic_models[table_name] = model
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get column information from cache
|
30
|
+
# @param table_name [String] Name of the table
|
31
|
+
# @return [Array<Hash>, nil] The cached column information or nil if not found
|
32
|
+
def get_columns(table_name)
|
33
|
+
@table_columns_cache[table_name]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Store column information in cache
|
37
|
+
# @param table_name [String] Name of the table
|
38
|
+
# @param columns [Array<Hash>] Column information
|
39
|
+
def store_columns(table_name, columns)
|
40
|
+
@table_columns_cache[table_name] = columns
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get table metadata from cache
|
44
|
+
# @param table_name [String] Name of the table
|
45
|
+
# @return [Hash, nil] The cached metadata or nil if not found
|
46
|
+
def get_metadata(table_name)
|
47
|
+
@table_metadata_cache[table_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Store table metadata in cache
|
51
|
+
# @param table_name [String] Name of the table
|
52
|
+
# @param metadata [Hash] Table metadata
|
53
|
+
def store_metadata(table_name, metadata)
|
54
|
+
@table_metadata_cache[table_name] = metadata
|
55
|
+
end
|
56
|
+
|
57
|
+
# Reset caches if they've been around too long
|
58
|
+
def reset_if_needed
|
59
|
+
cache_expiry = @config.cache_expiry || 300
|
60
|
+
|
61
|
+
if Time.now - @cache_last_reset > cache_expiry
|
62
|
+
@table_columns_cache = {}
|
63
|
+
@table_metadata_cache = {}
|
64
|
+
@cache_last_reset = Time.now
|
65
|
+
Rails.logger.debug("[DBViewer] Cache reset due to expiry after #{cache_expiry} seconds")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Clear all caches - useful when schema changes are detected
|
70
|
+
def clear_all
|
71
|
+
@dynamic_models = {}
|
72
|
+
@table_columns_cache = {}
|
73
|
+
@table_metadata_cache = {}
|
74
|
+
@cache_last_reset = Time.now
|
75
|
+
Rails.logger.debug("[DBViewer] All caches cleared")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -48,32 +48,4 @@ module Dbviewer
|
|
48
48
|
@admin_credentials = nil
|
49
49
|
end
|
50
50
|
end
|
51
|
-
|
52
|
-
# Class accessor for configuration
|
53
|
-
class << self
|
54
|
-
attr_accessor :configuration
|
55
|
-
end
|
56
|
-
|
57
|
-
# Configure the engine with a block
|
58
|
-
#
|
59
|
-
# @example
|
60
|
-
# Dbviewer.configure do |config|
|
61
|
-
# config.per_page_options = [10, 25, 50]
|
62
|
-
# config.default_per_page = 25
|
63
|
-
# end
|
64
|
-
def self.configure
|
65
|
-
self.configuration ||= Configuration.new
|
66
|
-
yield(configuration) if block_given?
|
67
|
-
end
|
68
|
-
|
69
|
-
# Reset configuration to defaults
|
70
|
-
def self.reset_configuration
|
71
|
-
self.configuration = Configuration.new
|
72
|
-
end
|
73
|
-
|
74
|
-
# Get the current configuration
|
75
|
-
# Creates a default configuration if none exists
|
76
|
-
def self.configuration
|
77
|
-
@configuration ||= Configuration.new
|
78
|
-
end
|
79
51
|
end
|
@@ -1,114 +1,71 @@
|
|
1
|
+
require "dbviewer/cache_manager"
|
2
|
+
require "dbviewer/table_metadata_manager"
|
3
|
+
require "dbviewer/dynamic_model_factory"
|
4
|
+
require "dbviewer/query_executor"
|
5
|
+
require "dbviewer/error_handler"
|
6
|
+
|
1
7
|
module Dbviewer
|
2
8
|
# DatabaseManager handles all database interactions for the DBViewer engine
|
3
9
|
# It provides methods to access database structure and data
|
4
10
|
class DatabaseManager
|
5
11
|
attr_reader :connection, :adapter_name
|
6
12
|
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# Cache for table column info
|
17
|
-
@@table_columns_cache = {}
|
13
|
+
# Initialize the database manager
|
14
|
+
def initialize
|
15
|
+
ensure_connection
|
16
|
+
@cache_manager = CacheManager.new(self.class.configuration)
|
17
|
+
@table_metadata_manager = TableMetadataManager.new(@connection, @cache_manager)
|
18
|
+
@dynamic_model_factory = DynamicModelFactory.new(@connection, @cache_manager)
|
19
|
+
@query_executor = QueryExecutor.new(@connection, self.class.configuration)
|
20
|
+
reset_cache_if_needed
|
21
|
+
end
|
18
22
|
|
19
|
-
#
|
20
|
-
|
23
|
+
# Get configuration from class method or Dbviewer
|
24
|
+
def self.configuration
|
25
|
+
Dbviewer.configuration
|
26
|
+
end
|
21
27
|
|
22
|
-
#
|
23
|
-
|
28
|
+
# Get default per page from configuration
|
29
|
+
def self.default_per_page
|
30
|
+
configuration.default_per_page
|
31
|
+
end
|
24
32
|
|
25
|
-
#
|
26
|
-
|
33
|
+
# Get max records from configuration
|
34
|
+
def self.max_records
|
35
|
+
configuration.max_records
|
36
|
+
end
|
27
37
|
|
28
|
-
|
29
|
-
|
30
|
-
|
38
|
+
# Get cache expiry from configuration
|
39
|
+
def self.cache_expiry
|
40
|
+
configuration.cache_expiry
|
31
41
|
end
|
32
42
|
|
33
43
|
# Returns a sorted list of all tables in the database
|
34
44
|
# @return [Array<String>] List of table names
|
35
45
|
def tables
|
36
|
-
|
37
|
-
|
38
|
-
begin
|
39
|
-
# Get and sort tables, cache is handled at the database adapter level
|
40
|
-
connection.tables.sort
|
41
|
-
rescue => e
|
42
|
-
Rails.logger.error("[DBViewer] Error retrieving tables: #{e.message}")
|
43
|
-
[]
|
44
|
-
end
|
46
|
+
@table_metadata_manager.tables
|
45
47
|
end
|
46
48
|
|
47
49
|
# Returns column information for a specific table
|
48
50
|
# @param table_name [String] Name of the table
|
49
51
|
# @return [Array<Hash>] List of column details with name, type, null, default
|
50
52
|
def table_columns(table_name)
|
51
|
-
|
52
|
-
|
53
|
-
# Return from cache if available
|
54
|
-
return @@table_columns_cache[table_name] if @@table_columns_cache[table_name]
|
55
|
-
|
56
|
-
begin
|
57
|
-
columns = connection.columns(table_name).map do |column|
|
58
|
-
{
|
59
|
-
name: column.name,
|
60
|
-
type: column.type,
|
61
|
-
null: column.null,
|
62
|
-
default: column.default,
|
63
|
-
primary: column.name == primary_key(table_name)
|
64
|
-
}
|
65
|
-
end
|
66
|
-
|
67
|
-
# Cache the result
|
68
|
-
@@table_columns_cache[table_name] = columns
|
69
|
-
columns
|
70
|
-
rescue => e
|
71
|
-
Rails.logger.error("[DBViewer] Error retrieving columns for table #{table_name}: #{e.message}")
|
72
|
-
[]
|
73
|
-
end
|
53
|
+
@table_metadata_manager.table_columns(table_name)
|
74
54
|
end
|
75
55
|
|
76
56
|
# Get detailed metadata about a table (primary keys, indexes, foreign keys)
|
77
57
|
# @param table_name [String] Name of the table
|
78
58
|
# @return [Hash] Table metadata
|
79
59
|
def table_metadata(table_name)
|
80
|
-
|
81
|
-
|
82
|
-
# Return from cache if available
|
83
|
-
return @@table_metadata_cache[table_name] if @@table_metadata_cache[table_name]
|
84
|
-
|
85
|
-
begin
|
86
|
-
metadata = {
|
87
|
-
primary_key: primary_key(table_name),
|
88
|
-
indexes: fetch_indexes(table_name),
|
89
|
-
foreign_keys: fetch_foreign_keys(table_name)
|
90
|
-
}
|
91
|
-
|
92
|
-
# Cache the result
|
93
|
-
@@table_metadata_cache[table_name] = metadata
|
94
|
-
metadata
|
95
|
-
rescue => e
|
96
|
-
Rails.logger.error("[DBViewer] Error retrieving metadata for table #{table_name}: #{e.message}")
|
97
|
-
{ primary_key: nil, indexes: [], foreign_keys: [] }
|
98
|
-
end
|
60
|
+
@table_metadata_manager.table_metadata(table_name)
|
99
61
|
end
|
100
62
|
|
101
63
|
# Get the total count of records in a table
|
102
64
|
# @param table_name [String] Name of the table
|
103
65
|
# @return [Integer] Number of records
|
104
66
|
def table_count(table_name)
|
105
|
-
|
106
|
-
|
107
|
-
model.count
|
108
|
-
rescue => e
|
109
|
-
Rails.logger.error("[DBViewer] Error counting records in table #{table_name}: #{e.message}")
|
110
|
-
0
|
111
|
-
end
|
67
|
+
model = get_model_for(table_name)
|
68
|
+
model.count
|
112
69
|
end
|
113
70
|
|
114
71
|
# Get records from a table with pagination and sorting
|
@@ -120,13 +77,9 @@ module Dbviewer
|
|
120
77
|
# @return [ActiveRecord::Result] Result set with columns and rows
|
121
78
|
def table_records(table_name, page = 1, order_by = nil, direction = "ASC", per_page = nil)
|
122
79
|
page = [ 1, page.to_i ].max
|
123
|
-
|
124
|
-
|
125
|
-
default_per_page = self.class.respond_to?(:default_per_page) ? self.class.default_per_page : DEFAULT_PER_PAGE
|
126
|
-
max_records = self.class.respond_to?(:max_records) ? self.class.max_records : MAX_RECORDS
|
127
|
-
|
80
|
+
default_per_page = self.class.default_per_page
|
81
|
+
max_records = self.class.max_records
|
128
82
|
per_page = (per_page || default_per_page).to_i
|
129
|
-
per_page = default_per_page if per_page <= 0
|
130
83
|
|
131
84
|
# Ensure we don't fetch too many records for performance/memory reasons
|
132
85
|
per_page = [ per_page, max_records ].min
|
@@ -143,8 +96,11 @@ module Dbviewer
|
|
143
96
|
# Apply pagination
|
144
97
|
records = query.limit(per_page).offset((page - 1) * per_page)
|
145
98
|
|
146
|
-
#
|
147
|
-
|
99
|
+
# Get column names for consistent ordering
|
100
|
+
column_names = table_columns(table_name).map { |c| c[:name] }
|
101
|
+
|
102
|
+
# Format results
|
103
|
+
@query_executor.to_result_set(records, column_names)
|
148
104
|
end
|
149
105
|
|
150
106
|
# Get the number of records in a table (alias for table_count)
|
@@ -158,24 +114,14 @@ module Dbviewer
|
|
158
114
|
# @param table_name [String] Name of the table
|
159
115
|
# @return [Integer] Number of columns
|
160
116
|
def column_count(table_name)
|
161
|
-
|
162
|
-
table_columns(table_name).size
|
163
|
-
rescue => e
|
164
|
-
Rails.logger.error("[DBViewer] Error counting columns in table #{table_name}: #{e.message}")
|
165
|
-
0
|
166
|
-
end
|
117
|
+
table_columns(table_name).size
|
167
118
|
end
|
168
119
|
|
169
120
|
# Get the primary key of a table
|
170
121
|
# @param table_name [String] Name of the table
|
171
122
|
# @return [String, nil] Primary key column name or nil if not found
|
172
123
|
def primary_key(table_name)
|
173
|
-
|
174
|
-
connection.primary_key(table_name)
|
175
|
-
rescue => e
|
176
|
-
Rails.logger.error("[DBViewer] Error retrieving primary key for table #{table_name}: #{e.message}")
|
177
|
-
nil
|
178
|
-
end
|
124
|
+
@table_metadata_manager.primary_key(table_name)
|
179
125
|
end
|
180
126
|
|
181
127
|
# Check if a column exists in a table
|
@@ -183,15 +129,7 @@ module Dbviewer
|
|
183
129
|
# @param column_name [String] Name of the column
|
184
130
|
# @return [Boolean] true if column exists, false otherwise
|
185
131
|
def column_exists?(table_name, column_name)
|
186
|
-
|
187
|
-
|
188
|
-
begin
|
189
|
-
columns = table_columns(table_name)
|
190
|
-
columns.any? { |col| col[:name].to_s == column_name.to_s }
|
191
|
-
rescue => e
|
192
|
-
Rails.logger.error("[DBViewer] Error checking column existence for #{column_name} in #{table_name}: #{e.message}")
|
193
|
-
false
|
194
|
-
end
|
132
|
+
@table_metadata_manager.column_exists?(table_name, column_name)
|
195
133
|
end
|
196
134
|
|
197
135
|
# Execute a raw SQL query after validating for safety
|
@@ -199,31 +137,7 @@ module Dbviewer
|
|
199
137
|
# @return [ActiveRecord::Result] Result set with columns and rows
|
200
138
|
# @raise [StandardError] If the query is invalid or unsafe
|
201
139
|
def execute_query(sql)
|
202
|
-
|
203
|
-
begin
|
204
|
-
# Validate and normalize the SQL
|
205
|
-
normalized_sql = ::Dbviewer::SqlValidator.validate!(sql.to_s)
|
206
|
-
|
207
|
-
# Get max records from configuration if available
|
208
|
-
max_records = self.class.respond_to?(:max_records) ? self.class.max_records : MAX_RECORDS
|
209
|
-
|
210
|
-
# Add a safety limit if not already present
|
211
|
-
unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
|
212
|
-
normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
|
213
|
-
end
|
214
|
-
|
215
|
-
# Log and execute the query
|
216
|
-
Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
|
217
|
-
start_time = Time.now
|
218
|
-
result = connection.exec_query(normalized_sql)
|
219
|
-
duration = Time.now - start_time
|
220
|
-
|
221
|
-
Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
|
222
|
-
result
|
223
|
-
rescue => e
|
224
|
-
Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
|
225
|
-
raise e
|
226
|
-
end
|
140
|
+
@query_executor.execute_query(sql)
|
227
141
|
end
|
228
142
|
|
229
143
|
# Execute a SQLite PRAGMA command without adding a LIMIT clause
|
@@ -231,16 +145,7 @@ module Dbviewer
|
|
231
145
|
# @return [ActiveRecord::Result] Result set with the PRAGMA value
|
232
146
|
# @raise [StandardError] If the query is invalid or cannot be executed
|
233
147
|
def execute_sqlite_pragma(pragma)
|
234
|
-
|
235
|
-
sql = "PRAGMA #{pragma}"
|
236
|
-
Rails.logger.debug("[DBViewer] Executing SQLite pragma: #{sql}")
|
237
|
-
result = connection.exec_query(sql)
|
238
|
-
Rails.logger.debug("[DBViewer] Pragma completed, returned #{result.rows.size} rows")
|
239
|
-
result
|
240
|
-
rescue => error
|
241
|
-
Rails.logger.error("[DBViewer] SQLite pragma error: #{error.message} for pragma: #{pragma}")
|
242
|
-
raise error
|
243
|
-
end
|
148
|
+
@query_executor.execute_sqlite_pragma(pragma)
|
244
149
|
end
|
245
150
|
|
246
151
|
# Query a table with more granular control using ActiveRecord
|
@@ -252,76 +157,47 @@ module Dbviewer
|
|
252
157
|
# @param where [String, Hash] Where conditions
|
253
158
|
# @return [ActiveRecord::Result] Result set with columns and rows
|
254
159
|
def query_table(table_name, select: nil, order: nil, limit: nil, offset: nil, where: nil)
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
160
|
+
model = get_model_for(table_name)
|
161
|
+
query = model.all
|
162
|
+
|
163
|
+
query = query.select(select) if select.present?
|
164
|
+
query = query.where(where) if where.present?
|
165
|
+
query = query.order(order) if order.present?
|
166
|
+
|
167
|
+
# Get max records from configuration
|
168
|
+
max_records = self.class.max_records
|
169
|
+
query = query.limit([ limit || max_records, max_records ].min) # Apply safety limit
|
170
|
+
query = query.offset(offset) if offset.present?
|
171
|
+
|
172
|
+
# Get column names for the result set
|
173
|
+
column_names = if select.is_a?(Array)
|
174
|
+
select
|
175
|
+
elsif select.is_a?(String) && !select.include?("*")
|
176
|
+
select.split(",").map(&:strip)
|
177
|
+
else
|
178
|
+
table_columns(table_name).map { |c| c[:name] }
|
272
179
|
end
|
180
|
+
|
181
|
+
@query_executor.to_result_set(query, column_names)
|
273
182
|
end
|
274
183
|
|
275
184
|
# Get table indexes
|
276
185
|
# @param table_name [String] Name of the table
|
277
186
|
# @return [Array<Hash>] List of indexes with details
|
278
187
|
def fetch_indexes(table_name)
|
279
|
-
|
280
|
-
|
281
|
-
begin
|
282
|
-
# Only some adapters support index retrieval
|
283
|
-
if connection.respond_to?(:indexes)
|
284
|
-
connection.indexes(table_name).map do |index|
|
285
|
-
{
|
286
|
-
name: index.name,
|
287
|
-
columns: index.columns,
|
288
|
-
unique: index.unique
|
289
|
-
}
|
290
|
-
end
|
291
|
-
else
|
292
|
-
[]
|
293
|
-
end
|
294
|
-
rescue => e
|
295
|
-
Rails.logger.error("[DBViewer] Error retrieving indexes for table #{table_name}: #{e.message}")
|
296
|
-
[]
|
297
|
-
end
|
188
|
+
@table_metadata_manager.fetch_indexes(table_name)
|
298
189
|
end
|
299
190
|
|
300
191
|
# Get foreign keys
|
301
192
|
# @param table_name [String] Name of the table
|
302
193
|
# @return [Array<Hash>] List of foreign keys with details
|
303
194
|
def fetch_foreign_keys(table_name)
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
{
|
311
|
-
name: fk.name,
|
312
|
-
from_table: fk.from_table,
|
313
|
-
to_table: fk.to_table,
|
314
|
-
column: fk.column,
|
315
|
-
primary_key: fk.primary_key
|
316
|
-
}
|
317
|
-
end
|
318
|
-
else
|
319
|
-
[]
|
320
|
-
end
|
321
|
-
rescue => e
|
322
|
-
Rails.logger.error("[DBViewer] Error retrieving foreign keys for table #{table_name}: #{e.message}")
|
323
|
-
[]
|
324
|
-
end
|
195
|
+
@table_metadata_manager.fetch_foreign_keys(table_name)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Clear all caches - useful when schema changes are detected
|
199
|
+
def clear_all_caches
|
200
|
+
@cache_manager.clear_all
|
325
201
|
end
|
326
202
|
|
327
203
|
private
|
@@ -331,120 +207,23 @@ module Dbviewer
|
|
331
207
|
def ensure_connection
|
332
208
|
return @connection if @connection
|
333
209
|
|
334
|
-
|
210
|
+
ErrorHandler.with_error_handling("establishing database connection") do
|
335
211
|
@connection = ActiveRecord::Base.connection
|
336
212
|
@adapter_name = @connection.adapter_name.downcase
|
337
213
|
@connection
|
338
|
-
rescue => e
|
339
|
-
Rails.logger.error("[DBViewer] Database connection error: #{e.message}")
|
340
|
-
raise e
|
341
214
|
end
|
342
215
|
end
|
343
216
|
|
344
217
|
# Reset caches if they've been around too long
|
345
218
|
def reset_cache_if_needed
|
346
|
-
|
347
|
-
cache_expiry = self.class.respond_to?(:cache_expiry) ? self.class.cache_expiry : CACHE_EXPIRY
|
348
|
-
|
349
|
-
if Time.now - @@cache_last_reset > cache_expiry
|
350
|
-
@@table_columns_cache = {}
|
351
|
-
@@table_metadata_cache = {}
|
352
|
-
@@cache_last_reset = Time.now
|
353
|
-
Rails.logger.debug("[DBViewer] Cache reset due to expiry after #{cache_expiry} seconds")
|
354
|
-
end
|
219
|
+
@cache_manager.reset_if_needed
|
355
220
|
end
|
356
221
|
|
357
|
-
#
|
358
|
-
def clear_all_caches
|
359
|
-
@@dynamic_models = {}
|
360
|
-
@@table_columns_cache = {}
|
361
|
-
@@table_metadata_cache = {}
|
362
|
-
@@cache_last_reset = Time.now
|
363
|
-
Rails.logger.debug("[DBViewer] All caches cleared")
|
364
|
-
end
|
365
|
-
|
366
|
-
# Dynamically create an ActiveRecord model for a table
|
222
|
+
# Get a dynamic AR model for a table
|
367
223
|
# @param table_name [String] Name of the table
|
368
|
-
# @return [Class] ActiveRecord model class
|
224
|
+
# @return [Class] ActiveRecord model class
|
369
225
|
def get_model_for(table_name)
|
370
|
-
|
371
|
-
|
372
|
-
begin
|
373
|
-
model_name = table_name.classify
|
374
|
-
|
375
|
-
# Return cached model if available
|
376
|
-
return @@dynamic_models[table_name] if @@dynamic_models[table_name]
|
377
|
-
|
378
|
-
# Create a new model class dynamically
|
379
|
-
model = Class.new(ActiveRecord::Base) do
|
380
|
-
self.table_name = table_name
|
381
|
-
|
382
|
-
# Some tables might not have primary keys, so we handle that
|
383
|
-
begin
|
384
|
-
primary_key = connection.primary_key(table_name)
|
385
|
-
self.primary_key = primary_key if primary_key.present?
|
386
|
-
rescue => e
|
387
|
-
# If we can't determine the primary key, default to id or set to nil
|
388
|
-
self.primary_key = "id"
|
389
|
-
end
|
390
|
-
|
391
|
-
# Disable STI
|
392
|
-
self.inheritance_column = :_type_disabled
|
393
|
-
|
394
|
-
# Disable timestamps for better compatibility
|
395
|
-
self.record_timestamps = false
|
396
|
-
end
|
397
|
-
|
398
|
-
# Set model name constant if not already taken
|
399
|
-
# Use a namespace to avoid polluting the global namespace
|
400
|
-
unless Dbviewer.const_defined?("DynamicModel_#{model_name}")
|
401
|
-
Dbviewer.const_set("DynamicModel_#{model_name}", model)
|
402
|
-
end
|
403
|
-
|
404
|
-
# Cache the model
|
405
|
-
@@dynamic_models[table_name] = model
|
406
|
-
model
|
407
|
-
rescue => e
|
408
|
-
Rails.logger.error("[DBViewer] Error creating model for table #{table_name}: #{e.message}")
|
409
|
-
raise e
|
410
|
-
end
|
411
|
-
end
|
412
|
-
|
413
|
-
# Convert ActiveRecord::Relation to the expected result format
|
414
|
-
# @param records [ActiveRecord::Relation] Records to convert
|
415
|
-
# @param table_name [String] Name of the table
|
416
|
-
# @return [ActiveRecord::Result] Result set with columns and rows
|
417
|
-
def to_result_set(records, table_name)
|
418
|
-
begin
|
419
|
-
column_names = table_columns(table_name).map { |c| c[:name] }
|
420
|
-
|
421
|
-
rows = records.map do |record|
|
422
|
-
column_names.map do |col|
|
423
|
-
# Handle serialized attributes
|
424
|
-
value = record[col]
|
425
|
-
serialize_if_needed(value)
|
426
|
-
end
|
427
|
-
end
|
428
|
-
|
429
|
-
ActiveRecord::Result.new(column_names, rows)
|
430
|
-
rescue => e
|
431
|
-
Rails.logger.error("[DBViewer] Error converting to result set for table #{table_name}: #{e.message}")
|
432
|
-
ActiveRecord::Result.new([], [])
|
433
|
-
end
|
434
|
-
end
|
435
|
-
|
436
|
-
# Serialize complex objects for display
|
437
|
-
# @param value [Object] Value to serialize
|
438
|
-
# @return [String, Object] Serialized value or original value
|
439
|
-
def serialize_if_needed(value)
|
440
|
-
case value
|
441
|
-
when Hash, Array
|
442
|
-
value.to_json rescue value.to_s
|
443
|
-
when Time, Date, DateTime
|
444
|
-
value.to_s
|
445
|
-
else
|
446
|
-
value
|
447
|
-
end
|
226
|
+
@dynamic_model_factory.get_model_for(table_name)
|
448
227
|
end
|
449
228
|
end
|
450
229
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
# DynamicModelFactory creates and manages ActiveRecord models for database tables
|
3
|
+
class DynamicModelFactory
|
4
|
+
# Initialize with a connection and cache manager
|
5
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
6
|
+
# @param cache_manager [Dbviewer::CacheManager] Cache manager instance
|
7
|
+
def initialize(connection, cache_manager)
|
8
|
+
@connection = connection
|
9
|
+
@cache_manager = cache_manager
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get or create an ActiveRecord model for a table
|
13
|
+
# @param table_name [String] Name of the table
|
14
|
+
# @return [Class] ActiveRecord model class for the table
|
15
|
+
def get_model_for(table_name)
|
16
|
+
cached_model = @cache_manager.get_model(table_name)
|
17
|
+
return cached_model if cached_model
|
18
|
+
|
19
|
+
model = create_model_for(table_name)
|
20
|
+
@cache_manager.store_model(table_name, model)
|
21
|
+
model
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Create a new ActiveRecord model for a table
|
27
|
+
# @param table_name [String] Name of the table
|
28
|
+
# @return [Class] ActiveRecord model class for the table
|
29
|
+
def create_model_for(table_name)
|
30
|
+
model_name = table_name.classify
|
31
|
+
|
32
|
+
# Create a new model class dynamically
|
33
|
+
model = Class.new(ActiveRecord::Base) do
|
34
|
+
self.table_name = table_name
|
35
|
+
|
36
|
+
# Some tables might not have primary keys, so we handle that
|
37
|
+
begin
|
38
|
+
primary_key = connection.primary_key(table_name)
|
39
|
+
self.primary_key = primary_key if primary_key.present?
|
40
|
+
rescue
|
41
|
+
self.primary_key = "id"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Disable STI
|
45
|
+
self.inheritance_column = :_type_disabled
|
46
|
+
|
47
|
+
# Disable timestamps for better compatibility
|
48
|
+
self.record_timestamps = false
|
49
|
+
end
|
50
|
+
|
51
|
+
# Set model name constant if not already taken
|
52
|
+
# Use a namespace to avoid polluting the global namespace
|
53
|
+
unless Dbviewer.const_defined?("DynamicModel_#{model_name}")
|
54
|
+
Dbviewer.const_set("DynamicModel_#{model_name}", model)
|
55
|
+
end
|
56
|
+
|
57
|
+
model
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/dbviewer/engine.rb
CHANGED
@@ -12,9 +12,11 @@ module Dbviewer
|
|
12
12
|
Dbviewer.setup
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
initializer "dbviewer.notifications" do
|
16
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
17
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
18
|
+
Logger.instance.add(event)
|
19
|
+
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
# ErrorHandler provides centralized error handling for database operations
|
3
|
+
class ErrorHandler
|
4
|
+
class << self
|
5
|
+
# Execute a block with error handling
|
6
|
+
# @param operation_name [String] Description of the operation for logging
|
7
|
+
# @param default_return [Object] Value to return on error
|
8
|
+
# @yield Block to execute
|
9
|
+
# @return [Object] Result of block or default value on error
|
10
|
+
def with_error_handling(operation_name, default_return = nil)
|
11
|
+
yield
|
12
|
+
rescue => e
|
13
|
+
Rails.logger.error("[DBViewer] Error #{operation_name}: #{e.message}")
|
14
|
+
default_return
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/dbviewer/logger.rb
CHANGED
@@ -4,13 +4,25 @@ module Dbviewer
|
|
4
4
|
include Singleton
|
5
5
|
|
6
6
|
def initialize
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@mutex = Mutex.new
|
7
|
+
set_storage
|
8
|
+
Rails.logger.info("[DBViewer] Query Logger initialized with #{mode} storage mode")
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
11
|
+
# Add a new SQL event query to the logger
|
12
|
+
def add(event)
|
13
|
+
return if QueryParser.should_skip_query?(event)
|
14
|
+
|
15
|
+
current_time = Time.now
|
16
|
+
@storage.add({
|
17
|
+
sql: event.payload[:sql],
|
18
|
+
name: event.payload[:name],
|
19
|
+
timestamp: current_time,
|
20
|
+
duration_ms: event.duration.round(2),
|
21
|
+
binds: QueryParser.format_binds(event.payload[:binds]),
|
22
|
+
request_id: event.transaction_id || current_time.to_i,
|
23
|
+
thread_id: Thread.current.object_id.to_s,
|
24
|
+
caller: event.payload[:caller]
|
25
|
+
})
|
14
26
|
end
|
15
27
|
|
16
28
|
# Clear all stored queries
|
@@ -38,65 +50,26 @@ module Dbviewer
|
|
38
50
|
QueryAnalyzer.generate_stats(queries)
|
39
51
|
end
|
40
52
|
|
53
|
+
class << self
|
54
|
+
extend Forwardable
|
55
|
+
|
56
|
+
# Delegate add method to the singleton instance so that it can be called directly on sql events
|
57
|
+
def_delegators :instance, :add
|
58
|
+
end
|
59
|
+
|
41
60
|
private
|
42
61
|
|
43
62
|
def mode
|
44
63
|
@mode ||= Dbviewer.configuration.query_logging_mode || :memory
|
45
64
|
end
|
46
65
|
|
47
|
-
|
48
|
-
def setup_storage
|
66
|
+
def set_storage
|
49
67
|
@storage ||= case mode
|
50
68
|
when :file
|
51
69
|
Storage::FileStorage.new
|
52
70
|
else
|
53
71
|
Storage::InMemoryStorage.new
|
54
72
|
end
|
55
|
-
|
56
|
-
Rails.logger.info("[DBViewer] Query Logger initialized with #{mode} storage mode")
|
57
|
-
end
|
58
|
-
|
59
|
-
def subscribe_to_sql_notifications
|
60
|
-
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
|
61
|
-
event = ActiveSupport::Notifications::Event.new(*args)
|
62
|
-
|
63
|
-
# Skip internal queries and schema queries
|
64
|
-
next if QueryParser.should_skip_query?(event)
|
65
|
-
|
66
|
-
# Record the query details
|
67
|
-
process_sql_event(event)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def process_sql_event(event)
|
72
|
-
@mutex.synchronize do
|
73
|
-
current_time = Time.now
|
74
|
-
update_request_id(current_time)
|
75
|
-
@last_query_time = current_time
|
76
|
-
|
77
|
-
# Create and store the query
|
78
|
-
@storage.add({
|
79
|
-
sql: event.payload[:sql],
|
80
|
-
name: event.payload[:name],
|
81
|
-
timestamp: current_time,
|
82
|
-
duration_ms: event.duration.round(2),
|
83
|
-
binds: QueryParser.format_binds(event.payload[:binds]),
|
84
|
-
request_id: @current_request_id,
|
85
|
-
thread_id: Thread.current.object_id.to_s,
|
86
|
-
caller: event.payload[:caller]
|
87
|
-
})
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
# Generate a request ID if:
|
92
|
-
# 1. This is the first query, or
|
93
|
-
# 2. More than 1 second has passed since the last query
|
94
|
-
#
|
95
|
-
# TODO: explore rails request ID and implement it here, otherwise fallback to current approach
|
96
|
-
def update_request_id(current_time)
|
97
|
-
if @current_request_id.nil? || @last_query_time.nil? || (current_time - @last_query_time) > 1.0
|
98
|
-
@current_request_id = "req-#{Time.now.to_i}-#{@request_counter += 1}"
|
99
|
-
end
|
100
73
|
end
|
101
74
|
end
|
102
75
|
end
|
@@ -6,10 +6,6 @@ module Dbviewer
|
|
6
6
|
# Maximum number of queries to keep in memory
|
7
7
|
MAX_QUERIES = 1000
|
8
8
|
|
9
|
-
def initialize
|
10
|
-
super
|
11
|
-
end
|
12
|
-
|
13
9
|
# Get recent queries, optionally filtered
|
14
10
|
def filter(limit: 100, table_filter: nil, request_id: nil, min_duration: nil)
|
15
11
|
result = all
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
# QueryExecutor handles executing SQL queries and formatting results
|
3
|
+
class QueryExecutor
|
4
|
+
# Initialize with a connection and configuration
|
5
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
6
|
+
# @param config [Dbviewer::Configuration] Configuration object
|
7
|
+
def initialize(connection, config = nil)
|
8
|
+
@connection = connection
|
9
|
+
@config = config || Dbviewer.configuration
|
10
|
+
end
|
11
|
+
|
12
|
+
# Execute a raw SQL query after validating for safety
|
13
|
+
# @param sql [String] SQL query to execute
|
14
|
+
# @return [ActiveRecord::Result] Result set with columns and rows
|
15
|
+
# @raise [StandardError] If the query is invalid or unsafe
|
16
|
+
def execute_query(sql)
|
17
|
+
# Validate and normalize the SQL
|
18
|
+
normalized_sql = ::Dbviewer::SqlValidator.validate!(sql.to_s)
|
19
|
+
|
20
|
+
# Get max records from configuration
|
21
|
+
max_records = @config.max_records || 10000
|
22
|
+
|
23
|
+
# Add a safety limit if not already present
|
24
|
+
unless normalized_sql =~ /\bLIMIT\s+\d+\s*$/i
|
25
|
+
normalized_sql = "#{normalized_sql} LIMIT #{max_records}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Log and execute the query
|
29
|
+
Rails.logger.debug("[DBViewer] Executing SQL query: #{normalized_sql}")
|
30
|
+
start_time = Time.now
|
31
|
+
result = @connection.exec_query(normalized_sql)
|
32
|
+
duration = Time.now - start_time
|
33
|
+
|
34
|
+
Rails.logger.debug("[DBViewer] Query completed in #{duration.round(2)}s, returned #{result.rows.size} rows")
|
35
|
+
result
|
36
|
+
rescue => e
|
37
|
+
Rails.logger.error("[DBViewer] SQL query error: #{e.message} for query: #{sql}")
|
38
|
+
raise e
|
39
|
+
end
|
40
|
+
|
41
|
+
# Execute a SQLite PRAGMA command without adding a LIMIT clause
|
42
|
+
# @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
|
43
|
+
# @return [ActiveRecord::Result] Result set with the PRAGMA value
|
44
|
+
# @raise [StandardError] If the query is invalid or cannot be executed
|
45
|
+
def execute_sqlite_pragma(pragma)
|
46
|
+
sql = "PRAGMA #{pragma}"
|
47
|
+
Rails.logger.debug("[DBViewer] Executing SQLite pragma: #{sql}")
|
48
|
+
result = @connection.exec_query(sql)
|
49
|
+
Rails.logger.debug("[DBViewer] Pragma completed, returned #{result.rows.size} rows")
|
50
|
+
result
|
51
|
+
rescue => e
|
52
|
+
Rails.logger.error("[DBViewer] SQLite pragma error: #{e.message} for pragma: #{pragma}")
|
53
|
+
raise e
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convert ActiveRecord::Relation to a standard result format
|
57
|
+
# @param records [ActiveRecord::Relation] Records to convert
|
58
|
+
# @param column_names [Array<String>] Column names
|
59
|
+
# @return [ActiveRecord::Result] Result set with columns and rows
|
60
|
+
def to_result_set(records, column_names)
|
61
|
+
rows = records.map do |record|
|
62
|
+
column_names.map do |col|
|
63
|
+
# Handle serialized attributes
|
64
|
+
value = record[col]
|
65
|
+
serialize_if_needed(value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
ActiveRecord::Result.new(column_names, rows)
|
70
|
+
rescue => e
|
71
|
+
Rails.logger.error("[DBViewer] Error converting to result set: #{e.message}")
|
72
|
+
ActiveRecord::Result.new([], [])
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Serialize complex objects for display
|
78
|
+
# @param value [Object] Value to serialize
|
79
|
+
# @return [String, Object] Serialized value or original value
|
80
|
+
def serialize_if_needed(value)
|
81
|
+
case value
|
82
|
+
when Hash, Array
|
83
|
+
value.to_json rescue value.to_s
|
84
|
+
when Time, Date, DateTime
|
85
|
+
value.to_s
|
86
|
+
else
|
87
|
+
value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
# TableMetadataManager handles retrieving and managing table structure information
|
3
|
+
class TableMetadataManager
|
4
|
+
# Initialize with a connection and cache manager
|
5
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
6
|
+
# @param cache_manager [Dbviewer::CacheManager] Cache manager instance
|
7
|
+
def initialize(connection, cache_manager)
|
8
|
+
@connection = connection
|
9
|
+
@cache_manager = cache_manager
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get all tables in the database
|
13
|
+
# @return [Array<String>] List of table names
|
14
|
+
def tables
|
15
|
+
@connection.tables.sort
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get column information for a table
|
19
|
+
# @param table_name [String] Name of the table
|
20
|
+
# @return [Array<Hash>] List of column details
|
21
|
+
def table_columns(table_name)
|
22
|
+
cached_columns = @cache_manager.get_columns(table_name)
|
23
|
+
return cached_columns if cached_columns
|
24
|
+
|
25
|
+
columns = @connection.columns(table_name).map do |column|
|
26
|
+
{
|
27
|
+
name: column.name,
|
28
|
+
type: column.type,
|
29
|
+
null: column.null,
|
30
|
+
default: column.default,
|
31
|
+
primary: column.name == primary_key(table_name)
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Cache the result
|
36
|
+
@cache_manager.store_columns(table_name, columns)
|
37
|
+
columns
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get detailed metadata about a table
|
41
|
+
# @param table_name [String] Name of the table
|
42
|
+
# @return [Hash] Table metadata
|
43
|
+
def table_metadata(table_name)
|
44
|
+
cached_metadata = @cache_manager.get_metadata(table_name)
|
45
|
+
return cached_metadata if cached_metadata
|
46
|
+
|
47
|
+
metadata = {
|
48
|
+
primary_key: primary_key(table_name),
|
49
|
+
indexes: fetch_indexes(table_name),
|
50
|
+
foreign_keys: fetch_foreign_keys(table_name)
|
51
|
+
}
|
52
|
+
|
53
|
+
@cache_manager.store_metadata(table_name, metadata)
|
54
|
+
metadata
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get the primary key of a table
|
58
|
+
# @param table_name [String] Name of the table
|
59
|
+
# @return [String, nil] Primary key column name or nil if not found
|
60
|
+
def primary_key(table_name)
|
61
|
+
@connection.primary_key(table_name)
|
62
|
+
rescue => e
|
63
|
+
Rails.logger.error("[DBViewer] Error retrieving primary key for table #{table_name}: #{e.message}")
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check if a column exists in a table
|
68
|
+
# @param table_name [String] Name of the table
|
69
|
+
# @param column_name [String] Name of the column
|
70
|
+
# @return [Boolean] true if column exists, false otherwise
|
71
|
+
def column_exists?(table_name, column_name)
|
72
|
+
columns = table_columns(table_name)
|
73
|
+
columns.any? { |col| col[:name].to_s == column_name.to_s }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get table indexes
|
77
|
+
# @param table_name [String] Name of the table
|
78
|
+
# @return [Array<Hash>] List of indexes with details
|
79
|
+
def fetch_indexes(table_name)
|
80
|
+
@connection.indexes(table_name).map do |index|
|
81
|
+
{
|
82
|
+
name: index.name,
|
83
|
+
columns: index.columns,
|
84
|
+
unique: index.unique
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get foreign keys
|
90
|
+
# @param table_name [String] Name of the table
|
91
|
+
# @return [Array<Hash>] List of foreign keys with details
|
92
|
+
def fetch_foreign_keys(table_name)
|
93
|
+
@connection.foreign_keys(table_name).map do |fk|
|
94
|
+
{
|
95
|
+
name: fk.name,
|
96
|
+
from_table: fk.from_table,
|
97
|
+
to_table: fk.to_table,
|
98
|
+
column: fk.column,
|
99
|
+
primary_key: fk.primary_key
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/dbviewer/version.rb
CHANGED
data/lib/dbviewer.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
require "dbviewer/version"
|
2
2
|
require "dbviewer/configuration"
|
3
3
|
require "dbviewer/engine"
|
4
|
-
require "dbviewer/
|
4
|
+
require "dbviewer/error_handler"
|
5
|
+
require "dbviewer/cache_manager"
|
6
|
+
require "dbviewer/table_metadata_manager"
|
7
|
+
require "dbviewer/dynamic_model_factory"
|
8
|
+
require "dbviewer/query_executor"
|
5
9
|
require "dbviewer/database_manager"
|
6
10
|
require "dbviewer/sql_validator"
|
7
11
|
|
@@ -17,11 +21,6 @@ module Dbviewer
|
|
17
21
|
@configuration ||= Configuration.new
|
18
22
|
end
|
19
23
|
|
20
|
-
# Alias for backward compatibility
|
21
|
-
def config
|
22
|
-
configuration
|
23
|
-
end
|
24
|
-
|
25
24
|
# Configure the engine with a block
|
26
25
|
#
|
27
26
|
# @example
|
@@ -30,7 +29,7 @@ module Dbviewer
|
|
30
29
|
# config.default_per_page = 25
|
31
30
|
# end
|
32
31
|
def configure
|
33
|
-
yield(
|
32
|
+
yield(configuration) if block_given?
|
34
33
|
end
|
35
34
|
|
36
35
|
# Reset configuration to defaults
|
@@ -40,18 +39,14 @@ module Dbviewer
|
|
40
39
|
|
41
40
|
# This class method will be called by the engine when it's appropriate
|
42
41
|
def setup
|
43
|
-
|
42
|
+
ActiveRecord::Base.connection
|
43
|
+
Rails.logger.info "DBViewer successfully connected to database"
|
44
|
+
rescue => e
|
45
|
+
Rails.logger.error "DBViewer could not connect to database: #{e.message}"
|
44
46
|
end
|
45
47
|
|
46
48
|
# Initialize engine with default values or user-provided configuration
|
47
49
|
def init
|
48
|
-
Dbviewer::DatabaseManager.singleton_class.class_eval do
|
49
|
-
define_method(:configuration) { Dbviewer.configuration }
|
50
|
-
define_method(:default_per_page) { Dbviewer.configuration.default_per_page }
|
51
|
-
define_method(:max_records) { Dbviewer.configuration.max_records }
|
52
|
-
define_method(:cache_expiry) { Dbviewer.configuration.cache_expiry }
|
53
|
-
end
|
54
|
-
|
55
50
|
# Define class methods to access configuration
|
56
51
|
Dbviewer::SqlValidator.singleton_class.class_eval do
|
57
52
|
define_method(:configuration) { Dbviewer.configuration }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbviewer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wailan Tirajoh
|
@@ -56,7 +56,6 @@ files:
|
|
56
56
|
- app/controllers/concerns/dbviewer/error_handling.rb
|
57
57
|
- app/controllers/concerns/dbviewer/pagination_concern.rb
|
58
58
|
- app/controllers/dbviewer/application_controller.rb
|
59
|
-
- app/controllers/dbviewer/databases_controller.rb
|
60
59
|
- app/controllers/dbviewer/entity_relationship_diagrams_controller.rb
|
61
60
|
- app/controllers/dbviewer/home_controller.rb
|
62
61
|
- app/controllers/dbviewer/logs_controller.rb
|
@@ -83,18 +82,22 @@ files:
|
|
83
82
|
- app/views/layouts/dbviewer/application.html.erb
|
84
83
|
- config/routes.rb
|
85
84
|
- lib/dbviewer.rb
|
85
|
+
- lib/dbviewer/cache_manager.rb
|
86
86
|
- lib/dbviewer/configuration.rb
|
87
87
|
- lib/dbviewer/database_manager.rb
|
88
|
+
- lib/dbviewer/dynamic_model_factory.rb
|
88
89
|
- lib/dbviewer/engine.rb
|
89
|
-
- lib/dbviewer/
|
90
|
+
- lib/dbviewer/error_handler.rb
|
90
91
|
- lib/dbviewer/logger.rb
|
91
92
|
- lib/dbviewer/query_analyzer.rb
|
92
93
|
- lib/dbviewer/query_collection.rb
|
94
|
+
- lib/dbviewer/query_executor.rb
|
93
95
|
- lib/dbviewer/query_parser.rb
|
94
96
|
- lib/dbviewer/sql_validator.rb
|
95
97
|
- lib/dbviewer/storage/base.rb
|
96
98
|
- lib/dbviewer/storage/file_storage.rb
|
97
99
|
- lib/dbviewer/storage/in_memory_storage.rb
|
100
|
+
- lib/dbviewer/table_metadata_manager.rb
|
98
101
|
- lib/dbviewer/version.rb
|
99
102
|
- lib/tasks/dbviewer_tasks.rake
|
100
103
|
homepage: https://github.com/wailantirajoh/dbviewer
|
File without changes
|
data/lib/dbviewer/initializer.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
module Dbviewer
|
2
|
-
class Initializer
|
3
|
-
class << self
|
4
|
-
def setup
|
5
|
-
if defined?(ActiveRecord::Base)
|
6
|
-
Rails.logger.warn "ActiveRecord is not available, skipping database connection check"
|
7
|
-
return
|
8
|
-
end
|
9
|
-
|
10
|
-
ensure_activerecord_connection
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def ensure_activerecord_connection
|
16
|
-
ActiveRecord::Base.connection
|
17
|
-
Rails.logger.info "DBViewer successfully connected to database"
|
18
|
-
rescue => e
|
19
|
-
Rails.logger.error "DBViewer could not connect to database: #{e.message}"
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|