rokaki 0.17.0 → 0.18.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 +7 -0
- data/Gemfile.lock +1 -1
- data/docs/_config.yml +8 -0
- data/docs/adapters.md +5 -1
- data/docs/dsl_syntax.md +8 -0
- data/docs/index.md +7 -7
- data/docs/usage.md +36 -4
- data/lib/rokaki/filter_model/basic_filter.rb +32 -2
- data/lib/rokaki/filter_model/nested_filter.rb +31 -1
- 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: 1c59d25915c0eda9030515a3b92835664ab91951cc51faf13be126bbc0f6053c
|
|
4
|
+
data.tar.gz: 38e490b4dc7cbe7a1303d03af2f5fa7f41f8100ac4b5787f36b9b3e2d1cf8648
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70819c707de3dfdc0228d238d7b750c48ddba7b6fc23b8f74746ddae1421e88c4dbd5be75a620473af32ae82852aaf4de90eb26927d0efd9ee97b6ecc2588281
|
|
7
|
+
data.tar.gz: 7c020e3c58a55c65329c25f6503c2669a7ff08f1e86fedde551b8e38a6ba2f3b50042daea0db255ca0adb27bd6e1d19f59bb8f9fe6a250c196d8dbd54634b387
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
### Unreleased
|
|
2
2
|
- (no changes yet)
|
|
3
3
|
|
|
4
|
+
### 0.18.0 — 2025-10-28
|
|
5
|
+
- Feature: inequality and nullability operators at leaf level across all adapters:
|
|
6
|
+
- `neq`, `not_in`, `is_null`, `is_not_null`, `gt`, `gte`, `lt`, `lte`
|
|
7
|
+
- Tests: shared examples added and wired into all adapter-aware suites; full matrix passing.
|
|
8
|
+
- Docs: usage and DSL syntax updated; adapters page notes portability of these operators.
|
|
9
|
+
- Chore: bump version to 0.18.0 and update install snippets to `~> 0.18`.
|
|
10
|
+
|
|
4
11
|
### 0.17.0 — 2025-10-28
|
|
5
12
|
- Version bump to 0.17.0.
|
|
6
13
|
- Documentation: Updated installation snippets to `~> 0.17`.
|
data/Gemfile.lock
CHANGED
data/docs/_config.yml
CHANGED
|
@@ -8,6 +8,14 @@ baseurl: "/rokaki"
|
|
|
8
8
|
url: "https://tevio.github.io"
|
|
9
9
|
theme: minima
|
|
10
10
|
|
|
11
|
+
# Limit header navigation to these pages to avoid top-level Oracle link
|
|
12
|
+
header_pages:
|
|
13
|
+
- index.md
|
|
14
|
+
- usage.md
|
|
15
|
+
- dsl_syntax.md
|
|
16
|
+
- adapters.md
|
|
17
|
+
- configuration.md
|
|
18
|
+
|
|
11
19
|
# Build settings
|
|
12
20
|
markdown: kramdown
|
|
13
21
|
kramdown:
|
data/docs/adapters.md
CHANGED
|
@@ -22,7 +22,7 @@ Rokaki generates adapter‑aware SQL for PostgreSQL, MySQL, SQL Server, Oracle,
|
|
|
22
22
|
- Case sensitivity follows DB collation by default; future versions may add inline `COLLATE` options
|
|
23
23
|
- Oracle
|
|
24
24
|
- Uses `LIKE`; arrays of terms are OR‑chained; case‑insensitive paths use `UPPER(column) LIKE UPPER(:q)`
|
|
25
|
-
- See the dedicated page: [Oracle connections](/adapters/oracle) for connection strings, NLS settings, and common errors.
|
|
25
|
+
- See the dedicated page: [Oracle connections](/rokaki/adapters/oracle) for connection strings, NLS settings, and common errors.
|
|
26
26
|
- SQLite
|
|
27
27
|
- Embedded (no separate server needed)
|
|
28
28
|
- Uses `LIKE`; arrays of terms are OR‑chained across predicates
|
|
@@ -81,6 +81,10 @@ database: ":memory:"
|
|
|
81
81
|
To persist a database file locally, set `SQLITE_DATABASE` to a path (e.g., `tmp/test.sqlite3`).
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
## Inequality and null filters
|
|
85
|
+
|
|
86
|
+
The following leaf-level operators are adapter-agnostic across PostgreSQL, MySQL, SQL Server, Oracle, and SQLite: `neq`, `not_in`, `is_null`, `is_not_null`, `gt`, `gte`, `lt`, `lte`. Rokaki composes standard SQL (`<>`, `NOT IN`, `IS NULL`/`IS NOT NULL`, `>`, `>=`, `<`, `<=`) with bound parameters.
|
|
87
|
+
|
|
84
88
|
## Range/BETWEEN filters
|
|
85
89
|
|
|
86
90
|
Rokaki’s range filters (`between`, lower-bound aliases like `from`/`min`, and upper-bound aliases like `to`/`max`) are adapter‑agnostic. The library always generates parameterized predicates using `BETWEEN`, `>=`, and `<=` on the target column.
|
data/docs/dsl_syntax.md
CHANGED
|
@@ -106,6 +106,14 @@ At a leaf field (e.g., `published` or `reviews.published`):
|
|
|
106
106
|
- `{ published: { min: t1 } }` → `published >= t1`
|
|
107
107
|
- `{ published: { max: t2 } }` → `published <= t2`
|
|
108
108
|
|
|
109
|
+
- Operator-hash (inequality/null) → non-equality predicates
|
|
110
|
+
- Keys: `neq`, `not_in`, `is_null`, `is_not_null`, `gt`, `gte`, `lt`, `lte`
|
|
111
|
+
- Examples:
|
|
112
|
+
- `{ title: { neq: "Draft" } }` → `title <> 'Draft'`
|
|
113
|
+
- `{ title: { not_in: ["Draft", "Archived"] } }` → `title NOT IN (...)`
|
|
114
|
+
- `{ content: { is_null: true } }` → `content IS NULL`
|
|
115
|
+
- `{ published: { gt: t1, lte: t2 } }` → `published > t1 AND published <= t2`
|
|
116
|
+
|
|
109
117
|
Notes:
|
|
110
118
|
- Only the leaf level interprets these reserved keys. Join-structure keys do not carry operators.
|
|
111
119
|
- Arrays never imply range; to express a range with an array, use `{ published: { between: [from, to] } }`.
|
data/docs/index.md
CHANGED
|
@@ -23,7 +23,7 @@ Get started below or jump to:
|
|
|
23
23
|
Add to your application's Gemfile:
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
gem "rokaki", "~> 0.
|
|
26
|
+
gem "rokaki", "~> 0.18"
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Then:
|
|
@@ -103,13 +103,13 @@ Where `params` can include keys like `q`, `author_first_name`, `author_last_name
|
|
|
103
103
|
|
|
104
104
|
All modes accept either a single string or an array of terms.
|
|
105
105
|
|
|
106
|
-
## What’s new in 0.
|
|
106
|
+
## What’s new in 0.17.0
|
|
107
107
|
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
108
|
+
- Range filters as first-class filters (non-aggregates): `between`, lower bounds (`from`/`since`/`after`/`start`/`min`), upper bounds (`to`/`until`/`before`/`end`/`max`) on top-level and nested fields.
|
|
109
|
+
- Arrays always mean equality `IN` lists; use a `Range` or `{ between: [from, to] }` to express ranges.
|
|
110
|
+
- Block-form DSL parity across `FilterModel` and `Filterable`, with nested declarations for associations and leaves.
|
|
111
|
+
- Backend auto-detection: adapter inferred from the model connection; pass `db:` only for multi-adapter apps or overrides.
|
|
112
|
+
- Oracle documentation moved under Adapters: see [Database adapters](./adapters) → [Oracle connections](/adapters/oracle).
|
|
113
113
|
|
|
114
114
|
## Next steps
|
|
115
115
|
|
data/docs/usage.md
CHANGED
|
@@ -12,7 +12,7 @@ For a formal description of the mapping DSL and how payloads are interpreted (jo
|
|
|
12
12
|
Add the gem to your Gemfile and bundle:
|
|
13
13
|
|
|
14
14
|
```ruby
|
|
15
|
-
gem "rokaki", "~> 0.
|
|
15
|
+
gem "rokaki", "~> 0.18"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```bash
|
|
@@ -123,8 +123,10 @@ Article.filter(published: { since: Date.new(2024,1,1), until: Date.new(2024,6,30
|
|
|
123
123
|
Article.filter(published: { min: Date.new(2024,1,1) }) # >= 2024-01-01
|
|
124
124
|
Article.filter(published: { max: Date.new(2024,12,31) }) # <= 2024-12-31
|
|
125
125
|
|
|
126
|
-
#
|
|
127
|
-
|
|
126
|
+
# Arrays are equality lists (IN), not ranges
|
|
127
|
+
# Use a Range or `{ between: [...] }` for range filtering
|
|
128
|
+
Article.filter(published: [Date.new(2024,5,1), Date.new(2024,12,1)]) # => IN (equality list)
|
|
129
|
+
Article.filter(published: { between: [Date.new(2024,5,1), Date.new(2024,12,1)] })
|
|
128
130
|
```
|
|
129
131
|
|
|
130
132
|
Nested fields use the same sub-keys and value shapes:
|
|
@@ -153,7 +155,7 @@ Article.filter(reviews_published: (Time.utc(2024,1,1)..Time.utc(2024,6,30)))
|
|
|
153
155
|
|
|
154
156
|
Behavior notes:
|
|
155
157
|
- `min`/`max` are interpreted as lower/upper bounds, not aggregate functions.
|
|
156
|
-
- Passing a `Range`
|
|
158
|
+
- Passing a `Range` directly as the field value is treated as a between filter automatically. Two-element `Array`s are not ranges unless wrapped with `{ between: [...] }`.
|
|
157
159
|
- Arrays with more than two elements are treated as equality lists (`IN (?)`) — use `{ between: [...] }` if you intend a range.
|
|
158
160
|
- `nil` bounds are ignored: only the provided side is applied (e.g., `{ from: t }` becomes `>= t`).
|
|
159
161
|
- All generated predicates are parameterized and adapter‑agnostic (`BETWEEN`, `>=`, `<=`).
|
|
@@ -385,3 +387,33 @@ Notes:
|
|
|
385
387
|
- This approach is production‑ready and requires no core changes to Rokaki.
|
|
386
388
|
- You can cache the generated class by a digest of the payload to avoid recompiling.
|
|
387
389
|
- For maximum safety, validate/allow‑list models/columns coming from untrusted payloads.
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
## Inequality and null filters
|
|
394
|
+
|
|
395
|
+
Use leaf-level sub-keys to express non-equality predicates on top-level or nested fields.
|
|
396
|
+
|
|
397
|
+
- Not equal: `{ field: { neq: value } }` → `<>`
|
|
398
|
+
- Not in: `{ field: { not_in: [v1, v2] } }` → `NOT IN (...)`
|
|
399
|
+
- Nullability:
|
|
400
|
+
- `{ field: { is_null: true } }` → `IS NULL`
|
|
401
|
+
- `{ field: { is_not_null: true } }` (or `{ is_null: false }`) → `IS NOT NULL`
|
|
402
|
+
- Explicit comparisons: `{ field: { gt: x, gte: x, lt: x, lte: x } }`
|
|
403
|
+
|
|
404
|
+
Examples:
|
|
405
|
+
```ruby
|
|
406
|
+
# Top-level
|
|
407
|
+
Article.filter(title: { neq: "Draft" })
|
|
408
|
+
Article.filter(title: { not_in: ["Draft", "Archived"] })
|
|
409
|
+
Article.filter(content: { is_null: true })
|
|
410
|
+
Article.filter(published: { gt: Time.utc(2024,1,1) })
|
|
411
|
+
|
|
412
|
+
# Nested
|
|
413
|
+
Article.filter(author: { first_name: { neq: "Ada" } })
|
|
414
|
+
Article.filter(reviews: { published: { lte: Time.utc(2024,6,1,12) } })
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Notes:
|
|
418
|
+
- Arrays remain equality `IN` lists unless used under `not_in`.
|
|
419
|
+
- These predicates are adapter-agnostic and work the same across PostgreSQL, MySQL, SQL Server, Oracle, and SQLite.
|
|
@@ -120,8 +120,38 @@ module Rokaki
|
|
|
120
120
|
elsif !_to.nil?
|
|
121
121
|
@model.where("#{key} <= :to", to: _to)
|
|
122
122
|
else
|
|
123
|
-
#
|
|
124
|
-
|
|
123
|
+
# Inequality/nullability operators
|
|
124
|
+
_op_neq = _val[:neq] || _val['neq']
|
|
125
|
+
_op_not_in = _val[:not_in] || _val['not_in']
|
|
126
|
+
_op_is_null = _val[:is_null] || _val['is_null']
|
|
127
|
+
_op_is_not_null = _val[:is_not_null] || _val['is_not_null']
|
|
128
|
+
_op_gt = _val[:gt] || _val['gt']
|
|
129
|
+
_op_gte = _val[:gte] || _val['gte']
|
|
130
|
+
_op_lt = _val[:lt] || _val['lt']
|
|
131
|
+
_op_lte = _val[:lte] || _val['lte']
|
|
132
|
+
|
|
133
|
+
if !_op_neq.nil?
|
|
134
|
+
@model.where("#{key} <> :v", v: _op_neq)
|
|
135
|
+
elsif _op_not_in
|
|
136
|
+
_arr = Array(_op_not_in)
|
|
137
|
+
return @model.none if _arr.empty?
|
|
138
|
+
@model.where("#{key} NOT IN (?)", _arr)
|
|
139
|
+
elsif _op_is_null == true
|
|
140
|
+
@model.where("#{key} IS NULL")
|
|
141
|
+
elsif _op_is_not_null == true || _op_is_null == false
|
|
142
|
+
@model.where("#{key} IS NOT NULL")
|
|
143
|
+
elsif !_op_gt.nil?
|
|
144
|
+
@model.where("#{key} > :v", v: _op_gt)
|
|
145
|
+
elsif !_op_gte.nil?
|
|
146
|
+
@model.where("#{key} >= :v", v: _op_gte)
|
|
147
|
+
elsif !_op_lt.nil?
|
|
148
|
+
@model.where("#{key} < :v", v: _op_lt)
|
|
149
|
+
elsif !_op_lte.nil?
|
|
150
|
+
@model.where("#{key} <= :v", v: _op_lte)
|
|
151
|
+
else
|
|
152
|
+
# Fall back to equality with the original hash
|
|
153
|
+
@model.where(#{key}: _val)
|
|
154
|
+
end
|
|
125
155
|
end
|
|
126
156
|
elsif _val.is_a?(Range)
|
|
127
157
|
#{build_between_query(filter: filter, key: key)}
|
|
@@ -185,7 +185,37 @@ module Rokaki
|
|
|
185
185
|
elsif !_to.nil?
|
|
186
186
|
rel.where("#{qualified_col} <= :to", to: _to)
|
|
187
187
|
else
|
|
188
|
-
|
|
188
|
+
# Inequality/nullability operators
|
|
189
|
+
_op_neq = _val[:neq] || _val['neq']
|
|
190
|
+
_op_not_in = _val[:not_in] || _val['not_in']
|
|
191
|
+
_op_is_null = _val[:is_null] || _val['is_null']
|
|
192
|
+
_op_is_not_null = _val[:is_not_null] || _val['is_not_null']
|
|
193
|
+
_op_gt = _val[:gt] || _val['gt']
|
|
194
|
+
_op_gte = _val[:gte] || _val['gte']
|
|
195
|
+
_op_lt = _val[:lt] || _val['lt']
|
|
196
|
+
_op_lte = _val[:lte] || _val['lte']
|
|
197
|
+
|
|
198
|
+
if !_op_neq.nil?
|
|
199
|
+
rel.where("#{qualified_col} <> :v", v: _op_neq)
|
|
200
|
+
elsif _op_not_in
|
|
201
|
+
_arr = Array(_op_not_in)
|
|
202
|
+
return rel.none if _arr.empty?
|
|
203
|
+
rel.where("#{qualified_col} NOT IN (?)", _arr)
|
|
204
|
+
elsif _op_is_null == true
|
|
205
|
+
rel.where("#{qualified_col} IS NULL")
|
|
206
|
+
elsif _op_is_not_null == true || _op_is_null == false
|
|
207
|
+
rel.where("#{qualified_col} IS NOT NULL")
|
|
208
|
+
elsif !_op_gt.nil?
|
|
209
|
+
rel.where("#{qualified_col} > :v", v: _op_gt)
|
|
210
|
+
elsif !_op_gte.nil?
|
|
211
|
+
rel.where("#{qualified_col} >= :v", v: _op_gte)
|
|
212
|
+
elsif !_op_lt.nil?
|
|
213
|
+
rel.where("#{qualified_col} < :v", v: _op_lt)
|
|
214
|
+
elsif !_op_lte.nil?
|
|
215
|
+
rel.where("#{qualified_col} <= :v", v: _op_lte)
|
|
216
|
+
else
|
|
217
|
+
rel.where("#{qualified_col} = :v", v: _val)
|
|
218
|
+
end
|
|
189
219
|
end
|
|
190
220
|
elsif _val.is_a?(Range)
|
|
191
221
|
rel.where("#{qualified_col} BETWEEN :from AND :to", from: _val.begin, to: _val.end)
|
data/lib/rokaki/version.rb
CHANGED