rokaki 0.9.0 → 0.11.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: 231b27e39d8bc7c95a1b3d217bd28cf9129fa9463e949b7d8626ea5213fefdbc
4
- data.tar.gz: b533f1aae5281fbf5403b81dd81d2ac483a231eb419510abb3df7348017b24a5
3
+ metadata.gz: a8fb8c022307112af51183df513130d6ad467142320a4db9be178870583c2394
4
+ data.tar.gz: 611539f7d9500f30c5a847a89a7548119c14d4eac47761e9c08b5e706ae9f77c
5
5
  SHA512:
6
- metadata.gz: 726dd3cea7f2f9e6c83c1456503bd16d2b71a844514a7740873b86f0122602a598ca059ebb1a6609104e029ab913723d812e9ea041b9fcd280cf48cea87c42cb
7
- data.tar.gz: c3b130ad1a46c837e932868f57372b794e7cd1ed6d1c5d1aaf852391fb7cf4a82b228306a2c710a88b6ecd0d2f85697c4c019e769f50beec3bad4c1bd6dc6ff3
6
+ metadata.gz: 3648caca4052f03440da996d1aed083c3ba9a29a5f14f06e9e9b2757d4d95f3356e793e22f1b1ebe0a149536a001367e6006d26d8521ce518885e32c36502130
7
+ data.tar.gz: ca064a797447597677bd5b2d2e00f6a2836bc5e69341990ac7da0742ccdc41383834f7a8c9bb6c4089a37e0293786c2536727b5c79d1dabf05a4c9df20f02b77
@@ -42,7 +42,7 @@ jobs:
42
42
 
43
43
  # Initializes the CodeQL tools for scanning.
44
44
  - name: Initialize CodeQL
45
- uses: github/codeql-action/init@v1
45
+ uses: github/codeql-action/init@v3
46
46
  with:
47
47
  languages: ${{ matrix.language }}
48
48
  # If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,7 +53,7 @@ jobs:
53
53
  # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54
54
  # If this step fails, then you should remove it and run the build manually (see below)
55
55
  - name: Autobuild
56
- uses: github/codeql-action/autobuild@v1
56
+ uses: github/codeql-action/autobuild@v3
57
57
 
58
58
  # ℹ️ Command-line programs to run using the OS shell.
59
59
  # 📚 https://git.io/JvXDl
@@ -67,4 +67,4 @@ jobs:
67
67
  # make release
68
68
 
69
69
  - name: Perform CodeQL Analysis
70
- uses: github/codeql-action/analyze@v1
70
+ uses: github/codeql-action/analyze@v3
@@ -0,0 +1,69 @@
1
+ name: Run RSpec tests
2
+ on: [push]
3
+ jobs:
4
+ run-rspec-tests:
5
+ runs-on: ubuntu-latest
6
+ services:
7
+ mysql:
8
+ image: mysql:9.4
9
+ env:
10
+ MYSQL_DATABASE: rokaki
11
+ MYSQL_USER: rokaki
12
+ MYSQL_PASSWORD: rokaki
13
+ MYSQL_ROOT_PASSWORD: rokaki
14
+ ports:
15
+ - 3306:3306
16
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
17
+
18
+ postgres:
19
+ image: postgres:13
20
+ env:
21
+ POSTGRES_USER: postgres
22
+ POSTGRES_PASSWORD: "postgres"
23
+ POSTGRES_DB: rokaki
24
+ ports:
25
+ - 5432:5432
26
+ # needed because the postgres container does not provide a healthcheck
27
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
28
+
29
+ sqlserver:
30
+ image: mcr.microsoft.com/mssql/server:2022-latest
31
+ env:
32
+ ACCEPT_EULA: "Y"
33
+ MSSQL_SA_PASSWORD: "5QL5£rv£r"
34
+ ports:
35
+ - 1433:1433
36
+ # No built-in healthcheck; we'll wait in a step using nc
37
+
38
+ steps:
39
+ - uses: actions/checkout@v2
40
+
41
+ - name: Install system dependencies
42
+ run: |
43
+ sudo apt-get update
44
+ sudo apt-get install -y build-essential libpq-dev default-libmysqlclient-dev freetds-dev netcat-openbsd
45
+
46
+ - name: Set up Ruby
47
+ uses: ruby/setup-ruby@v1
48
+ with:
49
+ # Not needed with a .ruby-version file
50
+ ruby-version: 3.3.0
51
+ # runs 'bundle install' and caches installed gems automatically
52
+ bundler-cache: true
53
+
54
+ - name: Wait for databases to be ready
55
+ shell: bash
56
+ run: |
57
+ for i in {1..60}; do
58
+ nc -z 127.0.0.1 3306 && echo "MySQL up" && break || sleep 1
59
+ done
60
+ for i in {1..60}; do
61
+ nc -z 127.0.0.1 5432 && echo "Postgres up" && break || sleep 1
62
+ done
63
+ for i in {1..120}; do
64
+ nc -z 127.0.0.1 1433 && echo "SQL Server up" && break || sleep 1
65
+ done
66
+
67
+ - name: Run tests
68
+ run: |
69
+ ./spec/ordered_run.sh
data/Gemfile.lock CHANGED
@@ -1,50 +1,69 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rokaki (0.9.0)
4
+ rokaki (0.11.0)
5
5
  activesupport
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.1.3.2)
11
- activesupport (= 7.1.3.2)
12
- activerecord (7.1.3.2)
13
- activemodel (= 7.1.3.2)
14
- activesupport (= 7.1.3.2)
10
+ activemodel (8.0.3)
11
+ activesupport (= 8.0.3)
12
+ activerecord (8.0.3)
13
+ activemodel (= 8.0.3)
14
+ activesupport (= 8.0.3)
15
15
  timeout (>= 0.4.0)
16
- activesupport (7.1.3.2)
16
+ activerecord-sqlserver-adapter (8.0.9)
17
+ activerecord (~> 8.0.0)
18
+ tiny_tds
19
+ activesupport (8.0.3)
17
20
  base64
21
+ benchmark (>= 0.3)
18
22
  bigdecimal
19
- concurrent-ruby (~> 1.0, >= 1.0.2)
23
+ concurrent-ruby (~> 1.0, >= 1.3.1)
20
24
  connection_pool (>= 2.2.5)
21
25
  drb
22
26
  i18n (>= 1.6, < 2)
27
+ logger (>= 1.4.2)
23
28
  minitest (>= 5.1)
24
- mutex_m
25
- tzinfo (~> 2.0)
26
- base64 (0.2.0)
27
- bigdecimal (3.1.7)
28
- byebug (11.1.3)
29
+ securerandom (>= 0.3)
30
+ tzinfo (~> 2.0, >= 2.0.5)
31
+ uri (>= 0.13.1)
32
+ base64 (0.3.0)
33
+ benchmark (0.4.1)
34
+ bigdecimal (3.2.3)
35
+ byebug (12.0.0)
29
36
  coderay (1.1.3)
30
- concurrent-ruby (1.2.3)
31
- connection_pool (2.4.1)
32
- database_cleaner-active_record (2.1.0)
37
+ concurrent-ruby (1.3.5)
38
+ connection_pool (2.5.4)
39
+ database_cleaner-active_record (2.2.2)
33
40
  activerecord (>= 5.a)
34
- database_cleaner-core (~> 2.0.0)
41
+ database_cleaner-core (~> 2.0)
35
42
  database_cleaner-core (2.0.1)
36
- diff-lcs (1.5.1)
37
- drb (2.2.1)
38
- factory_bot (6.4.6)
39
- activesupport (>= 5.0.0)
40
- ffi (1.16.3)
41
- formatador (1.1.0)
42
- guard (2.18.1)
43
+ diff-lcs (1.6.2)
44
+ drb (2.2.3)
45
+ factory_bot (6.5.5)
46
+ activesupport (>= 6.1.0)
47
+ ffi (1.17.2-aarch64-linux-gnu)
48
+ ffi (1.17.2-aarch64-linux-musl)
49
+ ffi (1.17.2-arm-linux-gnu)
50
+ ffi (1.17.2-arm-linux-musl)
51
+ ffi (1.17.2-arm64-darwin)
52
+ ffi (1.17.2-x86-linux-gnu)
53
+ ffi (1.17.2-x86-linux-musl)
54
+ ffi (1.17.2-x86_64-darwin)
55
+ ffi (1.17.2-x86_64-linux-gnu)
56
+ ffi (1.17.2-x86_64-linux-musl)
57
+ formatador (1.2.1)
58
+ reline
59
+ guard (2.19.1)
43
60
  formatador (>= 0.2.4)
44
61
  listen (>= 2.7, < 4.0)
62
+ logger (~> 1.6)
45
63
  lumberjack (>= 1.0.12, < 2.0)
46
64
  nenv (~> 0.1)
47
65
  notiffany (~> 0.0)
66
+ ostruct (~> 0.6)
48
67
  pry (>= 0.13.0)
49
68
  shellany (~> 0.0)
50
69
  thor (>= 0.18.1)
@@ -53,60 +72,85 @@ GEM
53
72
  guard (~> 2.1)
54
73
  guard-compat (~> 1.1)
55
74
  rspec (>= 2.99.0, < 4.0)
56
- i18n (1.14.4)
75
+ i18n (1.14.7)
57
76
  concurrent-ruby (~> 1.0)
77
+ io-console (0.8.1)
58
78
  listen (3.9.0)
59
79
  rb-fsevent (~> 0.10, >= 0.10.3)
60
80
  rb-inotify (~> 0.9, >= 0.9.10)
61
- lumberjack (1.2.10)
81
+ logger (1.7.0)
82
+ lumberjack (1.4.2)
62
83
  method_source (1.1.0)
63
- minitest (5.22.3)
64
- mutex_m (0.2.0)
84
+ minitest (5.25.5)
85
+ mysql2 (0.5.7)
86
+ bigdecimal
65
87
  nenv (0.3.0)
66
88
  notiffany (0.1.3)
67
89
  nenv (~> 0.1)
68
90
  shellany (~> 0.0)
69
- pg (1.5.6)
70
- pry (0.14.2)
91
+ ostruct (0.6.3)
92
+ pg (1.6.2)
93
+ pg (1.6.2-aarch64-linux)
94
+ pg (1.6.2-aarch64-linux-musl)
95
+ pg (1.6.2-arm64-darwin)
96
+ pg (1.6.2-x86_64-darwin)
97
+ pg (1.6.2-x86_64-linux)
98
+ pg (1.6.2-x86_64-linux-musl)
99
+ pry (0.15.2)
71
100
  coderay (~> 1.1)
72
101
  method_source (~> 1.0)
73
- pry-byebug (3.10.1)
74
- byebug (~> 11.0)
75
- pry (>= 0.13, < 0.15)
76
- rake (13.2.1)
102
+ pry-byebug (3.11.0)
103
+ byebug (~> 12.0)
104
+ pry (>= 0.13, < 0.16)
105
+ rake (13.3.0)
77
106
  rb-fsevent (0.11.2)
78
- rb-inotify (0.10.1)
107
+ rb-inotify (0.11.1)
79
108
  ffi (~> 1.0)
80
- rspec (3.13.0)
109
+ reline (0.6.2)
110
+ io-console (~> 0.5)
111
+ rspec (3.13.1)
81
112
  rspec-core (~> 3.13.0)
82
113
  rspec-expectations (~> 3.13.0)
83
114
  rspec-mocks (~> 3.13.0)
84
- rspec-core (3.13.0)
115
+ rspec-core (3.13.5)
85
116
  rspec-support (~> 3.13.0)
86
- rspec-expectations (3.13.0)
117
+ rspec-expectations (3.13.5)
87
118
  diff-lcs (>= 1.2.0, < 2.0)
88
119
  rspec-support (~> 3.13.0)
89
- rspec-mocks (3.13.0)
120
+ rspec-mocks (3.13.5)
90
121
  diff-lcs (>= 1.2.0, < 2.0)
91
122
  rspec-support (~> 3.13.0)
92
- rspec-support (3.13.1)
123
+ rspec-support (3.13.6)
124
+ securerandom (0.4.1)
93
125
  shellany (0.0.1)
94
- sqlite3 (2.0.1-aarch64-linux-gnu)
95
- sqlite3 (2.0.1-aarch64-linux-musl)
96
- sqlite3 (2.0.1-arm-linux-gnu)
97
- sqlite3 (2.0.1-arm-linux-musl)
98
- sqlite3 (2.0.1-arm64-darwin)
99
- sqlite3 (2.0.1-x86-linux-gnu)
100
- sqlite3 (2.0.1-x86-linux-musl)
101
- sqlite3 (2.0.1-x86_64-darwin)
102
- sqlite3 (2.0.1-x86_64-linux-gnu)
103
- sqlite3 (2.0.1-x86_64-linux-musl)
104
- thor (1.3.1)
105
- timeout (0.4.1)
126
+ sqlite3 (2.7.4-aarch64-linux-gnu)
127
+ sqlite3 (2.7.4-aarch64-linux-musl)
128
+ sqlite3 (2.7.4-arm-linux-gnu)
129
+ sqlite3 (2.7.4-arm-linux-musl)
130
+ sqlite3 (2.7.4-arm64-darwin)
131
+ sqlite3 (2.7.4-x86-linux-gnu)
132
+ sqlite3 (2.7.4-x86-linux-musl)
133
+ sqlite3 (2.7.4-x86_64-darwin)
134
+ sqlite3 (2.7.4-x86_64-linux-gnu)
135
+ sqlite3 (2.7.4-x86_64-linux-musl)
136
+ thor (1.4.0)
137
+ timeout (0.4.3)
138
+ tiny_tds (3.3.0)
139
+ bigdecimal (~> 3)
140
+ tiny_tds (3.3.0-aarch64-linux-gnu)
141
+ bigdecimal (~> 3)
142
+ tiny_tds (3.3.0-aarch64-linux-musl)
143
+ bigdecimal (~> 3)
144
+ tiny_tds (3.3.0-x86_64-linux-gnu)
145
+ bigdecimal (~> 3)
146
+ tiny_tds (3.3.0-x86_64-linux-musl)
147
+ bigdecimal (~> 3)
106
148
  tzinfo (2.0.6)
107
149
  concurrent-ruby (~> 1.0)
150
+ uri (1.0.3)
108
151
 
109
152
  PLATFORMS
153
+ aarch64-linux
110
154
  aarch64-linux-gnu
111
155
  aarch64-linux-musl
112
156
  arm-linux-gnu
@@ -115,16 +159,19 @@ PLATFORMS
115
159
  x86-linux-gnu
116
160
  x86-linux-musl
117
161
  x86_64-darwin
162
+ x86_64-linux
118
163
  x86_64-linux-gnu
119
164
  x86_64-linux-musl
120
165
 
121
166
  DEPENDENCIES
122
167
  activerecord
168
+ activerecord-sqlserver-adapter
123
169
  bundler (~> 2.0)
124
170
  database_cleaner-active_record
125
171
  factory_bot
126
172
  guard
127
173
  guard-rspec
174
+ mysql2
128
175
  pg
129
176
  pry
130
177
  pry-byebug
@@ -132,6 +179,7 @@ DEPENDENCIES
132
179
  rokaki!
133
180
  rspec (~> 3.0)
134
181
  sqlite3
182
+ tiny_tds
135
183
 
136
184
  BUNDLED WITH
137
185
  2.5.3
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019 Steve Martin
3
+ Copyright (c) 2025 Steve Martin
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Rokaki
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rokaki.svg)](https://badge.fury.io/rb/rokaki)
4
+ [![Run RSpec tests](https://github.com/tevio/rokaki/actions/workflows/spec.yml/badge.svg)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
4
5
 
5
- This gem was born out of a desire to dry up filtering services in Rails apps or any Ruby app that uses the concept of "filters" or "facets".
6
+ This gem was written to dry up filtering services in ActiveRecord based Rails apps or any plain Ruby app looking to implement "filters" or "faceted" search.
6
7
 
7
- There are two modes of use `Filterable` and `FilterModel` that can be activated through the use of two mixins respectively, `include Rokaki::Filterable` or `include Rokaki::FilterModel`.
8
+ The overall vision is to abstract away all of the lower level repetitive SQL and relational code to allow you to write model filters in a simple, relatively intuitive way, using ruby hashes and arrays mostly.
9
+
10
+ The DSL allows you to construct complex search models to filter results through without writing any SQL. I would recommend the reader to consult the specs in order to understand the features and syntax in detail, an intermediate understanding of Ruby and rspec TDD, and basic relational logic are recommended.
11
+
12
+ There are two modes of use, `Filterable` (designed for plain Ruby) and `FilterModel` (designed for Rails) that can be activated through the use of two mixins respectively, `include Rokaki::Filterable` or `include Rokaki::FilterModel`.
8
13
  ## Installation
9
14
 
10
15
  Add this line to your application's Gemfile:
@@ -137,7 +142,7 @@ You can specify several configuration options, for example a `filter_key_prefix`
137
142
  ## `Rokaki::FilterModel` - Usage
138
143
 
139
144
  ### ActiveRecord
140
- Include `Rokaki::FilterModel` in any ActiveRecord model (only AR >= 6.0.0 tested so far) you can generate the filter keys and the actual filter lookup code using the `filters` keyword on a model like so:-
145
+ Include `Rokaki::FilterModel` in any ActiveRecord model (only AR >= 8.0.3 tested so far) you can generate the filter keys and the actual filter lookup code using the `filters` keyword on a model like so:-
141
146
 
142
147
  ```ruby
143
148
  # Given the models
@@ -173,7 +178,7 @@ You can also filter collections of fields, simply pass an array of filter values
173
178
 
174
179
 
175
180
  ### Partial matching
176
- You can use `like` (or, if you use postgres, the case insensitive `ilike`) to perform a partial match on a specific field, there are 3 options:- `:prefix`, `:circumfix` and `:suffix`. There are two syntaxes you can use for this:-
181
+ You can use `like` or the case insensitive `ilike` to perform a partial match on a specific field, there are 3 options:- `:prefix`, `:circumfix` and `:suffix`. There are two syntaxes you can use for this:-
177
182
 
178
183
  #### 1. The `filter` command syntax
179
184
 
@@ -183,7 +188,7 @@ class ArticleFilter
183
188
  include Rokaki::FilterModel
184
189
 
185
190
  filter :article,
186
- like: { # you can use ilike here instead if you use postgres and want case insensitive results
191
+ like: { # you can use ilike here instead if you want case insensitive results
187
192
  author: {
188
193
  first_name: :circumfix,
189
194
  last_name: :circumfix
@@ -323,7 +328,7 @@ class ArticleFilter
323
328
 
324
329
  filters :date, :title, author: [:first_name, :last_name]
325
330
  like title: :circumfix
326
- # ilike title: :circumfix # case insensitive postgres mode
331
+ # ilike title: :circumfix # case insensitive mode
327
332
 
328
333
  attr_accessor :filters
329
334
 
@@ -422,14 +427,27 @@ filterable.results
422
427
  ### Ruby setup
423
428
  After checking out the repo, run `bin/setup` to install dependencies.
424
429
 
425
- ### Setting up the test database
430
+ ### Setting up the test databases
426
431
 
432
+ #### Postgres
427
433
  ```
428
434
  docker pull postgres
429
435
  docker run --name rokaki-postgres -e POSTGRES_USER=rokaki -e POSTGRES_PASSWORD=rokaki -d -p 5432:5432 postgres
430
436
  ```
431
437
 
432
- Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
438
+ #### Mysql
439
+ ```
440
+ docker pull mysql
441
+ docker run --name rokaki-mysql -e MYSQL_ROOT_PASSWORD=rokaki -e MYSQL_PASSWORD=rokaki -e MYSQL_DATABASE=rokaki -e MYSQL_USER=rokaki -d -p 3306:3306 mysql:latest mysqld
442
+ ```
443
+
444
+ ### Specialised test runner
445
+ The test suite is designed to run against all supported database backends, since this can cause problems with timing and connection pools, the recommended way to run the tests is via a shell script.
446
+
447
+ From the rokaki root directory run: `./spec/ordered_run.sh`. This is the same script that runs on the Github CI here: [![Run RSpec tests](https://github.com/tevio/rokaki/actions/workflows/spec.yml/badge.svg)](https://github.com/tevio/rokaki/actions/workflows/spec.yml)
448
+
449
+ ### Standard test runner (only recommended for development cycles)
450
+ You can still run `rake spec` to run the tests, there's no guarantee they will all pass due to race conditions from using multiple db backends (see above), but this mode is recommended for focusing on specific backends or tests during development (comment out what you don't want). You can also run `bin/console` for an interactive prompt that will allow you to experiment.
433
451
 
434
452
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
435
453
 
@@ -33,13 +33,37 @@ module Rokaki
33
33
  @filter_template = "@model = #{prefix}filter_#{name} if #{filter};"
34
34
  end
35
35
 
36
+ def case_sensitive
37
+ if db == :postgres
38
+ 'LIKE'
39
+ elsif db == :mysql
40
+ 'LIKE BINARY'
41
+ elsif db == :sqlserver
42
+ 'LIKE'
43
+ else
44
+ 'LIKE'
45
+ end
46
+ end
47
+
48
+ def case_insensitive
49
+ if db == :postgres
50
+ 'ILIKE'
51
+ elsif db == :mysql
52
+ 'LIKE'
53
+ elsif db == :sqlserver
54
+ 'LIKE'
55
+ else
56
+ 'LIKE'
57
+ end
58
+ end
59
+
36
60
  def _chain_filter_type(key)
37
61
  filter = "#{prefix}#{key}"
38
62
  query = ''
39
63
 
40
64
  if like_semantics && mode = like_semantics[key]
41
65
  query = build_like_query(
42
- type: 'LIKE',
66
+ type: case_sensitive,
43
67
  query: query,
44
68
  filter: filter,
45
69
  mode: mode,
@@ -47,7 +71,7 @@ module Rokaki
47
71
  )
48
72
  elsif i_like_semantics && mode = i_like_semantics[key]
49
73
  query = build_like_query(
50
- type: 'ILIKE',
74
+ type: case_insensitive,
51
75
  query: query,
52
76
  filter: filter,
53
77
  mode: mode,
@@ -60,10 +84,27 @@ module Rokaki
60
84
  @filter_query = query
61
85
  end
62
86
 
87
+ # # @model.where('`authors`.`first_name` LIKE BINARY :query', query: "%teev%").or(@model.where('`authors`.`first_name` LIKE BINARY :query', query: "%imi%"))
88
+ # if Array == filter
89
+ # first_term = filter.unshift
90
+ # query = "@model.where(\"#{key} #{type} ANY (ARRAY[?])\", "
91
+ # query += "prepare_terms(#{first_term}, :#{mode}))"
92
+ # filter.each { |term|
93
+ # query += ".or(@model.where(\"#{key} #{type} ANY (ARRAY[?])\", "
94
+ # query += "prepare_terms(#{first_term}, :#{mode})))"
95
+ # }
96
+ # else
97
+ # query = "@model.where(\"#{key.to_s.split(".").map { |item| "`#{item}`" }.join(".")} #{type.to_s.upcase} :query\", "
98
+ # query += "query: prepare_terms(#{filter}, \"#{type.to_s.upcase}\", :#{search_mode}))"
99
+ # end
100
+
63
101
  def build_like_query(type:, query:, filter:, mode:, key:)
64
102
  if db == :postgres
65
103
  query = "@model.where(\"#{key} #{type} ANY (ARRAY[?])\", "
66
104
  query += "prepare_terms(#{filter}, :#{mode}))"
105
+ elsif db == :sqlserver
106
+ # Delegate to helper that supports arrays and escaping with ESCAPE
107
+ query = "sqlserver_like(@model, \"#{key}\", \"#{type}\", #{filter}, :#{mode})"
67
108
  else
68
109
  query = "@model.where(\"#{key} #{type} :query\", "
69
110
  query += "query: \"%\#{#{filter}}%\")" if mode == :circumfix
@@ -65,6 +65,30 @@ module Rokaki
65
65
  current_like_key
66
66
  end
67
67
 
68
+ def case_sensitive
69
+ if db == :postgres
70
+ 'LIKE'
71
+ elsif db == :mysql
72
+ 'LIKE BINARY'
73
+ elsif db == :sqlserver
74
+ 'LIKE'
75
+ else
76
+ 'LIKE'
77
+ end
78
+ end
79
+
80
+ def case_insensitive
81
+ if db == :postgres
82
+ 'ILIKE'
83
+ elsif db == :mysql
84
+ 'LIKE'
85
+ elsif db == :sqlserver
86
+ 'LIKE'
87
+ else
88
+ 'LIKE'
89
+ end
90
+ end
91
+
68
92
  def _build_deep_chain(keys)
69
93
  name = ''
70
94
  count = keys.size - 1
@@ -80,9 +104,9 @@ module Rokaki
80
104
  leaf = nil
81
105
 
82
106
  if search_mode = find_like_key(keys)
83
- type = 'LIKE'
107
+ type = case_sensitive
84
108
  elsif search_mode = find_i_like_key(keys)
85
- type = 'ILIKE'
109
+ type = case_insensitive
86
110
  end
87
111
  leaf = keys.pop
88
112
 
@@ -115,19 +139,27 @@ module Rokaki
115
139
  where = where.join
116
140
 
117
141
  if search_mode
118
- query = build_like_query(
119
- type: type,
120
- query: '',
121
- filter: "#{prefix}#{name}",
122
- search_mode: search_mode,
123
- key: keys.last.to_s.pluralize,
124
- leaf: leaf
125
- )
126
-
127
- @filter_methods << "def #{prefix}filter#{infix}#{name};"\
128
- "@model.joins(#{joins}).#{query}; end;"
142
+ if db == :sqlserver
143
+ key_leaf = "#{keys.last.to_s.pluralize}.#{leaf}"
144
+ @filter_methods << "def #{prefix}filter#{infix}#{name};"\
145
+ "sqlserver_like(@model.joins(#{joins}), \"#{key_leaf}\", \"#{type}\", #{prefix}#{name}, :#{search_mode}); end;"
129
146
 
130
- @filter_templates << "@model = #{prefix}filter#{infix}#{name} if #{prefix}#{name};"
147
+ @filter_templates << "@model = #{prefix}filter#{infix}#{name} if #{prefix}#{name};"
148
+ else
149
+ query = build_like_query(
150
+ type: type,
151
+ query: '',
152
+ filter: "#{prefix}#{name}",
153
+ search_mode: search_mode,
154
+ key: keys.last.to_s.pluralize,
155
+ leaf: leaf
156
+ )
157
+
158
+ @filter_methods << "def #{prefix}filter#{infix}#{name};"\
159
+ "@model.joins(#{joins}).#{query}; end;"
160
+
161
+ @filter_templates << "@model = #{prefix}filter#{infix}#{name} if #{prefix}#{name};"
162
+ end
131
163
  else
132
164
  @filter_methods << "def #{prefix}filter#{infix}#{name};"\
133
165
  "@model.joins(#{joins}).where(#{where}); end;"
@@ -144,7 +176,6 @@ module Rokaki
144
176
  query = "where(\"#{key}.#{leaf} #{type} :query\", "
145
177
  query += "query: \"%\#{#{filter}}%\")" if search_mode == :circumfix
146
178
  query += "query: \"%\#{#{filter}}\")" if search_mode == :prefix
147
- query += "query: \"\#{#{filter}}%\")" if search_mode == :suffix
148
179
  end
149
180
 
150
181
  query
@@ -194,22 +194,37 @@ module Rokaki
194
194
  leaf = nil
195
195
  leaf = keys.pop
196
196
 
197
+ # Compute key_leaf (qualified column) like other branches
198
+ key_leaf = keys.last ? "#{keys.last.to_s.pluralize}.#{leaf}" : leaf
199
+
200
+ if db == :sqlserver
201
+ # Build relation base with joins
202
+ if join_map.empty?
203
+ rel_expr = "@model"
204
+ elsif join_map.is_a?(Array)
205
+ rel_expr = "@model.joins(*#{join_map})"
206
+ else
207
+ rel_expr = "@model.joins(**#{join_map})"
208
+ end
197
209
 
198
- query = build_like_query(
199
- type: type,
200
- query: '',
201
- filter: filter_name,
202
- search_mode: search_mode,
203
- key: keys.last,
204
- leaf: leaf
205
- )
206
-
207
- if join_map.empty?
208
- filter_query = "@model.#{query}"
209
- elsif join_map.is_a?(Array)
210
- filter_query = "@model.joins(*#{join_map}).#{query}"
210
+ filter_query = "sqlserver_like(#{rel_expr}, \"#{key_leaf}\", \"#{type.to_s.upcase}\", #{filter_name}, :#{search_mode})"
211
211
  else
212
- filter_query = "@model.joins(**#{join_map}).#{query}"
212
+ query = build_like_query(
213
+ type: type,
214
+ query: '',
215
+ filter: filter_name,
216
+ search_mode: search_mode,
217
+ key: keys.last,
218
+ leaf: leaf
219
+ )
220
+
221
+ if join_map.empty?
222
+ filter_query = "@model.#{query}"
223
+ elsif join_map.is_a?(Array)
224
+ filter_query = "@model.joins(*#{join_map}).#{query}"
225
+ else
226
+ filter_query = "@model.joins(**#{join_map}).#{query}"
227
+ end
213
228
  end
214
229
 
215
230
  if mode == or_key
@@ -225,11 +240,20 @@ module Rokaki
225
240
  if db == :postgres
226
241
  query = "where(\"#{key_leaf} #{type.to_s.upcase} ANY (ARRAY[?])\", "
227
242
  query += "prepare_terms(#{filter}, :#{search_mode}))"
228
- else
243
+ elsif db == :mysql
244
+ query = "where(\"#{key_leaf} #{type.to_s.upcase} :query\", "
245
+ query += "query: prepare_regex_terms(#{filter}, :#{search_mode}))"
246
+ else # :sqlserver and others
229
247
  query = "where(\"#{key_leaf} #{type.to_s.upcase} :query\", "
230
- query += "query: \"%\#{#{filter}}%\")" if search_mode == :circumfix
231
- query += "query: \"%\#{#{filter}}\")" if search_mode == :prefix
232
- query += "query: \"\#{#{filter}}%\")" if search_mode == :suffix
248
+ if search_mode == :circumfix
249
+ query += "query: \"%\#{#{filter}}%\")"
250
+ elsif search_mode == :prefix
251
+ query += "query: \"%\#{#{filter}}\")"
252
+ elsif search_mode == :suffix
253
+ query += "query: \"\#{#{filter}}%\")"
254
+ else
255
+ query += "query: \"%\#{#{filter}}%\")"
256
+ end
233
257
  end
234
258
 
235
259
  query
@@ -19,6 +19,84 @@ module Rokaki
19
19
  end
20
20
  end
21
21
 
22
+ # Escape special LIKE characters in SQL Server patterns: %, _, [ and \\
23
+ def escape_like(term)
24
+ term.to_s.gsub(/[\\%_\[]/) { |m| "\\#{m}" }
25
+ end
26
+
27
+ # Build LIKE patterns with proper prefix/suffix/circumfix and escaping for SQL Server
28
+ # Returns a String when param is scalar, or an Array of Strings when param is an Array
29
+ def prepare_like_terms(param, mode)
30
+ if Array === param
31
+ case mode
32
+ when :circumfix
33
+ param.map { |t| "%#{escape_like(t)}%" }
34
+ when :prefix
35
+ param.map { |t| "%#{escape_like(t)}" }
36
+ when :suffix
37
+ param.map { |t| "#{escape_like(t)}%" }
38
+ else
39
+ param.map { |t| "%#{escape_like(t)}%" }
40
+ end
41
+ else
42
+ case mode
43
+ when :circumfix
44
+ "%#{escape_like(param)}%"
45
+ when :prefix
46
+ "%#{escape_like(param)}"
47
+ when :suffix
48
+ "#{escape_like(param)}%"
49
+ else
50
+ "%#{escape_like(param)}%"
51
+ end
52
+ end
53
+ end
54
+
55
+ # Compose a SQL Server LIKE relation supporting arrays of terms (OR chained)
56
+ # column should be a fully qualified column expression, e.g., "authors.first_name" or "cs.title"
57
+ # type is usually "LIKE"
58
+ def sqlserver_like(model, column, type, value, mode)
59
+ terms = prepare_like_terms(value, mode)
60
+ if terms.is_a?(Array)
61
+ return model.none if terms.empty?
62
+ rel = model.where("#{column} #{type} :q0 ESCAPE '\\'", q0: terms[0])
63
+ terms[1..-1]&.each_with_index do |t, i|
64
+ rel = rel.or(model.where("#{column} #{type} :q#{i + 1} ESCAPE '\\'", "q#{i + 1}".to_sym => t))
65
+ end
66
+ rel
67
+ else
68
+ model.where("#{column} #{type} :q ESCAPE '\\'", q: terms)
69
+ end
70
+ end
71
+
72
+ def prepare_regex_terms(param, mode)
73
+ if Array === param
74
+ param_map = param.map { |term| ".*#{term}.*" } if mode == :circumfix
75
+ param_map = param.map { |term| ".*#{term}" } if mode == :prefix
76
+ param_map = param.map { |term| "#{term}.*" } if mode == :suffix
77
+ return param_map.join("|")
78
+ else
79
+ return [".*#{param}.*"] if mode == :circumfix
80
+ return [".*#{param}"] if mode == :prefix
81
+ return ["#{param}.*"] if mode == :suffix
82
+ end
83
+ end
84
+
85
+ # "SELECT `articles`.* FROM `articles` INNER JOIN `authors` ON `authors`.`id` = `articles`.`author_id` WHERE (`authors`.`first_name` LIKE BINARY '%teev%' OR `authors`.`first_name` LIKE BINARY '%arv%')"
86
+ def prepare_or_terms(param, type, mode)
87
+ if Array === param
88
+ param_map = param.map { |term| "%#{term}%" } if mode == :circumfix
89
+ param_map = param.map { |term| "%#{term}" } if mode == :prefix
90
+ param_map = param.map { |term| "#{term}%" } if mode == :suffix
91
+
92
+ return param_map.join(" OR #{type} ")
93
+ else
94
+ return ["%#{param}%"] if mode == :circumfix
95
+ return ["%#{param}"] if mode == :prefix
96
+ return ["#{param}%"] if mode == :suffix
97
+ end
98
+ end
99
+
22
100
 
23
101
  module ClassMethods
24
102
  include Filterable::ClassMethods
@@ -143,17 +221,48 @@ module Rokaki
143
221
  end
144
222
  end
145
223
 
146
- def filter_model(model_class)
224
+ def filter_db(db)
225
+ @_filter_db = db
226
+ end
227
+
228
+ def filter_model(model_class, db: nil)
229
+ @_filter_db = db if db
147
230
  @model = (model_class.is_a?(Class) ? model_class : Object.const_get(model_class.capitalize))
148
231
  class_eval "def set_model; @model ||= #{@model}; end;"
149
232
  end
150
233
 
234
+ def case_sensitive
235
+ if @_filter_db == :postgres
236
+ 'LIKE'
237
+ elsif @_filter_db == :mysql
238
+ # 'LIKE BINARY'
239
+ 'REGEXP'
240
+ elsif @_filter_db == :sqlserver
241
+ 'LIKE'
242
+ else
243
+ 'LIKE'
244
+ end
245
+ end
246
+
247
+ def case_insensitive
248
+ if @_filter_db == :postgres
249
+ 'ILIKE'
250
+ elsif @_filter_db == :mysql
251
+ # 'LIKE'
252
+ 'REGEXP'
253
+ elsif @_filter_db == :sqlserver
254
+ 'LIKE'
255
+ else
256
+ 'LIKE'
257
+ end
258
+ end
259
+
151
260
  def like(args)
152
261
  raise ArgumentError, 'argument mush be a hash' unless args.is_a? Hash
153
262
  @_like_semantics = (@_like_semantics || {}).merge(args)
154
263
 
155
264
  like_keys = LikeKeys.new(args)
156
- like_filters(like_keys, term_type: :like)
265
+ like_filters(like_keys, term_type: case_sensitive)
157
266
  end
158
267
 
159
268
  def ilike(args)
@@ -161,7 +270,7 @@ module Rokaki
161
270
  @i_like_semantics = (@i_like_semantics || {}).merge(args)
162
271
 
163
272
  like_keys = LikeKeys.new(args)
164
- like_filters(like_keys, term_type: :ilike)
273
+ like_filters(like_keys, term_type: case_insensitive)
165
274
  end
166
275
 
167
276
  # the model method is called to instatiate @model from the
@@ -1,3 +1,3 @@
1
1
  module Rokaki
2
- VERSION = "0.9.0"
2
+ VERSION = "0.11.0"
3
3
  end
data/rokaki.gemspec CHANGED
@@ -44,7 +44,11 @@ Gem::Specification.new do |spec|
44
44
  spec.add_development_dependency 'factory_bot'
45
45
 
46
46
  spec.add_development_dependency 'pg'
47
+ spec.add_development_dependency 'mysql2'
47
48
  spec.add_development_dependency 'sqlite3'
48
49
  spec.add_development_dependency 'database_cleaner-active_record'
50
+ # For SQL Server testing
51
+ spec.add_development_dependency 'tiny_tds'
52
+ spec.add_development_dependency 'activerecord-sqlserver-adapter'
49
53
 
50
54
  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.9.0
4
+ version: 0.11.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: 2024-04-21 00:00:00.000000000 Z
11
+ date: 2025-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -164,6 +164,20 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: mysql2
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
167
181
  - !ruby/object:Gem::Dependency
168
182
  name: sqlite3
169
183
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +206,34 @@ dependencies:
192
206
  - - ">="
193
207
  - !ruby/object:Gem::Version
194
208
  version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: tiny_tds
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: activerecord-sqlserver-adapter
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
195
237
  description: A dsl for filtering data in web requests
196
238
  email:
197
239
  - steve@martian.media
@@ -200,11 +242,11 @@ extensions: []
200
242
  extra_rdoc_files: []
201
243
  files:
202
244
  - ".github/workflows/codeql-analysis.yml"
245
+ - ".github/workflows/spec.yml"
203
246
  - ".gitignore"
204
247
  - ".rspec"
205
248
  - ".ruby-version"
206
249
  - ".travis.yml"
207
- - CODE_OF_CONDUCT.md
208
250
  - Gemfile
209
251
  - Gemfile.lock
210
252
  - Guardfile
data/CODE_OF_CONDUCT.md DELETED
@@ -1,74 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to making participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, gender identity and expression, level of experience,
9
- nationality, personal appearance, race, religion, or sexual identity and
10
- orientation.
11
-
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies both within project spaces and in public spaces
49
- when an individual is representing the project or its community. Examples of
50
- representing a project or community include using an official project e-mail
51
- address, posting via an official social media account, or acting as an appointed
52
- representative at an online or offline event. Representation of a project may be
53
- further defined and clarified by project maintainers.
54
-
55
- ## Enforcement
56
-
57
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at steve@martian.media. All
59
- complaints will be reviewed and investigated and will result in a response that
60
- is deemed necessary and appropriate to the circumstances. The project team is
61
- obligated to maintain confidentiality with regard to the reporter of an incident.
62
- Further details of specific enforcement policies may be posted separately.
63
-
64
- Project maintainers who do not follow or enforce the Code of Conduct in good
65
- faith may face temporary or permanent repercussions as determined by other
66
- members of the project's leadership.
67
-
68
- ## Attribution
69
-
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [http://contributor-covenant.org/version/1/4][version]
72
-
73
- [homepage]: http://contributor-covenant.org
74
- [version]: http://contributor-covenant.org/version/1/4/