dbviewer 0.5.2 → 0.5.3

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -0
  3. data/app/controllers/concerns/dbviewer/database_operations.rb +11 -19
  4. data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +84 -0
  5. data/app/controllers/dbviewer/api/queries_controller.rb +1 -1
  6. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +5 -6
  7. data/app/controllers/dbviewer/logs_controller.rb +1 -1
  8. data/app/controllers/dbviewer/tables_controller.rb +2 -8
  9. data/app/helpers/dbviewer/application_helper.rb +1 -1
  10. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +217 -100
  11. data/app/views/dbviewer/tables/show.html.erb +278 -404
  12. data/config/routes.rb +7 -0
  13. data/lib/dbviewer/database/cache_manager.rb +78 -0
  14. data/lib/dbviewer/database/dynamic_model_factory.rb +62 -0
  15. data/lib/dbviewer/database/manager.rb +204 -0
  16. data/lib/dbviewer/database/metadata_manager.rb +129 -0
  17. data/lib/dbviewer/datatable/query_operations.rb +330 -0
  18. data/lib/dbviewer/datatable/query_params.rb +41 -0
  19. data/lib/dbviewer/engine.rb +1 -1
  20. data/lib/dbviewer/query/analyzer.rb +250 -0
  21. data/lib/dbviewer/query/collection.rb +39 -0
  22. data/lib/dbviewer/query/executor.rb +93 -0
  23. data/lib/dbviewer/query/logger.rb +108 -0
  24. data/lib/dbviewer/query/parser.rb +56 -0
  25. data/lib/dbviewer/storage/file_storage.rb +0 -3
  26. data/lib/dbviewer/version.rb +1 -1
  27. data/lib/dbviewer.rb +24 -7
  28. metadata +14 -14
  29. data/lib/dbviewer/cache_manager.rb +0 -78
  30. data/lib/dbviewer/database_manager.rb +0 -249
  31. data/lib/dbviewer/dynamic_model_factory.rb +0 -60
  32. data/lib/dbviewer/error_handler.rb +0 -18
  33. data/lib/dbviewer/logger.rb +0 -77
  34. data/lib/dbviewer/query_analyzer.rb +0 -239
  35. data/lib/dbviewer/query_collection.rb +0 -37
  36. data/lib/dbviewer/query_executor.rb +0 -91
  37. data/lib/dbviewer/query_parser.rb +0 -53
  38. data/lib/dbviewer/table_metadata_manager.rb +0 -136
  39. data/lib/dbviewer/table_query_operations.rb +0 -621
  40. data/lib/dbviewer/table_query_params.rb +0 -39
data/config/routes.rb CHANGED
@@ -27,6 +27,13 @@ Dbviewer::Engine.routes.draw do
27
27
  end
28
28
  end
29
29
 
30
+ resources :entity_relationship_diagrams, only: [] do
31
+ collection do
32
+ get "relationships"
33
+ get "table_relationships"
34
+ end
35
+ end
36
+
30
37
  resource :database, only: [], controller: "database" do
31
38
  get "size"
32
39
  end
@@ -0,0 +1,78 @@
1
+ module Dbviewer
2
+ module Database
3
+ # CacheManager handles caching concerns for the DatabaseManager
4
+ # It provides an abstraction layer for managing caches efficiently
5
+ class CacheManager
6
+ # Initialize the cache manager
7
+ # @param cache_expiry [Integer] Cache expiration time in seconds (default: 300)
8
+ def initialize(cache_expiry = 300)
9
+ @cache_expiry = cache_expiry
10
+ @dynamic_models = {}
11
+ @table_columns_cache = {}
12
+ @table_metadata_cache = {}
13
+ @cache_last_reset = Time.now
14
+ end
15
+
16
+ # Get a model from cache or return nil
17
+ # @param table_name [String] Name of the table
18
+ # @return [Class, nil] The cached model or nil if not found
19
+ def get_model(table_name)
20
+ @dynamic_models[table_name]
21
+ end
22
+
23
+ # Store a model in the cache
24
+ # @param table_name [String] Name of the table
25
+ # @param model [Class] ActiveRecord model class
26
+ def store_model(table_name, model)
27
+ @dynamic_models[table_name] = model
28
+ end
29
+
30
+ # Get column information from cache
31
+ # @param table_name [String] Name of the table
32
+ # @return [Array<Hash>, nil] The cached column information or nil if not found
33
+ def get_columns(table_name)
34
+ @table_columns_cache[table_name]
35
+ end
36
+
37
+ # Store column information in cache
38
+ # @param table_name [String] Name of the table
39
+ # @param columns [Array<Hash>] Column information
40
+ def store_columns(table_name, columns)
41
+ @table_columns_cache[table_name] = columns
42
+ end
43
+
44
+ # Get table metadata from cache
45
+ # @param table_name [String] Name of the table
46
+ # @return [Hash, nil] The cached metadata or nil if not found
47
+ def get_metadata(table_name)
48
+ @table_metadata_cache[table_name]
49
+ end
50
+
51
+ # Store table metadata in cache
52
+ # @param table_name [String] Name of the table
53
+ # @param metadata [Hash] Table metadata
54
+ def store_metadata(table_name, metadata)
55
+ @table_metadata_cache[table_name] = metadata
56
+ end
57
+
58
+ # Reset caches if they've been around too long
59
+ def reset_if_needed
60
+ if Time.now - @cache_last_reset > @cache_expiry
61
+ @table_columns_cache = {}
62
+ @table_metadata_cache = {}
63
+ @cache_last_reset = Time.now
64
+ Rails.logger.debug("[DBViewer] Cache reset due to expiry after #{@cache_expiry} seconds")
65
+ end
66
+ end
67
+
68
+ # Clear all caches - useful when schema changes are detected
69
+ def clear_all
70
+ @dynamic_models = {}
71
+ @table_columns_cache = {}
72
+ @table_metadata_cache = {}
73
+ @cache_last_reset = Time.now
74
+ Rails.logger.debug("[DBViewer] All caches cleared")
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,62 @@
1
+ module Dbviewer
2
+ module Database
3
+ # DynamicModelFactory creates and manages ActiveRecord models for database tables
4
+ class DynamicModelFactory
5
+ # Initialize with a connection and cache manager
6
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
7
+ # @param cache_manager [Dbviewer::Database::CacheManager] Cache manager instance
8
+ def initialize(connection, cache_manager)
9
+ @connection = connection
10
+ @cache_manager = cache_manager
11
+ end
12
+
13
+ # Get or create an ActiveRecord model for a table
14
+ # @param table_name [String] Name of the table
15
+ # @return [Class] ActiveRecord model class for the table
16
+ def get_model_for(table_name)
17
+ cached_model = @cache_manager.get_model(table_name)
18
+ return cached_model if cached_model
19
+
20
+ model = create_model_for(table_name)
21
+ @cache_manager.store_model(table_name, model)
22
+ model
23
+ end
24
+
25
+ private
26
+
27
+ # Create a new ActiveRecord model for a table
28
+ # @param table_name [String] Name of the table
29
+ # @return [Class] ActiveRecord model class for the table
30
+ def create_model_for(table_name)
31
+ model_name = table_name.classify
32
+
33
+ # Create a new model class dynamically
34
+ model = Class.new(ActiveRecord::Base) do
35
+ self.table_name = table_name
36
+
37
+ # Some tables might not have primary keys, so we handle that
38
+ begin
39
+ primary_key = connection.primary_key(table_name)
40
+ self.primary_key = primary_key if primary_key.present?
41
+ rescue
42
+ self.primary_key = "id"
43
+ end
44
+
45
+ # Disable STI
46
+ self.inheritance_column = :_type_disabled
47
+
48
+ # Disable timestamps for better compatibility
49
+ self.record_timestamps = false
50
+ end
51
+
52
+ # Set model name constant if not already taken
53
+ # Use a namespace to avoid polluting the global namespace
54
+ unless Dbviewer.const_defined?("DynamicModel_#{model_name}")
55
+ Dbviewer.const_set("DynamicModel_#{model_name}", model)
56
+ end
57
+
58
+ model
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,204 @@
1
+ module Dbviewer
2
+ module Database
3
+ # Manager handles all database interactions for the DBViewer engine
4
+ # It provides methods to access database structure and data
5
+ class Manager
6
+ attr_reader :connection, :adapter_name, :table_query_operations
7
+
8
+ # Initialize the database manager
9
+ def initialize
10
+ ensure_connection
11
+ @cache_manager = ::Dbviewer::Database::CacheManager.new(configuration.cache_expiry)
12
+ @table_metadata_manager = ::Dbviewer::Database::MetadataManager.new(@connection, @cache_manager)
13
+ @dynamic_model_factory = ::Dbviewer::Database::DynamicModelFactory.new(@connection, @cache_manager)
14
+ @query_executor = ::Dbviewer::Query::Executor.new(@connection, configuration)
15
+ @table_query_operations = ::Dbviewer::Datatable::QueryOperations.new(
16
+ @connection,
17
+ @dynamic_model_factory,
18
+ @query_executor,
19
+ @table_metadata_manager
20
+ )
21
+ reset_cache_if_needed
22
+ end
23
+
24
+ # Get configuration from class method or Dbviewer
25
+ def configuration
26
+ @configuration ||= Dbviewer.configuration
27
+ end
28
+
29
+ # Returns a sorted list of all tables in the database
30
+ # @return [Array<String>] List of table names
31
+ def tables
32
+ @table_metadata_manager.tables
33
+ end
34
+
35
+ # Returns column information for a specific table
36
+ # @param table_name [String] Name of the table
37
+ # @return [Array<Hash>] List of column details with name, type, null, default
38
+ def table_columns(table_name)
39
+ @table_metadata_manager.table_columns(table_name)
40
+ end
41
+
42
+ # Get detailed metadata about a table (primary keys, indexes, foreign keys)
43
+ # @param table_name [String] Name of the table
44
+ # @return [Hash] Table metadata
45
+ def table_metadata(table_name)
46
+ @table_metadata_manager.table_metadata(table_name)
47
+ end
48
+
49
+ # Get the total count of records in a table
50
+ # @param table_name [String] Name of the table
51
+ # @return [Integer] Number of records
52
+ def table_count(table_name)
53
+ @table_query_operations.table_count(table_name)
54
+ end
55
+
56
+ # Get records from a table with pagination and sorting
57
+ # @param table_name [String] Name of the table
58
+ # @param query_params [Dbviewer::Datatable::QueryParams] Query parameters for pagination and sorting
59
+ # @return [ActiveRecord::Result] Result set with columns and rows
60
+ def table_records(table_name, query_params)
61
+ @table_query_operations.table_records(
62
+ table_name,
63
+ query_params
64
+ )
65
+ end
66
+
67
+ # Get the number of records in a table (alias for table_count)
68
+ # @param table_name [String] Name of the table
69
+ # @return [Integer] Number of records
70
+ def record_count(table_name)
71
+ @table_query_operations.record_count(table_name)
72
+ end
73
+
74
+ # Get the number of records in a table with filters applied
75
+ # @param table_name [String] Name of the table
76
+ # @param column_filters [Hash] Hash of column_name => filter_value for filtering
77
+ # @return [Integer] Number of filtered records
78
+ def filtered_record_count(table_name, column_filters = {})
79
+ @table_query_operations.filtered_record_count(table_name, column_filters)
80
+ end
81
+
82
+ # Get the number of columns in a table
83
+ # @param table_name [String] Name of the table
84
+ # @return [Integer] Number of columns
85
+ def column_count(table_name)
86
+ table_columns(table_name).size
87
+ end
88
+
89
+ # Get the primary key of a table
90
+ # @param table_name [String] Name of the table
91
+ # @return [String, nil] Primary key column name or nil if not found
92
+ def primary_key(table_name)
93
+ @table_metadata_manager.primary_key(table_name)
94
+ end
95
+
96
+ # Check if a column exists in a table
97
+ # @param table_name [String] Name of the table
98
+ # @param column_name [String] Name of the column
99
+ # @return [Boolean] true if column exists, false otherwise
100
+ def column_exists?(table_name, column_name)
101
+ @table_metadata_manager.column_exists?(table_name, column_name)
102
+ end
103
+
104
+ # Execute a raw SQL query after validating for safety
105
+ # @param sql [String] SQL query to execute
106
+ # @return [ActiveRecord::Result] Result set with columns and rows
107
+ # @raise [StandardError] If the query is invalid or unsafe
108
+ def execute_query(sql)
109
+ @table_query_operations.execute_query(sql)
110
+ end
111
+
112
+ # Execute a SQLite PRAGMA command without adding a LIMIT clause
113
+ # @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
114
+ # @return [ActiveRecord::Result] Result set with the PRAGMA value
115
+ # @raise [StandardError] If the query is invalid or cannot be executed
116
+ def execute_sqlite_pragma(pragma)
117
+ @table_query_operations.execute_sqlite_pragma(pragma)
118
+ end
119
+
120
+ # Get table indexes
121
+ # @param table_name [String] Name of the table
122
+ # @return [Array<Hash>] List of indexes with details
123
+ def fetch_indexes(table_name)
124
+ @table_metadata_manager.fetch_indexes(table_name)
125
+ end
126
+
127
+ # Get foreign keys
128
+ # @param table_name [String] Name of the table
129
+ # @return [Array<Hash>] List of foreign keys with details
130
+ def fetch_foreign_keys(table_name)
131
+ @table_metadata_manager.fetch_foreign_keys(table_name)
132
+ end
133
+
134
+ # Clear all caches - useful when schema changes are detected
135
+ def clear_all_caches
136
+ @cache_manager.clear_all
137
+ end
138
+
139
+ # Calculate the total size of the database schema
140
+ # @return [Integer, nil] Database size in bytes or nil if unsupported
141
+ def fetch_schema_size
142
+ case adapter_name
143
+ when /mysql/
144
+ fetch_mysql_size
145
+ when /postgres/
146
+ fetch_postgres_size
147
+ when /sqlite/
148
+ fetch_sqlite_size
149
+ else
150
+ nil # Unsupported database type for size calculation
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def fetch_mysql_size
157
+ query = "SELECT SUM(data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = DATABASE()"
158
+ fetch_size_from_query(query)
159
+ end
160
+
161
+ def fetch_postgres_size
162
+ query = "SELECT pg_database_size(current_database()) AS size"
163
+ fetch_size_from_query(query)
164
+ end
165
+
166
+ def fetch_sqlite_size
167
+ page_count = fetch_sqlite_pragma_value("page_count")
168
+ page_size = fetch_sqlite_pragma_value("page_size")
169
+ page_count * page_size
170
+ end
171
+
172
+ def fetch_sqlite_pragma_value(pragma_name)
173
+ execute_sqlite_pragma(pragma_name).first.values.first.to_i
174
+ end
175
+
176
+ def fetch_size_from_query(query)
177
+ result = execute_query(query).first
178
+ result ? result["size"].to_i : nil
179
+ end
180
+
181
+ # Ensure we have a valid database connection
182
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] The database connection
183
+ def ensure_connection
184
+ return @connection if @connection
185
+
186
+ @connection = ActiveRecord::Base.connection
187
+ @adapter_name = @connection.adapter_name.downcase
188
+ @connection
189
+ end
190
+
191
+ # Reset caches if they've been around too long
192
+ def reset_cache_if_needed
193
+ @cache_manager.reset_if_needed
194
+ end
195
+
196
+ # Get a dynamic AR model for a table
197
+ # @param table_name [String] Name of the table
198
+ # @return [Class] ActiveRecord model class
199
+ def get_model_for(table_name)
200
+ @dynamic_model_factory.get_model_for(table_name)
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,129 @@
1
+ module Dbviewer
2
+ module Database
3
+ # MetadataManager handles retrieving and managing table structure information
4
+ class MetadataManager
5
+ # Initialize with a connection and cache manager
6
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
7
+ # @param cache_manager [Dbviewer::Database::CacheManager] Cache manager instance
8
+ def initialize(connection, cache_manager)
9
+ @connection = connection
10
+ @cache_manager = cache_manager
11
+ end
12
+
13
+ # Get all tables in the database
14
+ # @return [Array<String>] List of table names
15
+ def tables
16
+ @connection.tables.sort
17
+ end
18
+
19
+ # Get column information for a table
20
+ # @param table_name [String] Name of the table
21
+ # @return [Array<Hash>] List of column details
22
+ def table_columns(table_name)
23
+ cached_columns = @cache_manager.get_columns(table_name)
24
+ return cached_columns if cached_columns
25
+
26
+ columns = @connection.columns(table_name).map do |column|
27
+ {
28
+ name: column.name,
29
+ type: column.type,
30
+ null: column.null,
31
+ default: column.default,
32
+ primary: column.name == primary_key(table_name)
33
+ }
34
+ end
35
+
36
+ # Cache the result
37
+ @cache_manager.store_columns(table_name, columns)
38
+ columns
39
+ end
40
+
41
+ # Get detailed metadata about a table
42
+ # @param table_name [String] Name of the table
43
+ # @return [Hash] Table metadata
44
+ def table_metadata(table_name)
45
+ cached_metadata = @cache_manager.get_metadata(table_name)
46
+ return cached_metadata if cached_metadata
47
+
48
+ metadata = {
49
+ primary_key: primary_key(table_name),
50
+ indexes: fetch_indexes(table_name),
51
+ foreign_keys: fetch_foreign_keys(table_name),
52
+ reverse_foreign_keys: fetch_reverse_foreign_keys(table_name)
53
+ }
54
+
55
+ @cache_manager.store_metadata(table_name, metadata)
56
+ metadata
57
+ end
58
+
59
+ # Get the primary key of a table
60
+ # @param table_name [String] Name of the table
61
+ # @return [String, nil] Primary key column name or nil if not found
62
+ def primary_key(table_name)
63
+ @connection.primary_key(table_name)
64
+ rescue => e
65
+ Rails.logger.error("[DBViewer] Error retrieving primary key for table #{table_name}: #{e.message}")
66
+ nil
67
+ end
68
+
69
+ # Check if a column exists in a table
70
+ # @param table_name [String] Name of the table
71
+ # @param column_name [String] Name of the column
72
+ # @return [Boolean] true if column exists, false otherwise
73
+ def column_exists?(table_name, column_name)
74
+ columns = table_columns(table_name)
75
+ columns.any? { |col| col[:name].to_s == column_name.to_s }
76
+ end
77
+
78
+ # Get table indexes
79
+ # @param table_name [String] Name of the table
80
+ # @return [Array<Hash>] List of indexes with details
81
+ def fetch_indexes(table_name)
82
+ @connection.indexes(table_name).map do |index|
83
+ {
84
+ name: index.name,
85
+ columns: index.columns,
86
+ unique: index.unique
87
+ }
88
+ end
89
+ end
90
+
91
+ # Get foreign keys
92
+ # @param table_name [String] Name of the table
93
+ # @return [Array<Hash>] List of foreign keys with details
94
+ def fetch_foreign_keys(table_name)
95
+ @connection.foreign_keys(table_name).map do |fk|
96
+ {
97
+ name: fk.name,
98
+ from_table: fk.from_table,
99
+ to_table: fk.to_table,
100
+ column: fk.column,
101
+ primary_key: fk.primary_key
102
+ }
103
+ end
104
+ end
105
+
106
+ # Get reverse foreign keys (tables that reference this table)
107
+ # @param table_name [String] Name of the table
108
+ # @return [Array<Hash>] List of reverse foreign keys with details
109
+ def fetch_reverse_foreign_keys(table_name)
110
+ tables
111
+ .reject { |other_table| other_table == table_name }
112
+ .flat_map do |other_table|
113
+ @connection.foreign_keys(other_table)
114
+ .select { |fk| fk.to_table == table_name }
115
+ .map do |fk|
116
+ {
117
+ name: fk.name,
118
+ from_table: fk.from_table,
119
+ to_table: fk.to_table,
120
+ column: fk.column,
121
+ primary_key: fk.primary_key,
122
+ relationship_type: "has_many"
123
+ }
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end