redcord 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,8 +12,8 @@ The number of keys deleted from Redis
12
12
  -- The arguments can be accessed by Lua using the KEYS global variable in the
13
13
  -- form of a one-based array (so KEYS[1], KEYS[2], ...).
14
14
  --
15
- -- KEYS[1] = Model.name
16
- -- KEYS[2] = id
15
+ -- KEYS = id, hash_tag
16
+ -- ARGV = Model.name index_attr_size range_index_attr_size [index_attr_key ...] [range_index_attr_key ...] [custom_index_name ...]
17
17
  <%= include_lua 'shared/index_helper_methods' %>
18
18
 
19
19
  -- Validate input to script before making Redis db calls
@@ -21,28 +21,37 @@ if #KEYS ~= 2 then
21
21
  error('Expected keys of be of size 2')
22
22
  end
23
23
 
24
- local model = KEYS[1]
25
- local id = KEYS[2]
24
+ local model = ARGV[1]
25
+ local id, hash_tag = unpack(KEYS)
26
26
 
27
27
  -- key = "#{model}:id:{id}"
28
28
  local key = model .. ':id:' .. id
29
29
 
30
+ local index_attr_pos = 4
31
+ local range_attr_pos = index_attr_pos + ARGV[2]
32
+ local custom_index_pos = range_attr_pos + ARGV[3]
33
+
30
34
  -- Clean up id sets for both index and range index attributes
31
- local index_attr_keys = redis.call('smembers', model .. ':index_attrs')
35
+ local index_attr_keys = {unpack(ARGV, index_attr_pos, range_attr_pos - 1)}
32
36
  if #index_attr_keys > 0 then
33
37
  -- Retrieve old index attr values so we can delete them in the attribute id sets
34
38
  local attr_vals = redis.call('hmget', key, unpack(index_attr_keys))
35
39
  for i=1, #index_attr_keys do
36
- delete_id_from_index_attr(model, index_attr_keys[i], attr_vals[i], id)
40
+ delete_id_from_index_attr(hash_tag, model, index_attr_keys[i], attr_vals[i], id)
37
41
  end
38
42
  end
39
- local range_index_attr_keys = redis.call('smembers', model .. ':range_index_attrs')
43
+ local range_index_attr_keys = {unpack(ARGV, range_attr_pos, custom_index_pos - 1)}
40
44
  if #range_index_attr_keys > 0 then
41
45
  local attr_vals = redis.call('hmget', key, unpack(range_index_attr_keys))
42
46
  for i=1, #range_index_attr_keys do
43
- delete_id_from_range_index_attr(model, range_index_attr_keys[i], attr_vals[i], id)
47
+ delete_id_from_range_index_attr(hash_tag, model, range_index_attr_keys[i], attr_vals[i], id)
44
48
  end
45
49
  end
50
+ -- Delete record from custom indexes
51
+ local custom_index_names = {unpack(ARGV, custom_index_pos)}
52
+ for _, index_name in ipairs(custom_index_names) do
53
+ delete_record_from_custom_index(hash_tag, model, index_name, id)
54
+ end
46
55
 
47
56
  -- delete the actual key
48
57
  return redis.call('del', key)
@@ -15,9 +15,11 @@ A hash of id:model of all the ids that match the query conditions given.
15
15
  -- accessed by Lua using the ARGV global variable, very similarly to what
16
16
  -- happens with keys (so ARGV[1], ARGV[2], ...).
17
17
  --
18
- -- KEYS[1] = Model.name attr_key attr_val [attr_key attr_val ..]
19
- -- ARGV[1...N] = attr_key [attr_key ..]
20
- --
18
+ -- KEYS[1] = hash_tag
19
+ -- ARGV = Model.name custom_index_name num_index_attr num_range_index_attr num_custom_index_attr num_query_conditions
20
+ -- [index_attrs ...] [range_index_attrs ...] [custom_index_attrs ...] [query_conidtions ...] [attr_selections ...]
21
+ -- [query_conidtions ...]: [attr_key1 attr_val1 attr_key2 attr_val2 ...]
22
+ -- [attr_selections ...]: [attr_key1 attr_key2 ...]
21
23
  -- For equality query conditions, key value pairs are expected to appear in
22
24
  -- the KEYS array as [attr_key, attr_val]
23
25
  -- For range query conditions, key value pairs are expected to appear in the
@@ -29,27 +31,59 @@ A hash of id:model of all the ids that match the query conditions given.
29
31
  <%= include_lua 'shared/lua_helper_methods' %>
30
32
  <%= include_lua 'shared/query_helper_methods' %>
31
33
 
32
- if #KEYS < 3 then
33
- error('Expected keys to be at least of size 3')
34
+ if #KEYS ~=1 then
35
+ error('Expected keys to be of size 1')
34
36
  end
35
37
 
36
- local model = KEYS[1]
37
- local index_sets, range_index_sets = unpack(validate_and_parse_query_conditions(model, KEYS))
38
+ local model = ARGV[1]
39
+
40
+ local index_name = ARGV[2]
41
+ local index_attr_pos = 7
42
+ local range_attr_pos = index_attr_pos + ARGV[3]
43
+ local custom_attr_pos = range_attr_pos + ARGV[4]
44
+ local query_cond_pos = custom_attr_pos + ARGV[5]
45
+ local attr_selection_pos = query_cond_pos + ARGV[6]
38
46
 
39
47
  -- Get all ids which have the corresponding attribute values.
40
48
  local ids_set = nil
41
- -- For normal sets, Redis has SINTER built in to return the set intersection
42
- if #index_sets > 0 then
43
- ids_set = to_set(redis.call('sinter', unpack(index_sets)))
44
- end
45
- -- For sorted sets, call helper function zinter_zrangebyscore, which calls
46
- -- ZRANGEBYSCORE for each {redis_key, min, max} tuple and returns the set intersection
47
- if #range_index_sets > 0 then
48
- ids_set = intersect_range_index_sets(ids_set, range_index_sets)
49
+
50
+ -- If custom index name is empty -> use single-attribute indices
51
+ if index_name == '' then
52
+ local index_sets, range_index_sets = unpack(validate_and_parse_query_conditions(
53
+ KEYS[1],
54
+ model,
55
+ to_set({unpack(ARGV, index_attr_pos, range_attr_pos - 1)}),
56
+ to_set({unpack(ARGV, range_attr_pos, custom_attr_pos - 1)}),
57
+ unpack(ARGV, query_cond_pos, attr_selection_pos - 1)
58
+ ))
59
+
60
+ -- For normal sets, Redis has SINTER built in to return the set intersection
61
+ if #index_sets > 0 then
62
+ ids_set = to_set(redis.call('sinter', unpack(index_sets)))
63
+ end
64
+ -- For sorted sets, call helper function zinter_zrangebyscore, which calls
65
+ -- ZRANGEBYSCORE for each {redis_key, min, max} tuple and returns the set intersection
66
+ if #range_index_sets > 0 then
67
+ ids_set = intersect_range_index_sets(ids_set, range_index_sets)
68
+ end
69
+ else
70
+ local custom_index_attrs = {unpack(ARGV, custom_attr_pos, query_cond_pos - 1)}
71
+ local custom_index_query = validate_and_parse_query_conditions_custom(
72
+ KEYS[1],
73
+ model,
74
+ index_name,
75
+ custom_index_attrs,
76
+ {unpack(ARGV, query_cond_pos, attr_selection_pos - 1)}
77
+ )
78
+ if #custom_index_query > 0 then
79
+ ids_set = get_id_set_from_custom_index(ids_set, custom_index_query)
80
+ else
81
+ ids_set = {}
82
+ end
49
83
  end
50
84
 
51
85
  -- Query for the hashes for all ids in the set intersection
52
- local res, stale_ids = unpack(batch_hget(model, ids_set, ARGV))
86
+ local res, stale_ids = unpack(batch_hget(model, ids_set, unpack(ARGV, attr_selection_pos)))
53
87
 
54
88
  -- Delete any stale ids which are no longer in redis from the id sets.
55
89
  -- This can happen if an entry was auto expired due to ttl, but not removed up yet
@@ -15,7 +15,9 @@ An integer number of records that match the query conditions given.
15
15
  -- accessed by Lua using the ARGV global variable, very similarly to what
16
16
  -- happens with keys (so ARGV[1], ARGV[2], ...).
17
17
  --
18
- -- KEYS[1] = Model.name attr_key attr_val [attr_key attr_val ..]
18
+ -- KEYS[1] = hash_tag
19
+ -- ARGV = Model.name custom_index_name num_index_attr num_range_index_attr num_custom_index_attr
20
+ -- [index_attrs ...] [range_index_attrs ...] [custom_index_attrs ...] [query_conidtions ...]
19
21
  --
20
22
  -- For equality query conditions, key value pairs are expected to appear in
21
23
  -- the KEYS array as [attr_key, attr_val]
@@ -25,25 +27,54 @@ An integer number of records that match the query conditions given.
25
27
  <%= include_lua 'shared/lua_helper_methods' %>
26
28
  <%= include_lua 'shared/query_helper_methods' %>
27
29
 
28
- if #KEYS < 3 then
29
- error('Expected keys to be at least of size 3')
30
+ if #KEYS ~=1 then
31
+ error('Expected keys to be of size 1')
30
32
  end
31
33
 
32
- local model = KEYS[1]
33
- local index_sets, range_index_sets = unpack(validate_and_parse_query_conditions(model, KEYS))
34
+ local model = ARGV[1]
35
+
36
+ local index_name = ARGV[2]
37
+ local index_attr_pos = 6
38
+ local range_attr_pos = index_attr_pos + ARGV[3]
39
+ local custom_attr_pos = range_attr_pos + ARGV[4]
40
+ local query_cond_pos = custom_attr_pos + ARGV[5]
34
41
 
35
42
  -- Get all ids which have the corresponding attribute values.
36
43
  local ids_set = nil
37
- -- For normal sets, Redis has SINTER built in to return the set intersection
38
- if #index_sets > 0 then
39
- ids_set = to_set(redis.call('sinter', unpack(index_sets)))
40
- end
41
- -- For sorted sets, call helper function zinter_zrangebyscore, which calls
42
- -- ZRANGEBYSCORE for each {redis_key, min, max} tuple and returns the set intersection
43
- if #range_index_sets > 0 then
44
- ids_set = intersect_range_index_sets(ids_set, range_index_sets)
45
- end
46
44
 
45
+ if index_name == '' then
46
+ local index_sets, range_index_sets = unpack(validate_and_parse_query_conditions(
47
+ KEYS[1],
48
+ model,
49
+ to_set({unpack(ARGV, index_attr_pos, range_attr_pos - 1)}),
50
+ to_set({unpack(ARGV, range_attr_pos, custom_attr_pos - 1)}),
51
+ unpack(ARGV, query_cond_pos)
52
+ ))
53
+
54
+ -- For normal sets, Redis has SINTER built in to return the set intersection
55
+ if #index_sets > 0 then
56
+ ids_set = to_set(redis.call('sinter', unpack(index_sets)))
57
+ end
58
+ -- For sorted sets, call helper function zinter_zrangebyscore, which calls
59
+ -- ZRANGEBYSCORE for each {redis_key, min, max} tuple and returns the set intersection
60
+ if #range_index_sets > 0 then
61
+ ids_set = intersect_range_index_sets(ids_set, range_index_sets)
62
+ end
63
+ else
64
+ local custom_index_attrs = {unpack(ARGV, custom_attr_pos, query_cond_pos - 1)}
65
+ local custom_index_query = validate_and_parse_query_conditions_custom(
66
+ KEYS[1],
67
+ model,
68
+ index_name,
69
+ custom_index_attrs,
70
+ {unpack(ARGV, query_cond_pos)}
71
+ )
72
+ if #custom_index_query > 0 then
73
+ ids_set = get_id_set_from_custom_index(ids_set, custom_index_query)
74
+ else
75
+ ids_set = {}
76
+ end
77
+ end
47
78
  -- Get the number of records which satisfy the query conditions.
48
79
  -- We do not delete stale ids as part of this function call because we do not have
49
80
  -- the list of ids which don't exist. The Redis command EXISTS key [key ...] is an O(1)
@@ -1,61 +1,90 @@
1
1
  -- Add an id to the id set of the index attribute
2
- local function add_id_to_index_attr(model, attr_key, attr_val, id)
2
+ local function add_id_to_index_attr(hash_tag, model, attr_key, attr_val, id)
3
3
  if attr_val then
4
4
  -- Call the Redis command: SADD "#{Model.name}:#{attr_name}:#{attr_val}" member ..
5
- redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val, id)
5
+ redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
6
6
  end
7
7
  end
8
8
 
9
9
  -- Remove an id from the id set of the index attribute
10
- local function delete_id_from_index_attr(model, attr_key, attr_val, id)
10
+ local function delete_id_from_index_attr(hash_tag, model, attr_key, attr_val, id)
11
11
  if attr_val then
12
12
  -- Call the Redis command: SREM "#{Model.name}:#{attr_name}:#{attr_val}" member ..
13
- redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val, id)
13
+ redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
14
14
  end
15
15
  end
16
16
 
17
17
  -- Move an id from one id set to another for the index attribute
18
- local function replace_id_in_index_attr(model, attr_key, prev_attr_val,curr_attr_val, id)
18
+ local function replace_id_in_index_attr(hash_tag, model, attr_key, prev_attr_val,curr_attr_val, id)
19
19
  -- If previous and new value differs, then modify the id sets accordingly
20
20
  if prev_attr_val ~= curr_attr_val then
21
- delete_id_from_index_attr(model, attr_key, prev_attr_val, id)
22
- add_id_to_index_attr(model, attr_key, curr_attr_val, id)
21
+ delete_id_from_index_attr(hash_tag, model, attr_key, prev_attr_val, id)
22
+ add_id_to_index_attr(hash_tag, model, attr_key, curr_attr_val, id)
23
23
  end
24
24
  end
25
25
 
26
26
  -- Add an id to the sorted id set of the range index attribute
27
- local function add_id_to_range_index_attr(model, attr_key, attr_val, id)
27
+ local function add_id_to_range_index_attr(hash_tag, model, attr_key, attr_val, id)
28
28
  if attr_val then
29
29
  -- Nil values of range indices are sent to Redis as an empty string. They are stored
30
30
  -- as a regular set at key "#{Model.name}:#{attr_name}:"
31
31
  if attr_val == "" then
32
- redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val, id)
32
+ redis.call('sadd', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
33
33
  else
34
34
  -- Call the Redis command: ZADD "#{Model.name}:#{attr_name}" #{attr_val} member ..,
35
35
  -- where attr_val is the score of the sorted set
36
- redis.call('zadd', model .. ':' .. attr_key, attr_val, id)
36
+ redis.call('zadd', model .. ':' .. attr_key .. hash_tag, attr_val, id)
37
37
  end
38
38
  end
39
39
  end
40
40
 
41
41
  -- Remove an id from the sorted id set of the range index attribute
42
- local function delete_id_from_range_index_attr(model, attr_key, attr_val, id)
42
+ local function delete_id_from_range_index_attr(hash_tag, model, attr_key, attr_val, id)
43
43
  if attr_val then
44
44
  -- Nil values of range indices are sent to Redis as an empty string. They are stored
45
45
  -- as a regular set at key "#{Model.name}:#{attr_name}:"
46
46
  if attr_val == "" then
47
- redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val, id)
47
+ redis.call('srem', model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag, id)
48
48
  else
49
49
  -- Call the Redis command: ZREM "#{Model.name}:#{attr_name}:#{attr_val}" member ..
50
- redis.call('zrem', model .. ':' .. attr_key, id)
50
+ redis.call('zrem', model .. ':' .. attr_key .. hash_tag, id)
51
51
  end
52
52
  end
53
53
  end
54
54
 
55
55
  -- Move an id from one sorted id set to another for the range index attribute
56
- local function replace_id_in_range_index_attr(model, attr_key, prev_attr_val, curr_attr_val, id)
56
+ local function replace_id_in_range_index_attr(hash_tag, model, attr_key, prev_attr_val, curr_attr_val, id)
57
57
  if prev_attr_val ~= curr_attr_val then
58
- delete_id_from_range_index_attr(model, attr_key, prev_attr_val, id)
59
- add_id_to_range_index_attr(model, attr_key, curr_attr_val, id)
58
+ delete_id_from_range_index_attr(hash_tag, model, attr_key, prev_attr_val, id)
59
+ add_id_to_range_index_attr(hash_tag, model, attr_key, curr_attr_val, id)
60
+ end
61
+ end
62
+
63
+ -- Add an index record to the sorted set of the custom index
64
+ local function add_record_to_custom_index(hash_tag, model, index_name, attr_values, id)
65
+ local sep = ':'
66
+ if attr_values then
67
+ local index_string = ''
68
+ local attr_value_string = ''
69
+ for i, attr_value in ipairs(attr_values) do
70
+ if i > 1 then
71
+ index_string = index_string .. sep
72
+ end
73
+ attr_value_string = adjust_string_length(attr_value)
74
+ index_string = index_string .. attr_value_string
75
+ end
76
+ redis.call('zadd', model .. sep .. 'custom_index' .. sep .. index_name .. hash_tag, 0, index_string .. sep .. id)
77
+ redis.call('hset', model .. sep .. 'custom_index' .. sep .. index_name .. '_content' .. hash_tag, id, index_string .. sep .. id)
78
+ end
79
+ end
80
+
81
+ -- Remove a record from the sorted set of the custom index
82
+ local function delete_record_from_custom_index(hash_tag, model, index_name, id)
83
+ local sep = ':'
84
+ local index_key = model .. sep .. 'custom_index' .. sep .. index_name
85
+ local index_string = redis.call('hget', index_key .. '_content' .. hash_tag, id)
86
+ if index_string then
87
+ redis.call('zremrangebylex', index_key .. hash_tag, '[' .. index_string, '[' .. index_string)
88
+ redis.call('hdel', index_key .. '_content' .. hash_tag, id)
60
89
  end
61
90
  end
@@ -1,9 +1,8 @@
1
1
  -- Helper function to convert argument array to hash set
2
- local function to_hash(list)
2
+ local function to_hash(...)
3
3
  local hash = {}
4
- if not list then return hash end
5
- for i=1, #list, 2 do
6
- hash[list[i]] = list[i+1]
4
+ for i=1, #arg, 2 do
5
+ hash[arg[i]] = arg[i+1]
7
6
  end
8
7
  return hash
9
8
  end
@@ -31,3 +30,20 @@ local function set_list_intersect(set, list)
31
30
  end
32
31
  return set_intersect
33
32
  end
33
+
34
+ -- Helper function to transform attribute values so that they become comparable as strings
35
+ local function adjust_string_length(value)
36
+ if value == '' or value == nil then
37
+ return '!'
38
+ end
39
+ if string.sub(value, 1, 1) == '-' then
40
+ error("Custom index currently doesn't support negative values")
41
+ end
42
+ local whole_digits_count = 19
43
+ local res = string.rep('0', whole_digits_count - string.len(value)) .. value
44
+ if string.len(res) > whole_digits_count then
45
+ error("Custom index can't be used if string representation of whole part of attribute value is longer than " ..
46
+ whole_digits_count .. ' characters')
47
+ end
48
+ return res
49
+ end
@@ -10,18 +10,37 @@ local function intersect_range_index_sets(set, tuples)
10
10
  return set
11
11
  end
12
12
 
13
+ -- Runs a query against a sorted set, extracts ids.
14
+ -- Response from redis: attr_value:[attr_value ...]:id
15
+ -- Returns a set of ids.
16
+ local function get_id_set_from_custom_index(set, query)
17
+ local ids = {}
18
+ local index_strings = {}
19
+ local sep = ':'
20
+ local id = ''
21
+ local key, min, max = unpack(query)
22
+ index_strings = redis.call('zrangebylex', key, min, max)
23
+ for _, index_string in ipairs(index_strings) do
24
+ id = string.match(index_string, '[^' .. sep .. ']+$')
25
+ table.insert(ids, id)
26
+ end
27
+ set = to_set(ids)
28
+
29
+ return set
30
+ end
31
+
13
32
  -- Gets the hash of all the ids given. Returns the results in a
14
33
  -- table, as well as any ids not found in Redis as a separate table
15
- local function batch_hget(model, ids_set, fields)
34
+ local function batch_hget(model, ids_set, ...)
16
35
  local res, stale_ids = {}, {}
17
36
  for id, _ in pairs(ids_set) do
18
37
  local instance = nil
19
- if fields and #fields > 0 then
20
- local values = redis.call('hmget', model .. ':id:' .. id, unpack(fields))
38
+ if #{...}> 0 then
39
+ local values = redis.call('hmget', model .. ':id:' .. id, ...)
21
40
  -- HMGET returns the value in the order of the fields given. Map back to
22
41
  -- field value [field value ..]
23
42
  instance = {}
24
- for i, field in ipairs(fields) do
43
+ for i, field in ipairs({...}) do
25
44
  if not values[i] then
26
45
  instance = nil
27
46
  break
@@ -76,30 +95,28 @@ end
76
95
  -- attributes. Parse query conditions into two separate tables:
77
96
  -- 1. index_sets formatted as the id set keys in Redis '#{Model.name}:#{attr_key}:#{attr_val}'
78
97
  -- 2. range_index_sets formatted as a tuple {id set key, min, max} => { '#{Model.name}:#{attr_key}' min max }
79
- local function validate_and_parse_query_conditions(model, args)
80
- local index_attrs = to_set(redis.call('smembers', model .. ':index_attrs'))
81
- local range_index_attrs = to_set(redis.call('smembers', model .. ':range_index_attrs'))
98
+ local function validate_and_parse_query_conditions(hash_tag, model, index_attrs, range_index_attrs, ...)
82
99
  -- Iterate through the arguments of the script to form the redis keys at which the
83
100
  -- indexed id sets are stored.
84
101
  local index_sets, range_index_sets = {}, {}
85
- local i = 2
86
- while i <= #args do
87
- local attr_key, attr_val = args[i], args[i+1]
102
+ local i = 1
103
+ while i <= #arg do
104
+ local attr_key, attr_val = arg[i], arg[i+1]
88
105
  if index_attrs[attr_key] then
89
106
  validate_attr_vals(attr_key, {attr_val})
90
107
  -- For normal index attributes, keys are stored at "#{Model.name}:#{attr_key}:#{attr_val}"
91
- table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val)
108
+ table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag)
92
109
  i = i + 2
93
110
  elseif range_index_attrs[attr_key] then
94
111
  -- For range attributes, nil values are stored as normal sets
95
112
  if attr_val == "" then
96
- table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val)
113
+ table.insert(index_sets, model .. ':' .. attr_key .. ':' .. attr_val .. hash_tag)
97
114
  i = i + 2
98
115
  else
99
- local min, max = args[i+1], args[i+2]
116
+ local min, max = arg[i+1], arg[i+2]
100
117
  validate_attr_vals(attr_key, {min, max})
101
118
  -- For range index attributes, they are stored at "#{Model.name}:#{attr_key}"
102
- table.insert(range_index_sets, {model .. ':' .. attr_key, min, max})
119
+ table.insert(range_index_sets, {model .. ':' .. attr_key .. hash_tag, min, max})
103
120
  i = i + 3
104
121
  end
105
122
  else
@@ -108,3 +125,53 @@ local function validate_and_parse_query_conditions(model, args)
108
125
  end
109
126
  return {index_sets, range_index_sets}
110
127
  end
128
+
129
+ -- Validates that attributes in query are in correct order and range condition is applied only on the last attribute.
130
+ -- '~' is used as a character that is lexicographically greater than any alphanumerical. '[' makes range inclusive (exclusive are not yet supported)
131
+ -- Returns a table {index_key, min_string, max_string} to be used for index query.
132
+ local function validate_and_parse_query_conditions_custom(hash_tag, model, index_name, custom_index_attrs, args)
133
+ if #custom_index_attrs == 0 then
134
+ error('Index ' .. index_name .. ' does not exist')
135
+ end
136
+ local sep = ':'
137
+ local i = 1
138
+ local j = 1
139
+ local min, value_string_min, query_string_min = '', '', ''
140
+ local max, value_string_max, query_string_max = '', '', ''
141
+ local is_prev_attr_query_range = false
142
+ while i <= #args do
143
+ if is_prev_attr_query_range then
144
+ error('Range can be applied to the last attribute of query only')
145
+ end
146
+ local attr_key = args[i]
147
+ if custom_index_attrs[j] == attr_key then
148
+ min, max = args[i+1], args[i+2]
149
+ if j > 1 then
150
+ query_string_min = query_string_min .. sep
151
+ query_string_max = query_string_max .. sep
152
+ else
153
+ query_string_min = query_string_min .. '['
154
+ query_string_max = query_string_max .. '['
155
+ end
156
+ if min ~= '-inf' then
157
+ value_string_min = adjust_string_length(min)
158
+ query_string_min = query_string_min .. value_string_min
159
+ end
160
+ if max ~= '+inf' then
161
+ value_string_max = adjust_string_length(max)
162
+ query_string_max = query_string_max .. value_string_max
163
+ else
164
+ query_string_max = query_string_max .. '~'
165
+ end
166
+ if min ~= max then
167
+ is_prev_attr_query_range = true
168
+ end
169
+ j = j + 1
170
+ i = i + 3
171
+ else
172
+ error(attr_key .. ' in position ' .. j .. ' is not supported by index ' .. index_name)
173
+ end
174
+ end
175
+ query_string_max = query_string_max .. sep .. '~'
176
+ return {model .. sep .. 'custom_index' .. sep .. index_name .. hash_tag, query_string_min, query_string_max}
177
+ end