trifle-stats 1.3.1 → 1.6.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/.devcontainer.json +1 -1
- data/.devops/docker/codespaces/Dockerfile +1 -1
- data/.devops/docker/environment/Dockerfile +3 -2
- data/.devops/docker/local/Dockerfile +1 -0
- data/.devops/docker/local/docker-compose.yml +30 -0
- data/.github/workflows/ruby.yml +85 -6
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.tool-versions +1 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +8 -7
- data/README.md +31 -0
- data/lib/trifle/stats/aggregator/avg.rb +32 -0
- data/lib/trifle/stats/aggregator/max.rb +29 -0
- data/lib/trifle/stats/aggregator/min.rb +29 -0
- data/lib/trifle/stats/aggregator/sum.rb +29 -0
- data/lib/trifle/stats/driver/mongo.rb +81 -27
- data/lib/trifle/stats/driver/postgres.rb +14 -2
- data/lib/trifle/stats/driver/process.rb +12 -0
- data/lib/trifle/stats/driver/redis.rb +19 -4
- data/lib/trifle/stats/driver/sqlite.rb +15 -2
- data/lib/trifle/stats/formatter/category.rb +32 -0
- data/lib/trifle/stats/formatter/timeline.rb +29 -0
- data/lib/trifle/stats/mixins/packer.rb +16 -1
- data/lib/trifle/stats/nocturnal.rb +24 -0
- data/lib/trifle/stats/operations/status/beam.rb +31 -0
- data/lib/trifle/stats/operations/status/scan.rb +35 -0
- data/lib/trifle/stats/operations/timeseries/classify.rb +1 -1
- data/lib/trifle/stats/operations/timeseries/increment.rb +1 -1
- data/lib/trifle/stats/operations/timeseries/set.rb +1 -1
- data/lib/trifle/stats/operations/timeseries/values.rb +25 -7
- data/lib/trifle/stats/series.rb +64 -0
- data/lib/trifle/stats/transponder/average.rb +31 -0
- data/lib/trifle/stats/transponder/ratio.rb +31 -0
- data/lib/trifle/stats/transponder/standard_deviation.rb +35 -0
- data/lib/trifle/stats/version.rb +1 -1
- data/lib/trifle/stats.rb +36 -3
- data/trifle-stats.gemspec +1 -0
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eece6621a74a0dfc38e7e1932897ad0481e3b85744ec85d016320cc6c7e5b395
|
4
|
+
data.tar.gz: a90a4b7bf5e885fdc77af3638b2fcb0fdef0f48e17001b2da925161164fd31f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7bb55a42c115a0e353277b7445f33db93bc16a6011202de515adb63126d0d154615bbc2b13bfad45bd6ca990aedec32de4911c5843553b2c5148676c01d21b99
|
7
|
+
data.tar.gz: 475e37cab204a8b07ea90ead1c209cb784844ba3683884362f284d41c0f2e8170f9f7ec858125dece018312561d934efd555a41ac6457ec16af2d31e36be0695
|
data/.devcontainer.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
FROM trifle/environment:1.
|
1
|
+
FROM trifle/environment:ruby_3.1.0_1
|
@@ -21,6 +21,7 @@ RUN apt-get update -q \
|
|
21
21
|
libssl-dev \
|
22
22
|
libreadline-dev \
|
23
23
|
zlib1g-dev \
|
24
|
+
libsqlite3-dev \
|
24
25
|
tmux \
|
25
26
|
htop \
|
26
27
|
vim \
|
@@ -45,8 +46,8 @@ RUN git clone https://github.com/asdf-vm/asdf.git ${ASDF_ROOT} --branch v0.10.0
|
|
45
46
|
RUN asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git
|
46
47
|
|
47
48
|
# install ruby
|
48
|
-
ENV RUBY_VERSION 3.
|
49
|
-
RUN
|
49
|
+
ENV RUBY_VERSION 3.1.0
|
50
|
+
RUN asdf install ruby ${RUBY_VERSION} \
|
50
51
|
&& asdf global ruby ${RUBY_VERSION}
|
51
52
|
|
52
53
|
RUN gem install bundler
|
@@ -0,0 +1 @@
|
|
1
|
+
FROM trifle/environment:ruby_3.1.0_1
|
@@ -0,0 +1,30 @@
|
|
1
|
+
version: "3.7"
|
2
|
+
name: "stats"
|
3
|
+
services:
|
4
|
+
postgres:
|
5
|
+
image: postgres:latest
|
6
|
+
environment:
|
7
|
+
POSTGRES_PASSWORD: password
|
8
|
+
redis:
|
9
|
+
image: redis:latest
|
10
|
+
mongo:
|
11
|
+
image: mongo:latest
|
12
|
+
app:
|
13
|
+
command: /bin/sh -c "while sleep 1000; do :; done"
|
14
|
+
build:
|
15
|
+
context: ../../..
|
16
|
+
dockerfile: .devops/docker/local/Dockerfile
|
17
|
+
depends_on:
|
18
|
+
- postgres
|
19
|
+
- redis
|
20
|
+
- mongo
|
21
|
+
environment:
|
22
|
+
PGHOST: postgres
|
23
|
+
PGUSER: postgres
|
24
|
+
REDIS_HOST: redis
|
25
|
+
REDIS_URL: redis://redis:6379
|
26
|
+
expose:
|
27
|
+
- 4000
|
28
|
+
volumes:
|
29
|
+
- ../../..:/workspaces/stats
|
30
|
+
working_dir: /workspaces/stats
|
data/.github/workflows/ruby.yml
CHANGED
@@ -16,21 +16,100 @@ on:
|
|
16
16
|
jobs:
|
17
17
|
test:
|
18
18
|
runs-on: ubuntu-latest
|
19
|
+
|
20
|
+
services:
|
21
|
+
postgres:
|
22
|
+
image: postgres:15
|
23
|
+
env:
|
24
|
+
POSTGRES_PASSWORD: postgres
|
25
|
+
POSTGRES_USER: postgres
|
26
|
+
POSTGRES_DB: test_db
|
27
|
+
options: >-
|
28
|
+
--health-cmd pg_isready
|
29
|
+
--health-interval 10s
|
30
|
+
--health-timeout 5s
|
31
|
+
--health-retries 5
|
32
|
+
ports:
|
33
|
+
- 5432:5432
|
34
|
+
|
35
|
+
redis:
|
36
|
+
image: redis:7
|
37
|
+
options: >-
|
38
|
+
--health-cmd "redis-cli ping"
|
39
|
+
--health-interval 10s
|
40
|
+
--health-timeout 5s
|
41
|
+
--health-retries 5
|
42
|
+
ports:
|
43
|
+
- 6379:6379
|
44
|
+
|
45
|
+
mongodb:
|
46
|
+
image: mongo:6.0
|
47
|
+
env:
|
48
|
+
MONGO_INITDB_ROOT_USERNAME: root
|
49
|
+
MONGO_INITDB_ROOT_PASSWORD: password
|
50
|
+
options: >-
|
51
|
+
--health-cmd "mongosh --eval 'db.adminCommand(\"ping\")'"
|
52
|
+
--health-interval 10s
|
53
|
+
--health-timeout 5s
|
54
|
+
--health-retries 5
|
55
|
+
ports:
|
56
|
+
- 27017:27017
|
57
|
+
|
19
58
|
strategy:
|
20
59
|
matrix:
|
21
|
-
ruby-version: ["3.
|
60
|
+
ruby-version: ["3.1"]
|
61
|
+
|
62
|
+
env:
|
63
|
+
# Database configuration
|
64
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
|
65
|
+
REDIS_URL: redis://localhost:6379/0
|
66
|
+
MONGODB_URL: mongodb://root:password@localhost:27017/test_db?authSource=admin
|
22
67
|
|
23
68
|
steps:
|
24
|
-
- uses: actions/checkout@
|
69
|
+
- uses: actions/checkout@v4
|
70
|
+
|
25
71
|
- name: Set up Ruby
|
26
|
-
|
27
|
-
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
28
|
-
# uses: ruby/setup-ruby@v1
|
29
|
-
uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
|
72
|
+
uses: ruby/setup-ruby@v1
|
30
73
|
with:
|
31
74
|
ruby-version: ${{ matrix.ruby-version }}
|
32
75
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
76
|
+
|
77
|
+
- name: Wait for services to be ready
|
78
|
+
run: |
|
79
|
+
# Wait for PostgreSQL
|
80
|
+
until pg_isready -h localhost -p 5432 -U postgres; do
|
81
|
+
echo "Waiting for PostgreSQL..."
|
82
|
+
sleep 2
|
83
|
+
done
|
84
|
+
|
85
|
+
# Wait for Redis
|
86
|
+
until timeout 1 bash -c "</dev/tcp/localhost/6379"; do
|
87
|
+
echo "Waiting for Redis..."
|
88
|
+
sleep 2
|
89
|
+
done
|
90
|
+
|
91
|
+
# Wait for MongoDB
|
92
|
+
until timeout 1 bash -c "</dev/tcp/localhost/27017"; do
|
93
|
+
echo "Waiting for MongoDB..."
|
94
|
+
sleep 2
|
95
|
+
done
|
96
|
+
|
97
|
+
- name: Setup Database
|
98
|
+
run: |
|
99
|
+
# Create database if needed (adjust based on your setup)
|
100
|
+
# bundle exec rails db:create db:migrate
|
101
|
+
env:
|
102
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
|
103
|
+
|
33
104
|
- name: Rspec
|
34
105
|
run: bundle exec rspec
|
106
|
+
env:
|
107
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
|
108
|
+
REDIS_URL: redis://localhost:6379/0
|
109
|
+
MONGODB_URL: mongodb://root:password@localhost:27017/test_db?authSource=admin
|
110
|
+
|
35
111
|
- name: Rubocop
|
36
112
|
run: bundle exec rubocop
|
113
|
+
|
114
|
+
# - name: Performance
|
115
|
+
# run: cd spec/performance && bundle install && ruby run.rb 1000 '{"a":1,"b":2,"c":1,"d":2,"e":1,"f":2,"g":1,"h":2,"i":1,"j":2,"k":1,"l":2,"m":1,"n":2,"o":1,"p":2,"q":1,"r":2,"s":1,"t":2,"u":1,"v":2,"w":1}'
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-3.
|
1
|
+
ruby-3.1.0
|
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby 3.
|
1
|
+
ruby 3.1.0
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
trifle-stats (1.
|
4
|
+
trifle-stats (1.6.0)
|
5
5
|
tzinfo (~> 2.0)
|
6
6
|
|
7
7
|
GEM
|
@@ -10,9 +10,10 @@ GEM
|
|
10
10
|
ast (2.4.2)
|
11
11
|
bson (4.12.1)
|
12
12
|
byebug (11.1.3)
|
13
|
-
concurrent-ruby (1.
|
13
|
+
concurrent-ruby (1.3.5)
|
14
14
|
diff-lcs (1.4.4)
|
15
15
|
dotenv (2.7.6)
|
16
|
+
mini_portile2 (2.8.9)
|
16
17
|
mongo (2.14.0)
|
17
18
|
bson (>= 4.8.2, < 5.0.0)
|
18
19
|
parallel (1.20.1)
|
@@ -49,9 +50,10 @@ GEM
|
|
49
50
|
rubocop-ast (1.4.1)
|
50
51
|
parser (>= 2.7.1.5)
|
51
52
|
ruby-progressbar (1.11.0)
|
52
|
-
|
53
|
-
|
54
|
-
|
53
|
+
sqlite3 (2.7.3)
|
54
|
+
mini_portile2 (~> 2.8.0)
|
55
|
+
sqlite3 (2.7.3-x86_64-darwin)
|
56
|
+
tzinfo (2.0.6)
|
55
57
|
concurrent-ruby (~> 1.0)
|
56
58
|
unicode-display_width (1.7.0)
|
57
59
|
|
@@ -69,8 +71,7 @@ DEPENDENCIES
|
|
69
71
|
redis
|
70
72
|
rspec (~> 3.0)
|
71
73
|
rubocop (= 1.0.0)
|
72
|
-
|
73
|
-
sqlite3 (>= 1.4.4)
|
74
|
+
sqlite3
|
74
75
|
trifle-stats!
|
75
76
|
|
76
77
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -124,6 +124,37 @@ Trifle::Stats.values(key: 'event::logs', from: Time.now, to: Time.now, range: :d
|
|
124
124
|
=> {:at=>[2021-01-25 00:00:00 +0200], :values=>[{"count"=>1, "duration"=>5, "lines"=>361}]}
|
125
125
|
```
|
126
126
|
|
127
|
+
## Testing
|
128
|
+
|
129
|
+
### Testing Principles
|
130
|
+
|
131
|
+
Tests are structured to be simple, isolated, and mirror the class structure. Each test is independent and self-contained.
|
132
|
+
|
133
|
+
#### Key Rules:
|
134
|
+
|
135
|
+
1. **Keep tests simple and isolated** - Each test should focus on a single class/method
|
136
|
+
2. **Independent tests** - Tests should not depend on each other and can be run in any order
|
137
|
+
3. **Self-contained setup** - Every test configures its own variables and dependencies
|
138
|
+
4. **Single layer testing** - Test only the specific class, not multiple layers of functionality
|
139
|
+
5. **Use appropriate stubbing** - When testing operations, stub driver methods. Let driver tests verify driver behavior
|
140
|
+
6. **Repeat yourself** - It's okay to repeat setup code for clarity and independence
|
141
|
+
|
142
|
+
#### Driver Testing:
|
143
|
+
|
144
|
+
- Driver tests use **real database connections** (Redis, PostgreSQL, MongoDB, SQLite)
|
145
|
+
- Clean data between tests to ensure isolation
|
146
|
+
- Use appropriate test databases (e.g., Redis database 15, test-specific DB names)
|
147
|
+
- The **Process driver** is ideal for testing environments as it uses in-memory storage
|
148
|
+
|
149
|
+
#### Test Structure:
|
150
|
+
|
151
|
+
Tests follow the same structure as the classes they test:
|
152
|
+
- `spec/stats/driver/` - Driver class tests
|
153
|
+
- `spec/stats/operations/` - Operation class tests
|
154
|
+
- `spec/stats/mixins/` - Mixin tests
|
155
|
+
|
156
|
+
This approach makes it easier to see initial configuration and expected results for each test.
|
157
|
+
|
127
158
|
## Contributing
|
128
159
|
|
129
160
|
Bug reports and pull requests are welcome on GitHub at https://github.com/trifle-io/trifle-stats.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trifle
|
4
|
+
module Stats
|
5
|
+
class Aggregator
|
6
|
+
class Avg
|
7
|
+
Trifle::Stats::Series.register_aggregator(:avg, self)
|
8
|
+
|
9
|
+
def aggregate(series:, path:, slices: 1)
|
10
|
+
return [] if series[:at].empty?
|
11
|
+
|
12
|
+
keys = path.split('.')
|
13
|
+
result = series[:values].map do |data|
|
14
|
+
data.dig(*keys)
|
15
|
+
end
|
16
|
+
sliced(result: result, slices: slices)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def sliced(result:, slices:)
|
22
|
+
result[(result.count - (result.count / slices * slices))..].each_slice(result.count / slices).map do |slice|
|
23
|
+
sum = slice.compact.sum
|
24
|
+
count = slice.compact.count
|
25
|
+
|
26
|
+
count.zero? ? 0 : sum / count
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trifle
|
4
|
+
module Stats
|
5
|
+
class Aggregator
|
6
|
+
class Max
|
7
|
+
Trifle::Stats::Series.register_aggregator(:max, self)
|
8
|
+
|
9
|
+
def aggregate(series:, path:, slices: 1)
|
10
|
+
return [] if series[:at].empty?
|
11
|
+
|
12
|
+
keys = path.split('.')
|
13
|
+
result = series[:values].map do |data|
|
14
|
+
data.dig(*keys)
|
15
|
+
end
|
16
|
+
sliced(result: result, slices: slices)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def sliced(result:, slices:)
|
22
|
+
result[(result.count - (result.count / slices * slices))..].each_slice(result.count / slices).map do |slice|
|
23
|
+
slice.compact.max
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trifle
|
4
|
+
module Stats
|
5
|
+
class Aggregator
|
6
|
+
class Min
|
7
|
+
Trifle::Stats::Series.register_aggregator(:min, self)
|
8
|
+
|
9
|
+
def aggregate(series:, path:, slices: 1)
|
10
|
+
return [] if series[:at].empty?
|
11
|
+
|
12
|
+
keys = path.split('.')
|
13
|
+
result = series[:values].map do |data|
|
14
|
+
data.dig(*keys)
|
15
|
+
end
|
16
|
+
sliced(result: result, slices: slices)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def sliced(result:, slices:)
|
22
|
+
result[(result.count - (result.count / slices * slices))..].each_slice(result.count / slices).map do |slice|
|
23
|
+
slice.compact.min
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Trifle
|
4
|
+
module Stats
|
5
|
+
class Aggregator
|
6
|
+
class Sum
|
7
|
+
Trifle::Stats::Series.register_aggregator(:sum, self)
|
8
|
+
|
9
|
+
def aggregate(series:, path:, slices: 1)
|
10
|
+
return [] if series[:at].empty?
|
11
|
+
|
12
|
+
keys = path.split('.')
|
13
|
+
result = series[:values].map do |data|
|
14
|
+
data.dig(*keys)
|
15
|
+
end
|
16
|
+
sliced(result: result, slices: slices)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def sliced(result:, slices:)
|
22
|
+
result[(result.count - (result.count / slices * slices))..].each_slice(result.count / slices).map do |slice|
|
23
|
+
slice.compact.sum
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -7,55 +7,109 @@ module Trifle
|
|
7
7
|
module Driver
|
8
8
|
class Mongo
|
9
9
|
include Mixins::Packer
|
10
|
-
attr_accessor :client, :collection_name
|
10
|
+
attr_accessor :client, :collection_name
|
11
11
|
|
12
|
-
def initialize(client, collection_name: 'trifle_stats')
|
12
|
+
def initialize(client, collection_name: 'trifle_stats', joined_identifier: true, expire_after: nil)
|
13
13
|
@client = client
|
14
14
|
@collection_name = collection_name
|
15
|
+
@joined_identifier = joined_identifier
|
16
|
+
@expire_after = expire_after
|
15
17
|
@separator = '::'
|
16
18
|
end
|
17
19
|
|
18
|
-
def self.setup!(client, collection_name: 'trifle_stats')
|
19
|
-
client[collection_name]
|
20
|
-
|
20
|
+
def self.setup!(client, collection_name: 'trifle_stats', joined_identifier: true, expire_after: nil)
|
21
|
+
collection = client[collection_name]
|
22
|
+
collection.create
|
23
|
+
if joined_identifier
|
24
|
+
collection.indexes.create_one({ key: 1 }, unique: true)
|
25
|
+
else
|
26
|
+
collection.indexes.create_one({ key: 1, range: 1, at: -1 }, unique: true)
|
27
|
+
end
|
28
|
+
collection.indexes.create_one({ expire_at: 1 }, expire_after_seconds: 0) if expire_after
|
29
|
+
end
|
30
|
+
|
31
|
+
def description
|
32
|
+
"#{self.class.name}(#{@joined_identifier ? 'J' : 'S'})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def separator
|
36
|
+
@joined_identifier ? @separator : nil
|
21
37
|
end
|
22
38
|
|
23
39
|
def inc(keys:, **values)
|
24
40
|
data = self.class.pack(hash: { data: values })
|
25
41
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
42
|
+
operations = keys.map do |key|
|
43
|
+
filter = key.identifier(separator)
|
44
|
+
expire_at = @expire_after ? key.at + @expire_after : nil
|
45
|
+
|
46
|
+
upsert_operation('$inc', filter: filter, data: data, expire_at: expire_at)
|
47
|
+
end
|
48
|
+
|
49
|
+
collection.bulk_write(operations)
|
31
50
|
end
|
32
51
|
|
33
52
|
def set(keys:, **values)
|
34
53
|
data = self.class.pack(hash: { data: values })
|
35
54
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
55
|
+
operations = keys.map do |key|
|
56
|
+
filter = key.identifier(separator)
|
57
|
+
expire_at = @expire_after ? key.at + @expire_after : nil
|
58
|
+
|
59
|
+
upsert_operation('$set', filter: filter, data: data, expire_at: expire_at)
|
60
|
+
end
|
61
|
+
|
62
|
+
collection.bulk_write(operations)
|
63
|
+
end
|
64
|
+
|
65
|
+
def ping(key:, **values)
|
66
|
+
data = self.class.pack(hash: { data: values, at: key.at })
|
67
|
+
identifier = key.identifier(separator)
|
68
|
+
expire_at = @expire_after ? key.at + @expire_after : nil
|
69
|
+
|
70
|
+
operations = [
|
71
|
+
upsert_operation('$set', filter: identifier.slice(:key), data: data, expire_at: expire_at)
|
72
|
+
]
|
73
|
+
|
74
|
+
collection.bulk_write(operations)
|
41
75
|
end
|
42
76
|
|
43
|
-
def upsert_operation(operation,
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
77
|
+
def upsert_operation(operation, filter:, data:, expire_at: nil)
|
78
|
+
# Merge if $set and $set
|
79
|
+
update_data = operation == '$set' && expire_at ? data.merge(expire_at: expire_at) : data
|
80
|
+
|
81
|
+
# Add if $inc and $set
|
82
|
+
update = {
|
83
|
+
operation => update_data,
|
84
|
+
**(operation != '$set' && expire_at ? { '$set' => { expire_at: expire_at } } : {})
|
50
85
|
}
|
86
|
+
|
87
|
+
{ update_many: { filter: filter, update: update, upsert: true } }
|
51
88
|
end
|
52
89
|
|
53
|
-
def get(keys:)
|
54
|
-
|
55
|
-
data = collection.find(
|
56
|
-
map = data.inject({})
|
90
|
+
def get(keys:) # rubocop:disable Metrics/AbcSize
|
91
|
+
combinations = keys.map { |key| key.identifier(separator) }
|
92
|
+
data = collection.find('$or' => combinations)
|
93
|
+
map = data.inject({}) do |o, d|
|
94
|
+
o.merge(
|
95
|
+
Nocturnal::Key.new(
|
96
|
+
key: d['key'], range: d['range'], at: d['at']
|
97
|
+
).identifier(separator) => d['data']
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
combinations.map { |combination| map[combination] || {} }
|
102
|
+
end
|
103
|
+
|
104
|
+
def scan(key:)
|
105
|
+
return [] if @joined_identifier
|
106
|
+
|
107
|
+
data = collection.find(
|
108
|
+
**key.identifier(separator)
|
109
|
+
).sort(at: -1).first # rubocop:disable Style/RedundantSort
|
110
|
+
return [] if data.nil?
|
57
111
|
|
58
|
-
|
112
|
+
[data['at'], data['data']]
|
59
113
|
end
|
60
114
|
|
61
115
|
private
|
@@ -19,6 +19,10 @@ module Trifle
|
|
19
19
|
client.exec("CREATE TABLE #{table_name} (key VARCHAR(255) PRIMARY KEY, data JSONB NOT NULL DEFAULT '{}'::jsonb)") # rubocop:disable Layout/LineLength
|
20
20
|
end
|
21
21
|
|
22
|
+
def description
|
23
|
+
"#{self.class.name}(J)"
|
24
|
+
end
|
25
|
+
|
22
26
|
def inc(keys:, **values)
|
23
27
|
data = self.class.pack(hash: values)
|
24
28
|
client.transaction do |c|
|
@@ -33,7 +37,7 @@ module Trifle
|
|
33
37
|
<<-SQL
|
34
38
|
INSERT INTO #{table_name} (key, data) VALUES ('#{key}', '#{data.to_json}')
|
35
39
|
ON CONFLICT (key) DO UPDATE SET data =
|
36
|
-
#{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', (COALESCE(
|
40
|
+
#{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', (COALESCE(#{table_name}.data->>'#{k}', '0')::int + #{v})::text::jsonb)" }};
|
37
41
|
SQL
|
38
42
|
end
|
39
43
|
|
@@ -51,7 +55,7 @@ module Trifle
|
|
51
55
|
<<-SQL
|
52
56
|
INSERT INTO #{table_name} (key, data) VALUES ('#{key}', '#{data.to_json}')
|
53
57
|
ON CONFLICT (key) DO UPDATE SET data =
|
54
|
-
#{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}',
|
58
|
+
#{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', '#{v.to_json}'::jsonb)" }}
|
55
59
|
SQL
|
56
60
|
end
|
57
61
|
|
@@ -78,6 +82,14 @@ module Trifle
|
|
78
82
|
SELECT * FROM #{table_name} WHERE key IN ('#{keys.join("', '")}');
|
79
83
|
SQL
|
80
84
|
end
|
85
|
+
|
86
|
+
def ping(*)
|
87
|
+
[]
|
88
|
+
end
|
89
|
+
|
90
|
+
def scan(*)
|
91
|
+
[]
|
92
|
+
end
|
81
93
|
end
|
82
94
|
end
|
83
95
|
end
|
@@ -12,6 +12,10 @@ module Trifle
|
|
12
12
|
@separator = '::'
|
13
13
|
end
|
14
14
|
|
15
|
+
def description
|
16
|
+
"#{self.class.name}(J)"
|
17
|
+
end
|
18
|
+
|
15
19
|
def inc(keys:, **values)
|
16
20
|
keys.map do |key|
|
17
21
|
self.class.pack(hash: values).each do |k, c|
|
@@ -39,6 +43,14 @@ module Trifle
|
|
39
43
|
)
|
40
44
|
end
|
41
45
|
end
|
46
|
+
|
47
|
+
def ping(*)
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
|
51
|
+
def scan(*)
|
52
|
+
[]
|
53
|
+
end
|
42
54
|
end
|
43
55
|
end
|
44
56
|
end
|
@@ -9,15 +9,20 @@ module Trifle
|
|
9
9
|
include Mixins::Packer
|
10
10
|
attr_accessor :client, :prefix, :separator
|
11
11
|
|
12
|
-
def initialize(client
|
12
|
+
def initialize(client, prefix: 'trfl')
|
13
13
|
@client = client
|
14
14
|
@prefix = prefix
|
15
15
|
@separator = '::'
|
16
16
|
end
|
17
17
|
|
18
|
+
def description
|
19
|
+
"#{self.class.name}(J)"
|
20
|
+
end
|
21
|
+
|
18
22
|
def inc(keys:, **values)
|
19
23
|
keys.map do |key|
|
20
|
-
|
24
|
+
key.prefix = prefix
|
25
|
+
pkey = key.join(separator)
|
21
26
|
|
22
27
|
self.class.pack(hash: values).each do |k, c|
|
23
28
|
client.hincrby(pkey, k, c)
|
@@ -27,7 +32,8 @@ module Trifle
|
|
27
32
|
|
28
33
|
def set(keys:, **values)
|
29
34
|
keys.map do |key|
|
30
|
-
|
35
|
+
key.prefix = prefix
|
36
|
+
pkey = key.join(separator)
|
31
37
|
|
32
38
|
client.hmset(pkey, *self.class.pack(hash: values))
|
33
39
|
end
|
@@ -35,13 +41,22 @@ module Trifle
|
|
35
41
|
|
36
42
|
def get(keys:)
|
37
43
|
keys.map do |key|
|
38
|
-
|
44
|
+
key.prefix = prefix
|
45
|
+
pkey = key.join(separator)
|
39
46
|
|
40
47
|
self.class.unpack(
|
41
48
|
hash: client.hgetall(pkey)
|
42
49
|
)
|
43
50
|
end
|
44
51
|
end
|
52
|
+
|
53
|
+
def ping(*)
|
54
|
+
[]
|
55
|
+
end
|
56
|
+
|
57
|
+
def scan(*)
|
58
|
+
[]
|
59
|
+
end
|
45
60
|
end
|
46
61
|
end
|
47
62
|
end
|