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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b17930ae6088ef2d4fb7f4e20c36479bc8bcc97ae2ed3bd2bfd8d7baed4266b0
4
- data.tar.gz: 7100335df50ba93ee28fb811d530cdf2231db2efaed2f149073144a15e389c41
3
+ metadata.gz: b47a2f9363e6fb03e19af88b8d34ab9d727e45e2ed7f05e95d41d23835f93693
4
+ data.tar.gz: cb868fa5858008ce6e2ec14188e81c41fb0ad554e85e491b71ebe625d357257e
5
5
  SHA512:
6
- metadata.gz: 52f8165082fb447dcc9219c968256f867ead4dba70722920928acedae4e150d9608ee3ffd5dd803131d556031f588b3bba7f8f17171d56cca4487ae427dfb6b4
7
- data.tar.gz: bdfdf6ecf8cf3495ef4ed157ea3e149d23ff16f5a7ff98a73329dd3f3a1516d80a8a57ddbea1b23dd26df43805a27164a28ba4836550ed9951f7392f07118eb9
6
+ metadata.gz: 3f9927adf42ec5fed6e65ea6c5a83a3166ea10ccadbe9d802a2d2bb6cc02647365df44865052566a6b255fa8dbb91be74f406a0fc61993982589222b2724724a
7
+ data.tar.gz: '08acf2f917e887a25e0c22aebd493aa861c1bc0fa827e80983ee577f352d1048efabb2285b152cb1d141ccf8846e34071828f4b77960d0b1b51909fb279fcd85'
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
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: 'John%',
50
+ name: 'Eclipsoid%',
51
51
  email: '%@example.com'
52
52
  )
53
- # => [{"id"=>9981, "name"=>"John Doe", "email"=>"johndoe@example.com"}]
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: 'Claude',
68
+ name: 'Eclipsoid',
69
69
  created_at: '2024-01-01'
70
70
  )
71
71
 
72
72
  user = users.first
73
- user.name # => "Claude"
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: 'Claude').first
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] # => "Claude"
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
- ## PostgreSQL Array Support
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: 'Claude').first
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
- ## Performance Considerations
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
- ### Rails Generator
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
- ### Configuration
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
- ## Why Kweerie?
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
- ## Contributing
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
- ## License
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
- def sql_path
18
- @sql_path ||= begin
19
- subclass_file = "#{name.underscore}.sql"
20
- possible_paths = Kweerie.configuration.sql_paths.call
21
-
22
- sql_file = possible_paths.map do |path|
23
- File.join(root_path, path, subclass_file)
24
- end.find { |f| File.exist?(f) }
25
-
26
- unless sql_file
27
- raise SQLFileNotFound,
28
- "Could not find SQL file for #{name} in paths: #{possible_paths.join(", ")}"
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
- sql_file
32
- end
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kweerie
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.5"
5
5
  end
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.3
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-07 00:00:00.000000000 Z
11
+ date: 2024-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest