skydb 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+