trifle-stats 1.3.0 → 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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer.json +1 -1
  3. data/.devops/docker/codespaces/Dockerfile +1 -1
  4. data/.devops/docker/environment/Dockerfile +3 -2
  5. data/.devops/docker/gitpod/base/.p10k.zsh +1626 -0
  6. data/.devops/docker/gitpod/base/.zshrc +13 -0
  7. data/.devops/docker/gitpod/base/Dockerfile +9 -32
  8. data/.devops/docker/local/Dockerfile +1 -0
  9. data/.devops/docker/local/docker-compose.yml +30 -0
  10. data/.github/workflows/ruby.yml +85 -6
  11. data/.gitignore +3 -1
  12. data/.ruby-version +1 -1
  13. data/.tool-versions +1 -1
  14. data/Gemfile +4 -0
  15. data/Gemfile.lock +11 -8
  16. data/README.md +31 -0
  17. data/lib/trifle/stats/aggregator/avg.rb +32 -0
  18. data/lib/trifle/stats/aggregator/max.rb +29 -0
  19. data/lib/trifle/stats/aggregator/min.rb +29 -0
  20. data/lib/trifle/stats/aggregator/sum.rb +29 -0
  21. data/lib/trifle/stats/driver/mongo.rb +81 -28
  22. data/lib/trifle/stats/driver/postgres.rb +14 -3
  23. data/lib/trifle/stats/driver/process.rb +12 -0
  24. data/lib/trifle/stats/driver/redis.rb +19 -5
  25. data/lib/trifle/stats/driver/sqlite.rb +15 -3
  26. data/lib/trifle/stats/formatter/category.rb +32 -0
  27. data/lib/trifle/stats/formatter/timeline.rb +29 -0
  28. data/lib/trifle/stats/mixins/packer.rb +16 -1
  29. data/lib/trifle/stats/nocturnal.rb +24 -0
  30. data/lib/trifle/stats/operations/status/beam.rb +31 -0
  31. data/lib/trifle/stats/operations/status/scan.rb +35 -0
  32. data/lib/trifle/stats/operations/timeseries/classify.rb +1 -1
  33. data/lib/trifle/stats/operations/timeseries/increment.rb +1 -1
  34. data/lib/trifle/stats/operations/timeseries/set.rb +1 -1
  35. data/lib/trifle/stats/operations/timeseries/values.rb +25 -7
  36. data/lib/trifle/stats/series.rb +64 -0
  37. data/lib/trifle/stats/transponder/average.rb +31 -0
  38. data/lib/trifle/stats/transponder/ratio.rb +31 -0
  39. data/lib/trifle/stats/transponder/standard_deviation.rb +35 -0
  40. data/lib/trifle/stats/version.rb +1 -1
  41. data/lib/trifle/stats.rb +36 -3
  42. data/trifle-stats.gemspec +1 -0
  43. metadata +20 -3
@@ -0,0 +1,13 @@
1
+ # Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.
2
+ # Initialization code that may require console input (password prompts, [y/n]
3
+ # confirmations, etc.) must go above this block; everything else may go below.
4
+ if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
5
+ source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
6
+ fi
7
+
8
+ source /root/powerlevel10k/powerlevel10k.zsh-theme
9
+
10
+ # To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.
11
+ [[ ! -f /root/.p10k.zsh ]] || source /root/.p10k.zsh
12
+
13
+ POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true
@@ -1,41 +1,27 @@
1
- FROM ubuntu:20.04
1
+ FROM debian:latest
2
2
 
3
3
  ENV TZ=Europe/Bratislava
4
4
  RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
5
5
 
6
- RUN useradd gitpod -u 33333
7
- RUN mkdir -p /home/gitpod && chown gitpod:gitpod /home/gitpod
8
-
9
6
  # install ubuntu packages
10
7
  RUN apt-get update -q \
11
8
  && apt-get install -y \
12
9
  build-essential \
13
10
  apt-transport-https \
14
- bash \
15
- xterm \
16
- xvfb \
17
- x11vnc \
11
+ zsh \
18
12
  libpq-dev \
19
13
  git \
20
14
  curl \
21
15
  wget \
22
16
  unzip \
23
- dirmngr \
24
17
  gpg \
25
18
  gnupg2 \
26
19
  locales \
27
20
  autoconf \
28
- libncurses5-dev \
29
- libgl1-mesa-dev \
30
- libglu1-mesa-dev \
31
- libpng-dev \
32
- unixodbc-dev \
33
21
  libssl-dev \
34
22
  libreadline-dev \
35
23
  zlib1g-dev \
36
- ffmpeg \
37
24
  tmux \
38
- runit-systemd \
39
25
  htop \
40
26
  vim \
41
27
  && apt-get clean
@@ -46,23 +32,15 @@ ENV LANG en_US.UTF-8
46
32
  ENV LANGUAGE en_US:en
47
33
  ENV LC_ALL en_US.UTF-8
48
34
 
49
- RUN curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -
50
- RUN echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list
51
-
52
- RUN apt-get update -q \
53
- && apt-get install -y \
54
- postgresql postgresql-contrib \
55
- # mongodb-org \
56
- redis-server \
57
- mariadb-server \
58
- && apt-get clean
59
-
60
- USER gitpod
35
+ RUN wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh || true
36
+ RUN git clone --depth=1 https://github.com/romkatv/powerlevel10k.git /root/powerlevel10k
37
+ COPY .zshrc /root/.zshrc
38
+ COPY .p10k.zsh /root/p10k.zsh
61
39
 
62
40
  #install asdf
63
- ENV ASDF_ROOT /home/gitpod/.asdf
41
+ ENV ASDF_ROOT /root/.asdf
64
42
  ENV PATH "${ASDF_ROOT}/bin:${ASDF_ROOT}/shims:$PATH"
65
- RUN git clone https://github.com/asdf-vm/asdf.git ${ASDF_ROOT} --branch v0.8.0
43
+ RUN git clone https://github.com/asdf-vm/asdf.git ${ASDF_ROOT} --branch v0.10.0
66
44
 
67
45
  RUN asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git
68
46
 
@@ -71,7 +49,6 @@ ENV RUBY_VERSION 3.0.0
71
49
  RUN ASDF_RUBY_BUILD_VERSION=v20201225 asdf install ruby ${RUBY_VERSION} \
72
50
  && asdf global ruby ${RUBY_VERSION}
73
51
 
74
- # throw errors if Gemfile has been modified since Gemfile.lock
75
52
  RUN gem install bundler
76
53
 
77
- CMD ["bash"]
54
+ CMD ["zsh"]
@@ -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
@@ -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.0"]
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@v2
69
+ - uses: actions/checkout@v4
70
+
25
71
  - name: Set up Ruby
26
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
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
@@ -10,4 +10,6 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  /.byebug_history
13
- stats.db
13
+ stats.db
14
+ .DS_Store
15
+ spec/performance/.byebug_history
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.0.0
1
+ ruby-3.1.0
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.0.0
1
+ ruby 3.1.0
data/Gemfile CHANGED
@@ -5,3 +5,7 @@ gemspec
5
5
 
6
6
  gem "rake", "~> 12.0"
7
7
  gem "rspec", "~> 3.0"
8
+ gem "mongo", require: false
9
+ gem "pg", require: false
10
+ gem "redis", require: false
11
+ gem "sqlite3", require: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trifle-stats (1.3.0)
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.1.10)
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,8 +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
- sqlite3 (1.4.4)
53
- tzinfo (2.0.5)
53
+ sqlite3 (2.7.3)
54
+ mini_portile2 (~> 2.8.0)
55
+ sqlite3 (2.7.3-x86_64-darwin)
56
+ tzinfo (2.0.6)
54
57
  concurrent-ruby (~> 1.0)
55
58
  unicode-display_width (1.7.0)
56
59
 
@@ -62,13 +65,13 @@ DEPENDENCIES
62
65
  bundler (~> 2.1)
63
66
  byebug
64
67
  dotenv
65
- mongo (>= 2.14.0)
66
- pg (>= 1.2)
68
+ mongo
69
+ pg
67
70
  rake (~> 12.0)
68
- redis (>= 4.2)
71
+ redis
69
72
  rspec (~> 3.0)
70
73
  rubocop (= 1.0.0)
71
- sqlite3 (>= 1.4.4)
74
+ sqlite3
72
75
  trifle-stats!
73
76
 
74
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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'mongo'
4
3
  require_relative '../mixins/packer'
5
4
 
6
5
  module Trifle
@@ -8,55 +7,109 @@ module Trifle
8
7
  module Driver
9
8
  class Mongo
10
9
  include Mixins::Packer
11
- attr_accessor :client, :collection_name, :separator
10
+ attr_accessor :client, :collection_name
12
11
 
13
- def initialize(client, collection_name: 'trifle_stats')
12
+ def initialize(client, collection_name: 'trifle_stats', joined_identifier: true, expire_after: nil)
14
13
  @client = client
15
14
  @collection_name = collection_name
15
+ @joined_identifier = joined_identifier
16
+ @expire_after = expire_after
16
17
  @separator = '::'
17
18
  end
18
19
 
19
- def self.setup!(client, collection_name: 'trifle_stats')
20
- client[collection_name].create
21
- client[collection_name].indexes.create_one({ key: 1 }, unique: true)
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
22
37
  end
23
38
 
24
39
  def inc(keys:, **values)
25
40
  data = self.class.pack(hash: { data: values })
26
41
 
27
- collection.bulk_write(
28
- keys.map do |key|
29
- upsert_operation('$inc', pkey: key.join(separator), data: data)
30
- end
31
- )
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)
32
50
  end
33
51
 
34
52
  def set(keys:, **values)
35
53
  data = self.class.pack(hash: { data: values })
36
54
 
37
- collection.bulk_write(
38
- keys.map do |key|
39
- upsert_operation('$set', pkey: key.join(separator), data: data)
40
- end
41
- )
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)
42
75
  end
43
76
 
44
- def upsert_operation(operation, pkey:, data:)
45
- {
46
- update_many: {
47
- filter: { key: pkey },
48
- update: { operation => data },
49
- upsert: true
50
- }
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 } } : {})
51
85
  }
86
+
87
+ { update_many: { filter: filter, update: update, upsert: true } }
52
88
  end
53
89
 
54
- def get(keys:)
55
- pkeys = keys.map { |key| key.join(separator) }
56
- data = collection.find(key: { '$in' => pkeys })
57
- map = data.inject({}) { |o, d| o.merge(d['key'] => d['data']) }
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?
58
111
 
59
- pkeys.map { |pkey| map[pkey] || {} }
112
+ [data['at'], data['data']]
60
113
  end
61
114
 
62
115
  private
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pg'
4
3
  require_relative '../mixins/packer'
5
4
 
6
5
  module Trifle
@@ -20,6 +19,10 @@ module Trifle
20
19
  client.exec("CREATE TABLE #{table_name} (key VARCHAR(255) PRIMARY KEY, data JSONB NOT NULL DEFAULT '{}'::jsonb)") # rubocop:disable Layout/LineLength
21
20
  end
22
21
 
22
+ def description
23
+ "#{self.class.name}(J)"
24
+ end
25
+
23
26
  def inc(keys:, **values)
24
27
  data = self.class.pack(hash: values)
25
28
  client.transaction do |c|
@@ -34,7 +37,7 @@ module Trifle
34
37
  <<-SQL
35
38
  INSERT INTO #{table_name} (key, data) VALUES ('#{key}', '#{data.to_json}')
36
39
  ON CONFLICT (key) DO UPDATE SET data =
37
- #{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', (COALESCE(trifle_stats.data->>'#{k}', '0')::int + #{v})::text::jsonb)" }};
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)" }};
38
41
  SQL
39
42
  end
40
43
 
@@ -52,7 +55,7 @@ module Trifle
52
55
  <<-SQL
53
56
  INSERT INTO #{table_name} (key, data) VALUES ('#{key}', '#{data.to_json}')
54
57
  ON CONFLICT (key) DO UPDATE SET data =
55
- #{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', (#{v})::text::jsonb)" }}
58
+ #{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', '#{v.to_json}'::jsonb)" }}
56
59
  SQL
57
60
  end
58
61
 
@@ -79,6 +82,14 @@ module Trifle
79
82
  SELECT * FROM #{table_name} WHERE key IN ('#{keys.join("', '")}');
80
83
  SQL
81
84
  end
85
+
86
+ def ping(*)
87
+ []
88
+ end
89
+
90
+ def scan(*)
91
+ []
92
+ end
82
93
  end
83
94
  end
84
95
  end