dbviewer 0.7.3 → 0.7.4

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: 50301b7fa1867ac7e266e3b48eb962e7e456de1e43a664750e8885a9b603f6ca
4
- data.tar.gz: 0377f20859609a2807bdecc8bc5f9118dae6581b7d4819504900e7e6a371dba3
3
+ metadata.gz: 243466a78ac9698ebe99e2f1cc225355bcb278eced6d87da6ca6d4cc87a636ba
4
+ data.tar.gz: eb7006f0fb710d799f9167f1f8a8081f6850c46e9bc9ed8d40245ca16024ffdc
5
5
  SHA512:
6
- metadata.gz: b17042fe0172fba83874c2498a99e9b9681092c5aedefb495cd2d55a41913d2d84dbbbdac3daa03a59928d465d7538183aa3dc5f12e41fda54add729b8963291
7
- data.tar.gz: e3163cce763297208aeccae790ee6fb9890a2f084a974e2ac3e41acc0dac51ea6d005409cc57c71e3e605d1d406e7ea95b1ae9b83647800ae6d26d1444b8083e
6
+ metadata.gz: f41b5f67b69a22e98a008c3e686c91a369428dc2d57beab21c429cf6ff9dca371cfa546c91a063db62e109819b4c3ff3ba1a6a3e0a333cf55f6e8e7d6f318ead
7
+ data.tar.gz: fd3467c65880b1f5b187537b7a10bdac6b971846704da02212f1dd04bf8f73c0f1d2ce355310f804c8fcb56d3f04da64ef9bca562027bf60958fc400bc1ee6b9
data/README.md CHANGED
@@ -340,11 +340,15 @@ graph TB
340
340
 
341
341
  subgraph "Database Namespace"
342
342
  Manager[Manager<br/>Database Operations]
343
- CacheManager[CacheManager<br/>Caching Layer]
344
343
  MetadataManager[MetadataManager<br/>Schema Information]
345
344
  DynamicModelFactory[DynamicModelFactory<br/>ActiveRecord Models]
346
345
  end
347
346
 
347
+ subgraph "Cache Namespace"
348
+ CacheBase[Base<br/>Cache Interface]
349
+ InMemoryCache[InMemory<br/>In-Memory Cache Storage]
350
+ end
351
+
348
352
  subgraph "Query Namespace"
349
353
  QueryExecutor[Executor<br/>SQL Execution]
350
354
  QueryLogger[Logger<br/>Query Logging]
@@ -371,7 +375,7 @@ graph TB
371
375
 
372
376
  %% Configuration Dependencies (Decoupled)
373
377
  Config -.->|"Dependency Injection"| Manager
374
- Manager -->|"cache_expiry"| CacheManager
378
+ Manager -->|"cache_expiry"| InMemoryCache
375
379
  Manager -->|"config object"| QueryExecutor
376
380
 
377
381
  %% Engine Initialization
@@ -394,14 +398,14 @@ graph TB
394
398
  DatabaseOperations --> QueryOperations
395
399
 
396
400
  %% Manager Dependencies
397
- Manager --> CacheManager
401
+ Manager --> InMemoryCache
398
402
  Manager --> MetadataManager
399
403
  Manager --> DynamicModelFactory
400
404
  Manager --> QueryOperations
401
405
 
402
406
  %% Cache Dependencies
403
- CacheManager --> DynamicModelFactory
404
- CacheManager --> MetadataManager
407
+ InMemoryCache --> DynamicModelFactory
408
+ InMemoryCache --> MetadataManager
405
409
 
406
410
  %% QueryOperations Dependencies (Refactored)
407
411
  QueryOperations --> DynamicModelFactory
@@ -422,10 +426,11 @@ graph TB
422
426
  ValidatorSql --> QueryExecutor
423
427
 
424
428
  %% Styling
425
- class CacheManager,QueryLogger decoupled
429
+ class InMemoryCache,QueryLogger decoupled
426
430
  class HomeController,TablesController,LogsController,ERDController,APIController,ConnectionsController controller
427
431
  class DatabaseOperations concern
428
432
  class Manager,MetadataManager,DynamicModelFactory database
433
+ class CacheBase,InMemoryCache cache
429
434
  class QueryExecutor,QueryAnalyzer,QueryParser,NotificationSubscriber query
430
435
  class StorageBase,InMemoryStorage,FileStorage storage
431
436
  class ColumnFiltering filtering
@@ -17,13 +17,6 @@
17
17
  </div>
18
18
  </div>
19
19
 
20
- <% if flash[:warning].present? %>
21
- <div class="alert alert-warning alert-dismissible fade show" role="alert">
22
- <%= flash[:warning] %>
23
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
24
- </div>
25
- <% end %>
26
-
27
20
  <div class="card mb-4">
28
21
  <div class="card-header">
29
22
  <h5>SQL Query (Read-Only)</h5>
@@ -0,0 +1,32 @@
1
+ module Dbviewer
2
+ module Cache
3
+ # Base handles caching concerns for the DatabaseManager
4
+ # It provides an abstraction layer for managing caches efficiently
5
+ class Base
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
+ @unified_cache = {}
11
+ @cache_last_reset = Time.now
12
+ end
13
+
14
+ # Fetch data from cache or execute block if not found/expired
15
+ # @param key [String] Cache key
16
+ # @param options [Hash] Options for the cache entry
17
+ # @option options [Integer] :expires_in Custom expiry time in seconds
18
+ # @yield Block to execute if cache miss or expired
19
+ # @return [Object] Cached value or result of block execution
20
+ def fetch(key, options = {}, &block)
21
+ raise NotImplementedError, "#{self.class}#fetch must be implemented by a subclass"
22
+ end
23
+
24
+ # Delete a specific cache entry by key
25
+ # @param key [String] Cache key to delete
26
+ # @return [Object, nil] The deleted value or nil if not found
27
+ def delete(key)
28
+ raise NotImplementedError, "#{self.class}#delete must be implemented by a subclass"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ module Dbviewer
2
+ module Cache
3
+ # InMemory cache storage for Dbviewer
4
+ # It provides an abstraction layer for managing caches efficiently
5
+ class InMemory < Dbviewer::Cache::Base
6
+ # Fetch data from cache or execute block if not found/expired
7
+ # @param key [String] Cache key
8
+ # @param options [Hash] Options for the cache entry
9
+ # @option options [Integer] :expires_in Custom expiry time in seconds
10
+ # @yield Block to execute if cache miss or expired
11
+ # @return [Object] Cached value or result of block execution
12
+ def fetch(key, options = {}, &block)
13
+ cache_entry = @unified_cache[key]
14
+ custom_expiry = options[:expires_in] || @cache_expiry
15
+ return cache_entry[:value] if cache_entry && !cache_expired?(cache_entry, custom_expiry)
16
+
17
+ result = block.call
18
+ @unified_cache[key] = {
19
+ value: result,
20
+ created_at: Time.now
21
+ }
22
+ result
23
+ end
24
+
25
+ # Delete a specific cache entry by key
26
+ # @param key [String] Cache key to delete
27
+ # @return [Object, nil] The deleted value or nil if not found
28
+ def delete(key)
29
+ deleted_entry = @unified_cache.delete(key)
30
+ deleted_entry&.fetch(:value)
31
+ end
32
+
33
+ private
34
+
35
+ # Check if a cache entry is expired
36
+ # @param cache_entry [Hash] Cache entry with :created_at timestamp
37
+ # @param expiry_time [Integer] Expiry time in seconds
38
+ # @return [Boolean] True if expired, false otherwise
39
+ def cache_expired?(cache_entry, expiry_time)
40
+ Time.now - cache_entry[:created_at] > expiry_time
41
+ end
42
+ end
43
+ end
44
+ end
@@ -14,12 +14,10 @@ module Dbviewer
14
14
  # @param table_name [String] Name of the table
15
15
  # @return [Class] ActiveRecord model class for the table
16
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
17
+ # Cache models for shorter time since they might need refreshing more frequently
18
+ @cache_manager.fetch("model-#{table_name}", expires_in: 300) do
19
+ create_model_for(table_name)
20
+ end
23
21
  end
24
22
 
25
23
  private
@@ -10,7 +10,7 @@ module Dbviewer
10
10
  def initialize(connection_key = nil)
11
11
  @connection_key = connection_key || Dbviewer.configuration.current_connection
12
12
  ensure_connection
13
- @cache_manager = ::Dbviewer::Database::CacheManager.new(configuration.cache_expiry)
13
+ @cache_manager = ::Dbviewer::Cache::InMemory.new(configuration.cache_expiry)
14
14
  @table_metadata_manager = ::Dbviewer::Database::MetadataManager.new(@connection, @cache_manager)
15
15
  @dynamic_model_factory = ::Dbviewer::Database::DynamicModelFactory.new(@connection, @cache_manager)
16
16
  @query_executor = ::Dbviewer::Query::Executor.new(@connection, configuration)
@@ -19,7 +19,6 @@ module Dbviewer
19
19
  @dynamic_model_factory,
20
20
  @table_metadata_manager
21
21
  )
22
- reset_cache_if_needed
23
22
  end
24
23
 
25
24
  # Get configuration from class method or Dbviewer
@@ -132,11 +131,6 @@ module Dbviewer
132
131
  @table_metadata_manager.fetch_foreign_keys(table_name)
133
132
  end
134
133
 
135
- # Clear all caches - useful when schema changes are detected
136
- def clear_all_caches
137
- @cache_manager.clear_all
138
- end
139
-
140
134
  # Calculate the total size of the database schema
141
135
  # @return [Integer, nil] Database size in bytes or nil if unsupported
142
136
  def fetch_schema_size
@@ -202,11 +196,6 @@ module Dbviewer
202
196
  @adapter_name = @connection.adapter_name.downcase
203
197
  @connection
204
198
  end
205
-
206
- # Reset caches if they've been around too long
207
- def reset_cache_if_needed
208
- @cache_manager.reset_if_needed
209
- end
210
199
  end
211
200
  end
212
201
  end
@@ -20,40 +20,33 @@ module Dbviewer
20
20
  # @param table_name [String] Name of the table
21
21
  # @return [Array<Hash>] List of column details
22
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
- }
23
+ # Cache columns for longer since schema changes are infrequent
24
+ @cache_manager.fetch("columns-#{table_name}", expires_in: 600) do
25
+ @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
34
  end
35
-
36
- # Cache the result
37
- @cache_manager.store_columns(table_name, columns)
38
- columns
39
35
  end
40
36
 
41
37
  # Get detailed metadata about a table
42
38
  # @param table_name [String] Name of the table
43
39
  # @return [Hash] Table metadata
44
40
  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
41
+ # Cache metadata for longer since schema relationships change infrequently
42
+ @cache_manager.fetch("metadata-#{table_name}", expires_in: 600) do
43
+ {
44
+ primary_key: primary_key(table_name),
45
+ indexes: fetch_indexes(table_name),
46
+ foreign_keys: fetch_foreign_keys(table_name),
47
+ reverse_foreign_keys: fetch_reverse_foreign_keys(table_name)
48
+ }
49
+ end
57
50
  end
58
51
 
59
52
  # Get the primary key of a table
@@ -32,7 +32,7 @@ module Dbviewer
32
32
  def add(event)
33
33
  # Return early if query logging is disabled
34
34
  return unless @enable_query_logging
35
- return if ::Dbviewer::Query::Parser.should_skip_query?(event)
35
+ return if ::Dbviewer::Query::Parser.should_skip_query?(event) || ::Dbviewer::Query::Parser.should_skip_internal_query?(event)
36
36
 
37
37
  current_time = Time.now
38
38
  @storage.add({
@@ -21,9 +21,6 @@ module Dbviewer
21
21
  # @param args [Array] Notification arguments from ActiveSupport::Notifications
22
22
  def process_notification(*args)
23
23
  event = ActiveSupport::Notifications::Event.new(*args)
24
-
25
- return if skip_internal_query?(event)
26
-
27
24
  Dbviewer::Query::Logger.instance.add(event)
28
25
  end
29
26
 
@@ -35,7 +32,7 @@ module Dbviewer
35
32
  return false unless caller_locations
36
33
 
37
34
  excluded_caller_locations = caller_locations.filter do |caller_location|
38
- !caller_location.path.include?("lib/dbviewer/engine.rb")
35
+ !caller_location.path.include?("lib/dbviewer/query/notification_subscriber.rb")
39
36
  end
40
37
 
41
38
  excluded_caller_locations.any? { |location| location.path.include?("dbviewer") }
@@ -42,6 +42,8 @@ module Dbviewer
42
42
 
43
43
  # Determine if a query should be skipped based on content
44
44
  # Rails and ActiveRecord often run internal queries that are not useful for logging
45
+ # @param event [ActiveSupport::Notifications::Event] The notification event
46
+ # @return [Boolean] True if the query should be skipped
45
47
  def self.should_skip_query?(event)
46
48
  event.payload[:name] == "SCHEMA" ||
47
49
  event.payload[:sql].include?("SHOW TABLES") ||
@@ -51,6 +53,17 @@ module Dbviewer
51
53
  event.payload[:sql].include?("ar_internal_metadata") ||
52
54
  event.payload[:sql].include?("pg_catalog")
53
55
  end
56
+
57
+ # Determine if an internal query should be skipped
58
+ # Internal queries are those that are part of the Dbviewer module itself
59
+ # and do not represent user-generated SQL queries.
60
+ # This helps avoid logging internal operations that are not relevant to users.
61
+ # @param event [ActiveSupport::Notifications::Event] The notification event
62
+ # @return [Boolean] True if the query should be skipped
63
+ def self.should_skip_internal_query?(event)
64
+ event.payload[:name].include?("Dbviewer::") ||
65
+ event.payload[:sql].include?("PRAGMA")
66
+ end
54
67
  end
55
68
  end
56
69
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbviewer
4
+ module Validator
5
+ class Sql
6
+ # Module for query normalization operations
7
+ # This module handles the cleaning and standardization of SQL queries
8
+ # to prepare them for validation and threat detection.
9
+ module QueryNormalizer
10
+ extend self
11
+
12
+ # Normalize SQL by removing comments and extra whitespace
13
+ # This prepares the query for consistent validation by:
14
+ # - Removing SQL comments (both -- and /* */ styles)
15
+ # - Normalizing whitespace to single spaces
16
+ # - Trimming leading/trailing whitespace
17
+ #
18
+ # @param sql [String] The SQL query to normalize
19
+ # @return [String] The normalized SQL query
20
+ def normalize(sql)
21
+ return "" if sql.nil?
22
+
23
+ begin
24
+ normalized = remove_comments(sql)
25
+ normalized = normalize_whitespace(normalized)
26
+ normalized.strip
27
+ rescue => e
28
+ # Log error if Rails logger is available, otherwise use basic error handling
29
+ if defined?(Rails) && Rails.respond_to?(:logger)
30
+ Rails.logger.error("[DBViewer] SQL normalization error: #{e.message}")
31
+ else
32
+ # Fallback to stderr if Rails is not available
33
+ $stderr.puts "[DBViewer] SQL normalization error: #{e.message}"
34
+ end
35
+ ""
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Remove SQL comments from the query
42
+ # Handles both single-line (--) and multi-line (/* */) comment styles
43
+ #
44
+ # @param sql [String] The SQL query
45
+ # @return [String] SQL with comments removed
46
+ def remove_comments(sql)
47
+ sql.gsub(/--.*$/, "") # Remove -- style comments
48
+ .gsub(/\/\*.*?\*\//m, "") # Remove /* */ style comments
49
+ end
50
+
51
+ # Normalize whitespace in the SQL query
52
+ # Converts all whitespace sequences to single spaces for consistent parsing
53
+ #
54
+ # @param sql [String] The SQL query
55
+ # @return [String] SQL with normalized whitespace
56
+ def normalize_whitespace(sql)
57
+ sql.gsub(/\s+/, " ") # Normalize whitespace
58
+ .gsub(/\s{2,}/, " ") # Replace multiple spaces with single space
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validation_config"
4
+
5
+ module Dbviewer
6
+ module Validator
7
+ class Sql
8
+ # Module for detecting various types of security threats in SQL queries
9
+ # This module contains all the logic for identifying SQL injection attempts,
10
+ # suspicious patterns, and other security vulnerabilities.
11
+ module ThreatDetector
12
+ extend self
13
+
14
+ # Check for suspicious patterns in SQL that might indicate an attack
15
+ # This method performs multiple checks on the raw SQL (before normalization)
16
+ # to catch threats that might be hidden by the normalization process.
17
+ #
18
+ # @param sql [String] Raw SQL query (before normalization)
19
+ # @return [Boolean] true if suspicious patterns found, false otherwise
20
+ def has_suspicious_patterns?(sql)
21
+ return true if has_comment_injection?(sql)
22
+ return true if has_string_concatenation?(sql)
23
+ return true if has_excessive_quotes?(sql)
24
+ return true if has_hex_encoding?(sql)
25
+
26
+ false
27
+ end
28
+
29
+ # Check for specific SQL injection patterns
30
+ # This method looks for known SQL injection attack patterns
31
+ # that are commonly used by attackers.
32
+ #
33
+ # @param sql [String] Raw SQL query (before normalization)
34
+ # @return [Boolean] true if injection patterns found, false otherwise
35
+ def has_injection_patterns?(sql)
36
+ ValidationConfig::INJECTION_PATTERNS.any? do |_name, pattern|
37
+ sql =~ pattern
38
+ end
39
+ end
40
+
41
+ # Check for forbidden keywords in the normalized SQL
42
+ # This method scans for keywords that could modify data or schema.
43
+ #
44
+ # @param normalized_sql [String] Normalized SQL query
45
+ # @return [String, nil] The forbidden keyword found, or nil if none
46
+ def detect_forbidden_keywords(normalized_sql)
47
+ ValidationConfig::FORBIDDEN_KEYWORDS.find do |keyword|
48
+ normalized_sql =~ /\b#{keyword}\b/i
49
+ end
50
+ end
51
+
52
+ # Check if the query starts with an allowed statement type
53
+ # Only SELECT and WITH (for CTEs) are allowed as query starters.
54
+ #
55
+ # @param normalized_sql [String] Normalized SQL query
56
+ # @return [Boolean] true if query starts with allowed statement
57
+ def valid_query_start?(normalized_sql)
58
+ normalized_sql =~ ValidationConfig::VALID_QUERY_START_PATTERN
59
+ end
60
+
61
+ # Check if the query is a SQLite PRAGMA statement
62
+ # PRAGMA statements are allowed for database introspection.
63
+ #
64
+ # @param normalized_sql [String] Normalized SQL query
65
+ # @return [Boolean] true if query is a PRAGMA statement
66
+ def pragma_statement?(normalized_sql)
67
+ normalized_sql =~ ValidationConfig::PRAGMA_PATTERN
68
+ end
69
+
70
+ # Check for multiple SQL statements separated by semicolons
71
+ # Multiple statements could allow SQL injection attacks.
72
+ #
73
+ # @param normalized_sql [String] Normalized SQL query
74
+ # @return [Boolean] true if multiple statements detected
75
+ def has_multiple_statements?(normalized_sql)
76
+ statements = normalized_sql.split(";").reject(&:blank?)
77
+ statements.size > 1
78
+ end
79
+
80
+ # Detect subqueries in the SQL by checking for unbalanced parentheses
81
+ # This is a heuristic method for feature detection.
82
+ #
83
+ # @param normalized_sql [String] Normalized SQL query
84
+ # @return [Boolean] true if subqueries are likely present
85
+ def detect_subqueries(normalized_sql)
86
+ # Check if there are unbalanced parentheses that likely contain subqueries
87
+ normalized_sql.count("(") > normalized_sql.count(")")
88
+ end
89
+
90
+ private
91
+
92
+ # Check for comment injection attempts
93
+ # Comments can be used to hide malicious SQL code
94
+ #
95
+ # @param sql [String] Raw SQL query
96
+ # @return [Boolean] true if comment injection detected
97
+ def has_comment_injection?(sql)
98
+ ValidationConfig::SUSPICIOUS_PATTERNS[:comment_injection] =~ sql
99
+ end
100
+
101
+ # Check for string concatenation that might be used for injection
102
+ # String concatenation can be used to build dynamic SQL attacks
103
+ #
104
+ # @param sql [String] Raw SQL query
105
+ # @return [Boolean] true if suspicious string concatenation detected
106
+ def has_string_concatenation?(sql)
107
+ ValidationConfig::SUSPICIOUS_PATTERNS[:string_concatenation] =~ sql
108
+ end
109
+
110
+ # Check for excessive quotes which might indicate injection attempts
111
+ # Large numbers of quotes can indicate SQL injection payload construction
112
+ #
113
+ # @param sql [String] Raw SQL query
114
+ # @return [Boolean] true if excessive quotes detected
115
+ def has_excessive_quotes?(sql)
116
+ single_quotes = sql.count("'")
117
+ double_quotes = sql.count('"')
118
+ single_quotes > ValidationConfig::QUOTE_LIMIT ||
119
+ double_quotes > ValidationConfig::QUOTE_LIMIT
120
+ end
121
+
122
+ # Check for hex encoding that might hide malicious code
123
+ # Long hex strings can be used to encode SQL injection payloads
124
+ #
125
+ # @param sql [String] Raw SQL query
126
+ # @return [Boolean] true if suspicious hex encoding detected
127
+ def has_hex_encoding?(sql)
128
+ ValidationConfig::SUSPICIOUS_PATTERNS[:hex_encoding] =~ sql
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbviewer
4
+ module Validator
5
+ class Sql
6
+ # Configuration constants and patterns for SQL validation
7
+ # This module centralizes all security-related patterns and rules
8
+ # used throughout the validation process.
9
+ module ValidationConfig
10
+ # List of SQL keywords that could modify data or schema
11
+ # These keywords are completely forbidden in user queries
12
+ FORBIDDEN_KEYWORDS = %w[
13
+ UPDATE INSERT DELETE DROP ALTER CREATE TRUNCATE REPLACE
14
+ RENAME GRANT REVOKE LOCK UNLOCK COMMIT ROLLBACK
15
+ SAVEPOINT INTO CALL EXECUTE EXEC
16
+ ].freeze
17
+
18
+ # List of SQL keywords that should only be allowed in specific contexts
19
+ # These are monitored but not automatically blocked
20
+ CONDITIONAL_KEYWORDS = {
21
+ # JOIN is allowed, but we should check for suspicious patterns
22
+ "JOIN" => /\bJOIN\b/i,
23
+ # UNION is allowed, but potential for injection
24
+ "UNION" => /\bUNION\b/i,
25
+ # WITH is allowed for CTEs, but need to ensure it's not a data modification
26
+ "WITH" => /\bWITH\b/i
27
+ }.freeze
28
+
29
+ # Maximum allowed query length (can be overridden by configuration)
30
+ DEFAULT_MAX_QUERY_LENGTH = 10000
31
+
32
+ # Patterns for detecting suspicious content that might indicate attacks
33
+ SUSPICIOUS_PATTERNS = {
34
+ comment_injection: /\s+--|\/\*/,
35
+ string_concatenation: /\|\||CONCAT\s*\(/i,
36
+ hex_encoding: /0x[0-9a-f]{16,}/i
37
+ }.freeze
38
+
39
+ # SQL injection attack patterns - these are definitive threats
40
+ INJECTION_PATTERNS = {
41
+ basic_or_injection: /'\s*OR\s*'.*'\s*=\s*'/i,
42
+ quoted_or_equals: /'\s*OR\s*1\s*=\s*1/i,
43
+ unquoted_or_equals: /\s+OR\s+1\s*=\s*1/i,
44
+ comment_termination: /'\s*;\s*--/i,
45
+ version_detection: /@@version/i,
46
+ version_function: /version\(\)/i,
47
+ file_access: /\bLOAD_FILE\s*\(/i,
48
+ outfile_access: /\bINTO\s+OUTFILE\b/i,
49
+ dumpfile_access: /\bINTO\s+DUMPFILE\b/i
50
+ }.freeze
51
+
52
+ # Database feature detection patterns for query analysis
53
+ FEATURE_PATTERNS = {
54
+ join: /\b(INNER|LEFT|RIGHT|FULL|CROSS)?\s*JOIN\b/i,
55
+ order_by: /\bORDER\s+BY\b/i,
56
+ group_by: /\bGROUP\s+BY\b/i,
57
+ having: /\bHAVING\b/i,
58
+ union: /\bUNION\b/i,
59
+ window_function: /\bOVER\s*\(/i
60
+ }.freeze
61
+
62
+ # Thresholds for suspicious activity detection
63
+ QUOTE_LIMIT = 20 # Maximum number of quotes before flagging as suspicious
64
+ HEX_MIN_LENGTH = 16 # Minimum hex string length to be considered suspicious
65
+
66
+ # PRAGMA statement pattern for SQLite introspection
67
+ PRAGMA_PATTERN = /\A\s*PRAGMA\s+[a-z0-9_]+(\([^)]*\))?\s*\z/i
68
+
69
+ # Valid query start patterns (SELECT or WITH for CTEs)
70
+ VALID_QUERY_START_PATTERN = /\A\s*(SELECT|WITH)\s+/i
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbviewer
4
+ module Validator
5
+ class Sql
6
+ # Validation result object to encapsulate validation state and errors
7
+ # This provides a structured way to return validation results with
8
+ # clear success/failure states and associated error messages.
9
+ ValidationResult = Struct.new(:valid?, :error_message, :normalized_sql, keyword_init: true) do
10
+ # Check if the validation was successful
11
+ # @return [Boolean] true if validation passed
12
+ def success?
13
+ valid?
14
+ end
15
+
16
+ # Check if the validation failed
17
+ # @return [Boolean] true if validation failed
18
+ def failure?
19
+ !valid?
20
+ end
21
+
22
+ # Create a successful validation result
23
+ # @param normalized_sql [String] The normalized SQL query
24
+ # @return [ValidationResult] Success result with normalized SQL
25
+ def self.success(normalized_sql)
26
+ new(valid?: true, normalized_sql: normalized_sql)
27
+ end
28
+
29
+ # Create a failed validation result
30
+ # @param error_message [String] Description of the validation failure
31
+ # @return [ValidationResult] Failure result with error message
32
+ def self.failure(error_message)
33
+ new(valid?: false, error_message: error_message)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,196 +1,208 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sql/validation_result"
4
+ require_relative "sql/validation_config"
5
+ require_relative "sql/query_normalizer"
6
+ require_relative "sql/threat_detector"
7
+
3
8
  module Dbviewer
4
9
  module Validator
5
10
  # Sql class handles SQL query validation and normalization
6
11
  # to ensure queries are safe (read-only) and properly formatted.
7
12
  # This helps prevent potentially destructive SQL operations.
8
13
  class Sql
9
- # List of SQL keywords that could modify data or schema
10
- FORBIDDEN_KEYWORDS = %w[
11
- UPDATE INSERT DELETE DROP ALTER CREATE TRUNCATE REPLACE
12
- RENAME GRANT REVOKE LOCK UNLOCK COMMIT ROLLBACK
13
- SAVEPOINT INTO CALL EXECUTE EXEC
14
- ]
15
-
16
- # List of SQL keywords that should only be allowed in specific contexts
17
- CONDITIONAL_KEYWORDS = {
18
- # JOIN is allowed, but we should check for suspicious patterns
19
- "JOIN" => /\bJOIN\b/i,
20
- # UNION is allowed, but potential for injection
21
- "UNION" => /\bUNION\b/i,
22
- # WITH is allowed for CTEs, but need to ensure it's not a data modification
23
- "WITH" => /\bWITH\b/i
24
- }
25
-
26
- # Maximum allowed query length
27
- MAX_QUERY_LENGTH = 10000
28
-
29
- # Determines if a query is safe (read-only)
30
- # @param sql [String] The SQL query to validate
31
- # @return [Boolean] true if the query is safe, false otherwise
32
- def self.safe_query?(sql)
33
- return false if sql.blank?
34
-
35
- # Get max query length from configuration
36
- max_length = Dbviewer.configuration.max_query_length || MAX_QUERY_LENGTH
37
- return false if sql.length > max_length
38
-
39
- normalized_sql = normalize(sql)
40
-
41
- # Case-insensitive check for SELECT at the beginning
42
- return false unless normalized_sql =~ /\A\s*SELECT\s+/i
43
-
44
- # Check for forbidden keywords that might be used in subqueries or other SQL constructs
45
- FORBIDDEN_KEYWORDS.each do |keyword|
46
- # Look for the keyword with word boundaries to avoid false positives
47
- return false if normalized_sql =~ /\b#{keyword}\b/i
14
+ # Core validation methods
15
+ class << self
16
+ # Determines if a query is safe (read-only)
17
+ # @param sql [String] The SQL query to validate
18
+ # @return [Boolean] true if the query is safe, false otherwise
19
+ def safe_query?(sql)
20
+ result = validate_query(sql, allow_pragma: false)
21
+ result.success?
48
22
  end
49
23
 
50
- # Check for suspicious patterns that might indicate SQL injection attempts
51
- return false if has_suspicious_patterns?(normalized_sql)
24
+ # Validates a query and raises an exception if it's unsafe
25
+ # @param sql [String] The SQL query to validate
26
+ # @raise [SecurityError] if the query is unsafe
27
+ # @return [String] The normalized SQL query if it's safe
28
+ def validate!(sql)
29
+ result = validate_query(sql, allow_pragma: true)
52
30
 
53
- # Check for multiple statements (;) which could allow executing multiple commands
54
- statements = normalized_sql.split(";").reject(&:blank?)
55
- return false if statements.size > 1
31
+ if result.failure?
32
+ raise SecurityError, result.error_message
33
+ end
56
34
 
57
- # Additional specific checks for common SQL injection patterns
58
- return false if has_injection_patterns?(normalized_sql)
35
+ result.normalized_sql
36
+ end
59
37
 
60
- true
61
- end
38
+ private
62
39
 
63
- # Check for suspicious patterns in SQL that might indicate an attack
64
- # @param sql [String] Normalized SQL query
65
- # @return [Boolean] true if suspicious patterns found, false otherwise
66
- def self.has_suspicious_patterns?(sql)
67
- # Check for SQL comment sequences that might be used to hide malicious code
68
- return true if sql =~ /\s+--/ || sql =~ /\/\*/
40
+ # Main validation logic that returns a structured result
41
+ # @param sql [String] The SQL query to validate
42
+ # @param allow_pragma [Boolean] Whether to allow PRAGMA statements
43
+ # @return [ValidationResult] Validation result with status and details
44
+ def validate_query(sql, allow_pragma: true)
45
+ # Step 1: Basic input validation
46
+ basic_validation_result = perform_basic_validation(sql)
47
+ return basic_validation_result if basic_validation_result
69
48
 
70
- # Check for string concatenation which might be used for injection
71
- return true if sql =~ /\|\|/ || sql =~ /CONCAT\s*\(/i
49
+ # Step 2: Security threat detection (before normalization)
50
+ threat_validation_result = perform_threat_validation(sql)
51
+ return threat_validation_result if threat_validation_result
72
52
 
73
- # Check for excessive number of quotes which might indicate injection
74
- single_quotes = sql.count("'")
75
- double_quotes = sql.count('"')
76
- return true if single_quotes > 20 || double_quotes > 20
53
+ # Step 3: Normalize the query
54
+ normalized_sql = QueryNormalizer.normalize(sql)
77
55
 
78
- # Check for hex/binary data which might hide malicious code
79
- return true if sql =~ /0x[0-9a-f]{16,}/i
56
+ # Step 4: Handle special cases (PRAGMA) - only if allowed
57
+ if allow_pragma
58
+ pragma_result = handle_pragma_statements(normalized_sql)
59
+ return pragma_result if pragma_result
60
+ end
80
61
 
81
- false
82
- end
62
+ # Step 5: Validate query structure and keywords
63
+ structure_validation_result = perform_structure_validation(normalized_sql)
64
+ return structure_validation_result if structure_validation_result
83
65
 
84
- # Check for specific SQL injection patterns
85
- # @param sql [String] Normalized SQL query
86
- # @return [Boolean] true if injection patterns found, false otherwise
87
- def self.has_injection_patterns?(sql)
88
- # Check for typical SQL injection test patterns
89
- return true if sql =~ /'\s*OR\s*'.*'\s*=\s*'/i
90
- return true if sql =~ /'\s*OR\s*1\s*=\s*1/i
91
- return true if sql =~ /'\s*;\s*--/i
66
+ # Step 6: Check for multiple statements
67
+ multiple_statements_result = validate_single_statement(normalized_sql)
68
+ return multiple_statements_result if multiple_statements_result
92
69
 
93
- # Check for attempts to determine database type
94
- return true if sql =~ /@@version/i
95
- return true if sql =~ /version\(\)/i
70
+ # Success case
71
+ ValidationResult.new(
72
+ valid?: true,
73
+ normalized_sql: normalized_sql
74
+ )
75
+ end
96
76
 
97
- false
98
- end
77
+ # Perform basic input validation (null, empty, length)
78
+ def perform_basic_validation(sql)
79
+ if sql.nil? || sql.strip.empty?
80
+ return ValidationResult.new(
81
+ valid?: false,
82
+ error_message: "Empty query is not allowed"
83
+ )
84
+ end
85
+
86
+ max_length = get_max_query_length
87
+ if sql.length > max_length
88
+ return ValidationResult.new(
89
+ valid?: false,
90
+ error_message: "Query exceeds maximum allowed length (#{max_length} chars)"
91
+ )
92
+ end
99
93
 
100
- # Normalize SQL by removing comments and extra whitespace
101
- # @param sql [String] The SQL query to normalize
102
- # @return [String] The normalized SQL query
103
- def self.normalize(sql)
104
- return "" if sql.nil?
105
-
106
- begin
107
- # Remove SQL comments (both -- and /* */ styles)
108
- normalized = sql.gsub(/--.*$/, "") # Remove -- style comments
109
- .gsub(/\/\*.*?\*\//m, "") # Remove /* */ style comments
110
- .gsub(/\s+/, " ") # Normalize whitespace
111
- .strip # Remove leading/trailing whitespace
112
-
113
- # Replace multiple spaces with a single space
114
- normalized.gsub(/\s{2,}/, " ")
115
- rescue => e
116
- Rails.logger.error("[DBViewer] SQL normalization error: #{e.message}")
117
- ""
94
+ nil # No validation errors
118
95
  end
119
- end
120
96
 
121
- # Validates a query and raises an exception if it's unsafe
122
- # @param sql [String] The SQL query to validate
123
- # @raise [SecurityError] if the query is unsafe
124
- # @return [String] The normalized SQL query if it's safe
125
- def self.validate!(sql)
126
- if sql.blank?
127
- raise SecurityError, "Empty query is not allowed"
97
+ # Perform security threat detection
98
+ def perform_threat_validation(sql)
99
+ if ThreatDetector.has_suspicious_patterns?(sql)
100
+ return ValidationResult.new(
101
+ valid?: false,
102
+ error_message: "Query contains suspicious patterns that may indicate SQL injection"
103
+ )
104
+ end
105
+
106
+ if ThreatDetector.has_injection_patterns?(sql)
107
+ return ValidationResult.new(
108
+ valid?: false,
109
+ error_message: "Query contains patterns commonly associated with SQL injection attempts"
110
+ )
111
+ end
112
+
113
+ nil # No security threats detected
128
114
  end
129
115
 
130
- # Get max query length from configuration
131
- max_length = Dbviewer.configuration.max_query_length || MAX_QUERY_LENGTH
132
- if sql.length > max_length
133
- raise SecurityError, "Query exceeds maximum allowed length (#{max_length} chars)"
116
+ # Handle special PRAGMA statements for SQLite
117
+ def handle_pragma_statements(normalized_sql)
118
+ if pragma_statement?(normalized_sql)
119
+ return ValidationResult.new(
120
+ valid?: true,
121
+ normalized_sql: normalized_sql
122
+ )
123
+ end
124
+
125
+ nil # Not a PRAGMA statement
134
126
  end
135
127
 
136
- normalized_sql = normalize(sql)
128
+ # Validate query structure and forbidden keywords
129
+ def perform_structure_validation(normalized_sql)
130
+ unless valid_query_start?(normalized_sql)
131
+ return ValidationResult.new(
132
+ valid?: false,
133
+ error_message: "Query must begin with SELECT or WITH for security reasons"
134
+ )
135
+ end
137
136
 
138
- # Special case for SQLite PRAGMA statements which are safe read-only commands
139
- if normalized_sql =~ /\A\s*PRAGMA\s+[a-z0-9_]+\s*\z/i
140
- return normalized_sql
137
+ forbidden_keyword = detect_forbidden_keywords(normalized_sql)
138
+ if forbidden_keyword
139
+ return ValidationResult.new(
140
+ valid?: false,
141
+ error_message: "Forbidden keyword '#{forbidden_keyword}' detected in query"
142
+ )
143
+ end
144
+
145
+ nil # Structure validation passed
141
146
  end
142
147
 
143
- unless normalized_sql =~ /\A\s*SELECT\s+/i
144
- raise SecurityError, "Query must begin with SELECT for security reasons"
148
+ # Validate that query contains only a single statement
149
+ def validate_single_statement(normalized_sql)
150
+ statements = normalized_sql.split(";").reject { |s| s.nil? || s.strip.empty? }
151
+ if statements.size > 1
152
+ return ValidationResult.new(
153
+ valid?: false,
154
+ error_message: "Multiple SQL statements are not allowed"
155
+ )
156
+ end
157
+
158
+ nil # Single statement validation passed
145
159
  end
146
160
 
147
- FORBIDDEN_KEYWORDS.each do |keyword|
148
- if normalized_sql =~ /\b#{keyword}\b/i
149
- raise SecurityError, "Forbidden keyword '#{keyword}' detected in query"
161
+ # Helper methods
162
+ def get_max_query_length
163
+ # Try to get from configuration if available, otherwise use default
164
+ if defined?(Dbviewer) && Dbviewer.respond_to?(:configuration) && Dbviewer.configuration.respond_to?(:max_query_length)
165
+ Dbviewer.configuration.max_query_length || ValidationConfig::DEFAULT_MAX_QUERY_LENGTH
166
+ else
167
+ ValidationConfig::DEFAULT_MAX_QUERY_LENGTH
150
168
  end
151
169
  end
152
170
 
153
- if has_suspicious_patterns?(normalized_sql)
154
- raise SecurityError, "Query contains suspicious patterns that may indicate SQL injection"
171
+ def pragma_statement?(normalized_sql)
172
+ normalized_sql =~ /\A\s*PRAGMA\s+[a-z0-9_]+(\([^)]*\))?\s*\z/i
155
173
  end
156
174
 
157
- # Check for multiple statements
158
- statements = normalized_sql.split(";").reject(&:blank?)
159
- if statements.size > 1
160
- raise SecurityError, "Multiple SQL statements are not allowed"
175
+ def valid_query_start?(normalized_sql)
176
+ normalized_sql =~ /\A\s*(SELECT|WITH)\s+/i
161
177
  end
162
178
 
163
- if has_injection_patterns?(normalized_sql)
164
- raise SecurityError, "Query contains patterns commonly associated with SQL injection attempts"
179
+ def detect_forbidden_keywords(normalized_sql)
180
+ ValidationConfig::FORBIDDEN_KEYWORDS.find do |keyword|
181
+ normalized_sql =~ /\b#{keyword}\b/i
182
+ end
165
183
  end
166
184
 
167
- normalized_sql
168
- end
185
+ def detect_subqueries(normalized_sql)
186
+ # Check if there are unbalanced parentheses that likely contain subqueries
187
+ normalized_sql.count("(") > normalized_sql.count(")")
188
+ end
189
+
190
+ # Method missing handler for backward compatibility with legacy method calls
191
+ def method_missing(method_name, *args, **kwargs, &block)
192
+ case method_name
193
+ when :has_suspicious_patterns?
194
+ ThreatDetector.has_suspicious_patterns?(*args)
195
+ when :has_injection_patterns?
196
+ ThreatDetector.has_injection_patterns?(*args)
197
+ when :normalize
198
+ QueryNormalizer.normalize(*args)
199
+ else
200
+ super
201
+ end
202
+ end
169
203
 
170
- # Check if a query is using a specific database feature that might need special handling
171
- # @param sql [String] The SQL query
172
- # @param feature [Symbol] The feature to check for (:join, :subquery, :order_by, etc.)
173
- # @return [Boolean] true if the feature is used in the query
174
- def self.uses_feature?(sql, feature)
175
- normalized = normalize(sql)
176
- case feature
177
- when :join
178
- normalized =~ /\b(INNER|LEFT|RIGHT|FULL|CROSS)?\s*JOIN\b/i
179
- when :subquery
180
- # Check if there are parentheses that likely contain a subquery
181
- normalized.count("(") > normalized.count(")")
182
- when :order_by
183
- normalized =~ /\bORDER\s+BY\b/i
184
- when :group_by
185
- normalized =~ /\bGROUP\s+BY\b/i
186
- when :having
187
- normalized =~ /\bHAVING\b/i
188
- when :union
189
- normalized =~ /\bUNION\b/i
190
- when :window_function
191
- normalized =~ /\bOVER\s*\(/i
192
- else
193
- false
204
+ def respond_to_missing?(method_name, include_private = false)
205
+ [ :has_suspicious_patterns?, :has_injection_patterns?, :normalize ].include?(method_name) || super
194
206
  end
195
207
  end
196
208
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.7.3"
2
+ VERSION = "0.7.4"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -7,13 +7,15 @@ require "dbviewer/storage/base"
7
7
  require "dbviewer/storage/in_memory_storage"
8
8
  require "dbviewer/storage/file_storage"
9
9
 
10
+ require "dbviewer/cache/base"
11
+ require "dbviewer/cache/in_memory"
12
+
10
13
  require "dbviewer/query/executor"
11
14
  require "dbviewer/query/analyzer"
12
15
  require "dbviewer/query/logger"
13
16
  require "dbviewer/query/notification_subscriber"
14
17
  require "dbviewer/query/parser"
15
18
 
16
- require "dbviewer/database/cache_manager"
17
19
  require "dbviewer/database/dynamic_model_factory"
18
20
  require "dbviewer/database/manager"
19
21
  require "dbviewer/database/metadata_manager"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-12 00:00:00.000000000 Z
11
+ date: 2025-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -120,8 +120,9 @@ files:
120
120
  - app/views/layouts/dbviewer/shared/_sidebar.html.erb
121
121
  - config/routes.rb
122
122
  - lib/dbviewer.rb
123
+ - lib/dbviewer/cache/base.rb
124
+ - lib/dbviewer/cache/in_memory.rb
123
125
  - lib/dbviewer/configuration.rb
124
- - lib/dbviewer/database/cache_manager.rb
125
126
  - lib/dbviewer/database/dynamic_model_factory.rb
126
127
  - lib/dbviewer/database/manager.rb
127
128
  - lib/dbviewer/database/metadata_manager.rb
@@ -137,6 +138,10 @@ files:
137
138
  - lib/dbviewer/storage/file_storage.rb
138
139
  - lib/dbviewer/storage/in_memory_storage.rb
139
140
  - lib/dbviewer/validator/sql.rb
141
+ - lib/dbviewer/validator/sql/query_normalizer.rb
142
+ - lib/dbviewer/validator/sql/threat_detector.rb
143
+ - lib/dbviewer/validator/sql/validation_config.rb
144
+ - lib/dbviewer/validator/sql/validation_result.rb
140
145
  - lib/dbviewer/version.rb
141
146
  - lib/generators/dbviewer/install_generator.rb
142
147
  - lib/generators/dbviewer/templates/initializer.rb
@@ -1,78 +0,0 @@
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