kweerie 0.1.4 → 0.1.5
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/.rubocop.yml +31 -1
- data/README.md +37 -17
- data/lib/kweerie/base.rb +152 -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,12 +65,12 @@ 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
|
|
|
@@ -155,13 +155,13 @@ Both methods work with `Kweerie::Base` and `Kweerie::BaseObjects`, returning arr
|
|
|
155
155
|
class AllUsers < Kweerie::Base
|
|
156
156
|
end
|
|
157
157
|
users = AllUsers.all
|
|
158
|
-
# => [{"id" => 1, "name" => "
|
|
158
|
+
# => [{"id" => 1, "name" => "Eclipsoid"}, ...]
|
|
159
159
|
|
|
160
160
|
# Returns array of objects
|
|
161
161
|
class AllUsers < Kweerie::BaseObjects
|
|
162
162
|
end
|
|
163
163
|
users = AllUsers.all
|
|
164
|
-
# => [#<AllUsers id=1 name="
|
|
164
|
+
# => [#<AllUsers id=1 name="Eclipsoid">, ...]
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
### Automatic Type Casting
|
|
@@ -181,7 +181,7 @@ SELECT
|
|
|
181
181
|
FROM users;
|
|
182
182
|
|
|
183
183
|
# In your Ruby code
|
|
184
|
-
user = UserSearch.with(name: '
|
|
184
|
+
user = UserSearch.with(name: 'Eclipsoid').first
|
|
185
185
|
|
|
186
186
|
user.created_at # => Time object
|
|
187
187
|
user.age # => Integer
|
|
@@ -220,7 +220,7 @@ BaseObjects provide several useful methods:
|
|
|
220
220
|
|
|
221
221
|
```ruby
|
|
222
222
|
# Hash-like access
|
|
223
|
-
user[:name] # => "
|
|
223
|
+
user[:name] # => "Eclipsoid"
|
|
224
224
|
user.fetch(:email, 'N/A') # => Returns 'N/A' if email is nil
|
|
225
225
|
|
|
226
226
|
# Serialization
|
|
@@ -250,13 +250,38 @@ create_table :users do |t|
|
|
|
250
250
|
end
|
|
251
251
|
|
|
252
252
|
# In your query
|
|
253
|
-
user = UserSearch.with(name: '
|
|
253
|
+
user = UserSearch.with(name: 'Eclipsoid').first
|
|
254
254
|
|
|
255
255
|
user.preferred_ordering # => [1, 3, 2]
|
|
256
256
|
user.tags # => ["ruby", "rails"]
|
|
257
257
|
user.scores # => [98.5, 87.2, 92.0]
|
|
258
258
|
```
|
|
259
259
|
|
|
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
|
+
|
|
260
285
|
### Performance Considerations
|
|
261
286
|
|
|
262
287
|
BaseObjects creates a unique class for each query result set, with the following optimizations:
|
|
@@ -312,14 +337,6 @@ end
|
|
|
312
337
|
- ✅ Configurable connection handling
|
|
313
338
|
- ✅ Parameter validation
|
|
314
339
|
|
|
315
|
-
### Why Kweerie?
|
|
316
|
-
|
|
317
|
-
- **SQL Views Overkill**: When a database view is too heavy-handed but you still want to keep SQL separate from Ruby
|
|
318
|
-
- **Version Control**: Keep your SQL under version control alongside your Ruby code
|
|
319
|
-
- **Parameter Safety**: Built-in parameter binding prevents SQL injection
|
|
320
|
-
- **Simple Interface**: Clean, simple API for executing parameterized queries
|
|
321
|
-
- **Rails Integration**: Works seamlessly with Rails and ActiveRecord
|
|
322
|
-
|
|
323
340
|
### Development
|
|
324
341
|
|
|
325
342
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
@@ -338,6 +355,9 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
338
355
|
|
|
339
356
|
## FAQ
|
|
340
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
|
+
|
|
341
361
|
**Q: Why PostgreSQL only?**
|
|
342
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.
|
|
343
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)
|
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
|