redisarray 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/Gemfile.lock +28 -0
- data/README.md +96 -0
- data/Rakefile +2 -0
- data/lib/redisarray.rb +12 -0
- data/lib/redisarray/core_ext/array/extract_options.rb +29 -0
- data/lib/redisarray/core_ext/class/attribute_accessors.rb +79 -0
- data/lib/redisarray/core_ext/hash/reverse_merge.rb +23 -0
- data/lib/redisarray/redis_hash_group.rb +22 -0
- data/lib/redisarray/redis_table.rb +187 -0
- data/lib/redisarray/redis_workbook.rb +81 -0
- data/lib/redisarray/version.rb +3 -0
- data/redisarray.gemspec +25 -0
- data/spec/fixtures/256_rows.yaml +2049 -0
- data/spec/fixtures/328_rows.yaml +2626 -0
- data/spec/fixtures/five_rows.yaml +41 -0
- data/spec/fixtures/one_row.yaml +9 -0
- data/spec/fixtures/one_row_with_headers/headers.yaml +37 -0
- data/spec/fixtures/one_row_with_headers/one_row.yaml +37 -0
- data/spec/fixtures/three_rows.yaml +25 -0
- data/spec/redis_hash_group_spec.rb +40 -0
- data/spec/redis_workbook_spec.rb +239 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/rspec.rake +9 -0
- metadata +135 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
redisarray (0.0.1)
|
5
|
+
redis (~> 2.2.2)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.1.3)
|
11
|
+
rake (0.9.2.2)
|
12
|
+
redis (2.2.2)
|
13
|
+
rspec (2.7.0)
|
14
|
+
rspec-core (~> 2.7.0)
|
15
|
+
rspec-expectations (~> 2.7.0)
|
16
|
+
rspec-mocks (~> 2.7.0)
|
17
|
+
rspec-core (2.7.1)
|
18
|
+
rspec-expectations (2.7.0)
|
19
|
+
diff-lcs (~> 1.1.2)
|
20
|
+
rspec-mocks (2.7.0)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
x86-mingw32
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
rake (~> 0.9)
|
27
|
+
redisarray!
|
28
|
+
rspec (~> 2.7.0)
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
redisarray
|
2
|
+
==========
|
3
|
+
|
4
|
+
[RedisArray] Implements memory efficient algorithm allowing to store tables or two dimensional arrays inside Redis.
|
5
|
+
|
6
|
+
Introduction
|
7
|
+
============
|
8
|
+
|
9
|
+
Often we need to store a table with headers and rows or just an adjacent 2D array inside our Redis key-value store. We could quickly design our own custom solution but it probably would not be optimized for memory usage.
|
10
|
+
The purpose of this project is to design general solution for storing two dimensional arrays inside Redis with memory optimization in mind.
|
11
|
+
How is that accomplished? All the data that is going to be written is just sliced on pieces behind the scenes and stored in hashes with 100 of fields each which is memory efficient. To read more about this technique follow this page: http://redis.io/topics/memory-optimization
|
12
|
+
|
13
|
+
|
14
|
+
Compatibility
|
15
|
+
=============
|
16
|
+
|
17
|
+
So far compatible with ruby 1.8.7. Does not work with 1.9.2. There are a few adjustments needed concerning the use of CSV class which has significantly changed in 1.9.2.
|
18
|
+
|
19
|
+
Install
|
20
|
+
=======
|
21
|
+
|
22
|
+
gem install redisarray
|
23
|
+
|
24
|
+
Usage
|
25
|
+
=====
|
26
|
+
|
27
|
+
There are two classes worth noting:
|
28
|
+
- RedisTable - where most of the action takes place
|
29
|
+
- RedisWorkbook - a wrapper for RedisTable that simplifies storage of array of arrays or a workbook of worksheets with headers which is itself an array of arrays
|
30
|
+
|
31
|
+
You can start using redisarray gem like this:
|
32
|
+
|
33
|
+
require 'rubygems'
|
34
|
+
require 'redisarray'
|
35
|
+
include RedisArray
|
36
|
+
|
37
|
+
group_name = RedisHashGroup.new.name
|
38
|
+
@workbook = RedisWorkbook.new group_name, 'my_workbook'
|
39
|
+
@workbook.set_sheet_data "my_sheet_name",
|
40
|
+
rows = [['row 1 cell 1','row 1 cell 2'], ['row 2 cell 1','row 2 cell 2']],
|
41
|
+
:start_from_row => 3
|
42
|
+
p @workbook.get_sheet_data("my_sheet_name")
|
43
|
+
|
44
|
+
By default it will assume redis is listening on localhost port 6379, to change it do this:
|
45
|
+
|
46
|
+
require 'rubygems'
|
47
|
+
require 'redisarray'
|
48
|
+
require 'redis'
|
49
|
+
include RedisArray
|
50
|
+
|
51
|
+
RedisTable.set_redis Redis.new(:host => 'localhost', :port => 6379)
|
52
|
+
|
53
|
+
group_name = RedisHashGroup.new.name
|
54
|
+
...
|
55
|
+
|
56
|
+
For more examples take a look at spec directory especially in redis_workbook_spec.rb
|
57
|
+
|
58
|
+
Developer Instructions
|
59
|
+
======================
|
60
|
+
|
61
|
+
The dependencies for the gem and for developing the gem are managed by Bundler.
|
62
|
+
|
63
|
+
gem install bundler
|
64
|
+
git clone http://github.com/ksob/redisarray.git
|
65
|
+
cd ./redisarray
|
66
|
+
bundle install
|
67
|
+
|
68
|
+
Specs can be run with (they require some preconditions to be met like running Redis on localhost port 6379):
|
69
|
+
|
70
|
+
bundle exec rspec spec
|
71
|
+
|
72
|
+
License
|
73
|
+
=======
|
74
|
+
|
75
|
+
(The MIT License)
|
76
|
+
|
77
|
+
Copyright (c) 2011 Kamil Sobieraj, ksobej@gmail.com
|
78
|
+
|
79
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
80
|
+
a copy of this software and associated documentation files (the
|
81
|
+
'Software'), to deal in the Software without restriction, including
|
82
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
83
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
84
|
+
permit persons to whom the Software is furnished to do so, subject to
|
85
|
+
the following conditions:
|
86
|
+
|
87
|
+
The above copyright notice and this permission notice shall be
|
88
|
+
included in all copies or substantial portions of the Software.
|
89
|
+
|
90
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
91
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
92
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
93
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
94
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
95
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
96
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/lib/redisarray.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module RedisArray
|
2
|
+
require 'redisarray/core_ext/hash/reverse_merge'
|
3
|
+
require 'redisarray/core_ext/class/attribute_accessors'
|
4
|
+
require 'csv'
|
5
|
+
require 'redis'
|
6
|
+
|
7
|
+
require 'redisarray/version'
|
8
|
+
require 'redisarray/redis_table'
|
9
|
+
require 'redisarray/redis_hash_group'
|
10
|
+
require 'redisarray/redis_workbook'
|
11
|
+
end
|
12
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Hash
|
2
|
+
# By default, only instances of Hash itself are extractable.
|
3
|
+
# Subclasses of Hash may implement this method and return
|
4
|
+
# true to declare themselves as extractable. If a Hash
|
5
|
+
# is extractable, Array#extract_options! pops it from
|
6
|
+
# the Array when it is the last element of the Array.
|
7
|
+
def extractable_options?
|
8
|
+
instance_of?(Hash)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Array
|
13
|
+
# Extracts options from a set of arguments. Removes and returns the last
|
14
|
+
# element in the array if it's a hash, otherwise returns a blank hash.
|
15
|
+
#
|
16
|
+
# def options(*args)
|
17
|
+
# args.extract_options!
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# options(1, 2) # => {}
|
21
|
+
# options(1, 2, :a => :b) # => {:a=>:b}
|
22
|
+
def extract_options!
|
23
|
+
if last.is_a?(Hash) && last.extractable_options?
|
24
|
+
pop
|
25
|
+
else
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "array/extract_options"))
|
2
|
+
|
3
|
+
# Extends the class object with class and instance accessors for class attributes,
|
4
|
+
# just like the native attr* accessors for instance attributes.
|
5
|
+
#
|
6
|
+
# Note that unlike +class_attribute+, if a subclass changes the value then that would
|
7
|
+
# also change the value for parent class. Similarly if parent class changes the value
|
8
|
+
# then that would change the value of subclasses too.
|
9
|
+
#
|
10
|
+
# class Person
|
11
|
+
# cattr_accessor :hair_colors
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
15
|
+
# Person.hair_colors # => [:brown, :black, :blonde, :red]
|
16
|
+
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
|
17
|
+
#
|
18
|
+
# To opt out of the instance writer method, pass :instance_writer => false.
|
19
|
+
# To opt out of the instance reader method, pass :instance_reader => false.
|
20
|
+
#
|
21
|
+
# class Person
|
22
|
+
# cattr_accessor :hair_colors, :instance_writer => false, :instance_reader => false
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Person.new.hair_colors = [:brown] # => NoMethodError
|
26
|
+
# Person.new.hair_colors # => NoMethodError
|
27
|
+
class Class
|
28
|
+
def cattr_reader(*syms)
|
29
|
+
options = syms.extract_options!
|
30
|
+
syms.each do |sym|
|
31
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
32
|
+
unless defined? @@#{sym}
|
33
|
+
@@#{sym} = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.#{sym}
|
37
|
+
@@#{sym}
|
38
|
+
end
|
39
|
+
EOS
|
40
|
+
|
41
|
+
unless options[:instance_reader] == false
|
42
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
43
|
+
def #{sym}
|
44
|
+
@@#{sym}
|
45
|
+
end
|
46
|
+
EOS
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def cattr_writer(*syms)
|
52
|
+
options = syms.extract_options!
|
53
|
+
syms.each do |sym|
|
54
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
55
|
+
unless defined? @@#{sym}
|
56
|
+
@@#{sym} = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.#{sym}=(obj)
|
60
|
+
@@#{sym} = obj
|
61
|
+
end
|
62
|
+
EOS
|
63
|
+
|
64
|
+
unless options[:instance_writer] == false
|
65
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
66
|
+
def #{sym}=(obj)
|
67
|
+
@@#{sym} = obj
|
68
|
+
end
|
69
|
+
EOS
|
70
|
+
end
|
71
|
+
self.send("#{sym}=", yield) if block_given?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def cattr_accessor(*syms, &blk)
|
76
|
+
cattr_reader(*syms)
|
77
|
+
cattr_writer(*syms, &blk)
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Hash
|
2
|
+
# Merges the caller into +other_hash+. For example,
|
3
|
+
#
|
4
|
+
# options = options.reverse_merge(:size => 25, :velocity => 10)
|
5
|
+
#
|
6
|
+
# is equivalent to
|
7
|
+
#
|
8
|
+
# options = {:size => 25, :velocity => 10}.merge(options)
|
9
|
+
#
|
10
|
+
# This is particularly useful for initializing an options hash
|
11
|
+
# with default values.
|
12
|
+
def reverse_merge(other_hash)
|
13
|
+
other_hash.merge(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Destructive +reverse_merge+.
|
17
|
+
def reverse_merge!(other_hash)
|
18
|
+
# right wins if there is no left
|
19
|
+
merge!(other_hash) { |key, left, right| left }
|
20
|
+
end
|
21
|
+
|
22
|
+
alias_method :reverse_update, :reverse_merge!
|
23
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module RedisArray
|
2
|
+
class RedisHashGroup
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize options={}
|
6
|
+
options.reverse_merge!(:existing_group_name => nil)
|
7
|
+
if options[:prefix]
|
8
|
+
@name = options[:prefix] + "-" + rand(100000).to_s
|
9
|
+
while not RedisTable.get_redis.keys("#{@name}:*").empty? do
|
10
|
+
@name = options[:prefix] + "-" + rand(100000).to_s
|
11
|
+
end
|
12
|
+
elsif options[:existing_group_name].nil?
|
13
|
+
@name = rand(100000).to_s
|
14
|
+
while not RedisTable.get_redis.keys("#{@name}:*").empty? do
|
15
|
+
@name = rand(100000).to_s
|
16
|
+
end
|
17
|
+
else
|
18
|
+
@name = options[:existing_group_name].to_s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'csv'
|
3
|
+
|
4
|
+
module RedisArray
|
5
|
+
class RedisTable
|
6
|
+
cattr_accessor :logger
|
7
|
+
@silence = false
|
8
|
+
|
9
|
+
def self.silence?
|
10
|
+
@silence
|
11
|
+
end
|
12
|
+
|
13
|
+
# Silence the logger.
|
14
|
+
def self.silence!
|
15
|
+
@silence = true
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
@redis = Redis.new(:host => 'localhost', :port => 6379)
|
20
|
+
|
21
|
+
def self.get_redis
|
22
|
+
@redis
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.set_redis redis
|
26
|
+
@redis = redis
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.get_table_names main_key_part
|
30
|
+
res = []
|
31
|
+
@redis.keys("#{main_key_part}:*").each do |key|
|
32
|
+
res << key.scan(/^#{Regexp.escape(main_key_part)}\:(.+):\d+/)[0][0]
|
33
|
+
end
|
34
|
+
res
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.get_row_ranges main_key_part, table_name
|
38
|
+
row_ranges = []
|
39
|
+
@redis.keys("#{main_key_part}:#{table_name}:*").each do |key|
|
40
|
+
row_ranges << key.scan(/^#{Regexp.escape(main_key_part)}\:#{Regexp.escape(table_name)}\:(\d+)/)[0][0]
|
41
|
+
end
|
42
|
+
row_ranges
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parameters:
|
46
|
+
# +rows+ - specifies a range of rows to return,
|
47
|
+
# it is header row agnostic in meaning that its up to you
|
48
|
+
# to track if you supplied a header or not
|
49
|
+
# As to the place of the header:
|
50
|
+
# if you supplied headers in +header_row+ parameter for +set_table+
|
51
|
+
# then it is stored at index 0 and you can retrieve it usign +get_table_data+
|
52
|
+
# in two ways:
|
53
|
+
# 1. specify +rows+ with value of :all
|
54
|
+
# 2. specify +rows+ with value of 0
|
55
|
+
def self.get_table_data main_key_part, table_name, rows = 0..-1
|
56
|
+
table = {}
|
57
|
+
if rows == (0..-1)
|
58
|
+
get_row_ranges(main_key_part, table_name).each do |kri|
|
59
|
+
@redis.hgetall("#{main_key_part}:#{table_name}:#{kri}").each_pair do |k, v|
|
60
|
+
table[(kri.to_i*100) + k.to_i] = v
|
61
|
+
end
|
62
|
+
end
|
63
|
+
elsif rows.kind_of? Integer
|
64
|
+
get_row_ranges(main_key_part, table_name).each do |kri|
|
65
|
+
next if kri.to_i != rows / 100
|
66
|
+
table[(kri.to_i*100) + rows.to_i] = @redis.hget("#{main_key_part}:#{table_name}:#{kri}", rows)
|
67
|
+
end
|
68
|
+
elsif rows.kind_of? Range
|
69
|
+
# TODO: handle case where rows is range like 15..20
|
70
|
+
raise "TODO: handle case where rows is range like 15..20"
|
71
|
+
else
|
72
|
+
raise "Unsupported type of 'rows' parameter!"
|
73
|
+
end
|
74
|
+
|
75
|
+
table
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.get_table_data_as_array main_key_part, table_name, rows = 0..-1
|
79
|
+
hash = get_table_data main_key_part, table_name, rows
|
80
|
+
res = hash.inject([]) do |array, (k, v)|
|
81
|
+
array[k] = CSV.parse(v+"\r\n"+v)[0].collect { |cell| cell.to_s } #if not cell.nil?}
|
82
|
+
array
|
83
|
+
end
|
84
|
+
if rows.kind_of? Integer
|
85
|
+
res.compact
|
86
|
+
else
|
87
|
+
res
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.append_table_data main_key_part, table_name, rows, options={}
|
92
|
+
|
93
|
+
kri = get_row_ranges(main_key_part, table_name).sort[-1].to_i
|
94
|
+
res = @redis.hgetall("#{main_key_part}:#{table_name}:#{kri}").sort do |p1, p2|
|
95
|
+
p1[0].to_i <=> p2[0].to_i
|
96
|
+
end
|
97
|
+
first_empty_ri = 0 # the nearest (to the beginning) empty row index
|
98
|
+
if res.length > 0
|
99
|
+
first_empty_ri = res[-1][0].to_i + 1
|
100
|
+
else
|
101
|
+
# it is empty so start from the beginning
|
102
|
+
end
|
103
|
+
|
104
|
+
start_from_row = kri.to_i * 100 + first_empty_ri
|
105
|
+
if start_from_row == 0
|
106
|
+
set_table_data main_key_part, table_name, rows
|
107
|
+
else
|
108
|
+
set_table_data main_key_part, table_name, rows, options={:start_from_row => start_from_row}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# options:
|
113
|
+
# :start_from_row - i.e. the output index or in other words an offset of the output data
|
114
|
+
# it cannot be greater than 99
|
115
|
+
# :header_row - array of strings - if specified they will be written at index 0 and the data rows will start at index 1
|
116
|
+
# :skip_headers - boolean - if specified the :header_row will be skipped and the data rows will start at index 0
|
117
|
+
# (unless the :start_from_row is specified at the same time that would overwrite this option,
|
118
|
+
# that is when :start_from_row is specified the :skip_headers will not cause shifting the data)
|
119
|
+
def self.set_table_data main_key_part, table_name, rows, options={}
|
120
|
+
if options[:skip_headers]
|
121
|
+
options.reverse_merge!(:start_from_row => 1)
|
122
|
+
else
|
123
|
+
options.reverse_merge!(:start_from_row => 0)
|
124
|
+
end
|
125
|
+
new_rows = Marshal::load(Marshal.dump(rows))
|
126
|
+
new_rows.unshift(options[:header_row]) if options[:header_row] and not options[:skip_headers]
|
127
|
+
source_row_index = 0 # the index in the source array of data (i.e. the one we are copying data from to put them to redis)
|
128
|
+
(0).upto((new_rows.count + options[:start_from_row]) / 100) do |kri|
|
129
|
+
next if kri < options[:start_from_row] / 100
|
130
|
+
(options[:start_from_row] / 100 == kri ? options[:start_from_row] % 100 : 0).upto(99).each do |ri|
|
131
|
+
break if source_row_index >= new_rows.count #+ options[:start_from_row]
|
132
|
+
curr_key = "#{main_key_part}:#{table_name}:#{kri}", ri # + (kri==0 ? options[:start_from_row] : 0)
|
133
|
+
log("HGET", "#{curr_key.to_s} == #{@redis.hget(*curr_key).to_s}")
|
134
|
+
log("HSET", "#{curr_key.to_s} == #{CSV.generate_line(new_rows[source_row_index]).to_s}")
|
135
|
+
if RUBY_VERSION < '1.9'
|
136
|
+
@redis.hset(*(curr_key << CSV.generate_line(new_rows[source_row_index])))
|
137
|
+
else
|
138
|
+
@redis.hset(*(curr_key << CSV.generate_line(new_rows[source_row_index], :row_sep => '')))
|
139
|
+
end
|
140
|
+
source_row_index += 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.delete_all_table main_key_part, table_name
|
146
|
+
self.get_row_ranges(main_key_part, table_name).each do |kri|
|
147
|
+
curr_key = "#{main_key_part}:#{table_name}:#{kri}"
|
148
|
+
@redis.del(curr_key)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.delete_table_data main_key_part, table_name, options={}
|
153
|
+
if options[:skip_headers]
|
154
|
+
options.reverse_merge!(:start_from_row => 1)
|
155
|
+
else
|
156
|
+
options.reverse_merge!(:start_from_row => 0)
|
157
|
+
end
|
158
|
+
|
159
|
+
# TODO: start_from_row is not handled yet
|
160
|
+
|
161
|
+
if options[:skip_headers]
|
162
|
+
header = self.get_table_data main_key_part, table_name, rows = 0
|
163
|
+
self.delete_all_table main_key_part, table_name
|
164
|
+
self.set_table_data main_key_part, table_name, [], :header_row => header
|
165
|
+
else
|
166
|
+
self.delete_all_table main_key_part, table_name
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def set_rows table_name, rows
|
171
|
+
rows.each_with_index do |row, idx|
|
172
|
+
@redis.zadd("matrix.#{table_name}.rows", idx, row)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def append_row table_name, row
|
177
|
+
@redis.zadd("matrix.#{table_name}.rows", idx, row)
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
def self.log(operation, message)
|
182
|
+
return unless logger && !silence?
|
183
|
+
logger.debug("RedisTable: #{operation}: #{message}")
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|