dbviewer 0.6.3 → 0.6.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.
@@ -0,0 +1,137 @@
1
+ module Dbviewer
2
+ module FilterHelper
3
+ # Determine default operator based on column type
4
+ def default_operator_for_column_type(column_type)
5
+ if column_type && column_type =~ /char|text|string|uuid|enum/i
6
+ "contains"
7
+ else
8
+ "eq"
9
+ end
10
+ end
11
+
12
+ # Generate operator options based on column type
13
+ def operator_options_for_column_type(column_type)
14
+ # Common operators for all types
15
+ common_operators = [
16
+ [ "is null", "is_null" ],
17
+ [ "is not null", "is_not_null" ]
18
+ ]
19
+
20
+ type_specific_operators = if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
21
+ # Date/Time operators
22
+ [
23
+ [ "=", "eq" ],
24
+ [ "≠", "neq" ],
25
+ [ "<", "lt" ],
26
+ [ ">", "gt" ],
27
+ [ "≤", "lte" ],
28
+ [ "≥", "gte" ]
29
+ ]
30
+ elsif column_type && column_type =~ /int|float|decimal|double|number|numeric|real|money|bigint|smallint|tinyint|mediumint|bit/i
31
+ # Numeric operators
32
+ [
33
+ [ "=", "eq" ],
34
+ [ "≠", "neq" ],
35
+ [ "<", "lt" ],
36
+ [ ">", "gt" ],
37
+ [ "≤", "lte" ],
38
+ [ "≥", "gte" ]
39
+ ]
40
+ else
41
+ # Text operators
42
+ [
43
+ [ "contains", "contains" ],
44
+ [ "not contains", "not_contains" ],
45
+ [ "=", "eq" ],
46
+ [ "≠", "neq" ],
47
+ [ "starts with", "starts_with" ],
48
+ [ "ends with", "ends_with" ]
49
+ ]
50
+ end
51
+
52
+ # Return type-specific operators first, then common operators
53
+ type_specific_operators + common_operators
54
+ end
55
+
56
+ # Render column filter input based on column type
57
+ def render_column_filter_input(form, column_name, column_type, column_filters)
58
+ # Get selected operator to check if it's a null operator
59
+ operator = column_filters["#{column_name}_operator"]
60
+ is_null_operator = operator == "is_null" || operator == "is_not_null"
61
+
62
+ # Clean up the value for non-null operators if the value contains a null operator
63
+ # This ensures we don't carry over 'is_null' or 'is_not_null' values when switching operators
64
+ value = column_filters[column_name]
65
+ if !is_null_operator && value.present? && (value == "is_null" || value == "is_not_null")
66
+ value = nil
67
+ end
68
+
69
+ # For null operators, display a non-editable field without placeholder
70
+ if is_null_operator
71
+ # Keep a hidden field for the actual value
72
+ hidden_field = form.hidden_field("column_filters[#{column_name}]",
73
+ value: operator,
74
+ class: "null-filter-value",
75
+ data: { column: column_name })
76
+
77
+ # Add a visible but disabled text field with no placeholder or value
78
+ visible_field = form.text_field("column_filters[#{column_name}_display]",
79
+ disabled: true,
80
+ value: "",
81
+ class: "form-control form-control-sm column-filter rounded-0 disabled-filter",
82
+ data: { column: "#{column_name}_display" })
83
+
84
+ hidden_field + visible_field
85
+ elsif column_type && column_type =~ /datetime/
86
+ form.datetime_local_field("column_filters[#{column_name}]",
87
+ value: value,
88
+ class: "form-control form-control-sm column-filter rounded-0",
89
+ data: { column: column_name })
90
+ elsif column_type && column_type =~ /^date$/
91
+ form.date_field("column_filters[#{column_name}]",
92
+ value: value,
93
+ class: "form-control form-control-sm column-filter rounded-0",
94
+ data: { column: column_name })
95
+ elsif column_type && column_type =~ /^time$/
96
+ form.time_field("column_filters[#{column_name}]",
97
+ value: value,
98
+ class: "form-control form-control-sm column-filter rounded-0",
99
+ data: { column: column_name })
100
+ else
101
+ form.text_field("column_filters[#{column_name}]",
102
+ value: value,
103
+ placeholder: "",
104
+ class: "form-control form-control-sm column-filter rounded-0",
105
+ data: { column: column_name })
106
+ end
107
+ end
108
+
109
+ # Render operator select for column filter
110
+ def render_operator_select(form, column_name, column_type, column_filters)
111
+ # Get previously selected operator or default
112
+ default_operator = default_operator_for_column_type(column_type)
113
+ selected_operator = column_filters["#{column_name}_operator"]
114
+ selected_operator = default_operator if selected_operator.nil? || selected_operator == "default"
115
+
116
+ # Get appropriate options
117
+ operator_options = operator_options_for_column_type(column_type)
118
+
119
+ form.select("column_filters[#{column_name}_operator]",
120
+ options_for_select(operator_options, selected_operator),
121
+ { include_blank: false },
122
+ { class: "form-select form-select-sm operator-select" })
123
+ end
124
+
125
+ # Render complete filter input group for a column
126
+ def render_column_filter(form, column_name, columns, column_filters)
127
+ column_type = column_type_from_info(column_name, columns)
128
+
129
+ content_tag(:div, class: "filter-input-group") do
130
+ operator_select = render_operator_select(form, column_name, column_type, column_filters)
131
+ input_field = render_column_filter_input(form, column_name, column_type, column_filters)
132
+
133
+ operator_select + input_field
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,30 @@
1
+ module Dbviewer
2
+ module FormattingHelper
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
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module Dbviewer
2
+ module NavigationHelper
3
+ # Helper method to determine if current controller and action match
4
+ def active_nav_class(controller_name, action_name = nil)
5
+ current_controller = params[:controller].split("/").last
6
+ active = current_controller == controller_name
7
+
8
+ if action_name.present?
9
+ active = active && params[:action] == action_name
10
+ end
11
+
12
+ active ? "active" : ""
13
+ end
14
+
15
+ # Helper for highlighting dashboard link
16
+ def dashboard_nav_class
17
+ active_nav_class("home")
18
+ end
19
+
20
+ # Helper for highlighting tables link
21
+ def tables_nav_class
22
+ active_nav_class("tables")
23
+ end
24
+
25
+ # Helper for highlighting ERD link
26
+ def erd_nav_class
27
+ active_nav_class("entity_relationship_diagrams")
28
+ end
29
+
30
+ # Helper for highlighting SQL Logs link
31
+ def logs_nav_class
32
+ active_nav_class("logs")
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,72 @@
1
+ module Dbviewer
2
+ module PaginationHelper
3
+ # Common parameters for pagination and filtering
4
+ def common_params(options = {})
5
+ params = {
6
+ order_by: @order_by,
7
+ order_direction: @order_direction,
8
+ per_page: @per_page,
9
+ column_filters: @column_filters
10
+ }.merge(options)
11
+
12
+ # Add creation filters if they exist
13
+ params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
14
+ params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
15
+
16
+ params
17
+ end
18
+
19
+ # Render pagination UI
20
+ def render_pagination(table_name, current_page, total_pages, params = {})
21
+ return unless total_pages && total_pages > 1
22
+
23
+ content_tag(:nav, 'aria-label': "Page navigation") do
24
+ content_tag(:ul, class: "pagination justify-content-center") do
25
+ prev_link = content_tag(:li, class: "page-item #{current_page == 1 ? 'disabled' : ''}") do
26
+ link_to "«", table_path(table_name, params.merge(page: [ current_page - 1, 1 ].max)), class: "page-link"
27
+ end
28
+
29
+ # Calculate page range to display
30
+ start_page = [ 1, current_page - 2 ].max
31
+ end_page = [ start_page + 4, total_pages ].min
32
+ start_page = [ 1, end_page - 4 ].max
33
+
34
+ # Generate page links
35
+ page_links = (start_page..end_page).map do |page_num|
36
+ content_tag(:li, class: "page-item #{page_num == current_page ? 'active' : ''}") do
37
+ link_to page_num, table_path(table_name, params.merge(page: page_num)), class: "page-link"
38
+ end
39
+ end.join.html_safe
40
+
41
+ next_link = content_tag(:li, class: "page-item #{current_page == total_pages ? 'disabled' : ''}") do
42
+ link_to "»", table_path(table_name, params.merge(page: [ current_page + 1, total_pages ].min)), class: "page-link"
43
+ end
44
+
45
+ prev_link + page_links + next_link
46
+ end
47
+ end
48
+ end
49
+
50
+ # Generate URL parameters for per-page dropdown
51
+ def per_page_url_params(table_name)
52
+ # Start with the dynamic part for the select element
53
+ url_params = "per_page=' + this.value + '&page=1"
54
+
55
+ # Add all other common parameters except per_page and page which we already set
56
+ params = common_params.except(:per_page, :page)
57
+
58
+ # Convert the params hash to URL parameters
59
+ params.each do |key, value|
60
+ if key == :column_filters && value.is_a?(Hash) && value.reject { |_, v| v.blank? }.any?
61
+ value.reject { |_, v| v.blank? }.each do |filter_key, filter_value|
62
+ url_params += "&column_filters[#{filter_key}]=#{CGI.escape(filter_value.to_s)}"
63
+ end
64
+ elsif value.present?
65
+ url_params += "&#{key}=#{CGI.escape(value.to_s)}"
66
+ end
67
+ end
68
+
69
+ url_params
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,47 @@
1
+ module Dbviewer
2
+ module SortingHelper
3
+ # Returns a sort icon based on the current sort direction
4
+ def sort_icon(column_name, current_order_by, current_direction)
5
+ if column_name == current_order_by
6
+ direction = current_direction == "ASC" ? "up" : "down"
7
+ "<i class='bi bi-sort-#{direction}'></i>".html_safe
8
+ else
9
+ "<i class='bi bi-filter invisible sort-icon'></i>".html_safe
10
+ end
11
+ end
12
+
13
+ # Determine the next sort direction based on the current one
14
+ def next_sort_direction(column_name, current_order_by, current_direction)
15
+ if column_name == current_order_by && current_direction == "ASC"
16
+ "DESC"
17
+ else
18
+ "ASC"
19
+ end
20
+ end
21
+
22
+ # Generate a sortable column header link
23
+ def sortable_column_header(column_name, current_order_by, current_direction, table_name, current_page, per_page, column_filters)
24
+ is_sorted = column_name == current_order_by
25
+ sort_direction = next_sort_direction(column_name, current_order_by, current_direction)
26
+
27
+ aria_sort = if is_sorted
28
+ current_direction.downcase == "asc" ? "ascending" : "descending"
29
+ else
30
+ "none"
31
+ end
32
+
33
+ # Use common_params helper to build parameters
34
+ sort_params = common_params(order_by: column_name, order_direction: sort_direction)
35
+
36
+ link_to table_path(table_name, sort_params),
37
+ class: "d-flex align-items-center text-decoration-none text-reset column-sort-link",
38
+ title: "Sort by #{column_name} (#{sort_direction.downcase})",
39
+ "aria-sort": aria_sort,
40
+ role: "button",
41
+ tabindex: "0" do
42
+ content_tag(:span, column_name, class: "column-name") +
43
+ content_tag(:span, sort_icon(column_name, current_order_by, current_direction), class: "sort-icon-container")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,145 @@
1
+ module Dbviewer
2
+ module TableRenderingHelper
3
+ # Render a complete table header row with sortable columns
4
+ def render_sortable_header_row(records, order_by, order_direction, table_name, current_page, per_page, column_filters)
5
+ return content_tag(:tr) { content_tag(:th, "No columns available") } unless records&.columns
6
+
7
+ content_tag(:tr) do
8
+ # Start with action column header (sticky first column)
9
+ headers = [
10
+ content_tag(:th, class: "px-3 py-2 text-center action-column action-column-header", width: "60px", rowspan: 2) do
11
+ content_tag(:span, "Actions")
12
+ end
13
+ ]
14
+
15
+ # Add all data columns
16
+ headers += records.columns.map do |column_name|
17
+ is_sorted = order_by == column_name
18
+ content_tag(:th, class: "px-3 py-2 sortable-column #{is_sorted ? 'sorted' : ''}") do
19
+ sortable_column_header(column_name, order_by, order_direction, table_name, current_page, per_page, column_filters)
20
+ end
21
+ end
22
+
23
+ headers.join.html_safe
24
+ end
25
+ end
26
+
27
+ # Render the column filters row
28
+ def render_column_filters_row(form, records, columns, column_filters)
29
+ return content_tag(:tr) { content_tag(:th, "") } unless records&.columns
30
+
31
+ content_tag(:tr, class: "column-filters") do
32
+ filters = records.columns.map do |column_name|
33
+ content_tag(:th, class: "p-0") do
34
+ render_column_filter(form, column_name, columns, column_filters)
35
+ end
36
+ end
37
+
38
+ filters.join.html_safe
39
+ end
40
+ end
41
+
42
+ # Render a cell that may include a foreign key link
43
+ def render_table_cell(cell, column_name, metadata)
44
+ cell_value = format_cell_value(cell)
45
+ foreign_key = metadata && metadata[:foreign_keys] ?
46
+ metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
47
+ nil
48
+
49
+ if foreign_key && !cell.nil?
50
+ fk_params = { column_filters: { foreign_key[:primary_key] => cell } }
51
+ fk_params = fk_params.merge(common_params.except(:column_filters))
52
+
53
+ content_tag(:td, title: "#{cell_value} (Click to view referenced record)") do
54
+ link_to(cell_value, table_path(foreign_key[:to_table], fk_params),
55
+ class: "text-decoration-none foreign-key-link") +
56
+ content_tag(:i, "", class: "bi bi-link-45deg text-muted small")
57
+ end
58
+ else
59
+ content_tag(:td, cell_value, title: cell_value)
60
+ end
61
+ end
62
+
63
+ # Render a table row with cells
64
+ def render_table_row(row, records, metadata)
65
+ content_tag(:tr) do
66
+ # Start with action column (sticky first column)
67
+ cells = [ render_action_cell(row, records.columns, metadata) ]
68
+
69
+ # Add all data cells
70
+ cells += row.each_with_index.map do |cell, cell_index|
71
+ column_name = records.columns[cell_index]
72
+ render_table_cell(cell, column_name, metadata)
73
+ end
74
+
75
+ cells.join.html_safe
76
+ end
77
+ end
78
+
79
+ # Render the entire table body with rows
80
+ def render_table_body(records, metadata)
81
+ if records.nil? || records.rows.nil? || records.empty?
82
+ content_tag(:tbody) do
83
+ content_tag(:tr) do
84
+ # Adding +1 to account for the action column
85
+ total_columns = records&.columns&.size.to_i + 1
86
+ content_tag(:td, "No records found or table is empty.", colspan: total_columns, class: "text-center")
87
+ end
88
+ end
89
+ else
90
+ content_tag(:tbody) do
91
+ records.rows.map do |row|
92
+ render_table_row(row, records, metadata)
93
+ end.join.html_safe
94
+ end
95
+ end
96
+ end
97
+
98
+ # Render action buttons for a record
99
+ def render_action_cell(row_data, columns, metadata = nil)
100
+ data_attributes = {}
101
+
102
+ # Create a hash of column_name: value pairs for data attributes
103
+ columns.each_with_index do |column_name, index|
104
+ data_attributes[column_name] = row_data[index].to_s
105
+ end
106
+
107
+ content_tag(:td, class: "text-center action-column") do
108
+ content_tag(:div, class: "d-flex gap-1 justify-content-center") do
109
+ # View Record button (existing)
110
+ view_button = button_tag(
111
+ type: "button",
112
+ class: "btn btn-sm btn-primary view-record-btn",
113
+ title: "View Record Details",
114
+ data: {
115
+ bs_toggle: "modal",
116
+ bs_target: "#recordDetailModal",
117
+ record_data: data_attributes.to_json,
118
+ foreign_keys: metadata && metadata[:foreign_keys] ? metadata[:foreign_keys].to_json : "[]",
119
+ reverse_foreign_keys: metadata && metadata[:reverse_foreign_keys] ? metadata[:reverse_foreign_keys].to_json : "[]"
120
+ }
121
+ ) do
122
+ content_tag(:i, "", class: "bi bi-eye")
123
+ end
124
+
125
+ # Copy FactoryBot button (new)
126
+ copy_factory_button = button_tag(
127
+ type: "button",
128
+ class: "btn btn-sm btn-outline-secondary copy-factory-btn",
129
+ title: "Copy to JSON",
130
+ data: {
131
+ record_data: data_attributes.to_json,
132
+ table_name: @table_name
133
+ },
134
+ onclick: "copyToJson(this)"
135
+ ) do
136
+ content_tag(:i, "", class: "bi bi-clipboard")
137
+ end
138
+
139
+ # Concatenate both buttons
140
+ view_button + copy_factory_button
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,41 @@
1
+ module Dbviewer
2
+ module UiHelper
3
+ # Dark mode helper methods
4
+
5
+ # Returns the theme toggle icon based on the current theme
6
+ def theme_toggle_icon
7
+ '<i class="bi bi-moon"></i><i class="bi bi-sun"></i>'.html_safe
8
+ end
9
+
10
+ # Returns the aria label for the theme toggle button
11
+ def theme_toggle_label
12
+ "Toggle dark mode"
13
+ end
14
+
15
+ # Returns the appropriate background class for stat cards that adapts to dark mode
16
+ def stat_card_bg_class
17
+ "stat-card-bg"
18
+ end
19
+
20
+ # Helper method for code blocks background that adapts to dark mode
21
+ def code_block_bg_class
22
+ "sql-code-block"
23
+ end
24
+
25
+ # Render time grouping links
26
+ def time_grouping_links(table_name, current_grouping)
27
+ params = common_params
28
+
29
+ content_tag(:div, class: "btn-group btn-group-sm", role: "group", 'aria-label': "Time grouping") do
30
+ [
31
+ link_to("Hourly", table_path(table_name, params.merge(time_group: "hourly")),
32
+ class: "btn btn-outline-primary #{current_grouping == 'hourly' ? 'active' : ''}"),
33
+ link_to("Daily", table_path(table_name, params.merge(time_group: "daily")),
34
+ class: "btn btn-outline-primary #{current_grouping == 'daily' ? 'active' : ''}"),
35
+ link_to("Weekly", table_path(table_name, params.merge(time_group: "weekly")),
36
+ class: "btn btn-outline-primary #{current_grouping == 'weekly' ? 'active' : ''}")
37
+ ].join.html_safe
38
+ end
39
+ end
40
+ end
41
+ end
@@ -17,7 +17,6 @@ module Dbviewer
17
17
  @table_query_operations = ::Dbviewer::Datatable::QueryOperations.new(
18
18
  @connection,
19
19
  @dynamic_model_factory,
20
- @query_executor,
21
20
  @table_metadata_manager
22
21
  )
23
22
  reset_cache_if_needed
@@ -108,7 +107,7 @@ module Dbviewer
108
107
  # @return [ActiveRecord::Result] Result set with columns and rows
109
108
  # @raise [StandardError] If the query is invalid or unsafe
110
109
  def execute_query(sql)
111
- @table_query_operations.execute_query(sql)
110
+ @query_executor.execute_query(sql)
112
111
  end
113
112
 
114
113
  # Execute a SQLite PRAGMA command without adding a LIMIT clause
@@ -116,7 +115,7 @@ module Dbviewer
116
115
  # @return [ActiveRecord::Result] Result set with the PRAGMA value
117
116
  # @raise [StandardError] If the query is invalid or cannot be executed
118
117
  def execute_sqlite_pragma(pragma)
119
- @table_query_operations.execute_sqlite_pragma(pragma)
118
+ @query_executor.execute_sqlite_pragma(pragma)
120
119
  end
121
120
 
122
121
  # Get table indexes
@@ -8,13 +8,11 @@ module Dbviewer
8
8
  # Initialize with dependencies
9
9
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] The database connection
10
10
  # @param dynamic_model_factory [Dbviewer::Database::DynamicModelFactory] Factory for creating dynamic AR models
11
- # @param query_executor [Dbviewer::Query::Executor] Executor for raw SQL queries
12
11
  # @param table_metadata_manager [Dbviewer::Database::MetadataManager] Manager for table metadata
13
- def initialize(connection, dynamic_model_factory, query_executor, table_metadata_manager)
12
+ def initialize(connection, dynamic_model_factory, table_metadata_manager)
14
13
  @connection = connection
15
14
  @adapter_name = connection.adapter_name.downcase
16
15
  @dynamic_model_factory = dynamic_model_factory
17
- @query_executor = query_executor
18
16
  @table_metadata_manager = table_metadata_manager
19
17
  @query_analyzer = ::Dbviewer::Query::Analyzer.new(connection)
20
18
  end
@@ -49,7 +47,7 @@ module Dbviewer
49
47
  column_names = table_columns(table_name).map { |c| c[:name] }
50
48
 
51
49
  # Format results
52
- @query_executor.to_result_set(records, column_names)
50
+ to_result_set(records, column_names)
53
51
  rescue => e
54
52
  Rails.logger.error("[DBViewer] Error executing table query: #{e.message}")
55
53
  raise e
@@ -92,22 +90,6 @@ module Dbviewer
92
90
 
93
91
  ## -- Delegator
94
92
 
95
- # Execute a raw SQL query after validating for safety
96
- # @param sql [String] SQL query to execute
97
- # @return [ActiveRecord::Result] Result set with columns and rows
98
- # @raise [StandardError] If the query is invalid or unsafe
99
- def execute_query(sql)
100
- @query_executor.execute_query(sql)
101
- end
102
-
103
- # Execute a SQLite PRAGMA command without adding a LIMIT clause
104
- # @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
105
- # @return [ActiveRecord::Result] Result set with the PRAGMA value
106
- # @raise [StandardError] If the query is invalid or cannot be executed
107
- def execute_sqlite_pragma(pragma)
108
- @query_executor.execute_sqlite_pragma(pragma)
109
- end
110
-
111
93
  # Analyze query patterns and return performance recommendations
112
94
  # @param table_name [String] Name of the table
113
95
  # @param query_params [Dbviewer::Datatable::QueryParams] Query parameters
@@ -200,6 +182,39 @@ module Dbviewer
200
182
  def primary_key(table_name)
201
183
  @table_metadata_manager.primary_key(table_name)
202
184
  end
185
+
186
+ # Convert ActiveRecord::Relation to a standard result format
187
+ # @param records [ActiveRecord::Relation] Records to convert
188
+ # @param column_names [Array<String>] Column names
189
+ # @return [ActiveRecord::Result] Result set with columns and rows
190
+ def to_result_set(records, column_names)
191
+ rows = records.map do |record|
192
+ column_names.map do |col|
193
+ # Handle serialized attributes
194
+ value = record[col]
195
+ serialize_if_needed(value)
196
+ end
197
+ end
198
+
199
+ ActiveRecord::Result.new(column_names, rows)
200
+ rescue => e
201
+ Rails.logger.error("[DBViewer] Error converting to result set: #{e.message}")
202
+ ActiveRecord::Result.new([], [])
203
+ end
204
+
205
+ # Serialize complex objects for display
206
+ # @param value [Object] Value to serialize
207
+ # @return [String, Object] Serialized value or original value
208
+ def serialize_if_needed(value)
209
+ case value
210
+ when Hash, Array
211
+ value.to_json rescue value.to_s
212
+ when Time, Date, DateTime
213
+ value.to_s
214
+ else
215
+ value
216
+ end
217
+ end
203
218
  end
204
219
  end
205
220
  end
@@ -53,41 +53,6 @@ module Dbviewer
53
53
  Rails.logger.error("[DBViewer] SQLite pragma error: #{e.message} for pragma: #{pragma}")
54
54
  raise e
55
55
  end
56
-
57
- # Convert ActiveRecord::Relation to a standard result format
58
- # @param records [ActiveRecord::Relation] Records to convert
59
- # @param column_names [Array<String>] Column names
60
- # @return [ActiveRecord::Result] Result set with columns and rows
61
- def to_result_set(records, column_names)
62
- rows = records.map do |record|
63
- column_names.map do |col|
64
- # Handle serialized attributes
65
- value = record[col]
66
- serialize_if_needed(value)
67
- end
68
- end
69
-
70
- ActiveRecord::Result.new(column_names, rows)
71
- rescue => e
72
- Rails.logger.error("[DBViewer] Error converting to result set: #{e.message}")
73
- ActiveRecord::Result.new([], [])
74
- end
75
-
76
- private
77
-
78
- # Serialize complex objects for display
79
- # @param value [Object] Value to serialize
80
- # @return [String, Object] Serialized value or original value
81
- def serialize_if_needed(value)
82
- case value
83
- when Hash, Array
84
- value.to_json rescue value.to_s
85
- when Time, Date, DateTime
86
- value.to_s
87
- else
88
- value
89
- end
90
- end
91
56
  end
92
57
  end
93
58
  end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.6.3"
2
+ VERSION = "0.6.4"
3
3
  end