kweerie 0.1.3 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +31 -1
- data/README.md +132 -22
- data/lib/kweerie/base.rb +158 -14
- data/lib/kweerie/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b47a2f9363e6fb03e19af88b8d34ab9d727e45e2ed7f05e95d41d23835f93693
|
4
|
+
data.tar.gz: cb868fa5858008ce6e2ec14188e81c41fb0ad554e85e491b71ebe625d357257e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f9927adf42ec5fed6e65ea6c5a83a3166ea10ccadbe9d802a2d2bb6cc02647365df44865052566a6b255fa8dbb91be74f406a0fc61993982589222b2724724a
|
7
|
+
data.tar.gz: '08acf2f917e887a25e0c22aebd493aa861c1bc0fa827e80983ee577f352d1048efabb2285b152cb1d141ccf8846e34071828f4b77960d0b1b51909fb279fcd85'
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
AllCops:
|
2
|
-
TargetRubyVersion:
|
2
|
+
TargetRubyVersion: 3.3
|
3
3
|
|
4
4
|
Style/StringLiterals:
|
5
5
|
Enabled: true
|
@@ -11,3 +11,33 @@ Style/StringLiteralsInInterpolation:
|
|
11
11
|
|
12
12
|
Layout/LineLength:
|
13
13
|
Max: 120
|
14
|
+
|
15
|
+
Style/Documentation:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Metrics/ClassLength:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Metrics/CyclomaticComplexity:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics/MethodLength:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Metrics/AbcSize:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Style/MultilineBlockChain:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Metrics/PerceivedComplexity:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Metrics/AbcSize:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Gemspec/RequiredRubyVersion:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Metrics/BlockLength:
|
43
|
+
Enabled: false
|
data/README.md
CHANGED
@@ -47,10 +47,10 @@ Execute your query:
|
|
47
47
|
|
48
48
|
```ruby
|
49
49
|
results = UserSearch.with(
|
50
|
-
name: '
|
50
|
+
name: 'Eclipsoid%',
|
51
51
|
email: '%@example.com'
|
52
52
|
)
|
53
|
-
# => [{"id"=>
|
53
|
+
# => [{"id"=>109981, "name"=>"Eclipsoid Doe", "email"=>"eclipsoiddoe@example.com"}]
|
54
54
|
```
|
55
55
|
|
56
56
|
### Object Mapping
|
@@ -65,15 +65,105 @@ end
|
|
65
65
|
|
66
66
|
# Returns array of objects instead of hashes
|
67
67
|
users = UserSearch.with(
|
68
|
-
name: '
|
68
|
+
name: 'Eclipsoid',
|
69
69
|
created_at: '2024-01-01'
|
70
70
|
)
|
71
71
|
|
72
72
|
user = users.first
|
73
|
-
user.name # => "
|
73
|
+
user.name # => "Eclipsoid"
|
74
74
|
user.created_at # => 2024-01-01 00:00:00 +0000 (Time object)
|
75
75
|
```
|
76
76
|
|
77
|
+
## Querying
|
78
|
+
|
79
|
+
Kweerie provides two main ways to execute queries based on whether they have parameters:
|
80
|
+
|
81
|
+
### Parameterized Queries
|
82
|
+
|
83
|
+
When your query needs parameters, use the `.with` method:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
class UsersByDepartment < Kweerie::Base
|
87
|
+
bind :department, as: '$1'
|
88
|
+
bind :active, as: '$2'
|
89
|
+
end
|
90
|
+
|
91
|
+
# app/queries/users_by_department.sql
|
92
|
+
SELECT *
|
93
|
+
FROM users
|
94
|
+
WHERE department = $1
|
95
|
+
AND active = $2;
|
96
|
+
|
97
|
+
# Using the query
|
98
|
+
users = UsersByDepartment.with(
|
99
|
+
department: 'Engineering',
|
100
|
+
active: true
|
101
|
+
)
|
102
|
+
```
|
103
|
+
|
104
|
+
### Parameter-free Queries
|
105
|
+
|
106
|
+
For queries that don't require any parameters, you can use the more semantically appropriate `.all` method:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class AllUsers < Kweerie::Base
|
110
|
+
end
|
111
|
+
|
112
|
+
# app/queries/all_users.sql
|
113
|
+
SELECT *
|
114
|
+
FROM users
|
115
|
+
WHERE active = true
|
116
|
+
ORDER BY created_at DESC;
|
117
|
+
|
118
|
+
# Using the query
|
119
|
+
users = AllUsers.all
|
120
|
+
```
|
121
|
+
|
122
|
+
The `.all` method provides a cleaner interface when you're not binding any parameters. It will raise an error if you try to use it on a query class that has parameter bindings:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# This will raise an ArgumentError
|
126
|
+
UsersByDepartment.all
|
127
|
+
# => ArgumentError: Cannot use .all on queries with bindings. Use .with instead.
|
128
|
+
```
|
129
|
+
|
130
|
+
### Choosing Between .all and .with
|
131
|
+
|
132
|
+
- Use `.all` when your SQL query is completely static with no parameters
|
133
|
+
- Use `.with` when you need to pass parameters to your query
|
134
|
+
- Even for parameterized queries, you can use `.with` without arguments if all parameters are optional
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
# A query with no parameters
|
138
|
+
class RecentUsers < Kweerie::Base
|
139
|
+
end
|
140
|
+
RecentUsers.all # ✓ Clean and semantic
|
141
|
+
RecentUsers.with # ✓ Works but less semantic
|
142
|
+
|
143
|
+
# A query with parameters
|
144
|
+
class UsersByStatus < Kweerie::Base
|
145
|
+
bind :status, as: '$1'
|
146
|
+
end
|
147
|
+
UsersByStatus.all # ✗ Raises ArgumentError
|
148
|
+
UsersByStatus.with(status: 'active') # ✓ Correct usage
|
149
|
+
```
|
150
|
+
|
151
|
+
Both methods work with `Kweerie::Base` and `Kweerie::BaseObjects`, returning arrays of hashes or objects respectively:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
# Returns array of hashes
|
155
|
+
class AllUsers < Kweerie::Base
|
156
|
+
end
|
157
|
+
users = AllUsers.all
|
158
|
+
# => [{"id" => 1, "name" => "Eclipsoid"}, ...]
|
159
|
+
|
160
|
+
# Returns array of objects
|
161
|
+
class AllUsers < Kweerie::BaseObjects
|
162
|
+
end
|
163
|
+
users = AllUsers.all
|
164
|
+
# => [#<AllUsers id=1 name="Eclipsoid">, ...]
|
165
|
+
```
|
166
|
+
|
77
167
|
### Automatic Type Casting
|
78
168
|
|
79
169
|
BaseObjects automatically casts common PostgreSQL types to their Ruby equivalents:
|
@@ -91,7 +181,7 @@ SELECT
|
|
91
181
|
FROM users;
|
92
182
|
|
93
183
|
# In your Ruby code
|
94
|
-
user = UserSearch.with(name: '
|
184
|
+
user = UserSearch.with(name: 'Eclipsoid').first
|
95
185
|
|
96
186
|
user.created_at # => Time object
|
97
187
|
user.age # => Integer
|
@@ -130,7 +220,7 @@ BaseObjects provide several useful methods:
|
|
130
220
|
|
131
221
|
```ruby
|
132
222
|
# Hash-like access
|
133
|
-
user[:name] # => "
|
223
|
+
user[:name] # => "Eclipsoid"
|
134
224
|
user.fetch(:email, 'N/A') # => Returns 'N/A' if email is nil
|
135
225
|
|
136
226
|
# Serialization
|
@@ -147,7 +237,7 @@ user.changes # => Hash of changes with [old, new] values
|
|
147
237
|
user.original_attributes # => Original attributes from DB
|
148
238
|
```
|
149
239
|
|
150
|
-
|
240
|
+
### PostgreSQL Array Support
|
151
241
|
|
152
242
|
BaseObjects handles PostgreSQL arrays by converting them to Ruby arrays with proper type casting:
|
153
243
|
|
@@ -160,14 +250,39 @@ create_table :users do |t|
|
|
160
250
|
end
|
161
251
|
|
162
252
|
# In your query
|
163
|
-
user = UserSearch.with(name: '
|
253
|
+
user = UserSearch.with(name: 'Eclipsoid').first
|
164
254
|
|
165
255
|
user.preferred_ordering # => [1, 3, 2]
|
166
256
|
user.tags # => ["ruby", "rails"]
|
167
257
|
user.scores # => [98.5, 87.2, 92.0]
|
168
258
|
```
|
169
259
|
|
170
|
-
|
260
|
+
### SQL File Location
|
261
|
+
|
262
|
+
By default, Kweerie looks for SQL files adjacent to their Ruby query classes. You can customize this behavior:
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
# Default behavior - looks for user_search.sql next to this file
|
266
|
+
class UserSearch < Kweerie::Base
|
267
|
+
end
|
268
|
+
|
269
|
+
# Specify absolute path from Rails root
|
270
|
+
class UserSearch < Kweerie::Base
|
271
|
+
sql_file_location root: 'db/queries/complex_user_search.sql'
|
272
|
+
end
|
273
|
+
|
274
|
+
# Specify path relative to the Ruby file
|
275
|
+
class UserSearch < Kweerie::Base
|
276
|
+
sql_file_location relative: '../sql/user_search.sql'
|
277
|
+
end
|
278
|
+
|
279
|
+
# Explicitly use default behavior
|
280
|
+
class UserSearch < Kweerie::Base
|
281
|
+
sql_file_location :default
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
### Performance Considerations
|
171
286
|
|
172
287
|
BaseObjects creates a unique class for each query result set, with the following optimizations:
|
173
288
|
|
@@ -179,7 +294,7 @@ BaseObjects creates a unique class for each query result set, with the following
|
|
179
294
|
|
180
295
|
For queries where you don't need the object interface, use `Kweerie::Base` instead for slightly better performance.
|
181
296
|
|
182
|
-
|
297
|
+
## Rails Generator
|
183
298
|
|
184
299
|
If you're using Rails, you can use the generator to create new query files:
|
185
300
|
|
@@ -193,7 +308,7 @@ rails generate kweerie UserSearch email name
|
|
193
308
|
|
194
309
|
This will create both the Ruby class and SQL file with the appropriate structure.
|
195
310
|
|
196
|
-
|
311
|
+
## Configuration
|
197
312
|
|
198
313
|
By default, Kweerie uses ActiveRecord's connection if available. You can configure this and other options:
|
199
314
|
|
@@ -222,19 +337,11 @@ end
|
|
222
337
|
- ✅ Configurable connection handling
|
223
338
|
- ✅ Parameter validation
|
224
339
|
|
225
|
-
|
226
|
-
|
227
|
-
- **SQL Views Overkill**: When a database view is too heavy-handed but you still want to keep SQL separate from Ruby
|
228
|
-
- **Version Control**: Keep your SQL under version control alongside your Ruby code
|
229
|
-
- **Parameter Safety**: Built-in parameter binding prevents SQL injection
|
230
|
-
- **Simple Interface**: Clean, simple API for executing parameterized queries
|
231
|
-
- **Rails Integration**: Works seamlessly with Rails and ActiveRecord
|
232
|
-
|
233
|
-
## Development
|
340
|
+
### Development
|
234
341
|
|
235
342
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
236
343
|
|
237
|
-
|
344
|
+
### Contributing
|
238
345
|
|
239
346
|
1. Fork it
|
240
347
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
@@ -242,12 +349,15 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
242
349
|
4. Push to the branch (`git push origin my-new-feature`)
|
243
350
|
5. Create a new Pull Request
|
244
351
|
|
245
|
-
|
352
|
+
### License
|
246
353
|
|
247
354
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
248
355
|
|
249
356
|
## FAQ
|
250
357
|
|
358
|
+
**Q: Why does Kweerie exist?**
|
359
|
+
A: PostgreSQL DB views are powerful and (honestly) preferred for what kweerie offers, but they need migrations to change, which isn't always practical when you just need a query. Kweerie provides that flexibility. Plus, SQL is more readable in a single file than when nested in Ruby code. SQL is powerful and doesn’t always need to be hidden behind abstractions.
|
360
|
+
|
251
361
|
**Q: Why PostgreSQL only?**
|
252
362
|
A: Kweerie uses PostgreSQL-specific features for parameter binding and result handling. Supporting other databases would require different parameter binding syntax and result handling.
|
253
363
|
|
data/lib/kweerie/base.rb
CHANGED
@@ -5,37 +5,175 @@ module Kweerie
|
|
5
5
|
class << self
|
6
6
|
def inherited(subclass)
|
7
7
|
subclass.instance_variable_set(:@bindings, {})
|
8
|
+
subclass.instance_variable_set(:@class_location, caller_locations(1, 1)[0].path)
|
8
9
|
super
|
9
10
|
end
|
10
11
|
|
12
|
+
# == Parameter Binding
|
13
|
+
#
|
14
|
+
# Binds a parameter to a SQL placeholder. Parameters are required by default and
|
15
|
+
# must be provided when executing the query.
|
16
|
+
#
|
17
|
+
# === Options
|
18
|
+
#
|
19
|
+
# * <tt>:as</tt> - The SQL placeholder (e.g., '$1', '$2') that this parameter maps to
|
20
|
+
#
|
21
|
+
# === Examples
|
22
|
+
#
|
23
|
+
# class UserSearch < Kweerie::Base
|
24
|
+
# # Single parameter
|
25
|
+
# bind :name, as: '$1'
|
26
|
+
#
|
27
|
+
# # Multiple parameters
|
28
|
+
# bind :email, as: '$2'
|
29
|
+
# bind :status, as: '$3'
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # Using the query
|
33
|
+
# UserSearch.with(
|
34
|
+
# name: 'Eclipsoid',
|
35
|
+
# email: '%@example.com',
|
36
|
+
# status: 'active'
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# === Notes
|
40
|
+
#
|
41
|
+
# * Parameters must be provided in the order they appear in the SQL
|
42
|
+
# * All bound parameters are required
|
43
|
+
# * Use PostgreSQL's COALESCE for optional parameters
|
44
|
+
#
|
11
45
|
def bind(param_name, as:)
|
12
46
|
@bindings[param_name] = as
|
13
47
|
end
|
14
48
|
|
15
49
|
attr_reader :bindings
|
16
50
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
51
|
+
# == SQL File Location
|
52
|
+
#
|
53
|
+
# Specifies the location of the SQL file for this query. By default, Kweerie looks for
|
54
|
+
# an SQL file with the same name as the query class in the same directory.
|
55
|
+
#
|
56
|
+
# === Options
|
57
|
+
#
|
58
|
+
# * <tt>:default</tt> - Use default file naming (class_name.sql in same directory)
|
59
|
+
# * <tt>root: 'path'</tt> - Path relative to Rails.root
|
60
|
+
# * <tt>relative: 'path'</tt> - Path relative to the query class file
|
61
|
+
#
|
62
|
+
# === Examples
|
63
|
+
#
|
64
|
+
# class UserSearch < Kweerie::Base
|
65
|
+
# # Default behavior - looks for user_search.sql in same directory
|
66
|
+
# sql_file_location :default
|
67
|
+
#
|
68
|
+
# # Use a specific file from Rails root
|
69
|
+
# sql_file_location root: 'db/queries/complex_user_search.sql'
|
70
|
+
#
|
71
|
+
# # Use a file relative to this class
|
72
|
+
# sql_file_location relative: '../sql/user_search.sql'
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# === Notes
|
76
|
+
#
|
77
|
+
# * Root paths require Rails to be defined
|
78
|
+
# * Paths should use forward slashes even on Windows
|
79
|
+
# * File extensions should be included in the path
|
80
|
+
# * Relative paths are relative to the query class file location
|
81
|
+
#
|
82
|
+
def sql_file_location(location = :default)
|
83
|
+
@sql_file_location =
|
84
|
+
case location
|
85
|
+
when :default
|
86
|
+
nil
|
87
|
+
when Hash
|
88
|
+
if location.key?(:root)
|
89
|
+
{ root: location[:root].to_s }
|
90
|
+
elsif location.key?(:relative)
|
91
|
+
{ relative: location[:relative].to_s }
|
92
|
+
else
|
93
|
+
raise ArgumentError,
|
94
|
+
"Invalid sql_file_location option. Use :default, root: 'path', or relative: 'path'"
|
95
|
+
end
|
96
|
+
else
|
97
|
+
raise ArgumentError,
|
98
|
+
"Invalid sql_file_location option. Use :default, root: 'path', or relative: 'path'"
|
29
99
|
end
|
100
|
+
end
|
30
101
|
|
31
|
-
|
32
|
-
|
102
|
+
def sql_path
|
103
|
+
@sql_path ||=
|
104
|
+
if @sql_file_location&.key?(:root)
|
105
|
+
raise ConfigurationError, "Root path requires Rails to be defined" unless defined?(Rails)
|
106
|
+
|
107
|
+
path = Rails.root.join(@sql_file_location[:root]).to_s
|
108
|
+
raise SQLFileNotFound, "Could not find SQL file at #{path}" unless File.exist?(path)
|
109
|
+
|
110
|
+
path
|
111
|
+
|
112
|
+
elsif @sql_file_location&.key?(:relative)
|
113
|
+
configured_paths = Kweerie.configuration.sql_paths.call
|
114
|
+
sql_file = configured_paths.map do |path|
|
115
|
+
full_path = File.join(path, @sql_file_location[:relative])
|
116
|
+
full_path if File.exist?(full_path)
|
117
|
+
end.compact.first
|
118
|
+
unless sql_file
|
119
|
+
raise SQLFileNotFound,
|
120
|
+
"Could not find SQL file #{@sql_file_location[:relative]}"
|
121
|
+
end
|
122
|
+
|
123
|
+
sql_file
|
124
|
+
else # default behavior
|
125
|
+
sql_filename = "#{name.underscore}.sql"
|
126
|
+
configured_paths = Kweerie.configuration.sql_paths.call
|
127
|
+
|
128
|
+
sql_file = configured_paths.map do |path|
|
129
|
+
full_path = File.join(path, sql_filename)
|
130
|
+
full_path if File.exist?(full_path)
|
131
|
+
end.compact.first
|
132
|
+
|
133
|
+
raise SQLFileNotFound, "SQL file not found for #{name}" unless sql_file
|
134
|
+
|
135
|
+
sql_file
|
136
|
+
end
|
33
137
|
end
|
34
138
|
|
35
139
|
def sql_content
|
36
140
|
@sql_content ||= File.read(sql_path)
|
37
141
|
end
|
38
142
|
|
143
|
+
# == Execute Query with Parameters
|
144
|
+
#
|
145
|
+
# Executes the SQL query with the provided parameters. All bound parameters must be provided
|
146
|
+
# unless using .all for parameter-free queries.
|
147
|
+
#
|
148
|
+
# === Parameters
|
149
|
+
#
|
150
|
+
# * <tt>params</tt> - Hash of parameter names and values that match the bound parameters
|
151
|
+
#
|
152
|
+
# === Returns
|
153
|
+
#
|
154
|
+
# Array of hashes representing the query results. When using Kweerie::BaseObjects,
|
155
|
+
# returns array of typed objects instead.
|
156
|
+
#
|
157
|
+
# === Examples
|
158
|
+
#
|
159
|
+
# # With parameters
|
160
|
+
# UserSearch.with(
|
161
|
+
# name: 'Eclipsoid',
|
162
|
+
# email: '%@example.com'
|
163
|
+
# )
|
164
|
+
# # => [{"id"=>1, "name"=>"Eclipsoid", "email"=>"eclipsoid@example.com"}]
|
165
|
+
#
|
166
|
+
# # With type casting (BaseObjects)
|
167
|
+
# UserSearch.with(created_after: '2024-01-01')
|
168
|
+
# # => [#<UserSearch id=1 created_at=2024-01-01 00:00:00 +0000>]
|
169
|
+
#
|
170
|
+
# === Notes
|
171
|
+
#
|
172
|
+
# * Raises ArgumentError if required parameters are missing
|
173
|
+
# * Raises ArgumentError if extra parameters are provided
|
174
|
+
# * Returns empty array if no results found
|
175
|
+
# * Parameters are bound safely using pg-ruby's parameter binding
|
176
|
+
#
|
39
177
|
def with(params = {})
|
40
178
|
validate_params!(params)
|
41
179
|
param_values = order_params(params)
|
@@ -45,6 +183,12 @@ module Kweerie
|
|
45
183
|
result.to_a
|
46
184
|
end
|
47
185
|
|
186
|
+
def all
|
187
|
+
raise ArgumentError, "Cannot use .all on queries with bindings. Use .with instead." if bindings.any?
|
188
|
+
|
189
|
+
with
|
190
|
+
end
|
191
|
+
|
48
192
|
private
|
49
193
|
|
50
194
|
def validate_params!(params)
|
data/lib/kweerie/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kweerie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Toby
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|