1 | # frozen_string_literal: true |
||
2 | |||
3 | 1 | module NoSE |
|
4 | 1 | module Backend |
|
5 | # Simple backend which persists data to a file |
||
6 | 1 | class FileBackend < Backend |
|
7 | 1 | include Subtype |
|
8 | |||
9 | 1 | def initialize(model, indexes, plans, update_plans, config) |
|
10 | 8 | super |
|
11 | |||
12 | # Try to load data from file or start fresh |
||
13 | 8 | @index_data = if !config[:file].nil? && File.file?(config[:file]) |
|
14 | Marshal.load File.open(config[:file]) |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
15 | else |
||
16 | 8 | {} |
|
17 | end |
||
18 | |||
19 | # Ensure the data is saved when we exit |
||
20 | 8 | ObjectSpace.define_finalizer self, self.class.finalize(@index_data, |
|
21 | config[:file]) |
||
22 | end |
||
23 | |||
24 | # Save data when the object is destroyed |
||
25 | 1 | def self.finalize(index_data, file) |
|
26 | 8 | proc do |
|
27 | 8 | if !file.nil? |
|
0 ignored issues
–
show
|
|||
28 | Marshal.dump(index_data, File.open(file, 'w')) |
||
29 | end |
||
30 | end |
||
31 | end |
||
32 | 1 | ||
33 | # Check for an empty array for the data |
||
34 | def index_empty?(index) |
||
35 | !index_exists?(index) || @index_data[index.key].empty? |
||
36 | end |
||
37 | 1 | ||
38 | # Check if we have prepared space for this index |
||
39 | def index_exists?(index) |
||
40 | @index_data.key? index.key |
||
41 | end |
||
42 | 1 | ||
43 | # @abstract Subclasses implement to allow inserting |
||
44 | def index_insert_chunk(index, chunk) |
||
45 | @index_data[index.key].concat chunk |
||
46 | end |
||
47 | 1 | ||
48 | # Generate a simple UUID |
||
49 | def generate_id |
||
50 | SecureRandom.uuid |
||
51 | end |
||
52 | 1 | ||
53 | # Allocate space for data on the new indexes |
||
54 | def indexes_ddl(execute = false, skip_existing = false, |
||
0 ignored issues
–
show
|
|||
55 | drop_existing = false) |
||
0 ignored issues
–
show
|
|||
56 | @indexes.each do |index| |
||
0 ignored issues
–
show
|
|||
57 | # Do the appropriate behaviour based on the flags passed in |
||
58 | if index_exists?(index) |
||
59 | next if skip_existing |
||
60 | fail unless drop_existing |
||
61 | end |
||
62 | 1 | ||
63 | @index_data[index.key] = [] |
||
64 | end if execute |
||
65 | 1 | ||
66 | # We just use the original index definition as DDL |
||
67 | @indexes.map(&:inspect) |
||
68 | end |
||
69 | 1 | ||
70 | # Sample a number of values from the given index |
||
71 | def index_sample(index, count) |
||
72 | data = @index_data[index.key] |
||
73 | data.nil? ? [] : data.sample(count) |
||
74 | end |
||
75 | |||
76 | 1 | # We just produce the data here which can be manipulated as needed |
|
77 | 15 | # @return [Hash] |
|
78 | def client |
||
79 | @index_data |
||
80 | end |
||
81 | |||
82 | 1 | # Provide some helper functions which allow the matching of rows |
|
83 | # based on a set of list of conditions |
||
84 | module RowMatcher |
||
85 | 1 | # Check if a row matches the given condition |
|
86 | 12 | # @return [Boolean] |
|
87 | def row_matches?(row, conditions) |
||
88 | row_matches_eq?(row, conditions) && |
||
89 | row_matches_range?(row, conditions) |
||
90 | end |
||
91 | |||
92 | 1 | # Check if a row matches the given condition on equality predicates |
|
93 | 12 | # @return [Boolean] |
|
94 | 24 | def row_matches_eq?(row, conditions) |
|
95 | @eq_fields.all? do |field| |
||
96 | row[field.id] == conditions.find { |c| c.field == field }.value |
||
97 | end |
||
98 | end |
||
99 | |||
100 | 1 | # Check if a row matches the given condition on the range predicate |
|
101 | 9 | # @return [Boolean] |
|
102 | def row_matches_range?(row, conditions) |
||
103 | return true if @range_field.nil? |
||
104 | |||
105 | range_cond = conditions.find { |c| c.field == @range_field } |
||
106 | row[@range_field.id].send range_cond.operator, range_cond.value |
||
107 | end |
||
108 | end |
||
109 | 1 | ||
110 | 1 | # Look up data on an index in the backend |
|
111 | class IndexLookupStatementStep < Backend::IndexLookupStatementStep |
||
112 | include RowMatcher |
||
113 | 1 | ||
114 | # Filter all the rows in the specified index to those requested |
||
115 | 12 | def process(conditions, results) |
|
0 ignored issues
–
show
|
|||
116 | 12 | # Get the set of conditions we need to process |
|
117 | results = initial_results(conditions) if results.nil? |
||
118 | condition_list = result_conditions conditions, results |
||
119 | 12 | ||
120 | 12 | # Loop through all rows to find the matching ones |
|
121 | 24 | rows = @client[@index.key] || [] |
|
122 | selected = condition_list.flat_map do |condition| |
||
123 | rows.select { |row| row_matches? row, condition } |
||
124 | end.compact |
||
125 | 12 | ||
126 | 12 | # Apply the limit and only return selected fields |
|
127 | 35 | field_ids = Set.new @step.fields.map(&:id).to_set |
|
128 | selected[0..(@step.limit.nil? ? -1 : @step.limit)].map do |row| |
||
129 | row.select { |k, _| field_ids.include? k } |
||
130 | end |
||
131 | end |
||
132 | end |
||
133 | 1 | ||
134 | # Insert data into an index on the backend |
||
135 | 1 | class InsertStatementStep < Backend::InsertStatementStep |
|
136 | 3 | # Add new rows to the index |
|
137 | def process(results) |
||
0 ignored issues
–
show
|
|||
138 | 3 | key_ids = (@index.hash_fields + @index.order_fields).map(&:id).to_set |
|
139 | |||
140 | 3 | results.each do |row| |
|
141 | 7 | # Pick out primary key fields we can use to match |
|
142 | conditions = row.select do |field_id| |
||
143 | key_ids.include? field_id |
||
144 | end |
||
145 | 3 | ||
146 | # If we have all the primary keys, check for a match |
||
147 | 2 | if conditions.length == key_ids.length |
|
148 | 1 | # Try to find a row with this ID and update it |
|
149 | matching_row = @client[index.key].find do |index_row| |
||
150 | index_row.merge(conditions) == index_row |
||
151 | 2 | end |
|
152 | 1 | ||
153 | 1 | unless matching_row.nil? |
|
154 | matching_row.merge! row |
||
155 | next |
||
156 | end |
||
157 | end |
||
158 | 2 | ||
159 | 5 | # Populate IDs as needed |
|
160 | key_ids.each do |key_id| |
||
161 | row[key_id] = SecureRandom.uuid if row[key_id].nil? |
||
162 | 2 | end |
|
163 | |||
164 | @client[index.key] << row |
||
165 | end |
||
166 | end |
||
167 | end |
||
168 | 1 | ||
169 | 1 | # Delete data from an index on the backend |
|
170 | class DeleteStatementStep < Backend::DeleteStatementStep |
||
171 | include RowMatcher |
||
172 | 1 | ||
173 | # Remove rows matching the results from the dataset |
||
174 | 3 | def process(results) |
|
175 | # Loop over all rows |
||
176 | 3 | @client[index.key].reject! do |row| |
|
177 | # Check against all results |
||
178 | 3 | results.any? do |result| |
|
179 | 11 | # If all fields match, drop the row |
|
180 | result.all? do |field, value| |
||
181 | row[field] == value |
||
182 | end |
||
183 | end |
||
184 | end |
||
185 | end |
||
186 | end |
||
187 | end |
||
188 | end |
||
189 | end |
||
190 |