simple_query 0.3.2 → 0.4.0

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: 6aeb71d493d14743e43b8c9a37eed697f131db105ffc4dc0fa35dd0dd189255b
4
- data.tar.gz: 82b1815e569f24a4c4da9e4cd727d95ad6c938403bd2b781eccfc39398cb264b
3
+ metadata.gz: 8a62c7c2fe2d6c43a27b9cc2dd22e7ddcf14a01f9ca335144b472db1559f4b0a
4
+ data.tar.gz: 172eddfa79634dcf78d3547031af80a9f1e008dc9e0414c0f5671c66b4264617
5
5
  SHA512:
6
- metadata.gz: d422a6a3484d4242b6666bb6536f4828b666c0f16dd6a0c42104b12e3f077bd30e580dc9cf5eb4292ecbb1cd0eda218aabe29af6e221a86c3291fc116b62b856
7
- data.tar.gz: cf6e7c8a7e198d1b5376053237d7bdfac59819e07571adb5656e859de9a3bb840dace69e2e600703b14a5ff01bfedbf448d1aadb471a3875874c7d390599878a
6
+ metadata.gz: abbd8b5034e7b3a9a72b5de79f0d522a1ba2ac2b066bcc78387fb068ad9647b685c006318f6efafd31b5b616b48bd18ea677a5c74e7c25647a38c062a934109b
7
+ data.tar.gz: e499597efd33f703d136b0e053006ef109dea762e19148acd6bb5dcf6717f14f8b63845d61a3ea7b770353dd37e17178ca8b51ef7785e0714828ee78bda1fb8e
data/README.md CHANGED
@@ -176,32 +176,70 @@ Each scope block (e.g. by_name) is evaluated in the context of the SimpleQuery b
176
176
  Parameterized scopes accept arguments — passed directly to the block (e.g. |name| above).
177
177
  Scopes return self, so you can chain multiple scopes or mix them with standard query methods.
178
178
 
179
- ## Features
180
-
181
- - Efficient query building
182
- - Support for complex joins
183
- - Lazy execution
184
- - DISTINCT queries
185
- - Aggregations
186
- - LIMIT and OFFSET
187
- - ORDER BY clause
188
- - Having and Grouping
189
- - Subqueries
190
- - Custom Read models
191
- - Named Scopes
179
+ ## Streaming Large Datasets
180
+
181
+ For massive queries (millions of rows), **SimpleQuery** offers a `.stream_each` method to avoid loading the entire result set into memory. It **automatically** picks a streaming approach depending on your database adapter:
182
+
183
+ - **PostgreSQL**: Uses a **server-side cursor** via `DECLARE ... FETCH`.
184
+ - **MySQL**: Uses `mysql2` gem’s **streaming** (`stream: true, cache_rows: false, as: :hash`).
185
+
186
+ ```ruby
187
+ # Example usage:
188
+ User.simple_query
189
+ .where(active: true)
190
+ .stream_each(batch_size: 10_000) do |row|
191
+ # row is a struct or read-model instance
192
+ puts row.name
193
+ end
194
+ ```
192
195
 
193
196
  ## Performance
194
197
 
195
- SimpleQuery is designed to potentially outperform standard ActiveRecord queries on large datasets. In our benchmarks with 100,000 records, SimpleQuery showed improved performance compared to equivalent ActiveRecord queries.
198
+ SimpleQuery aims to outperform standard ActiveRecord queries at scale. We’ve benchmarked **1,000,000** records on **both PostgreSQL** and **MySQL**, with the following results:
199
+
200
+ ### PostgreSQL (1,000,000 records)
201
+ ```
202
+ 🚀 Performance Results (1000,000 records):
203
+ ActiveRecord Query: 10.36932 seconds
204
+ SimpleQuery Execution (Struct): 3.46136 seconds
205
+ SimpleQuery Execution (Read model): 2.20905 seconds
206
+
207
+ ----------------------------------------------------
208
+ ActiveRecord find_each: 6.10077 seconds
209
+ SimpleQuery stream_each: 2.75639 seconds
210
+
211
+ --- AR find_each Memory Report ---
212
+ Total allocated: 1.98 GB (16,001,659 objects)
213
+ Retained: ~2 KB
214
+
215
+ --- SimpleQuery stream_each Memory Report ---
216
+ Total allocated: 1.38 GB (8,000,211 objects)
217
+ Retained: ~3 KB
218
+ ```
219
+ - **Struct-based** approach remains the fastest, skipping model overhead.
220
+ - **Read model** approach is still significantly faster than standard ActiveRecord while allowing domain-specific logic.
196
221
 
222
+ ### MySQL (1,000,000 records)
197
223
  ```
198
- 🚀 Performance Results (100,000 records):
199
- ActiveRecord Query: 0.47441 seconds
200
- SimpleQuery Execution (Struct): 0.05346 seconds
201
- SimpleQuery Execution (Read model): 0.14408 seconds
224
+ 🚀 Performance Results (1000,000 records):
225
+ ActiveRecord Query: 10.45833 seconds
226
+ SimpleQuery Execution (Struct): 3.04655 seconds
227
+ SimpleQuery Execution (Read model): 3.69052 seconds
228
+
229
+ ----------------------------------------------------
230
+ ActiveRecord find_each: 5.04671 seconds
231
+ SimpleQuery stream_each: 2.96602 seconds
232
+
233
+ --- AR find_each Memory Report ---
234
+ Total allocated: 1.32 GB (11,001,445 objects)
235
+ Retained: ~2.7 KB
236
+
237
+ --- SimpleQuery stream_each Memory Report ---
238
+ Total allocated: 1.22 GB (8,000,068 objects)
239
+ Retained: ~3.9 KB
202
240
  ```
203
- - The **Struct-based** approach is the fastest.
204
- - The **Read model** approach is still significantly faster than ActiveRecord, while letting you define custom logic or domain-specific attributes.
241
+ - Even in MySQL, **Struct** was roughly **three times faster** than ActiveRecord’s overhead.
242
+ - Read models still outperform AR, though by a narrower margin in this scenario.
205
243
 
206
244
  ## Development
207
245
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  module SimpleQuery
4
4
  class Builder
5
+ include SimpleQuery::Stream::PostgresStream
6
+ include SimpleQuery::Stream::MysqlStream
7
+
5
8
  attr_reader :model, :arel_table
6
9
 
7
10
  def initialize(source)
@@ -94,6 +97,30 @@ module SimpleQuery
94
97
  self
95
98
  end
96
99
 
100
+ def bulk_update(set:)
101
+ table_name = @arel_table.name
102
+ set_sql = SetClause.new(set).to_sql
103
+
104
+ raise ArgumentError, "No columns to update" if set_sql.empty?
105
+
106
+ where_sql = build_where_sql
107
+ sql = "UPDATE #{table_name} SET #{set_sql}"
108
+ sql += " WHERE #{where_sql}" unless where_sql.nil? || where_sql.empty?
109
+
110
+ ActiveRecord::Base.connection.execute(sql)
111
+ end
112
+
113
+ def stream_each(batch_size: 1000, &block)
114
+ adapter = ActiveRecord::Base.connection.adapter_name.downcase
115
+ if adapter.include?("postgres")
116
+ stream_each_postgres(batch_size, &block)
117
+ elsif adapter.include?("mysql")
118
+ stream_each_mysql(&block)
119
+ else
120
+ raise "stream_each is only implemented for Postgres and MySQL."
121
+ end
122
+ end
123
+
97
124
  def execute
98
125
  records = ActiveRecord::Base.connection.select_all(cached_sql)
99
126
  build_result_objects_from_rows(records)
@@ -133,6 +160,13 @@ module SimpleQuery
133
160
 
134
161
  private
135
162
 
163
+ def build_where_sql
164
+ condition = @wheres.to_arel
165
+ return "" unless condition
166
+
167
+ condition.to_sql
168
+ end
169
+
136
170
  def reset_query
137
171
  @query_built = false
138
172
  @query_cache.clear
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleQuery
4
+ class SetClause
5
+ def initialize(set_hash)
6
+ @set_hash = set_hash
7
+ end
8
+
9
+ def to_sql
10
+ @set_hash.map do |col, val|
11
+ "#{quote_column(col)} = #{quote_value(val)}"
12
+ end.join(", ")
13
+ end
14
+
15
+ private
16
+
17
+ def quote_column(col)
18
+ ActiveRecord::Base.connection.quote_column_name(col)
19
+ end
20
+
21
+ def quote_value(val)
22
+ ActiveRecord::Base.connection.quote(val)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleQuery
4
+ module Stream
5
+ module MysqlStream
6
+ def stream_each_mysql(&block)
7
+ select_sql = cached_sql
8
+
9
+ raw_conn = ActiveRecord::Base.connection.raw_connection
10
+
11
+ result = raw_conn.query(select_sql, stream: true, cache_rows: false, as: :hash)
12
+ result.each do |mysql_row|
13
+ record = build_row_object_mysql(mysql_row)
14
+ block.call(record)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def build_row_object_mysql(mysql_row)
21
+ if @read_model_class
22
+ obj = @read_model_class.allocate
23
+ @read_model_class.attributes.each do |attr_name, col_name|
24
+ obj.instance_variable_set(:"@#{attr_name}", mysql_row[col_name])
25
+ end
26
+ obj
27
+ else
28
+ columns = mysql_row.keys
29
+ values = columns.map { |k| mysql_row[k] }
30
+ struct = result_struct(columns)
31
+ struct.new(*values)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleQuery
4
+ module Stream
5
+ module PostgresStream
6
+ # rubocop:disable Metrics/MethodLength
7
+ def stream_each_postgres(batch_size, &block)
8
+ select_sql = cached_sql
9
+
10
+ conn = ActiveRecord::Base.connection.raw_connection
11
+ cursor_name = "simple_query_cursor_#{object_id}"
12
+
13
+ begin
14
+ conn.exec("BEGIN")
15
+ declare_sql = "DECLARE #{cursor_name} NO SCROLL CURSOR FOR #{select_sql}"
16
+ conn.exec(declare_sql)
17
+
18
+ loop do
19
+ res = conn.exec("FETCH #{batch_size} FROM #{cursor_name}")
20
+ break if res.ntuples.zero?
21
+
22
+ res.each do |pg_row|
23
+ record = build_row_object(pg_row)
24
+ block.call(record)
25
+ end
26
+ end
27
+
28
+ conn.exec("CLOSE #{cursor_name}")
29
+ conn.exec("COMMIT")
30
+ rescue StandardError => e
31
+ begin
32
+ conn.exec("ROLLBACK")
33
+ rescue StandardError
34
+ nil
35
+ end
36
+ raise e
37
+ end
38
+ end
39
+ # rubocop:enable Metrics/MethodLength
40
+
41
+ private
42
+
43
+ def build_row_object(pg_row)
44
+ if @read_model_class
45
+ obj = @read_model_class.allocate
46
+ @read_model_class.attributes.each do |attr_name, col_name|
47
+ obj.instance_variable_set(:"@#{attr_name}", pg_row[col_name])
48
+ end
49
+ obj
50
+ else
51
+ columns = pg_row.keys
52
+ values = columns.map { |k| pg_row[k] }
53
+ struct = result_struct(columns)
54
+ struct.new(*values)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleQuery
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/simple_query.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require "active_support/concern"
4
4
  require "active_record"
5
5
 
6
+ require_relative "simple_query/stream/mysql_stream"
7
+ require_relative "simple_query/stream/postgres_stream"
6
8
  require_relative "simple_query/builder"
7
9
  require_relative "simple_query/read_model"
8
10
  require_relative "simple_query/clauses/where_clause"
@@ -11,6 +13,7 @@ require_relative "simple_query/clauses/order_clause"
11
13
  require_relative "simple_query/clauses/distinct_clause"
12
14
  require_relative "simple_query/clauses/limit_offset_clause"
13
15
  require_relative "simple_query/clauses/group_having_clause"
16
+ require_relative "simple_query/clauses/set_clause"
14
17
 
15
18
  module SimpleQuery
16
19
  extend ActiveSupport::Concern
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_query
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Kholodniak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-01 00:00:00.000000000 Z
11
+ date: 2025-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -30,62 +30,6 @@ dependencies:
30
30
  - - "<="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '8.0'
33
- - !ruby/object:Gem::Dependency
34
- name: rake
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '13.0'
40
- type: :development
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '13.0'
47
- - !ruby/object:Gem::Dependency
48
- name: rspec
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '3.0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '3.0'
61
- - !ruby/object:Gem::Dependency
62
- name: rubocop
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '1.21'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '1.21'
75
- - !ruby/object:Gem::Dependency
76
- name: sqlite3
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: 1.5.0
82
- type: :development
83
- prerelease: false
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: 1.5.0
89
33
  description: SimpleQuery provides a flexible and performant way to construct complex
90
34
  database queries in Ruby on Rails applications. It offers an intuitive interface
91
35
  for building queries with joins, conditions, and aggregations, while potentially
@@ -105,8 +49,11 @@ files:
105
49
  - lib/simple_query/clauses/join_clause.rb
106
50
  - lib/simple_query/clauses/limit_offset_clause.rb
107
51
  - lib/simple_query/clauses/order_clause.rb
52
+ - lib/simple_query/clauses/set_clause.rb
108
53
  - lib/simple_query/clauses/where_clause.rb
109
54
  - lib/simple_query/read_model.rb
55
+ - lib/simple_query/stream/mysql_stream.rb
56
+ - lib/simple_query/stream/postgres_stream.rb
110
57
  - lib/simple_query/version.rb
111
58
  homepage: https://github.com/kholdrex/simple_query
112
59
  licenses:
@@ -133,5 +80,6 @@ requirements: []
133
80
  rubygems_version: 3.5.9
134
81
  signing_key:
135
82
  specification_version: 4
136
- summary: A lightweight and efficient query builder for ActiveRecord.
83
+ summary: A lightweight, multi-DB-friendly, and high-performance query builder for
84
+ ActiveRecord, featuring streaming, bulk updates, and read-model support.
137
85
  test_files: []