skydb 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/bin/sky +85 -0
  2. data/lib/ext/hash.rb +11 -0
  3. data/lib/ext/treetop.rb +19 -0
  4. data/lib/skydb.rb +10 -3
  5. data/lib/skydb/client.rb +92 -28
  6. data/lib/skydb/import.rb +7 -0
  7. data/lib/skydb/import/importer.rb +258 -0
  8. data/lib/skydb/import/transforms/sky.yml +20 -0
  9. data/lib/skydb/import/transforms/snowplow.yml +1 -0
  10. data/lib/skydb/import/translator.rb +119 -0
  11. data/lib/skydb/message.rb +17 -12
  12. data/lib/skydb/message/create_table.rb +64 -0
  13. data/lib/skydb/message/delete_table.rb +66 -0
  14. data/lib/skydb/message/get_table.rb +74 -0
  15. data/lib/skydb/message/lookup.rb +79 -0
  16. data/lib/skydb/property.rb +5 -5
  17. data/lib/skydb/query.rb +198 -0
  18. data/lib/skydb/query/after.rb +103 -0
  19. data/lib/skydb/query/ast/selection_field_syntax_node.rb +26 -0
  20. data/lib/skydb/query/ast/selection_fields_syntax_node.rb +16 -0
  21. data/lib/skydb/query/ast/selection_group_syntax_node.rb +16 -0
  22. data/lib/skydb/query/ast/selection_groups_syntax_node.rb +16 -0
  23. data/lib/skydb/query/selection.rb +268 -0
  24. data/lib/skydb/query/selection_field.rb +74 -0
  25. data/lib/skydb/query/selection_fields_grammar.treetop +46 -0
  26. data/lib/skydb/query/selection_fields_parse_error.rb +30 -0
  27. data/lib/skydb/query/selection_group.rb +57 -0
  28. data/lib/skydb/query/selection_groups_grammar.treetop +31 -0
  29. data/lib/skydb/query/selection_groups_parse_error.rb +30 -0
  30. data/lib/skydb/query/validation_error.rb +8 -0
  31. data/lib/skydb/table.rb +69 -0
  32. data/lib/skydb/version.rb +1 -1
  33. data/test/import/importer_test.rb +42 -0
  34. data/test/import/translator_test.rb +88 -0
  35. data/test/message/add_event_message_test.rb +1 -1
  36. data/test/message/add_property_message_test.rb +2 -2
  37. data/test/message/create_table_message_test.rb +34 -0
  38. data/test/message/delete_table_message_test.rb +34 -0
  39. data/test/message/get_table_message_test.rb +19 -0
  40. data/test/message/lookup_message_test.rb +27 -0
  41. data/test/message_test.rb +1 -1
  42. data/test/query/after_test.rb +71 -0
  43. data/test/query/selection_test.rb +273 -0
  44. data/test/query_test.rb +156 -0
  45. data/test/test_helper.rb +3 -0
  46. metadata +129 -3
@@ -9,11 +9,11 @@ class SkyDB
9
9
  ##########################################################################
10
10
 
11
11
  # Initializes the property.
12
- def initialize(id=0, type=nil, data_type="String", name=nil)
13
- self.id = id
14
- self.type = type
15
- self.data_type = data_type
16
- self.name = name
12
+ def initialize(options={})
13
+ self.id = options[:id]
14
+ self.type = options[:type]
15
+ self.data_type = options[:data_type]
16
+ self.name = options[:name]
17
17
  end
18
18
 
19
19
 
@@ -0,0 +1,198 @@
1
+ require 'skydb/query/selection'
2
+ require 'skydb/query/after'
3
+ require 'skydb/query/validation_error'
4
+
5
+ class SkyDB
6
+ # The Query object represents a high level abstraction of how data is
7
+ # processed and retrieved from the database. It is inspired by ActiveRecord
8
+ # in the sense that commands can be chained together.
9
+ #
10
+ # The query is not executed until the execute() method is called.
11
+ class Query
12
+ ##########################################################################
13
+ #
14
+ # Constructor
15
+ #
16
+ ##########################################################################
17
+
18
+ def initialize(options={})
19
+ self.client = options[:client]
20
+ self.selection = options[:selection] || SkyDB::Query::Selection.new()
21
+ self.conditions = options[:conditions] || []
22
+ end
23
+
24
+
25
+ ##########################################################################
26
+ #
27
+ # Attributes
28
+ #
29
+ ##########################################################################
30
+
31
+ # The client that is used for executing the query.
32
+ attr_accessor :client
33
+
34
+ # The properties that should be selected from the database.
35
+ attr_accessor :selection
36
+
37
+ # A list of conditions that must be fulfilled before selection can occur.
38
+ attr_accessor :conditions
39
+
40
+ # The number of idle seconds that separates sessions.
41
+ attr_accessor :session_idle_time
42
+
43
+
44
+ ##########################################################################
45
+ #
46
+ # Methods
47
+ #
48
+ ##########################################################################
49
+
50
+ ####################################
51
+ # Helpers
52
+ ####################################
53
+
54
+ # Adds a list of fields to the selection.
55
+ #
56
+ # @param [String] fields A list of fields to add to the selection.
57
+ #
58
+ # @return [Query] The query object is returned.
59
+ def select(*fields)
60
+ selection.select(*fields)
61
+ return self
62
+ end
63
+
64
+ # Adds one or more grouping fields to the selection of the query.
65
+ #
66
+ # @param [String] groups A list of groups to add to the selection.
67
+ #
68
+ # @return [Query] The query object is returned.
69
+ def group_by(*groups)
70
+ selection.group_by(*groups)
71
+ return self
72
+ end
73
+
74
+ # Adds an 'after' condition to the query.
75
+ #
76
+ # @param [Hash] options The options to pass to the 'after' condition.
77
+ #
78
+ # @return [Query] The query object is returned.
79
+ def after(options={})
80
+ conditions << SkyDB::Query::After.new(options)
81
+ return self
82
+ end
83
+
84
+ # Sets the session idle seconds and returns the query object.
85
+ #
86
+ # @param [Fixnum] seconds The number of idle seconds.
87
+ #
88
+ # @return [Query] The query object is returned.
89
+ def session(seconds)
90
+ self.session_idle_time = seconds
91
+ return self
92
+ end
93
+
94
+
95
+ ####################################
96
+ # Execution
97
+ ####################################
98
+
99
+ # Executes the query and returns the resulting data.
100
+ def execute
101
+ # Generate the Lua code for this query.
102
+ code = codegen()
103
+
104
+ # Send it to the server.
105
+ results = client.aggregate(code)
106
+
107
+ # Return the results.
108
+ return results
109
+ end
110
+
111
+
112
+ ####################################
113
+ # Validation
114
+ ####################################
115
+
116
+ # Validates that all the elements of the query are valid.
117
+ def validate!
118
+ selection.validate!
119
+ end
120
+
121
+
122
+ ####################################
123
+ # Codegen
124
+ ####################################
125
+
126
+ # Generates the Lua code that represents the query.
127
+ def codegen
128
+ # Lookup all actions & properties.
129
+ lookup_identifiers()
130
+
131
+ # Validate everything in query before proceeding.
132
+ validate!
133
+
134
+ # Generate selection.
135
+ code = []
136
+ code << selection.codegen_select()
137
+
138
+ # Generate condition functions.
139
+ conditions.each_with_index do |condition, index|
140
+ condition.function_name ||= "__condition#{nextseq}"
141
+ code << condition.codegen
142
+ end
143
+
144
+ # Generate the invocation of the conditions.
145
+ conditionals = conditions.map {|condition| "#{condition.function_name}(cursor, data)"}.join(' and ')
146
+ conditionals = "true" if conditions.length == 0
147
+
148
+ # Generate aggregate() function.
149
+ code << "function aggregate(cursor, data)"
150
+ code << " cursor:set_session_idle(#{session_idle_time.to_i})" if session_idle_time.to_i > 0
151
+ code << " while cursor:next_session() do"
152
+ code << " while cursor:next() do"
153
+ code << " if #{conditionals} then"
154
+ code << " select(cursor, data)"
155
+ code << " end"
156
+ code << " end"
157
+ code << " end"
158
+ code << "end"
159
+ code << ""
160
+
161
+ # Generate merge function.
162
+ code << selection.codegen_merge()
163
+
164
+ return code.join("\n")
165
+ end
166
+
167
+
168
+ ####################################
169
+ # Utility
170
+ ####################################
171
+
172
+ private
173
+
174
+ # Generates a sequence number used for uniquely naming objects and
175
+ # functions in the query.
176
+ def nextseq
177
+ @sequence = (@sequence || 0) + 1
178
+ end
179
+
180
+ # Looks up all actions and properties that are missing an identifier.
181
+ def lookup_identifiers
182
+ # Find all the actions on conditions that are missing an id.
183
+ actions = []
184
+ conditions.each do |condition|
185
+ if condition.action.is_a?(SkyDB::Action) && condition.action.id.to_i == 0
186
+ actions << condition.action
187
+ end
188
+ end
189
+
190
+ # Lookup all the actions.
191
+ if actions.length > 0
192
+ client.lookup(:actions => actions)
193
+ end
194
+
195
+ return nil
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,103 @@
1
+ class SkyDB
2
+ class Query
3
+ # The 'after' condition filters out selection only after the condition
4
+ # has been fulfilled.
5
+ class After
6
+ ##########################################################################
7
+ #
8
+ # Constructor
9
+ #
10
+ ##########################################################################
11
+
12
+ def initialize(action=nil, options={})
13
+ options.merge!(action.is_a?(Hash) ? action : {:action => action})
14
+ self.action = options[:action]
15
+ self.function_name = options[:function_name]
16
+ end
17
+
18
+
19
+ ##########################################################################
20
+ #
21
+ # Attributes
22
+ #
23
+ ##########################################################################
24
+
25
+ # The function name to use when generating the code.
26
+ attr_accessor :function_name
27
+
28
+ # The action to match. If set to a string or id then it is automatically
29
+ # wrapped in an Action object.
30
+ attr_reader :action
31
+
32
+ def action=(value)
33
+ if value.is_a?(Symbol)
34
+ @action = :enter
35
+ elsif value.is_a?(String)
36
+ @action = SkyDB::Action.new(:name => value)
37
+ elsif value.is_a?(Fixnum)
38
+ @action = SkyDB::Action.new(:id => value)
39
+ elsif value.is_a?(SkyDB::Action)
40
+ @action = value
41
+ else
42
+ @action = nil
43
+ end
44
+ end
45
+
46
+
47
+ ##########################################################################
48
+ #
49
+ # Methods
50
+ #
51
+ ##########################################################################
52
+
53
+ ##################################
54
+ # Validation
55
+ ##################################
56
+
57
+ # Validates that the object is correct before executing a codegen.
58
+ def validate!
59
+ # Require the action identifier.
60
+ if action.nil? || action.id.to_i == 0
61
+ raise SkyDB::Query::ValidationError.new("Action with non-zero identifier required.")
62
+ end
63
+
64
+ # Require the function name. This should be set automatically by the
65
+ # query.
66
+ if function_name.to_s.index(/^\w+$/).nil?
67
+ raise SkyDB::Query::ValidationError.new("Invalid function name '#{function_name.to_s}'.")
68
+ end
69
+
70
+ return nil
71
+ end
72
+
73
+
74
+ ##################################
75
+ # Codegen
76
+ ##################################
77
+
78
+ # Generates Lua code to match a given action.
79
+ def codegen(options={})
80
+ header, body, footer = "function #{function_name.to_s}(cursor, data)\n", [], "end\n"
81
+
82
+ # If the action is :enter then just check for the beginning of a session.
83
+ if action == :enter
84
+ body << "return (cursor.session_event_index == 0)"
85
+ else
86
+ # Only move to the next event if directed to by the options.
87
+ body << "repeat"
88
+ body << " if cursor.event.action_id == #{action.id.to_i} then"
89
+ body << " cursor:next()"
90
+ body << " return true"
91
+ body << " end"
92
+ body << "until not cursor:next()"
93
+ body << "return false"
94
+ end
95
+
96
+ # Indent body and return.
97
+ body.map! {|line| " " + line}
98
+ return header + body.join("\n") + "\n" + footer
99
+ end
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,26 @@
1
+ class SkyDB
2
+ class Query
3
+ class Ast
4
+ module SelectionFieldSyntaxNode
5
+ # Generates the SelectionField object from the node.
6
+ def generate
7
+ field = SkyDB::Query::SelectionField.new()
8
+
9
+ # If there is an expression present then use it.
10
+ if respond_to?('expression')
11
+ field.expression = expression.text_value
12
+
13
+ # Otherwise we'll typically use the whole value unless there is an
14
+ # aggregation type mentioned. An example of this is: "count()".
15
+ elsif !respond_to?('aggregation_type')
16
+ field.expression = text_value
17
+ end
18
+
19
+ field.alias_name = alias_name.text_value if respond_to?('alias_name')
20
+ field.aggregation_type = aggregation_type.text_value.downcase.to_sym if respond_to?('aggregation_type')
21
+ return field
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ class SkyDB
2
+ class Query
3
+ class Ast
4
+ module SelectionFieldsSyntaxNode
5
+ # Generates a list of selection fields.
6
+ def generate
7
+ fields = []
8
+ Treetop.search(self, SelectionFieldSyntaxNode).each do |field_node|
9
+ fields << field_node.generate()
10
+ end
11
+ return fields
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ class SkyDB
2
+ class Query
3
+ class Ast
4
+ module SelectionGroupSyntaxNode
5
+ # Generates the SelectionGroup object from the node.
6
+ def generate
7
+ group = SkyDB::Query::SelectionGroup.new(
8
+ :expression => (respond_to?('expression') ? expression.text_value : text_value)
9
+ )
10
+ group.alias_name = alias_name.text_value if respond_to?('alias_name')
11
+ return group
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ class SkyDB
2
+ class Query
3
+ class Ast
4
+ module SelectionGroupsSyntaxNode
5
+ # Generates a list of selection groups.
6
+ def generate
7
+ groups = []
8
+ Treetop.search(self, SelectionGroupSyntaxNode).each do |group_node|
9
+ groups << group_node.generate()
10
+ end
11
+ return groups
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,268 @@
1
+ require 'skydb/query/selection_fields_parse_error'
2
+ require 'skydb/query/selection_field'
3
+
4
+ require 'skydb/query/selection_groups_parse_error'
5
+ require 'skydb/query/selection_group'
6
+
7
+ require 'skydb/query/ast/selection_fields_syntax_node'
8
+ require 'skydb/query/ast/selection_field_syntax_node'
9
+ require 'skydb/query/ast/selection_groups_syntax_node'
10
+ require 'skydb/query/ast/selection_group_syntax_node'
11
+ require 'skydb/query/selection_fields_grammar'
12
+ require 'skydb/query/selection_groups_grammar'
13
+
14
+ class SkyDB
15
+ class Query
16
+ # The selection object contains a list of all fields and their aliases.
17
+ # Selection fields can include simple properties as well as aggregation
18
+ # functions.
19
+ class Selection
20
+ ##########################################################################
21
+ #
22
+ # Static Methods
23
+ #
24
+ ##########################################################################
25
+
26
+ # Parses a string into a list of selection fields.
27
+ #
28
+ # @param [String] str A formatted list of fields to select.
29
+ #
30
+ # @return [Array] An array of selection fields.
31
+ def self.parse_fields(str)
32
+ # Parse the selection fields string.
33
+ parser = SelectionFieldsGrammarParser.new()
34
+ ast = parser.parse(str)
35
+
36
+ # If there was a problem then throw a parse error.
37
+ if ast.nil?
38
+ raise SkyDB::Query::SelectionFieldsParseError.new(parser.failure_reason,
39
+ :line => parser.failure_line,
40
+ :column => parser.failure_column
41
+ )
42
+ end
43
+
44
+ return ast.generate
45
+ end
46
+
47
+ # Parses a string into a list of selection groups.
48
+ #
49
+ # @param [String] str A formatted list of fields to group by.
50
+ #
51
+ # @return [Array] An array of selection groups.
52
+ def self.parse_groups(str)
53
+ # Parse the selection groups string.
54
+ parser = SelectionGroupsGrammarParser.new()
55
+ ast = parser.parse(str)
56
+
57
+ # If there was a problem then throw a parse error.
58
+ if ast.nil?
59
+ raise SkyDB::Query::SelectionGroupsParseError.new(parser.failure_reason,
60
+ :line => parser.failure_line,
61
+ :column => parser.failure_column
62
+ )
63
+ end
64
+
65
+ return ast.generate
66
+ end
67
+
68
+
69
+ ##########################################################################
70
+ #
71
+ # Constructor
72
+ #
73
+ ##########################################################################
74
+
75
+ def initialize(options={})
76
+ self.fields = options[:fields] || []
77
+ self.groups = options[:groups] || []
78
+ end
79
+
80
+
81
+ ##########################################################################
82
+ #
83
+ # Attributes
84
+ #
85
+ ##########################################################################
86
+
87
+ # A list of fields that will be returned from the server.
88
+ attr_accessor :fields
89
+
90
+ # A list of expressions to group the returned data by.
91
+ attr_accessor :groups
92
+
93
+
94
+ ##########################################################################
95
+ #
96
+ # Methods
97
+ #
98
+ ##########################################################################
99
+
100
+ ####################################
101
+ # Helpers
102
+ ####################################
103
+
104
+ # Adds a list of fields to the selection.
105
+ #
106
+ # @param [String] args A list of fields to add to the selection.
107
+ #
108
+ # @return [Selection] The selection object is returned.
109
+ def select(*args)
110
+ args.each do |arg|
111
+ if arg.is_a?(String)
112
+ self.fields = self.fields.concat(SkyDB::Query::Selection.parse_fields(arg))
113
+ elsif arg.is_a?(Symbol)
114
+ self.fields << SelectionField.new(:expression => arg.to_s)
115
+ else
116
+ raise "Invalid selection argument: #{arg} (#{arg.class})"
117
+ end
118
+ end
119
+
120
+ return self
121
+ end
122
+
123
+ # Adds one or more grouping fields to the selection of the query.
124
+ #
125
+ # @param [String] args A list of groups to add to the selection.
126
+ #
127
+ # @return [Selection] The selection object is returned.
128
+ def group_by(*args)
129
+ args.each do |arg|
130
+ if arg.is_a?(String)
131
+ self.groups = self.groups.concat(SkyDB::Query::Selection.parse_groups(arg))
132
+ elsif arg.is_a?(Symbol)
133
+ self.groups << SelectionGroup.new(:expression => arg.to_s)
134
+ else
135
+ raise "Invalid group by argument: #{arg} (#{arg.class})"
136
+ end
137
+ end
138
+
139
+ return self
140
+ end
141
+
142
+
143
+ ####################################
144
+ # Validation
145
+ ####################################
146
+
147
+ # Validates that all the elements of the query are valid.
148
+ def validate!
149
+ # Require that at least one field exist.
150
+ if fields.length == 0
151
+ raise SkyDB::Query::ValidationError.new("At least one selection field is required for #{self.inspect}.")
152
+ end
153
+
154
+ fields.each do |field|
155
+ field.validate!
156
+ end
157
+
158
+ groups.each do |group|
159
+ group.validate!
160
+ end
161
+ end
162
+
163
+
164
+ ####################################
165
+ # Codegen
166
+ ####################################
167
+
168
+ # Generates Lua code for the aggregation based on the selection.
169
+ def codegen_select
170
+ header, body, footer = "function select(cursor, data)\n", [], "end\n"
171
+
172
+ # Setup target object.
173
+ body << "target = data"
174
+ body << "" if groups.length > 0
175
+
176
+ # Initialize groups.
177
+ groups.each do |group|
178
+ body << "group_value = #{group.accessor}"
179
+ body << "if cursor:eos() or cursor:eof() then group_value = -1 end" if group.expression == 'action_id'
180
+ body << "if target[group_value] == nil then"
181
+ body << " target[group_value] = {}"
182
+ body << "end"
183
+ body << "target = target[group_value]"
184
+ body << ""
185
+ end
186
+
187
+ # Generate the assignment for each field.
188
+ fields.each do |field|
189
+ alias_name = field.target_name
190
+
191
+ case field.aggregation_type
192
+ when nil
193
+ body << "target.#{alias_name} = #{field.accessor}"
194
+ when :count
195
+ body << "target.#{alias_name} = (target.#{alias_name} or 0) + 1"
196
+ when :sum
197
+ body << "target.#{alias_name} = (target.#{alias_name} or 0) + #{field.accessor}"
198
+ when :min
199
+ body << "if(target.#{alias_name} == nil or target.#{alias_name} > #{field.accessor}) then"
200
+ body << " target.#{alias_name} = #{field.accessor}"
201
+ body << "end"
202
+ when :max
203
+ body << "if(target.#{alias_name} == nil or target.#{alias_name} < #{field.accessor}) then"
204
+ body << " target.#{alias_name} = #{field.accessor}"
205
+ body << "end"
206
+ else
207
+ raise StandardError.new("Invalid aggregation type: #{field.aggregation_type}")
208
+ end
209
+ end
210
+
211
+ # Indent body and return.
212
+ body.map! {|line| " " + line}
213
+ return header + body.join("\n") + "\n" + footer
214
+ end
215
+
216
+ # Generates Lua code for the merge function.
217
+ def codegen_merge
218
+ header, body, footer = "function merge(results, data)\n", [], "end\n"
219
+
220
+ # Open group loops.
221
+ groups.each_with_index do |group, index|
222
+ data_item = "data" + (0...index).to_a.map {|i| "[k#{i}]"}.join('')
223
+ results_item = "results" + (0..index).to_a.map {|i| "[k#{i}]"}.join('')
224
+ body << "#{' ' * index}for k#{index},v#{index} in pairs(#{data_item}) do"
225
+ body << "#{' ' * index} if #{results_item} == nil then #{results_item} = {} end"
226
+ end
227
+
228
+ indent = ' ' * groups.length
229
+ body << "#{indent}a = results" + (0...groups.length).to_a.map {|i| "[k#{i}]"}.join('')
230
+ body << "#{indent}b = data" + (0...groups.length).to_a.map {|i| "[k#{i}]"}.join('')
231
+
232
+ # Generate the merge for each field.
233
+ fields.each do |field|
234
+ alias_name = field.target_name
235
+
236
+ case field.aggregation_type
237
+ when nil
238
+ body << "#{indent}a.#{alias_name} = b.#{alias_name}"
239
+ when :count
240
+ body << "#{indent}a.#{alias_name} = (a.#{alias_name} or 0) + (b.#{alias_name} or 0)"
241
+ when :sum
242
+ body << "#{indent}a.#{alias_name} = (a.#{alias_name} or 0) + (b.#{alias_name} or 0)"
243
+ when :min
244
+ body << "#{indent}if(a.#{alias_name} == nil or a.#{alias_name} > b.#{alias_name}) then"
245
+ body << "#{indent} a.#{alias_name} = b.#{alias_name}"
246
+ body << "#{indent}end"
247
+ when :max
248
+ body << "#{indent}if(a.#{alias_name} == nil or a.#{alias_name} < b.#{alias_name}) then"
249
+ body << "#{indent} a.#{alias_name} = b.#{alias_name}"
250
+ body << "#{indent}end"
251
+ else
252
+ raise StandardError.new("Invalid aggregation type: #{field.aggregation_type}")
253
+ end
254
+ end
255
+
256
+ # Close group loops.
257
+ groups.reverse.each_with_index do |group, index|
258
+ body << "#{' ' * (groups.length-index-1)}end"
259
+ end
260
+
261
+ # Indent body and return.
262
+ body.map! {|line| " " + line}
263
+ return header + body.join("\n") + "\n" + footer
264
+ end
265
+ end
266
+ end
267
+ end
268
+