dbwatcher 1.1.2 → 1.1.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/dbwatcher_manifest.js +1 -0
  3. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +184 -97
  4. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  5. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  6. data/app/assets/stylesheets/dbwatcher/application.css +298 -1
  7. data/app/assets/stylesheets/dbwatcher/application.scss +1 -0
  8. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  9. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +27 -17
  10. data/app/controllers/dbwatcher/sessions_controller.rb +1 -1
  11. data/app/views/dbwatcher/sessions/_layout.html.erb +5 -2
  12. data/app/views/dbwatcher/sessions/_summary.html.erb +1 -1
  13. data/app/views/dbwatcher/sessions/{_changes.html.erb → _tables.html.erb} +84 -5
  14. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  15. data/app/views/dbwatcher/sessions/show.html.erb +3 -1
  16. data/app/views/layouts/dbwatcher/application.html.erb +1 -0
  17. data/config/routes.rb +2 -1
  18. data/lib/dbwatcher/configuration.rb +11 -0
  19. data/lib/dbwatcher/logging.rb +23 -1
  20. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  21. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  22. data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +62 -36
  23. data/lib/dbwatcher/services/diagram_generator.rb +35 -69
  24. data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +23 -9
  25. data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +16 -22
  26. data/lib/dbwatcher/services/diagram_strategies/diagram_strategy_helpers.rb +33 -0
  27. data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +20 -25
  28. data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +20 -25
  29. data/lib/dbwatcher/services/diagram_strategies/standard_diagram_strategy.rb +80 -0
  30. data/lib/dbwatcher/services/diagram_system.rb +14 -1
  31. data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +2 -0
  32. data/lib/dbwatcher/services/mermaid_syntax_builder.rb +10 -8
  33. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  34. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  35. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  36. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  37. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  38. data/lib/dbwatcher/version.rb +1 -1
  39. data/lib/dbwatcher.rb +1 -1
  40. metadata +13 -4
@@ -0,0 +1,260 @@
1
+ <%# Timeline View - Simple chronological list of changes %>
2
+ <div class="h-full"
3
+ x-data="DBWatcher.getComponent('timeline', { sessionId: '<%= @session.id %>' })"
4
+ x-init="init()">
5
+
6
+ <!-- Timeline Header -->
7
+ <div class="p-3 border-b border-gray-300 bg-gray-50">
8
+ <div class="flex items-center justify-between">
9
+ <h3 class="text-sm font-medium text-gray-900">Timeline View</h3>
10
+ <div class="flex items-center gap-3 text-xs">
11
+ <template x-if="!loading && filteredData.length > 0">
12
+ <span class="text-gray-600">
13
+ <span x-text="filteredData.length"></span> operations
14
+ <template x-if="filteredData.length !== timelineData.length">
15
+ <span x-text="`of ${timelineData.length} total`"></span>
16
+ </template>
17
+ </span>
18
+ </template>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ <!-- Loading State -->
24
+ <div x-show="loading" class="flex items-center justify-center h-64">
25
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-medium"></div>
26
+ <span class="ml-2 text-gray-600">Loading timeline...</span>
27
+ </div>
28
+
29
+ <!-- Error State -->
30
+ <div x-show="error" class="p-4 bg-red-50 border border-red-200 rounded m-4">
31
+ <p class="text-red-700" x-text="error"></p>
32
+ <button @click="loadTimelineData()" class="mt-2 text-red-600 hover:text-red-800 underline">Retry</button>
33
+ </div>
34
+
35
+ <!-- Timeline Content -->
36
+ <div x-show="!loading && !error" class="h-full flex">
37
+ <!-- Sidebar Filters -->
38
+ <div class="w-64 bg-gray-50 border-r border-gray-200 p-3 overflow-auto">
39
+ <!-- Table Filter -->
40
+ <div class="mb-4">
41
+ <h4 class="text-sm font-medium text-gray-900 mb-2">Filter by Tables</h4>
42
+
43
+ <!-- Select All / Clear All -->
44
+ <div class="mb-2 flex gap-2">
45
+ <button @click="filters.tables = getAvailableTables(); applyFilters()"
46
+ class="text-xs text-blue-medium hover:text-navy-dark underline"
47
+ :disabled="loading">
48
+ Select All
49
+ </button>
50
+ <button @click="clearFilters()"
51
+ class="text-xs text-blue-medium hover:text-navy-dark underline"
52
+ :disabled="loading">
53
+ Clear All
54
+ </button>
55
+ </div>
56
+
57
+ <!-- Search tables -->
58
+ <input type="text"
59
+ x-model="tableSearch"
60
+ placeholder="Search tables..."
61
+ class="w-full px-2 py-1 mb-2 border border-gray-300 rounded text-xs">
62
+
63
+ <!-- Tables list with max height and scrolling -->
64
+ <div class="max-h-32 overflow-y-auto space-y-1 border border-gray-200 rounded p-2 bg-white">
65
+ <template x-for="table in getAvailableTables().filter(t => !tableSearch || t.toLowerCase().includes(tableSearch.toLowerCase()))" :key="table">
66
+ <label class="flex items-center gap-2 p-1 rounded hover:bg-gray-100 cursor-pointer">
67
+ <input type="checkbox"
68
+ :value="table"
69
+ x-model="filters.tables"
70
+ @change="applyFilters()"
71
+ class="form-checkbox h-3 w-3 text-blue-medium">
72
+ <span class="text-xs text-gray-700" x-text="table"></span>
73
+ </label>
74
+ </template>
75
+ </div>
76
+
77
+ <!-- Selected count -->
78
+ <div class="mt-2 text-xs text-gray-500" x-show="filters.tables.length > 0">
79
+ <span x-text="filters.tables.length"></span> table(s) selected
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Search -->
84
+ <div class="mb-4">
85
+ <h4 class="text-sm font-medium text-gray-900 mb-2">Search</h4>
86
+ <input type="text"
87
+ x-model="filters.searchText"
88
+ @input="applyFilters()"
89
+ placeholder="Table, operation, or record ID"
90
+ class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
91
+ </div>
92
+
93
+ <!-- Analytics Summary -->
94
+ <template x-if="!loading && timelineData.length > 0">
95
+ <div class="bg-white border border-gray-200 rounded p-3">
96
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Session Analytics</h4>
97
+ <div class="space-y-2 text-xs">
98
+ <div class="flex justify-between">
99
+ <span class="text-gray-600">Operations:</span>
100
+ <span class="font-medium" x-text="`${filteredData.length}/${timelineData.length}`"></span>
101
+ </div>
102
+ <div class="flex justify-between">
103
+ <span class="text-gray-600">Tables:</span>
104
+ <span class="font-medium" x-text="getAvailableTables().length"></span>
105
+ </div>
106
+ <div class="flex justify-between">
107
+ <span class="text-gray-600">Duration:</span>
108
+ <span class="font-medium" x-text="metadata.session_duration || 'N/A'"></span>
109
+ </div>
110
+
111
+ <!-- Operation Counts -->
112
+ <div class="mt-3 space-y-1">
113
+ <template x-for="[operation, count] in Object.entries(metadata.operation_counts || {})" :key="operation">
114
+ <div class="flex justify-between items-center">
115
+ <span class="badge badge-sm" :class="`badge-${operation.toLowerCase()}`" x-text="operation"></span>
116
+ <span class="text-xs font-medium" x-text="count"></span>
117
+ </div>
118
+ </template>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
123
+ </div>
124
+
125
+ <!-- Main Timeline Content -->
126
+ <div class="flex-1 overflow-auto">
127
+ <!-- Empty State -->
128
+ <div x-show="filteredData.length === 0" class="flex flex-col items-center justify-center h-64">
129
+ <svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
131
+ </svg>
132
+ <p class="mt-2 text-gray-500">No operations found for the current filters</p>
133
+ <button @click="clearFilters()" class="mt-2 text-blue-medium hover:text-navy-dark underline">Clear filters</button>
134
+ </div>
135
+
136
+ <!-- Operations List -->
137
+ <template x-if="filteredData.length > 0">
138
+ <div class="divide-y divide-gray-200">
139
+ <template x-for="operation in filteredData" :key="operation.id">
140
+ <div class="transition-colors" x-data="{ expanded: false }">
141
+ <!-- Main Operation Row (Clickable) -->
142
+ <div class="p-4 hover:bg-gray-50 cursor-pointer" @click="expanded = !expanded">
143
+ <div class="flex items-start justify-between">
144
+ <!-- Operation Info -->
145
+ <div class="flex items-start gap-3 flex-1">
146
+ <div class="flex-1 min-w-0">
147
+ <div class="flex items-center gap-2 mb-1 flex-wrap">
148
+ <span class="badge badge-sm flex-shrink-0" :class="`badge-${operation.operation.toLowerCase()}`" x-text="operation.operation"></span>
149
+ <span class="text-sm font-medium text-gray-900 truncate" x-text="operation.table_name"></span>
150
+ <span x-show="operation.record_id" class="text-xs text-gray-500 flex-shrink-0">
151
+ ID: <span x-text="operation.record_id"></span>
152
+ </span>
153
+ </div>
154
+ <div class="text-xs text-gray-500">
155
+ <span x-text="formatTimestamp(operation.timestamp)"></span>
156
+ <span class="ml-2">•</span>
157
+ <span class="ml-2" x-text="formatRelativeTime(operation)"></span>
158
+ </div>
159
+
160
+ <!-- Changes Preview (if any) -->
161
+ <template x-if="operation.changes && Object.keys(operation.changes).length > 0 && !expanded">
162
+ <div class="mt-2 text-xs">
163
+ <span class="text-gray-400">Changes:</span>
164
+ <span class="text-gray-600" x-text="Object.keys(operation.changes).slice(0, 3).join(', ') + (Object.keys(operation.changes).length > 3 ? '...' : '')"></span>
165
+ </div>
166
+ </template>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Expand/Collapse Icon and Sequence -->
171
+ <div class="flex items-center gap-2 ml-4 flex-shrink-0">
172
+ <div class="text-xs text-gray-400">
173
+ #<span x-text="operation.sequence + 1"></span>
174
+ </div>
175
+ <svg class="w-4 h-4 text-gray-400 transition-transform" :class="{ 'rotate-180': expanded }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
176
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
177
+ </svg>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- Expanded Details -->
183
+ <div x-show="expanded" x-collapse class="bg-gray-50 border-t border-gray-200">
184
+ <div class="p-4 pl-10">
185
+ <!-- Operation Details -->
186
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm mb-4">
187
+ <div>
188
+ <span class="font-medium text-gray-700">Table:</span>
189
+ <span class="ml-2 text-gray-900" x-text="operation.table_name"></span>
190
+ </div>
191
+ <div>
192
+ <span class="font-medium text-gray-700">Operation:</span>
193
+ <span class="ml-2 badge badge-sm" :class="`badge-${operation.operation.toLowerCase()}`" x-text="operation.operation"></span>
194
+ </div>
195
+ <div x-show="operation.record_id">
196
+ <span class="font-medium text-gray-700">Record ID:</span>
197
+ <span class="ml-2 text-gray-900" x-text="operation.record_id"></span>
198
+ </div>
199
+ <div>
200
+ <span class="font-medium text-gray-700">Timestamp:</span>
201
+ <span class="ml-2 text-gray-900" x-text="formatTimestamp(operation.timestamp)"></span>
202
+ </div>
203
+ <div>
204
+ <span class="font-medium text-gray-700">Relative Time:</span>
205
+ <span class="ml-2 text-gray-900" x-text="formatRelativeTime(operation)"></span>
206
+ </div>
207
+ <div>
208
+ <span class="font-medium text-gray-700">Sequence:</span>
209
+ <span class="ml-2 text-gray-900" x-text="operation.sequence + 1"></span>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Changes Details -->
214
+ <template x-if="operation.changes && Object.keys(operation.changes).length > 0">
215
+ <div class="mt-4">
216
+ <h5 class="font-medium text-gray-700 mb-2">Changes:</h5>
217
+ <div class="bg-white border border-gray-200 rounded p-3 text-xs overflow-auto max-h-32">
218
+ <template x-for="[field, change] in Object.entries(operation.changes)" :key="field">
219
+ <div class="mb-2 last:mb-0">
220
+ <span class="font-medium text-gray-700" x-text="field"></span>:
221
+ <template x-if="change.from !== undefined">
222
+ <span>
223
+ <span class="text-red-600" x-text="change.from"></span>
224
+
225
+ <span class="text-green-600" x-text="change.to"></span>
226
+ </span>
227
+ </template>
228
+ <template x-if="change.from === undefined">
229
+ <span class="text-green-600" x-text="change.to || change"></span>
230
+ </template>
231
+ </div>
232
+ </template>
233
+ </div>
234
+ </div>
235
+ </template>
236
+
237
+ <!-- Metadata -->
238
+ <template x-if="operation.metadata && Object.keys(operation.metadata).length > 0">
239
+ <div class="mt-4">
240
+ <h5 class="font-medium text-gray-700 mb-2">Metadata:</h5>
241
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
242
+ <template x-for="[key, value] in Object.entries(operation.metadata).filter(([k, v]) => v !== null && v !== undefined)" :key="key">
243
+ <div>
244
+ <span class="font-medium text-gray-600 capitalize" x-text="key.replace('_', ' ')"></span>:
245
+ <span class="ml-1 text-gray-900" x-text="value"></span>
246
+ </div>
247
+ </template>
248
+ </div>
249
+ </div>
250
+ </template>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </template>
255
+ </div>
256
+ </template>
257
+ </div>
258
+ </div>
259
+
260
+ </div>
@@ -1,10 +1,12 @@
1
1
  <%= render layout: 'layout', locals: { active_tab: @active_tab, session: @session } do %>
2
2
  <% case @active_tab %>
3
+ <% when 'timeline' %>
4
+ <%= render partial: 'timeline' %>
3
5
  <% when 'summary' %>
4
6
  <%= render partial: 'summary' %>
5
7
  <% when 'diagrams' %>
6
8
  <%= render partial: 'diagrams' %>
7
9
  <% else %>
8
- <%= render partial: 'changes' %>
10
+ <%= render partial: 'tables' %>
9
11
  <% end %>
10
12
  <% end %>
@@ -30,6 +30,7 @@
30
30
  <%= javascript_include_tag "dbwatcher/components/diagrams" %>
31
31
  <%= javascript_include_tag "dbwatcher/components/summary" %>
32
32
  <%= javascript_include_tag "dbwatcher/components/dashboard" %>
33
+ <%= javascript_include_tag "dbwatcher/components/timeline" %>
33
34
 
34
35
  <!-- DBWatcher Services -->
35
36
  <%= javascript_include_tag "dbwatcher/core/alpine_store" %>
data/config/routes.rb CHANGED
@@ -27,9 +27,10 @@ Dbwatcher::Engine.routes.draw do
27
27
  namespace :v1 do
28
28
  resources :sessions, only: [] do
29
29
  member do
30
- get :changes_data
30
+ get :tables_data
31
31
  get :summary_data
32
32
  get :diagram_data
33
+ get :timeline_data
33
34
  end
34
35
 
35
36
  collection do
@@ -26,6 +26,9 @@ module Dbwatcher
26
26
  :collect_sensitive_env_vars, :system_info_cache_duration,
27
27
  :system_info_include_performance_metrics
28
28
 
29
+ # Logging configuration
30
+ attr_accessor :debug_logging
31
+
29
32
  # Initialize with default values
30
33
  def initialize
31
34
  # Storage configuration defaults
@@ -47,6 +50,9 @@ module Dbwatcher
47
50
 
48
51
  # Initialize system information configuration with defaults
49
52
  initialize_system_info_config
53
+
54
+ # Initialize logging configuration with defaults
55
+ initialize_logging_config
50
56
  end
51
57
 
52
58
  # Initialize diagram configuration with default values
@@ -73,6 +79,11 @@ module Dbwatcher
73
79
  @system_info_include_performance_metrics = true
74
80
  end
75
81
 
82
+ # Initialize logging configuration with default values
83
+ def initialize_logging_config
84
+ @debug_logging = false
85
+ end
86
+
76
87
  # Validate configuration
77
88
  #
78
89
  # @return [Boolean] true if configuration is valid
@@ -14,9 +14,12 @@ module Dbwatcher
14
14
  end
15
15
 
16
16
  # Log a debug message with optional context
17
+ # Only logs if debug mode is enabled
17
18
  # @param message [String] the log message
18
19
  # @param context [Hash] additional context data
19
20
  def log_debug(message, context = {})
21
+ return unless debug_enabled?
22
+
20
23
  log_with_level(:debug, message, context)
21
24
  end
22
25
 
@@ -34,6 +37,16 @@ module Dbwatcher
34
37
  log_with_level(:error, message, context)
35
38
  end
36
39
 
40
+ # Check if debug logging is enabled
41
+ # @return [Boolean] true if debug logging is enabled
42
+ def debug_enabled?
43
+ return Dbwatcher.configuration.debug_logging if defined?(Dbwatcher.configuration) &&
44
+ Dbwatcher.configuration.respond_to?(:debug_logging)
45
+ return Rails.env.development? if defined?(Rails)
46
+
47
+ false
48
+ end
49
+
37
50
  private
38
51
 
39
52
  def log_with_level(level, message, context)
@@ -51,7 +64,16 @@ module Dbwatcher
51
64
  end
52
65
 
53
66
  def component_name
54
- self.class.name.split("::").last
67
+ if is_a?(Module) && !is_a?(Class)
68
+ # For modules
69
+ name.to_s.split("::").last
70
+ elsif self.class.name
71
+ # For classes
72
+ self.class.name.split("::").last
73
+ else
74
+ # Fallback
75
+ "Logger"
76
+ end
55
77
  end
56
78
 
57
79
  def rails_logger
@@ -51,7 +51,8 @@ module Dbwatcher
51
51
  sample_record: nil,
52
52
  total_operations: 0,
53
53
  operations: { insert: 0, update: 0, delete: 0 },
54
- changes: []
54
+ changes: [],
55
+ model_class: find_model_class(table_name)
55
56
  }
56
57
  end
57
58
 
@@ -196,6 +197,106 @@ module Dbwatcher
196
197
  total_operations: tables.values.sum { |t| t[:total_operations] }
197
198
  }
198
199
  end
200
+
201
+ # Find the actual Rails model class for a table name
202
+ #
203
+ # @param table_name [String] database table name
204
+ # @return [String, nil] model class name or nil if not found
205
+ def find_model_class(table_name)
206
+ return nil unless table_name.is_a?(String)
207
+
208
+ Rails.logger.debug "Finding model class for table: #{table_name}"
209
+ ensure_models_loaded
210
+
211
+ # Try conventional naming first
212
+ model_name = find_by_convention(table_name)
213
+ return model_name if model_name
214
+
215
+ # Fallback to searching all ActiveRecord descendants
216
+ find_by_table_name_search(table_name)
217
+ rescue StandardError => e
218
+ log_model_finding_error(table_name, e)
219
+ nil
220
+ end
221
+
222
+ # Ensure all models are loaded in development
223
+ def ensure_models_loaded
224
+ Rails.application.eager_load! if Rails.env.development?
225
+ end
226
+
227
+ # Find model by conventional naming (table_name.classify)
228
+ #
229
+ # @param table_name [String] database table name
230
+ # @return [String, nil] model class name or nil
231
+ def find_by_convention(table_name)
232
+ model_name = table_name.classify
233
+ Rails.logger.debug "Expected model name: #{model_name}"
234
+
235
+ return nil unless Object.const_defined?(model_name)
236
+
237
+ model_class = Object.const_get(model_name)
238
+ Rails.logger.debug "Found model class: #{model_class}"
239
+
240
+ validate_and_return_model(model_class, table_name)
241
+ end
242
+
243
+ # Validate model class and return name if valid
244
+ #
245
+ # @param model_class [Class] the model class to validate
246
+ # @param table_name [String] expected table name
247
+ # @return [String, nil] model name or nil
248
+ def validate_and_return_model(model_class, table_name)
249
+ unless active_record_model?(model_class)
250
+ Rails.logger.debug "#{model_class} is not an ActiveRecord model"
251
+ return nil
252
+ end
253
+
254
+ if model_class.table_name == table_name
255
+ Rails.logger.debug "Model #{model_class.name} matches table #{table_name}"
256
+ model_class.name
257
+ else
258
+ log_table_name_mismatch(model_class, table_name)
259
+ nil
260
+ end
261
+ end
262
+
263
+ # Check if class is an ActiveRecord model
264
+ #
265
+ # @param model_class [Class] class to check
266
+ # @return [Boolean] true if ActiveRecord model
267
+ def active_record_model?(model_class)
268
+ model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
269
+ end
270
+
271
+ # Find model by searching all ActiveRecord descendants
272
+ #
273
+ # @param table_name [String] database table name
274
+ # @return [String, nil] model class name or nil
275
+ def find_by_table_name_search(table_name)
276
+ Rails.logger.debug "Checking all ActiveRecord descendants..."
277
+
278
+ ActiveRecord::Base.descendants.each do |model|
279
+ if model.table_name == table_name
280
+ Rails.logger.debug "Found matching model: #{model.name} for table #{table_name}"
281
+ return model.name
282
+ end
283
+ end
284
+
285
+ Rails.logger.debug "No model found for table: #{table_name}"
286
+ nil
287
+ end
288
+
289
+ # Log table name mismatch
290
+ def log_table_name_mismatch(model_class, table_name)
291
+ message = "Model #{model_class.name} table_name (#{model_class.table_name}) doesn't match #{table_name}"
292
+ Rails.logger.debug message
293
+ end
294
+
295
+ # Log model finding error
296
+ def log_model_finding_error(table_name, error)
297
+ Rails.logger.debug "Error finding model class for table #{table_name}: #{error.message}"
298
+ Rails.logger.debug error.backtrace.first(5).join("\n")
299
+ end
199
300
  end
200
301
  end
201
302
  end
@@ -3,25 +3,25 @@
3
3
  module Dbwatcher
4
4
  module Services
5
5
  module Api
6
- # Service for handling filtered changes data
6
+ # Service for handling filtered tables data
7
7
  #
8
- # Provides changes data for the sessions changes view and API endpoints
8
+ # Provides tables data for the sessions tables view and API endpoints
9
9
  # with filtering and caching support.
10
- class ChangesDataService < BaseApiService
10
+ class TablesDataService < BaseApiService
11
11
  def call
12
12
  start_time = Time.now
13
13
 
14
14
  # Check for nil session first
15
15
  return { error: "Session not found" } unless session
16
16
 
17
- log_service_start("Getting changes data for session #{session.id}")
17
+ log_service_start("Getting tables data for session #{session.id}")
18
18
 
19
19
  validation_error = validate_session
20
20
  return validation_error if validation_error
21
21
 
22
22
  begin
23
23
  result = with_cache(cache_suffix) do
24
- build_changes_response
24
+ build_tables_response
25
25
  end
26
26
 
27
27
  log_service_completion(start_time, session_id: session.id, filters: filter_params)
@@ -33,7 +33,7 @@ module Dbwatcher
33
33
 
34
34
  private
35
35
 
36
- def build_changes_response
36
+ def build_tables_response
37
37
  {
38
38
  tables_summary: build_filtered_summary,
39
39
  filters: filter_params || {},
@@ -232,6 +232,37 @@ module Dbwatcher
232
232
  # @param primary_key [String, nil] optional primary key for testing
233
233
  # @return [Boolean] true if likely self-referential
234
234
  def self_referential_column?(column_name, table_name, primary_key = nil)
235
+ # Get the singular form of the table name
236
+ base_name = singularize(table_name)
237
+
238
+ # Special case for post_id in posts table - not a self-reference
239
+ return false if column_name == "#{base_name}_id" && table_name == "posts" && base_name == "post"
240
+
241
+ # Check primary key if this is a table-specific reference
242
+ if column_name == "#{base_name}_id"
243
+ if primary_key.nil?
244
+ begin
245
+ primary_key = connection.primary_key(table_name)
246
+ rescue StandardError
247
+ return false
248
+ end
249
+ end
250
+
251
+ return column_name != primary_key
252
+ end
253
+
254
+ # Use pattern matching to check various self-referential patterns
255
+ common_pattern?(column_name) ||
256
+ hierarchy_pattern?(column_name, base_name) ||
257
+ relationship_pattern?(column_name) ||
258
+ directional_pattern?(column_name)
259
+ end
260
+
261
+ # Check if column matches common self-referential patterns
262
+ #
263
+ # @param column_name [String] column name to check
264
+ # @return [Boolean] true if matches common patterns
265
+ def common_pattern?(column_name)
235
266
  # Common self-referential patterns
236
267
  self_ref_patterns = %w[
237
268
  parent_id
@@ -257,52 +288,47 @@ module Dbwatcher
257
288
  replied_to_id
258
289
  ]
259
290
 
260
- # Check for exact matches with common patterns
261
- return true if self_ref_patterns.include?(column_name)
262
-
263
- # Get the singular form of the table name
264
- base_name = singularize(table_name)
265
-
266
- # Special case for post_id in posts table - not a self-reference
267
- return false if column_name == "#{base_name}_id" && table_name == "posts" && base_name == "post"
268
-
269
- # Check for table-specific self-references (e.g., comment_id in comments table)
270
- if column_name == "#{base_name}_id"
271
- # Check if this is not the primary key column
272
- if primary_key.nil?
273
- begin
274
- primary_key = connection.primary_key(table_name)
275
- rescue StandardError
276
- return false
277
- end
278
- end
279
-
280
- return column_name != primary_key
281
- end
291
+ self_ref_patterns.include?(column_name)
292
+ end
282
293
 
283
- # Check for hierarchy patterns with table name
294
+ # Check for hierarchy patterns with table name
295
+ #
296
+ # @param column_name [String] column name to check
297
+ # @param base_name [String] singular form of table name
298
+ # @return [Boolean] true if matches hierarchy patterns
299
+ def hierarchy_pattern?(column_name, base_name)
284
300
  hierarchy_prefixes = %w[parent child ancestor descendant superior subordinate manager supervisor]
285
- hierarchy_prefixes.each do |prefix|
286
- # Check for patterns like parent_comment_id in comments table
287
- return true if column_name.start_with?("#{prefix}_#{base_name}_id")
288
301
 
289
- # Check for patterns like parent_of_id in any table
290
- return true if column_name.start_with?("#{prefix}_of_id")
302
+ hierarchy_prefixes.any? do |prefix|
303
+ # Check for patterns like parent_comment_id in comments table
304
+ column_name.start_with?("#{prefix}_#{base_name}_id") ||
305
+ # Check for patterns like parent_of_id in any table
306
+ column_name.start_with?("#{prefix}_of_id")
291
307
  end
308
+ end
292
309
 
293
- # Check for relationship patterns
310
+ # Check for relationship patterns
311
+ #
312
+ # @param column_name [String] column name to check
313
+ # @return [Boolean] true if matches relationship patterns
314
+ def relationship_pattern?(column_name)
294
315
  relationship_patterns = %w[related linked connected associated referenced]
295
- relationship_patterns.each do |pattern|
296
- return true if column_name.start_with?("#{pattern}_")
316
+
317
+ relationship_patterns.any? do |pattern|
318
+ column_name.start_with?("#{pattern}_")
297
319
  end
320
+ end
298
321
 
299
- # Check for directional patterns
322
+ # Check for directional patterns
323
+ #
324
+ # @param column_name [String] column name to check
325
+ # @return [Boolean] true if matches directional patterns
326
+ def directional_pattern?(column_name)
300
327
  directional_patterns = %w[previous next original copy source target]
301
- directional_patterns.each do |pattern|
302
- return true if column_name.start_with?("#{pattern}_")
303
- end
304
328
 
305
- false
329
+ directional_patterns.any? do |pattern|
330
+ column_name.start_with?("#{pattern}_")
331
+ end
306
332
  end
307
333
 
308
334
  # Analyze junction tables (many-to-many relationships)