redis-time-series 0.5.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 60ddc8ba631c4016031490a9eb8c3e8659d6b38d3f21e3798a253ec3f63b2815
4
- data.tar.gz: 7993209c75f9f23ed0fae6f918020f2a7a793575e6636e57c1bc225db3f3a63d
3
+ metadata.gz: 400fac8affe8b6c5acbc7892bd9704fb39da3135929451ce282ad43278f698ff
4
+ data.tar.gz: 70a10d9ca8cd01e8b113e6b769008ccb0e9a167cbd16777773d2df8d8a41d2c2
5
5
  SHA512:
6
- metadata.gz: 1bd033ed2dfef155ba923ebee94089e41f507dcb8fcb63711a56585703d1603c32ff86b99067641b8a248587925d2bf489affd79822118e5fb42b05dbcf1c374
7
- data.tar.gz: 553ffbea629efff4b1b436faf7542f35204cd9a0f47c2cf28dcd3ecc0261bd399eb19ac57a56985fc3fbeaef0e4b2e24786483bc385e06928fedcbad4f6709d0
6
+ metadata.gz: 7b166b6d74161936686e1f0d8d6d5b27238414036b16fc8c4537cc02a2848734b8bf26a183820069b7ad662d45465fc38e6fc1c307dbc543c028a746a30d5024
7
+ data.tar.gz: 3f3f178d267e261e9c3ee17b50fff7e5826d231bf9051364c070727af13841a6118596c255857bf750973445a9860cbbeaa007001425f602d409381dd24a851f
@@ -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,26 @@
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
+
12
+ ## 0.6.0
13
+ * Add CHUNK_SIZE param to CREATE, ADD, INCRBY, DECRBY commands (#53)
14
+ * Add duplication policy to TS.CREATE and TS.ADD commands (#51)
15
+ * Add support for endless ranges to TS.RANGE (#50)
16
+ * Cast label values to integers in Info struct (#49)
17
+ * Build against edge upstream in addition to latest stable (#48)
18
+
19
+ ## 0.5.2
20
+ * Add chunk_type to info struct (#47)
21
+
22
+ ## 0.5.1
23
+ * Update Info struct for RTS 1.4 compatibility (#45)
24
+
5
25
  ## 0.5.0
6
26
  * Fix aggregations for TS.RANGE command (#34)
7
27
  * Extract client handling into Client module (#32)
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,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
@@ -37,6 +41,9 @@ class Redis
37
41
  # @see https://oss.redislabs.com/redistimeseries/commands/#tsinfo
38
42
  Info = Struct.new(
39
43
  :chunk_count,
44
+ :chunk_size,
45
+ :chunk_type,
46
+ :duplicate_policy,
40
47
  :first_timestamp,
41
48
  :labels,
42
49
  :last_timestamp,
@@ -49,18 +56,43 @@ class Redis
49
56
  :total_samples,
50
57
  keyword_init: true
51
58
  ) do
52
- # @api private
53
- # @return [Info]
54
- def self.parse(series:, data:)
55
- data.each_slice(2).reduce({}) do |h, (key, value)|
56
- # Convert camelCase info keys to snake_case
57
- h[key.gsub(/(.)([A-Z])/,'\1_\2').downcase] = value
58
- h
59
- end.then do |parsed_hash|
60
- parsed_hash['series'] = series
61
- parsed_hash['labels'] = parsed_hash['labels'].to_h
62
- parsed_hash['rules'] = parsed_hash['rules'].map { |d| Rule.new(source: series, data: d) }
63
- 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
64
96
  end
65
97
  end
66
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.0'
4
+ VERSION = '0.7.0'
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.0
4
+ version: 0.7.0
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-07-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
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.0)
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.1)
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