parent
8c734dfa94
commit
8600720ca2
@ -1,304 +1,87 @@ |
|||||||
class CostQuery::SqlStatement |
class CostQuery::SqlStatement |
||||||
class Union |
class Union |
||||||
attr_accessor :first, :second, :as |
|
||||||
def initialize(first, second, as = nil) |
|
||||||
@first, @second, @as = first, second, as |
|
||||||
end |
|
||||||
|
|
||||||
def to_s |
|
||||||
"((\n#{first.gsub("\n", "\n\t")}\n) UNION (\n" \ |
|
||||||
"#{second.gsub("\n", "\n\t")}\n))#{" AS #{as}" if as}\n" |
|
||||||
end |
|
||||||
|
|
||||||
def each_subselect |
|
||||||
yield first |
|
||||||
yield second |
|
||||||
end |
|
||||||
|
|
||||||
def gsub(*args, &block) |
|
||||||
to_s.gsub(*args, &block) |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
include CostQuery::QueryUtils |
|
||||||
attr_accessor :desc |
|
||||||
|
|
||||||
COMMON_FIELDS = %w[ |
|
||||||
user_id project_id issue_id rate_id |
|
||||||
comments spent_on created_on updated_on tyear tmonth tweek |
|
||||||
costs overridden_costs type |
|
||||||
] |
|
||||||
|
|
||||||
## |
|
||||||
# Generates new SqlStatement. |
|
||||||
# |
|
||||||
# @param [String, #to_s] table Table name (or subselect) for from part. |
|
||||||
def initialize(table) |
|
||||||
from table |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Generates SqlStatement that maps time_entries and cost_entries to a common structure. |
|
||||||
# |
|
||||||
# Mapping for direct fields: |
|
||||||
# |
|
||||||
# Result | Time Entires | Cost entries |
|
||||||
# --------------------------|--------------------------|-------------------------- |
|
||||||
# id | id | id |
|
||||||
# user_id | user_id | user_id |
|
||||||
# project_id | project_id | project_id |
|
||||||
# issue_id | issue_id | issue_id |
|
||||||
# rate_id | rate_id | rate_id |
|
||||||
# comments | comments | comments |
|
||||||
# spent_on | spent_on | spent_on |
|
||||||
# created_on | created_on | created_on |
|
||||||
# updated_on | updated_on | updated_on |
|
||||||
# tyear | tyear | tyear |
|
||||||
# tmonth | tmonth | tmonth |
|
||||||
# tweek | tweek | tweek |
|
||||||
# costs | costs | costs |
|
||||||
# overridden_costs | overridden_costs | overridden_costs |
|
||||||
# units | hours | units |
|
||||||
# activity_id | activity_id | -1 |
|
||||||
# cost_type_id | -1 | cost_type_id |
|
||||||
# type | "TimeEntry" | "CostEntry" |
|
||||||
# count | 1 | 1 |
|
||||||
# |
|
||||||
# Also: This _should_ handle joining activities and cost_types, as the logic differs for time_entries |
|
||||||
# and cost_entries. |
|
||||||
# |
|
||||||
# @param [#table_name] model The model to map |
|
||||||
# @return [CostQuery::SqlStatement] Generated statement |
|
||||||
def self.unified_entry(model) |
|
||||||
table = table_name_for model |
|
||||||
new(table).tap do |query| |
|
||||||
query.select COMMON_FIELDS |
|
||||||
query.desc = "Subquery for #{table}" |
|
||||||
query.select({ |
|
||||||
:count => 1, :id => [model, :id], :display_costs => 1, |
|
||||||
:real_costs => switch("#{table}.overridden_costs IS NULL" => [model, :costs], :else => [model, :overridden_costs]), |
|
||||||
:week => iso_year_week(:spent_on, model), |
|
||||||
:singleton_value => 1 |
|
||||||
}) |
|
||||||
#FIXME: build this subquery from a sql_statement |
|
||||||
query.from "(SELECT *, #{typed :text, model.model_name} AS type FROM #{table}) AS #{table}" |
|
||||||
send("unify_#{table}", query) |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Applies logic for mapping time entries to general entries structure. |
|
||||||
# |
|
||||||
# @param [CostQuery::SqlStatement] query The statement to adjust |
|
||||||
def self.unify_time_entries(query) |
|
||||||
query.select :activity_id, :units => :hours, :cost_type_id => -1 |
|
||||||
query.select :cost_type => quoted_label(:caption_labor) |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Applies logic for mapping cost entries to general entries structure. |
|
||||||
# |
|
||||||
# @param [CostQuery::SqlStatement] query The statement to adjust |
|
||||||
def self.unify_cost_entries(query) |
|
||||||
query.select :units, :cost_type_id, :activity_id => -1 |
|
||||||
query.select :cost_type => "cost_types.name" |
|
||||||
query.join CostType |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Generates a statement based on all entries (i.e. time entries and cost entries) mapped to the general entries structure, |
|
||||||
# and therefore usable by filters and such. |
|
||||||
# |
|
||||||
# @return [CostQuery::SqlStatement] Generated statement |
|
||||||
def self.for_entries |
|
||||||
new unified_entry(TimeEntry).union(unified_entry(CostEntry), "entries") |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Creates a uninon of the caller and the callee. |
|
||||||
# |
|
||||||
# @param [CostQuery::SqlStatement] other Second part of the union |
|
||||||
# @return [String] The sql query. |
|
||||||
def union(other, as = nil) |
|
||||||
Union.new(self, other, as) |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Adds sum(..) part to select. |
|
||||||
# |
|
||||||
# @param [#to_s] field Name of the field to aggregate on |
|
||||||
# @param [#to_s] name Name of the result (defaults to sum) |
|
||||||
def sum(field, name = :sum, type = :sum) |
|
||||||
@sql = nil |
|
||||||
return sum({ name => field }, nil, type) unless field.respond_to? :to_hash |
|
||||||
field.each { |k,v| field[k] = "#{type}(#{v})" } |
|
||||||
select field |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Adds count(..) part to select. |
|
||||||
# |
|
||||||
# @param [#to_s] field Name of the field to aggregate on (defaults to *) |
|
||||||
# @param [#to_s] name Name of the result (defaults to sum) |
|
||||||
def count(field = "*", name = :count) |
|
||||||
sum field, name, :count |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Generates the SQL query. |
|
||||||
# Code looks ugly in exchange for pretty output (so one does unterstand those). |
|
||||||
# |
|
||||||
# @return [String] The query |
|
||||||
def to_s |
|
||||||
# FIXME I'm ugly |
|
||||||
@sql ||= begin |
|
||||||
sql = "\n-- BEGIN #{desc}\n" \ |
|
||||||
"SELECT\n#{select.map { |e| "\t#{e}" }.join ",\n"}" \ |
|
||||||
"\nFROM\n\t#{from.gsub("\n", "\n\t")}" \ |
|
||||||
"\n\t#{joins.map { |e| e.gsub("\n", "\n\t") }.join "\n\t"}" \ |
|
||||||
"\nWHERE #{where.join " AND "}\n" |
|
||||||
sql << "GROUP BY #{group_by.join ', '}\nORDER BY #{group_by.join ', '}\n" if group_by? |
|
||||||
sql << "-- END #{desc}\n" |
|
||||||
sql.gsub!('--', '#') if mysql? |
|
||||||
sql # << " LIMIT 100" |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# @overload from |
|
||||||
# Reads the from part. |
|
||||||
# @return [#to_s] From part |
|
||||||
# @overload from(table) |
|
||||||
# Sets the from part. |
|
||||||
# @param [#to_s] table |
|
||||||
# @param [#to_s] From part |
|
||||||
def from(table = nil) |
|
||||||
return @from unless table |
|
||||||
@sql = nil |
|
||||||
@from = table |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# Where conditions. Will be joined together by AND. |
|
||||||
# |
|
||||||
# @overload where |
|
||||||
# Reads the where part |
|
||||||
# @return [Array<#to_s>] Where clauses |
|
||||||
# @overload where(fields) |
|
||||||
# Adds condition to where clause |
|
||||||
# @param [Array, Hash, String] fields Parameters passed to sanitize_sql_for_conditions. |
|
||||||
# @see CostQuery::QueryUtils#sanitize_sql_for_conditions |
|
||||||
def where(fields = nil) |
|
||||||
@where ||= ["1=1"] |
|
||||||
unless fields.nil? |
|
||||||
@where << sanitize_sql_for_conditions(fields) |
|
||||||
@sql = nil |
|
||||||
end |
|
||||||
@where |
|
||||||
end |
|
||||||
|
|
||||||
## |
|
||||||
# @return [Array<String>] List of table joins |
|
||||||
def joins |
|
||||||
(@joins ||= []).tap { |j| j.uniq! } |
|
||||||
end |
|
||||||
|
|
||||||
## |
COMMON_FIELDS = %w[ |
||||||
# Adds an "left outer join" (guessing field names) to #joins. |
user_id project_id issue_id rate_id |
||||||
# |
comments spent_on created_on updated_on tyear tmonth tweek |
||||||
# @overload join(name) |
costs overridden_costs type |
||||||
# @param [Symbol, String] name Singular table name to join with, will join plural from on table.id = table_id |
] |
||||||
# @overload join(model) |
|
||||||
# @param [#table_name, #model_name] model ActiveRecord model to join with |
## |
||||||
# @overload join(hash) |
# Generates SqlStatement that maps time_entries and cost_entries to a common structure. |
||||||
# @param [Hash<#to_s => #to_s>] hash Key is singular table name to join with, value is field to join on |
# |
||||||
# @overload join(*list) |
# Mapping for direct fields: |
||||||
# @param [Array<String,Symbol,Array>] list Will generate join entries (according to guessings described above) |
# |
||||||
# @see #joins |
# Result | Time Entires | Cost entries |
||||||
def join(*list) |
# --------------------------|--------------------------|-------------------------- |
||||||
@sql = nil |
# id | id | id |
||||||
join_syntax = "LEFT OUTER JOIN %1$s ON %1$s.id = %2$s_id" |
# user_id | user_id | user_id |
||||||
list.each do |e| |
# project_id | project_id | project_id |
||||||
case e |
# issue_id | issue_id | issue_id |
||||||
when Class then joins << (join_syntax % [table_name_for(e), e.model_name.underscore]) |
# rate_id | rate_id | rate_id |
||||||
when / / then joins << e |
# comments | comments | comments |
||||||
when Symbol, String then joins << (join_syntax % [table_name_for(e), e]) |
# spent_on | spent_on | spent_on |
||||||
when Hash then e.each { |k,v| joins << (join_syntax % [table_name_for(k), field_name_for(v)]) } |
# created_on | created_on | created_on |
||||||
when Array then join(*e) |
# updated_on | updated_on | updated_on |
||||||
else raise ArgumentError, "cannot join #{e.inspect}" |
# tyear | tyear | tyear |
||||||
|
# tmonth | tmonth | tmonth |
||||||
|
# tweek | tweek | tweek |
||||||
|
# costs | costs | costs |
||||||
|
# overridden_costs | overridden_costs | overridden_costs |
||||||
|
# units | hours | units |
||||||
|
# activity_id | activity_id | -1 |
||||||
|
# cost_type_id | -1 | cost_type_id |
||||||
|
# type | "TimeEntry" | "CostEntry" |
||||||
|
# count | 1 | 1 |
||||||
|
# |
||||||
|
# Also: This _should_ handle joining activities and cost_types, as the logic differs for time_entries |
||||||
|
# and cost_entries. |
||||||
|
# |
||||||
|
# @param [#table_name] model The model to map |
||||||
|
# @return [CostQuery::SqlStatement] Generated statement |
||||||
|
def self.unified_entry(model) |
||||||
|
table = table_name_for model |
||||||
|
new(table).tap do |query| |
||||||
|
query.select COMMON_FIELDS |
||||||
|
query.desc = "Subquery for #{table}" |
||||||
|
query.select({ |
||||||
|
:count => 1, :id => [model, :id], :display_costs => 1, |
||||||
|
:real_costs => switch("#{table}.overridden_costs IS NULL" => [model, :costs], :else => [model, :overridden_costs]), |
||||||
|
:week => iso_year_week(:spent_on, model), |
||||||
|
:singleton_value => 1 |
||||||
|
}) |
||||||
|
#FIXME: build this subquery from a sql_statement |
||||||
|
query.from "(SELECT *, #{typed :text, model.model_name} AS type FROM #{table}) AS #{table}" |
||||||
|
send("unify_#{table}", query) |
||||||
end |
end |
||||||
end |
end |
||||||
end |
|
||||||
|
|
||||||
## |
## |
||||||
# @overload select |
# Applies logic for mapping time entries to general entries structure. |
||||||
# @return [Array<String>] All fields/statements for select part |
# |
||||||
# |
# @param [CostQuery::SqlStatement] query The statement to adjust |
||||||
# @overload select(*fields) |
def self.unify_time_entries(query) |
||||||
# Adds fields to select query. |
query.select :activity_id, :units => :hours, :cost_type_id => -1 |
||||||
# @example |
query.select :cost_type => quoted_label(:caption_labor) |
||||||
# SqlStatement.new.select(some_sql_statement) # [some_sql_statement.to_s] |
|
||||||
# SqlStatement.new.select("sum(foo)") # ["sum(foo)"] |
|
||||||
# SqlStatement.new.select(:a).select(:b) # ["a", "b"] |
|
||||||
# SqlStatement.new.select(:bar => :foo) # ["foo as bar"] |
|
||||||
# SqlStatement.new.select(:bar => nil) # ["NULL as bar"] |
|
||||||
# @param [Array, Hash, String, Symbol, SqlStatement] fields Fields to add to select part |
|
||||||
# @return [Array<String>] All fields/statements for select part |
|
||||||
def select(*fields) |
|
||||||
return(@select || ["*"]) if fields.empty? |
|
||||||
returning(@select ||= []) do |
|
||||||
@sql = nil |
|
||||||
fields.each do |f| |
|
||||||
case f |
|
||||||
when Array |
|
||||||
if f.size == 2 and f.first.respond_to? :table_name then select field_name_for(f) |
|
||||||
else select(*f) |
|
||||||
end |
|
||||||
when Hash then select f.map { |k,v| "#{field_name_for v} as #{field_name_for k}" } |
|
||||||
when String, Symbol then @select << field_name_for(f) |
|
||||||
when CostQuery::SqlStatement then @select << f.to_s |
|
||||||
else raise ArgumentError, "cannot handle #{f.inspect}" |
|
||||||
end |
|
||||||
end |
|
||||||
# when doing a union in sql, both subselects must have the same order. |
|
||||||
# by sorting here we never ever have to worry about this again, sucker! |
|
||||||
@select = @select.uniq.sort_by { |x| x.split(" as ").last } |
|
||||||
end |
end |
||||||
end |
|
||||||
|
|
||||||
## |
## |
||||||
# @overload group_by |
# Applies logic for mapping cost entries to general entries structure. |
||||||
# @return [Array<String>] All fields/statements for group by part |
# |
||||||
# |
# @param [CostQuery::SqlStatement] query The statement to adjust |
||||||
# @overload group(*fields) |
def self.unify_cost_entries(query) |
||||||
# Adds fields to group by query |
query.select :units, :cost_type_id, :activity_id => -1 |
||||||
# @param [Array, String, Symbol] fields Fields to add |
query.select :cost_type => "cost_types.name" |
||||||
def group_by(*fields) |
query.join CostType |
||||||
@sql = nil unless fields.empty? |
|
||||||
returning(@group_by ||= []) do |
|
||||||
fields.each do |e| |
|
||||||
if e.is_a? Array and (e.size != 2 or !e.first.respond_to? :table_name) |
|
||||||
group_by(*e) |
|
||||||
else |
|
||||||
@group_by << field_name_for(e) |
|
||||||
end |
|
||||||
end |
|
||||||
@group_by.uniq! |
|
||||||
end |
end |
||||||
end |
|
||||||
|
|
||||||
## |
## |
||||||
# @return [TrueClass, FalseClass] Whether or not to add a group by part. |
# Generates a statement based on all entries (i.e. time entries and cost entries) mapped to the general entries structure, |
||||||
def group_by? |
# and therefore usable by filters and such. |
||||||
!group_by.empty? |
# |
||||||
end |
# @return [CostQuery::SqlStatement] Generated statement |
||||||
|
def self.for_entries |
||||||
def inspect |
new unified_entry(TimeEntry).union(unified_entry(CostEntry), "entries") |
||||||
"#<SqlStatement: #{to_s.inspect}>" |
end |
||||||
end |
|
||||||
|
|
||||||
def gsub(*args, &block) |
|
||||||
to_s.gsub(*args, &block) |
|
||||||
end |
end |
||||||
|
|
||||||
end |
end |
||||||
|
Loading…
Reference in new issue