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.
- checksums.yaml +4 -4
- data/app/assets/config/dbwatcher_manifest.js +1 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +184 -97
- data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
- data/app/assets/stylesheets/dbwatcher/application.css +298 -1
- data/app/assets/stylesheets/dbwatcher/application.scss +1 -0
- data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +27 -17
- data/app/controllers/dbwatcher/sessions_controller.rb +1 -1
- data/app/views/dbwatcher/sessions/_layout.html.erb +5 -2
- data/app/views/dbwatcher/sessions/_summary.html.erb +1 -1
- data/app/views/dbwatcher/sessions/{_changes.html.erb → _tables.html.erb} +84 -5
- data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -1
- data/app/views/layouts/dbwatcher/application.html.erb +1 -0
- data/config/routes.rb +2 -1
- data/lib/dbwatcher/configuration.rb +11 -0
- data/lib/dbwatcher/logging.rb +23 -1
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
- data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +62 -36
- data/lib/dbwatcher/services/diagram_generator.rb +35 -69
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +23 -9
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +16 -22
- data/lib/dbwatcher/services/diagram_strategies/diagram_strategy_helpers.rb +33 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +20 -25
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +20 -25
- data/lib/dbwatcher/services/diagram_strategies/standard_diagram_strategy.rb +80 -0
- data/lib/dbwatcher/services/diagram_system.rb +14 -1
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +2 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +10 -8
- data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
- data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
- data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
- data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +1 -1
- 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: '
|
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
@@ -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
|
data/lib/dbwatcher/logging.rb
CHANGED
@@ -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
|
-
|
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
|
6
|
+
# Service for handling filtered tables data
|
7
7
|
#
|
8
|
-
# Provides
|
8
|
+
# Provides tables data for the sessions tables view and API endpoints
|
9
9
|
# with filtering and caching support.
|
10
|
-
class
|
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
|
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
|
-
|
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
|
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
|
-
|
261
|
-
|
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
|
-
|
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
|
-
|
290
|
-
|
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
|
-
|
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
|
-
|
296
|
-
|
316
|
+
|
317
|
+
relationship_patterns.any? do |pattern|
|
318
|
+
column_name.start_with?("#{pattern}_")
|
297
319
|
end
|
320
|
+
end
|
298
321
|
|
299
|
-
|
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
|
-
|
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)
|