rails_pulse 0.1.2 → 0.1.3
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 +10 -4
- data/app/assets/images/rails_pulse/dashboard.png +0 -0
- data/app/assets/images/rails_pulse/request.png +0 -0
- data/app/assets/stylesheets/rails_pulse/application.css +28 -5
- data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
- data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
- data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
- data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
- data/app/controllers/concerns/zoom_range_concern.rb +31 -0
- data/app/controllers/rails_pulse/application_controller.rb +5 -1
- data/app/controllers/rails_pulse/queries_controller.rb +46 -1
- data/app/controllers/rails_pulse/requests_controller.rb +14 -1
- data/app/controllers/rails_pulse/routes_controller.rb +40 -1
- data/app/helpers/rails_pulse/chart_helper.rb +15 -7
- data/app/javascript/rails_pulse/application.js +34 -3
- data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
- data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
- data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +241 -11
- data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
- data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +19 -19
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +13 -8
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +13 -8
- data/app/models/rails_pulse/query.rb +46 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +17 -19
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +13 -8
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +13 -8
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +13 -8
- data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
- data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
- data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
- data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +146 -0
- data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
- data/app/services/rails_pulse/query_analysis_service.rb +125 -0
- data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
- data/app/views/layouts/rails_pulse/application.html.erb +0 -2
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
- data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
- data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
- data/app/views/rails_pulse/components/_metric_card.html.erb +27 -4
- data/app/views/rails_pulse/components/_panel.html.erb +1 -1
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
- data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/operations/show.html.erb +17 -15
- data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
- data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +87 -0
- data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
- data/app/views/rails_pulse/queries/_show_table.html.erb +1 -1
- data/app/views/rails_pulse/queries/_table.html.erb +1 -1
- data/app/views/rails_pulse/queries/index.html.erb +48 -51
- data/app/views/rails_pulse/queries/show.html.erb +56 -52
- data/app/views/rails_pulse/requests/_operations.html.erb +30 -43
- data/app/views/rails_pulse/requests/_table.html.erb +3 -1
- data/app/views/rails_pulse/requests/index.html.erb +48 -51
- data/app/views/rails_pulse/routes/_table.html.erb +1 -1
- data/app/views/rails_pulse/routes/index.html.erb +49 -52
- data/app/views/rails_pulse/routes/show.html.erb +4 -4
- data/config/routes.rb +5 -1
- data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +13 -0
- data/db/rails_pulse_schema.rb +9 -0
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +65 -0
- data/lib/generators/rails_pulse/install_generator.rb +71 -18
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +22 -0
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
- data/lib/generators/rails_pulse/upgrade_generator.rb +225 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/tasks/rails_pulse.rake +27 -8
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +53 -53
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +23 -5
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/images/rails_pulse/routes.png +0 -0
- data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
@@ -10,64 +10,61 @@
|
|
10
10
|
<% end %>
|
11
11
|
|
12
12
|
<div
|
13
|
-
class="row"
|
14
13
|
data-controller="rails-pulse--index"
|
15
14
|
data-rails-pulse--index-chart-id-value="average_response_times_chart"
|
16
15
|
>
|
17
|
-
|
18
|
-
<%=
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
16
|
+
<%= render 'rails_pulse/components/panel', { title: 'Average Response Time', card_classes: 'b-full' } do %>
|
17
|
+
<%= search_form_for @ransack_query, url: routes_path, class: "flex items-center justify-between gap mb-4" do |form| %>
|
18
|
+
<div class="flex items-center grow gap">
|
19
|
+
<%= form.search_field :route_path_cont, placeholder: "Filter by route", autocomplete: "off", class: "input", style: "max-inline-size: 250px" %>
|
20
|
+
<%= form.select :period_start_range,
|
21
|
+
RailsPulse::RoutesController::TIME_RANGE_OPTIONS,
|
22
|
+
{ selected: @selected_time_range },
|
23
|
+
{ class: "input" }
|
24
|
+
%>
|
25
|
+
<%= form.select :avg_duration,
|
26
|
+
duration_options(:route),
|
27
|
+
{ selected: @selected_response_range },
|
28
|
+
{ class: "input" }
|
29
|
+
%>
|
30
|
+
<%= link_to "Reset", routes_path, class: "btn btn--borderless show@md" if params.has_key?(:q) %>
|
31
|
+
<%= form.submit "Search", class: "btn show@sm" %>
|
32
|
+
</div>
|
33
|
+
<% end %>
|
34
|
+
|
35
|
+
<% if @has_data %>
|
36
|
+
<% if @chart_data && @chart_data.values.any? { |v| v > 0 } %>
|
37
|
+
<div
|
38
|
+
class="chart-container chart-container--slim"
|
39
|
+
data-rails-pulse--index-target="chart"
|
40
|
+
>
|
41
|
+
<%= bar_chart(
|
42
|
+
@chart_data,
|
43
|
+
code: false,
|
44
|
+
id: "average_response_times_chart",
|
45
|
+
height: "100%",
|
46
|
+
options: bar_chart_options(
|
47
|
+
units: "ms",
|
48
|
+
zoom: true,
|
49
|
+
chart_start: 0,
|
50
|
+
chart_end: @chart_data.length - 1,
|
51
|
+
xaxis_formatter: @xaxis_formatter,
|
52
|
+
tooltip_formatter: @tooltip_formatter,
|
53
|
+
zoom_start: @zoom_start,
|
54
|
+
zoom_end: @zoom_end,
|
55
|
+
chart_data: @chart_data
|
56
|
+
)
|
57
|
+
) %>
|
34
58
|
</div>
|
35
59
|
<% end %>
|
36
60
|
|
37
|
-
|
38
|
-
|
39
|
-
<div
|
40
|
-
class="chart-container chart-container--slim"
|
41
|
-
data-rails-pulse--index-target="chart"
|
42
|
-
>
|
43
|
-
<%= bar_chart(
|
44
|
-
@chart_data,
|
45
|
-
code: false,
|
46
|
-
id: "average_response_times_chart",
|
47
|
-
height: "100%",
|
48
|
-
options: bar_chart_options(
|
49
|
-
units: "ms",
|
50
|
-
zoom: true,
|
51
|
-
chart_start: 0,
|
52
|
-
chart_end: @chart_data.length - 1,
|
53
|
-
xaxis_formatter: @xaxis_formatter,
|
54
|
-
tooltip_formatter: @tooltip_formatter,
|
55
|
-
zoom_start: @zoom_start,
|
56
|
-
zoom_end: @zoom_end,
|
57
|
-
chart_data: @chart_data
|
58
|
-
)
|
59
|
-
) %>
|
60
|
-
</div>
|
61
|
-
<% end %>
|
62
|
-
|
63
|
-
<%= turbo_frame_tag :routes_index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
64
|
-
<%= render 'rails_pulse/routes/table' %>
|
65
|
-
<% end %>
|
66
|
-
<% else %>
|
67
|
-
<%= render 'rails_pulse/components/empty_state',
|
68
|
-
title: 'No route data found for the selected filters.',
|
69
|
-
description: 'Try adjusting your time range or filters to see results.' %>
|
61
|
+
<%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
62
|
+
<%= render 'rails_pulse/routes/table' %>
|
70
63
|
<% end %>
|
64
|
+
<% else %>
|
65
|
+
<%= render 'rails_pulse/components/empty_state',
|
66
|
+
title: 'No route data found for the selected filters.',
|
67
|
+
description: 'Try adjusting your time range or filters to see results.' %>
|
71
68
|
<% end %>
|
72
|
-
|
69
|
+
<% end %>
|
73
70
|
</div>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<%= render 'rails_pulse/components/breadcrumbs' %>
|
2
2
|
|
3
|
-
<h1 class="text-2xl
|
3
|
+
<h1 class="text-2xl mis-2"><%= @route.path_and_method %></h1>
|
4
4
|
|
5
5
|
<% unless turbo_frame_request? %>
|
6
6
|
<div class="row">
|
@@ -61,12 +61,12 @@
|
|
61
61
|
</div>
|
62
62
|
<% end %>
|
63
63
|
|
64
|
-
<%= turbo_frame_tag :index_table do %>
|
64
|
+
<%= turbo_frame_tag :index_table, data: { rails_pulse__index_target: "indexTable" } do %>
|
65
65
|
<%= render 'rails_pulse/requests/table' %>
|
66
66
|
<% end %>
|
67
67
|
<% else %>
|
68
|
-
<%= render 'rails_pulse/components/empty_state',
|
69
|
-
title: 'No route requests found for the selected filters.',
|
68
|
+
<%= render 'rails_pulse/components/empty_state',
|
69
|
+
title: 'No route requests found for the selected filters.',
|
70
70
|
description: 'Try adjusting your time range or filters to see results.' %>
|
71
71
|
<% end %>
|
72
72
|
<% end %>
|
data/config/routes.rb
CHANGED
@@ -3,7 +3,11 @@ RailsPulse::Engine.routes.draw do
|
|
3
3
|
|
4
4
|
resources :routes, only: %i[index show]
|
5
5
|
resources :requests, only: %i[index show]
|
6
|
-
resources :queries, only: %i[index show]
|
6
|
+
resources :queries, only: %i[index show] do
|
7
|
+
member do
|
8
|
+
post :analyze
|
9
|
+
end
|
10
|
+
end
|
7
11
|
resources :operations, only: %i[show]
|
8
12
|
resources :caches, only: %i[show], as: :cache
|
9
13
|
patch "pagination/limit", to: "application#set_pagination_limit"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class AddAnalysisToRailsPulseQueries < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
add_column :rails_pulse_queries, :analyzed_at, :datetime, comment: "When query analysis was last performed"
|
4
|
+
add_column :rails_pulse_queries, :explain_plan, :text, comment: "EXPLAIN output from actual SQL execution"
|
5
|
+
add_column :rails_pulse_queries, :issues, :text, comment: "JSON array of detected performance issues"
|
6
|
+
add_column :rails_pulse_queries, :metadata, :text, comment: "JSON object containing query complexity metrics"
|
7
|
+
add_column :rails_pulse_queries, :query_stats, :text, comment: "JSON object with query characteristics analysis"
|
8
|
+
add_column :rails_pulse_queries, :backtrace_analysis, :text, comment: "JSON object with call chain and N+1 detection"
|
9
|
+
add_column :rails_pulse_queries, :suggestions, :text, comment: "JSON array of optimization recommendations"
|
10
|
+
add_column :rails_pulse_queries, :index_recommendations, :text, comment: "JSON array of database index recommendations"
|
11
|
+
add_column :rails_pulse_queries, :n_plus_one_analysis, :text, comment: "JSON object with enhanced N+1 query detection results"
|
12
|
+
end
|
13
|
+
end
|
data/db/rails_pulse_schema.rb
CHANGED
@@ -25,6 +25,15 @@ RailsPulse::Schema = lambda do |connection|
|
|
25
25
|
|
26
26
|
connection.create_table :rails_pulse_queries do |t|
|
27
27
|
t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
|
28
|
+
t.datetime :analyzed_at, comment: "When query analysis was last performed"
|
29
|
+
t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
|
30
|
+
t.text :issues, comment: "JSON array of detected performance issues"
|
31
|
+
t.text :metadata, comment: "JSON object containing query complexity metrics"
|
32
|
+
t.text :query_stats, comment: "JSON object with query characteristics analysis"
|
33
|
+
t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
|
34
|
+
t.text :index_recommendations, comment: "JSON array of database index recommendations"
|
35
|
+
t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
|
36
|
+
t.text :suggestions, comment: "JSON array of optimization recommendations"
|
28
37
|
t.timestamps
|
29
38
|
end
|
30
39
|
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module Generators
|
3
|
+
class ConvertToMigrationsGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
source_root File.expand_path("templates", __dir__)
|
6
|
+
|
7
|
+
desc "Convert Rails Pulse schema file to migrations for single database setup"
|
8
|
+
|
9
|
+
def self.next_migration_number(path)
|
10
|
+
next_migration_number = current_migration_number(path) + 1
|
11
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_schema_file
|
15
|
+
unless File.exist?("db/rails_pulse_schema.rb")
|
16
|
+
say "No db/rails_pulse_schema.rb file found. Run 'rails generate rails_pulse:install' first.", :red
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
if rails_pulse_tables_exist?
|
21
|
+
say "Rails Pulse tables already exist. No conversion needed.", :yellow
|
22
|
+
say "Use 'rails generate rails_pulse:upgrade' to update existing installation.", :blue
|
23
|
+
exit 0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_conversion_migration
|
28
|
+
say "Converting db/rails_pulse_schema.rb to migration...", :green
|
29
|
+
|
30
|
+
migration_template(
|
31
|
+
"migrations/install_rails_pulse_tables.rb",
|
32
|
+
"db/migrate/install_rails_pulse_tables.rb"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def display_completion_message
|
37
|
+
say <<~MESSAGE
|
38
|
+
|
39
|
+
Conversion complete!
|
40
|
+
|
41
|
+
Next steps:
|
42
|
+
1. Run: rails db:migrate
|
43
|
+
2. Delete: db/rails_pulse_schema.rb (no longer needed)
|
44
|
+
3. Remove db/rails_pulse_migrate/ directory if it exists
|
45
|
+
4. Restart your Rails server
|
46
|
+
|
47
|
+
Future Rails Pulse updates will come as regular migrations.
|
48
|
+
|
49
|
+
MESSAGE
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def rails_pulse_tables_exist?
|
55
|
+
return false unless defined?(ActiveRecord::Base)
|
56
|
+
|
57
|
+
connection = ActiveRecord::Base.connection
|
58
|
+
%w[rails_pulse_routes rails_pulse_queries rails_pulse_requests rails_pulse_operations rails_pulse_summaries]
|
59
|
+
.all? { |table| connection.table_exists?(table) }
|
60
|
+
rescue
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -1,9 +1,18 @@
|
|
1
1
|
module RailsPulse
|
2
2
|
module Generators
|
3
3
|
class InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
4
5
|
source_root File.expand_path("templates", __dir__)
|
5
6
|
|
6
|
-
desc "Install Rails Pulse with
|
7
|
+
desc "Install Rails Pulse with flexible database setup options"
|
8
|
+
|
9
|
+
class_option :database, type: :string, default: "single",
|
10
|
+
desc: "Database setup: 'single' (default) or 'separate'"
|
11
|
+
|
12
|
+
def self.next_migration_number(path)
|
13
|
+
next_migration_number = current_migration_number(path) + 1
|
14
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
15
|
+
end
|
7
16
|
|
8
17
|
def copy_schema
|
9
18
|
copy_file "db/rails_pulse_schema.rb", "db/rails_pulse_schema.rb"
|
@@ -13,41 +22,85 @@ module RailsPulse
|
|
13
22
|
copy_file "rails_pulse.rb", "config/initializers/rails_pulse.rb"
|
14
23
|
end
|
15
24
|
|
16
|
-
def
|
25
|
+
def setup_database_configuration
|
17
26
|
if separate_database?
|
18
|
-
|
27
|
+
create_separate_database_setup
|
28
|
+
else
|
29
|
+
create_single_database_setup
|
19
30
|
end
|
20
31
|
end
|
21
32
|
|
22
33
|
def display_post_install_message
|
34
|
+
if separate_database?
|
35
|
+
display_separate_database_message
|
36
|
+
else
|
37
|
+
display_single_database_message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def separate_database?
|
44
|
+
options[:database] == "separate"
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_separate_database_setup
|
48
|
+
create_file "db/rails_pulse_migrate/.keep"
|
49
|
+
|
50
|
+
say "Setting up separate database configuration...", :green
|
51
|
+
|
52
|
+
# Could add database.yml configuration here if needed
|
53
|
+
# For now, users will configure manually
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_single_database_setup
|
57
|
+
say "Setting up single database configuration...", :green
|
58
|
+
|
59
|
+
# Create a migration that loads the schema
|
60
|
+
migration_template(
|
61
|
+
"migrations/install_rails_pulse_tables.rb",
|
62
|
+
"db/migrate/install_rails_pulse_tables.rb"
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def display_separate_database_message
|
23
67
|
say <<~MESSAGE
|
24
68
|
|
25
|
-
Rails Pulse installation complete!
|
69
|
+
Rails Pulse installation complete! (Separate Database Setup)
|
26
70
|
|
27
71
|
Next steps:
|
28
|
-
1.
|
72
|
+
1. Add Rails Pulse database configuration to config/database.yml:
|
73
|
+
|
74
|
+
#{Rails.env}:
|
75
|
+
rails_pulse:
|
76
|
+
<<: *default
|
77
|
+
database: storage/#{Rails.env}_rails_pulse.sqlite3
|
78
|
+
migrations_paths: db/rails_pulse_migrate
|
79
|
+
|
29
80
|
2. Run: rails db:prepare (creates database and loads schema)
|
30
81
|
3. Restart your Rails server
|
31
82
|
|
32
|
-
|
33
|
-
#{environment}:
|
34
|
-
rails_pulse:
|
35
|
-
<<: *default
|
36
|
-
database: storage/#{environment}_rails_pulse.sqlite3
|
37
|
-
migrations_paths: db/rails_pulse_migrate
|
83
|
+
Future schema changes will come as regular migrations in db/rails_pulse_migrate/
|
38
84
|
|
39
85
|
MESSAGE
|
40
86
|
end
|
41
87
|
|
42
|
-
|
88
|
+
def display_single_database_message
|
89
|
+
say <<~MESSAGE
|
43
90
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
91
|
+
Rails Pulse installation complete! (Single Database Setup)
|
92
|
+
|
93
|
+
Next steps:
|
94
|
+
1. Run: rails db:migrate (creates Rails Pulse tables in your main database)
|
95
|
+
2. Delete: db/rails_pulse_schema.rb (no longer needed)
|
96
|
+
3. Restart your Rails server
|
48
97
|
|
49
|
-
|
50
|
-
|
98
|
+
Future schema changes will come as regular migrations in db/migrate/
|
99
|
+
|
100
|
+
Note: The installation migration was created from db/rails_pulse_schema.rb
|
101
|
+
and includes all current Rails Pulse tables and columns.
|
102
|
+
|
103
|
+
MESSAGE
|
51
104
|
end
|
52
105
|
end
|
53
106
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Generated from Rails Pulse schema - automatically loads current schema definition
|
2
|
+
class InstallRailsPulseTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
3
|
+
def change
|
4
|
+
# Load and execute the Rails Pulse schema directly
|
5
|
+
# This ensures the migration is always in sync with the schema file
|
6
|
+
schema_file = File.join(::Rails.root.to_s, "db/rails_pulse_schema.rb")
|
7
|
+
|
8
|
+
if File.exist?(schema_file)
|
9
|
+
say "Loading Rails Pulse schema from db/rails_pulse_schema.rb"
|
10
|
+
|
11
|
+
# Load the schema file to define RailsPulse::Schema
|
12
|
+
load schema_file
|
13
|
+
|
14
|
+
# Execute the schema in the context of this migration
|
15
|
+
RailsPulse::Schema.call(connection)
|
16
|
+
|
17
|
+
say "Rails Pulse tables created successfully"
|
18
|
+
else
|
19
|
+
raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Upgrade Rails Pulse tables with new features
|
2
|
+
class UpgradeRailsPulseTables < ActiveRecord::Migration[<%= @migration_version %>]
|
3
|
+
def change
|
4
|
+
<% @missing_columns.each do |table_name, columns| %>
|
5
|
+
<% columns.each do |column_name, definition| %>
|
6
|
+
# Add <%= column_name %> column to <%= table_name %>
|
7
|
+
add_column :<%= table_name %>, :<%= column_name %>, :<%= definition[:type] %><% if definition[:comment] %>, comment: "<%= definition[:comment] %>"<% end %>
|
8
|
+
<% end %>
|
9
|
+
<% end %>
|
10
|
+
end
|
11
|
+
|
12
|
+
def down
|
13
|
+
<% @missing_columns.each do |table_name, columns| %>
|
14
|
+
<% columns.each do |column_name, _definition| %>
|
15
|
+
remove_column :<%= table_name %>, :<%= column_name %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module RailsPulse
|
2
|
+
module Generators
|
3
|
+
class UpgradeGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
source_root File.expand_path("templates", __dir__)
|
6
|
+
|
7
|
+
desc "Upgrade Rails Pulse database schema to the latest version"
|
8
|
+
|
9
|
+
class_option :database, type: :string, default: "detect",
|
10
|
+
desc: "Database setup: 'single', 'separate', or 'detect' (default)"
|
11
|
+
|
12
|
+
def self.next_migration_number(path)
|
13
|
+
next_migration_number = current_migration_number(path) + 1
|
14
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
15
|
+
end
|
16
|
+
|
17
|
+
def check_current_installation
|
18
|
+
@database_type = detect_database_setup
|
19
|
+
|
20
|
+
say "Detected database setup: #{@database_type}", :green
|
21
|
+
|
22
|
+
case @database_type
|
23
|
+
when :single
|
24
|
+
upgrade_single_database
|
25
|
+
when :separate
|
26
|
+
upgrade_separate_database
|
27
|
+
when :schema_only
|
28
|
+
offer_conversion_to_migrations
|
29
|
+
when :not_installed
|
30
|
+
say "Rails Pulse not detected. Run 'rails generate rails_pulse:install' first.", :red
|
31
|
+
exit 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def detect_database_setup
|
38
|
+
# Override with command line option if provided
|
39
|
+
return options[:database].to_sym if options[:database] != "detect"
|
40
|
+
|
41
|
+
# Check for existing Rails Pulse tables
|
42
|
+
tables_exist = rails_pulse_tables_exist?
|
43
|
+
|
44
|
+
if !tables_exist && File.exist?("db/rails_pulse_schema.rb")
|
45
|
+
:schema_only
|
46
|
+
elsif !tables_exist
|
47
|
+
:not_installed
|
48
|
+
elsif File.exist?("db/rails_pulse_migrate")
|
49
|
+
:separate
|
50
|
+
else
|
51
|
+
:single
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def rails_pulse_tables_exist?
|
56
|
+
return false unless defined?(ActiveRecord::Base)
|
57
|
+
|
58
|
+
connection = ActiveRecord::Base.connection
|
59
|
+
required_tables = get_rails_pulse_table_names
|
60
|
+
|
61
|
+
required_tables.all? { |table| connection.table_exists?(table) }
|
62
|
+
rescue
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_rails_pulse_table_names
|
67
|
+
# Load the schema file to get the table names dynamically
|
68
|
+
schema_file = File.join(Rails.root, "db/rails_pulse_schema.rb")
|
69
|
+
|
70
|
+
if File.exist?(schema_file)
|
71
|
+
# Read the schema file and extract the required_tables array
|
72
|
+
schema_content = File.read(schema_file)
|
73
|
+
|
74
|
+
# Extract the required_tables line using regex
|
75
|
+
if match = schema_content.match(/required_tables\s*=\s*\[(.*?)\]/m)
|
76
|
+
# Parse the array content, handling symbols and strings
|
77
|
+
table_names = match[1].scan(/:(\w+)/).flatten
|
78
|
+
return table_names.map(&:to_s)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Fallback to default table names if schema file parsing fails
|
83
|
+
%w[rails_pulse_routes rails_pulse_queries rails_pulse_requests rails_pulse_operations rails_pulse_summaries]
|
84
|
+
end
|
85
|
+
|
86
|
+
def upgrade_single_database
|
87
|
+
missing_columns = detect_missing_columns
|
88
|
+
|
89
|
+
if missing_columns.empty?
|
90
|
+
say "Rails Pulse is up to date! No migration needed.", :green
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
# Format missing columns by table for the template
|
95
|
+
missing_by_table = format_missing_columns_by_table(missing_columns)
|
96
|
+
|
97
|
+
say "Creating upgrade migration for missing columns: #{missing_columns.keys.join(', ')}", :blue
|
98
|
+
|
99
|
+
# Set instance variables for template
|
100
|
+
@migration_version = ActiveRecord::Migration.current_version
|
101
|
+
@missing_columns = missing_by_table
|
102
|
+
|
103
|
+
migration_template(
|
104
|
+
"migrations/upgrade_rails_pulse_tables.rb",
|
105
|
+
"db/migrate/upgrade_rails_pulse_tables.rb"
|
106
|
+
)
|
107
|
+
|
108
|
+
say <<~MESSAGE
|
109
|
+
|
110
|
+
Upgrade migration created successfully!
|
111
|
+
|
112
|
+
Next steps:
|
113
|
+
1. Run: rails db:migrate
|
114
|
+
2. Restart your Rails server
|
115
|
+
|
116
|
+
This migration will add: #{missing_columns.keys.join(', ')}
|
117
|
+
|
118
|
+
MESSAGE
|
119
|
+
end
|
120
|
+
|
121
|
+
def upgrade_separate_database
|
122
|
+
# For separate database, we'd need to check the schema file and generate migrations
|
123
|
+
# in db/rails_pulse_migrate/ directory
|
124
|
+
say "Separate database upgrade not implemented yet. Please check db/rails_pulse_schema.rb for updates.", :yellow
|
125
|
+
end
|
126
|
+
|
127
|
+
def offer_conversion_to_migrations
|
128
|
+
say <<~MESSAGE
|
129
|
+
|
130
|
+
Rails Pulse schema detected but no tables found.
|
131
|
+
|
132
|
+
To convert to single database setup:
|
133
|
+
1. Run: rails generate rails_pulse:convert_to_migrations
|
134
|
+
2. Run: rails db:migrate
|
135
|
+
3. Delete: db/rails_pulse_schema.rb
|
136
|
+
|
137
|
+
MESSAGE
|
138
|
+
end
|
139
|
+
|
140
|
+
def detect_missing_columns
|
141
|
+
return {} unless rails_pulse_tables_exist?
|
142
|
+
|
143
|
+
connection = ActiveRecord::Base.connection
|
144
|
+
missing = {}
|
145
|
+
|
146
|
+
# Get expected schema from the schema file
|
147
|
+
expected_schema = get_expected_schema_from_file
|
148
|
+
|
149
|
+
expected_schema.each do |table_name, columns|
|
150
|
+
table_symbol = table_name.to_sym
|
151
|
+
|
152
|
+
if connection.table_exists?(table_symbol)
|
153
|
+
existing_columns = connection.columns(table_symbol).map(&:name)
|
154
|
+
|
155
|
+
columns.each do |column_name, definition|
|
156
|
+
unless existing_columns.include?(column_name)
|
157
|
+
missing[column_name] = definition
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
missing
|
164
|
+
end
|
165
|
+
|
166
|
+
def get_expected_schema_from_file
|
167
|
+
schema_file = File.join(Rails.root, "db/rails_pulse_schema.rb")
|
168
|
+
return {} unless File.exist?(schema_file)
|
169
|
+
|
170
|
+
schema_content = File.read(schema_file)
|
171
|
+
expected_columns = {}
|
172
|
+
|
173
|
+
# Find each create_table block and parse its contents
|
174
|
+
table_blocks = schema_content.scan(/connection\.create_table\s+:(\w+).*?do\s*\|t\|(.*?)(?:connection\.add_index|connection\.create_table|\z)/m)
|
175
|
+
|
176
|
+
table_blocks.each do |table_name, table_block|
|
177
|
+
columns = {}
|
178
|
+
|
179
|
+
# Split the table block into lines and process each line
|
180
|
+
table_block.split("\n").each do |line|
|
181
|
+
# Match column definitions like: t.text :index_recommendations, comment: "..."
|
182
|
+
if match = line.match(/t\.(\w+)\s+:([a-zA-Z_][a-zA-Z0-9_]*)(?:.*?comment:\s*"([^"]*)")?/)
|
183
|
+
column_type, column_name, comment = match.captures
|
184
|
+
|
185
|
+
# Skip timestamps and references as they're handled by Rails
|
186
|
+
next if %w[timestamps references].include?(column_type)
|
187
|
+
|
188
|
+
columns[column_name] = {
|
189
|
+
type: column_type.to_sym,
|
190
|
+
comment: comment
|
191
|
+
}.compact
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
expected_columns[table_name] = columns if columns.any?
|
196
|
+
end
|
197
|
+
|
198
|
+
expected_columns
|
199
|
+
end
|
200
|
+
|
201
|
+
def format_missing_columns_by_table(missing_columns)
|
202
|
+
# The missing_columns are already organized by table from detect_missing_columns
|
203
|
+
# but we need to restructure them for the template
|
204
|
+
missing_by_table = {}
|
205
|
+
|
206
|
+
# Get expected schema to find which table each missing column belongs to
|
207
|
+
expected_schema = get_expected_schema_from_file
|
208
|
+
|
209
|
+
expected_schema.each do |table_name, expected_columns|
|
210
|
+
table_missing = {}
|
211
|
+
|
212
|
+
expected_columns.each do |column_name, definition|
|
213
|
+
if missing_columns.key?(column_name)
|
214
|
+
table_missing[column_name] = definition
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
missing_by_table[table_name] = table_missing if table_missing.any?
|
219
|
+
end
|
220
|
+
|
221
|
+
missing_by_table
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
data/lib/rails_pulse/version.rb
CHANGED