sqlreport 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 521ae617f946f8878d3ec77d64198cdb9abe65567c8c752b731661ff62112550
4
- data.tar.gz: 6115f8c4a9c3032ba983464ddd1ec8eafa853583d7dc71c0f8dce0a95c0b9192
3
+ metadata.gz: e9ab7d553ecd33126c38418100399abab18686a28faba11e0e4ac0d46491c01e
4
+ data.tar.gz: 7b77b6af58c083760be6ce350903ad58cf9ea7742084aa464a6e64173ca0a8e6
5
5
  SHA512:
6
- metadata.gz: 77a80f7b76808e6d6496549709bf2e697ee7b983a08533c7515fc1918d24a68eb002fe0e95bdb1c8079a36897bf958d1fe3f8aea055db411b8656bb025e3ef85
7
- data.tar.gz: 85d22692915240b6fcf6b4049d81a72a33770542e631feec663f25fd6850b51d19cffbe70621d802b8d2b666eeb2481169e3dcf28d291d787bf5249d0b03d42f
6
+ metadata.gz: fe9b20a35baad6c7a39fd0d5b598aea299bb6f9e8c661b1972c6ed46cd0bb45676ebdc66ee1e8fff9ad32f0af643d35cd25e0072b896a632b94d0bf51c2483e6
7
+ data.tar.gz: ff031385aa5b1e84973e668cda73b2ef705d736485c500f7366d07268db8b286891fcd637a3b136162f17782b0fa257256c14cbd664e997e524c0863ebff718f
data/CHANGELOG.md CHANGED
@@ -1,4 +1,9 @@
1
- ## [Unreleased]
1
+ ## [0.1.2]
2
+
3
+ - Added batch processing for large datasets
4
+ - Added streaming CSV export for memory-efficient processing
5
+ - Added progress tracking for batch operations
6
+ - Added ActiveRecord integration for direct model-to-CSV conversion
2
7
 
3
8
  ## [0.1.1] - 2025-03-09
4
9
 
data/README.md CHANGED
@@ -10,6 +10,12 @@ This gem provides an easy way to convert SQL database queries to CSV. Below you
10
10
  - [Write result to CSV file](#write-result-to-csv-file)
11
11
  - [Get columns / headers](#get-column-headers)
12
12
  - [Get rows](#get-rows)
13
+ - [Batch Processing](#batch-processing)
14
+ - [Creating a Batch Manager](#creating-a-batch-manager)
15
+ - [Processing Batches](#processing-batches)
16
+ - [Streaming to CSV](#streaming-to-csv)
17
+ - [Tracking Progress](#tracking-progress)
18
+ - [ActiveRecord Integration](#activerecord-integration)
13
19
  - [Compatibility](#compatibility)
14
20
  - [Todo](#todo)
15
21
  - [Contributing](#contributing)
@@ -94,13 +100,88 @@ This gem is tested with the following Ruby versions on Linux and Mac OS X:
94
100
 
95
101
  - Ruby > 2.2.2
96
102
 
103
+ ## Batch Processing
104
+
105
+ For handling large datasets, SQLreport provides batch processing capabilities that allow you to process data in chunks to avoid memory issues.
106
+
107
+ ### Creating a Batch Manager
108
+
109
+ ```ruby
110
+ batch_manager = Sqlreport.batch_query("SELECT * FROM large_table", batch_size: 1000)
111
+ ```
112
+
113
+ ### Processing Batches
114
+
115
+ You can process one batch at a time:
116
+
117
+ ```ruby
118
+ # Get the next batch
119
+ batch = batch_manager.next_batch
120
+ # Process the batch
121
+ batch.rows.each do |row|
122
+ # Process each row
123
+ end
124
+ ```
125
+
126
+ Or process all batches at once with a block:
127
+
128
+ ```ruby
129
+ batch_manager.process_all do |batch|
130
+ # Process each batch
131
+ puts "Processing batch with #{batch.rows.count} rows"
132
+ end
133
+ ```
134
+
135
+ ### Streaming to CSV
136
+
137
+ For very large datasets, you can stream directly to a CSV file without loading all data into memory:
138
+
139
+ ```ruby
140
+ batch_manager.stream_to_csv("large_table.csv")
141
+ ```
142
+
143
+ ### Tracking Progress
144
+
145
+ You can track the progress of batch processing:
146
+
147
+ ```ruby
148
+ batch_manager.count_total_rows # Get total row count for progress calculation
149
+ batch_manager.next_batch
150
+ puts "Processed #{batch_manager.processed_rows} of #{batch_manager.total_rows} rows"
151
+ puts "Progress: #{batch_manager.progress_percentage}%"
152
+ ```
153
+
154
+ ## ActiveRecord Integration
155
+
156
+ SQLreport can be used directly with ActiveRecord models and relations, allowing for a more fluent interface:
157
+
158
+ ```ruby
159
+ # Generate a report from an ActiveRecord relation
160
+ User.where(active: true).sqlreport.result.write_csv("active_users.csv")
161
+
162
+ # Or with more options
163
+ Post.where(published: true)
164
+ .order(created_at: :desc)
165
+ .limit(100)
166
+ .sqlreport
167
+ .result
168
+ .to_csv(include_headers: true, separator: ",")
169
+
170
+ # Use batch processing with ActiveRecord
171
+ User.where(created_at: 1.month.ago..Time.current)
172
+ .sqlreport_batch(batch_size: 500)
173
+ .stream_to_csv("new_users.csv")
174
+ ```
175
+
176
+ This integration makes it easy to generate reports directly from your models without having to write raw SQL.
177
+
97
178
  ## Todo
98
179
 
99
- - Batch jobs
180
+ - ~~Add simple safeguard validations~~
181
+ - ~~Allow it to use different databases~~
182
+ - ~~Batch jobs (for bigger tables)~~
183
+ - ~~Tie into Rails models~~
100
184
  - Add support for multiple export options (PDF, textfile, LaTex)
101
- - Add safeguard validations
102
- - Tie into Rails models
103
- - Allow it to use different databases
104
185
  - ..
105
186
 
106
187
  ## Contributing
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "sqlreport"
6
+ require "active_record"
7
+
8
+ # Set up a test database
9
+ ActiveRecord::Base.establish_connection(
10
+ adapter: "sqlite3",
11
+ database: ":memory:"
12
+ )
13
+
14
+ # Create a test table
15
+ ActiveRecord::Base.connection.create_table(:test_table, force: true) do |t|
16
+ t.column :user_id, :integer
17
+ t.column :name, :string
18
+ t.column :json_object, :json
19
+ end
20
+
21
+ # Insert test data
22
+ 10.times do |i|
23
+ ActiveRecord::Base.connection.execute("INSERT INTO test_table (user_id, name) VALUES (#{i}, 'test name #{i}');")
24
+ end
25
+
26
+ # Create a model for the test table
27
+ class TestTable < ActiveRecord::Base
28
+ self.table_name = "test_table" # Use the singular table name
29
+ end
30
+
31
+ puts "=== Basic Query ==="
32
+ result = Sqlreport.query("SELECT * FROM test_table").result
33
+ puts "Columns: #{result.columns.join(", ")}"
34
+ puts "Rows: #{result.rows.count}"
35
+ puts "CSV: #{result.to_csv.lines.first}"
36
+
37
+ puts "\n=== Batch Query ==="
38
+ batch_manager = Sqlreport.batch_query("SELECT * FROM test_table", batch_size: 3)
39
+ batch_manager.count_total_rows
40
+ puts "Total rows: #{batch_manager.total_rows}"
41
+
42
+ batch = batch_manager.next_batch
43
+ puts "First batch rows: #{batch.rows.count}"
44
+ puts "Processed rows: #{batch_manager.processed_rows}"
45
+ puts "Progress: #{batch_manager.progress_percentage}%"
46
+
47
+ batch = batch_manager.next_batch
48
+ puts "Second batch rows: #{batch.rows.count}"
49
+ puts "Processed rows: #{batch_manager.processed_rows}"
50
+ puts "Progress: #{batch_manager.progress_percentage}%"
51
+
52
+ puts "\n=== ActiveRecord Integration ==="
53
+ # The extensions are automatically included in ActiveRecord::Relation
54
+ # by the ActiveSupport.on_load(:active_record) hook
55
+
56
+ # Test the sqlreport method
57
+ result = TestTable.where(user_id: 1..5).sqlreport.result
58
+ puts "ActiveRecord query rows: #{result.rows.count}"
59
+
60
+ # Test the sqlreport_batch method
61
+ batch_manager = TestTable.where(user_id: 1..5).sqlreport_batch(batch_size: 2)
62
+ batch = batch_manager.next_batch
63
+ puts "ActiveRecord batch rows: #{batch.rows.count}"
64
+ puts "Processed rows: #{batch_manager.processed_rows}"
65
+
66
+ puts "\n=== CSV Export ==="
67
+ # Write to CSV
68
+ csv_file = "test_table.csv"
69
+ result.write_csv(csv_file)
70
+ puts "CSV file created: #{File.exist?(csv_file)}"
71
+ puts "CSV content: #{File.read(csv_file).lines.first}"
72
+ FileUtils.rm_f(csv_file)
73
+
74
+ # Stream to CSV
75
+ batch_csv_file = "batch_test_table.csv"
76
+ batch_manager = TestTable.where(user_id: 1..5).sqlreport_batch(batch_size: 3)
77
+ batch_manager.stream_to_csv(batch_csv_file)
78
+ puts "Batch CSV file created: #{File.exist?(batch_csv_file)}"
79
+ puts "Batch CSV content: #{File.read(batch_csv_file).lines.first}"
80
+ FileUtils.rm_f(batch_csv_file)
81
+
82
+ puts "\nAll examples completed successfully!"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sqlreport
4
+ # ActiveRecordExtension
5
+ # Extends ActiveRecord::Relation with SQLReport functionality
6
+ module ActiveRecordExtension
7
+ # Provides SQLReport functionality to ActiveRecord models
8
+ module RelationMethods
9
+ # Convert the relation to a SQLReport result
10
+ def sqlreport
11
+ # Get the SQL query from the relation
12
+ sql = to_sql
13
+
14
+ # Create a SQLReport result from the query
15
+ ::Sqlreport::Result.new(sql)
16
+ end
17
+ end
18
+
19
+ # Provides batch processing functionality to ActiveRecord models
20
+ module BatchMethods
21
+ # Convert the relation to a SQLReport batch manager
22
+ def sqlreport_batch(batch_size: 1000)
23
+ # Get the SQL query from the relation
24
+ sql = to_sql
25
+
26
+ # Create a SQLReport batch manager from the query
27
+ ::Sqlreport::BatchManager.new(sql, batch_size: batch_size)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ # Extend ActiveRecord::Relation with SQLReport functionality
34
+ ActiveSupport.on_load(:active_record) do
35
+ ActiveRecord::Relation.include(Sqlreport::ActiveRecordExtension::RelationMethods)
36
+ ActiveRecord::Relation.include(Sqlreport::ActiveRecordExtension::BatchMethods)
37
+ end
@@ -7,4 +7,29 @@ module Sqlreport
7
7
  def self.query(sql_query)
8
8
  ::Sqlreport::Result.new(sql_query)
9
9
  end
10
+
11
+ def self.batch_query(sql_query, batch_size: 1000)
12
+ ::Sqlreport::BatchManager.new(sql_query, batch_size: batch_size)
13
+ end
14
+
15
+ def self.database(db_config)
16
+ # Return a DatabaseConnector instance
17
+ DatabaseConnector.new(db_config)
18
+ end
19
+
20
+ # DatabaseConnector
21
+ # Handles database connection switching
22
+ class DatabaseConnector
23
+ def initialize(db_config)
24
+ @db_config = db_config
25
+ end
26
+
27
+ def query(sql_query)
28
+ ::Sqlreport::Result.new(sql_query, db_config: @db_config)
29
+ end
30
+
31
+ def batch_query(sql_query, batch_size: 1000)
32
+ ::Sqlreport::BatchManager.new(sql_query, batch_size: batch_size, db_config: @db_config)
33
+ end
34
+ end
10
35
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sqlreport
4
+ # BatchManager
5
+ # Handles batch processing of SQL queries for large datasets
6
+ class BatchManager
7
+ attr_reader :total_rows, :processed_rows, :batch_size, :current_offset
8
+
9
+ def initialize(query, batch_size: 1000, db_config: false)
10
+ @query = query
11
+ @batch_size = batch_size
12
+ @db_config = db_config
13
+ @total_rows = nil
14
+ @processed_rows = 0
15
+ @current_offset = 0
16
+ @complete = false
17
+ end
18
+
19
+ def connection
20
+ @connection ||= if @db_config
21
+ ActiveRecord::Base.establish_connection(@db_config)
22
+ else
23
+ ActiveRecord::Base.connection
24
+ end
25
+ end
26
+
27
+ def count_total_rows
28
+ return @total_rows if @total_rows
29
+
30
+ # Extract the FROM clause and any WHERE conditions to count total rows
31
+ from_clause = @query.match(/FROM\s+([^\s;]+)(\s+WHERE\s+(.+?))?(\s+ORDER BY|\s+GROUP BY|\s+LIMIT|\s*;|\s*$)/i)
32
+ return nil unless from_clause
33
+
34
+ table = from_clause[1]
35
+ where_clause = from_clause[3]
36
+
37
+ count_query = "SELECT COUNT(*) FROM #{table}"
38
+ count_query += " WHERE #{where_clause}" if where_clause
39
+
40
+ result = connection.exec_query(count_query)
41
+ @total_rows = result.rows.first.first.to_i
42
+ end
43
+
44
+ def next_batch
45
+ return nil if @complete
46
+
47
+ # Add LIMIT and OFFSET to the query
48
+ paginated_query = if @query.include?("LIMIT")
49
+ # If query already has LIMIT, we need to handle differently
50
+ raise "Batch processing not supported for queries with LIMIT clause"
51
+ else
52
+ "#{@query.chomp(";")} LIMIT #{@batch_size} OFFSET #{@current_offset}"
53
+ end
54
+
55
+ result = Sqlreport::Result.new(paginated_query, db_config: @db_config).result
56
+
57
+ # Update state
58
+ rows_in_batch = result.rows.count
59
+ @processed_rows += rows_in_batch
60
+ @current_offset += @batch_size
61
+ @complete = rows_in_batch < @batch_size
62
+
63
+ result
64
+ end
65
+
66
+ def process_all
67
+ results = []
68
+ while (batch = next_batch)
69
+ results << batch
70
+ yield batch if block_given?
71
+ end
72
+ results
73
+ end
74
+
75
+ def stream_to_csv(path, include_headers: true, separator: ",", quote_char: '"')
76
+ first_batch = true
77
+
78
+ File.open(path, "w") do |file|
79
+ process_all do |batch|
80
+ if first_batch && include_headers
81
+ headers = batch.columns.join(separator)
82
+ file.puts headers
83
+ first_batch = false
84
+ elsif first_batch
85
+ first_batch = false
86
+ end
87
+
88
+ # Write rows without loading all into memory
89
+ batch.rows.each do |row|
90
+ csv_row = CSV.generate_line(row, col_sep: separator, quote_char: quote_char)
91
+ file.write(csv_row)
92
+ end
93
+ end
94
+ end
95
+
96
+ true
97
+ end
98
+
99
+ def progress_percentage
100
+ return 0 unless @total_rows && @total_rows.positive?
101
+
102
+ (@processed_rows.to_f / @total_rows * 100).round(2)
103
+ end
104
+ end
105
+ end
@@ -1,17 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "csv"
4
+ require "yaml"
4
5
 
5
6
  module Sqlreport
6
7
  # Result
7
8
  class Result
8
- def initialize(query)
9
+ def initialize(query, db_config: false)
9
10
  @query = query
10
- @connection = ActiveRecord::Base.connection
11
+ @db_config = db_config
11
12
  @response = nil
12
13
  end
13
14
 
14
- def result
15
+ def connection
16
+ @connection ||= if @db_config
17
+ ActiveRecord::Base.establish_connection(@db_config)
18
+ else
19
+ ActiveRecord::Base.connection
20
+ end
21
+ end
22
+
23
+ def result(validate: true)
24
+ connection
25
+ validations = validate_input if validate
26
+ return validations if validations
27
+
15
28
  @response = @connection.exec_query(@query)
16
29
  self
17
30
  end
@@ -36,5 +49,14 @@ module Sqlreport
36
49
  File.write(path, data)
37
50
  true
38
51
  end
52
+
53
+ private
54
+
55
+ def validate_input
56
+ return "DELETE, UPDATE, DROP, RENAME, ALTER cannot be used in queries" \
57
+ if %w[DELETE UPDATE DROP RENAME ALTER].any? { |needle| @query.include? needle }
58
+
59
+ false
60
+ end
39
61
  end
40
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sqlreport
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/sqlreport.rb CHANGED
@@ -6,4 +6,6 @@ require "active_support/lazy_load_hooks"
6
6
  ActiveSupport.on_load(:active_record) do
7
7
  require "sqlreport/base"
8
8
  require "sqlreport/result"
9
+ require "sqlreport/batch_manager"
10
+ require "sqlreport/active_record_extension"
9
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqlreport
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gem shards
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-03-15 00:00:00.000000000 Z
11
+ date: 2025-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -79,8 +79,11 @@ files:
79
79
  - LICENSE.txt
80
80
  - README.md
81
81
  - Rakefile
82
+ - examples/batch_operations.rb
82
83
  - lib/sqlreport.rb
84
+ - lib/sqlreport/active_record_extension.rb
83
85
  - lib/sqlreport/base.rb
86
+ - lib/sqlreport/batch_manager.rb
84
87
  - lib/sqlreport/result.rb
85
88
  - lib/sqlreport/version.rb
86
89
  - sig/sqlreport.rbs