rokaki 0.15.0 → 0.16.0

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: e754206bd6927db93fa0977aed409762b9bc9aa2a663c5b0d94977c2d18ba284
4
- data.tar.gz: 9df4b1d7d067be43ac1df89f1b5962b2950b91e6bc5e915817cde2ef625f55f0
3
+ metadata.gz: f6c0197e9af455ef746957b92e5a28a4aa8a34ad35712c2aacae5ad6797f4d18
4
+ data.tar.gz: 7cc5acc3e65cd850a50de797285c55fdbdab9c47fc2a790a68d6ec23c4064b4d
5
5
  SHA512:
6
- metadata.gz: bf35a2999e21604de1d4daa615042036b0f16b651e251f569b4a13706d9e0e0c17022d0856303dfa71183b2c77af03631e3cae83e3b4cb790c855d47980ccc1c
7
- data.tar.gz: ad6b7c1e51bfee821d54923ccf6447ed91b0633bc084d0cd0adbd6a901d36200025334b3415c53814f62c0cf644bbb8e5a6e1a8418c1c8bac9cdf5add470e8f8
6
+ metadata.gz: addeae565323de0ce359cd73d01e3c7e31f457849b59fb373942436358561fd085fcf36117a4c8c75279ce549a26473364298067c2b760285eaec1f964423193
7
+ data.tar.gz: 147a531f6489bc41a102b60b16689c21bc1d852fb18a4d1658115943b35d0d64905d44b81c1ff1d1c4832b5d7739dac4758a436b262c7a6e64aa75db161d92ca
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ### Unreleased
2
+ - Documentation: Added backend auto‑detection feature docs across README and site (index, usage, adapters, configuration). Examples now prefer auto‑detection by default and explain explicit overrides and ambiguity errors.
3
+ - Tests: Added shared examples to exercise auto‑detection behavior under each adapter suite.
4
+
1
5
  ### 0.15.0 — 2025-10-27
2
6
  - Add first-class SQLite support: adapter-aware LIKE behavior with OR expansion for arrays.
3
7
  - Added SQLite badge in README.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rokaki (0.15.0)
4
+ rokaki (0.16.0)
5
5
  activesupport
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -16,6 +16,7 @@ Rokaki is a small DSL for building safe, composable filters for ActiveRecord que
16
16
  - Works with ActiveRecord 7.1 and 8.x
17
17
  - LIKE modes: `:prefix`, `:suffix`, `:circumfix` (+ synonyms) and array‑of‑terms
18
18
  - Nested filters with auto‑joins and qualified columns
19
+ - Auto‑detects the database backend; specify `db:` only when your app uses multiple adapters or you need an override
19
20
  - Block‑form DSL (`filter_map do ... end`) and classic argument form
20
21
  - Runtime usage: build an anonymous filter class from a payload (no predeclared class needed)
21
22
 
data/docs/adapters.md CHANGED
@@ -52,6 +52,16 @@ When you pass an array of terms, Rokaki composes adapter‑appropriate SQL that
52
52
  - SQL Server: The server/database/column collation determines sensitivity. Rokaki currently defers to your DB’s default. If you need deterministic behavior regardless of DB defaults, consider using a case‑sensitive collation on the column or open an issue to discuss inline `COLLATE` options.
53
53
 
54
54
 
55
+ ## Backend auto-detection
56
+
57
+ Rokaki auto-detects the adapter from your model’s ActiveRecord connection in typical single-adapter apps. If multiple adapters are detected in the process and you do not specify one, Rokaki raises a helpful error asking you to choose.
58
+
59
+ - Default: no `db:` needed; the adapter is inferred from the model connection.
60
+ - Multiple adapters present: pass `db:` to `filter_model` (or call `filter_db`) to select one explicitly.
61
+ - Errors you may see:
62
+ - `Rokaki::Error: Multiple database adapters detected (...). Please declare which backend to use via db: or filter_db.`
63
+ - `Rokaki::Error: Unable to auto-detect database adapter. Ensure your model is connected or pass db: explicitly.`
64
+
55
65
  ## SQLite
56
66
 
57
67
  SQLite is embedded and requires no separate server process. Rokaki treats it as a first-class adapter.
@@ -42,6 +42,16 @@ Rokaki's test helpers (used in the specs) support environment variable overrides
42
42
  ### SQLite
43
43
  - `SQLITE_DATABASE` (path to a SQLite file; if unset, tests use an in-memory DB via `":memory:"`)
44
44
 
45
+ ## Backend auto-detection
46
+
47
+ By default, Rokaki infers the database adapter from your model’s ActiveRecord connection.
48
+
49
+ - Single-adapter apps: no `db:` needed.
50
+ - Multiple adapters present: pass `db:` to `filter_model` (or call `filter_db`) to choose explicitly.
51
+ - Errors:
52
+ - `Rokaki::Error: Multiple database adapters detected (...). Please declare which backend to use via db: or filter_db.`
53
+ - `Rokaki::Error: Unable to auto-detect database adapter. Ensure your model is connected or pass db: explicitly.`
54
+
45
55
  ## SQL Server notes
46
56
 
47
57
  - Rokaki uses `LIKE` with proper escaping and OR expansion for arrays of terms.
data/docs/index.md CHANGED
@@ -10,6 +10,7 @@ Rokaki is a small Ruby library that helps you build safe, composable filters for
10
10
  - Supports simple and nested filters
11
11
  - LIKE-based matching with prefix/suffix/circumfix modes (circumfix also accepts synonyms: parafix, confix, ambifix)
12
12
  - Array-of-terms matching (adapter-aware)
13
+ - Auto-detects the database backend; specify db only when your app uses multiple adapters or you need an override
13
14
 
14
15
  Get started below or jump to:
15
16
  - [Usage](./usage)
@@ -40,8 +41,9 @@ Argument-based form:
40
41
  class ArticleQuery
41
42
  include Rokaki::FilterModel
42
43
 
43
- # Tell Rokaki which model to query and which DB adapter semantics to use
44
- filter_model :article, db: :postgres # or :mysql, :sqlserver, :oracle, :sqlite
44
+ # Tell Rokaki which model to query. Adapter is auto-detected from the connection.
45
+ # If your app uses multiple adapters, pass db: explicitly (e.g., db: :postgres)
46
+ filter_model :article
45
47
 
46
48
  # Map a single query key (:q) to multiple LIKE targets on Article
47
49
  define_query_key :q
@@ -66,7 +68,9 @@ Block-form DSL (same behavior):
66
68
  class ArticleQuery
67
69
  include Rokaki::FilterModel
68
70
 
69
- filter_model :article, db: :postgres # or :mysql, :sqlserver
71
+ # Adapter is auto-detected from the connection by default.
72
+ # If your app uses multiple adapters, pass db: explicitly (e.g., db: :postgres)
73
+ filter_model :article
70
74
  define_query_key :q
71
75
 
72
76
  filter_map do
data/docs/usage.md CHANGED
@@ -31,8 +31,9 @@ class ArticleQuery
31
31
  include Rokaki::FilterModel
32
32
  belongs_to :author
33
33
 
34
- # Choose model and adapter
35
- filter_model :article, db: :postgres # or :mysql, :sqlserver, :oracle, :sqlite
34
+ # Choose model; adapter is auto-detected from the model's connection.
35
+ # If your app uses multiple adapters, pass db: explicitly (e.g., db: :postgres)
36
+ filter_model :article
36
37
 
37
38
  # Map a single query key (:q) to multiple LIKE targets
38
39
  define_query_key :q
@@ -112,8 +113,9 @@ Rokaki also supports a block-form DSL that is equivalent to the argument-based f
112
113
  class ArticleQuery
113
114
  include Rokaki::FilterModel
114
115
 
115
- # Choose model and adapter
116
- filter_model :article, db: :postgres # or :mysql, :sqlserver
116
+ # Choose model; adapter is auto-detected from the model's connection.
117
+ # If your app uses multiple adapters, pass db: explicitly (e.g., db: :postgres)
118
+ filter_model :article
117
119
 
118
120
  # Declare a single query key used by all LIKE/equality filters below
119
121
  define_query_key :q
@@ -188,6 +190,59 @@ Tips:
188
190
  - Inside the block, `nested :association` affects all `filters` declared within it.
189
191
 
190
192
 
193
+ ## Backend auto-detection
194
+
195
+ By default, Rokaki auto-detects which database adapter to use from your model’s ActiveRecord connection. This means you usually don’t need to pass `db:` explicitly.
196
+
197
+ - Single-adapter apps: No configuration needed — Rokaki infers the adapter from the model connection.
198
+ - Multi-adapter apps: If more than one adapter is detected in the process, Rokaki raises a clear error asking you to declare which backend to use.
199
+ - Explicit override: You can always specify `db:` on `filter_model` or call `filter_db` later.
200
+
201
+ Examples:
202
+
203
+ ```ruby
204
+ class ArticleQuery
205
+ include Rokaki::FilterModel
206
+
207
+ # Adapter auto-detected (recommended default)
208
+ filter_model :article
209
+ define_query_key :q
210
+
211
+ filter_map do
212
+ like title: :circumfix
213
+ end
214
+ end
215
+ ```
216
+
217
+ Explicit selection/override:
218
+
219
+ ```ruby
220
+ class ArticleQuery
221
+ include Rokaki::FilterModel
222
+
223
+ # Option A: choose upfront
224
+ filter_model :article, db: :postgres
225
+
226
+ # Option B: or set it later
227
+ # filter_model :article
228
+ # filter_db :sqlite
229
+ end
230
+ ```
231
+
232
+ Ambiguity behavior (apps with multiple adapters):
233
+
234
+ - If Rokaki sees multiple adapters in use and you haven’t specified one, it raises:
235
+
236
+ ```
237
+ Rokaki::Error: Multiple database adapters detected (...). Please declare which backend to use via db: or filter_db.
238
+ ```
239
+
240
+ - If it cannot detect any adapter at all, it raises:
241
+
242
+ ```
243
+ Rokaki::Error: Unable to auto-detect database adapter. Ensure your model is connected or pass db: explicitly.
244
+ ```
245
+
191
246
  ## Dynamic runtime listener (no code changes needed)
192
247
 
193
248
  You can construct a Rokaki filter class at runtime from a payload (e.g., JSON → Hash) and use it immediately — no prior class is required. Rokaki will compile the tiny class on the fly and generate the methods once.
@@ -197,7 +252,7 @@ You can construct a Rokaki filter class at runtime from a payload (e.g., JSON
197
252
  # Example payload (e.g., parsed JSON)
198
253
  payload = {
199
254
  model: :article,
200
- db: :postgres, # or :mysql, :sqlserver, :oracle
255
+ db: :postgres, # optional; or :mysql, :sqlserver, :oracle, :sqlite
201
256
  query_key: :q, # the key in params with search term(s)
202
257
  like: { # like mappings (deeply nested allowed)
203
258
  title: :circumfix,
@@ -159,6 +159,89 @@ module Rokaki
159
159
  end
160
160
  end
161
161
 
162
+ # Map AR adapter names to internal symbols
163
+ def map_adapter_name(name)
164
+ n = name.to_s.downcase
165
+ case n
166
+ when 'postgresql', 'postgres', 'postgis'
167
+ :postgres
168
+ when 'mysql2', 'mysql'
169
+ :mysql
170
+ when 'sqlite3', 'sqlite'
171
+ :sqlite
172
+ when 'sqlserver'
173
+ :sqlserver
174
+ when 'oracle_enhanced', 'oracle'
175
+ :oracle
176
+ else
177
+ nil
178
+ end
179
+ end
180
+
181
+ # Try to detect adapter from a model's connection
182
+ def detect_adapter_from_model(model)
183
+ return nil unless model
184
+ begin
185
+ adapter = model.connection_db_config&.adapter
186
+ return map_adapter_name(adapter) if adapter
187
+ rescue StandardError
188
+ # fall through
189
+ end
190
+ begin
191
+ adapter = model.connection&.adapter_name
192
+ return map_adapter_name(adapter)
193
+ rescue StandardError
194
+ nil
195
+ end
196
+ end
197
+
198
+ # Scan known AR models to see how many adapters are in use
199
+ def adapters_in_use
200
+ adapters = []
201
+ begin
202
+ bases = [::ActiveRecord::Base] + (::ActiveRecord::Base.descendants rescue [])
203
+ bases.uniq.each do |k|
204
+ next unless k.respond_to?(:connection_db_config)
205
+ begin
206
+ a = k.connection_db_config&.adapter
207
+ adapters << a if a
208
+ rescue StandardError
209
+ # ignore not connected models
210
+ end
211
+ end
212
+ rescue StandardError
213
+ # ignore
214
+ end
215
+ adapters.compact.map { |a| map_adapter_name(a) }.compact.uniq
216
+ end
217
+
218
+ # Determine @_filter_db or raise if ambiguous in multi-adapter apps
219
+ def resolve_filter_db!(model: @model, explicit: nil)
220
+ if explicit
221
+ @_filter_db = explicit
222
+ return @_filter_db
223
+ end
224
+ # Prefer model-specific detection
225
+ detected = detect_adapter_from_model(model)
226
+ return (@_filter_db = detected) if detected
227
+
228
+ # Fallback to a single global adapter if unambiguous
229
+ used = adapters_in_use
230
+ if used.size == 1
231
+ @_filter_db = used.first
232
+ elsif used.size > 1
233
+ raise ::Rokaki::Error, "Multiple database adapters detected (#{used.join(', ')}). Please declare which backend to use via db: or filter_db."
234
+ else
235
+ # As a last resort, try ActiveRecord::Base connection
236
+ begin
237
+ base_detected = map_adapter_name(::ActiveRecord::Base.connection_db_config&.adapter)
238
+ return (@_filter_db = base_detected) if base_detected
239
+ rescue StandardError
240
+ end
241
+ raise ::Rokaki::Error, "Unable to auto-detect database adapter. Ensure your model is connected or pass db: explicitly."
242
+ end
243
+ end
244
+
162
245
  # Merge two nested like/ilike mappings
163
246
  def deep_merge_like(a, b)
164
247
  return b if a.nil? || a == {}
@@ -196,7 +279,7 @@ module Rokaki
196
279
  if block_given? && args.empty?
197
280
  raise ArgumentError, 'define_query_key must be called before block filter_map' unless @filter_map_query_key
198
281
  raise ArgumentError, 'filter_model must be called before block filter_map' unless @model
199
- @_filter_db ||= :postgres
282
+ resolve_filter_db!(model: @model)
200
283
 
201
284
  # Enter block-collection mode
202
285
  @__in_filter_map_block = true
@@ -228,22 +311,22 @@ module Rokaki
228
311
  filter_model(model)
229
312
  @filter_map_query_key = query_key
230
313
 
231
- @_filter_db = options[:db] || :postgres
232
- @_filter_mode = options[:mode] || :and
233
- like(options[:like]) if options[:like]
234
- ilike(options[:ilike]) if options[:ilike]
235
- filters(*options[:match]) if options[:match]
314
+ resolve_filter_db!(model: @model, explicit: options && options[:db])
315
+ @_filter_mode = (options && options[:mode]) || :and
316
+ like(options[:like]) if options && options[:like]
317
+ ilike(options[:ilike]) if options && options[:ilike]
318
+ filters(*options[:match]) if options && options[:match]
236
319
  end
237
320
 
238
321
  def filter(model, options)
239
322
  filter_model(model)
240
323
  @filter_map_query_key = nil
241
324
 
242
- @_filter_db = options[:db] || :postgres
243
- @_filter_mode = options[:mode] || :and
244
- like(options[:like]) if options[:like]
245
- ilike(options[:ilike]) if options[:ilike]
246
- filters(*options[:match]) if options[:match]
325
+ resolve_filter_db!(model: @model, explicit: options && options[:db])
326
+ @_filter_mode = (options && options[:mode]) || :and
327
+ like(options[:like]) if options && options[:like]
328
+ ilike(options[:ilike]) if options && options[:ilike]
329
+ filters(*options[:match]) if options && options[:match]
247
330
  end
248
331
 
249
332
  def filters(*filter_keys)
@@ -353,9 +436,10 @@ module Rokaki
353
436
  end
354
437
 
355
438
  def filter_model(model_class, db: nil)
356
- @_filter_db = db if db
357
439
  @model = (model_class.is_a?(Class) ? model_class : Object.const_get(model_class.capitalize))
358
440
  class_eval "def set_model; @model ||= #{@model}; end;"
441
+ # Only resolve here if an explicit db is provided; otherwise defer to callers
442
+ resolve_filter_db!(model: @model, explicit: db) if db
359
443
  end
360
444
 
361
445
  def case_sensitive
@@ -1,3 +1,3 @@
1
1
  module Rokaki
2
- VERSION = "0.15.0"
2
+ VERSION = "0.16.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rokaki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Martin