Skip to content

Commit

Permalink
Added query_parse, enabled setting relations with sqless.
Browse files Browse the repository at this point in the history
  • Loading branch information
gaspard committed Dec 8, 2010
1 parent 9a510c1 commit 09169b7
Show file tree
Hide file tree
Showing 17 changed files with 255 additions and 49 deletions.
2 changes: 2 additions & 0 deletions History.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Using VirtualClass as proxy for type compilation (RubyLess, Zafu).
* Caching Roles and VirtualClass.
* Added 'integer' property with index.
* Added query_parse (parse form content such as '>34' or '10..34').
* Enable setting relations with sqless.

== 1.0.0.beta4

Expand Down
1 change: 1 addition & 0 deletions app/controllers/nodes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ def export
end

def update
params['node'] ||= {}
file, file_error = get_attachment
params['node']['file'] = file if file

Expand Down
41 changes: 37 additions & 4 deletions app/models/relation_proxy.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class RelationProxy < Relation
attr_accessor :side, :link_errors, :start, :other_link, :last_target
attr_accessor :side, :link_errors, :start, :other_link, :last_target, :add_links
LINK_ATTRIBUTES = Zena::Use::Relations::LINK_ATTRIBUTES
LINK_ATTRIBUTES_SQL = LINK_ATTRIBUTES.map {|sym| connection.quote_column_name(sym)}.join(',')
LINK_SELECT = "nodes.*,links.id AS link_id,#{LINK_ATTRIBUTES.map {|l| "links.#{l} AS l_#{l}"}.join(',')}"
Expand Down Expand Up @@ -120,6 +120,23 @@ def other_vclass
VirtualClass.find_by_kpath(@side == :source ? self[:target_kpath] : self[:source_kpath])
end

# set from query builder
def qb=(qb)
if qb.blank?
self.other_id = []
else
query = @start.class.build_query(:all, qb,
:node_name => '@start',
:main_class => @start.virtual_class,
:rubyless_helper => @start.virtual_class,
:default => {:order => 'id asc'}
)
self.other_id = secure(Node) {Node.find_by_sql(eval(query.to_s))} || []
end
rescue ::QueryBuilder::Error => err
attributes_to_update[:errors] = {'base' => err.message}
end

# set
def other_id=(v)
attributes_to_update[:errors] = {}
Expand All @@ -131,8 +148,18 @@ def other_id=(v)
else
# ignore
end
elsif v.kind_of?(Array)
if v.first.kind_of?(Node)
node_by_id = attributes_to_update[:nodes] = {}
v.each do |r|
node_by_id[r.id.to_i] = r
end
attributes_to_update[:id] = v.map{|r| r.id.to_i}
else
attributes_to_update[:id] = v.map(&:to_i)
end
else
attributes_to_update[:id] = v.kind_of?(Array) ? v.uniq.compact.map {|v| v.to_i} : (v.blank? ? nil : v.to_i)
attributes_to_update[:id] = v.blank? ? nil : v.to_i
end
end

Expand Down Expand Up @@ -332,6 +359,8 @@ def attributes_to_update_valid?
@add_links.each do |hash|
# last_target is used by "linked_node" from Node to get hold of the last linked node
if @last_target = find_target(hash[:id])
# store remote node so that we can use in index rebuild (scope_index)
hash[:node] = @last_target
# make sure we can overwrite previous link if as_unique
if as_unique?
if previous_link = Link.find(:first, :conditions => ["relation_id = ? AND #{other_side} = ?", self[:id], @last_target[:id]])
Expand Down Expand Up @@ -438,12 +467,16 @@ def find_node(obj_id, unique)

def find_target(obj_id)
if as_unique?
secure_drive(relation_class) { relation_class.find(:first, :conditions=>['id = ? AND kpath LIKE ?', obj_id, "#{other_kpath}%"]) }
cache[obj_id] ||= secure_drive(relation_class) { relation_class.find(:first, :conditions=>['id = ? AND kpath LIKE ?', obj_id, "#{other_kpath}%"]) }
else
secure_write(relation_class) { relation_class.find(:first, :conditions=>['id = ? AND kpath LIKE ?', obj_id, "#{other_kpath}%"]) }
cache[obj_id] ||= secure_write(relation_class) { relation_class.find(:first, :conditions=>['id = ? AND kpath LIKE ?', obj_id, "#{other_kpath}%"]) }
end
end

def cache
@cache ||= attributes_to_update[:nodes] || {}
end

def attributes_to_update
@attributes_to_update ||= {}
end
Expand Down
4 changes: 2 additions & 2 deletions app/models/virtual_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,12 @@ def <(other_class)
end


# Return the pseudo sql query compiler.
# Return the sqless query compiler.
def query_compiler
real_class.query_compiler
end

# Build pseudo sql query.
# Build sqless query.
def build_query(*args)
real_class.build_query(*args)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/zena/db_helper/abstract_db.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def next_zip(site_id)
raise Exception.new("Database Adapter #{adapter.inspect} not supported yet (you can probably fix this).")
end

# Return a string matching the pseudo sql function.
# Return a string matching the sqless function.
def sql_function(function, key)
raise Exception.new("Database Adapter #{adapter.inspect} does not support function #{function.inspect}.")
end
Expand Down
2 changes: 1 addition & 1 deletion lib/zena/db_helper/mysql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def next_zip(site_id)
rows.fetch_row[0].to_i
end

# Return a string matching the pseudo sql function.
# Return a string matching the sqless function.
def sql_function(function, key)
return key unless function
case function
Expand Down
2 changes: 1 addition & 1 deletion lib/zena/db_helper/postgresql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def next_zip(site_id)
res['zip'].to_i
end

# Return a string matching the pseudo sql function.
# Return a string matching the sqless function.
def sql_function(function, key)
return key unless function
# TODO
Expand Down
2 changes: 1 addition & 1 deletion lib/zena/db_helper/sqlite3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def next_zip(site_id)
fetch_attribute("SELECT zip FROM zips WHERE site_id = '#{site_id}'").to_i
end

# Return a string matching the pseudo sql function.
# Return a string matching the sqless function.
def sql_function(function, key)
return key unless function
case function
Expand Down
2 changes: 1 addition & 1 deletion lib/zena/remote/interface.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def count(query, options = {})
private
def process_find(count, query, options)
if query.kind_of?(String)
# Consider string as pseudo sql
# Consider string as sqless
result = get(find_url, :query => options.merge(:qb => query, :_find => count))

elsif query.kind_of?(Fixnum)
Expand Down
50 changes: 34 additions & 16 deletions lib/zena/use/query_builder.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Zena
module Use
module QueryBuilder
DATE_FIELD_REGEXP = %r{\d+[\.-/]\d+[\.-/]\d+}
module ViewMethods
include RubyLess
safe_method [:query_parse, String] => {:class => String, :accept_nil => true}
Expand Down Expand Up @@ -34,7 +35,7 @@ def query(class_name, node_name, pseudo_sql, opts = {})
type == :count ? 0 : nil
end

# Takes a hash of parameters and builds query arguments for pseudo sql
# Takes a hash of parameters and builds query arguments for sqless
# the query ends up like ' AND param_name = "foo" AND param_name > 35'...
def query_parse(params)
params ||= {}
Expand All @@ -51,7 +52,7 @@ def query_parse(params)
end

private
# Transforms special syntax into pseudo sql. Argument can be
# Transforms special syntax into sqless. Argument can be
#
# '' ==> (empty fields are ignored)
# '""' ==> key = ""
Expand All @@ -64,29 +65,46 @@ def query_parse(params)
# '"foo%"' ==> key = "foo%"
def query_build_clause(key, arg)
first = arg.first
case arg.first
when "'", '"'
return "not (#{query_build_clause(key, arg[1..-1])})" if first == '!'
if ["'", '"'].include?(first)
return "#{key} = #{arg}"
when '>','<','='
return "#{key} #{arg}"
end

if arg == 'null'
return "#{key} is null"
if arg =~ DATE_FIELD_REGEXP
arg = query_parse_dates(arg)
first = arg.first
end

if ['>','<','='].include?(first)
"#{key} #{arg}"
elsif arg == 'null'
"#{key} is null"
elsif arg == ''
# ignore
return ''
elsif arg =~ /%/
''
elsif arg =~ /\*/
# like
return "#{key} like #{arg.inspect}"
"#{key} like #{arg.gsub('%','*').inspect}"
elsif arg =~ /^(.+)\.\.(.+)$/
# interval
return "#{key} >= #{$1} and #{key} <= #{$2}"
"#{key} >= #{$1} and #{key} <= #{$2}"
elsif arg =~ /^\d+$/
# number
return "#{key} = #{arg}"
"#{key} = #{arg}"
elsif first == "'"
"#{key} = #{arg}"
else
return "#{key} = #{arg.inspect}"
"#{key} = #{arg.inspect}"
end
end

def query_parse_dates(str)
str.gsub(DATE_FIELD_REGEXP) do |date_str|
if date = DateTime.parse(date_str) rescue nil
date.strftime("'%Y-%m-%d'")
else
date_str
end
end
end
end # ViewMethods
Expand Down Expand Up @@ -242,7 +260,7 @@ def node_context_vars(finder)
end

private
# Build a Query object from pseudo sql.
# Build a Query object from sqless.
def build_query(count, pseudo_sql, raw_filters = [])

if !node.klass.respond_to?(:build_query)
Expand Down Expand Up @@ -344,7 +362,7 @@ def get_count(method, params)
((params[:paginate] || child['each'] || child['group'] || Node.plural_relation?(method)) ? :all : :first)
end

# Build pseudo sql from the parameters
# Build sqless from the parameters
# comments where ... from ... in ... order ... limit
def get_pseudo_sql(rel, params)
parts = [rel.dup]
Expand Down
9 changes: 7 additions & 2 deletions lib/zena/use/relations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ def link_id=(v)
# FIXME: this method does an 'update' not only 'add'
def add_link(role, hash)
if rel = relation_proxy(role)
rel.qb = hash[:qb] if hash.has_key?(:qb)
rel.other_id = hash[:other_id] if hash.has_key?(:other_id)
rel.other_ids = hash[:other_ids] if hash.has_key?(:other_ids)
rel.other_zip = hash[:other_zip] if hash.has_key?(:other_zip)
Expand Down Expand Up @@ -189,11 +190,15 @@ def rel_attributes=(hash)
elsif role =~ /^(.+)_attributes$/
# key used as role
definition['role'] ||= $1
else
elsif definition.kind_of?(Hash)
# key used as role, without the '_attributes'
definition['role'] ||= role
else
# qb
definition = {'role' => role, 'qb' => definition}
end
add_link(definition.delete('role'), definition.symbolize_keys) # TODO: only use string keys
# TODO: only use string keys
add_link(definition.delete('role'), definition.symbolize_keys)
end
end

Expand Down
24 changes: 21 additions & 3 deletions lib/zena/use/scope_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def set_site_id

module ModelMethods
def self.included(base)
base.after_save :update_scope_indices
base.after_save :update_scope_indices
base.safe_context :scope_index => scope_index_proc
base.alias_method_chain :rebuild_index!, :scope_index
end
Expand Down Expand Up @@ -170,6 +170,14 @@ def scope_index
# Update scope indices (project/section).
def update_scope_indices
return unless version.status == Zena::Status[:pub]
update_scope_indices_on_prop_change
update_scope_indices_on_link_change
rescue ::QueryBuilder::Error
# log and ignore: we cannot recover here ?
# FIXME: raise when we have transactional save.
end

def update_scope_indices_on_prop_change
if virtual_class && scopes = virtual_class.idx_scope
scopes = safe_eval(scopes)
return unless scopes.kind_of?(Hash)
Expand All @@ -189,8 +197,18 @@ def update_scope_indices
end
end
end
# rescue QueryBuilder::Error
# ignore
end

def update_scope_indices_on_link_change
(@relation_proxies || {}).values.compact.each do |rel|
if add_links = rel.add_links
add_links.each do |hash|
if node = hash[:node]
node.update_scope_indices
end
end
end
end
end
end # ModelMethods
end # ScopeIndex
Expand Down

0 comments on commit 09169b7

Please sign in to comment.