dbviewer 0.3.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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +250 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/dbviewer/application.css +21 -0
  6. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  7. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  8. data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
  9. data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
  10. data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
  11. data/app/controllers/dbviewer/application_controller.rb +21 -0
  12. data/app/controllers/dbviewer/databases_controller.rb +0 -0
  13. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
  14. data/app/controllers/dbviewer/home_controller.rb +10 -0
  15. data/app/controllers/dbviewer/logs_controller.rb +39 -0
  16. data/app/controllers/dbviewer/tables_controller.rb +73 -0
  17. data/app/helpers/dbviewer/application_helper.rb +118 -0
  18. data/app/jobs/dbviewer/application_job.rb +4 -0
  19. data/app/mailers/dbviewer/application_mailer.rb +6 -0
  20. data/app/models/dbviewer/application_record.rb +5 -0
  21. data/app/services/dbviewer/file_storage.rb +0 -0
  22. data/app/services/dbviewer/in_memory_storage.rb +0 -0
  23. data/app/services/dbviewer/query_analyzer.rb +0 -0
  24. data/app/services/dbviewer/query_collection.rb +0 -0
  25. data/app/services/dbviewer/query_logger.rb +0 -0
  26. data/app/services/dbviewer/query_parser.rb +82 -0
  27. data/app/services/dbviewer/query_storage.rb +0 -0
  28. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
  29. data/app/views/dbviewer/home/index.html.erb +237 -0
  30. data/app/views/dbviewer/logs/index.html.erb +614 -0
  31. data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
  32. data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
  33. data/app/views/dbviewer/tables/index.html.erb +128 -0
  34. data/app/views/dbviewer/tables/query.html.erb +600 -0
  35. data/app/views/dbviewer/tables/show.html.erb +271 -0
  36. data/app/views/layouts/dbviewer/application.html.erb +728 -0
  37. data/config/routes.rb +22 -0
  38. data/lib/dbviewer/configuration.rb +79 -0
  39. data/lib/dbviewer/database_manager.rb +450 -0
  40. data/lib/dbviewer/engine.rb +20 -0
  41. data/lib/dbviewer/initializer.rb +23 -0
  42. data/lib/dbviewer/logger.rb +102 -0
  43. data/lib/dbviewer/query_analyzer.rb +109 -0
  44. data/lib/dbviewer/query_collection.rb +41 -0
  45. data/lib/dbviewer/query_parser.rb +82 -0
  46. data/lib/dbviewer/sql_validator.rb +194 -0
  47. data/lib/dbviewer/storage/base.rb +31 -0
  48. data/lib/dbviewer/storage/file_storage.rb +96 -0
  49. data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
  50. data/lib/dbviewer/version.rb +3 -0
  51. data/lib/dbviewer.rb +65 -0
  52. data/lib/tasks/dbviewer_tasks.rake +4 -0
  53. metadata +126 -0
@@ -0,0 +1,42 @@
1
+ module Dbviewer
2
+ module ErrorHandling
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Ensure common database errors are handled gracefully
7
+ rescue_from ActiveRecord::ActiveRecordError, with: :handle_database_error
8
+ end
9
+
10
+ # Handle database connection errors
11
+ def handle_database_error(exception)
12
+ message = case exception
13
+ when ActiveRecord::ConnectionNotEstablished
14
+ "Database connection could not be established."
15
+ when ActiveRecord::StatementInvalid
16
+ "Invalid SQL statement: #{exception.message}"
17
+ else
18
+ "Database error: #{exception.message}"
19
+ end
20
+
21
+ flash.now[:error] = message
22
+ Rails.logger.error("Database error: #{exception.message}\n#{exception.backtrace.join("\n")}")
23
+
24
+ # Determine where to redirect based on the current action
25
+ if action_name == "show" || action_name == "query"
26
+ @error = message
27
+ @records = nil
28
+ render action_name
29
+ else
30
+ @tables = []
31
+ render :index
32
+ end
33
+ end
34
+
35
+ def log_error(error, prefix = "Error")
36
+ error_msg = "#{prefix}: #{error.message}"
37
+ flash.now[:error] = error_msg
38
+ Rails.logger.error("#{prefix}: #{error.message}\n#{error.backtrace.join("\n")}")
39
+ error_msg
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ module Dbviewer
2
+ module PaginationConcern
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Sort direction validation
7
+ const_set(:VALID_SORT_DIRECTIONS, %w[ASC DESC].freeze) unless const_defined?(:VALID_SORT_DIRECTIONS)
8
+ end
9
+
10
+ module ClassMethods
11
+ # Use configuration values from Dbviewer module
12
+ def per_page_options
13
+ Dbviewer.configuration.per_page_options
14
+ end
15
+
16
+ def default_per_page
17
+ Dbviewer.configuration.default_per_page
18
+ end
19
+ end
20
+
21
+ # Set pagination parameters from request or defaults
22
+ def set_pagination_params
23
+ @current_page = [ 1, params[:page].to_i ].max
24
+ @per_page = params[:per_page] ? params[:per_page].to_i : self.class.default_per_page
25
+ @per_page = self.class.default_per_page unless self.class.per_page_options.include?(@per_page)
26
+ end
27
+
28
+ # Set sorting parameters from request or defaults
29
+ def set_sorting_params
30
+ @order_by = params[:order_by].presence ||
31
+ database_manager.primary_key(@table_name).presence ||
32
+ (@columns.first ? @columns.first[:name] : nil)
33
+
34
+ @order_direction = params[:order_direction].upcase if params[:order_direction].present?
35
+ @order_direction = "ASC" unless self.class::VALID_SORT_DIRECTIONS.include?(@order_direction)
36
+ end
37
+
38
+ # Calculate the total number of pages
39
+ def calculate_total_pages(total_count, per_page)
40
+ (total_count.to_f / per_page).ceil
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ module Dbviewer
2
+ class ApplicationController < ActionController::Base
3
+ include Dbviewer::DatabaseOperations
4
+ include Dbviewer::ErrorHandling
5
+
6
+ before_action :ensure_development_environment
7
+ before_action :set_tables
8
+
9
+ private
10
+
11
+ def ensure_development_environment
12
+ unless Rails.env.development? || Rails.env.test? || params[:override_env_check] == ENV["DBVIEWER_PRODUCTION_ACCESS_KEY"]
13
+ render plain: "DBViewer is only available in development and test environments for security reasons.", status: :forbidden
14
+ end
15
+ end
16
+
17
+ def set_tables
18
+ @tables = fetch_tables_with_stats
19
+ end
20
+ end
21
+ end
File without changes
@@ -0,0 +1,24 @@
1
+ module Dbviewer
2
+ class EntityRelationshipDiagramsController < ApplicationController
3
+ def index
4
+ @tables = fetch_tables_with_stats
5
+
6
+ if @tables.present?
7
+ @table_relationships = fetch_table_relationships
8
+ else
9
+ @table_relationships = []
10
+ flash.now[:warning] = "No tables found in database to generate ERD."
11
+ end
12
+
13
+ respond_to do |format|
14
+ format.html # Default to HTML view
15
+ format.json do
16
+ render json: {
17
+ tables: @tables,
18
+ relationships: @table_relationships
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ module Dbviewer
2
+ class HomeController < ApplicationController
3
+ skip_before_action :set_tables
4
+
5
+ def index
6
+ @tables = fetch_tables_with_stats(include_record_counts: true)
7
+ @analytics = fetch_database_analytics
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,39 @@
1
+ module Dbviewer
2
+ class LogsController < ApplicationController
3
+ before_action :set_filters, only: [ :index ]
4
+
5
+ def index
6
+ @queries = Dbviewer::Logger.instance.recent_queries(
7
+ limit: @limit,
8
+ table_filter: @table_filter,
9
+ request_id: @request_id,
10
+ min_duration: @min_duration
11
+ )
12
+
13
+ if @request_id.present? || @table_filter.present? || @min_duration.present?
14
+ @stats = Dbviewer::Logger.instance.stats_for_queries(@queries)
15
+ @filtered_stats = true
16
+ else
17
+ @stats = Dbviewer::Logger.instance.stats
18
+ @filtered_stats = false
19
+ end
20
+ end
21
+
22
+ def destroy_all
23
+ Dbviewer::Logger.instance.clear
24
+ flash[:success] = "Query logs cleared successfully"
25
+
26
+ redirect_to logs_path
27
+ end
28
+
29
+ private
30
+
31
+ def set_filters
32
+ @table_filter = params[:table_filter]
33
+ @request_id = params[:request_id]
34
+ @min_duration = params[:min_duration]
35
+ @limit = (params[:limit] || 100).to_i
36
+ @limit = 1000 if @limit > 1000
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ module Dbviewer
2
+ class TablesController < ApplicationController
3
+ include Dbviewer::PaginationConcern
4
+
5
+ def index
6
+ @tables = fetch_tables_with_stats
7
+ end
8
+
9
+ def show
10
+ @table_name = params[:id]
11
+ @columns = fetch_table_columns(@table_name)
12
+ @metadata = fetch_table_metadata(@table_name)
13
+ @tables = fetch_tables_with_stats # Fetch tables for sidebar
14
+
15
+ set_pagination_params
16
+ set_sorting_params
17
+
18
+ @total_count = fetch_table_record_count(@table_name)
19
+ @total_pages = calculate_total_pages(@total_count, @per_page)
20
+ @records = fetch_table_records(@table_name)
21
+
22
+ # Fetch timestamp visualization data if the table has a created_at column
23
+ if has_timestamp_column?(@table_name)
24
+ @time_grouping = params[:time_group] || "daily"
25
+ @timestamp_data = fetch_timestamp_data(@table_name, @time_grouping)
26
+ end
27
+
28
+ respond_to do |format|
29
+ format.html # Default HTML response
30
+ format.json do
31
+ render json: {
32
+ table_name: @table_name,
33
+ columns: @columns,
34
+ metadata: @metadata,
35
+ record_count: @total_count
36
+ }
37
+ end
38
+ end
39
+ end
40
+
41
+ def query
42
+ @table_name = params[:id]
43
+ @read_only_mode = true # Flag to indicate we're in read-only mode
44
+ @columns = fetch_table_columns(@table_name)
45
+ @tables = fetch_tables_with_stats # Fetch tables for sidebar
46
+
47
+ # Set active table for sidebar highlighting
48
+ @active_table = @table_name
49
+
50
+ prepare_query
51
+ execute_query
52
+
53
+ render :query
54
+ end
55
+
56
+ def export_csv
57
+ table_name = params[:id]
58
+ limit = (params[:limit] || 10000).to_i
59
+ include_headers = params[:include_headers] != "0"
60
+
61
+ csv_data = export_table_to_csv(table_name, limit, include_headers)
62
+
63
+ # Set filename with timestamp for uniqueness
64
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
65
+ filename = "#{table_name}_export_#{timestamp}.csv"
66
+
67
+ # Send data as file
68
+ send_data csv_data,
69
+ type: "text/csv; charset=utf-8; header=present",
70
+ disposition: "attachment; filename=#{filename}"
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,118 @@
1
+ module Dbviewer
2
+ module ApplicationHelper
3
+ def format_cell_value(value)
4
+ return "NULL" if value.nil?
5
+ return value.to_s.truncate(100) unless value.is_a?(String)
6
+
7
+ case value
8
+ when /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
9
+ # ISO 8601 datetime
10
+ begin
11
+ Time.parse(value).strftime("%Y-%m-%d %H:%M:%S")
12
+ rescue
13
+ value.to_s.truncate(100)
14
+ end
15
+ when /\A\d{4}-\d{2}-\d{2}\z/
16
+ # Date
17
+ value
18
+ when /\A{.+}\z/, /\A\[.+\]\z/
19
+ # JSON
20
+ begin
21
+ JSON.pretty_generate(JSON.parse(value)).truncate(100)
22
+ rescue
23
+ value.to_s.truncate(100)
24
+ end
25
+ else
26
+ value.to_s.truncate(100)
27
+ end
28
+ end
29
+
30
+ # Dark mode helper methods
31
+
32
+ # Returns the theme toggle icon based on the current theme
33
+ def theme_toggle_icon
34
+ '<i class="bi bi-moon"></i><i class="bi bi-sun"></i>'.html_safe
35
+ end
36
+
37
+ # Returns the aria label for the theme toggle button
38
+ def theme_toggle_label
39
+ "Toggle dark mode"
40
+ end
41
+
42
+ # Returns the appropriate background class for stat cards that adapts to dark mode
43
+ def stat_card_bg_class
44
+ "stat-card-bg"
45
+ end
46
+
47
+ # Helper method for code blocks background that adapts to dark mode
48
+ def code_block_bg_class
49
+ "sql-code-block"
50
+ end
51
+
52
+ # Determine if the current table should be active in the sidebar
53
+ def current_table?(table_name)
54
+ @table_name.present? && @table_name == table_name
55
+ end
56
+
57
+ # Format table name for display - truncate if too long
58
+ def format_table_name(table_name)
59
+ if table_name.length > 20
60
+ "#{table_name.first(17)}..."
61
+ else
62
+ table_name
63
+ end
64
+ end
65
+
66
+ # Get appropriate icon for column data type
67
+ def column_type_icon(column_type)
68
+ case column_type.to_s.downcase
69
+ when /int/, /serial/, /number/, /decimal/, /float/, /double/
70
+ "bi-123"
71
+ when /char/, /text/, /string/, /uuid/
72
+ "bi-fonts"
73
+ when /date/, /time/
74
+ "bi-calendar"
75
+ when /bool/
76
+ "bi-toggle-on"
77
+ when /json/, /jsonb/
78
+ "bi-braces"
79
+ when /array/
80
+ "bi-list-ol"
81
+ else
82
+ "bi-file-earmark"
83
+ end
84
+ end
85
+
86
+ # Helper method to determine if current controller and action match
87
+ def active_nav_class(controller_name, action_name = nil)
88
+ current_controller = params[:controller].split("/").last
89
+ active = current_controller == controller_name
90
+
91
+ if action_name.present?
92
+ active = active && params[:action] == action_name
93
+ end
94
+
95
+ active ? "active" : ""
96
+ end
97
+
98
+ # Helper for highlighting dashboard link
99
+ def dashboard_nav_class
100
+ active_nav_class("home")
101
+ end
102
+
103
+ # Helper for highlighting tables link
104
+ def tables_nav_class
105
+ active_nav_class("tables")
106
+ end
107
+
108
+ # Helper for highlighting ERD link
109
+ def erd_nav_class
110
+ active_nav_class("entity_relationship_diagrams")
111
+ end
112
+
113
+ # Helper for highlighting SQL Logs link
114
+ def logs_nav_class
115
+ active_nav_class("logs")
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,4 @@
1
+ module Dbviewer
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Dbviewer
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Dbviewer
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,82 @@
1
+ module Dbviewer
2
+ # QueryParser handles parsing SQL queries and extracting useful information
3
+ class QueryParser
4
+ # Extract table names from an SQL query string
5
+ def self.extract_tables(sql)
6
+ return [] if sql.nil?
7
+
8
+ # Convert to lowercase for case-insensitive matching
9
+ sql = sql.downcase
10
+
11
+ # Extract table names after FROM or JOIN
12
+ sql.scan(/(?:from|join)\s+[`"']?(\w+)[`"']?/).flatten.uniq
13
+ end
14
+
15
+ # Normalize a SQL query to find similar patterns
16
+ # Replaces specific values with placeholders
17
+ def self.normalize(sql)
18
+ return "" if sql.nil?
19
+
20
+ sql.gsub(/\b\d+\b/, "N")
21
+ .gsub(/'[^']*'/, "'X'")
22
+ .gsub(/"[^"]*"/, '"X"')
23
+ end
24
+
25
+ # Check if the query is from the DBViewer library
26
+ def self.from_dbviewer?(event)
27
+ # Check if the SQL itself references DBViewer tables
28
+ if event.payload[:sql].match(/\b(from|join|update|into)\s+["`']?dbviewer_/i)
29
+ return true
30
+ end
31
+
32
+ # Check the caller information if available
33
+ caller = event.payload[:caller]
34
+ if caller.is_a?(String) && caller.include?("/dbviewer/")
35
+ return true
36
+ end
37
+
38
+ # Check if query name indicates it's from DBViewer
39
+ if event.payload[:name].is_a?(String) &&
40
+ (event.payload[:name].include?("Dbviewer") || event.payload[:name].include?("DBViewer") || event.payload[:name] == "SQL")
41
+ return true
42
+ end
43
+
44
+ # Check for common DBViewer operations
45
+ sql = event.payload[:sql].downcase
46
+ if sql.include?("table_structure") ||
47
+ sql.include?("schema_migrations") ||
48
+ sql.include?("database_analytics")
49
+ return true
50
+ end
51
+
52
+ false
53
+ end
54
+
55
+ # Format bind parameters for storage
56
+ def self.format_binds(binds)
57
+ return [] unless binds.respond_to?(:map)
58
+
59
+ binds.map do |bind|
60
+ if bind.respond_to?(:value)
61
+ bind.value
62
+ elsif bind.is_a?(Array) && bind.size == 2
63
+ bind.last
64
+ else
65
+ bind.to_s
66
+ end
67
+ end
68
+ rescue
69
+ []
70
+ end
71
+
72
+ # Determine if a query should be skipped based on content
73
+ def self.should_skip_query?(event)
74
+ event.payload[:name] == "SCHEMA" ||
75
+ event.payload[:sql].include?("SHOW TABLES") ||
76
+ event.payload[:sql].include?("sqlite_master") ||
77
+ event.payload[:sql].include?("information_schema") ||
78
+ event.payload[:sql].include?("pg_catalog") ||
79
+ from_dbviewer?(event)
80
+ end
81
+ end
82
+ end
File without changes