diff --git a/Rakefile b/Rakefile index 4245ce4..c9ac2a2 100644 --- a/Rakefile +++ b/Rakefile @@ -15,6 +15,7 @@ begin gem.add_dependency 'activerecord' gem.add_dependency 'activesupport' gem.add_dependency 'json' + gem.add_dependency 'methodphitamine' end Jeweler::GemcutterTasks.new rescue LoadError diff --git a/VERSION b/VERSION index 5a5831a..3c8f915 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.7 +0.0.4.5 diff --git a/lib/smql_to_ar.rb b/lib/smql_to_ar.rb index 702c554..fdf32e9 100644 --- a/lib/smql_to_ar.rb +++ b/lib/smql_to_ar.rb @@ -43,9 +43,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 +55,49 @@ 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 def initialize path - super path: path + super :path => path end end class ConColumnError < SMQLError def initialize expected, got - super expected: expected, got: got + super :expected => expected, :got => got end end @@ -120,7 +120,6 @@ class SmqlToAR attr_accessor :model class Col - include Comparable attr_accessor :col, :as def initialize col, as = nil if col.kind_of? Col @@ -140,11 +139,12 @@ class SmqlToAR def == other other = Col.new other - @col == other.col && @as == other.as + @col = other.col && @as == other.col end def === other - @col == Col.new( other).col + other = Col.new other + @col == other.col end end @@ -152,10 +152,10 @@ class SmqlToAR @model = model @last_model = nil *@path, @col = *Array.wrap( col). - collect {|x| x.to_s.split /[.\/]/ }. + collect( &it.to_s.split( /[.\/]/)). flatten. collect( &Col.method( :new)). - reject {|x| x === :self } + reject( &it===:self) end def last_model @@ -222,8 +222,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 < refl.macro, :model => refl.klass.name, :through => refl.through_reflection.name} + when ActiveRecord::Reflection::AssociationReflection then {:macro => refl.macro, :model => refl.klass.name} else raise "Ups: #{refl.class}" end models.push refl.klass unless r.keys.include? refl.klass.name @@ -292,27 +294,8 @@ class SmqlToAR def self.reload_library lib_dir = File.dirname __FILE__ fj = lambda {|*a| File.join lib_dir, *a } - Object.send :remove_const, :SmqlToAR - load fj[ 'smql_to_ar.rb'] - load fj[ 'smql_to_ar', 'condition_types.rb'] - load fj[ 'smql_to_ar', 'query_builder.rb'] - end - - module ActiveRecordExtensions - def self.included base - base.extend ClassMethods - end - - module ClassMethods - def smql *params - SmqlToAR.to_ar self, *params - end - - def smql_protected - [] - end - end + load fj.call( 'smql_to_ar.rb') + load fj.call( 'smql_to_ar', 'condition_types.rb') + load fj.call( 'smql_to_ar', 'query_builder.rb') end end - -ActiveRecord::Base.send :include, SmqlToAR::ActiveRecordExtensions diff --git a/lib/smql_to_ar/condition_types.rb b/lib/smql_to_ar/condition_types.rb index 548cd89..639c9bf 100644 --- a/lib/smql_to_ar/condition_types.rb +++ b/lib/smql_to_ar/condition_types.rb @@ -14,9 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# 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, @@ -103,11 +100,11 @@ class SmqlToAR # Passt das Object, die Klasse instanzieren. def try_parse model, cols, op, val #p :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} + new model, cols, val if self::Operator === op and self::Expected.any?( &it === val) end def inspect - "#{self.name}( :operator=>#{self::Operator.inspect}, :expected=>#{self::Expected.inspect}, :where=>#{self::Where.inspect})" + "#{self.name}(:operator=>#{self::Operator.inspect}, :expected=>#{self::Expected.inspect}, :where=>#{self::Where.inspect})" end end @@ -153,7 +150,7 @@ class SmqlToAR # 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 } if 1 == @cols.length @@ -174,16 +171,15 @@ class SmqlToAR 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 + 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,7 +187,7 @@ class SmqlToAR super model, cols, val end - def not_in_range_build builder, table + def build builder, table builder.wobs (v1 = builder.vid).to_sym => @value.begin, (v2 = builder.vid).to_sym => @value.end @cols.each do |col| col.joins builder, table @@ -199,57 +195,15 @@ class SmqlToAR 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,7 +211,6 @@ class SmqlToAR end self end - alias build not_in_build end In = simple_condition NotIn, '|=', '%s IN (%s)', [Array] @@ -266,20 +219,13 @@ class SmqlToAR NotEqual2 = 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 ', "%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] @@ -380,7 +319,7 @@ class SmqlToAR raise_unless col.exist_in? || SmqlToAR.model_of( col.last_model, col.col), NonExistingSelectableError.new( 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 +331,6 @@ class SmqlToAR end self end - alias build select_build end class Functions < Condition @@ -407,11 +345,11 @@ class SmqlToAR class <#{self::Name}, :expected=>#{self::Expected})" + "#{self.name}(:name=>#{self::Name}, :expected=>#{self::Expected})" end end @@ -441,7 +379,7 @@ class SmqlToAR 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 @@ -451,29 +389,26 @@ class SmqlToAR 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 + def 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 + def 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 diff --git a/lib/smql_to_ar/query_builder.rb b/lib/smql_to_ar/query_builder.rb index 4e8ad3c..ded1481 100644 --- a/lib/smql_to_ar/query_builder.rb +++ b/lib/smql_to_ar/query_builder.rb @@ -32,36 +32,13 @@ class SmqlToAR end class Aliases < Hash - attr_accessor :counter, :prefix - - def initialize prefix, *a, &e - @counter, @prefix = 0, prefix || 'smql' + def self.new prefix, *a, &e + e ||= lambda do |h, k| + j = Array.wrap( k).compact + h[k] = h.key?(j) ? h[j] : "#{prefix},#{j.collect( &:to_alias).join( ',')}" + end 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 @@ -71,7 +48,7 @@ class SmqlToAR @prefix = "smql" @logger = SmqlToAR.logger @table_alias = Aliases.new @prefix - @_vid, @_where, @_wobs, @model, @quote = 0, SmqlToAR::And[], {}, model, model.connection + @_vid, @_where, @_wobs, @model, @quoter = 0, SmqlToAR::And[], {}, model, model.connection @base_table = [Column::Col.new( model.table_name)] @table_alias[ @base_table] = @base_table.first t = quote_table_name @base_table.first.col @@ -100,7 +77,7 @@ 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 @@ -108,7 +85,7 @@ class SmqlToAR 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 @@ -116,7 +93,7 @@ class SmqlToAR 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" + " LEFT JOIN #{orig} AS #{quote_table_name table} ON #{column pretable, prekey} = #{column table, key} " end def sub_joins table, col, model, query @@ -127,33 +104,32 @@ class SmqlToAR 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] + premodel = @table_model[ pretable] 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)] + throughtable = table[0...-1]+[Column::Col.new( through.name, table.last.as)] + srctable = throughtable+[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 + join_ throughtable, through.klass, quote_table_name( through.table_name) + join_ srctable, refl.klass, query, throughtable when ActiveRecord::Reflection::AssociationReflection case refl.macro when :has_many, :has_one - @_joins += build_join query, pretable, t, premodel.primary_key, refl.foreign_key + @_joins += build_join query, pretable, t, premodel.primary_key, refl.primary_key_name when :belongs_to - @_joins += build_join query, pretable, t, refl.foreign_key, premodel.primary_key + @_joins += build_join query, pretable, t, refl.primary_key_name, 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 + jointable = [','] + table + @_joins += build_join refl.options[:join_table], pretable, @table_alias[jointable], premodel.primary_key, refl.primary_key_name @_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}" + else raise BuilderError, "Unkown reflection type: #{refl.class.name}" end self end @@ -219,7 +195,7 @@ class SmqlToAR class SubBuilder < Array attr_reader :parent, :_where - delegate :wobs, :joins, :includes, :sub_joins, :vid, :quote_column_name, :quote, :quote_table_name, :column, :to => :parent + delegate :wobs, :joins, :includes, :sub_joins, :vid, :quote_column_name, :quoter, :quote_table_name, :column, :to => :parent def initialize parent, tmp = false @parent = parent @@ -248,14 +224,12 @@ class SmqlToAR def optimize! ext = [] collect! do |sub| - sub = sub.optimize! if sub.kind_of? SubBuilder + sub = sub.optimize! if sub.kind_of? Array if self.class == sub.class ext.push *sub nil elsif sub.blank? nil - elsif 1 == sub.size - sub.first else sub end @@ -269,22 +243,21 @@ class SmqlToAR 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})" } + def collect_build_where + collect {|x| x.respond_to?( :build_where) ? x.build_where : 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) + def build_where + collect_build_where.join ' AND ' end end class Or < SubBuilder - def build_where indent = nil - collect_build_where( indent).join " OR\n"+"\t"*(indent||0) + def build_where + collect_build_where.join ' OR ' end end end