dbwatcher 1.0.0 → 1.1.1
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/README.md +81 -210
- data/app/assets/config/dbwatcher_manifest.js +15 -0
- data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
- data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
- data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
- data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
- data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
- data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
- data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
- data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
- data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
- data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
- data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
- data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
- data/app/assets/stylesheets/dbwatcher/application.css +423 -0
- data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
- data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
- data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
- data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
- data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
- data/app/controllers/dbwatcher/base_controller.rb +8 -2
- data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/session_helper.rb +3 -2
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
- data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
- data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
- data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
- data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
- data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
- data/app/views/dbwatcher/sessions/index.html.erb +14 -10
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -346
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/layouts/dbwatcher/application.html.erb +125 -247
- data/bin/compile_scss +49 -0
- data/config/routes.rb +26 -0
- data/lib/dbwatcher/configuration.rb +102 -8
- data/lib/dbwatcher/engine.rb +17 -7
- data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
- data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
- data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
- data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
- data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
- data/lib/dbwatcher/services/base_service.rb +64 -0
- data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
- data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +603 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
- data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
- data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
- data/lib/dbwatcher/services/diagram_data.rb +65 -0
- data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
- data/lib/dbwatcher/services/diagram_generator.rb +154 -0
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_system.rb +69 -0
- data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
- data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +140 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
- data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +118 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
- data/lib/dbwatcher/storage/api/session_api.rb +47 -0
- data/lib/dbwatcher/storage/base_storage.rb +7 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +58 -1
- metadata +94 -2
@@ -0,0 +1,502 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string/inflections" if defined?(ActiveSupport)
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Services
|
7
|
+
module DiagramAnalyzers
|
8
|
+
# Analyzes relationships based on naming conventions and data patterns
|
9
|
+
#
|
10
|
+
# This service infers relationships between tables when explicit foreign keys
|
11
|
+
# are not present, using naming conventions, column patterns, and junction table detection.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# analyzer = InferredRelationshipAnalyzer.new(session)
|
15
|
+
# dataset = analyzer.call
|
16
|
+
class InferredRelationshipAnalyzer < BaseAnalyzer
|
17
|
+
# Initialize with session
|
18
|
+
#
|
19
|
+
# @param session [Session] session to analyze (optional for global analysis)
|
20
|
+
def initialize(session = nil)
|
21
|
+
@session = session
|
22
|
+
@connection = ActiveRecord::Base.connection if defined?(ActiveRecord::Base)
|
23
|
+
@session_tables = session ? extract_session_tables : []
|
24
|
+
super()
|
25
|
+
end
|
26
|
+
|
27
|
+
# Analyze inferred relationships
|
28
|
+
#
|
29
|
+
# @param context [Hash] analysis context
|
30
|
+
# @return [Array<Hash>] array of inferred relationship data
|
31
|
+
def analyze(_context)
|
32
|
+
return [] unless schema_available?
|
33
|
+
|
34
|
+
Rails.logger.debug "InferredRelationshipAnalyzer: Starting analysis with #{tables_to_analyze.length} tables"
|
35
|
+
relationships = []
|
36
|
+
|
37
|
+
# Analyze naming convention relationships
|
38
|
+
relationships.concat(analyze_naming_conventions)
|
39
|
+
|
40
|
+
# Analyze junction tables
|
41
|
+
relationships.concat(analyze_junction_tables)
|
42
|
+
|
43
|
+
# Analyze column patterns
|
44
|
+
relationships.concat(analyze_column_patterns)
|
45
|
+
|
46
|
+
Rails.logger.info "InferredRelationshipAnalyzer: Found #{relationships.length} inferred relationships"
|
47
|
+
relationships
|
48
|
+
end
|
49
|
+
|
50
|
+
# Transform raw relationship data to Dataset
|
51
|
+
#
|
52
|
+
# @param raw_data [Array<Hash>] raw relationship data
|
53
|
+
# @return [DiagramData::Dataset] standardized dataset
|
54
|
+
def transform_to_dataset(raw_data)
|
55
|
+
dataset = create_empty_dataset
|
56
|
+
dataset.metadata.merge!({
|
57
|
+
total_relationships: raw_data.length,
|
58
|
+
tables_analyzed: tables_to_analyze.length,
|
59
|
+
inference_types: raw_data.map { |r| r[:inference_type] }.uniq
|
60
|
+
})
|
61
|
+
|
62
|
+
# Create entities for each unique table
|
63
|
+
table_entities = {}
|
64
|
+
|
65
|
+
# First, collect all unique tables from the relationships
|
66
|
+
tables = []
|
67
|
+
raw_data.each do |relationship|
|
68
|
+
tables << relationship[:from_table] if relationship[:from_table]
|
69
|
+
tables << relationship[:to_table] if relationship[:to_table]
|
70
|
+
end
|
71
|
+
tables.uniq!
|
72
|
+
|
73
|
+
# Create entities for all tables
|
74
|
+
tables.each do |table_name|
|
75
|
+
entity = create_entity(
|
76
|
+
id: table_name,
|
77
|
+
name: table_name,
|
78
|
+
type: "table",
|
79
|
+
metadata: {
|
80
|
+
table_name: table_name,
|
81
|
+
source: "inferred_analysis"
|
82
|
+
}
|
83
|
+
)
|
84
|
+
dataset.add_entity(entity)
|
85
|
+
table_entities[table_name] = entity
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create relationships in a separate loop
|
89
|
+
raw_data.each do |relationship|
|
90
|
+
next unless relationship[:from_table] && relationship[:to_table]
|
91
|
+
|
92
|
+
# Include self-referential relationships (source and target are the same)
|
93
|
+
# but log them for debugging
|
94
|
+
if relationship[:from_table] == relationship[:to_table]
|
95
|
+
Rails.logger.info "InferredRelationshipAnalyzer: Including self-referential relationship for " \
|
96
|
+
"#{relationship[:from_table]} " \
|
97
|
+
"(#{relationship[:from_column]} -> #{relationship[:to_column]})"
|
98
|
+
end
|
99
|
+
|
100
|
+
relationship_obj = create_relationship({
|
101
|
+
source_id: relationship[:from_table],
|
102
|
+
target_id: relationship[:to_table],
|
103
|
+
type: relationship[:type],
|
104
|
+
label: relationship[:label],
|
105
|
+
metadata: {
|
106
|
+
inference_type: relationship[:inference_type],
|
107
|
+
confidence: relationship[:confidence],
|
108
|
+
from_column: relationship[:from_column],
|
109
|
+
to_column: relationship[:to_column],
|
110
|
+
original_type: relationship[:type],
|
111
|
+
self_referential: relationship[:from_table] ==
|
112
|
+
relationship[:to_table]
|
113
|
+
}
|
114
|
+
})
|
115
|
+
|
116
|
+
dataset.add_relationship(relationship_obj)
|
117
|
+
end
|
118
|
+
|
119
|
+
dataset
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get analyzer type
|
123
|
+
#
|
124
|
+
# @return [String] analyzer type identifier
|
125
|
+
def analyzer_type
|
126
|
+
"inferred_relationship"
|
127
|
+
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
# Build analysis context for this analyzer
|
132
|
+
#
|
133
|
+
# @return [Hash] analysis context
|
134
|
+
def analysis_context
|
135
|
+
{
|
136
|
+
session: session,
|
137
|
+
session_tables: session_tables,
|
138
|
+
tables_to_analyze: tables_to_analyze
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get the database connection
|
143
|
+
#
|
144
|
+
# @return [ActiveRecord::ConnectionAdapters::AbstractAdapter] database connection
|
145
|
+
attr_reader :connection
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
attr_reader :session, :session_tables
|
150
|
+
|
151
|
+
# Check if schema analysis is available
|
152
|
+
#
|
153
|
+
# @return [Boolean] true if schema can be analyzed
|
154
|
+
def schema_available?
|
155
|
+
defined?(ActiveRecord::Base) &&
|
156
|
+
connection.respond_to?(:tables) &&
|
157
|
+
connection.respond_to?(:columns)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Extract tables that were involved in the session
|
161
|
+
#
|
162
|
+
# @return [Array<String>] unique table names
|
163
|
+
def extract_session_tables
|
164
|
+
return [] unless session&.changes
|
165
|
+
|
166
|
+
session.changes.map do |change|
|
167
|
+
change[:table_name] || change["table_name"]
|
168
|
+
end.compact.uniq
|
169
|
+
end
|
170
|
+
|
171
|
+
# Get tables to analyze (session tables or all tables if no session)
|
172
|
+
#
|
173
|
+
# @return [Array<String>] table names to analyze
|
174
|
+
def tables_to_analyze
|
175
|
+
session_tables.any? ? session_tables : connection.tables
|
176
|
+
end
|
177
|
+
|
178
|
+
# Analyze naming convention relationships (e.g., user_id -> users)
|
179
|
+
#
|
180
|
+
# @return [Array<Hash>] naming convention relationships
|
181
|
+
def analyze_naming_conventions
|
182
|
+
relationships = []
|
183
|
+
|
184
|
+
tables_to_analyze.each do |table_name|
|
185
|
+
next unless table_exists?(table_name)
|
186
|
+
|
187
|
+
columns = get_table_columns(table_name)
|
188
|
+
|
189
|
+
columns.each do |column|
|
190
|
+
# Look for _id columns that might reference other tables
|
191
|
+
next unless column.name.end_with?("_id") && column.name != "id"
|
192
|
+
|
193
|
+
# Check for common self-referential patterns
|
194
|
+
if self_referential_column?(column.name, table_name)
|
195
|
+
relationships << {
|
196
|
+
from_table: table_name,
|
197
|
+
to_table: table_name,
|
198
|
+
type: "inferred_belongs_to",
|
199
|
+
inference_type: "self_referential",
|
200
|
+
confidence: 0.9,
|
201
|
+
from_column: column.name,
|
202
|
+
to_column: "id",
|
203
|
+
label: "inferred (#{column.name})"
|
204
|
+
}
|
205
|
+
next
|
206
|
+
end
|
207
|
+
|
208
|
+
referenced_table = infer_table_from_column(column.name)
|
209
|
+
|
210
|
+
next unless referenced_table && tables_to_analyze.include?(referenced_table)
|
211
|
+
|
212
|
+
relationships << {
|
213
|
+
from_table: table_name,
|
214
|
+
to_table: referenced_table,
|
215
|
+
type: "inferred_belongs_to",
|
216
|
+
inference_type: "naming_convention",
|
217
|
+
confidence: 0.8,
|
218
|
+
from_column: column.name,
|
219
|
+
to_column: "id",
|
220
|
+
label: "inferred (#{column.name})"
|
221
|
+
}
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
relationships
|
226
|
+
end
|
227
|
+
|
228
|
+
# Check if a column name suggests a self-referential relationship
|
229
|
+
#
|
230
|
+
# @param column_name [String] column name to check
|
231
|
+
# @param table_name [String] current table name
|
232
|
+
# @param primary_key [String, nil] optional primary key for testing
|
233
|
+
# @return [Boolean] true if likely self-referential
|
234
|
+
def self_referential_column?(column_name, table_name, primary_key = nil)
|
235
|
+
# Common self-referential patterns
|
236
|
+
self_ref_patterns = %w[
|
237
|
+
parent_id
|
238
|
+
ancestor_id
|
239
|
+
child_id
|
240
|
+
reply_to_id
|
241
|
+
reference_id
|
242
|
+
original_id
|
243
|
+
source_id
|
244
|
+
target_id
|
245
|
+
superior_id
|
246
|
+
manager_id
|
247
|
+
supervisor_id
|
248
|
+
predecessor_id
|
249
|
+
successor_id
|
250
|
+
previous_id
|
251
|
+
next_id
|
252
|
+
related_id
|
253
|
+
duplicate_id
|
254
|
+
clone_id
|
255
|
+
copy_id
|
256
|
+
forwarded_id
|
257
|
+
replied_to_id
|
258
|
+
]
|
259
|
+
|
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
|
282
|
+
|
283
|
+
# Check for hierarchy patterns with table name
|
284
|
+
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
|
+
|
289
|
+
# Check for patterns like parent_of_id in any table
|
290
|
+
return true if column_name.start_with?("#{prefix}_of_id")
|
291
|
+
end
|
292
|
+
|
293
|
+
# Check for relationship patterns
|
294
|
+
relationship_patterns = %w[related linked connected associated referenced]
|
295
|
+
relationship_patterns.each do |pattern|
|
296
|
+
return true if column_name.start_with?("#{pattern}_")
|
297
|
+
end
|
298
|
+
|
299
|
+
# Check for directional patterns
|
300
|
+
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
|
+
|
305
|
+
false
|
306
|
+
end
|
307
|
+
|
308
|
+
# Analyze junction tables (many-to-many relationships)
|
309
|
+
#
|
310
|
+
# @return [Array<Hash>] junction table relationships
|
311
|
+
def analyze_junction_tables
|
312
|
+
relationships = []
|
313
|
+
|
314
|
+
tables_to_analyze.each do |table_name|
|
315
|
+
next unless junction_table?(table_name)
|
316
|
+
|
317
|
+
junction_relationships = analyze_junction_table(table_name)
|
318
|
+
relationships.concat(junction_relationships)
|
319
|
+
end
|
320
|
+
|
321
|
+
relationships
|
322
|
+
end
|
323
|
+
|
324
|
+
# Analyze column patterns for relationships
|
325
|
+
#
|
326
|
+
# @return [Array<Hash>] column pattern relationships
|
327
|
+
def analyze_column_patterns
|
328
|
+
relationships = []
|
329
|
+
|
330
|
+
# Look for common patterns like created_by_id, updated_by_id, etc.
|
331
|
+
tables_to_analyze.each do |table_name|
|
332
|
+
next unless table_exists?(table_name)
|
333
|
+
|
334
|
+
columns = get_table_columns(table_name)
|
335
|
+
|
336
|
+
columns.each do |column|
|
337
|
+
# Look for audit columns that might reference users
|
338
|
+
next unless audit_column?(column.name)
|
339
|
+
|
340
|
+
user_table = find_user_table
|
341
|
+
|
342
|
+
next unless user_table && tables_to_analyze.include?(user_table)
|
343
|
+
|
344
|
+
relationships << {
|
345
|
+
from_table: table_name,
|
346
|
+
to_table: user_table,
|
347
|
+
type: "inferred_audit",
|
348
|
+
inference_type: "audit_pattern",
|
349
|
+
confidence: 0.6,
|
350
|
+
from_column: column.name,
|
351
|
+
to_column: "id",
|
352
|
+
label: "audit (#{column.name})"
|
353
|
+
}
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
relationships
|
358
|
+
end
|
359
|
+
|
360
|
+
# Check if table exists in database
|
361
|
+
#
|
362
|
+
# @param table_name [String] table name
|
363
|
+
# @return [Boolean] true if table exists
|
364
|
+
def table_exists?(table_name)
|
365
|
+
connection.table_exists?(table_name)
|
366
|
+
rescue StandardError
|
367
|
+
false
|
368
|
+
end
|
369
|
+
|
370
|
+
# Get columns for a table
|
371
|
+
#
|
372
|
+
# @param table_name [String] table name
|
373
|
+
# @return [Array] column objects
|
374
|
+
def get_table_columns(table_name)
|
375
|
+
connection.columns(table_name)
|
376
|
+
rescue StandardError => e
|
377
|
+
Rails.logger.warn "InferredRelationshipAnalyzer: Could not get columns for #{table_name}: #{e.message}"
|
378
|
+
[]
|
379
|
+
end
|
380
|
+
|
381
|
+
# Infer table name from column name (e.g., user_id -> users)
|
382
|
+
#
|
383
|
+
# @param column_name [String] column name ending with _id
|
384
|
+
# @return [String, nil] inferred table name
|
385
|
+
def infer_table_from_column(column_name)
|
386
|
+
base_name = column_name.gsub(/_id$/, "")
|
387
|
+
|
388
|
+
# Try pluralized version first
|
389
|
+
plural_table = pluralize(base_name)
|
390
|
+
return plural_table if connection.table_exists?(plural_table)
|
391
|
+
|
392
|
+
# Try singular version
|
393
|
+
return base_name if connection.table_exists?(base_name)
|
394
|
+
|
395
|
+
nil
|
396
|
+
end
|
397
|
+
|
398
|
+
# Get the plural form of a table name
|
399
|
+
#
|
400
|
+
# @param table_name [String] table name
|
401
|
+
# @return [String] plural form of table name
|
402
|
+
def pluralize(table_name)
|
403
|
+
return table_name if table_name.nil? || table_name.empty?
|
404
|
+
|
405
|
+
# Use ActiveSupport if available
|
406
|
+
return table_name.pluralize if table_name.respond_to?(:pluralize)
|
407
|
+
|
408
|
+
# Simple fallback pluralization rules
|
409
|
+
if table_name.end_with?("y") && !table_name.end_with?("ay", "ey", "iy", "oy", "uy")
|
410
|
+
"#{table_name[0...-1]}ies"
|
411
|
+
elsif table_name.end_with?("s", "x", "z", "ch", "sh")
|
412
|
+
"#{table_name}es"
|
413
|
+
else
|
414
|
+
"#{table_name}s"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Check if table is likely a junction table
|
419
|
+
#
|
420
|
+
# @param table_name [String] table name
|
421
|
+
# @return [Boolean] true if likely junction table
|
422
|
+
def junction_table?(table_name)
|
423
|
+
# Common junction table patterns
|
424
|
+
return true if table_name.include?("_")
|
425
|
+
|
426
|
+
columns = get_table_columns(table_name)
|
427
|
+
id_columns = columns.select { |c| c.name.end_with?("_id") && c.name != "id" }
|
428
|
+
|
429
|
+
# Junction tables typically have 2+ foreign key columns and few other columns
|
430
|
+
id_columns.length >= 2 && columns.length <= (id_columns.length + 3)
|
431
|
+
end
|
432
|
+
|
433
|
+
# Analyze a junction table for relationships
|
434
|
+
#
|
435
|
+
# @param table_name [String] junction table name
|
436
|
+
# @return [Array<Hash>] junction relationships
|
437
|
+
def analyze_junction_table(table_name)
|
438
|
+
relationships = []
|
439
|
+
columns = get_table_columns(table_name)
|
440
|
+
id_columns = columns.select { |c| c.name.end_with?("_id") && c.name != "id" }
|
441
|
+
|
442
|
+
# Create many-to-many relationships between the referenced tables
|
443
|
+
id_columns.combination(2).each do |col1, col2|
|
444
|
+
table1 = infer_table_from_column(col1.name)
|
445
|
+
table2 = infer_table_from_column(col2.name)
|
446
|
+
|
447
|
+
next unless table1 && table2 && tables_to_analyze.include?(table1) && tables_to_analyze.include?(table2)
|
448
|
+
|
449
|
+
relationships << {
|
450
|
+
from_table: table1,
|
451
|
+
to_table: table2,
|
452
|
+
type: "inferred_many_to_many",
|
453
|
+
inference_type: "junction_table",
|
454
|
+
confidence: 0.9,
|
455
|
+
from_column: "id",
|
456
|
+
to_column: "id",
|
457
|
+
label: "many-to-many via #{table_name}"
|
458
|
+
}
|
459
|
+
end
|
460
|
+
|
461
|
+
relationships
|
462
|
+
end
|
463
|
+
|
464
|
+
# Check if column is an audit column
|
465
|
+
#
|
466
|
+
# @param column_name [String] column name
|
467
|
+
# @return [Boolean] true if audit column
|
468
|
+
def audit_column?(column_name)
|
469
|
+
%w[created_by_id updated_by_id deleted_by_id author_id modifier_id].include?(column_name)
|
470
|
+
end
|
471
|
+
|
472
|
+
# Find the user table in available tables
|
473
|
+
#
|
474
|
+
# @return [String, nil] user table name
|
475
|
+
def find_user_table
|
476
|
+
user_tables = %w[users user accounts account people person]
|
477
|
+
user_tables.find { |table| tables_to_analyze.include?(table) }
|
478
|
+
end
|
479
|
+
|
480
|
+
# Get the singular form of a table name
|
481
|
+
#
|
482
|
+
# @param table_name [String] table name
|
483
|
+
# @return [String] singular form of table name
|
484
|
+
def singularize(table_name)
|
485
|
+
return table_name if table_name.nil? || table_name.empty?
|
486
|
+
|
487
|
+
# Use ActiveSupport if available
|
488
|
+
return table_name.singularize if table_name.respond_to?(:singularize)
|
489
|
+
|
490
|
+
# Simple fallback singularization rules
|
491
|
+
if table_name.end_with?("ies")
|
492
|
+
"#{table_name[0...-3]}y"
|
493
|
+
elsif table_name.end_with?("s")
|
494
|
+
table_name[0...-1]
|
495
|
+
else
|
496
|
+
table_name
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|