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 +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/docs/adapters.md +10 -0
- data/docs/configuration.md +10 -0
- data/docs/index.md +7 -3
- data/docs/usage.md +60 -5
- data/lib/rokaki/filter_model.rb +96 -12
- data/lib/rokaki/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6c0197e9af455ef746957b92e5a28a4aa8a34ad35712c2aacae5ad6797f4d18
|
|
4
|
+
data.tar.gz: 7cc5acc3e65cd850a50de797285c55fdbdab9c47fc2a790a68d6ec23c4064b4d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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.
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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
|
|
116
|
-
|
|
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,
|
data/lib/rokaki/filter_model.rb
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
data/lib/rokaki/version.rb
CHANGED