cubicle 0.1.0
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.
- data/CHANGELOG.rdoc +2 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +173 -0
- data/Rakefile +49 -0
- data/cubicle.gemspec +91 -0
- data/examples/cubicles/poker_hand_cubicle.rb +16 -0
- data/examples/models/poker_hand.rb +3 -0
- data/lib/cubicle.rb +389 -0
- data/lib/cubicle/aggregation.rb +10 -0
- data/lib/cubicle/calculated_measure.rb +18 -0
- data/lib/cubicle/data.rb +84 -0
- data/lib/cubicle/data_level.rb +60 -0
- data/lib/cubicle/date_time.rb +33 -0
- data/lib/cubicle/dimension.rb +4 -0
- data/lib/cubicle/measure.rb +16 -0
- data/lib/cubicle/member.rb +53 -0
- data/lib/cubicle/member_list.rb +11 -0
- data/lib/cubicle/mongo_environment.rb +102 -0
- data/lib/cubicle/mongo_mapper/aggregate_plugin.rb +17 -0
- data/lib/cubicle/query.rb +315 -0
- data/lib/cubicle/ratio.rb +12 -0
- data/lib/cubicle/support.rb +46 -0
- data/lib/cubicle/version.rb +3 -0
- data/test/config/database.yml +15 -0
- data/test/cubicle/cubicle_aggregation_test.rb +21 -0
- data/test/cubicle/cubicle_data_level_test.rb +58 -0
- data/test/cubicle/cubicle_data_test.rb +51 -0
- data/test/cubicle/cubicle_query_test.rb +326 -0
- data/test/cubicle/cubicle_test.rb +85 -0
- data/test/cubicle/mongo_mapper/aggregate_plugin_test.rb +39 -0
- data/test/cubicles/defect_cubicle.rb +26 -0
- data/test/log/test.log +6813 -0
- data/test/models/defect.rb +73 -0
- data/test/test_helper.rb +62 -0
- metadata +144 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module Cubicle
|
2
|
+
class CalculatedMeasure < Measure
|
3
|
+
|
4
|
+
def initialize(*args)
|
5
|
+
opts = args.extract_options!
|
6
|
+
opts[:aggregation_method] = :calculation
|
7
|
+
args << opts
|
8
|
+
super(*args)
|
9
|
+
end
|
10
|
+
#calculated members to not participate in the map/reduce
|
11
|
+
#cycle. They are a finalization-time only
|
12
|
+
#concept.
|
13
|
+
def to_js_keys
|
14
|
+
[]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/lib/cubicle/data.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Cubicle
|
2
|
+
class Data < Array
|
3
|
+
|
4
|
+
attr_reader :dimension_names, :measure_names, :total_count
|
5
|
+
|
6
|
+
def initialize(query,query_results,total_count = nil)
|
7
|
+
@dimension_names = query.dimensions.map{|d|d.name}
|
8
|
+
@measure_names = query.measures.map{|m|m.name}
|
9
|
+
@time_dimension_name = query.time_dimension.name if query.respond_to?(:time_dimension) && query.time_dimension
|
10
|
+
@time_period = query.time_period if query.respond_to?(:time_period)
|
11
|
+
@time_range = query.time_range if query.respond_to?(:time_range)
|
12
|
+
extract_data(query_results)
|
13
|
+
@total_count = total_count if total_count
|
14
|
+
end
|
15
|
+
|
16
|
+
def hierarchize(*args)
|
17
|
+
args = [@time_dimension_name || @dimension_names].flatten if args.blank?
|
18
|
+
extract_dimensions args, self
|
19
|
+
end
|
20
|
+
alias hierarchize_by hierarchize
|
21
|
+
alias by hierarchize
|
22
|
+
|
23
|
+
def records_per_page=(records_per_page)
|
24
|
+
@records_per_page=records_per_page
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_pages
|
28
|
+
if (!defined?(@total_count))
|
29
|
+
raise "Cannot find the total number of pages without setting the total count"
|
30
|
+
end
|
31
|
+
|
32
|
+
if (!defined?(@records_per_page))
|
33
|
+
raise "Cannot find the total number of pages without setting the number of records per page"
|
34
|
+
end
|
35
|
+
|
36
|
+
(@total_count.to_f / @records_per_page.to_f).ceil
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def extract_dimensions(dimension_names, data)
|
42
|
+
data, dimension_names = data.dup, dimension_names.dup
|
43
|
+
|
44
|
+
return data.map{|measures|Cubicle::DataLevel.new(:measures,measures)} if dimension_names.blank?
|
45
|
+
|
46
|
+
dim_name = dimension_names.shift
|
47
|
+
|
48
|
+
result = Cubicle::DataLevel.new(dim_name)
|
49
|
+
data.each do |tuple|
|
50
|
+
member_name = tuple.delete(dim_name.to_s) || "Unknown"
|
51
|
+
result[member_name] << tuple
|
52
|
+
end
|
53
|
+
|
54
|
+
result.each do |key,value|
|
55
|
+
result[key] = extract_dimensions(dimension_names,value)
|
56
|
+
end
|
57
|
+
|
58
|
+
expand_time_dimension_if_required(result)
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
def extract_data(data)
|
64
|
+
data.each do |result|
|
65
|
+
new = result.dup
|
66
|
+
self << new.delete("_id").merge(new.delete("value"))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def expand_time_dimension_if_required(data_level)
|
71
|
+
return unless data_level.leaf_level? && @time_dimension_name && @time_dimension_name.to_s == data_level.name.to_s &&
|
72
|
+
@time_range && @time_period
|
73
|
+
|
74
|
+
@time_range.by!(@time_period)
|
75
|
+
|
76
|
+
@time_range.each do |date|
|
77
|
+
formatted_date = date.to_cubicle(@time_period)
|
78
|
+
data_level[formatted_date] = [Cubicle::DataLevel.new(:measures,{})] unless data_level.include?(formatted_date)
|
79
|
+
end
|
80
|
+
data_level.keys.sort!
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Cubicle
|
2
|
+
class DataLevel < OrderedHash
|
3
|
+
|
4
|
+
def initialize(name = "Unknown Level", initial_data = {})
|
5
|
+
@name = name
|
6
|
+
merge!(initial_data.stringify_keys)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
attr_accessor :missing_member_default
|
11
|
+
|
12
|
+
alias member_names keys
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
key = key.to_s
|
16
|
+
self[key] = [] unless self.keys.include?(key)
|
17
|
+
super(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(key,val)
|
21
|
+
super(key.to_s,val)
|
22
|
+
end
|
23
|
+
|
24
|
+
def include?(key)
|
25
|
+
super(key.to_s)
|
26
|
+
end
|
27
|
+
|
28
|
+
def flatten(member_name = nil, opts={}, &block)
|
29
|
+
|
30
|
+
default_val = opts[:default] || @missing_member_default || 0
|
31
|
+
|
32
|
+
self.inject([]) do |output, (key, data)|
|
33
|
+
data.inject(output) do |flattened, value|
|
34
|
+
value.missing_member_default = default_val if value.respond_to?(:missing_member_default)
|
35
|
+
|
36
|
+
if block_given?
|
37
|
+
flat_val = block.arity == 1 ? (yield value) : (value.instance_eval(&block))
|
38
|
+
end
|
39
|
+
flat_val ||= value[member_name] if member_name && value.include?(member_name)
|
40
|
+
flat_val ||= default_val
|
41
|
+
flattened << flat_val
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def leaf_level?
|
48
|
+
return self.length < 1 ||
|
49
|
+
!self[self.keys[0]].is_a?(Cubicle::DataLevel)
|
50
|
+
end
|
51
|
+
|
52
|
+
def method_missing(sym,*args,&block)
|
53
|
+
return self[sym.to_s[0..-2]] = args[0] if sym.to_s =~ /.*=$/
|
54
|
+
return self[sym] if self.keys.include?(sym.to_s)
|
55
|
+
missing_member_default
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Cubicle
|
2
|
+
module DateTime
|
3
|
+
def self.db_time_format
|
4
|
+
@time_format ||= :iso8601 #or :native || :time || anything not :iso8601
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.db_time_format=(time_format)
|
8
|
+
raise "db_time_format must be :iso8601 or :native" unless [:iso8601,:native].include?(time_format)
|
9
|
+
@time_format=time_format
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.iso8601?
|
13
|
+
self.db_time_format == :iso8601
|
14
|
+
end
|
15
|
+
|
16
|
+
def iso8601?
|
17
|
+
Cubicle::DateTime.iso8601?
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_cubicle(period = :date)
|
21
|
+
case period
|
22
|
+
when :year, :years then iso8601? ? self.strftime('%Y') : beginning_of_year
|
23
|
+
when :quarter, :quarters then iso8601? ? "#{db_year}-Q#{(month+2) / 3}" : beginning_of_quarter
|
24
|
+
when :month, :months then iso8601? ? self.strftime('%Y-%m') : beginning_of_month
|
25
|
+
else iso8601? ? self.strftime('%Y-%m-%d') : self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def beginning_of(period)
|
30
|
+
self.send "beginning_of_#{period.to_s.singularize}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Cubicle
|
2
|
+
class Measure < Member
|
3
|
+
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@aggregation_method = self.options.delete(:aggregation_method) || :count
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :aggregation_method #can be :sum, :average, :count
|
10
|
+
|
11
|
+
def to_js_value
|
12
|
+
return super unless aggregation_method == :count
|
13
|
+
"((#{super}) ? 1 : 0)"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Cubicle
|
2
|
+
class Member
|
3
|
+
|
4
|
+
attr_accessor :name,
|
5
|
+
:expression,
|
6
|
+
:expression_type, #can be :field_name, :javascript
|
7
|
+
:options,
|
8
|
+
:alias_list
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
opts = args.extract_options!
|
12
|
+
@name = args.shift.to_sym
|
13
|
+
|
14
|
+
self.options = (opts || {}).symbolize_keys
|
15
|
+
|
16
|
+
if (@expression = self.options.delete(:field_name))
|
17
|
+
@expression_type = :field_name
|
18
|
+
elsif (@expression = self.options.delete(:expression))
|
19
|
+
@expression_type = :javascript
|
20
|
+
else
|
21
|
+
@expression = @name
|
22
|
+
@expression_type = :field_name
|
23
|
+
end
|
24
|
+
|
25
|
+
member_alias = self.options[:alias]
|
26
|
+
if (member_alias)
|
27
|
+
member_alias = [member_alias] unless member_alias.is_a?(Array)
|
28
|
+
@alias_list = member_alias.map{|a|a.to_s}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def matches(member_name)
|
34
|
+
return name.to_s == member_name.to_s || (@alias_list||=[]).include?(member_name.to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
def included_in?(list_of_member_names)
|
38
|
+
list_of_member_names.each do |member_name|
|
39
|
+
return true if matches(member_name)
|
40
|
+
end
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_js_keys
|
45
|
+
["#{name}:#{to_js_value}"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_js_value
|
49
|
+
prefix, suffix = expression_type == :field_name ? ['this.',''] : ['(',')']
|
50
|
+
"#{prefix}#{expression}#{suffix}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Cubicle
|
2
|
+
class MongoEnvironment
|
3
|
+
|
4
|
+
# @api public
|
5
|
+
def self.connection
|
6
|
+
@@connection ||= Mongo::Connection.new
|
7
|
+
end
|
8
|
+
|
9
|
+
# @api public
|
10
|
+
def self.connection=(new_connection)
|
11
|
+
@@connection = new_connection
|
12
|
+
end
|
13
|
+
|
14
|
+
# @api public
|
15
|
+
def self.logger
|
16
|
+
connection.logger
|
17
|
+
end
|
18
|
+
|
19
|
+
# @api public
|
20
|
+
def self.database=(name)
|
21
|
+
@@database = nil
|
22
|
+
@@database_name = name
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api public
|
26
|
+
def self.database
|
27
|
+
if @@database_name.blank?
|
28
|
+
raise 'You forgot to set the default database name: MongoMapper.database = "foobar"'
|
29
|
+
end
|
30
|
+
|
31
|
+
@@database ||= connection.db(@@database_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.config=(hash)
|
35
|
+
@@config = hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.config
|
39
|
+
raise 'Set config before connecting. Cubicle.mongo.config = {...}' unless defined?(@@config)
|
40
|
+
@@config
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def self.config_for_environment(environment)
|
45
|
+
env = config[environment]
|
46
|
+
return env if env['uri'].blank?
|
47
|
+
|
48
|
+
uri = URI.parse(env['uri'])
|
49
|
+
raise InvalidScheme.new('must be mongodb') unless uri.scheme == 'mongodb'
|
50
|
+
{
|
51
|
+
'host' => uri.host,
|
52
|
+
'port' => uri.port,
|
53
|
+
'database' => uri.path.gsub(/^\//, ''),
|
54
|
+
'username' => uri.user,
|
55
|
+
'password' => uri.password,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.connect(environment, options={})
|
60
|
+
raise 'Set config before connecting. Cubicle.mongo.config = {...}' if config.blank?
|
61
|
+
env = config_for_environment(environment)
|
62
|
+
self.connection = Mongo::Connection.new(env['host'], env['port'], options)
|
63
|
+
self.database = env['database']
|
64
|
+
self.database.authenticate(env['username'], env['password']) if env['username'] && env['password']
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.setup(config, environment, options={})
|
68
|
+
using_passenger = options.delete(:passenger)
|
69
|
+
handle_passenger_forking if using_passenger
|
70
|
+
self.config = config
|
71
|
+
connect(environment, options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.handle_passenger_forking
|
75
|
+
if defined?(PhusionPassenger)
|
76
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
77
|
+
connection.connect_to_master if forked
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
def self.use_time_zone?
|
84
|
+
Time.respond_to?(:zone) && Time.zone ? true : false
|
85
|
+
end
|
86
|
+
|
87
|
+
# @api private
|
88
|
+
def self.time_class
|
89
|
+
use_time_zone? ? Time.zone : Time
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api private
|
93
|
+
def self.normalize_object_id(value)
|
94
|
+
value.is_a?(String) ? Mongo::ObjectID.from_string(value) : value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
#This class represents MongoDB. It is lifted line for line from MongoMapper
|
99
|
+
#http://github.com/jnunemaker/mongomapper/blob/master/lib/mongo_mapper.rb
|
100
|
+
#Actually, if the MongoMapper gem is loaded, Cubicle will simply use it for
|
101
|
+
#providing the MongoEnvironment. However, if MongoMapper isn't loaded,
|
102
|
+
#this stuff is still required, so why reinvent the wheel?
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Cubicle
|
2
|
+
module MongoMapper
|
3
|
+
module AggregatePlugin
|
4
|
+
module ClassMethods
|
5
|
+
def aggregate(&block)
|
6
|
+
return Cubicle::Aggregation.new(self.collection_name,&block)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(model)
|
11
|
+
model.plugin AggregatePlugin
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
MongoMapper::Document.append_inclusions(Cubicle::MongoMapper::AggregatePlugin)
|
@@ -0,0 +1,315 @@
|
|
1
|
+
module Cubicle
|
2
|
+
class Query
|
3
|
+
|
4
|
+
attr_reader :time_period, :transient
|
5
|
+
attr_accessor :source_collection_name
|
6
|
+
def initialize(cubicle)
|
7
|
+
@cubicle = cubicle
|
8
|
+
|
9
|
+
@dimensions = Cubicle::MemberList.new
|
10
|
+
@measures = Cubicle::MemberList.new
|
11
|
+
@source_collection_name = @cubicle.target_collection_name
|
12
|
+
@where, @from_date, @to_date, @date_dimension, @time_period, @limit, @offset = nil
|
13
|
+
@all_dimensions, @all_measures = false, false
|
14
|
+
@transient = false
|
15
|
+
@by=[]
|
16
|
+
@order_by=[]
|
17
|
+
@from_date_filter = "$gte"
|
18
|
+
@to_date_filter = "$lte"
|
19
|
+
end
|
20
|
+
|
21
|
+
def clone
|
22
|
+
Marshal.load(Marshal.dump(self))
|
23
|
+
end
|
24
|
+
|
25
|
+
def select_all
|
26
|
+
select :all_dimensions, :all_measures
|
27
|
+
end
|
28
|
+
|
29
|
+
def selected?(member = nil)
|
30
|
+
return (@dimensions.length > 0 || @measures.length > 0) unless member
|
31
|
+
member_name = member.kind_of?(Cubicle::Member) ? member.name : member.to_s
|
32
|
+
return @dimensions[member_name] ||
|
33
|
+
@measures[member_name]
|
34
|
+
end
|
35
|
+
|
36
|
+
def transient?
|
37
|
+
@transient || @cubicle.transient?
|
38
|
+
end
|
39
|
+
|
40
|
+
def transient!
|
41
|
+
@transient = true
|
42
|
+
@source_collection_name = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def all_measures?
|
46
|
+
@all_measures
|
47
|
+
end
|
48
|
+
|
49
|
+
def all_dimensions?
|
50
|
+
@all_dimensions
|
51
|
+
end
|
52
|
+
|
53
|
+
def select(*args)
|
54
|
+
args = args[0] if args[0].is_a?(Array)
|
55
|
+
|
56
|
+
if (args.include?(:all))
|
57
|
+
select_all
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
if (args.include?(:all_measures))
|
62
|
+
@all_measures = true
|
63
|
+
@measures = Cubicle::MemberList.new
|
64
|
+
end
|
65
|
+
if (args.include?(:all_dimensions))
|
66
|
+
@all_dimensions = true
|
67
|
+
@dimensions = Cubicle::MemberList.new
|
68
|
+
end
|
69
|
+
|
70
|
+
return if args.length == 1 && selected?(args[0])
|
71
|
+
|
72
|
+
found=[:all_measures,:all_dimensions]
|
73
|
+
|
74
|
+
if args.length == 1 && !all_dimensions? && args[0].is_a?(Cubicle::Dimension)
|
75
|
+
@dimensions << convert_dimension(args.pop)
|
76
|
+
elsif args.length == 1 && !all_measures? && args[0].is_a?(Cubicle::Measure)
|
77
|
+
@measures << convert_measure(args.pop)
|
78
|
+
else
|
79
|
+
#remove from the list any dimensions or measures that are already
|
80
|
+
#selected. This allows select to be idempotent,
|
81
|
+
#which is useful for ensuring certain members are selected
|
82
|
+
#even though the user may already have selected them previously
|
83
|
+
args.each do |member_name|
|
84
|
+
if (member = @cubicle.dimensions[member_name])
|
85
|
+
@dimensions << convert_dimension(member)
|
86
|
+
elsif (member = @cubicle.measures[member_name])
|
87
|
+
@measures << convert_measure(member)
|
88
|
+
end
|
89
|
+
found << member_name if member || selected?(member_name)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
args = args - found
|
93
|
+
raise "You selected one or more members that do not exist in the underlying data source:#{args.inspect}" unless args.blank?
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def limit(in_limit = nil)
|
98
|
+
return @limit unless in_limit
|
99
|
+
@limit = in_limit
|
100
|
+
return self
|
101
|
+
end
|
102
|
+
|
103
|
+
def offset(in_offset = nil)
|
104
|
+
return @offset unless in_offset
|
105
|
+
@offset = in_offset
|
106
|
+
return self
|
107
|
+
end
|
108
|
+
alias skip offset
|
109
|
+
|
110
|
+
def by(*args)
|
111
|
+
return @by unless args.length > 0
|
112
|
+
|
113
|
+
#We'll need these in the result set
|
114
|
+
select *args
|
115
|
+
|
116
|
+
#replace any alias names with actual member names
|
117
|
+
@by = args.map{|member_name|@cubicle.find_member(member_name).name}
|
118
|
+
return if @time_dimension #If a time dimension has been explicitly specified, the following isn't helpful.
|
119
|
+
|
120
|
+
#Now let's see if we can find ourselves a time dimension
|
121
|
+
# if (@cubicle.time_dimension && time_dimension.included_in?(args))
|
122
|
+
# time_dimension(@cubicle.time_dimension)
|
123
|
+
# else
|
124
|
+
# args.each do |by_member|
|
125
|
+
# if (detected = detect_time_period by_member)
|
126
|
+
# time_dimension by_member
|
127
|
+
# @time_period = detected
|
128
|
+
# break
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
end
|
133
|
+
|
134
|
+
def order_by(*args)
|
135
|
+
return @order_by unless args.length > 0
|
136
|
+
args.each do |order|
|
137
|
+
@order_by << (order.is_a?(Array) ? order : [order,:asc])
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def time_range(date_range = nil)
|
142
|
+
return nil unless date_range || @from_date || @to_date
|
143
|
+
return ((@from_date || Time.now)..(@to_date || Time.now)) unless date_range
|
144
|
+
@to_date_filter = date_range.exclude_end? ? "$lt" : "$lte"
|
145
|
+
@from_date, @to_date = date_range.first, date_range.last if date_range
|
146
|
+
end
|
147
|
+
|
148
|
+
def time_dimension(dimension = nil)
|
149
|
+
return (@time_dimension ||= @cubicle.time_dimension) unless dimension
|
150
|
+
@time_dimension = dimension.is_a?(Cubicle::Dimension) ? dimension : @cubicle.dimensions[dimension]
|
151
|
+
raise "No dimension matching the name #{dimension} could be found in the underlying data source" unless @time_dimension
|
152
|
+
#select @time_dimension unless selected?(dimension)
|
153
|
+
end
|
154
|
+
alias date_dimension time_dimension
|
155
|
+
|
156
|
+
def last(duration,as_of = Time.now)
|
157
|
+
duration = 1.send(duration) if [:year,:month,:week,:day].include?(duration)
|
158
|
+
period = duration.parts[0][0]
|
159
|
+
@from_date = duration.ago(as_of).advance(period=>1)
|
160
|
+
@to_date = as_of
|
161
|
+
end
|
162
|
+
alias for_the_last last
|
163
|
+
|
164
|
+
def last_complete(duration,as_of = Time.now)
|
165
|
+
duration = 1.send(duration) if [:year,:month,:week,:day].include?(duration)
|
166
|
+
period = duration.parts[0][0]
|
167
|
+
@to_date = as_of.beginning_of(period)
|
168
|
+
@from_date = duration.ago(@to_date)
|
169
|
+
@to_date_filter = "$lt"
|
170
|
+
end
|
171
|
+
alias for_the_last_complete last_complete
|
172
|
+
|
173
|
+
def next(duration,as_of = Time.now)
|
174
|
+
duration = 1.send(duration) if [:year,:month,:week,:day].include?(duration)
|
175
|
+
period = duration.parts[0][0]
|
176
|
+
@to_date = duration.from_now(as_of).advance(period=>-1)
|
177
|
+
@from_date = as_of
|
178
|
+
end
|
179
|
+
alias for_the_next next
|
180
|
+
|
181
|
+
def this(period,as_of = Time.now)
|
182
|
+
@from_date = as_of.beginning_of(period)
|
183
|
+
@to_date = as_of
|
184
|
+
self
|
185
|
+
end
|
186
|
+
|
187
|
+
def from(time = nil)
|
188
|
+
return @from_date unless time
|
189
|
+
@from_date = if time.is_a?(Symbol)
|
190
|
+
Time.send(time) if Time.respond_to?(time)
|
191
|
+
Date.send(time).to_time if Date.respond_to?(time)
|
192
|
+
else
|
193
|
+
time.to_time
|
194
|
+
end
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
def until(time = nil)
|
199
|
+
return @to_date unless time
|
200
|
+
@to_date = if time.is_a?(Symbol)
|
201
|
+
Time.send(time) if Time.respond_to?(time)
|
202
|
+
Date.send(time).to_time if Date.respond_to?(time)
|
203
|
+
else
|
204
|
+
time.to_time
|
205
|
+
end
|
206
|
+
self
|
207
|
+
end
|
208
|
+
|
209
|
+
def ytd(as_of = Time.now)
|
210
|
+
this :year, as_of
|
211
|
+
end
|
212
|
+
alias year_to_date ytd
|
213
|
+
|
214
|
+
def mtd(as_of = Time.now)
|
215
|
+
this :month, as_of
|
216
|
+
end
|
217
|
+
alias month_to_date mtd
|
218
|
+
|
219
|
+
def where(filter = nil)
|
220
|
+
return prepare_filter unless filter
|
221
|
+
(@where ||= {}).merge!(filter)
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
def dimension_names
|
226
|
+
return dimensions.map{|dim|dim.name.to_s}
|
227
|
+
end
|
228
|
+
|
229
|
+
def member_names
|
230
|
+
return (dimensions + measures).map{|m|m.name.to_s}
|
231
|
+
end
|
232
|
+
|
233
|
+
def dimensions
|
234
|
+
return @dimensions unless all_dimensions?
|
235
|
+
@cubicle.dimensions.collect{|dim|convert_dimension(dim)}
|
236
|
+
end
|
237
|
+
|
238
|
+
def measures
|
239
|
+
return @measures unless all_measures?
|
240
|
+
@cubicle.measures.collect{|measure|convert_measure(measure)}
|
241
|
+
end
|
242
|
+
|
243
|
+
def execute(options={})
|
244
|
+
@cubicle.execute_query(self,options)
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
def prepare_filter
|
249
|
+
if @from_date || @to_date
|
250
|
+
unless time_dimension
|
251
|
+
raise "A date range was specified for this query (#{@from_date}->#{@to_date}) however, a time dimension wasn't detected. Please use the time_dimension directive to name a field in your source data that represents the date/time you want to use to filter your query"
|
252
|
+
end
|
253
|
+
@time_period ||= detect_time_period || :date
|
254
|
+
|
255
|
+
if transient? && time_dimension.expression_type != :field_name
|
256
|
+
raise "You are attempting to filter against the derived dimension (#{time_dimension.name}=#{time_dimension.expression}) in a transient query. This is not allowed in transient queries, which only allow filtering fields specified using :field_name"
|
257
|
+
end
|
258
|
+
|
259
|
+
time_filter = {}
|
260
|
+
|
261
|
+
dim_name = time_dimension.name
|
262
|
+
time_filter[@from_date_filter]=@from_date.utc.to_cubicle(@time_period) if @from_date
|
263
|
+
time_filter[@to_date_filter]=@to_date.utc.to_cubicle(@time_period) if @to_date
|
264
|
+
(@where ||= {})[dim_name] = time_filter
|
265
|
+
end
|
266
|
+
@where
|
267
|
+
end
|
268
|
+
|
269
|
+
def detect_time_period(dimension_name = (time_dimension ? time_dimension.name : nil))
|
270
|
+
return nil unless dimension_name
|
271
|
+
return case dimension_name.to_s.singularize
|
272
|
+
when /.*month$/ then :month
|
273
|
+
when /.*year$/ then :year
|
274
|
+
when /.*quarter$/ then :quarter
|
275
|
+
when /.*day$/ then :date
|
276
|
+
when /.*date$/ then :date
|
277
|
+
else nil
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def convert_dimension(dimension)
|
282
|
+
return dimension if transient?
|
283
|
+
Cubicle::Dimension.new(dimension.name, :expression=>"this._id.#{dimension.name}")
|
284
|
+
end
|
285
|
+
|
286
|
+
def convert_measure(measure)
|
287
|
+
|
288
|
+
#If the measure is a ratio, we want to make
|
289
|
+
#sure each of the ratio components will be in the output
|
290
|
+
#Other than that, no change is required to the measure
|
291
|
+
#However, if all measures are included, this won't be necessary
|
292
|
+
#and would cancel out the implicit all_member inclusion. If this
|
293
|
+
#causes a bug down the line, we may need a specific :all_members
|
294
|
+
#flag rather than the implicit "no selections means all members"
|
295
|
+
#shortcut.
|
296
|
+
if (measure.is_a?(Cubicle::Ratio))
|
297
|
+
select measure.numerator, measure.denominator unless all_measures?
|
298
|
+
return measure
|
299
|
+
end
|
300
|
+
|
301
|
+
return measure if transient?
|
302
|
+
|
303
|
+
#when selecting from a cached map_reduce query, we no longer want to count rows, but aggregate
|
304
|
+
#the pre-calculated counts stored in the cached collection. Therefore, any :counts become :sum
|
305
|
+
aggregation = measure.aggregation_method == :count ? :sum : measure.aggregation_method
|
306
|
+
expression = "this.value.#{measure.name}"
|
307
|
+
if (aggregation == :average)
|
308
|
+
count_field = expression + "_count"
|
309
|
+
expression = "#{expression}*#{count_field}"
|
310
|
+
end
|
311
|
+
Cubicle::Measure.new(measure.name, :expression=>expression,:aggregation_method=>aggregation)
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
end
|