dbviewer 0.3.15 → 0.3.16

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,621 @@
1
+ require "dbviewer/error_handler"
2
+ require "dbviewer/table_query_params"
3
+ require "dbviewer/query_analyzer"
4
+
5
+ module Dbviewer
6
+ # TableQueryOperations handles CRUD operations and data querying for database tables
7
+ # It provides methods to fetch, filter and manipulate data in tables
8
+ class TableQueryOperations
9
+ attr_reader :connection, :adapter_name
10
+
11
+ # Initialize with dependencies
12
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] The database connection
13
+ # @param dynamic_model_factory [DynamicModelFactory] Factory for creating dynamic AR models
14
+ # @param query_executor [QueryExecutor] Executor for raw SQL queries
15
+ # @param table_metadata_manager [TableMetadataManager] Manager for table metadata
16
+ def initialize(connection, dynamic_model_factory, query_executor, table_metadata_manager)
17
+ @connection = connection
18
+ @adapter_name = connection.adapter_name.downcase
19
+ @dynamic_model_factory = dynamic_model_factory
20
+ @query_executor = query_executor
21
+ @table_metadata_manager = table_metadata_manager
22
+ @query_analyzer = Dbviewer::QueryAnalyzer.new(connection)
23
+ end
24
+
25
+ # Get the number of columns in a table
26
+ # @param table_name [String] Name of the table
27
+ # @return [Integer] Number of columns
28
+ def column_count(table_name)
29
+ table_columns(table_name).size
30
+ end
31
+
32
+ # Get records from a table with pagination and sorting
33
+ # @param table_name [String] Name of the table
34
+ # @param params [TableQueryParams] Query parameters object
35
+ # @return [ActiveRecord::Result] Result set with columns and rows
36
+ def table_records(table_name, params)
37
+ model = get_model_for(table_name)
38
+ query = model.all
39
+
40
+ # Apply column filters if provided
41
+ query = apply_column_filters(query, table_name, params.column_filters)
42
+
43
+ # Apply sorting if provided
44
+ if params.order_by.present? && column_exists?(table_name, params.order_by)
45
+ query = query.order("#{connection.quote_column_name(params.order_by)} #{params.direction}")
46
+ end
47
+
48
+ # Apply pagination
49
+ records = query.limit(params.per_page).offset((params.page - 1) * params.per_page)
50
+
51
+ # Get column names for consistent ordering
52
+ column_names = table_columns(table_name).map { |c| c[:name] }
53
+
54
+ # Format results
55
+ @query_executor.to_result_set(records, column_names)
56
+ end
57
+
58
+ # Get the total count of records in a table
59
+ # @param table_name [String] Name of the table
60
+ # @return [Integer] Number of records
61
+ def table_count(table_name)
62
+ model = get_model_for(table_name)
63
+ model.count
64
+ end
65
+
66
+ # Alias for table_count
67
+ # @param table_name [String] Name of the table
68
+ # @return [Integer] Number of records
69
+ def record_count(table_name)
70
+ table_count(table_name)
71
+ end
72
+
73
+ # Get the number of records in a table with filters applied
74
+ # @param table_name [String] Name of the table
75
+ # @param column_filters [Hash] Hash of column_name => filter_value for filtering
76
+ # @return [Integer] Number of filtered records
77
+ def filtered_record_count(table_name, column_filters = {})
78
+ model = get_model_for(table_name)
79
+ query = model.all
80
+
81
+ # Apply column filters if provided
82
+ query = apply_column_filters(query, table_name, column_filters)
83
+
84
+ query.count
85
+ end
86
+
87
+ # Execute a raw SQL query after validating for safety
88
+ # @param sql [String] SQL query to execute
89
+ # @return [ActiveRecord::Result] Result set with columns and rows
90
+ # @raise [StandardError] If the query is invalid or unsafe
91
+ def execute_query(sql)
92
+ @query_executor.execute_query(sql)
93
+ end
94
+
95
+ # Execute a SQLite PRAGMA command without adding a LIMIT clause
96
+ # @param pragma [String] PRAGMA command to execute (without the "PRAGMA" keyword)
97
+ # @return [ActiveRecord::Result] Result set with the PRAGMA value
98
+ # @raise [StandardError] If the query is invalid or cannot be executed
99
+ def execute_sqlite_pragma(pragma)
100
+ @query_executor.execute_sqlite_pragma(pragma)
101
+ end
102
+
103
+ # Query a table with more granular control using ActiveRecord
104
+ # @param table_name [String] Name of the table
105
+ # @param select [String, Array] Columns to select
106
+ # @param order [String, Hash] Order by clause
107
+ # @param limit [Integer] Maximum number of records to return
108
+ # @param offset [Integer] Offset from which to start returning records
109
+ # @param where [String, Hash] Where conditions
110
+ # @return [ActiveRecord::Result] Result set with columns and rows
111
+ def query_table(table_name, select: nil, order: nil, limit: nil, offset: nil, where: nil, max_records: 1000)
112
+ model = get_model_for(table_name)
113
+ query = model.all
114
+
115
+ query = query.select(select) if select.present?
116
+ query = query.where(where) if where.present?
117
+ query = query.order(order) if order.present?
118
+
119
+ # Apply safety limit
120
+ query = query.limit([ limit || max_records, max_records ].min)
121
+ query = query.offset(offset) if offset.present?
122
+
123
+ # Get column names for the result set
124
+ column_names = if select.is_a?(Array)
125
+ select
126
+ elsif select.is_a?(String) && !select.include?("*")
127
+ select.split(",").map(&:strip)
128
+ else
129
+ table_columns(table_name).map { |c| c[:name] }
130
+ end
131
+
132
+ @query_executor.to_result_set(query, column_names)
133
+ end
134
+
135
+ # Fetch timestamp data for visualization
136
+ # @param table_name [String] Name of the table
137
+ # @param grouping [String] Grouping type (hourly, daily, weekly, monthly)
138
+ # @param column [String] Timestamp column name (defaults to created_at)
139
+ # @return [Array<Hash>] Array of timestamp data with labels and counts
140
+ def fetch_timestamp_data(table_name, grouping = "daily", column = "created_at")
141
+ return [] unless column_exists?(table_name, column)
142
+
143
+ adapter = @connection.adapter_name.downcase
144
+
145
+ date_format = case grouping
146
+ when "hourly"
147
+ if adapter =~ /mysql/
148
+ "DATE_FORMAT(#{column}, '%Y-%m-%d %H:00')"
149
+ elsif adapter =~ /sqlite/
150
+ "strftime('%Y-%m-%d %H:00', #{column})"
151
+ else # postgresql
152
+ "TO_CHAR(#{column}, 'YYYY-MM-DD HH24:00')"
153
+ end
154
+ when "weekly"
155
+ if adapter =~ /mysql/
156
+ "DATE_FORMAT(#{column}, '%Y-%v')"
157
+ elsif adapter =~ /sqlite/
158
+ "strftime('%Y-%W', #{column})"
159
+ else # postgresql
160
+ "TO_CHAR(#{column}, 'YYYY-IW')"
161
+ end
162
+ when "monthly"
163
+ if adapter =~ /mysql/
164
+ "DATE_FORMAT(#{column}, '%Y-%m')"
165
+ elsif adapter =~ /sqlite/
166
+ "strftime('%Y-%m', #{column})"
167
+ else # postgresql
168
+ "TO_CHAR(#{column}, 'YYYY-MM')"
169
+ end
170
+ else # daily is default
171
+ if adapter =~ /mysql/
172
+ "DATE(#{column})"
173
+ elsif adapter =~ /sqlite/
174
+ "date(#{column})"
175
+ else # postgresql
176
+ "DATE(#{column})"
177
+ end
178
+ end
179
+
180
+ # Query works the same for all database adapters
181
+ query = "SELECT #{date_format} as label, COUNT(*) as count FROM #{table_name}
182
+ WHERE #{column} IS NOT NULL
183
+ GROUP BY label
184
+ ORDER BY MIN(#{column}) DESC LIMIT 30"
185
+
186
+ begin
187
+ result = @connection.execute(query)
188
+
189
+ # Format depends on adapter
190
+ if adapter =~ /mysql/
191
+ result.to_a.map { |row| { label: row[0], value: row[1] } }
192
+ elsif adapter =~ /sqlite/
193
+ result.map { |row| { label: row["label"], value: row["count"] } }
194
+ else # postgresql
195
+ result.map { |row| { label: row["label"], value: row["count"] } }
196
+ end
197
+ rescue => e
198
+ Rails.logger.error("Error fetching timestamp data: #{e.message}")
199
+ []
200
+ end
201
+ end
202
+
203
+ # Analyze query performance for a table with given filters
204
+ # @param table_name [String] Name of the table
205
+ # @param params [TableQueryParams] Query parameters object
206
+ # @return [Hash] Performance analysis and recommendations
207
+ def analyze_query_performance(table_name, params)
208
+ @query_analyzer.analyze_query(table_name, params)
209
+ end
210
+
211
+ private
212
+
213
+ # Apply column filters to a query
214
+ # @param query [ActiveRecord::Relation] The query to apply filters to
215
+ # @param table_name [String] Name of the table
216
+ # @param column_filters [Hash] Hash of column_name => filter_value pairs
217
+ # @return [ActiveRecord::Relation] The filtered query
218
+ def apply_column_filters(query, table_name, column_filters)
219
+ return query unless column_filters.present?
220
+
221
+ # Create a copy of column_filters to modify without affecting the original
222
+ filters = column_filters.dup
223
+
224
+ # First check if we have a datetime range filter for created_at
225
+ if filters["created_at"].present? &&
226
+ filters["created_at_end"].present? &&
227
+ column_exists?(table_name, "created_at")
228
+
229
+ # Handle datetime range for created_at
230
+ begin
231
+ start_datetime = Time.parse(filters["created_at"].to_s)
232
+ end_datetime = Time.parse(filters["created_at_end"].to_s)
233
+
234
+ # Make sure end_datetime is at the end of the day/minute if it doesn't have time component
235
+ if end_datetime.to_s.match(/00:00:00/)
236
+ end_datetime = end_datetime.end_of_day
237
+ end
238
+
239
+ Rails.logger.info("[DBViewer] Applying date range filter on #{table_name}.created_at: #{start_datetime} to #{end_datetime}")
240
+
241
+ # Use qualified column name for tables with schema
242
+ column_name = "#{table_name}.created_at"
243
+
244
+ # Different databases may require different SQL for datetime comparison
245
+ adapter_name = connection.adapter_name.downcase
246
+ if adapter_name.include?("sqlite")
247
+ # SQLite needs special handling for datetimes
248
+ query = query.where("datetime(#{column_name}) BETWEEN datetime(?) AND datetime(?)",
249
+ start_datetime.iso8601, end_datetime.iso8601)
250
+ else
251
+ # Standard SQL for most databases
252
+ query = query.where("#{column_name} BETWEEN ? AND ?", start_datetime, end_datetime)
253
+ end
254
+
255
+ # Remove these keys so they're not processed again
256
+ filters.delete("created_at")
257
+ filters.delete("created_at_end")
258
+ filters.delete("created_at_operator")
259
+ rescue => e
260
+ Rails.logger.error("[DBViewer] Failed to parse datetime range: #{e.message}")
261
+ Rails.logger.error(e.backtrace.join("\n"))
262
+ end
263
+ end
264
+
265
+ # Process remaining filters
266
+ filters.each do |column, value|
267
+ # Skip operator entries (they'll be handled with their corresponding value)
268
+ next if column.to_s.end_with?("_operator") || column.to_s.end_with?("_end")
269
+ next if value.blank?
270
+ next unless column_exists?(table_name, column)
271
+
272
+ # Get operator if available, otherwise use default
273
+ operator = filters["#{column}_operator"]
274
+
275
+ query = apply_column_filter(query, column, value, table_name, operator)
276
+ end
277
+
278
+ query
279
+ end
280
+
281
+ # Get a dynamic AR model for a table
282
+ # @param table_name [String] Name of the table
283
+ # @return [Class] ActiveRecord model class
284
+ def get_model_for(table_name)
285
+ @dynamic_model_factory.get_model_for(table_name)
286
+ end
287
+
288
+ # Get column information for a table
289
+ # @param table_name [String] Name of the table
290
+ # @return [Array<Hash>] List of column info hashes
291
+ def table_columns(table_name)
292
+ @table_metadata_manager.table_columns(table_name)
293
+ end
294
+
295
+ # Check if column exists in table
296
+ # @param table_name [String] Name of the table
297
+ # @param column_name [String] Name of the column
298
+ # @return [Boolean] True if column exists
299
+ def column_exists?(table_name, column_name)
300
+ @table_metadata_manager.column_exists?(table_name, column_name)
301
+ end
302
+
303
+ # Helper method to apply column filters to a query
304
+ # @param query [ActiveRecord::Relation] The query to apply filters to
305
+ # @param column [String] The column name to filter on
306
+ # @param value [String] The value to filter by
307
+ # @param table_name [String] The name of the table being queried
308
+ # @param operator [String] The operator to use for filtering (eq, neq, lt, gt, etc.)
309
+ # @return [ActiveRecord::Relation] The filtered query
310
+ def apply_column_filter(query, column, value, table_name, operator = nil)
311
+ column_info = table_columns(table_name).find { |c| c[:name] == column }
312
+ return query unless column_info
313
+
314
+ column_type = column_info[:type].to_s
315
+ quoted_column = connection.quote_column_name(column)
316
+
317
+ # Default to appropriate operator if none specified or "default" value is used
318
+ operator = default_operator_for_type(column_type) if operator.nil? || operator == "default"
319
+
320
+ # Detect the column type for appropriate filtering
321
+ if datetime_type?(column_type)
322
+ filter_datetime_column(query, quoted_column, value, operator)
323
+ elsif date_type?(column_type)
324
+ filter_date_column(query, quoted_column, value, operator)
325
+ elsif time_type?(column_type)
326
+ filter_time_column(query, quoted_column, value, operator)
327
+ elsif numeric_type?(column_type)
328
+ filter_numeric_column(query, quoted_column, value, operator)
329
+ elsif string_type?(column_type)
330
+ filter_string_column(query, quoted_column, value, operator)
331
+ else
332
+ # For unknown types, try numeric filter if value is numeric, otherwise string filter
333
+ numeric_pattern = /\A[+-]?\d+(\.\d+)?\z/
334
+ if value =~ numeric_pattern
335
+ filter_numeric_column(query, quoted_column, value, operator)
336
+ else
337
+ filter_string_column(query, quoted_column, value, operator)
338
+ end
339
+ end
340
+ end
341
+
342
+ # Determine if column is a datetime type
343
+ # @param column_type [String] The column type from database
344
+ # @return [Boolean] True if datetime type
345
+ def datetime_type?(column_type)
346
+ column_type =~ /datetime|timestamp/i
347
+ end
348
+
349
+ # Determine if column is a date type
350
+ # @param column_type [String] The column type from database
351
+ # @return [Boolean] True if date type
352
+ def date_type?(column_type)
353
+ column_type =~ /^date$/i
354
+ end
355
+
356
+ # Determine if column is a time type
357
+ # @param column_type [String] The column type from database
358
+ # @return [Boolean] True if time type
359
+ def time_type?(column_type)
360
+ column_type =~ /^time$/i
361
+ end
362
+
363
+ # Determine if column is a numeric type
364
+ # @param column_type [String] The column type from database
365
+ # @return [Boolean] True if numeric type
366
+ def numeric_type?(column_type)
367
+ column_type =~ /int|float|decimal|double|number|numeric|real|money|bigint|smallint|tinyint|mediumint|bit/i
368
+ end
369
+
370
+ # Determine if column is a string type
371
+ # @param column_type [String] The column type from database
372
+ # @return [Boolean] True if string type
373
+ def string_type?(column_type)
374
+ column_type =~ /char|text|string|uuid|enum/i
375
+ end
376
+
377
+ # Get default operator based on column type
378
+ # @param column_type [String] The column type from database
379
+ # @return [String] The default operator
380
+ def default_operator_for_type(column_type)
381
+ if string_type?(column_type)
382
+ "contains"
383
+ else
384
+ "eq"
385
+ end
386
+ end
387
+
388
+ # Apply comparison operator to query
389
+ # @param query [ActiveRecord::Relation] The query
390
+ # @param column [String] The quoted column name
391
+ # @param value [Object] The value to compare
392
+ # @param operator [String] The operator
393
+ # @return [ActiveRecord::Relation] The modified query
394
+ def apply_comparison_operator(query, column, value, operator)
395
+ case operator
396
+ when "eq"
397
+ query.where("#{column} = ?", value)
398
+ when "neq"
399
+ query.where("#{column} != ?", value)
400
+ when "lt"
401
+ query.where("#{column} < ?", value)
402
+ when "gt"
403
+ query.where("#{column} > ?", value)
404
+ when "lte"
405
+ query.where("#{column} <= ?", value)
406
+ when "gte"
407
+ query.where("#{column} >= ?", value)
408
+ else
409
+ # Default to equality
410
+ query.where("#{column} = ?", value)
411
+ end
412
+ end
413
+
414
+ # Apply string comparison operator
415
+ # @param query [ActiveRecord::Relation] The query
416
+ # @param column [String] The quoted column name
417
+ # @param value [String] The string value to compare
418
+ # @param operator [String] The operator
419
+ # @return [ActiveRecord::Relation] The modified query
420
+ def apply_string_operator(query, column, value, operator)
421
+ # Cast to text for UUID columns when using string operations like LIKE
422
+ column_expr = @adapter_name =~ /postgresql/ ? "CAST(#{column} AS TEXT)" : column
423
+
424
+ case operator
425
+ when "contains"
426
+ query.where("#{column_expr} LIKE ?", "%#{value}%")
427
+ when "not_contains"
428
+ query.where("#{column_expr} NOT LIKE ?", "%#{value}%")
429
+ when "eq"
430
+ query.where("#{column} = ?", value)
431
+ when "neq"
432
+ query.where("#{column} != ?", value)
433
+ when "starts_with"
434
+ query.where("#{column_expr} LIKE ?", "#{value}%")
435
+ when "ends_with"
436
+ query.where("#{column_expr} LIKE ?", "%#{value}")
437
+ else
438
+ # Default to contains
439
+ query.where("#{column_expr} LIKE ?", "%#{value}%")
440
+ end
441
+ end
442
+
443
+ # Filter datetime column
444
+ # @param query [ActiveRecord::Relation] The query
445
+ # @param quoted_column [String] The quoted column name
446
+ # @param value [String] The value to filter by
447
+ # @param operator [String] The operator
448
+ # @return [ActiveRecord::Relation] The modified query
449
+ def filter_datetime_column(query, quoted_column, value, operator)
450
+ begin
451
+ Rails.logger.info("[DBViewer] Filtering datetime column #{quoted_column} with value: #{value.inspect} and operator: #{operator.inspect}")
452
+
453
+ # Handle HTML datetime-local format (2023-05-10T14:30)
454
+ parsed_date = if value.to_s.include?("T")
455
+ Time.parse(value.to_s)
456
+ else
457
+ # Handle regular date/time string
458
+ Time.parse(value.to_s)
459
+ end
460
+
461
+ # Different databases handle datetime differently, so adapt the query based on the adapter
462
+ adapter = connection.adapter_name.downcase
463
+
464
+ # Apply the appropriate operator - special handling for 'eq' on dates to match full day
465
+ if operator == "eq"
466
+ # For equality, match the entire day (from midnight to 23:59:59)
467
+ start_of_day = parsed_date.beginning_of_day
468
+ end_of_day = parsed_date.end_of_day
469
+
470
+ if adapter.include?("sqlite")
471
+ query.where("datetime(#{quoted_column}) BETWEEN datetime(?) AND datetime(?)",
472
+ start_of_day.iso8601, end_of_day.iso8601)
473
+ else
474
+ query.where("#{quoted_column} BETWEEN ? AND ?", start_of_day, end_of_day)
475
+ end
476
+ elsif operator == "gte"
477
+ if adapter.include?("sqlite")
478
+ query.where("datetime(#{quoted_column}) >= datetime(?)", parsed_date.iso8601)
479
+ else
480
+ query.where("#{quoted_column} >= ?", parsed_date)
481
+ end
482
+ elsif operator == "lte"
483
+ if adapter.include?("sqlite")
484
+ query.where("datetime(#{quoted_column}) <= datetime(?)", parsed_date.iso8601)
485
+ else
486
+ query.where("#{quoted_column} <= ?", parsed_date)
487
+ end
488
+ elsif operator == "gt"
489
+ if adapter.include?("sqlite")
490
+ query.where("datetime(#{quoted_column}) > datetime(?)", parsed_date.iso8601)
491
+ else
492
+ query.where("#{quoted_column} > ?", parsed_date)
493
+ end
494
+ elsif operator == "lt"
495
+ if adapter.include?("sqlite")
496
+ query.where("datetime(#{quoted_column}) < datetime(?)", parsed_date.iso8601)
497
+ else
498
+ query.where("#{quoted_column} < ?", parsed_date)
499
+ end
500
+ else
501
+ # Default to equality
502
+ apply_comparison_operator(query, quoted_column, parsed_date, operator || "eq")
503
+ end
504
+
505
+ Rails.logger.info("[DBViewer] Successfully applied datetime filter")
506
+ query
507
+ rescue => e
508
+ Rails.logger.error("[DBViewer] Failed to parse datetime: #{e.message}")
509
+ Rails.logger.error(e.backtrace.join("\n"))
510
+ # Fallback to string comparison if parsing fails
511
+ query.where("CAST(#{quoted_column} AS CHAR) LIKE ?", "%#{value}%")
512
+ end
513
+ end
514
+
515
+ # Filter date column
516
+ # @param query [ActiveRecord::Relation] The query
517
+ # @param quoted_column [String] The quoted column name
518
+ # @param value [String] The value to filter by
519
+ # @param operator [String] The operator
520
+ # @return [ActiveRecord::Relation] The modified query
521
+ def filter_date_column(query, quoted_column, value, operator)
522
+ begin
523
+ parsed_date = Date.parse(value.to_s)
524
+ apply_comparison_operator(query, quoted_column, parsed_date, operator)
525
+ rescue => e
526
+ Rails.logger.debug("[DBViewer] Failed to parse date: #{e.message}")
527
+ # Fallback to string comparison if parsing fails
528
+ query.where("CAST(#{quoted_column} AS CHAR) LIKE ?", "%#{value}%")
529
+ end
530
+ end
531
+
532
+ # Filter time column
533
+ # @param query [ActiveRecord::Relation] The query
534
+ # @param quoted_column [String] The quoted column name
535
+ # @param value [String] The value to filter by
536
+ # @param operator [String] The operator
537
+ # @return [ActiveRecord::Relation] The modified query
538
+ def filter_time_column(query, quoted_column, value, operator)
539
+ begin
540
+ parsed_time = Time.parse(value.to_s)
541
+ formatted_time = parsed_time.strftime("%H:%M:%S")
542
+
543
+ # Prepare time expression based on database adapter
544
+ time_expr = get_time_expression(quoted_column)
545
+
546
+ apply_comparison_operator(query, time_expr, formatted_time, operator)
547
+ rescue => e
548
+ Rails.logger.debug("[DBViewer] Failed to parse time: #{e.message}")
549
+ # Fallback to string comparison if parsing fails
550
+ query.where("CAST(#{quoted_column} AS CHAR) LIKE ?", "%#{value}%")
551
+ end
552
+ end
553
+
554
+ # Get the appropriate time expression based on database adapter
555
+ # @param quoted_column [String] The quoted column name
556
+ # @return [String] The time expression
557
+ def get_time_expression(quoted_column)
558
+ if @adapter_name =~ /mysql/
559
+ "TIME(#{quoted_column})"
560
+ elsif @adapter_name =~ /postgresql/
561
+ "CAST(#{quoted_column} AS TIME)"
562
+ else
563
+ # SQLite and others
564
+ quoted_column
565
+ end
566
+ end
567
+
568
+ # Filter numeric column
569
+ # @param query [ActiveRecord::Relation] The query
570
+ # @param quoted_column [String] The quoted column name
571
+ # @param value [String] The value to filter by
572
+ # @param operator [String] The operator
573
+ # @return [ActiveRecord::Relation] The modified query
574
+ def filter_numeric_column(query, quoted_column, value, operator)
575
+ numeric_pattern = /\A[+-]?\d+(\.\d+)?\z/
576
+
577
+ if value =~ numeric_pattern
578
+ # Convert to proper numeric type for comparison
579
+ numeric_value = value.include?(".") ? value.to_f : value.to_i
580
+ Rails.logger.debug("[DBViewer] Converting value #{value} to numeric: #{numeric_value}")
581
+
582
+ apply_comparison_operator(query, quoted_column, numeric_value, operator)
583
+ else
584
+ # Non-numeric value for numeric column, try string comparison
585
+ filter_non_numeric_value(query, quoted_column, value, operator)
586
+ end
587
+ end
588
+
589
+ # Filter non-numeric value for numeric column
590
+ # @param query [ActiveRecord::Relation] The query
591
+ # @param quoted_column [String] The quoted column name
592
+ # @param value [String] The value to filter by
593
+ # @param operator [String] The operator
594
+ # @return [ActiveRecord::Relation] The modified query
595
+ def filter_non_numeric_value(query, quoted_column, value, operator)
596
+ case operator
597
+ when "contains", "starts_with", "ends_with"
598
+ query.where("CAST(#{quoted_column} AS CHAR) LIKE ?", "%#{value}%")
599
+ when "not_contains"
600
+ query.where("CAST(#{quoted_column} AS CHAR) NOT LIKE ?", "%#{value}%")
601
+ when "eq"
602
+ query.where("CAST(#{quoted_column} AS CHAR) = ?", value)
603
+ when "neq"
604
+ query.where("CAST(#{quoted_column} AS CHAR) != ?", value)
605
+ else
606
+ # Default to contains
607
+ query.where("CAST(#{quoted_column} AS CHAR) LIKE ?", "%#{value}%")
608
+ end
609
+ end
610
+
611
+ # Filter string column
612
+ # @param query [ActiveRecord::Relation] The query
613
+ # @param quoted_column [String] The quoted column name
614
+ # @param value [String] The value to filter by
615
+ # @param operator [String] The operator
616
+ # @return [ActiveRecord::Relation] The modified query
617
+ def filter_string_column(query, quoted_column, value, operator)
618
+ apply_string_operator(query, quoted_column, value, operator)
619
+ end
620
+ end
621
+ end
@@ -0,0 +1,39 @@
1
+ module Dbviewer
2
+ # TableQueryParams encapsulates parameters for table querying operations
3
+ class TableQueryParams
4
+ attr_reader :page, :order_by, :direction, :per_page, :column_filters, :max_records
5
+
6
+ # Initialize query parameters with defaults
7
+ # @param page [Integer] Page number (1-based)
8
+ # @param order_by [String, nil] Column to sort by
9
+ # @param direction [String] Sort direction ('ASC' or 'DESC')
10
+ # @param per_page [Integer, nil] Number of records per page
11
+ # @param column_filters [Hash, nil] Hash of column filters
12
+ # @param max_records [Integer] Maximum number of records to fetch
13
+ def initialize(
14
+ page: 1,
15
+ order_by: nil,
16
+ direction: "ASC",
17
+ per_page: nil,
18
+ column_filters: nil,
19
+ max_records: 1000
20
+ )
21
+ @page = [ 1, page.to_i ].max
22
+ @order_by = order_by
23
+ @direction = normalize_direction(direction)
24
+ @per_page = normalize_per_page(per_page || 25, max_records)
25
+ @column_filters = column_filters || {}
26
+ @max_records = max_records
27
+ end
28
+
29
+ private
30
+
31
+ def normalize_direction(dir)
32
+ %w[ASC DESC].include?(dir.to_s.upcase) ? dir.to_s.upcase : "ASC"
33
+ end
34
+
35
+ def normalize_per_page(per_page_value, max)
36
+ [ per_page_value.to_i, max ].min
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.3.15"
2
+ VERSION = "0.3.16"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -6,6 +6,7 @@ require "dbviewer/cache_manager"
6
6
  require "dbviewer/table_metadata_manager"
7
7
  require "dbviewer/dynamic_model_factory"
8
8
  require "dbviewer/query_executor"
9
+ require "dbviewer/table_query_operations"
9
10
  require "dbviewer/database_manager"
10
11
  require "dbviewer/sql_validator"
11
12