dbwatcher 0.1.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/controllers/dbwatcher/base_controller.rb +95 -0
  4. data/app/controllers/dbwatcher/dashboard_controller.rb +12 -0
  5. data/app/controllers/dbwatcher/queries_controller.rb +24 -0
  6. data/app/controllers/dbwatcher/sessions_controller.rb +15 -20
  7. data/app/controllers/dbwatcher/tables_controller.rb +38 -0
  8. data/app/helpers/dbwatcher/application_helper.rb +103 -0
  9. data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
  10. data/app/helpers/dbwatcher/session_helper.rb +27 -0
  11. data/app/views/dbwatcher/dashboard/index.html.erb +177 -0
  12. data/app/views/dbwatcher/queries/index.html.erb +240 -0
  13. data/app/views/dbwatcher/sessions/index.html.erb +120 -27
  14. data/app/views/dbwatcher/sessions/show.html.erb +326 -129
  15. data/app/views/dbwatcher/shared/_badge.html.erb +4 -0
  16. data/app/views/dbwatcher/shared/_data_table.html.erb +20 -0
  17. data/app/views/dbwatcher/shared/_header.html.erb +7 -0
  18. data/app/views/dbwatcher/shared/_page_layout.html.erb +20 -0
  19. data/app/views/dbwatcher/shared/_section_panel.html.erb +9 -0
  20. data/app/views/dbwatcher/shared/_stats_card.html.erb +11 -0
  21. data/app/views/dbwatcher/shared/_tab_bar.html.erb +6 -0
  22. data/app/views/dbwatcher/tables/changes.html.erb +225 -0
  23. data/app/views/dbwatcher/tables/index.html.erb +123 -0
  24. data/app/views/dbwatcher/tables/show.html.erb +86 -0
  25. data/app/views/layouts/dbwatcher/application.html.erb +375 -26
  26. data/config/routes.rb +17 -3
  27. data/lib/dbwatcher/configuration.rb +9 -1
  28. data/lib/dbwatcher/engine.rb +12 -7
  29. data/lib/dbwatcher/logging.rb +72 -0
  30. data/lib/dbwatcher/services/dashboard_data_aggregator.rb +121 -0
  31. data/lib/dbwatcher/services/query_filter_processor.rb +114 -0
  32. data/lib/dbwatcher/services/table_statistics_collector.rb +119 -0
  33. data/lib/dbwatcher/sql_logger.rb +107 -0
  34. data/lib/dbwatcher/storage/api/base_api.rb +134 -0
  35. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +172 -0
  36. data/lib/dbwatcher/storage/api/query_api.rb +95 -0
  37. data/lib/dbwatcher/storage/api/session_api.rb +134 -0
  38. data/lib/dbwatcher/storage/api/table_api.rb +86 -0
  39. data/lib/dbwatcher/storage/base_storage.rb +113 -0
  40. data/lib/dbwatcher/storage/change_processor.rb +65 -0
  41. data/lib/dbwatcher/storage/concerns/data_normalizer.rb +134 -0
  42. data/lib/dbwatcher/storage/concerns/error_handler.rb +75 -0
  43. data/lib/dbwatcher/storage/concerns/timestampable.rb +74 -0
  44. data/lib/dbwatcher/storage/concerns/validatable.rb +117 -0
  45. data/lib/dbwatcher/storage/date_helper.rb +21 -0
  46. data/lib/dbwatcher/storage/errors.rb +86 -0
  47. data/lib/dbwatcher/storage/file_manager.rb +122 -0
  48. data/lib/dbwatcher/storage/null_session.rb +39 -0
  49. data/lib/dbwatcher/storage/query_storage.rb +338 -0
  50. data/lib/dbwatcher/storage/query_validator.rb +24 -0
  51. data/lib/dbwatcher/storage/session.rb +58 -0
  52. data/lib/dbwatcher/storage/session_operations.rb +37 -0
  53. data/lib/dbwatcher/storage/session_query.rb +71 -0
  54. data/lib/dbwatcher/storage/session_storage.rb +322 -0
  55. data/lib/dbwatcher/storage/table_storage.rb +237 -0
  56. data/lib/dbwatcher/storage.rb +112 -85
  57. data/lib/dbwatcher/tracker.rb +4 -55
  58. data/lib/dbwatcher/version.rb +1 -1
  59. data/lib/dbwatcher.rb +12 -2
  60. metadata +47 -1
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Storage
5
+ module Concerns
6
+ # Provides timestamping capabilities for storage objects
7
+ #
8
+ # This concern adds created_at and updated_at functionality to storage
9
+ # objects, following Rails conventions for timestamp management.
10
+ #
11
+ # @example
12
+ # class SessionStorage < BaseStorage
13
+ # include Concerns::Timestampable
14
+ # end
15
+ module Timestampable
16
+ def self.included(base)
17
+ base.attr_reader :created_at, :updated_at
18
+ end
19
+
20
+ # Sets initial timestamps on creation
21
+ #
22
+ # @return [void]
23
+ def initialize_timestamps
24
+ now = current_time
25
+ @created_at = now
26
+ @updated_at = now
27
+ end
28
+
29
+ # Updates the updated_at timestamp
30
+ #
31
+ # @return [Time] the new updated_at timestamp
32
+ def touch_updated_at
33
+ @updated_at = current_time
34
+ end
35
+
36
+ # Calculates age since creation
37
+ #
38
+ # @return [Float] age in seconds since creation
39
+ def age
40
+ current_time - created_at
41
+ end
42
+
43
+ # Checks if the object was recently created
44
+ #
45
+ # @param threshold [Integer] threshold in seconds (default: 1 hour)
46
+ # @return [Boolean] true if created within threshold
47
+ def recently_created?(threshold = 3600)
48
+ age < threshold
49
+ end
50
+
51
+ # Checks if the object was recently updated
52
+ #
53
+ # @param threshold [Integer] threshold in seconds (default: 1 hour)
54
+ # @return [Boolean] true if updated within threshold
55
+ def recently_updated?(threshold = 3600)
56
+ (current_time - updated_at) < threshold
57
+ end
58
+
59
+ private
60
+
61
+ # Returns current time (compatible with and without Rails)
62
+ #
63
+ # @return [Time] current time
64
+ def current_time
65
+ if defined?(Time.current)
66
+ Time.current
67
+ else
68
+ Time.now
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Storage
5
+ module Concerns
6
+ # Provides validation capabilities for storage classes
7
+ #
8
+ # This concern adds common validation methods and patterns used
9
+ # across different storage implementations.
10
+ #
11
+ # @example
12
+ # class MyStorage < BaseStorage
13
+ # include Concerns::Validatable
14
+ #
15
+ # def save(data)
16
+ # validate_presence!(data, :id, :name)
17
+ # # save logic
18
+ # end
19
+ # end
20
+ module Validatable
21
+ def self.included(base)
22
+ base.extend(ClassMethods)
23
+ end
24
+
25
+ module ClassMethods
26
+ # Defines required attributes for validation
27
+ #
28
+ # @param attributes [Array<Symbol>] list of required attributes
29
+ # @return [void]
30
+ def validates_presence_of(*attributes)
31
+ @required_attributes = attributes
32
+ end
33
+
34
+ # Returns list of required attributes
35
+ #
36
+ # @return [Array<Symbol>] required attributes
37
+ def required_attributes
38
+ @required_attributes || []
39
+ end
40
+ end
41
+
42
+ # Validates presence of required attributes
43
+ #
44
+ # @param data [Hash] data to validate
45
+ # @param attributes [Array<Symbol>] specific attributes to check
46
+ # @raise [ValidationError] if any required attribute is missing
47
+ # @return [void]
48
+ def validate_presence!(data, *attributes)
49
+ attrs_to_check = attributes.any? ? attributes : self.class.required_attributes
50
+
51
+ attrs_to_check.each do |attr|
52
+ next if data.key?(attr) && !blank_value?(data[attr])
53
+
54
+ raise ValidationError, "#{attr} is required but was #{data[attr].inspect}"
55
+ end
56
+ end
57
+
58
+ # Validates that an ID is present and valid
59
+ #
60
+ # @param id [String, Integer, nil] ID to validate
61
+ # @raise [ValidationError] if ID is invalid
62
+ # @return [void]
63
+ def validate_id!(id)
64
+ return unless id.nil? || id.to_s.strip.empty?
65
+
66
+ raise ValidationError, "ID cannot be nil or empty"
67
+ end
68
+
69
+ # Validates that a name is present and valid
70
+ #
71
+ # @param name [String, nil] name to validate
72
+ # @raise [ValidationError] if name is invalid
73
+ # @return [void]
74
+ def validate_name!(name)
75
+ return unless name.nil? || name.to_s.strip.empty?
76
+
77
+ raise ValidationError, "Name cannot be nil or empty"
78
+ end
79
+
80
+ # Checks if an ID is valid
81
+ #
82
+ # @param id [String, Integer, nil] ID to check
83
+ # @return [Boolean] true if ID is valid
84
+ def valid_id?(id)
85
+ !id.nil? && !id.to_s.strip.empty?
86
+ end
87
+
88
+ # Checks if a name is valid
89
+ #
90
+ # @param name [String, nil] name to check
91
+ # @return [Boolean] true if name is valid
92
+ def valid_name?(name)
93
+ !name.nil? && !name.to_s.strip.empty?
94
+ end
95
+
96
+ private
97
+
98
+ # Checks if a value should be considered blank
99
+ #
100
+ # @param value [Object] value to check
101
+ # @return [Boolean] true if value is blank
102
+ def blank_value?(value)
103
+ case value
104
+ when String
105
+ value.strip.empty?
106
+ when Array, Hash
107
+ value.empty?
108
+ when NilClass
109
+ true
110
+ else
111
+ false
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Storage
5
+ module DateHelper
6
+ DEFAULT_CLEANUP_DAYS = 30
7
+
8
+ def format_date(timestamp)
9
+ timestamp.strftime("%Y-%m-%d")
10
+ end
11
+
12
+ def cleanup_cutoff_date(days_to_keep = DEFAULT_CLEANUP_DAYS)
13
+ Time.now - (days_to_keep * 24 * 60 * 60)
14
+ end
15
+
16
+ def date_file_path(base_path, date)
17
+ File.join(base_path, "#{date}.json")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Storage
5
+ # Base storage error class
6
+ #
7
+ # All storage-related errors inherit from this class to provide
8
+ # consistent error handling across the storage module.
9
+ class StorageError < StandardError; end
10
+
11
+ # Raised when validation fails on storage operations
12
+ class ValidationError < StorageError; end
13
+
14
+ # Raised when a requested session cannot be found
15
+ class SessionNotFoundError < StorageError; end
16
+
17
+ # Raised when a requested query cannot be found
18
+ class QueryNotFoundError < StorageError; end
19
+
20
+ # Raised when a requested table cannot be found
21
+ class TableNotFoundError < StorageError; end
22
+
23
+ # Raised when storage data becomes corrupted
24
+ class CorruptedDataError < StorageError; end
25
+
26
+ # Raised when storage permissions are insufficient
27
+ class PermissionError < StorageError; end
28
+
29
+ # Provides error handling capabilities for storage operations
30
+ #
31
+ # This module can be included in storage classes to provide
32
+ # standardized error handling and logging capabilities.
33
+ #
34
+ # @example
35
+ # class MyStorage
36
+ # include ErrorHandler
37
+ #
38
+ # def risky_operation
39
+ # safe_operation("my operation") do
40
+ # # potentially failing code
41
+ # end
42
+ # end
43
+ # end
44
+ module ErrorHandler
45
+ # Executes a block with error handling and optional default return value
46
+ #
47
+ # @param operation_name [String] description of the operation for logging
48
+ # @param default_value [Object] value to return if operation fails
49
+ # @yield [] the block to execute safely
50
+ # @return [Object] the result of the block or default_value on error
51
+ def safe_operation(operation_name, default_value = nil, &block)
52
+ block.call
53
+ rescue JSON::ParserError => e
54
+ log_error("JSON parsing failed in #{operation_name}", e)
55
+ default_value
56
+ rescue StandardError => e
57
+ log_error("#{operation_name} failed", e)
58
+ default_value
59
+ end
60
+
61
+ # Executes a block with error handling that raises StorageError on failure
62
+ #
63
+ # @param operation [String] description of the operation
64
+ # @yield [] the block to execute
65
+ # @return [Object] the result of the block
66
+ # @raise [StorageError] if the operation fails
67
+ def with_error_handling(operation, &block)
68
+ block.call
69
+ rescue StandardError => e
70
+ warn "Storage #{operation} failed: #{e.message}"
71
+ raise StorageError, "#{operation} failed: #{e.message}"
72
+ end
73
+
74
+ private
75
+
76
+ # Logs an error message with exception details
77
+ #
78
+ # @param message [String] the error message
79
+ # @param error [Exception] the exception that occurred
80
+ # @return [void]
81
+ def log_error(message, error)
82
+ warn "#{message}: #{error.message}"
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Storage
5
+ # Manages file system operations for storage classes
6
+ #
7
+ # This class provides a centralized interface for file operations
8
+ # including JSON serialization, directory management, and file
9
+ # system utilities used throughout the storage module.
10
+ #
11
+ # @example
12
+ # manager = FileManager.new("/path/to/storage")
13
+ # manager.write_json("data.json", { key: "value" })
14
+ # data = manager.read_json("data.json")
15
+ class FileManager
16
+ # @return [String] the base storage path
17
+ attr_reader :storage_path
18
+
19
+ # Initializes file manager with storage path
20
+ #
21
+ # Creates the base storage directory if it doesn't exist.
22
+ #
23
+ # @param storage_path [String] base path for file operations
24
+ def initialize(storage_path)
25
+ @storage_path = storage_path
26
+ ensure_directories
27
+ end
28
+
29
+ # Writes data to a JSON file
30
+ #
31
+ # Serializes the provided data as pretty-printed JSON and writes
32
+ # it to the specified file path.
33
+ #
34
+ # @param file_path [String] path to write the JSON file
35
+ # @param data [Object] data to serialize as JSON
36
+ # @return [Integer] number of bytes written
37
+ # @raise [JSON::GeneratorError] if data cannot be serialized
38
+ def write_json(file_path, data)
39
+ # Ensure parent directory exists
40
+ ensure_directory(File.dirname(file_path))
41
+ File.write(file_path, JSON.pretty_generate(data))
42
+ end
43
+
44
+ # Reads and parses a JSON file
45
+ #
46
+ # Reads the specified file and parses it as JSON with symbolized keys.
47
+ # Returns empty array if file doesn't exist.
48
+ #
49
+ # @param file_path [String] path to the JSON file
50
+ # @return [Object] parsed JSON data with symbolized keys
51
+ # @raise [JSON::ParserError] if file contains invalid JSON
52
+ def read_json(file_path)
53
+ return default_empty_result unless File.exist?(file_path)
54
+
55
+ JSON.parse(File.read(file_path), symbolize_names: true)
56
+ end
57
+
58
+ # Checks if a file exists
59
+ #
60
+ # @param file_path [String] path to check
61
+ # @return [Boolean] true if file exists
62
+ def file_exists?(file_path)
63
+ File.exist?(file_path)
64
+ end
65
+
66
+ # Deletes a file
67
+ #
68
+ # @param file_path [String] path to file to delete
69
+ # @return [Integer] number of files deleted (1 or 0)
70
+ def delete_file(file_path)
71
+ File.delete(file_path)
72
+ end
73
+
74
+ # Deletes a directory and its contents
75
+ #
76
+ # @param dir_path [String] path to directory to delete
77
+ # @return [Boolean] true if directory was deleted, false if it didn't exist
78
+ def delete_directory(dir_path)
79
+ return false unless Dir.exist?(dir_path)
80
+
81
+ FileUtils.rm_rf(dir_path)
82
+ true
83
+ rescue Errno::ENOENT
84
+ false
85
+ end
86
+
87
+ # Returns files matching a glob pattern
88
+ #
89
+ # @param pattern [String] glob pattern to match
90
+ # @return [Array<String>] array of matching file paths
91
+ def glob_files(pattern)
92
+ Dir.glob(pattern)
93
+ end
94
+
95
+ # Ensures a directory exists
96
+ #
97
+ # Creates the directory and any necessary parent directories.
98
+ #
99
+ # @param path [String] directory path to create
100
+ # @return [Array, nil] array of created directories or nil if already exists
101
+ def ensure_directory(path)
102
+ FileUtils.mkdir_p(path)
103
+ end
104
+
105
+ private
106
+
107
+ # Creates the base storage directory
108
+ #
109
+ # @return [Array, nil] array of created directories or nil if already exists
110
+ def ensure_directories
111
+ FileUtils.mkdir_p(@storage_path)
112
+ end
113
+
114
+ # Default return value for empty JSON files
115
+ #
116
+ # @return [Array] empty array as default
117
+ def default_empty_result
118
+ []
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Storage
5
+ class NullSession
6
+ def self.instance
7
+ @instance ||= new
8
+ end
9
+
10
+ def id
11
+ nil
12
+ end
13
+
14
+ def name
15
+ "Unknown Session"
16
+ end
17
+
18
+ def changes
19
+ []
20
+ end
21
+
22
+ def started_at
23
+ nil
24
+ end
25
+
26
+ def ended_at
27
+ nil
28
+ end
29
+
30
+ def present?
31
+ false
32
+ end
33
+
34
+ def nil?
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end