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 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