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 +4 -4
- data/README.md +58 -20
- data/lib/simple_query/builder.rb +34 -0
- data/lib/simple_query/clauses/set_clause.rb +25 -0
- data/lib/simple_query/stream/mysql_stream.rb +36 -0
- data/lib/simple_query/stream/postgres_stream.rb +59 -0
- data/lib/simple_query/version.rb +1 -1
- data/lib/simple_query.rb +3 -0
- metadata +7 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a62c7c2fe2d6c43a27b9cc2dd22e7ddcf14a01f9ca335144b472db1559f4b0a
|
4
|
+
data.tar.gz: 172eddfa79634dcf78d3547031af80a9f1e008dc9e0414c0f5671c66b4264617
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
##
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
-
|
184
|
-
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
-
|
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
|
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 (
|
199
|
-
ActiveRecord Query:
|
200
|
-
SimpleQuery Execution (Struct):
|
201
|
-
SimpleQuery Execution (Read model):
|
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
|
-
-
|
204
|
-
-
|
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
|
|
data/lib/simple_query/builder.rb
CHANGED
@@ -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
|
data/lib/simple_query/version.rb
CHANGED
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.
|
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-
|
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
|
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: []
|