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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8c4f949ad0ee144a37015f2444ad19721390ee94a59b1fc118599409b6b0f0a
4
- data.tar.gz: a8b741b11e8dd572f6fccba9f06becbb75b06f0444e879f43b21f70d164d2ba7
3
+ metadata.gz: b47a2f9363e6fb03e19af88b8d34ab9d727e45e2ed7f05e95d41d23835f93693
4
+ data.tar.gz: cb868fa5858008ce6e2ec14188e81c41fb0ad554e85e491b71ebe625d357257e
5
5
  SHA512:
6
- metadata.gz: 368206d1376231a56d197911245878f8efdf8eb33f86f47135c079f2d3c28b2ac5a70786b7bafaa694b980f6f192ea6ac2d43ffcdb41f77bbd24aee295c7b9c4
7
- data.tar.gz: 82c8cb0e9a1171f3a0e90b0db23ebc5f35e2b82401f4db2066b3903db383b8e08f619ab25edefbc0bea4ebab93fde9072d86895bfa864a1f793c4370f5f02f03
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,12 +65,12 @@ 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
 
@@ -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" => "Claude"}, ...]
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="Claude">, ...]
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: 'Claude').first
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] # => "Claude"
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: 'Claude').first
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
- 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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kweerie
4
- VERSION = "0.1.4"
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.4
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