diff --git a/.gitignore b/.gitignore index 1b861d7..73633ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ pkg/** -.*.swp smql.gemspec diff --git a/TODO b/TODO index e2a2eb8..28c5c5c 100644 --- a/TODO +++ b/TODO @@ -1,3 +1 @@ 1) gem-plugin -2) deprecated-warnings rails 3.1: - * DEPRECATION WARNING: primary_key_name is deprecated and will be removed from Rails 3.2 (use foreign_key instead). diff --git a/VERSION b/VERSION index 5a5831a..4e379d2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.7 +0.0.2 diff --git a/lib/smql_to_ar.rb b/lib/smql_to_ar.rb index 702c554..63e1a10 100644 --- a/lib/smql_to_ar.rb +++ b/lib/smql_to_ar.rb @@ -15,18 +15,6 @@ # along with this program. If not, see . class SmqlToAR - module Assertion - def raise_unless cond, exception = nil, *args - cond, exception, *args = yield. cond, exception, *args if block_given? - raise exception || Exception, *args unless cond - end - - def raise_if cond, exception = nil, *args - cond, exception, *args = yield. cond, exception, *args if block_given? - raise exception || Exception, *args if cond - end - end - include ActiveSupport::Benchmarkable ############################################################################r # Exceptions @@ -43,9 +31,9 @@ class SmqlToAR # Die eigentliche Exception wird in @data[:exception] hinterlegt. class SubSMQLError < SMQLError def initialize query, model, exception - ex = {class: exception.class, message: exception.message} + ex = {:class => exception.class, :message => exception.message} ex[:data] = exception.data if exception.respond_to? :data - super query: query, model: model.to_s, exception: ex + super :query => query, :model => model.to_s, :exception => ex set_backtrace exception.backtrace end end @@ -55,49 +43,43 @@ class SmqlToAR # Malformed ColOp class UnexpectedColOpError < ParseError def initialize model, colop, val - super got: colop, val: val, model: model.to_s + super :got => colop, :val => val, :model => model.to_s end end class UnexpectedError < ParseError def initialize model, colop, val - super got: {colop => val}, model: model.to_s + super :got => {colop => val}, :model => model.to_s end end class NonExistingSelectableError < SMQLError def initialize got - super got: got + super :got => got end end class NonExistingColumnError < SMQLError def initialize expected, got - super expected: expected, got: got + super :expected => expected, :got => got end end class NonExistingRelationError < SMQLError def initialize expected, got - super expected: expected, got: got + super :expected => expected, :got => got end end class ProtectedColumnError < SMQLError def initialize protected_column - super protected_column: protected_column + super :protected_column => protected_column end end - class RootOnlyFunctionError < SMQLError + class OnlyOrderOnBaseError < SMQLError def initialize path - super path: path - end - end - - class ConColumnError < SMQLError - def initialize expected, got - super expected: expected, got: got + super :path => path end end @@ -107,8 +89,7 @@ class SmqlToAR # Model der Relation `rel` von `model` def self.model_of model, rel - r = model.reflections[ rel.to_sym].andand.klass - r.nil? && rel === :self ? model : r + model.reflections[ rel.to_sym].andand.klass end # Eine Spalte in einer Tabelle, relativ zu `Column#model`. @@ -119,43 +100,10 @@ class SmqlToAR attr_reader :path, :col attr_accessor :model - class Col - include Comparable - attr_accessor :col, :as - def initialize col, as = nil - if col.kind_of? Col - @col, @as = col.col, col.as - elsif /^(.*?)\[(.*)\]$/ =~ col.to_s - @col, @as = $1, $2 - else - @col, @as = col.to_s, as.to_s - @as = nil if @as.blank? - end - end - - def to_s() @col end - def to_sym() to_s.to_sym end - def inspect() "#{@col}[@#{@as}]" end - def to_alias() "#{@col}[#{@as}]" end - - def == other - other = Col.new other - @col == other.col && @as == other.as - end - - def === other - @col == Col.new( other).col - end - end - def initialize model, *col @model = model @last_model = nil - *@path, @col = *Array.wrap( col). - collect {|x| x.to_s.split /[.\/]/ }. - flatten. - collect( &Col.method( :new)). - reject {|x| x === :self } + *@path, @col = Array.wrap( col).collect {|s| s.to_s.split /[.\/]/ }.flatten.collect &:to_sym end def last_model @@ -165,11 +113,9 @@ class SmqlToAR def each model = @model @path.each do |rel| - unless rel === :self - model = SmqlToAR.model_of model, rel - return false unless model - yield rel, model - end + model = SmqlToAR.model_of model, rel + return false unless model + yield rel, model end model end @@ -194,24 +140,19 @@ class SmqlToAR def joins builder = nil, table = nil, &exe pp = [] table = Array.wrap table - exe ||= builder ? lambda {|j, m| builder.joins table+j, m} : Array.method( :[]) + exe ||= builder ? lambda {|j, m| builder.join table+j, m} : Array.method( :[]) collect do |rel, model| pp.push rel exe.call pp, model end end - - def self?() !@col end - def length() @path.length+(self.self? ? 0 : 1) end - def size() @path.size+(self.self? ? 0 : 1) end - def to_a() @path+(self.self? ? [] : [@col]) end + def to_a() @path+[@col] end def to_s() to_a.join '.' end def to_sym() to_s.to_sym end def to_json() to_s end def inspect() "#" end - def relation() self.self? ? model : SmqlToAR.model_of( last_model, @col) end + def relation() SmqlToAR.model_of last_model, @col end def allowed?() ! self.protected? end - def child?() @path.empty? and !!relation end end attr_reader :model, :query, :conditions, :builder, :order @@ -222,8 +163,10 @@ class SmqlToAR SmqlToAR.logger = ::Rails.logger end end + else + require 'logger' + @@logger = Logger.new $stdout end - @@logger = ::Rails.logger || begin require 'logger'; Logger.new( $stdout) end class <. -# TODO: -# * Array als Typ ist zu allgemein. Allgemeine Lösung gesucht, um die Typen im Array angeben zu können. - class SmqlToAR ############################################################################# # Alle Subklassen (qualitativ: ConditionTypes::*), die als Superklasse Condition haben, @@ -28,44 +25,28 @@ class SmqlToAR # Nimmt eine Klasse ein Objekt an, so soll diese Klasse instanziert werden. # Alles weitere siehe Condition. module ConditionTypes - extend SmqlToAR::Assertion - class < [:givenname, :surname, :nick] def split_keys k k.split( '|').collect &:to_sym end - def conditions &e - unless block_given? - r = Enumerator.new( self, :conditions) - s = self - r.define_singleton_method :[] do |k| - s.conditions.select {|c| c::Operator === k } - end - return r - end - constants.each do |c| - next if :Condition == c - c = const_get c - next if Condition === c - yield c - end - end - # Eine Regel parsen. # Ex: Person, "givenname=", "Peter" def try_parse_it model, colop, val r = nil #p :try_parse => { :model => model, :colop => colop, :value => val } - conditions.each do |c| - raise_unless colop =~ /^(?:\d*:)?(.*?)((?:\W*(?!\])\W)?)$/, UnexpectedColOpError.new( model, colop, val) + constants.each do |c| + next if :Condition == c + c = const_get c + next if Condition === c + raise UnexpectedColOpError.new( model, colop, val) unless colop =~ /^(?:\d*:)?(.*?)(\W*)$/ col, op = $1, $2 col = split_keys( col).collect {|c| Column.new model, c } r = c.try_parse model, col, op, val break if r end - raise_unless r, UnexpectedError.new( model, colop, val) + raise UnexpectedError.new( model, colop, val) unless r r end @@ -91,28 +72,19 @@ class SmqlToAR end class Condition - include SmqlToAR::Assertion - extend SmqlToAR::Assertion attr_reader :value, :cols Operator = nil Expected = [] Where = nil - class < self, :self => name, :try_parse => op, :cols => cols, :with => self::Operator, :value => val, :expected => self::Expected, :model => model.name - new model, cols, val if self::Operator === op and self::Expected.any? {|x| x === val} - end - - def inspect - "#{self.name}( :operator=>#{self::Operator.inspect}, :expected=>#{self::Expected.inspect}, :where=>#{self::Where.inspect})" - end + # Versuche das Objekt zu erkennen. Operator und Expected muessen passen. + # Passt das Object, die Klasse instanzieren. + def self.try_parse model, cols, op, val + #p :self => name, :try_parse => op, :cols => cols, :with => self::Operator, :value => val, :expected => self::Expected, :model => model.name + new model, cols, val if self::Operator === op and self::Expected.any? {|i| i === val } end def initialize model, cols, val - #p init: self, caller: caller @model, @cols = model, cols @value = case val when Hash, Range then val @@ -121,10 +93,6 @@ class SmqlToAR verify end - def inspect - "#<#{self.class.name}:0x#{(self.object_id<<1).to_s 16} model: #{self.class.name}, cols: #{@cols.inspect}, value: #{@value.inspect}>" - end - def verify @cols.each do |col| verify_column col @@ -135,55 +103,53 @@ class SmqlToAR # Gibt es eine Spalte diesen Namens? # Oder: Gibt es eine Relation diesen Namens? (Hier nicht der Fall) def verify_column col - raise_unless col.exist_in?, NonExistingColumnError.new( %w[Column], col) + raise NonExistingColumnError.new( %w[Column], col) unless col.exist_in? end # Modelle koennen Spalten/Relationen verbieten mit Model#smql_protected. # Dieses muss ein Object mit #include?( name_als_string) zurueckliefern, # welches true fuer verboten und false fuer, erlaubt steht. def verify_allowed col - raise_if col.protected?, ProtectedColumnError.new( col) + raise ProtectedColumnError.new( col) if col.protected? end # Erstelle alle noetigen Klauseln. builder nimmt diese entgegen, - # wobei builder.joins, builder.select, builder.where und builder.wobs von interesse sind. - # mehrere Schluessel bedeuten, dass die Values _alle_ zutreffen muessen, wobei die Schluessel geODERt werden. + # wobei builder.join, builder.select, builder.where und builder.wobs von interesse sind. + # mehrere Schluessel bedeuten, dass die Values _alle_ zutreffen muessen, wobei die Schluessel geodert werden. # Ex: # 1) {"givenname=", "Peter"} #=> givenname = 'Peter' # 2) {"givenname=", ["Peter", "Hans"]} #=> ( givenname = 'Peter' OR givenname = 'Hans' ) # 3) {"givenname|surname=", ["Peter", "Mueller"]} # #=> ( givenname = 'Peter' OR surname = 'Peter' ) AND ( givenname = 'Mueller' OR surname = 'Mueller' ) - def condition_build builder, table + def build builder, table values = Hash[ @value.collect {|value| [ builder.vid, value ] } ] - values.each {|k, v| builder.wobs k.to_sym => v } + values.each {|k, v| builder.wobs k.sym => v } if 1 == @cols.length @cols.each do |col| col.joins builder, table col = builder.column table+col.path, col.col - builder.where values.keys.collect {|vid| self.class::Where % [ col, vid.to_s ] } + builder.where *values.keys.collect {|vid| self.class::Where % [ col, vid.to_s ] } end else - b2 = SmqlToAR::And.new builder values.keys.each do |vid| - b2.where SmqlToAR::Or[ *@cols.collect {|col| + builder.where *@cols.collect {|col| col.joins builder, table col = builder.column table+col.path, col.col self.class::Where % [ col, vid.to_s ] - }] + } end end self end - alias build condition_build end class NotInRange < Condition Operator = '!..' - Where = '%s NOT BETWEEN %s AND %s' + Where = "%s NOT BETWEEN %s AND %s" Expected = [Range, lambda {|val| Array === val && 2 == val.length } ] - def initialize model, cols, val - if Array === val + def initialze model, cols, val + if Array === val && 2 == val.length f, l = val f, l = Time.parse(f), Time.parse(l) if f.kind_of? String val = f..l @@ -191,65 +157,23 @@ class SmqlToAR super model, cols, val end - def not_in_range_build builder, table - builder.wobs (v1 = builder.vid).to_sym => @value.begin, (v2 = builder.vid).to_sym => @value.end + def build builder, table + builder.wobs (v1 = builder.vid) => @value.begin, (v2 = builder.vid) => @value.end @cols.each do |col| col.joins builder, table builder.where self.class::Where % [ builder.column( table+col.path, col.col), v1, v2] end self end - alias build not_in_range_build end - InRange = simple_condition NotInRange, '..', '%s BETWEEN %s AND %s' - - # Every key-pair will be ORed. No multiple values possible. - class Overlaps < Condition - Operator, Where = '<=>', '(%s, %s) OVERLAPS (%s, %s)' - Expected = [Range, lambda {|val| - Array === val && 2 == val.length && - [Time, Date, String].any? {|v|v===val[0]} && - [Numeric, String].any? {|v|v===val[1]} - }] - - def initialize model, cols, val - if Array === val - f = Time.parse( val[0]).localtime - l = val[1] - l = case l - when String then Time.parse( l).localtime - when Numeric then f+l - else raise ArgumentError, "Unexpected type for end-value #{l.inspect}" - end - f += f.utc_offset - l += l.utc_offset - val = f.utc..l.utc - end - super model, cols, val - end - - def overlaps_build builder, table - builder = Or.new builder - builder.wobs (v1 = builder.vid).to_sym => @value.begin, (v2 = builder.vid).to_sym => @value.end - v1 = "TIMESTAMP #{v1}" - v2 = "TIMESTAMP #{v2}" - @cols.each do |col| - col.joins builder, table - end.each_slice 2 do |f,s| - builder.where self.class::Where % [ - builder.column( table+f.path, f.col), builder.column( table+s.path, s.col), v1, v2] - end - end - alias build overlaps_build - end - NotOverlaps = simple_condition Overlaps, '<=>', 'NOT (%s, %s) OVERLAPS (%s, %s)' + InRange = simple_condition NotInRange, '..', "%s BETWEEN %s AND %s" class NotIn < Condition Operator = '!|=' Where = "%s NOT IN (%s)" Expected = [Array] - def not_in_build builder, table + def build builder, table builder.wobs (v = builder.vid).to_sym => @value @cols.each do |col| col.joins builder, table @@ -257,119 +181,54 @@ class SmqlToAR end self end - alias build not_in_build end In = simple_condition NotIn, '|=', '%s IN (%s)', [Array] In2 = simple_condition In, '', nil, [Array] - NotEqual = simple_condition Condition, '!=', "%s <> %s", [Array, String, Numeric] - NotEqual2 = simple_condition Condition, '<>', "%s <> %s", [Array, String, Numeric] + NotEqual = simple_condition Condition, /\!=|<>/, "%s <> %s", [Array, String, Numeric] GreaterThanOrEqual = simple_condition Condition, '>=', "%s >= %s", [Array, Numeric] LesserThanOrEqual = simple_condition Condition, '<=', "%s <= %s", [Array, Numeric] - class StringTimeGreaterThanOrEqual < Condition - Operator, Where, Expected = '>=', '%s >= %s', [Time, Date, String] - def initialize model, cols, val - super model, cols, Time.parse( val.to_s) - end - end - StringTimeLesserThanOrEqual = simple_condition StringTimeGreaterThanOrEqual, '<=', "%s <= %s" - - # Examples: - # { 'articles=>' => { id: 1 } } - # { 'articles=>' => [ { id: 1 }, { id: 2 } ] } class EqualJoin @model.reflections.keys + raise NonExistingRelationError.new( %w[Relation], col) unless refl end - def equal_join_build builder, table - if 2 < @cols.first.second.length - b2, b3 = And, Or - else - b2, b3 = Or, And - end - b2 = b2.new builder + def build builder, table @cols.each do |col, sub| - model, *sub = sub t = table + col.path + [col.col] - col.joins builder, table - builder.joins t, model - b4 = b3.new( b2) - sub.each do |i| - b5 = And.new b4 - i.collect {|j| j.build b5, t } - end + #p sub: sub + p col: col, joins: col.joins + col.joins.each {|j, m| builder.join table+j, m } + builder.join t, SmqlToAR.model_of( col.last_model, col.col) + sub[1..-1].each {|one| one.build builder, t } end self end - alias build equal_join_build end - - # Takes to Queries. - # First Query will be a Subquery, second a regular query. - # Example: - # Person.smql 'sub.articles:' => [{'limit:' => 1, 'order:': 'updated_at desc'}, {'content~' => 'some text'}] - # Person must have as last Article (compared by updated_at) owned by Person a Artive which has 'some text' in content. - # The last Article needn't to have 'some text' has content, the subquery takes it anyway. - # But the second query compares to it and never to any other Article, because these are filtered by first query. - # The difference to - # Person.smql :articles => {'content~' => 'some text', 'limit:' => 1, 'order:': 'updated_at desc'} - # is, second is not allowed (limit and order must be in root) and this means something like - # "Person must have the Article owned by Person which has 'some text' in content. - # limit and order has no function in this query and this article needn't to be the last." -=begin - class SubEqualJoin < EqualJoin - Operator = '()' - Expected = [lambda {|x| x.kind_of?( Array) and (1..2).include?( x.length) and x.all?( &it.kind_of?( Hash))}] - - def initialize model, cols, val - super model, cols, val[1] - # sub: model, subquery, sub(condition) - @cols.each {|col, sub| sub[ 1..-1] = SmqlToAR.new( col.relation, val[0]).parse, *sub[-1] } - end - - def verify_column col - raise_unless col.child?, ConColumnError.new( [:Column], col) - end - - def sub_equal_join_build builder, table - @cols.each do |col, sub| - t = table+col.to_a - builder.sub_joins t, col, *sub[0..1] - #ap sub: sub[2..-1] - sub[2..-1].each {|x| x.build builder, t } - end - self - end - alias build sub_equal_join_build - end -=end - - Equal = simple_condition Condition, '=', "%s = %s", [Array, String, Numeric, Date, Time] - Equal2 = simple_condition Equal, '', "%s = %s", [String, Numeric, Date, Time] + Equal = simple_condition Condition, '=', "%s = %s", [Array, String, Numeric] + Equal2 = simple_condition Equal, '', "%s = %s", [String, Numeric] GreaterThan = simple_condition Condition, '>', "%s > %s", [Array, Numeric] - StringTimeGreaterThan = simple_condition StringTimeGreaterThanOrEqual, '>', "%s > %s" LesserThan = simple_condition Condition, '<', "%s < %s", [Array, Numeric] - StringTimeLesserThan = simple_condition StringTimeGreaterThanOrEqual, '<', "%s < %s" NotIlike = simple_condition Condition, '!~', "%s NOT ILIKE %s", [Array, String] Ilike = simple_condition Condition, '~', "%s ILIKE %s", [Array, String] - Exists = simple_condition Condition, '', '%s IS NOT NULL', [TrueClass] - NotExists = simple_condition Condition, '', '%s IS NULL', [FalseClass] + ####### No Operator ####### Join = simple_condition EqualJoin, '', nil, [Hash] InRange2 = simple_condition InRange, '', nil, [Range] class Select < Condition @@ -377,10 +236,10 @@ class SmqlToAR Expected = [nil] def verify_column col - raise_unless col.exist_in? || SmqlToAR.model_of( col.last_model, col.col), NonExistingSelectableError.new( col) + raise NonExistingSelectableError.new( col) unless col.exist_in? or SmqlToAR.model_of( col.last_model, col.col) end - def select_build builder, table + def build builder, table @cols.each do |col| if col.exist_in? col.joins builder, table @@ -392,7 +251,6 @@ class SmqlToAR end self end - alias build select_build end class Functions < Condition @@ -400,19 +258,13 @@ class SmqlToAR Expected = [String, Array, Hash, Numeric, nil] class Function - include SmqlToAR::Assertion Name = nil Expected = [] attr_reader :model, :func, :args - class <#{self::Name}, :expected=>#{self::Expected})" - end + def self.try_parse model, func, args + SmqlToAR.logger.info( { try_parse: [func,args]}.inspect) + self.new model, func, args if self::Name === func and self::Expected.any? {|e| e === args } end def initialize model, func, args @@ -425,64 +277,47 @@ class SmqlToAR Expected = [String, Array, Hash, nil] def initialize model, func, args + SmqlToAR.logger.info( {args: args}.inspect) args = case args when String then [args] when Array, Hash then args.to_a when nil then nil else raise 'Oops' end + SmqlToAR.logger.info( {args: args}.inspect) args.andand.collect! do |o| o = Array.wrap o col = Column.new model, o.first o = 'desc' == o.last.to_s.downcase ? :DESC : :ASC - raise_unless col.exist_in?, NonExistingColumnError.new( [:Column], col) + raise NonExistingColumnError.new( [:Column], col) unless col.exist_in? [col, o] end + SmqlToAR.logger.info( {args: args}.inspect) super model, func, args end - def order_build builder, table + def build builder, table return if @args.blank? @args.each do |o| col, o = o col.joins builder, table t = table + col.path - #raise_unless 1 == t.length, RootOnlyFunctionError.new( t) + #raise OnlyOrderOnBaseError.new( t) unless 1 == t.length builder.order t, col.col, o end end - alias build order_build - end - - class Limit < Function - Name = :limit - Expected = [Fixnum] - - def limit_build builder, table - raise_unless 1 == table.length, RootOnlyFunctionError.new( table) - builder.limit = Array.wrap(@args).first.to_i - end - alias build limit_build - end - - class Offset < Function - Name = :offset - Expected = [Fixnum] - - def offset_build builder, table - raise_unless 1 == table.length, RootOnlyFunctionError.new( table) - builder.offset = Array.wrap(@args).first.to_i - end - alias build offset_build end def self.new model, col, val + SmqlToAR.logger.info( { function: col.first.to_sym }.inspect) r = nil constants.each do |c| next if [:Function, :Where, :Expected, :Operator].include? c c = const_get c next if Function === c or not c.respond_to?( :try_parse) + SmqlToAR.logger.info( {f: c}.inspect) r = c.try_parse model, col.first.to_sym, val + SmqlToAR.logger.info( {r: r}.inspect) break if r end r diff --git a/lib/smql_to_ar/query_builder.rb b/lib/smql_to_ar/query_builder.rb index 4e8ad3c..0eb06a5 100644 --- a/lib/smql_to_ar/query_builder.rb +++ b/lib/smql_to_ar/query_builder.rb @@ -22,74 +22,36 @@ class SmqlToAR class Vid attr_reader :vid def initialize( vid) @vid = vid end - def to_s() ":smql_c#{@vid}" end - def to_sym() "smql_c#{@vid}".to_sym end + def to_s() ":c#{@vid}" end + def to_sym() "c#{@vid}".to_sym end alias sym to_sym def to_i() @vid end - def === other - to_s === other || to_sym === other || to_i === other || self == other || self === other - end end - class Aliases < Hash - attr_accessor :counter, :prefix + attr_reader :table_alias, :model, :table_model, :base_table, :_where, :_select, :_wobs, :_joins + attr_accessor :logger - def initialize prefix, *a, &e - @counter, @prefix = 0, prefix || 'smql' - super *a, &e - end - - def format name - pre, suf = name.split( ',', 2) - return name unless suf - pre += ",#{@counter += 1}," - l = 60-pre.length - n = suf[(suf.length<=l ? 0 : -l)..-1] - n == suf ? pre+n : "#{pre},,,#{n}" - end - - def name n - n.collect( &:to_alias).join ',' - end - - def [] k - n = name k - v = super n - v = self[k] = format( "#{prefix},#{n}") unless v - v - end - - def []= k, v - super name( k), v - end - end - - attr_reader :table_alias, :model, :table_model, :base_table, :_where, :_select, :_wobs, :_joins, :prefix, :_vid - attr_accessor :logger, :limit, :offset - - def initialize model, prefix = nil - @prefix = "smql" + def initialize model @logger = SmqlToAR.logger - @table_alias = Aliases.new @prefix - @_vid, @_where, @_wobs, @model, @quote = 0, SmqlToAR::And[], {}, model, model.connection - @base_table = [Column::Col.new( model.table_name)] + @table_alias = Hash.new do |h, k| + k = Array.wrap k + h[k] = "smql,#{k.join(',')}" + end + @_vid, @_where, @_wobs, @model, @quoter = 0, [], {}, model, model.connection + @base_table = [model.table_name.to_sym] @table_alias[ @base_table] = @base_table.first - t = quote_table_name @base_table.first.col - @_select, @_joins, @_joined, @_includes, @_order = ["DISTINCT #{t}.*"], "", [@base_table], [], [] + t = quote_table_name @table_alias[ @base_table] + @_select, @_joins, @_joined, @_includes, @_order = ["DISTINCT #{t}.*"], "", [], [], [] @table_model = {@base_table => @model} end def vid() Vid.new( @_vid+=1) end - def inspect - "#<#{self.class.name}:#{"0x%x"% (self.object_id<<1)}|#{@prefix}:#{@base_table}:#{@model} vid=#{@_vid} where=#{@_where} wobs=#{@_wobs} select=#{@_select} aliases=#{@table_alias}>" - end - # Jede via where uebergebene Condition wird geodert und alle zusammen werden geundet. # "Konjunktive Normalform". Allerdings duerfen Conditions auch Komplexe Abfragen enthalten. - # Ex: builder.where( ['a = a', 'b = c']).where( ['c = d', 'e = e']).where( 'x = y').where( ['( m = n AND o = p )', 'f = g']) + # Ex: builder.where( 'a = a', 'b = c').where( 'c = d', 'e = e').where( 'x = y').where( '( m = n AND o = p )', 'f = g') # #=> WHERE ( a = a OR b = c ) AND ( c = d OR e = e ) AND x = y ( ( m = n AND o = p ) OR f = g ) - def where cond + def where *cond @_where.push cond self end @@ -100,69 +62,42 @@ class SmqlToAR end def quote_column_name name - @quote.quote_column_name( name).gsub /"\."/, ',' + @quoter.quote_column_name( name).gsub /"\."/, ',' end def quote_table_name name - name = case name - when Array, Column::Col then @table_alias[Array.wrap name] - else name.to_s - end - @quote.quote_table_name name + @quoter.quote_table_name( name).gsub /"\."/, ',' end def column table, name - "#{quote_table_name table}.#{quote_column_name name}" + "#{quote_table_name table.kind_of?(String) ? table : @table_alias[table]}.#{quote_column_name name}" end def build_join orig, pretable, table, prekey, key - "\tLEFT JOIN #{orig} AS #{quote_table_name table}\n\tON #{column pretable, prekey} = #{column table, key}\n" + " JOIN #{quote_table_name orig.to_sym} AS #{quote_table_name table} ON #{column pretable, prekey} = #{column table, key} " end - def sub_joins table, col, model, query - prefix, base_table = "#{@prefix}_sub", col.relation.table_name - join_ table, model, "(#{query.build( prefix).ar.to_sql})" - end - - def join_ table, model, query, pretable = nil - pretable ||= table[0...-1] - @table_model[ table] = model - @table_model.rehash - premodel = @table_model.find {|k,v| pretable == k }[1] - t = @table_alias[ table] - pt = quote_table_name table[ 0...-1] - refl = premodel.reflections[table.last.to_sym] - case refl - when ActiveRecord::Reflection::ThroughReflection - through = refl.through_reflection - through_table = table[0...-1]+[Column::Col.new( through.name, table.last.as)] - srctable = through_table+[Column::Col.new( refl.source_reflection.name, table.last.as)] - @table_model[ srctable] = model - @table_alias[ table] = @table_alias[ srctable] - join_ through_table, through.klass, quote_table_name( through.table_name) - join_ srctable, refl.klass, query, through_table - when ActiveRecord::Reflection::AssociationReflection - case refl.macro - when :has_many, :has_one - @_joins += build_join query, pretable, t, premodel.primary_key, refl.foreign_key - when :belongs_to - @_joins += build_join query, pretable, t, refl.foreign_key, premodel.primary_key - when :has_and_belongs_to_many - jointable = [Column::Col.new(',')] + table - @_joins += build_join refl.options[:join_table], pretable, @table_alias[ jointable], premodel.primary_key, refl.foreign_key - @_joins += build_join query, jointable, t, refl.association_foreign_key, refl.association_primary_key - else raise BuilderError, "Unkown reflection macro: #{refl.macro.inspect}" - end - else raise BuilderError, "Unkown reflection type: #{refl.class.name} #{refl.macro.inspect}" - end - self - end - - def joins table, model - table = table.flatten.compact + def join table, model return self if @_joined.include? table # Already joined - join_ table, model, quote_table_name( model.table_name) + pretable = table[0...-1] + @table_model[ table] = model + premodel = @table_model[ pretable] + t = @table_alias[ table] + pt = quote_table_name @table_alias[ table[ 0...-1]] + refl = premodel.reflections[table.last] + case refl.macro + when :has_many + @_joins += build_join model.table_name, pretable, t, premodel.primary_key, refl.primary_key_name + when :belongs_to + @_joins += build_join model.table_name, pretable, t, refl.primary_key_name, premodel.primary_key + when :has_and_belongs_to_many + jointable = [','] + table + @_joins += build_join refl.options[:join_table], pretable, @table_alias[jointable], premodel.primary_key, refl.primary_key_name + @_joins += build_join model.table_name, jointable, t, refl.association_foreign_key, refl.association_primary_key + else raise BuilderError, "Unkown reflection macro: #{refl.macro.inspect}" + end @_joined.push table + self end def includes table @@ -176,28 +111,36 @@ class SmqlToAR end def order table, col, o - ct = column table, col + tc = column table, col @_select.push ct @_order.push "#{ct} #{:DESC == o ? :DESC : :ASC}" self end + class Dummy + def method_missing m, *a, &e + #p :dummy => m, :pars => a, :block => e + self + end + end + def build_ar - where_str = @_where.type_correction!.optimize!.build_where + where_str = @_where.collect do |w| + w = Array.wrap w + 1 == w.length ? w.first : "( #{w.join( ' OR ')} )" + end.join ' AND ' incls = {} @_includes.each do |inc| b = incls inc[1..-1].collect {|rel| b = b[rel] ||= {} } end + @logger.debug incls: incls, joins: @_joins @model = @model. select( @_select.join( ', ')). joins( @_joins). where( where_str, @_wobs). order( @_order.join( ', ')). includes( incls) - @model = @model.limit @limit if @limit - @model = @model.offset @offset if @offset - @model end def fix_calculate @@ -216,75 +159,4 @@ class SmqlToAR @model end end - - class SubBuilder < Array - attr_reader :parent, :_where - delegate :wobs, :joins, :includes, :sub_joins, :vid, :quote_column_name, :quote, :quote_table_name, :column, :to => :parent - - def initialize parent, tmp = false - @parent = parent - @parent.where self unless @parend.nil? && tmp - end - - def new parent, tmp = false - super parent, tmp - #return parent if self.class == parent.class - #super parent - end - - alias where push - - def type_correction! - collect! do |sub| - if sub.kind_of? Array - sub = default[ *sub] unless sub.respond_to?( :type_correction!) - sub.type_correction! - end - sub - end - self - end - - def optimize! - ext = [] - collect! do |sub| - sub = sub.optimize! if sub.kind_of? SubBuilder - if self.class == sub.class - ext.push *sub - nil - elsif sub.blank? - nil - elsif 1 == sub.size - sub.first - else - sub - end - end.compact! - push *ext - self - end - - def inspect - "#{self.class.name.sub( /.*::/, '')}[ #{collect(&:inspect).join ', '}]" - end - def default() SmqlToAR::And end - def default_new( parent) default.new self, parent, false end - def collect_build_where indent = nil - indent = (indent||0) + 1 - collect {|x| "(#{x.respond_to?( :build_where) ? x.build_where( indent) : x.to_s})" } - end - end - - class And < SubBuilder - def default; SmqlToAR::Or; end - def build_where indent = nil - collect_build_where( indent).join " AND\n"+"\t"*(indent||0) - end - end - - class Or < SubBuilder - def build_where indent = nil - collect_build_where( indent).join " OR\n"+"\t"*(indent||0) - end - end end