kweerie 0.1.4 → 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 +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
|