init
This commit is contained in:
commit
c61dbfe31d
11 changed files with 1475 additions and 0 deletions
327
lib/smql_to_ar/condition_types.rb
Normal file
327
lib/smql_to_ar/condition_types.rb
Normal file
|
@ -0,0 +1,327 @@
|
|||
# SmqlToAR - Parser: Converts SMQL to ActiveRecord
|
||||
# Copyright (C) 2011 Denis Knauf
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class SmqlToAR
|
||||
#############################################################################
|
||||
# Alle Subklassen (qualitativ: ConditionTypes::*), die als Superklasse Condition haben,
|
||||
# stellen eine Regel dar, unter diesen sie das gesuchte Objekt annehmen.
|
||||
# Nimmt eine solche Klasse ein Object nicht an, so wird die naechste Klasse ausprobiert.
|
||||
# Es wird in der Reihenfolge abgesucht, in der #constants die Klassen liefert,
|
||||
# wobei angenommen wird, dass diese nach dem Erstellungszeitpunkt sortiert sind,
|
||||
# aeltere zuerst.
|
||||
# Nimmt eine Klasse ein Objekt an, so soll diese Klasse instanziert werden.
|
||||
# Alles weitere siehe Condition.
|
||||
module ConditionTypes
|
||||
class <<self
|
||||
# Ex: 'givenname|surname|nick' => [:givenname, :surname, :nick]
|
||||
def split_keys k
|
||||
k.split( '|').collect &:to_sym
|
||||
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 }
|
||||
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 UnexpectedError.new( model, colop, val) unless r
|
||||
r
|
||||
end
|
||||
|
||||
# Alle Regeln parsen. Die Regeln sind in einem Hash der Form {colop => val}
|
||||
# Ex: Person, {"givenname=", "Peter", "surname=", "Mueller"}
|
||||
def try_parse model, colopvals
|
||||
colopvals.collect do |colop, val|
|
||||
#p :try_parse => { colop: colop, val: val, model: model }
|
||||
try_parse_it model, colop, val
|
||||
end
|
||||
rescue SMQLError => e
|
||||
raise SubSMQLError.new( colopvals, model, e)
|
||||
end
|
||||
|
||||
# Erstellt eine Condition fuer eine Regel.
|
||||
def simple_condition superclass, op = nil, where = nil, expected = nil
|
||||
cl = Class.new superclass
|
||||
cl.const_set :Operator, op if op
|
||||
cl.const_set :Where, where if where
|
||||
cl.const_set :Expected, expected if expected
|
||||
cl
|
||||
end
|
||||
end
|
||||
|
||||
class Condition
|
||||
attr_reader :value, :cols
|
||||
Operator = nil
|
||||
Expected = []
|
||||
Where = nil
|
||||
|
||||
# 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
|
||||
@model, @cols = model, cols
|
||||
@value = case val
|
||||
when Hash, Range then val
|
||||
else Array.wrap val
|
||||
end
|
||||
verify
|
||||
end
|
||||
|
||||
def verify
|
||||
@cols.each do |col|
|
||||
verify_column col
|
||||
verify_allowed col
|
||||
end
|
||||
end
|
||||
|
||||
# Gibt es eine Spalte diesen Namens?
|
||||
# Oder: Gibt es eine Relation diesen Namens? (Hier nicht der Fall)
|
||||
def verify_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 ProtectedColumnError.new( col) if col.protected?
|
||||
end
|
||||
|
||||
# Erstelle alle noetigen Klauseln. builder nimmt diese entgegen,
|
||||
# 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 build builder, table
|
||||
values = Hash[ @value.collect {|value| [ builder.vid, value ] } ]
|
||||
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 ] }
|
||||
end
|
||||
else
|
||||
values.keys.each do |vid|
|
||||
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
|
||||
end
|
||||
|
||||
class NotInRange < Condition
|
||||
Operator = '!..'
|
||||
Where = "%s NOT BETWEEN %s AND %s"
|
||||
Expected = [Range, lambda {|val| Array === val && 2 == val.length } ]
|
||||
|
||||
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
|
||||
end
|
||||
super model, cols, val
|
||||
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
|
||||
end
|
||||
InRange = simple_condition NotInRange, '..', "%s BETWEEN %s AND %s"
|
||||
|
||||
class NotIn < Condition
|
||||
Operator = '!|='
|
||||
Where = "%s NOT IN (%s)"
|
||||
Expected = [Array]
|
||||
|
||||
def build builder, table
|
||||
builder.wobs (v = builder.vid).to_sym => @value
|
||||
@cols.each do |col|
|
||||
col.joins builder, table
|
||||
builder.where self.class::Where % [ builder.column( table, col), v.to_s]
|
||||
end
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
In = simple_condition NotIn, '|=', '%s IN (%s)', [Array]
|
||||
In2 = simple_condition In, '', nil, [Array]
|
||||
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 EqualJoin <Condition
|
||||
Operator = '='
|
||||
Expected = [Hash]
|
||||
|
||||
def initialize *pars
|
||||
super( *pars)
|
||||
cols = {}
|
||||
@cols.each do |col|
|
||||
col_model = SmqlToAR.model_of col.last_model, col.col
|
||||
#p col_model: col_model.to_s, value: @value
|
||||
cols[col] = [col_model] + ConditionTypes.try_parse( col_model, @value)
|
||||
end
|
||||
@cols = cols
|
||||
end
|
||||
|
||||
def verify_column col
|
||||
refl = SmqlToAR.model_of col.last_model, col.col
|
||||
#p refl: refl, model: @model.name, col: col, :reflections => @model.reflections.keys
|
||||
raise NonExistingRelationError.new( %w[Relation], col) unless refl
|
||||
end
|
||||
|
||||
def build builder, table
|
||||
@cols.each do |col, sub|
|
||||
t = table + col.path + [col.col]
|
||||
#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
|
||||
end
|
||||
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]
|
||||
LesserThan = simple_condition Condition, '<', "%s < %s", [Array, Numeric]
|
||||
NotIlike = simple_condition Condition, '!~', "%s NOT ILIKE %s", [Array, String]
|
||||
Ilike = simple_condition Condition, '~', "%s ILIKE %s", [Array, String]
|
||||
|
||||
####### No Operator #######
|
||||
Join = simple_condition EqualJoin, '', nil, [Hash]
|
||||
InRange2 = simple_condition InRange, '', nil, [Range]
|
||||
class Select < Condition
|
||||
Operator = ''
|
||||
Expected = [nil]
|
||||
|
||||
def verify_column col
|
||||
raise NonExistingSelectableError.new( col) unless col.exist_in? or SmqlToAR.model_of( col.last_model, col.col)
|
||||
end
|
||||
|
||||
def build builder, table
|
||||
@cols.each do |col|
|
||||
if col.exist_in?
|
||||
col.joins builder, table
|
||||
builder.select table+col.to_a
|
||||
else
|
||||
col.joins {|j, m| builder.includes table+j }
|
||||
builder.includes table+col.to_a
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Functions < Condition
|
||||
Operator = ':'
|
||||
Expected = [String, Array, Hash, Numeric, nil]
|
||||
|
||||
class Function
|
||||
Name = nil
|
||||
Expected = []
|
||||
attr_reader :model, :func, :args
|
||||
|
||||
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
|
||||
@model, @func, @args = model, func, args
|
||||
end
|
||||
end
|
||||
|
||||
class Order < Function
|
||||
Name = :order
|
||||
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 NonExistingColumnError.new( [:Column], col) unless col.exist_in?
|
||||
[col, o]
|
||||
end
|
||||
SmqlToAR.logger.info( {args: args}.inspect)
|
||||
super model, func, args
|
||||
end
|
||||
|
||||
def build builder, table
|
||||
return if @args.blank?
|
||||
@args.each do |o|
|
||||
col, o = o
|
||||
col.joins builder, table
|
||||
t = table + col.path
|
||||
raise OnlyOrderOnBaseError.new( t) unless 1 == t.length
|
||||
builder.order t, col.col, o
|
||||
end
|
||||
end
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
160
lib/smql_to_ar/query_builder.rb
Normal file
160
lib/smql_to_ar/query_builder.rb
Normal file
|
@ -0,0 +1,160 @@
|
|||
# SmqlToAR - Builds AR-querys: Converts SMQL to ActiveRecord
|
||||
# Copyright (C) 2011 Denis Knauf
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class SmqlToAR
|
||||
#######################################################################################
|
||||
# Baut die Queries zusammen.
|
||||
class QueryBuilder
|
||||
# Erzeugt einen eindeutigen Identikator "cX", wobei X iteriert wird.
|
||||
class Vid
|
||||
attr_reader :vid
|
||||
def initialize( vid) @vid = vid end
|
||||
def to_s() ":c#{@vid}" end
|
||||
def to_sym() "c#{@vid}".to_sym end
|
||||
alias sym to_sym
|
||||
def to_i() @vid end
|
||||
end
|
||||
|
||||
attr_reader :table_alias, :model, :table_model, :base_table, :_where, :_select, :_wobs, :_joins
|
||||
attr_accessor :logger
|
||||
@@logger = SmqlToAR.logger
|
||||
|
||||
def initialize model
|
||||
@logger = @@logger
|
||||
@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 @table_alias[ @base_table]
|
||||
@_select, @_joins, @_joined, @_includes, @_order = ["DISTINCT #{t}.*"], "", [], [], []
|
||||
@table_model = {@base_table => @model}
|
||||
end
|
||||
|
||||
def vid() Vid.new( @_vid+=1) 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')
|
||||
# #=> 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
|
||||
@_where.push cond
|
||||
self
|
||||
end
|
||||
|
||||
def wobs vals
|
||||
@_wobs.update vals
|
||||
self
|
||||
end
|
||||
|
||||
def quote_column_name name
|
||||
@quoter.quote_column_name( name).gsub /"\."/, ','
|
||||
end
|
||||
|
||||
def quote_table_name name
|
||||
@quoter.quote_table_name( name).gsub /"\."/, ','
|
||||
end
|
||||
|
||||
def column table, 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
|
||||
" JOIN #{quote_table_name orig.to_sym} AS #{quote_table_name table} ON #{column pretable, prekey} = #{column table, key} "
|
||||
end
|
||||
|
||||
def join table, model
|
||||
return self if @_joined.include? table # Already joined
|
||||
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
|
||||
@_includes.push table
|
||||
self
|
||||
end
|
||||
|
||||
def select col
|
||||
@_select.push quote_column_name( @table_alias[col])
|
||||
self
|
||||
end
|
||||
|
||||
def order table, col, o
|
||||
@_order.push "#{column table, col} #{:DESC == o ? :DESC : :ASC}"
|
||||
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.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)
|
||||
end
|
||||
|
||||
def fix_calculate
|
||||
def @model.calculate operation, column_name, options = nil
|
||||
options = options.try(:dup) || {}
|
||||
options[:distinct] = true unless options.except(:distinct).present?
|
||||
column_name = klass.primary_key unless column_name.present?
|
||||
super operation, column_name, options
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def to_ar
|
||||
build_ar
|
||||
fix_calculate
|
||||
@model
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue