rokaki 0.16.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: f6c0197e9af455ef746957b92e5a28a4aa8a34ad35712c2aacae5ad6797f4d18
4
- data.tar.gz: 7cc5acc3e65cd850a50de797285c55fdbdab9c47fc2a790a68d6ec23c4064b4d
3
+ metadata.gz: 1c59d25915c0eda9030515a3b92835664ab91951cc51faf13be126bbc0f6053c
4
+ data.tar.gz: 38e490b4dc7cbe7a1303d03af2f5fa7f41f8100ac4b5787f36b9b3e2d1cf8648
5
5
  SHA512:
6
- metadata.gz: addeae565323de0ce359cd73d01e3c7e31f457849b59fb373942436358561fd085fcf36117a4c8c75279ce549a26473364298067c2b760285eaec1f964423193
7
- data.tar.gz: 147a531f6489bc41a102b60b16689c21bc1d852fb18a4d1658115943b35d0d64905d44b81c1ff1d1c4832b5d7739dac4758a436b262c7a6e64aa75db161d92ca
6
+ metadata.gz: 70819c707de3dfdc0228d238d7b750c48ddba7b6fc23b8f74746ddae1421e88c4dbd5be75a620473af32ae82852aaf4de90eb26927d0efd9ee97b6ecc2588281
7
+ data.tar.gz: 7c020e3c58a55c65329c25f6503c2669a7ff08f1e86fedde551b8e38a6ba2f3b50042daea0db255ca0adb27bd6e1d19f59bb8f9fe6a250c196d8dbd54634b387
data/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
1
  ### Unreleased
2
+ - (no changes yet)
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
+
11
+ ### 0.17.0 — 2025-10-28
12
+ - Version bump to 0.17.0.
13
+ - Documentation: Updated installation snippets to `~> 0.17`.
14
+ - Documentation: Added comprehensive Range/BETWEEN/MIN/MAX filter docs across README and site (usage, adapters). Clarified sub-key aliases (`between`, `from`/`since`/`after`/`start`/`min`, `to`/`until`/`before`/`end`/`max`), accepted value shapes (Range, 2‑element Array, Hash), nested examples, and equality array semantics (`IN` vs `BETWEEN`).
15
+ - Added Range/BETWEEN/MIN/MAX filters
16
+
17
+ ### 0.16.0 — 2025-10-27
2
18
  - 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
19
  - Tests: Added shared examples to exercise auto‑detection behavior under each adapter suite.
4
20
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rokaki (0.16.0)
4
+ rokaki (0.18.0)
5
5
  activesupport
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -38,6 +38,25 @@ Docs
38
38
 
39
39
  Tip: For a dynamic runtime listener (build a filter class from a JSON/hash payload at runtime), see “Dynamic runtime listener” in the Usage docs.
40
40
 
41
+ ## Range filters (between/min/max)
42
+
43
+ Use the field name as the key and the filter type as a sub-key, or pass a `Range` directly. Aliases are supported.
44
+
45
+ ```ruby
46
+ # Top-level
47
+ Article.filter(published: { from: Date.new(2024,1,1), to: Date.new(2024,12,31) })
48
+ Article.filter(published: (Date.new(2024,1,1)..Date.new(2024,12,31)))
49
+
50
+ # Nested
51
+ Article.filter(reviews_published: { max: Time.utc(2024,6,30) })
52
+ ```
53
+
54
+ - Lower bound aliases (>=): `from`, `since`, `after`, `start`, `min`
55
+ - Upper bound aliases (<=): `to`, `until`, `before`, `end`, `max`
56
+ - Arrays always mean `IN (?)` for equality. Use a `Range` or `{ between: [from, to] }` for range filtering
57
+
58
+ See full docs: https://tevio.github.io/rokaki/usage#range-between-min-and-max-filters
59
+
41
60
  ---
42
61
 
43
62
  ## Further reading
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,6 +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](/rokaki/adapters/oracle) for connection strings, NLS settings, and common errors.
25
26
  - SQLite
26
27
  - Embedded (no separate server needed)
27
28
  - Uses `LIKE`; arrays of terms are OR‑chained across predicates
@@ -78,3 +79,24 @@ database: ":memory:"
78
79
  ```
79
80
 
80
81
  To persist a database file locally, set `SQLITE_DATABASE` to a path (e.g., `tmp/test.sqlite3`).
82
+
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
+
88
+ ## Range/BETWEEN filters
89
+
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.
91
+
92
+ Adapter notes:
93
+ - PostgreSQL: Uses regular `WHERE column BETWEEN $1 AND $2` (or `>=`/`<=`). No special handling is required.
94
+ - MySQL/MariaDB: Uses `BETWEEN ? AND ?` (or `>=`/`<=`). Datetime values are compared with the column precision configured by your schema.
95
+ - SQLite: Uses `BETWEEN ? AND ?` (or `>=`/`<=`).
96
+ - SQL Server: Uses `BETWEEN @from AND @to` (or `>=`/`<=`). Parameters are bound via ActiveRecord.
97
+ - Oracle: Uses `BETWEEN :from AND :to` (or `>=`/`<=`). If your column type is `DATE`, be aware it has second precision; `TIMESTAMP` supports fractional seconds.
98
+
99
+ Tips:
100
+ - For date-only upper bounds (e.g., `2024-12-31`), Rokaki treats them inclusively and, when applicable, will extend to the end of day in basic filters to match expectations. If you need precise control, pass explicit `Time` values.
101
+ - Arrays are treated as equality lists (`IN (?)`) across all adapters. Use a `Range` or `{ between: [from, to] }` for range filtering.
102
+ - `nil` bounds are ignored: only the provided side is applied.
@@ -0,0 +1,204 @@
1
+ ---
2
+ layout: page
3
+ title: Rokaki's DSL Syntax
4
+ permalink: /dsl-syntax
5
+ ---
6
+
7
+ This page describes Rokaki’s domain-specific language (DSL) for declaring mappings and how incoming payloads are interpreted, with a focus on the difference between join-structure keys and leaf-level field keys.
8
+
9
+ The same concepts apply to both DSL entry points:
10
+ - FilterModel (querying a specific ActiveRecord model)
11
+ - Filterable (key mapping only)
12
+
13
+ See also: Usage, Adapters, and Configuration pages linked from the site index.
14
+
15
+ ## Key ideas
16
+
17
+ - You declare the shape of your filterable graph in code. This defines which associations (joins) are traversed and which fields are addressable (leaves).
18
+ - At runtime, the payload mirrors only the declared structure. Values at leaves drive the operator semantics (equality, LIKE, range). The structure of joins does not change at runtime.
19
+
20
+ ## Join-structure keys vs leaf-level field keys
21
+
22
+ - Join-structure keys represent associations. They appear only in the mapping you write (and mirrored by the payload). They do not carry operators by themselves. Examples: `author`, `reviews`, `articles`.
23
+ - Leaf-level field keys represent actual database columns on the current model or on a joined association. Examples: `title`, `content`, `published`, `first_name`.
24
+ - The mapping defines where a key is treated as a join (non-leaf) vs a field (leaf). At a declared leaf, the value can be a scalar, array, range, or an operator-hash (see below). Rokaki will not traverse deeper than the declared leaf.
25
+
26
+ ## Declaring mappings
27
+
28
+ Two equivalent styles are supported:
29
+
30
+ ### Argument-based form (classic)
31
+
32
+ ```ruby
33
+ class ArticleQuery
34
+ include Rokaki::FilterModel
35
+
36
+ filter_model :article # Adapter auto-detected; pass db: if needed
37
+ define_query_key :q # Map a single query key to many fields
38
+
39
+ like title: :circumfix, content: :circumfix # LIKE mappings (no modes: option)
40
+
41
+ # Nested LIKEs and filters via association-shaped hashes
42
+ like author: { first_name: :prefix, last_name: :suffix }
43
+
44
+ # Declare equality/range-capable fields (leafs)
45
+ filters :published # enables :published in payload
46
+ filters reviews: :published # enables nested reviews.published
47
+
48
+ attr_accessor :filters
49
+ def initialize(filters: {})
50
+ @filters = filters
51
+ end
52
+ end
53
+ ```
54
+
55
+ ### Block-form DSL
56
+
57
+ ```ruby
58
+ class ArticleQuery
59
+ include Rokaki::FilterModel
60
+
61
+ filter_model :article
62
+ define_query_key :q
63
+
64
+ filter_map do
65
+ like title: :circumfix, content: :circumfix
66
+
67
+ nested :author do
68
+ like first_name: :prefix, last_name: :suffix
69
+ filters :id # leaf field under author
70
+ end
71
+
72
+ nested :reviews do
73
+ filters :published # leaf field under reviews
74
+ end
75
+ end
76
+
77
+ attr_accessor :filters
78
+ def initialize(filters: {})
79
+ @filters = filters
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## Payload rules (what values mean at a leaf)
85
+
86
+ At a leaf field (e.g., `published` or `reviews.published`):
87
+
88
+ - Scalar value → equality on the column
89
+ - Example: `{ published: Time.utc(2024,1,1) }` → `WHERE published = :v`
90
+
91
+ - Array value → equality `IN` list
92
+ - Arrays always mean `IN` across adapters.
93
+ - Example: `{ published: [t1, t2, t3] }` → `WHERE published IN (?, ?, ?)`
94
+
95
+ - Range (`a..b`) → between
96
+ - Example: `{ published: (t1..t2) }` → `WHERE published BETWEEN :from AND :to`
97
+
98
+ - Operator-hash (range-style keys) → between or open-ended bounds
99
+ - Reserved keys at the leaf indicate operator semantics:
100
+ - `between`
101
+ - Lower-bound aliases (>=): `from`, `since`, `after`, `start`, `min`
102
+ - Upper-bound aliases (<=): `to`, `until`, `before`, `end`, `max`
103
+ - Examples:
104
+ - `{ published: { from: t1, to: t2 } }`
105
+ - `{ published: { between: [t1, t2] } }`
106
+ - `{ published: { min: t1 } }` → `published >= t1`
107
+ - `{ published: { max: t2 } }` → `published <= t2`
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
+
117
+ Notes:
118
+ - Only the leaf level interprets these reserved keys. Join-structure keys do not carry operators.
119
+ - Arrays never imply range; to express a range with an array, use `{ published: { between: [from, to] } }`.
120
+ - Nil bounds are ignored: `{ published: { from: t1 } }` applies only the lower bound.
121
+
122
+ ## LIKE mappings and payloads
123
+
124
+ - You declare LIKE semantics in code via the `like` mapping; the payload provides the terms.
125
+ - Modes:
126
+ - `:prefix` → `%term`
127
+ - `:suffix` → `term%`
128
+ - `:circumfix` (synonyms: `:parafix`, `:confix`, `:ambifix`) → `%term%`
129
+ - Payload values for LIKE can be a string or an array of strings. Arrays are matched with adapter-aware OR semantics.
130
+
131
+ Examples:
132
+ ```ruby
133
+ like title: :circumfix
134
+ like author: { first_name: :prefix }
135
+
136
+ # Payload examples
137
+ { q: "First" }
138
+ { author: { first_name: ["Ada", "Al"] } }
139
+ ```
140
+
141
+ ## Nested examples
142
+
143
+ Top-level field range:
144
+ ```ruby
145
+ ArticleQuery.new(filters: { published: { since: Time.utc(2024,1,1), until: Time.utc(2024,6,30) } }).results
146
+ ```
147
+
148
+ Nested association field range:
149
+ ```ruby
150
+ ArticleQuery.new(filters: { reviews: { published: (Time.utc(2024,1,1)..Time.utc(2024,6,30)) } }).results
151
+ ```
152
+
153
+ Deep nested example (author → articles → reviews.published):
154
+ ```ruby
155
+ class AuthorQuery
156
+ include Rokaki::FilterModel
157
+ filter_model :author
158
+ filter_map do
159
+ nested :articles do
160
+ nested :reviews do
161
+ filters :published
162
+ end
163
+ end
164
+ end
165
+ attr_accessor :filters
166
+ def initialize(filters: {}) ; @filters = filters ; end
167
+ end
168
+
169
+ AuthorQuery.new(filters: { articles: { reviews: { published: { max: Time.utc(2024,6,30) } } } }).results
170
+ ```
171
+
172
+ ## Dynamic runtime listener
173
+
174
+ You can build a filter class at runtime from a payload (see Usage → Dynamic runtime listener). The same rules apply: the mapping fixes the join structure; leaf values drive operators.
175
+
176
+ ```ruby
177
+ payload = {
178
+ model: :article, db: :postgres, query_key: :q,
179
+ like: { title: :circumfix, author: { first_name: :prefix } }
180
+ }
181
+ listener = Class.new do
182
+ include Rokaki::FilterModel
183
+ filter_model payload[:model], db: payload[:db]
184
+ define_query_key payload[:query_key]
185
+ filter_map { like payload[:like] }
186
+ attr_accessor :filters
187
+ def initialize(filters: {}) ; @filters = filters ; end
188
+ end
189
+
190
+ listener.new(filters: { q: "First" }).results
191
+ ```
192
+
193
+ ## Adapter behavior
194
+
195
+ - Range/bounds predicates (`BETWEEN`, `>=`, `<=`) are adapter-agnostic; Rokaki binds parameters appropriately for PostgreSQL, MySQL, SQL Server, Oracle, and SQLite.
196
+ - LIKE behavior is adapter-aware (e.g., Postgres `ANY(ARRAY[..])`, SQL Server `ESCAPE` clause, Oracle `UPPER()` for case-insensitive paths). See the Adapters page for details.
197
+
198
+ ## Quick reference
199
+
200
+ - Join-structure keys: associations, declared in code, mirrored in payload structure; never carry operators.
201
+ - Leaf-level keys: columns/fields, declared in code with `filters`, accept values that determine semantics.
202
+ - Reserved leaf operator keys: `between`, `from`/`since`/`after`/`start`/`min`, `to`/`until`/`before`/`end`/`max`.
203
+ - Arrays: always equality `IN`.
204
+ - Ranges or operator-hash: range filtering.
data/docs/index.md CHANGED
@@ -14,6 +14,7 @@ Rokaki is a small Ruby library that helps you build safe, composable filters for
14
14
 
15
15
  Get started below or jump to:
16
16
  - [Usage](./usage)
17
+ - [Rokaki's DSL Syntax](./dsl-syntax)
17
18
  - [Database adapters](./adapters)
18
19
  - [Configuration](./configuration)
19
20
 
@@ -22,7 +23,7 @@ Get started below or jump to:
22
23
  Add to your application's Gemfile:
23
24
 
24
25
  ```ruby
25
- gem "rokaki", "~> 0.15"
26
+ gem "rokaki", "~> 0.18"
26
27
  ```
27
28
 
28
29
  Then:
@@ -102,13 +103,13 @@ Where `params` can include keys like `q`, `author_first_name`, `author_last_name
102
103
 
103
104
  All modes accept either a single string or an array of terms.
104
105
 
105
- ## What’s new in 0.13.0
106
+ ## What’s new in 0.17.0
106
107
 
107
- - Block-form DSL parity across both FilterModel and Filterable
108
- - Circumfix affix synonyms supported: :parafix, :confix, :ambifix
109
- - SQL Server adapter support and CI coverage
110
- - ENV overrides for all adapters in test helpers; improved DB bootstrap in specs
111
- - 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).
112
113
 
113
114
  ## Next steps
114
115
 
data/docs/oracle.md ADDED
@@ -0,0 +1,138 @@
1
+ ---
2
+ layout: page
3
+ title: Oracle connections
4
+ permalink: /adapters/oracle
5
+ ---
6
+
7
+ This page collects Oracle‑specific connection tips for Rokaki (and ActiveRecord in general), including environment variables, client library notes, and how to avoid common errors during local development and CI runs.
8
+
9
+ Rokaki uses ActiveRecord’s `oracle_enhanced` adapter and ruby‑oci8 under the hood. All examples below assume ActiveRecord 7.1–8.x as used by Rokaki.
10
+
11
+ ## Quick start: commands that work
12
+
13
+ - Preferred full descriptor (stable across environments):
14
+
15
+ ```bash
16
+ RBENV_VERSION=3.3.0 \
17
+ ORACLE_USERNAME=system ORACLE_PASSWORD=oracle \
18
+ NLS_LANG=AMERICAN_AMERICA.AL32UTF8 \
19
+ ORACLE_DATABASE='(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=FREEPDB1)))' \
20
+ bundle exec rspec spec/lib/04_oracle_aware_spec.rb --format documentation
21
+ ```
22
+
23
+ - EZCONNECT (be sure to include the double slash prefix):
24
+
25
+ ```bash
26
+ RBENV_VERSION=3.3.0 \
27
+ ORACLE_DATABASE=//127.0.0.1:1521/FREEPDB1 \
28
+ ORACLE_USERNAME=system ORACLE_PASSWORD=oracle \
29
+ NLS_LANG=AMERICAN_AMERICA.AL32UTF8 \
30
+ bundle exec rspec spec/lib/04_oracle_aware_spec.rb
31
+ ```
32
+
33
+ Notes:
34
+ - Oracle Free images typically expose a pluggable database (PDB) service named `FREEPDB1`.
35
+ - Oracle XE images typically use `XEPDB1` instead.
36
+ - If you hit listener errors (ORA‑12514/12521), verify the exact service via `lsnrctl status` inside your container and confirm host port mapping.
37
+
38
+ ## Environment variables Rokaki tests understand
39
+
40
+ The spec helper accepts overrides which are passed to ActiveRecord’s connection config (see `spec/support/database_manager.rb`):
41
+
42
+ - `ORACLE_HOST` — defaults to `localhost`
43
+ - `ORACLE_PORT` — defaults to `1521`
44
+ - `ORACLE_USERNAME` — database username
45
+ - `ORACLE_PASSWORD` — database password
46
+ - `ORACLE_DATABASE` — EZCONNECT or TNS/descriptor, takes precedence over `ORACLE_SERVICE_NAME`
47
+ - `ORACLE_SERVICE_NAME` — service name (e.g., `FREEPDB1` or `XEPDB1`)
48
+ - `NLS_LANG` — recommended: `AMERICAN_AMERICA.AL32UTF8`
49
+
50
+ If only `ORACLE_SERVICE_NAME` is provided, Rokaki’s test helper composes a full descriptor automatically. If `ORACLE_DATABASE` is provided, it is used as‑is (recommended for EZCONNECT or explicit descriptors).
51
+
52
+ ## ruby‑oci8 and Instant Client
53
+
54
+ - Build‑time (already set in this repo’s `.bundle/config`):
55
+
56
+ ```yaml
57
+ BUNDLE_BUILD__RUBY___OCI8: "--with-instant-client-dir=/opt/oracle/instantclient_23_3 \
58
+ --with-instant-client-include=/opt/oracle/instantclient_23_3/sdk/include \
59
+ --with-instant-client-lib=/opt/oracle/instantclient_23_3"
60
+ ```
61
+
62
+ - Runtime (set these if the client libraries aren’t found or you see NLS errors):
63
+
64
+ macOS:
65
+ ```bash
66
+ export DYLD_LIBRARY_PATH=/opt/oracle/instantclient_23_3:$DYLD_LIBRARY_PATH
67
+ ```
68
+ Linux:
69
+ ```bash
70
+ export LD_LIBRARY_PATH=/opt/oracle/instantclient_23_3:$LD_LIBRARY_PATH
71
+ ```
72
+ Optional (explicit NLS data path):
73
+ ```bash
74
+ export OCI_NLS10=/opt/oracle/instantclient_23_3/nls/data
75
+ ```
76
+
77
+ ## Common errors and fixes
78
+
79
+ - ORA‑12705: Cannot access NLS data files or invalid environment specified
80
+ - Cause: invalid/missing NLS settings or client libraries not found.
81
+ - Fix: set `NLS_LANG=AMERICAN_AMERICA.AL32UTF8` (or leave unset), ensure Instant Client libraries are on `DYLD_LIBRARY_PATH` (macOS) or `LD_LIBRARY_PATH` (Linux). Optionally set `OCI_NLS10`.
82
+
83
+ - ORA‑12514 / ORA‑12521: TNS:listener does not currently know of service requested in connect descriptor / service not registered
84
+ - Cause: wrong `SERVICE_NAME`, wrong host/port, container not exposing the service.
85
+ - Fix: run `lsnrctl status` inside the container; use the exact `SERVICE_NAME` (e.g., `FREEPDB1`), confirm host port mapping. For EZCONNECT, remember the `//` prefix: `//HOST:PORT/SERVICE`.
86
+
87
+ - ORA‑01017: invalid username/password; logon denied
88
+ - Cause: wrong credentials for the target service/PDB.
89
+ - Fix: double‑check username/password; for tests you can connect as `SYSTEM` to bootstrap schema, or create a dedicated test user (see below).
90
+
91
+ ## Creating a dedicated test schema user
92
+
93
+ From `SYSTEM` (connected to the target PDB service):
94
+
95
+ ```sql
96
+ CREATE USER ROKAKI IDENTIFIED BY rokaki;
97
+ GRANT CONNECT, RESOURCE, CREATE TABLE, CREATE SEQUENCE TO ROKAKI;
98
+ ALTER USER ROKAKI QUOTA UNLIMITED ON USERS;
99
+ ```
100
+
101
+ Then connect with:
102
+
103
+ ```bash
104
+ ORACLE_USERNAME=ROKAKI ORACLE_PASSWORD=rokaki \
105
+ ORACLE_DATABASE=//127.0.0.1:1521/FREEPDB1 \
106
+ NLS_LANG=AMERICAN_AMERICA.AL32UTF8 \
107
+ bundle exec rspec spec/lib/04_oracle_aware_spec.rb
108
+ ```
109
+
110
+ ## Rails `database.yml` examples
111
+
112
+ Using `oracle_enhanced` with service name:
113
+
114
+ ```yaml
115
+ production:
116
+ adapter: oracle_enhanced
117
+ host: <%= ENV["ORACLE_HOST"] || "localhost" %>
118
+ port: <%= (ENV["ORACLE_PORT"] || 1521).to_i %>
119
+ username: <%= ENV["ORACLE_USERNAME"] %>
120
+ password: <%= ENV["ORACLE_PASSWORD"] %>
121
+ service_name: <%= ENV["ORACLE_SERVICE_NAME"] || "FREEPDB1" %>
122
+ ```
123
+
124
+ Using EZCONNECT/descriptor directly:
125
+
126
+ ```yaml
127
+ production:
128
+ adapter: oracle_enhanced
129
+ username: <%= ENV["ORACLE_USERNAME"] %>
130
+ password: <%= ENV["ORACLE_PASSWORD"] %>
131
+ database: <%= ENV["ORACLE_DATABASE"] %> # e.g., //127.0.0.1:1521/FREEPDB1
132
+ ```
133
+
134
+ ## CI and local tips
135
+
136
+ - Prefer the full `(DESCRIPTION=...)` form in CI to avoid resolver quirks.
137
+ - On Oracle Free containers the default service is `FREEPDB1`; on XE it’s `XEPDB1`.
138
+ - If your tests need to create tables, use a user with `CREATE TABLE` and `CREATE SEQUENCE` privileges (our specs do this automatically).
data/docs/usage.md CHANGED
@@ -5,13 +5,14 @@ permalink: /usage
5
5
  ---
6
6
 
7
7
  This page shows how to use Rokaki to define filters and apply them to ActiveRecord relations.
8
+ For a formal description of the mapping DSL and how payloads are interpreted (join structure vs leaf-level keys), see Rokaki's DSL Syntax: [/dsl-syntax](/dsl-syntax).
8
9
 
9
10
  ## Installation
10
11
 
11
12
  Add the gem to your Gemfile and bundle:
12
13
 
13
14
  ```ruby
14
- gem "rokaki", "~> 0.15"
15
+ gem "rokaki", "~> 0.18"
15
16
  ```
16
17
 
17
18
  ```bash
@@ -80,6 +81,85 @@ Each accepts a single string or an array of strings. Rokaki generates adapter‑
80
81
  - MySQL: `LIKE`/`LIKE BINARY` and, in nested-like contexts, `REGEXP` where designed
81
82
  - SQL Server: `LIKE` with safe escaping; arrays expand into OR chains of parameterized `LIKE` predicates
82
83
 
84
+ ## Range, BETWEEN, MIN, and MAX filters
85
+
86
+ Rokaki supports range-style filters as normal filters (not aggregates) across all adapters. You don’t have to declare special operators per field — the value shape (and optional sub-keys) drive the behavior.
87
+
88
+ Preferred syntax: use the field name as the key and the filter type as a sub-key. Aliases are supported.
89
+
90
+ - Sub-keys:
91
+ - `between` → interpret the value as a range and generate `BETWEEN`/`>=`/`<=` as appropriate
92
+ - Lower bound aliases → `>=`: `from`, `since`, `after`, `start`, `min`
93
+ - Upper bound aliases → `<=`: `to`, `until`, `before`, `end`, `max`
94
+
95
+ Accepted value shapes for `between` (also works when you pass a range directly as the field value):
96
+ - Ruby `Range`: `1..10`, `Time.utc(2024,1,1)..Time.utc(2024,12,31)`
97
+ - Two-element `Array`: `[from, to]` (only when wrapped with `{ between: [...] }`)
98
+ - Hash with aliases: `{ from:, to: }`, `{ since:, until: }`, `{ after:, before: }`, `{ start:, end: }`
99
+
100
+ Examples (top-level field):
101
+
102
+ ```ruby
103
+ class ArticleQuery
104
+ include Rokaki::FilterModel
105
+ filter_model :article
106
+
107
+ # equality filters (existing)
108
+ filters :author_id, :published
109
+
110
+ # LIKEs (existing)
111
+ define_query_key :q
112
+ like title: :circumfix
113
+ end
114
+
115
+ # Between with a Range
116
+ Article.filter(published: Date.new(2024,1,1)..Date.new(2024,12,31))
117
+
118
+ # Between with a Hash + aliases
119
+ Article.filter(published: { from: Date.new(2024,1,1), to: Date.new(2024,12,31) })
120
+ Article.filter(published: { since: Date.new(2024,1,1), until: Date.new(2024,6,30) })
121
+
122
+ # Open-ended bounds
123
+ Article.filter(published: { min: Date.new(2024,1,1) }) # >= 2024-01-01
124
+ Article.filter(published: { max: Date.new(2024,12,31) }) # <= 2024-12-31
125
+
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)] })
130
+ ```
131
+
132
+ Nested fields use the same sub-keys and value shapes:
133
+
134
+ ```ruby
135
+ class ArticleQuery
136
+ include Rokaki::FilterModel
137
+ filter_model :article
138
+
139
+ filter_map do
140
+ nested :author do
141
+ # Range filters are value-driven; declaring the field enables the param key
142
+ filters :created_at # enables :author_created_at
143
+ end
144
+
145
+ nested :reviews do
146
+ filters :published # enables :reviews_published
147
+ end
148
+ end
149
+ end
150
+
151
+ # Params examples
152
+ Article.filter(author_created_at: { from: Time.utc(2024,1,1), to: Time.utc(2024,6,30) })
153
+ Article.filter(reviews_published: (Time.utc(2024,1,1)..Time.utc(2024,6,30)))
154
+ ```
155
+
156
+ Behavior notes:
157
+ - `min`/`max` are interpreted as lower/upper bounds, not aggregate functions.
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: [...] }`.
159
+ - Arrays with more than two elements are treated as equality lists (`IN (?)`) — use `{ between: [...] }` if you intend a range.
160
+ - `nil` bounds are ignored: only the provided side is applied (e.g., `{ from: t }` becomes `>= t`).
161
+ - All generated predicates are parameterized and adapter‑agnostic (`BETWEEN`, `>=`, `<=`).
162
+
83
163
  ## Nested filters
84
164
 
85
165
  Use `nested :association` to scope filters to joined tables. Rokaki handles the necessary joins and qualified columns.
@@ -307,3 +387,33 @@ Notes:
307
387
  - This approach is production‑ready and requires no core changes to Rokaki.
308
388
  - You can cache the generated class by a digest of the payload to avoid recompiling.
309
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.
@@ -3,13 +3,16 @@
3
3
  module Rokaki
4
4
  module FilterModel
5
5
  class BasicFilter
6
- def initialize(keys:, prefix:, infix:, like_semantics:, i_like_semantics:, db:)
6
+ def initialize(keys:, prefix:, infix:, like_semantics:, i_like_semantics:, db:, between_keys: nil, min_keys: nil, max_keys: nil)
7
7
  @keys = keys
8
8
  @prefix = prefix
9
9
  @infix = infix
10
10
  @like_semantics = like_semantics
11
11
  @i_like_semantics = i_like_semantics
12
12
  @db = db
13
+ @between_keys = Array(between_keys).compact
14
+ @min_keys = Array(min_keys).compact
15
+ @max_keys = Array(max_keys).compact
13
16
  @filter_query = nil
14
17
  end
15
18
  attr_reader :keys, :prefix, :infix, :like_semantics, :i_like_semantics, :db, :filter_query
@@ -83,12 +86,144 @@ module Rokaki
83
86
  key: key
84
87
  )
85
88
  else
86
- query = "@model.where(#{key}: #{filter})"
89
+ # New preferred style: field => { between:/from:/to:/min:/max: }
90
+ # Also accept direct Range/Array/Hash with from/to aliases.
91
+ query = <<-RUBY
92
+ begin
93
+ _val = #{filter}
94
+ if _val.is_a?(Hash)
95
+ # Support wrapper keys like :between as well as bound aliases/min/max
96
+ _inner = _val
97
+ if _val.key?(:between) || _val.key?('between')
98
+ _inner = _val[:between] || _val['between']
99
+ end
100
+ _from = _inner[:from] || _inner['from'] || _inner[:since] || _inner['since'] || _inner[:after] || _inner['after'] || _inner[:start] || _inner['start'] || _inner[:min] || _inner['min']
101
+ _to = _inner[:to] || _inner['to'] || _inner[:until] || _inner['until'] || _inner[:before] || _inner['before'] || _inner[:end] || _inner['end'] || _inner[:max] || _inner['max']
102
+
103
+ if _from.nil? && _to.nil?
104
+ # If hash contains range-like but with different container (e.g., { between: range })
105
+ if _inner.is_a?(Range)
106
+ _from = _inner.begin; _to = _inner.end
107
+ elsif _inner.is_a?(Array)
108
+ _from, _to = _inner[0], _inner[1]
109
+ end
110
+ end
111
+
112
+ # Adjust inclusive end-of-day behavior if upper bound appears to be a date or midnight time
113
+ if !_to.nil? && (_to.is_a?(Date) && !_to.is_a?(DateTime) || (_to.respond_to?(:hour) && _to.hour == 0 && _to.min == 0 && _to.sec == 0))
114
+ _to = (_to.respond_to?(:to_time) ? _to.to_time : _to) + 86399
115
+ end
116
+ if !_from.nil? && !_to.nil?
117
+ @model.where("#{key} BETWEEN :from AND :to", from: _from, to: _to)
118
+ elsif !_from.nil?
119
+ @model.where("#{key} >= :from", from: _from)
120
+ elsif !_to.nil?
121
+ @model.where("#{key} <= :to", to: _to)
122
+ else
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
155
+ end
156
+ elsif _val.is_a?(Range)
157
+ #{build_between_query(filter: filter, key: key)}
158
+ else
159
+ # Equality and IN semantics for arrays and scalars (Arrays are always IN lists)
160
+ @model.where(#{key}: _val)
161
+ end
162
+ end
163
+ RUBY
87
164
  end
88
165
 
89
166
  @filter_query = query
90
167
  end
91
168
 
169
+ def build_between_query(filter:, key:)
170
+ # Accept [from, to], Range, or {from:, to:}
171
+ # Build appropriate where conditions with bound params
172
+ <<-RUBY
173
+ begin
174
+ _val = #{filter}
175
+ _from = _to = nil
176
+ if _val.is_a?(Range)
177
+ _from = _val.begin
178
+ _to = _val.end
179
+ elsif _val.is_a?(Array)
180
+ _from, _to = _val[0], _val[1]
181
+ elsif _val.is_a?(Hash)
182
+ # allow aliases for from/to
183
+ _from = _val[:from] || _val['from'] || _val[:since] || _val['since'] || _val[:after] || _val['after'] || _val[:start] || _val['start']
184
+ _to = _val[:to] || _val['to'] || _val[:until] || _val['until'] || _val[:before] || _val['before'] || _val[:end] || _val['end']
185
+ else
186
+ # single value → equality
187
+ return @model.where(#{key}: _val)
188
+ end
189
+
190
+ if !_from.nil? && !_to.nil?
191
+ @model.where("#{key} BETWEEN :from AND :to", from: _from, to: _to)
192
+ elsif !_from.nil?
193
+ @model.where("#{key} >= :from", from: _from)
194
+ elsif !_to.nil?
195
+ @model.where("#{key} <= :to", to: _to)
196
+ else
197
+ @model
198
+ end
199
+ end
200
+ RUBY
201
+ end
202
+
203
+ def parse_range_semantics(key)
204
+ k = key.to_s
205
+ %w[_between _min _max _from _to _after _before _since _until _start _end].each do |suf|
206
+ if k.end_with?(suf)
207
+ base = k.sub(/#{Regexp.escape(suf)}\z/, '')
208
+ op = case suf
209
+ when '_between' then :between
210
+ when '_min' then :min
211
+ when '_max' then :max
212
+ when '_from','_after','_since','_start' then :from
213
+ when '_to','_before','_until','_end' then :to
214
+ else nil
215
+ end
216
+ return [base, op]
217
+ end
218
+ end
219
+ [nil, nil]
220
+ end
221
+
222
+ def build_compare_query(op:, filter:, column:)
223
+ operator = (op == :'>=') ? '>=' : '<='
224
+ %Q{@model.where("#{column} #{operator} :v", v: #{filter})}
225
+ end
226
+
92
227
  # # @model.where('`authors`.`first_name` LIKE BINARY :query', query: "%teev%").or(@model.where('`authors`.`first_name` LIKE BINARY :query', query: "%imi%"))
93
228
  # if Array == filter
94
229
  # first_term = filter.unshift
@@ -129,21 +129,17 @@ module Rokaki
129
129
  where_after.push(" }")
130
130
  end
131
131
 
132
- joins = joins_before + joins_after
132
+ joins_arr = joins_before + joins_after
133
+ joins_str = joins_arr.join
133
134
 
134
135
  name += "#{leaf}"
135
- where_middle = ["{ #{leaf}: #{prefix}#{name} }"]
136
-
137
- where = where_before + where_middle + where_after
138
- joins = joins.join
139
- where = where.join
140
136
 
141
137
  if search_mode
142
138
  if db == :sqlserver || db == :oracle
143
139
  key_leaf = "#{keys.last.to_s.pluralize}.#{leaf}"
144
140
  helper = db == :sqlserver ? 'sqlserver_like' : 'oracle_like'
145
141
  @filter_methods << "def #{prefix}filter#{infix}#{name};"\
146
- "#{helper}(@model.joins(#{joins}), \"#{key_leaf}\", \"#{type}\", #{prefix}#{name}, :#{search_mode}); end;"
142
+ "#{helper}(@model.joins(#{joins_str}), \"#{key_leaf}\", \"#{type}\", #{prefix}#{name}, :#{search_mode}); end;"
147
143
 
148
144
  @filter_templates << "@model = #{prefix}filter#{infix}#{name} if #{prefix}#{name};"
149
145
  else
@@ -157,14 +153,81 @@ module Rokaki
157
153
  )
158
154
 
159
155
  @filter_methods << "def #{prefix}filter#{infix}#{name};"\
160
- "@model.joins(#{joins}).#{query}; end;"
156
+ "@model.joins(#{joins_str}).#{query}; end;"
161
157
 
162
158
  @filter_templates << "@model = #{prefix}filter#{infix}#{name} if #{prefix}#{name};"
163
159
  end
164
160
  else
165
- @filter_methods << "def #{prefix}filter#{infix}#{name};"\
166
- "@model.joins(#{joins}).where(#{where}); end;"
161
+ # Preferred: value Hash with sub-keys between/from/to/min/max; also accept Range/Array directly
162
+ qualified_col = "#{keys.last.to_s.pluralize}.#{leaf}"
163
+ body = <<-RUBY
164
+ begin
165
+ _val = #{prefix}#{name}
166
+ rel = @model.joins(#{joins_str})
167
+ if _val.is_a?(Hash)
168
+ _inner = _val
169
+ if _val.key?(:between) || _val.key?('between')
170
+ _inner = _val[:between] || _val['between']
171
+ end
172
+ _from = _inner[:from] || _inner['from'] || _inner[:since] || _inner['since'] || _inner[:after] || _inner['after'] || _inner[:start] || _inner['start'] || _inner[:min] || _inner['min']
173
+ _to = _inner[:to] || _inner['to'] || _inner[:until] || _inner['until'] || _inner[:before] || _inner['before'] || _inner[:end] || _inner['end'] || _inner[:max] || _inner['max']
174
+ if _from.nil? && _to.nil?
175
+ if _inner.is_a?(Range)
176
+ _from = _inner.begin; _to = _inner.end
177
+ elsif _inner.is_a?(Array)
178
+ _from, _to = _inner[0], _inner[1]
179
+ end
180
+ end
181
+ if !_from.nil? && !_to.nil?
182
+ rel.where("#{qualified_col} BETWEEN :from AND :to", from: _from, to: _to)
183
+ elsif !_from.nil?
184
+ rel.where("#{qualified_col} >= :from", from: _from)
185
+ elsif !_to.nil?
186
+ rel.where("#{qualified_col} <= :to", to: _to)
187
+ else
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']
167
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
219
+ end
220
+ elsif _val.is_a?(Range)
221
+ rel.where("#{qualified_col} BETWEEN :from AND :to", from: _val.begin, to: _val.end)
222
+ elsif _val.is_a?(Array)
223
+ # Arrays represent IN semantics for equality; use BETWEEN only when explicitly wrapped via :between
224
+ rel.where("#{qualified_col} IN (?)", _val)
225
+ else
226
+ rel.where("#{qualified_col} = :v", v: _val)
227
+ end
228
+ end
229
+ RUBY
230
+ @filter_methods << "def #{prefix}filter#{infix}#{name};#{body}; end;"
168
231
  @filter_templates << "@model = #{prefix}filter#{infix}#{name} if #{prefix}#{name};"
169
232
  end
170
233
  end
@@ -182,6 +245,25 @@ module Rokaki
182
245
  query
183
246
  end
184
247
 
248
+ def parse_range_semantics(key)
249
+ k = key.to_s
250
+ %w[_between _min _max _from _to _after _before _since _until _start _end].each do |suf|
251
+ if k.end_with?(suf)
252
+ base = k.sub(/#{Regexp.escape(suf)}\z/, '')
253
+ op = case suf
254
+ when '_between' then :between
255
+ when '_min' then :from # min → lower bound
256
+ when '_max' then :to # max → upper bound
257
+ when '_from','_after','_since','_start' then :from
258
+ when '_to','_before','_until','_end' then :to
259
+ else nil
260
+ end
261
+ return [base, op]
262
+ end
263
+ end
264
+ [nil, nil]
265
+ end
266
+
185
267
  end
186
268
  end
187
269
  end
@@ -1,3 +1,3 @@
1
1
  module Rokaki
2
- VERSION = "0.16.0"
2
+ VERSION = "0.18.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rokaki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Martin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-27 00:00:00.000000000 Z
11
+ date: 2025-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -296,7 +296,9 @@ files:
296
296
  - docs/_config.yml
297
297
  - docs/adapters.md
298
298
  - docs/configuration.md
299
+ - docs/dsl_syntax.md
299
300
  - docs/index.md
301
+ - docs/oracle.md
300
302
  - docs/usage.md
301
303
  - lib/rokaki.rb
302
304
  - lib/rokaki/filter_model.rb