2026-01-02 13:41:09 +00:00
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
2022-12-02 14:55:24 +00:00
2023-11-03 12:47:39 +00:00
class Selector :: Sql < Selector :: Base
2026-02-17 15:47:43 +00:00
VALID_BLOCK_OPERATORS = %w[ AND OR NOT ] . freeze
2023-04-11 09:19:48 +00:00
VALID_OPERATORS = [
2023-08-16 10:10:45 +00:00
'after (absolute)' ,
'after (relative)' ,
'before (absolute)' ,
'before (relative)' ,
'contains all not' ,
'contains all' ,
'contains not' ,
'contains one not' ,
'contains one' ,
'contains' ,
'does not match regex' ,
'ends with one of' ,
'ends with' , # keep for compatibility with old conditions
'from (relative)' ,
2023-04-11 09:19:48 +00:00
'has changed' ,
'has reached warning' ,
2023-08-16 10:10:45 +00:00
'has reached' ,
'is any of' ,
2023-04-11 09:19:48 +00:00
'is in working time' ,
2025-04-25 08:50:51 +00:00
'is less than' ,
2025-04-28 10:06:21 +00:00
'is less than or equal to' ,
2025-04-25 09:00:37 +00:00
'is greater than' ,
2025-04-28 10:06:21 +00:00
'is greater than or equal to' ,
2023-08-16 10:10:45 +00:00
'is none of' ,
2023-06-29 09:55:28 +00:00
'is not in working time' ,
2023-08-16 10:10:45 +00:00
'is not' ,
'is set' ,
'is' ,
2026-05-14 12:50:39 +00:00
'matches' ,
2023-08-04 05:38:03 +00:00
'matches regex' ,
2023-08-16 10:10:45 +00:00
'not set' ,
2023-08-09 08:27:51 +00:00
'starts with one of' ,
2023-08-16 10:10:45 +00:00
'starts with' , # keep for compatibility with old conditions
'till (relative)' ,
'today' ,
'within last (relative)' ,
'within next (relative)' ,
2023-04-11 09:19:48 +00:00
] . freeze
2025-04-28 10:06:21 +00:00
OPERATOR_WORDING_TO_SYNTAX = {
'is less than' = > '<' ,
'is less than or equal to' = > '<=' ,
'is greater than' = > '>' ,
'is greater than or equal to' = > '>=' ,
} . freeze
2023-04-13 11:09:35 +00:00
attr_accessor :final_query , :final_bind_params , :final_tables , :changed_attributes
2022-12-02 14:55:24 +00:00
def get
@final_query = [ ]
@final_bind_params = [ ]
@final_tables = [ ]
@final_query = run ( selector , 0 )
[ query_sql , final_bind_params , tables_sql ]
rescue InvalidCondition = > e
2023-11-03 12:47:39 +00:00
Rails . logger . error " Selector::Sql.get->InvalidCondition: #{ e } "
2022-12-02 14:55:24 +00:00
nil
rescue = > e
2023-11-03 12:47:39 +00:00
Rails . logger . error " Selector::Sql.get->default: #{ e } "
2022-12-02 14:55:24 +00:00
raise e
end
def query_sql
2023-04-13 11:09:35 +00:00
Array ( final_query ) . join ( ' AND ' )
2022-12-02 14:55:24 +00:00
end
def tables_sql
return '' if final_tables . blank?
2023-04-13 11:09:35 +00:00
" #{ final_tables . join ( ' ' ) } "
2022-12-02 14:55:24 +00:00
end
def run ( block , level )
if block . key? ( :conditions )
run_block ( block , level )
else
2023-04-13 11:09:35 +00:00
query , bind_params , tables = condition_sql ( block )
2022-12-02 14:55:24 +00:00
@final_bind_params += bind_params
@final_tables |= tables
query
end
end
def run_block ( block , level )
2026-02-17 15:47:43 +00:00
validate_block_operator! ( block )
2024-04-09 14:09:18 +00:00
block_query = block [ :conditions ] . map do | sub_block |
run ( sub_block , level + 1 )
2022-12-02 14:55:24 +00:00
end
block_query = block_query . compact
return if block_query . blank?
return " NOT( #{ block_query . join ( ' AND ' ) } ) " if block [ :operator ] == 'NOT'
" ( #{ block_query . join ( " #{ block [ :operator ] } " ) } ) "
end
def condition_sql ( block_condition )
current_user = options [ :current_user ]
current_user_id = UserInfo . current_user_id
if current_user
current_user_id = current_user . id
end
raise InvalidCondition , " No block condition #{ block_condition . inspect } " if block_condition . blank?
raise InvalidCondition , " No block condition name #{ block_condition . inspect } " if block_condition [ :name ] . blank?
# remember query and bind params
query = [ ]
tables = [ ]
bind_params = [ ]
attribute_table , attribute_name = block_condition [ :name ] . split ( '.' )
# get tables to join
return if ! attribute_name
return if ! attribute_table
2023-11-03 12:47:39 +00:00
sql_helper = SqlHelper . new ( object : target_class )
if attribute_table && %w[ execution_time ticket_owner ticket_customer ] . exclude? ( attribute_table ) && attribute_table != target_name && tables . exclude? ( attribute_table ) && ! ( attribute_table == 'ticket' && attribute_name != 'mention_user_ids' ) && ! ( attribute_table == 'ticket' && attribute_name == 'mention_user_ids' && block_condition [ :pre_condition ] == 'not_set' ) && ! ( attribute_table == 'article' && attribute_name == 'action' )
2022-12-02 14:55:24 +00:00
case attribute_table
when 'customer'
2023-11-03 12:47:39 +00:00
tables |= [ " INNER JOIN users customers ON #{ target_table } .customer_id = customers.id " ]
2023-04-14 14:31:51 +00:00
sql_helper = SqlHelper . new ( object : User , table_name : 'customers' )
2022-12-02 14:55:24 +00:00
when 'organization'
2023-11-03 12:47:39 +00:00
tables |= [ " LEFT JOIN organizations ON #{ target_table } .organization_id = organizations.id " ]
2023-04-14 14:31:51 +00:00
sql_helper = SqlHelper . new ( object : Organization )
2022-12-02 14:55:24 +00:00
when 'owner'
2023-04-13 11:09:35 +00:00
tables |= [ 'INNER JOIN users owners ON tickets.owner_id = owners.id' ]
2023-04-14 14:31:51 +00:00
sql_helper = SqlHelper . new ( object : User , table_name : 'owners' )
2022-12-02 14:55:24 +00:00
when 'article'
2023-04-13 11:09:35 +00:00
tables |= [ 'INNER JOIN ticket_articles articles ON tickets.id = articles.ticket_id' ]
2023-10-25 11:50:57 +00:00
sql_helper = SqlHelper . new ( object : Ticket :: Article , table_name : 'articles' )
2022-12-02 14:55:24 +00:00
when 'ticket_state'
2023-04-13 11:09:35 +00:00
tables |= [ 'INNER JOIN ticket_states ON tickets.state_id = ticket_states.id' ]
2023-04-14 14:31:51 +00:00
sql_helper = SqlHelper . new ( object : Ticket :: State )
2025-06-12 14:35:52 +00:00
when 'content'
tables |= [ 'INNER JOIN knowledge_base_answer_translation_contents ON knowledge_base_answer_translations.content_id = knowledge_base_answer_translation_contents.id' ]
sql_helper = SqlHelper . new ( object : KnowledgeBase :: Answer :: Translation :: Content , table_name : 'knowledge_base_answer_translation_contents' )
2022-12-02 14:55:24 +00:00
else
raise " invalid selector #{ attribute_table } , #{ attribute_name } "
end
end
2023-04-11 09:19:48 +00:00
validate_operator! block_condition
validate_pre_condition_blank! block_condition
2022-12-02 14:55:24 +00:00
2023-04-11 09:19:48 +00:00
validate_pre_condition_values! block_condition
2022-12-02 14:55:24 +00:00
2023-10-25 11:50:57 +00:00
is_json_column = sql_helper . json_column? ( attribute_name )
2022-12-02 14:55:24 +00:00
# get attributes
2023-10-25 11:50:57 +00:00
attribute = is_json_column ? sql_helper . json_db_column_with_key ( attribute_name , 'value' ) : sql_helper . db_column ( attribute_name )
2022-12-02 14:55:24 +00:00
# magic block_condition
if attribute_table == 'ticket' && attribute_name == 'out_of_office_replacement_id'
attribute = " #{ ActiveRecord :: Base . connection . quote_table_name ( " #{ attribute_table } s " ) } . #{ ActiveRecord :: Base . connection . quote_column_name ( 'owner_id' ) } "
end
if attribute_table == 'ticket' && attribute_name == 'tags'
block_condition [ :value ] = block_condition [ :value ] . split ( ',' ) . collect ( & :strip )
end
2023-10-11 12:27:01 +00:00
# Performance: use left join instead of sub select if tags value is only one element and contains all is used
2025-06-23 11:21:06 +00:00
if attribute_table == 'ticket' && attribute_name == 'tags' && block_condition [ :operator ] == 'contains all' && block_condition [ :value ] . one?
2023-10-11 12:27:01 +00:00
block_condition [ :operator ] = 'contains one'
end
2023-11-03 12:47:39 +00:00
# User customer tickets last_contact_at
query_wrap = nil
if attribute_table == 'ticket_customer' && attribute_name == 'last_contact_at'
attribute = 'last_contact_at'
query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
end
# User customer tickets last_contact_agent_at
if attribute_table == 'ticket_customer' && attribute_name == 'last_contact_agent_at'
attribute = 'last_contact_agent_at'
query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
end
# User customer tickets last_contact_customer_at
if attribute_table == 'ticket_customer' && attribute_name == 'last_contact_customer_at'
attribute = 'last_contact_customer_at'
query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
end
# User customer tickets updated_at
if attribute_table == 'ticket_customer' && attribute_name == 'updated_at'
attribute = 'updated_at'
query_wrap = 'users.id IN (SELECT DISTINCT tickets.customer_id FROM tickets WHERE ###QUERY###)'
end
2022-12-02 14:55:24 +00:00
#
# checks
#
2023-04-13 13:51:49 +00:00
#
2022-12-02 14:55:24 +00:00
2023-04-13 13:51:49 +00:00
if attribute_table == 'article' && options . key? ( :article_id ) && options [ :article_id ] . blank? && attribute_name != 'action'
2022-12-02 14:55:24 +00:00
query << '1 = 0'
elsif block_condition [ :operator ] . include? ( 'in working time' )
raise __ ( 'Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)' ) if ! options [ :execution_time ]
biz = Calendar . lookup ( id : block_condition [ :value ] ) & . biz
query << if biz . present? && attribute_name == 'calendar_id' && ( ( block_condition [ :operator ] == 'is in working time' && ! biz . in_hours? ( Time . zone . now ) ) || ( block_condition [ :operator ] == 'is not in working time' && biz . in_hours? ( Time . zone . now ) ) )
'1 = 0'
else
'1 = 1'
end
elsif block_condition [ :operator ] == 'has changed'
query << if changed_attributes [ block_condition [ :name ] ]
'1 = 1'
else
'1 = 0'
end
2023-04-11 09:19:48 +00:00
elsif block_condition [ :operator ] == 'has reached'
query << if time_based_trigger? ( block_condition , warning : false )
'1 = 1'
else
'1 = 0'
end
elsif block_condition [ :operator ] == 'has reached warning'
query << if time_based_trigger? ( block_condition , warning : true )
'1 = 1'
else
'1 = 0'
end
2022-12-02 14:55:24 +00:00
elsif attribute_table == 'ticket' && attribute_name == 'action'
check = options [ :ticket_action ] == block_condition [ :value ] ? 1 : 0
2023-04-11 09:19:48 +00:00
query << if update_action_requires_changed_attributes? ( block_condition , check )
2022-12-02 14:55:24 +00:00
'1 = 0'
elsif block_condition [ :operator ] == 'is'
" 1 = #{ check } "
else
" 0 = #{ check } " # is not
end
2023-04-13 13:51:49 +00:00
elsif attribute_table == 'article' && attribute_name == 'action'
check = options [ :article_id ] ? 1 : 0
query << if block_condition [ :operator ] == 'is'
" 1 = #{ check } "
else
" 0 = #{ check } " # is not
end
2023-08-16 10:10:45 +00:00
elsif attribute_table == 'article' && attribute_name == 'time_accounting'
tables |= [ " LEFT JOIN ticket_time_accountings ON ticket_time_accountings.ticket_article_id = #{ options [ :article_id ] . to_i } " ]
query << if block_condition [ :operator ] == 'is set'
'ticket_time_accountings.id IS NOT NULL'
else
'ticket_time_accountings.id IS NULL' # not set
end
2022-12-02 14:55:24 +00:00
# because of no grouping support we select not_set by sub select for mentions
elsif attribute_table == 'ticket' && attribute_name == 'mention_user_ids'
if block_condition [ :pre_condition ] == 'not_set'
2023-10-06 09:48:28 +00:00
tables |= [ " LEFT JOIN mentions ON tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket' " ]
2022-12-02 14:55:24 +00:00
query << if block_condition [ :operator ] == 'is'
2023-10-06 09:48:28 +00:00
'mentions.user_id IS NULL'
2022-12-02 14:55:24 +00:00
else
2023-10-06 09:48:28 +00:00
'mentions.user_id IS NOT NULL'
2022-12-02 14:55:24 +00:00
end
else
query << if block_condition [ :operator ] == 'is'
2023-10-06 09:48:28 +00:00
tables |= [ " LEFT JOIN mentions ON tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket' " ]
'mentions.user_id IN (?)'
2022-12-02 14:55:24 +00:00
else
2023-10-11 12:27:01 +00:00
" tickets.id NOT IN (SELECT mentionable_id FROM mentions WHERE mentionable_type = 'Ticket' AND user_id IN (?)) "
2022-12-02 14:55:24 +00:00
end
if block_condition [ :pre_condition ] == 'current_user.id'
bind_params . push current_user_id
else
bind_params . push block_condition [ :value ]
end
end
2023-11-03 12:47:39 +00:00
elsif attribute_table == 'user' && attribute_name == 'role_ids'
query << if block_condition [ :operator ] == 'is'
" users.id IN (
SELECT
roles_users . user_id
FROM
roles , roles_users
WHERE
roles . id = roles_users . role_id
AND roles . id IN ( ?)
GROUP BY
roles_users . user_id
) "
else
" users.id NOT IN (
SELECT
roles_users . user_id
FROM
roles , roles_users
WHERE
roles . id = roles_users . role_id
AND roles . id IN ( ?)
GROUP BY
roles_users . user_id
) "
end
bind_params . push block_condition [ :value ]
elsif attribute_table == 'organization' && attribute_name == 'members_existing'
query << if ( block_condition [ :operator ] == 'is' && block_condition [ :value ] . to_s == 'true' ) || ( block_condition [ :operator ] == 'is not' && block_condition [ :value ] . to_s != 'true' )
'organizations.id IN (SELECT DISTINCT organizations.id FROM organizations, users WHERE organizations.id = users.organization_id)'
else
'organizations.id NOT IN (SELECT DISTINCT organizations.id FROM organizations, users WHERE organizations.id = users.organization_id)'
end
elsif %w[ ticket_customer ticket_owner ] . include? ( attribute_table ) && %w[ existing open_existing ] . include? ( attribute_name )
distinct_column = if attribute_table == 'ticket_customer'
'customer_id'
else
'owner_id'
end
query_where = ''
if attribute_name == 'open_existing'
query_where = ' WHERE state_id IN (?)'
2023-11-24 11:29:37 +00:00
bind_params . push Ticket :: State . by_category_ids ( :open )
2023-11-03 12:47:39 +00:00
end
query << if ( block_condition [ :operator ] == 'is' && block_condition [ :value ] . to_s == 'true' ) || ( block_condition [ :operator ] == 'is not' && block_condition [ :value ] . to_s != 'true' )
" users.id IN (SELECT DISTINCT #{ distinct_column } FROM tickets #{ query_where } ) "
else
" users.id NOT IN (SELECT DISTINCT #{ distinct_column } FROM tickets #{ query_where } ) "
end
2023-06-29 09:55:28 +00:00
elsif block_condition [ :operator ] == 'starts with'
2025-05-22 11:18:24 +00:00
query << " #{ attribute } ILIKE (?) "
2023-09-07 05:39:48 +00:00
bind_params . push " #{ SqlHelper . quote_like ( block_condition [ :value ] ) } % "
2023-08-09 08:27:51 +00:00
elsif block_condition [ :operator ] == 'starts with one of'
block_condition [ :value ] = Array . wrap ( block_condition [ :value ] )
sub_query = [ ]
block_condition [ :value ] . each do | value |
2025-05-22 11:18:24 +00:00
sub_query << " #{ attribute } ILIKE (?) "
2023-09-07 05:39:48 +00:00
bind_params . push " #{ SqlHelper . quote_like ( value ) } % "
2023-08-09 08:27:51 +00:00
end
query << " ( #{ sub_query . join ( ' OR ' ) } ) " if sub_query . present?
2023-06-29 09:55:28 +00:00
elsif block_condition [ :operator ] == 'ends with'
2025-05-22 11:18:24 +00:00
query << " #{ attribute } ILIKE (?) "
2023-09-07 05:39:48 +00:00
bind_params . push " % #{ SqlHelper . quote_like ( block_condition [ :value ] ) } "
2023-08-09 08:27:51 +00:00
elsif block_condition [ :operator ] == 'ends with one of'
block_condition [ :value ] = Array . wrap ( block_condition [ :value ] )
sub_query = [ ]
block_condition [ :value ] . each do | value |
2025-05-22 11:18:24 +00:00
sub_query << " #{ attribute } ILIKE (?) "
2023-09-07 05:39:48 +00:00
bind_params . push " % #{ SqlHelper . quote_like ( value ) } "
2023-08-09 08:27:51 +00:00
end
query << " ( #{ sub_query . join ( ' OR ' ) } ) " if sub_query . present?
elsif block_condition [ :operator ] == 'is any of'
block_condition [ :value ] = Array . wrap ( block_condition [ :value ] )
block_condition [ :value ] = block_condition [ :value ] . empty? ? [ '' ] : block_condition [ :value ]
sub_query = [ ]
block_condition [ :value ] . each do | value |
sub_query << " #{ attribute } IN (?) "
bind_params . push value
end
query << " ( #{ sub_query . join ( ' OR ' ) } ) " if sub_query . present?
elsif block_condition [ :operator ] == 'is none of'
block_condition [ :value ] = Array . wrap ( block_condition [ :value ] )
block_condition [ :value ] = block_condition [ :value ] . empty? ? [ '' ] : block_condition [ :value ]
sub_query = [ ]
block_condition [ :value ] . each do | value |
sub_query << " #{ attribute } NOT IN (?) "
bind_params . push value
end
query << " ( #{ sub_query . join ( ' AND ' ) } ) " if sub_query . present?
2022-12-02 14:55:24 +00:00
elsif block_condition [ :operator ] == 'is'
if block_condition [ :pre_condition ] == 'not_set'
if attribute_name . match? ( %r{ ^(created_by|updated_by|owner|customer|user)_id } )
query << " ( #{ attribute } IS NULL OR #{ attribute } IN (?)) "
bind_params . push 1
else
query << " #{ attribute } IS NULL "
end
elsif block_condition [ :pre_condition ] == 'current_user.id'
raise " Use current_user.id in block_condition, but no current_user is set #{ block_condition . inspect } " if ! current_user_id
query << " #{ attribute } IN (?) "
if attribute_name == 'out_of_office_replacement_id'
bind_params . push User . find ( current_user_id ) . out_of_office_agent_of . pluck ( :id )
else
bind_params . push current_user_id
end
elsif block_condition [ :pre_condition ] == 'current_user.organization_id'
raise " Use current_user.id in block_condition, but no current_user is set #{ block_condition . inspect } " if ! current_user_id
query << " #{ attribute } IN (?) "
user = User . find_by ( id : current_user_id )
bind_params . push user . all_organization_ids
else
# rubocop:disable Style/IfInsideElse, Metrics/BlockNesting
if block_condition [ :value ] . nil?
query << " #{ attribute } IS NULL "
else
2023-10-25 11:50:57 +00:00
if is_json_column
query << " #{ attribute } IN (?) "
bind_params . push ( Array . wrap ( block_condition [ :value ] ) . map { | item | item [ :value ] . to_s } )
elsif attribute_name == 'out_of_office_replacement_id'
2022-12-02 14:55:24 +00:00
query << " #{ attribute } IN (?) "
2023-08-18 08:42:54 +00:00
bind_params . push User . where ( id : Array . wrap ( block_condition [ :value ] ) ) . map ( & :out_of_office_agent_of ) . flatten . map ( & :id )
2022-12-02 14:55:24 +00:00
else
2023-08-09 08:27:51 +00:00
block_condition [ :value ] = Array . wrap ( block_condition [ :value ] )
2023-06-29 09:55:28 +00:00
2022-12-02 14:55:24 +00:00
query << if block_condition [ :value ] . include? ( '' )
" ( #{ attribute } IN (?) OR #{ attribute } IS NULL) "
else
" #{ attribute } IN (?) "
end
bind_params . push block_condition [ :value ]
end
end
# rubocop:enable Style/IfInsideElse, Metrics/BlockNesting
end
elsif block_condition [ :operator ] == 'is not'
if block_condition [ :pre_condition ] == 'not_set'
if attribute_name . match? ( %r{ ^(created_by|updated_by|owner|customer|user)_id } )
query << " ( #{ attribute } IS NOT NULL AND #{ attribute } NOT IN (?)) "
bind_params . push 1
else
query << " #{ attribute } IS NOT NULL "
end
elsif block_condition [ :pre_condition ] == 'current_user.id'
query << " ( #{ attribute } IS NULL OR #{ attribute } NOT IN (?)) "
if attribute_name == 'out_of_office_replacement_id'
bind_params . push User . find ( current_user_id ) . out_of_office_agent_of . pluck ( :id )
else
bind_params . push current_user_id
end
elsif block_condition [ :pre_condition ] == 'current_user.organization_id'
query << " ( #{ attribute } IS NULL OR #{ attribute } NOT IN (?)) "
user = User . find_by ( id : current_user_id )
bind_params . push user . organization_id
else
# rubocop:disable Style/IfInsideElse, Metrics/BlockNesting
if block_condition [ :value ] . nil?
query << " #{ attribute } IS NOT NULL "
else
2023-10-25 11:50:57 +00:00
if is_json_column
query << " #{ attribute } NOT IN (?) "
bind_params . push ( Array . wrap ( block_condition [ :value ] ) . map { | item | item [ :value ] . to_s } )
elsif attribute_name == 'out_of_office_replacement_id'
2022-12-02 14:55:24 +00:00
bind_params . push User . find ( block_condition [ :value ] ) . out_of_office_agent_of . pluck ( :id )
query << " ( #{ attribute } IS NULL OR #{ attribute } NOT IN (?)) "
else
2023-08-09 08:27:51 +00:00
block_condition [ :value ] = Array . wrap ( block_condition [ :value ] )
2023-06-29 09:55:28 +00:00
2022-12-02 14:55:24 +00:00
query << if block_condition [ :value ] . include? ( '' )
" ( #{ attribute } IS NOT NULL AND #{ attribute } NOT IN (?)) "
else
" ( #{ attribute } IS NULL OR #{ attribute } NOT IN (?)) "
end
bind_params . push block_condition [ :value ]
end
end
# rubocop:enable Style/IfInsideElse, Metrics/BlockNesting
end
elsif block_condition [ :operator ] == 'contains'
2025-05-22 11:18:24 +00:00
query << " #{ attribute } ILIKE (?) "
2023-09-07 05:39:48 +00:00
bind_params . push " % #{ SqlHelper . quote_like ( block_condition [ :value ] ) } % "
2022-12-02 14:55:24 +00:00
elsif block_condition [ :operator ] == 'contains not'
2023-12-27 10:19:24 +00:00
# NOT LIKE is always false on NULL values
# https://github.com/zammad/zammad/issues/4948
2025-05-22 11:18:24 +00:00
query << " #{ attribute } NOT ILIKE (?) OR #{ attribute } IS NULL "
2023-09-07 05:39:48 +00:00
bind_params . push " % #{ SqlHelper . quote_like ( block_condition [ :value ] ) } % "
2026-05-14 12:50:39 +00:00
elsif block_condition [ :operator ] == 'matches'
query << " #{ attribute } ILIKE (?) "
if wildcard_value? ( block_condition [ :value ] )
bind_params . push SqlHelper . quote_like ( block_condition [ :value ] ) . gsub ( MATCH_WILDCARD_REGEX , '%' )
else
bind_params . push " % #{ SqlHelper . quote_like ( block_condition [ :value ] ) } % "
end
2023-08-04 05:38:03 +00:00
elsif block_condition [ :operator ] == 'matches regex'
query << sql_helper . regex_match ( attribute , negated : false )
bind_params . push block_condition [ :value ]
elsif block_condition [ :operator ] == 'does not match regex'
query << sql_helper . regex_match ( attribute , negated : true )
bind_params . push block_condition [ :value ]
2022-12-02 14:55:24 +00:00
elsif block_condition [ :operator ] == 'contains all'
if attribute_table == 'ticket' && attribute_name == 'tags'
2023-10-11 12:27:01 +00:00
query << " tickets.id IN (
SELECT
tags . o_id
FROM
tag_objects , tag_items , tags
WHERE
tag_objects . id = tags . tag_object_id
AND tag_objects . name = 'Ticket'
AND tag_items . id = tags . tag_item_id
AND tag_items . name IN ( ?)
GROUP BY
tags . o_id
HAVING
COUNT ( * ) = ?
) "
2022-12-02 14:55:24 +00:00
bind_params . push block_condition [ :value ]
2023-10-11 12:27:01 +00:00
bind_params . push block_condition [ :value ] . count
2023-04-14 14:31:51 +00:00
elsif sql_helper . containable? ( attribute_name )
query << sql_helper . array_contains_all ( attribute_name , block_condition [ :value ] )
2026-04-22 08:20:01 +00:00
bind_params += Array . wrap ( block_condition [ :value ] )
2022-12-02 14:55:24 +00:00
end
2023-04-14 14:31:51 +00:00
elsif block_condition [ :operator ] == 'contains one'
if attribute_name == 'tags' && attribute_table == 'ticket'
2023-04-13 11:09:35 +00:00
tables |= [ " LEFT JOIN tags ON tickets.id = tags.o_id LEFT JOIN tag_objects ON tag_objects.id = tags.tag_object_id AND tag_objects.name = 'Ticket' LEFT JOIN tag_items ON tag_items.id = tags.tag_item_id " ]
query << 'tag_items.name IN (?)'
2022-12-02 14:55:24 +00:00
bind_params . push block_condition [ :value ]
2023-04-14 14:31:51 +00:00
elsif sql_helper . containable? ( attribute_name )
query << sql_helper . array_contains_one ( attribute_name , block_condition [ :value ] )
2026-04-22 08:20:01 +00:00
bind_params += Array . wrap ( block_condition [ :value ] )
2022-12-02 14:55:24 +00:00
end
2023-04-14 14:31:51 +00:00
elsif block_condition [ :operator ] == 'contains all not'
if attribute_name == 'tags' && attribute_table == 'ticket'
2023-10-11 12:27:01 +00:00
query << " tickets.id NOT IN (
2022-12-02 14:55:24 +00:00
SELECT
2023-10-11 12:27:01 +00:00
DISTINCT tags . o_id
2022-12-02 14:55:24 +00:00
FROM
2023-10-11 12:27:01 +00:00
tag_objects , tag_items , tags
2022-12-02 14:55:24 +00:00
WHERE
2023-10-11 12:27:01 +00:00
tag_objects . id = tags . tag_object_id
AND tag_objects . name = 'Ticket'
AND tag_items . id = tags . tag_item_id
AND tag_items . name IN ( ?)
2023-10-13 12:43:19 +00:00
GROUP BY
tags . o_id
HAVING
COUNT ( * ) = ?
2022-12-02 14:55:24 +00:00
) "
bind_params . push block_condition [ :value ]
2023-10-13 12:43:19 +00:00
bind_params . push block_condition [ :value ] . count
2023-04-14 14:31:51 +00:00
elsif sql_helper . containable? ( attribute_name )
query << sql_helper . array_contains_all ( attribute_name , block_condition [ :value ] , negated : true )
2026-04-22 08:20:01 +00:00
bind_params += Array . wrap ( block_condition [ :value ] )
2022-12-02 14:55:24 +00:00
end
2023-04-14 14:31:51 +00:00
elsif block_condition [ :operator ] == 'contains one not'
if attribute_name == 'tags' && attribute_table == 'ticket'
2023-10-13 12:43:19 +00:00
query << " tickets.id NOT IN (
2023-10-11 12:27:01 +00:00
SELECT
DISTINCT tags . o_id
FROM
tag_objects , tag_items , tags
WHERE
tag_objects . id = tags . tag_object_id
AND tag_objects . name = 'Ticket'
AND tag_items . id = tags . tag_item_id
2023-10-13 12:43:19 +00:00
AND tag_items . name IN ( ?)
2023-10-11 12:27:01 +00:00
) "
2022-12-02 14:55:24 +00:00
bind_params . push block_condition [ :value ]
2023-04-14 14:31:51 +00:00
elsif sql_helper . containable? ( attribute_name )
query << sql_helper . array_contains_one ( attribute_name , block_condition [ :value ] , negated : true )
2026-04-22 08:20:01 +00:00
bind_params += Array . wrap ( block_condition [ :value ] )
2022-12-02 14:55:24 +00:00
end
elsif block_condition [ :operator ] == 'today'
2024-03-19 15:29:12 +00:00
Time . use_zone ( Setting . get ( 'timezone_default' ) ) do
2022-12-02 14:55:24 +00:00
day_start = Time . zone . now . beginning_of_day . utc
day_end = Time . zone . now . end_of_day . utc
query << " #{ attribute } BETWEEN ? AND ? "
bind_params . push day_start
bind_params . push day_end
end
elsif block_condition [ :operator ] == 'before (absolute)'
query << " #{ attribute } <= ? "
bind_params . push block_condition [ :value ]
elsif block_condition [ :operator ] == 'after (absolute)'
query << " #{ attribute } >= ? "
bind_params . push block_condition [ :value ]
elsif block_condition [ :operator ] == 'within last (relative)'
query << " #{ attribute } BETWEEN ? AND ? "
time = range ( block_condition ) . ago
bind_params . push time
bind_params . push Time . zone . now
elsif block_condition [ :operator ] == 'within next (relative)'
query << " #{ attribute } BETWEEN ? AND ? "
time = range ( block_condition ) . from_now
bind_params . push Time . zone . now
bind_params . push time
elsif block_condition [ :operator ] == 'before (relative)'
query << " #{ attribute } <= ? "
time = range ( block_condition ) . ago
bind_params . push time
elsif block_condition [ :operator ] == 'after (relative)'
query << " #{ attribute } >= ? "
time = range ( block_condition ) . from_now
bind_params . push time
elsif block_condition [ :operator ] == 'till (relative)'
query << " #{ attribute } <= ? "
time = range ( block_condition ) . from_now
bind_params . push time
elsif block_condition [ :operator ] == 'from (relative)'
query << " #{ attribute } >= ? "
time = range ( block_condition ) . ago
bind_params . push time
2025-04-28 10:06:21 +00:00
elsif [ 'is less than' , 'is less than or equal to' , 'is greater than' , 'is greater than or equal to' ] . include? ( block_condition [ :operator ] )
operator = OPERATOR_WORDING_TO_SYNTAX [ block_condition [ :operator ] ]
2025-04-25 09:00:37 +00:00
query << " #{ attribute } #{ operator } ? "
2025-04-25 08:50:51 +00:00
bind_params . push block_condition [ :value ]
2022-12-02 14:55:24 +00:00
else
raise " Invalid operator ' #{ block_condition [ :operator ] } ' for ' #{ block_condition [ :value ] . inspect } ' "
end
2023-11-03 12:47:39 +00:00
if query_wrap . present?
query << query_wrap . gsub ( '###QUERY###' , query . pop )
end
2025-08-06 06:17:01 +00:00
query . map! { " ( #{ it } ) " }
2024-01-02 20:22:01 +00:00
2023-04-13 11:09:35 +00:00
[ query , bind_params , tables ]
2022-12-02 14:55:24 +00:00
end
def range ( selector )
selector [ :value ] . to_i . send ( selector [ :range ] . pluralize )
rescue
raise 'unknown selector'
end
2023-04-11 09:19:48 +00:00
2026-02-17 15:47:43 +00:00
def validate_block_operator! ( condition )
raise " Invalid condition, block operator missing #{ condition . inspect } " if condition [ :operator ] . blank?
return true if self . class . valid_block_operator? ( condition [ :operator ] )
2023-04-11 09:19:48 +00:00
2026-02-17 15:47:43 +00:00
raise " Invalid condition, block operator ' #{ condition [ :operator ] } ' is invalid #{ condition . inspect } "
end
def validate_operator! ( condition )
raise " Invalid condition, operator missing #{ condition . inspect } " if condition [ :operator ] . blank?
2023-11-03 12:47:39 +00:00
return true if self . class . valid_operator? ( condition [ :operator ] )
2023-04-11 09:19:48 +00:00
2023-08-04 05:38:03 +00:00
raise " Invalid condition, operator ' #{ condition [ :operator ] } ' is invalid #{ condition . inspect } "
2023-04-11 09:19:48 +00:00
end
def time_based_trigger? ( condition , warning : )
case [ condition [ :name ] , options [ :ticket_action ] ]
in 'ticket.pending_time' , 'reminder_reached'
true
in 'ticket.escalation_at' , 'escalation'
! warning
in 'ticket.escalation_at' , 'escalation_warning'
warning
else
false
end
end
# validate pre_condition values
def validate_pre_condition_values! ( condition )
return if [ 'has changed' , 'has reached' , 'has reached warning' ] . include? condition [ :operator ]
return if condition [ :pre_condition ] . blank?
return if %w[ not_set current_user. specific ] . any? { | elem | condition [ :pre_condition ] . start_with? elem }
raise InvalidCondition , " Invalid condition pre_condition not set #{ condition } ! "
end
# validate value / allow blank but only if pre_condition exists and is not specific
def validate_pre_condition_blank! ( condition )
2023-08-16 10:10:45 +00:00
return if [ 'has changed' , 'has reached' , 'has reached warning' , 'is any of' , 'is none of' , 'is set' , 'not set' ] . include? condition [ :operator ]
2023-04-11 09:19:48 +00:00
if ( condition [ :operator ] != 'today' && ! condition . key? ( :value ) ) ||
( condition [ :value ] . instance_of? ( Array ) && condition [ :value ] . respond_to? ( :blank? ) && condition [ :value ] . blank? ) ||
( condition [ :operator ] . start_with? ( 'contains' ) && condition [ :value ] . respond_to? ( :blank? ) && condition [ :value ] . blank? )
raise InvalidCondition , " Invalid condition pre_condition nil #{ condition } ! " if condition [ :pre_condition ] . nil?
raise InvalidCondition , " Invalid condition pre_condition blank #{ condition } ! " if condition [ :pre_condition ] . respond_to? ( :blank? ) && condition [ :pre_condition ] . blank?
raise InvalidCondition , " Invalid condition pre_condition specific #{ condition } ! " if condition [ :pre_condition ] == 'specific'
end
end
def update_action_requires_changed_attributes? ( condition , check )
condition [ :value ] == 'update' && check && options [ :changes_required ] && changed_attributes . blank?
end
2026-02-17 15:47:43 +00:00
def self . valid_block_operator? ( operator )
VALID_BLOCK_OPERATORS . include? ( operator )
end
2023-04-11 09:19:48 +00:00
def self . valid_operator? ( operator )
2023-08-16 10:10:45 +00:00
VALID_OPERATORS . include? ( operator )
2023-04-11 09:19:48 +00:00
end
2023-08-04 05:38:03 +00:00
def valid?
2024-11-15 14:01:26 +00:00
object_count , _objects = target_class . selectors ( selector , ** options , limit : 1 , execution_time : true , ticket_id : 1 , access : 'ignore' )
2023-11-03 12:47:39 +00:00
! object_count . nil?
2023-08-04 05:38:03 +00:00
rescue
false
end
2022-12-02 14:55:24 +00:00
end