redis-time-series 0.6.0 → 0.7.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: 4648678522434687a6e278490c7a308c64f6460394b8ebde16b2ce2daa9e92ad
4
- data.tar.gz: e2f48fb1c8efebb3b4d6e4c47d1d86137a10ac8a54735c94b3bdb35ef6396651
3
+ metadata.gz: 400fac8affe8b6c5acbc7892bd9704fb39da3135929451ce282ad43278f698ff
4
+ data.tar.gz: 70a10d9ca8cd01e8b113e6b769008ccb0e9a167cbd16777773d2df8d8a41d2c2
5
5
  SHA512:
6
- metadata.gz: 269a787d8d2a2a467a52e915ab717bc5b20eb28cacc55eb5dfc9ce3909a7d83cdd4e15b19afc92612d19fa551c6ba793df76ceef03d8bb8bdb16b6dc0253d2cc
7
- data.tar.gz: c5d845052ca8f2a518fb3a20a82a8d8aa35128cb824f54d2531a949767e90f8c7979ede07ba1bd56ae30a825edb9f6be90e52ca404a14199bfd13484026d8d40
6
+ metadata.gz: 7b166b6d74161936686e1f0d8d6d5b27238414036b16fc8c4537cc02a2848734b8bf26a183820069b7ad662d45465fc38e6fc1c307dbc543c028a746a30d5024
7
+ data.tar.gz: 3f3f178d267e261e9c3ee17b50fff7e5826d231bf9051364c070727af13841a6118596c255857bf750973445a9860cbbeaa007001425f602d409381dd24a851f
@@ -12,9 +12,10 @@ jobs:
12
12
  spec:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
+ fail-fast: false
15
16
  matrix:
16
17
  image_version: ['latest', 'edge']
17
- ruby_version: ['2.6', '2.7']
18
+ ruby_version: ['2.6', '2.7', '3.0', '3.1']
18
19
  services:
19
20
  redis:
20
21
  image: redislabs/redistimeseries:${{ matrix.image_version }}
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.0
6
+ * Add Ruby 3.1 to build matrix (#70)
7
+ * Add Ruby 3.0 to build matrix (#63)
8
+ * Relax Redis version constraint (#62)
9
+ * Add TS.REVRANGE, TS.MRANGE, and TS.MREVRANGE commands (#19)
10
+ * Update TS.MADD commands to consolidate parsing (#58)
11
+
5
12
  ## 0.6.0
6
13
  * Add CHUNK_SIZE param to CREATE, ADD, INCRBY, DECRBY commands (#53)
7
14
  * Add duplication policy to TS.CREATE and TS.ADD commands (#51)
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
 
@@ -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.6.0'
4
+ VERSION = '0.7.0'
5
5
  end
6
6
  end
@@ -41,7 +41,8 @@ class Redis
41
41
  # A duplication policy to resolve conflicts when adding values to the series.
42
42
  # Valid values are in Redis::TimeSeries::DuplicatePolicy::VALID_POLICIES
43
43
  # @option options [Integer] :chunk_size
44
- # Amount of memory, in bytes, to allocate for each chunk of data
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.
45
46
  #
46
47
  # @return [Redis::TimeSeries] the created time series
47
48
  # @see https://oss.redislabs.com/redistimeseries/commands/#tscreate
@@ -88,6 +89,23 @@ class Redis
88
89
  redis.del key
89
90
  end
90
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
+ #
91
109
  def madd(data)
92
110
  data.reduce([]) do |memo, (key, value)|
93
111
  memo << parse_madd_values(key, value)
@@ -98,6 +116,46 @@ class Redis
98
116
  end
99
117
  end
100
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
101
159
 
102
160
  # Search for a time series matching the provided filters. Refer to the {Filters} documentation
103
161
  # for more details on how to filter.
@@ -124,19 +182,36 @@ class Redis
124
182
 
125
183
  private
126
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
+
127
199
  def key_for(series_or_string)
128
200
  series_or_string.is_a?(self) ? series_or_string.key : series_or_string.to_s
129
201
  end
130
202
 
131
203
  def parse_madd_values(key, raw)
132
- if raw.is_a?(Hash) || (raw.is_a?(Array) && raw.first.is_a?(Array))
204
+ if raw.is_a? Hash
133
205
  # multiple timestamp => value pairs
134
206
  raw.map do |timestamp, value|
135
207
  [key, timestamp, value]
136
208
  end
137
209
  elsif raw.is_a? Array
138
- # single [timestamp, value]
139
- [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
140
215
  else
141
216
  # single value, no timestamp
142
217
  [key, '*', raw]
@@ -301,32 +376,30 @@ class Redis
301
376
  cmd 'TS.ALTER', key, 'LABELS', val.to_a
302
377
  end
303
378
 
304
- def madd(*values)
305
- if values.one? && values.first.is_a?(Hash)
306
- # Hash of timestamp => value pairs
307
- args = values.first.map do |ts, val|
308
- [key, ts, val]
309
- end.flatten
310
- elsif values.one? && values.first.is_a?(Array)
311
- # Array of values, no timestamps
312
- initial_ts = Time.now.ts_msec
313
- args = values.first.each_with_index.map do |val, idx|
314
- [key, initial_ts + idx, val]
315
- end.flatten
316
- else
317
- # Values as individual arguments, no timestamps
318
- initial_ts = Time.now.ts_msec
319
- args = values.each_with_index.map do |val, idx|
320
- [key, initial_ts + idx, val]
321
- 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])
322
394
  end
323
- # TODO: return Sample objects here
324
- cmd 'TS.MADD', args
325
395
  end
396
+ alias multi_add madd
397
+ alias add_multiple madd
326
398
 
327
- # Get a range of values from the series
399
+ # Get a range of values from the series, from earliest to most recent
328
400
  #
329
- # @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.
330
403
  # @param count [Integer] the maximum number of results to return
331
404
  # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
332
405
  # The aggregation to apply. Can be an {Aggregation} object, or an array of
@@ -336,20 +409,23 @@ class Redis
336
409
  #
337
410
  # @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
338
411
  def range(range, count: nil, aggregation: nil)
339
- if range.is_a?(Hash)
340
- # This is to support from: and to: passed in as hash keys
341
- # `range` will swallow all parameters if they're all hash syntax
342
- count = range.delete(:count)
343
- aggregation = range.delete(:aggregation)
344
- range = range.fetch(:from)..range[:to]
345
- end
346
- cmd('TS.RANGE',
347
- key,
348
- (range.begin || '-'),
349
- (range.end || '+'),
350
- (['COUNT', count] if count),
351
- Aggregation.parse(aggregation)&.to_a
352
- ).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)
353
429
  end
354
430
 
355
431
  # Set data retention time for the series using +TS.ALTER+
@@ -369,5 +445,17 @@ class Redis
369
445
  return false unless other.is_a?(self.class)
370
446
  key == other.key && redis == other.redis
371
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
372
460
  end
373
461
  end
@@ -7,6 +7,7 @@ require 'redis/time_series/errors'
7
7
  require 'redis/time_series/aggregation'
8
8
  require 'redis/time_series/duplicate_policy'
9
9
  require 'redis/time_series/filters'
10
+ require 'redis/time_series/multi'
10
11
  require 'redis/time_series/rule'
11
12
  require 'redis/time_series/info'
12
13
  require 'redis/time_series/sample'
@@ -31,10 +31,10 @@ 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', '>= 3.3', '< 5'
34
+ spec.add_dependency 'redis', '>= 3.3'
35
35
 
36
36
  spec.add_development_dependency 'activesupport', '~> 6.0'
37
- spec.add_development_dependency 'appraisal'
37
+ spec.add_development_dependency 'appraisal', '>= 2.4.1'
38
38
  spec.add_development_dependency 'bundler', '~> 2.0'
39
39
  spec.add_development_dependency 'pry', '~> 0.13'
40
40
  spec.add_development_dependency 'rake', '~> 13.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-time-series
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Duszynski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-20 00:00:00.000000000 Z
11
+ date: 2022-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.3'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '5'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,9 +24,6 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '3.3'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '5'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: activesupport
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -50,14 +44,14 @@ dependencies:
50
44
  requirements:
51
45
  - - ">="
52
46
  - !ruby/object:Gem::Version
53
- version: '0'
47
+ version: 2.4.1
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
51
  requirements:
58
52
  - - ">="
59
53
  - !ruby/object:Gem::Version
60
- version: '0'
54
+ version: 2.4.1
61
55
  - !ruby/object:Gem::Dependency
62
56
  name: bundler
63
57
  requirement: !ruby/object:Gem::Requirement
@@ -155,6 +149,7 @@ files:
155
149
  - lib/redis/time_series/errors.rb
156
150
  - lib/redis/time_series/filters.rb
157
151
  - lib/redis/time_series/info.rb
152
+ - lib/redis/time_series/multi.rb
158
153
  - lib/redis/time_series/rule.rb
159
154
  - lib/redis/time_series/sample.rb
160
155
  - lib/redis/time_series/version.rb
@@ -181,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
176
  - !ruby/object:Gem::Version
182
177
  version: '0'
183
178
  requirements: []
184
- rubygems_version: 3.2.2
179
+ rubygems_version: 3.2.22
185
180
  signing_key:
186
181
  specification_version: 4
187
182
  summary: A Ruby adapter for the RedisTimeSeries module.