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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52ffc6e9a926824c6730632c566d42570997aa5e2a2674ea07e79d0722a628fc
4
- data.tar.gz: 248ecf8294fe48488f0b8a8990f1602ab7cd3c10af95731be2a8e3f0933905c8
3
+ metadata.gz: 1c59d25915c0eda9030515a3b92835664ab91951cc51faf13be126bbc0f6053c
4
+ data.tar.gz: 38e490b4dc7cbe7a1303d03af2f5fa7f41f8100ac4b5787f36b9b3e2d1cf8648
5
5
  SHA512:
6
- metadata.gz: ff1071aaa8e6a151fa0382ec294df042a4afb5d8c8e00bd5a25fe2ffdc67ba54971b649edc5cfd3019895a44c64ee2b0ce352032f8f24d2a525a3cd2ef6dadd0
7
- data.tar.gz: 99dcd53ddb3c068522a7a2eba6b262c8f5bfb9bc78433596e4d15a8ea1363e31895be3b2c39270276955a802fe2198ae2d74ddba4d60a063519903ace4f71263
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rokaki (0.17.0)
4
+ rokaki (0.18.0)
5
5
  activesupport
6
6
 
7
7
  GEM
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.17"
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.13.0
106
+ ## What’s new in 0.17.0
107
107
 
108
- - Block-form DSL parity across both FilterModel and Filterable
109
- - Circumfix affix synonyms supported: :parafix, :confix, :ambifix
110
- - SQL Server adapter support and CI coverage
111
- - ENV overrides for all adapters in test helpers; improved DB bootstrap in specs
112
- - Documentation site via GitHub Pages
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.17"
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
- # Between with a two-element Array
127
- Article.filter(published: [Date.new(2024,5,1), Date.new(2024,12,1)])
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` or two-element `Array` directly as the field value is treated as a between filter automatically.
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
- # Fall back to equality with the original hash
124
- @model.where(#{key}: _val)
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
- rel.where("#{qualified_col} = :v", v: _val)
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)
@@ -1,3 +1,3 @@
1
1
  module Rokaki
2
- VERSION = "0.17.0"
2
+ VERSION = "0.18.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.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Martin