redis-time-series 0.5.2 → 0.7.1

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: 461a6f3842867d1ba51440e7576aad3e9b92ecd02700cd59e5508c0e8bbfcaa7
4
- data.tar.gz: 0c03af91af0eaaa37917db2cad21417c38709a53b223ed2864bad0f9857fa1f7
3
+ metadata.gz: 16477ce0296ad40319f56ba74622e19816b6739ef15f8ec1b173f560e6966020
4
+ data.tar.gz: 878d7209216e9bed1b565ed607a4cb882b795efdf879cee33956377b8ddd4928
5
5
  SHA512:
6
- metadata.gz: 56dcd4439b2f4f06469237d2925d84b2ffabc40e2dccb608d5489b78f0e2a5b299881487b55e67fb48f1c9d0f7f56951ee98254ae84229b1827fb4cf3683ca44
7
- data.tar.gz: 860e638f906b9dc19358e6156ad6a0488452be0be499853926742073cf6e85d00f4e95755f1659feae9763334eebbab044147fa3d94512dbb2a91360ec08728b
6
+ metadata.gz: 260b8d730198d05ae379e5b97c8135bc06d62b4ad69dc67e37b6af7a65a0c9ff8f13f3d4669e45280b7d7f4b14496d8ef62c35a0f57b5523b91d41bd8b3f46d2
7
+ data.tar.gz: 3ab2ec330930620a7b20f911d5279e7cdab8c56fcf73472c8daae4d77630c315f355437c819beeae0b82038cf5f8ab5ba14df0234fed76e72a21b2e0b138c1ae
@@ -11,9 +11,14 @@ on:
11
11
  jobs:
12
12
  spec:
13
13
  runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ image_version: ['latest', 'edge']
18
+ ruby_version: ['2.6', '2.7', '3.0', '3.1']
14
19
  services:
15
20
  redis:
16
- image: redislabs/redistimeseries:latest
21
+ image: redislabs/redistimeseries:${{ matrix.image_version }}
17
22
  ports:
18
23
  - 6379:6379/tcp
19
24
  env:
@@ -25,16 +30,18 @@ jobs:
25
30
  - name: Set up Ruby
26
31
  uses: ruby/setup-ruby@v1
27
32
  with:
28
- ruby-version: 2.6
33
+ ruby-version: ${{ matrix.ruby_version }}
29
34
  - name: Set up CodeClimate
30
35
  run: |
31
36
  curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
32
37
  chmod +x ./cc-test-reporter
33
38
  ./cc-test-reporter before-build
34
39
  - name: Install dependencies
35
- run: bundle install
40
+ run: |
41
+ bundle install
42
+ bundle exec appraisal install
36
43
  - name: Run specs
37
- run: bundle exec rake spec
44
+ run: bundle exec appraisal rake spec
38
45
  - name: Upload coverage report
39
46
  run: ./cc-test-reporter after-build -t simplecov coverage/.resultset.json
40
47
  - uses: actions/upload-artifact@v2
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
9
10
 
10
11
  # rspec failure tracking
11
12
  .rspec_status
data/Appraisals ADDED
@@ -0,0 +1,7 @@
1
+ appraise 'redis 3' do
2
+ gem 'redis', '~> 3.3'
3
+ end
4
+
5
+ appraise 'redis 4' do
6
+ gem 'redis', '~> 4.0'
7
+ end
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.1
6
+ * Handle ActiveSupport::TimeWithZone objects (#75)
7
+
8
+ ## 0.7.0
9
+ * Add Ruby 3.1 to build matrix (#70)
10
+ * Add Ruby 3.0 to build matrix (#63)
11
+ * Relax Redis version constraint (#62)
12
+ * Add TS.REVRANGE, TS.MRANGE, and TS.MREVRANGE commands (#19)
13
+ * Update TS.MADD commands to consolidate parsing (#58)
14
+
15
+ ## 0.6.0
16
+ * Add CHUNK_SIZE param to CREATE, ADD, INCRBY, DECRBY commands (#53)
17
+ * Add duplication policy to TS.CREATE and TS.ADD commands (#51)
18
+ * Add support for endless ranges to TS.RANGE (#50)
19
+ * Cast label values to integers in Info struct (#49)
20
+ * Build against edge upstream in addition to latest stable (#48)
21
+
5
22
  ## 0.5.2
6
23
  * Add chunk_type to info struct (#47)
7
24
 
data/README.md CHANGED
@@ -24,7 +24,7 @@ ts.add 56
24
24
  => #<Redis::TimeSeries::Sample:0x00007f8c0d26c460 @time=2020-06-25 23:23:16 -0700, @value=0.56e2>
25
25
  ts.add 78
26
26
  => #<Redis::TimeSeries::Sample:0x00007f8c0d276618 @time=2020-06-25 23:23:20 -0700, @value=0.78e2>
27
- ts.range (Time.now.to_i - 100)..Time.now.to_i
27
+ ts.range (Time.now.to_i - 100)..Time.now.to_i * 1000
28
28
  => [#<Redis::TimeSeries::Sample:0x00007f8c0d297200 @time=2020-06-25 23:23:04 -0700, @value=0.1234e4>,
29
29
  #<Redis::TimeSeries::Sample:0x00007f8c0d297048 @time=2020-06-25 23:23:16 -0700, @value=0.56e2>,
30
30
  #<Redis::TimeSeries::Sample:0x00007f8c0d296e90 @time=2020-06-25 23:23:20 -0700, @value=0.78e2>]
@@ -213,6 +213,38 @@ Redis::TimeSeries.where('foo=bar')
213
213
  @retention=nil,
214
214
  @uncompressed=false>]
215
215
  ```
216
+
217
+ ### Querying Multiple Series
218
+ Get all samples from matching series over a time range with `mrange`
219
+ ```ruby
220
+ [4] pry(main)> result = Redis::TimeSeries.mrange(1.minute.ago.., filter: { foo: 'bar' })
221
+ => [#<struct Redis::TimeSeries::Multi::Result
222
+ series=
223
+ #<Redis::TimeSeries:0x00007f833e408ad0
224
+ @key="ts3",
225
+ @redis=#<Redis client v4.2.5 for redis://127.0.0.1:6379/0>>,
226
+ labels=[],
227
+ samples=
228
+ [#<Redis::TimeSeries::Sample:0x00007f833e408a58
229
+ @time=2021-06-17 20:58:33 3246391/4194304 -0700,
230
+ @value=0.1e1>,
231
+ #<Redis::TimeSeries::Sample:0x00007f833e408850
232
+ @time=2021-06-17 20:58:33 413139/524288 -0700,
233
+ @value=0.3e1>,
234
+ #<Redis::TimeSeries::Sample:0x00007f833e408670
235
+ @time=2021-06-17 20:58:33 1679819/2097152 -0700,
236
+ @value=0.2e1>]>]
237
+ [5] pry(main)> result.keys
238
+ => ["ts3"]
239
+ [6] pry(main)> result['ts3'].values
240
+ => [0.1e1, 0.3e1, 0.2e1]
241
+ ```
242
+ Order them from newest to oldest with `mrevrange`
243
+ ```ruby
244
+ [8] pry(main)> Redis::TimeSeries.mrevrange(1.minute.ago.., filter: { foo: 'bar' }).first.values
245
+ => [0.2e1, 0.3e1, 0.1e1]
246
+ ```
247
+
216
248
  ### Filter DSL
217
249
  You can provide filter strings directly, per the time series documentation.
218
250
  ```ruby
@@ -299,11 +331,8 @@ ts.rules.first.delete
299
331
  Redis::TimeSeries.delete_rule(source: ts, dest: 'other_ts')
300
332
  ```
301
333
 
302
-
303
334
  ### TODO
304
- * `TS.REVRANGE`
305
- * `TS.MRANGE`/`TS.MREVRANGE`
306
- * Probably a bunch more stuff
335
+ * Check the [open issues](https://github.com/dzunk/redis-time-series/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) on Github.
307
336
 
308
337
  ## Development
309
338
 
data/lib/ext/time_msec.rb CHANGED
@@ -26,4 +26,12 @@ module TimeMsec
26
26
  at(timestamp / 1000.0)
27
27
  end
28
28
  end
29
+
30
+ if defined?(ActiveSupport::TimeWithZone)
31
+ refine ActiveSupport::TimeWithZone do
32
+ def ts_msec
33
+ utc.ts_msec
34
+ end
35
+ end
36
+ end
29
37
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ class Redis
3
+ class TimeSeries
4
+ # Duplication policies can be applied to a time series in order to resolve conflicts
5
+ # when adding data that already exists in the series.
6
+ #
7
+ # @see https://oss.redislabs.com/redistimeseries/master/configuration/#duplicate_policy
8
+ class DuplicatePolicy
9
+ VALID_POLICIES = %i[
10
+ block
11
+ first
12
+ last
13
+ min
14
+ max
15
+ sum
16
+ ].freeze
17
+
18
+ attr_reader :policy
19
+
20
+ def initialize(policy)
21
+ policy = policy.to_s.downcase.to_sym
22
+ if VALID_POLICIES.include?(policy)
23
+ @policy = policy
24
+ else
25
+ raise UnknownPolicyError, "#{policy} is not a valid duplicate policy"
26
+ end
27
+ end
28
+
29
+ def to_a(cmd = 'DUPLICATE_POLICY')
30
+ [cmd, policy]
31
+ end
32
+
33
+ def to_s(cmd = 'DUPLICATE_POLICY')
34
+ to_a(cmd).join(' ')
35
+ end
36
+
37
+ def ==(other)
38
+ return policy == other.policy if other.is_a?(self.class)
39
+ policy == self.class.new(other).policy
40
+ end
41
+
42
+ VALID_POLICIES.each do |policy|
43
+ define_method("#{policy}?") do
44
+ @policy == policy
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -15,5 +15,10 @@ class Redis
15
15
  # an unknown type, or when calling a command with an invalid aggregation value.
16
16
  # @see Redis::TimeSeries::Aggregation
17
17
  class AggregationError < Error; end
18
+
19
+ # +UnknownPolicyError+ is raised when attempting to apply an unkown type of
20
+ # duplicate policy when creating or adding to a series.
21
+ # @see Redis::TimeSeries::DuplicatePolicy
22
+ class UnknownPolicyError < Error; end
18
23
  end
19
24
  end
@@ -10,6 +10,10 @@ class Redis
10
10
  #
11
11
  # @!attribute [r] chunk_count
12
12
  # @return [Integer] number of memory chunks used for the time-series
13
+ # @!attribute [r] chunk_size
14
+ # @return [Integer] amount of allocated memory in bytes
15
+ # @!attribute [r] chunk_type
16
+ # @return [String] whether the chunk is "compressed" or "uncompressed"
13
17
  # @!attribute [r] first_timestamp
14
18
  # @return [Integer] first timestamp present in the time-series (milliseconds since epoch)
15
19
  # @!attribute [r] labels
@@ -52,20 +56,43 @@ class Redis
52
56
  :total_samples,
53
57
  keyword_init: true
54
58
  ) do
55
- # @api private
56
- # @return [Info]
57
- def self.parse(series:, data:)
58
- data.each_slice(2).reduce({}) do |h, (key, value)|
59
- # Convert camelCase info keys to snake_case
60
- key = key.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
61
- next h unless members.include?(key)
62
- h[key] = value
63
- h
64
- end.then do |parsed_hash|
65
- parsed_hash[:series] = series
66
- parsed_hash[:labels] = parsed_hash[:labels].to_h
67
- parsed_hash[:rules] = parsed_hash[:rules].map { |d| Rule.new(source: series, data: d) }
68
- new(parsed_hash)
59
+ class << self
60
+ # @api private
61
+ # @return [Info]
62
+ def parse(series:, data:)
63
+ build_hash(data)
64
+ .merge(series: series)
65
+ .then(&method(:parse_labels))
66
+ .then(&method(:parse_policies))
67
+ .then(&method(:parse_rules))
68
+ .then(&method(:new))
69
+ end
70
+
71
+ private
72
+
73
+ def build_hash(data)
74
+ data.each_slice(2).reduce({}) do |h, (key, value)|
75
+ # Convert camelCase info keys to snake_case
76
+ key = key.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
77
+ # Skip unknown properties
78
+ next h unless members.include?(key)
79
+ h.merge(key => value)
80
+ end
81
+ end
82
+
83
+ def parse_labels(hash)
84
+ hash[:labels] = hash[:labels].to_h.transform_values { |v| v.to_i.to_s == v ? v.to_i : v }
85
+ hash
86
+ end
87
+
88
+ def parse_policies(hash)
89
+ hash[:duplicate_policy] = DuplicatePolicy.new(hash[:duplicate_policy]) if hash[:duplicate_policy]
90
+ hash
91
+ end
92
+
93
+ def parse_rules(hash)
94
+ hash[:rules] = hash[:rules].map { |d| Rule.new(source: hash[:series], data: d) }
95
+ hash
69
96
  end
70
97
  end
71
98
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+ class Redis
3
+ class TimeSeries
4
+ # A {Multi} is a collection of multiple series and their samples, returned
5
+ # from a multi command (e.g. TS.MGET or TS.MRANGE).
6
+ #
7
+ # @see TimeSeries.mrange
8
+ # @see TimeSeries.mrevrange
9
+ class Multi < DelegateClass(Array)
10
+ # Multis are initialized by one of the class-level query commands.
11
+ # There's no need to ever create one yourself.
12
+ # @api private
13
+ def initialize(result_array)
14
+ super(result_array.map do |res|
15
+ Result.new(
16
+ TimeSeries.new(res[0]),
17
+ res[1],
18
+ res[2].map { |s| Sample.new(s[0], s[1]) }
19
+ )
20
+ end)
21
+ end
22
+
23
+ # Access a specific result by either array position, or hash lookup.
24
+ #
25
+ # @param index_or_key [Integer, String] The integer position, or series
26
+ # key, of the specific result to return.
27
+ # @return [Multi::Result, nil] A single series result, or nil if there is
28
+ # no matching series.
29
+ def [](index_or_key)
30
+ return super if index_or_key.is_a?(Integer)
31
+ find { |result| result.series.key == index_or_key.to_s }
32
+ end
33
+
34
+ # Get all the series keys that are present in this result collection.
35
+ # @return [Array<String>] An array of the series keys in these results.
36
+ def keys
37
+ map { |r| r.series.key }
38
+ end
39
+
40
+ # Get all the series objects that are present in this result collection.
41
+ # @return [Array<TimeSeries>] An array of the series in these results.
42
+ def series
43
+ map(&:series)
44
+ end
45
+
46
+ # Convert these results into a hash, keyed by series name.
47
+ # @return [Hash<Array>] A hash of series names and samples.
48
+ # @example
49
+ # {"ts3"=>
50
+ # [{:timestamp=>1623945216042, :value=>0.1e1},
51
+ # {:timestamp=>1623945216055, :value=>0.3e1},
52
+ # {:timestamp=>1623945216069, :value=>0.2e1}]}
53
+ def to_h
54
+ super do |result|
55
+ [result.series.key, result.samples.map(&:to_h)]
56
+ end
57
+ end
58
+
59
+ # Get a count of all matching samples from all series in this result collection.
60
+ # @return [Integer] The total size of all samples from all series in these results.
61
+ def sample_count
62
+ reduce(0) { |size, r| size += r.samples.size }
63
+ end
64
+
65
+ Result = Struct.new(:series, :labels, :samples) do
66
+ def values
67
+ samples.map(&:value)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  class Redis
3
3
  class TimeSeries
4
- VERSION = '0.5.2'
4
+ VERSION = '0.7.1'
5
5
  end
6
6
  end
@@ -37,6 +37,12 @@ class Redis
37
37
  # With no value, the series will not be trimmed.
38
38
  # @option options [Boolean] :uncompressed
39
39
  # When true, series data will be stored in an uncompressed format.
40
+ # @option options [String, Symbol] :duplicate_policy
41
+ # A duplication policy to resolve conflicts when adding values to the series.
42
+ # Valid values are in Redis::TimeSeries::DuplicatePolicy::VALID_POLICIES
43
+ # @option options [Integer] :chunk_size
44
+ # Amount of memory, in bytes, to allocate for each chunk of data. Must be a multiple
45
+ # of 8. Default for a series is 4096.
40
46
  #
41
47
  # @return [Redis::TimeSeries] the created time series
42
48
  # @see https://oss.redislabs.com/redistimeseries/commands/#tscreate
@@ -83,6 +89,23 @@ class Redis
83
89
  redis.del key
84
90
  end
85
91
 
92
+ # Add multiple values to multiple series.
93
+ #
94
+ # @example Adding multiple values with timestamps
95
+ # Redis::TimeSeries.madd(
96
+ # foo: { 2.minutes.ago => 123, 1.minute.ago => 456, Time.current => 789) },
97
+ # bar: { 2.minutes.ago => 987, 1.minute.ago => 654, Time.current => 321) }
98
+ # )
99
+ # @example Adding multiple values without timestamps
100
+ # Redis::TimeSeries.madd(foo: 1, bar: 2, baz: 3)
101
+ #
102
+ # @param data [Hash] A hash of key-value pairs, with the key being the name of
103
+ # the series, and the value being a single scalar value or a nested hash
104
+ # of timestamp => value pairs
105
+ # @return [Array<Sample, Redis::CommandError>] an array of the resulting samples
106
+ # added, or a CommandError if the sample in question could not be added to the
107
+ # series
108
+ #
86
109
  def madd(data)
87
110
  data.reduce([]) do |memo, (key, value)|
88
111
  memo << parse_madd_values(key, value)
@@ -93,6 +116,46 @@ class Redis
93
116
  end
94
117
  end
95
118
  end
119
+ alias multi_add madd
120
+ alias add_multiple madd
121
+
122
+ # Query across multiple series, returning values from oldest to newest.
123
+ #
124
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
125
+ # indicate oldest and most recent timestamp, respectively.
126
+ # @param filter [Hash, String] a set of filters to query with. Refer to the {Filters}
127
+ # documentation for more details on how to filter.
128
+ # @param count [Integer] The maximum number of results to return for each series.
129
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
130
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
131
+ # aggregation_type and duration +[:avg, 120000]+
132
+ # @param with_labels [Boolean] Whether to return the label details of the matched
133
+ # series in the result object.
134
+ # @return [Multi] A multi-series collection of results
135
+ #
136
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsmrangetsmrevrange
137
+ def mrange(range, filter:, count: nil, aggregation: nil, with_labels: false)
138
+ multi_cmd('TS.MRANGE', range, filter, count, aggregation, with_labels)
139
+ end
140
+
141
+ # Query across multiple series, returning values from newest to oldest.
142
+ #
143
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
144
+ # indicate oldest and most recent timestamp, respectively.
145
+ # @param filter [Hash, String] a set of filters to query with. Refer to the {Filters}
146
+ # documentation for more details on how to filter.
147
+ # @param count [Integer] The maximum number of results to return for each series.
148
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
149
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
150
+ # aggregation_type and duration +[:avg, 120000]+
151
+ # @param with_labels [Boolean] Whether to return the label details of the matched
152
+ # series in the result object.
153
+ # @return [Multi] A multi-series collection of results
154
+ #
155
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsmrangetsmrevrange
156
+ def mrevrange(range, filter:, count: nil, aggregation: nil, with_labels: false)
157
+ multi_cmd('TS.MREVRANGE', range, filter, count, aggregation, with_labels)
158
+ end
96
159
 
97
160
  # Search for a time series matching the provided filters. Refer to the {Filters} documentation
98
161
  # for more details on how to filter.
@@ -119,19 +182,36 @@ class Redis
119
182
 
120
183
  private
121
184
 
185
+ def multi_cmd(cmd_name, range, filter, count, agg, with_labels)
186
+ filters = Filters.new(filter)
187
+ filters.validate!
188
+ cmd(
189
+ cmd_name,
190
+ (range.begin || '-'),
191
+ (range.end || '+'),
192
+ (['COUNT', count] if count),
193
+ Aggregation.parse(agg)&.to_a,
194
+ ('WITHLABELS' if with_labels),
195
+ ['FILTER', filters.to_a]
196
+ ).then { |response| Multi.new(response) }
197
+ end
198
+
122
199
  def key_for(series_or_string)
123
200
  series_or_string.is_a?(self) ? series_or_string.key : series_or_string.to_s
124
201
  end
125
202
 
126
203
  def parse_madd_values(key, raw)
127
- if raw.is_a?(Hash) || (raw.is_a?(Array) && raw.first.is_a?(Array))
204
+ if raw.is_a? Hash
128
205
  # multiple timestamp => value pairs
129
206
  raw.map do |timestamp, value|
130
207
  [key, timestamp, value]
131
208
  end
132
209
  elsif raw.is_a? Array
133
- # single [timestamp, value]
134
- [key, raw.first, raw.last]
210
+ # multiple values, no timestamps
211
+ now = Time.now.ts_msec
212
+ raw.each_with_index.map do |value, index|
213
+ [key, now + index, value]
214
+ end
135
215
  else
136
216
  # single value, no timestamp
137
217
  [key, '*', raw]
@@ -154,21 +234,33 @@ class Redis
154
234
  # @param value [Numeric] the value to add
155
235
  # @param timestamp [Time, Numeric] the +Time+, or integer timestamp in milliseconds, to add the value
156
236
  # @param uncompressed [Boolean] if true, stores data in an uncompressed format
237
+ # @param on_duplicate [String, Symbol] a duplication policy for conflict resolution
238
+ # @param chunk_size [Integer] set default chunk size, in bytes, for the time series
157
239
  #
158
240
  # @return [Sample] the value that was added
159
241
  # @raise [Redis::CommandError] if the value being added is older than the latest timestamp in the series
160
- def add(value, timestamp = '*', uncompressed: nil)
161
- ts = cmd 'TS.ADD', key, timestamp, value, ('UNCOMPRESSED' if uncompressed)
242
+ #
243
+ # @see TimeSeries::DuplicatePolicy
244
+ def add(value, timestamp = '*', uncompressed: nil, on_duplicate: nil, chunk_size: nil)
245
+ ts = cmd 'TS.ADD',
246
+ key,
247
+ timestamp,
248
+ value,
249
+ ('UNCOMPRESSED' if uncompressed),
250
+ (['CHUNK_SIZE', chunk_size] if chunk_size),
251
+ (DuplicatePolicy.new(on_duplicate).to_a('ON_DUPLICATE') if on_duplicate)
162
252
  Sample.new(ts, value)
163
253
  end
164
254
 
165
255
  # Issues a TS.CREATE command for the current series.
166
256
  # You should use class method {Redis::TimeSeries.create} instead.
167
257
  # @api private
168
- def create(retention: nil, uncompressed: nil, labels: nil)
258
+ def create(retention: nil, uncompressed: nil, labels: nil, duplicate_policy: nil, chunk_size: nil)
169
259
  cmd 'TS.CREATE', key,
170
260
  (['RETENTION', retention] if retention),
171
261
  ('UNCOMPRESSED' if uncompressed),
262
+ (['CHUNK_SIZE', chunk_size] if chunk_size),
263
+ (DuplicatePolicy.new(duplicate_policy).to_a if duplicate_policy),
172
264
  (['LABELS', labels.to_a] if labels&.any?)
173
265
  self
174
266
  end
@@ -206,11 +298,17 @@ class Redis
206
298
  # @param value [Integer] the amount to decrement by
207
299
  # @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
208
300
  # @param uncompressed [Boolean] if true, stores data in an uncompressed format
301
+ # @param chunk_size [Integer] set default chunk size, in bytes, for the time series
209
302
  #
210
303
  # @return [Integer] the timestamp the value was stored at
211
304
  # @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
212
- def decrby(value = 1, timestamp = nil, uncompressed: nil)
213
- cmd 'TS.DECRBY', key, value, (timestamp if timestamp), ('UNCOMPRESSED' if uncompressed)
305
+ def decrby(value = 1, timestamp = nil, uncompressed: nil, chunk_size: nil)
306
+ cmd 'TS.DECRBY',
307
+ key,
308
+ value,
309
+ (timestamp if timestamp),
310
+ ('UNCOMPRESSED' if uncompressed),
311
+ (['CHUNK_SIZE', chunk_size] if chunk_size)
214
312
  end
215
313
  alias decrement decrby
216
314
 
@@ -241,11 +339,17 @@ class Redis
241
339
  # @param value [Integer] the amount to increment by
242
340
  # @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
243
341
  # @param uncompressed [Boolean] if true, stores data in an uncompressed format
342
+ # @param chunk_size [Integer] set default chunk size, in bytes, for the time series
244
343
  #
245
344
  # @return [Integer] the timestamp the value was stored at
246
345
  # @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
247
- def incrby(value = 1, timestamp = nil, uncompressed: nil)
248
- cmd 'TS.INCRBY', key, value, (timestamp if timestamp), ('UNCOMPRESSED' if uncompressed)
346
+ def incrby(value = 1, timestamp = nil, uncompressed: nil, chunk_size: nil)
347
+ cmd 'TS.INCRBY',
348
+ key,
349
+ value,
350
+ (timestamp if timestamp),
351
+ ('UNCOMPRESSED' if uncompressed),
352
+ (['CHUNK_SIZE', chunk_size] if chunk_size)
249
353
  end
250
354
  alias increment incrby
251
355
 
@@ -272,32 +376,30 @@ class Redis
272
376
  cmd 'TS.ALTER', key, 'LABELS', val.to_a
273
377
  end
274
378
 
275
- def madd(*values)
276
- if values.one? && values.first.is_a?(Hash)
277
- # Hash of timestamp => value pairs
278
- args = values.first.map do |ts, val|
279
- [key, ts, val]
280
- end.flatten
281
- elsif values.one? && values.first.is_a?(Array)
282
- # Array of values, no timestamps
283
- initial_ts = Time.now.ts_msec
284
- args = values.first.each_with_index.map do |val, idx|
285
- [key, initial_ts + idx, val]
286
- end.flatten
287
- else
288
- # Values as individual arguments, no timestamps
289
- initial_ts = Time.now.ts_msec
290
- args = values.each_with_index.map do |val, idx|
291
- [key, initial_ts + idx, val]
292
- end.flatten
379
+ # Add multiple values to the series.
380
+ #
381
+ # @example Adding multiple values with timestamps
382
+ # ts.madd(2.minutes.ago => 987, 1.minute.ago => 654, Time.current => 321)
383
+ #
384
+ # @param data [Hash] A hash of key-value pairs, with the key being a Time
385
+ # object or integer timestamp, and the value being a single scalar value
386
+ # @return [Array<Sample, Redis::CommandError>] an array of the resulting samples
387
+ # added, or a CommandError if the sample in question could not be added to the
388
+ # series
389
+ #
390
+ def madd(data)
391
+ args = self.class.send(:parse_madd_values, key, data)
392
+ cmd('TS.MADD', args).each_with_index.map do |result, idx|
393
+ result.is_a?(Redis::CommandError) ? result : Sample.new(result, args[idx][2])
293
394
  end
294
- # TODO: return Sample objects here
295
- cmd 'TS.MADD', args
296
395
  end
396
+ alias multi_add madd
397
+ alias add_multiple madd
297
398
 
298
- # Get a range of values from the series
399
+ # Get a range of values from the series, from earliest to most recent
299
400
  #
300
- # @param range [Hash, Range] a time range, or hash of +from+ and +to+ values
401
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
402
+ # indicate oldest and most recent timestamp, respectively.
301
403
  # @param count [Integer] the maximum number of results to return
302
404
  # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
303
405
  # The aggregation to apply. Can be an {Aggregation} object, or an array of
@@ -307,20 +409,23 @@ class Redis
307
409
  #
308
410
  # @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
309
411
  def range(range, count: nil, aggregation: nil)
310
- if range.is_a?(Hash)
311
- # This is to support from: and to: passed in as hash keys
312
- # `range` will swallow all parameters if they're all hash syntax
313
- count = range.delete(:count)
314
- aggregation = range.delete(:aggregation)
315
- range = range.fetch(:from)..range.fetch(:to)
316
- end
317
- cmd('TS.RANGE',
318
- key,
319
- range.min,
320
- range.max,
321
- (['COUNT', count] if count),
322
- Aggregation.parse(aggregation)&.to_a
323
- ).map { |ts, val| Sample.new(ts, val) }
412
+ range_cmd('TS.RANGE', range, count, aggregation)
413
+ end
414
+
415
+ # Get a range of values from the series, from most recent to earliest
416
+ #
417
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
418
+ # indicate oldest and most recent timestamp, respectively.
419
+ # @param count [Integer] the maximum number of results to return
420
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
421
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
422
+ # aggregation_type and duration +[:avg, 120000]+
423
+ #
424
+ # @return [Array<Sample>] an array of samples matching the range query
425
+ #
426
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
427
+ def revrange(range, count: nil, aggregation: nil)
428
+ range_cmd('TS.REVRANGE', range, count, aggregation)
324
429
  end
325
430
 
326
431
  # Set data retention time for the series using +TS.ALTER+
@@ -340,5 +445,17 @@ class Redis
340
445
  return false unless other.is_a?(self.class)
341
446
  key == other.key && redis == other.redis
342
447
  end
448
+
449
+ private
450
+
451
+ def range_cmd(cmd_name, range, count, agg)
452
+ cmd(cmd_name,
453
+ key,
454
+ (range.begin || '-'),
455
+ (range.end || '+'),
456
+ (['COUNT', count] if count),
457
+ Aggregation.parse(agg)&.to_a
458
+ ).map { |ts, val| Sample.new(ts, val) }
459
+ end
343
460
  end
344
461
  end
@@ -1,10 +1,13 @@
1
1
  require 'bigdecimal'
2
2
  require 'forwardable'
3
3
  require 'ext/time_msec'
4
+
4
5
  require 'redis/time_series/client'
5
6
  require 'redis/time_series/errors'
6
7
  require 'redis/time_series/aggregation'
8
+ require 'redis/time_series/duplicate_policy'
7
9
  require 'redis/time_series/filters'
10
+ require 'redis/time_series/multi'
8
11
  require 'redis/time_series/rule'
9
12
  require 'redis/time_series/info'
10
13
  require 'redis/time_series/sample'
@@ -31,10 +31,11 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ['lib']
33
33
 
34
- spec.add_dependency 'redis', '~> 4.0'
34
+ spec.add_dependency 'redis', '>= 3.3'
35
35
 
36
36
  spec.add_development_dependency 'activesupport', '~> 6.0'
37
- spec.add_development_dependency 'bundler', '~> 1.17'
37
+ spec.add_development_dependency 'appraisal', '>= 2.4.1'
38
+ spec.add_development_dependency 'bundler', '~> 2.0'
38
39
  spec.add_development_dependency 'pry', '~> 0.13'
39
40
  spec.add_development_dependency 'rake', '~> 13.0'
40
41
  spec.add_development_dependency 'rspec', '~> 3.0'
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-time-series
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Duszynski
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-10 00:00:00.000000000 Z
11
+ date: 2022-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.0'
19
+ version: '3.3'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4.0'
26
+ version: '3.3'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,20 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: appraisal
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.4.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.4.1
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '1.17'
61
+ version: '2.0'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '1.17'
68
+ version: '2.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: pry
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -108,7 +122,7 @@ dependencies:
108
122
  - - "<"
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0.18'
111
- description:
125
+ description:
112
126
  email:
113
127
  - dzunk@hey.com
114
128
  executables: []
@@ -118,9 +132,9 @@ files:
118
132
  - ".github/workflows/rspec.yml"
119
133
  - ".gitignore"
120
134
  - ".rspec"
135
+ - Appraisals
121
136
  - CHANGELOG.md
122
137
  - Gemfile
123
- - Gemfile.lock
124
138
  - LICENSE.txt
125
139
  - README.md
126
140
  - Rakefile
@@ -131,9 +145,11 @@ files:
131
145
  - lib/redis/time_series.rb
132
146
  - lib/redis/time_series/aggregation.rb
133
147
  - lib/redis/time_series/client.rb
148
+ - lib/redis/time_series/duplicate_policy.rb
134
149
  - lib/redis/time_series/errors.rb
135
150
  - lib/redis/time_series/filters.rb
136
151
  - lib/redis/time_series/info.rb
152
+ - lib/redis/time_series/multi.rb
137
153
  - lib/redis/time_series/rule.rb
138
154
  - lib/redis/time_series/sample.rb
139
155
  - lib/redis/time_series/version.rb
@@ -145,7 +161,7 @@ metadata:
145
161
  homepage_uri: https://github.com/dzunk/redis-time-series
146
162
  source_code_uri: https://github.com/dzunk/redis-time-series
147
163
  changelog_uri: https://github.com/dzunk/redis-time-series/blob/master/CHANGELOG.md
148
- post_install_message:
164
+ post_install_message:
149
165
  rdoc_options: []
150
166
  require_paths:
151
167
  - lib
@@ -160,8 +176,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
176
  - !ruby/object:Gem::Version
161
177
  version: '0'
162
178
  requirements: []
163
- rubygems_version: 3.0.3
164
- signing_key:
179
+ rubygems_version: 3.2.22
180
+ signing_key:
165
181
  specification_version: 4
166
182
  summary: A Ruby adapter for the RedisTimeSeries module.
167
183
  test_files: []
data/Gemfile.lock DELETED
@@ -1,66 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- redis-time-series (0.5.2)
5
- redis (~> 4.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- activesupport (6.0.3.1)
11
- concurrent-ruby (~> 1.0, >= 1.0.2)
12
- i18n (>= 0.7, < 2)
13
- minitest (~> 5.1)
14
- tzinfo (~> 1.1)
15
- zeitwerk (~> 2.2, >= 2.2.2)
16
- coderay (1.1.3)
17
- concurrent-ruby (1.1.6)
18
- diff-lcs (1.3)
19
- docile (1.3.2)
20
- i18n (1.8.3)
21
- concurrent-ruby (~> 1.0)
22
- json (2.3.1)
23
- method_source (1.0.0)
24
- minitest (5.14.1)
25
- pry (0.13.1)
26
- coderay (~> 1.1)
27
- method_source (~> 1.0)
28
- rake (13.0.1)
29
- redis (4.2.2)
30
- rspec (3.9.0)
31
- rspec-core (~> 3.9.0)
32
- rspec-expectations (~> 3.9.0)
33
- rspec-mocks (~> 3.9.0)
34
- rspec-core (3.9.2)
35
- rspec-support (~> 3.9.3)
36
- rspec-expectations (3.9.2)
37
- diff-lcs (>= 1.2.0, < 2.0)
38
- rspec-support (~> 3.9.0)
39
- rspec-mocks (3.9.1)
40
- diff-lcs (>= 1.2.0, < 2.0)
41
- rspec-support (~> 3.9.0)
42
- rspec-support (3.9.3)
43
- simplecov (0.17.1)
44
- docile (~> 1.1)
45
- json (>= 1.8, < 3)
46
- simplecov-html (~> 0.10.0)
47
- simplecov-html (0.10.2)
48
- thread_safe (0.3.6)
49
- tzinfo (1.2.7)
50
- thread_safe (~> 0.1)
51
- zeitwerk (2.3.0)
52
-
53
- PLATFORMS
54
- ruby
55
-
56
- DEPENDENCIES
57
- activesupport (~> 6.0)
58
- bundler (~> 1.17)
59
- pry (~> 0.13)
60
- rake (~> 13.0)
61
- redis-time-series!
62
- rspec (~> 3.0)
63
- simplecov (< 0.18)
64
-
65
- BUNDLED WITH
66
- 1.17.2