# Feature WHERE clause building. # each call to where method stack where clause. # Theses clauses are then combined together using the `AND` operator. # Therefore, `query.where("a").where("b")` will return `a AND b` # module Clear::SQL::Query::Where macro included # Return the list of where clause; each where clause are transformed into # Clear::Expression::Node getter wheres : Array(Clear::Expression::Node) end # Build SQL `where` condition using a Clear::Expression::Node # ```crystal # query.where(Clear::Expression::Node::InArray.new("id", ['1', '2', '3', '4'])) # # Note: in this example, InArray node use unsafe strings # ``` # If useful for moving a where clause from a request to another one: # ```crystal # query1.where { a == b } # WHERE a = b # ``` # ``` # query2.where(query1.wheres[0]) # WHERE a = b # ``` def where(node : Clear::Expression::Node) @wheres << node change! end # Build SQL `or_where` condition using a Clear::Expression::Node # ```crystal # query.or_where(Clear::Expression::Node::InArray.new("id", ['1', '2', '3', '4'])) # # Note: in this example, InArray node use unsafe strings # ``` # If useful for moving a where clause from a request to another one: # ```crystal # query1.or_where { a == b } # WHERE a = b # ``` # ``` # query2.or_where(query1.wheres[0]) # WHERE a = b # ``` def or_where(node : Clear::Expression::Node) return where(node) if @wheres.empty? # Optimisation: if we have a OR Array as root, we use it and append directly the element. if @wheres.size == 1 && (n = @wheres.first) && n.is_a?(Clear::Expression::Node::NodeArray) && n.link == "OR" n.expression << node else # Concatenate the old clauses in a list of AND conditions if @wheres.size == 1 old_clause = @wheres.first else old_clause = Clear::Expression::Node::NodeArray.new(@wheres, "AND") end @wheres.clear @wheres << Clear::Expression::Node::NodeArray.new([old_clause, node], "OR") end change! end # Build SQL `where` condition using the Expression engine. # ```crystal # query.where { id == 1 } # ``` def where(&block) where(Clear::Expression.ensure_node!(with Clear::Expression.new yield)) end def where(**tuple) where(__conditions: tuple) end # Build SQL `where` condition using a NamedTuple. # this will use: # - the `=` operator if compared with a literal # ```crystal # query.where({keyword: "hello"}) # WHERE keyword = 'hello' # ``` # - the `IN` operator if compared with an array: # ```crystal # query.where({x: [1, 2]}) # WHERE x in (1, 2) # ``` # - the `>=` and `<=` | `<` if compared with a range: # ```crystal # query.where({x: (1..4)}) # WHERE x >= 1 AND x <= 4 # query.where({x: (1...4)}) # WHERE x >= 1 AND x < 4 # ``` # - You also can put another select query as argument: # ```crystal # query.where({x: another_select}) # WHERE x IN (SELECT ... ) # ``` def where(__conditions : NamedTuple | Hash(String, Clear::SQL::Any)) __conditions.each do |k, v| k = Clear::Expression::Node::Variable.new(k.to_s) @wheres << case v when Array Clear::Expression::Node::InArray.new(k, v.map { |it| Clear::Expression[it] }) when SelectBuilder Clear::Expression::Node::InSelect.new(k, v) when Range Clear::Expression::Node::InRange.new(k, Clear::Expression[v.begin]..Clear::Expression[v.end], v.exclusive?) else Clear::Expression::Node::DoubleOperator.new(k, Clear::Expression::Node::Literal.new(v), (v.nil? ? "IS" : "=") ) end end change! end # Build SQL `where` interpolating `:keyword` with the NamedTuple passed in argument. # ```crystal # where("id = :id OR date >= :start", id: 1, start: 1.day.ago) # # WHERE id = 1 AND date >= '201x-xx-xx ...' # ``` def where(__template : String, **__tuple) where(Clear::Expression::Node::Raw.new(Clear::SQL.raw(__template, **__tuple))) end # Build SQL `where` condition using a template string and # interpolating `?` characters with parameters given in a tuple or array. # ```crystal # where("x = ? OR y = ?", 1, "l'eau") # WHERE x = 1 OR y = 'l''eau' # ``` # Raise error if there's not enough parameters to cover all the `?` placeholders def where(__template : String, *__args) where(Clear::Expression::Node::Raw.new(Clear::SQL.raw(__template, *__args))) end def or_where(__template : String, **__named_tuple) or_where(Clear::Expression::Node::Raw.new(Clear::Expression.raw("(#{__template})", **__named_tuple))) end def or_where(__template : String, *__args) or_where(Clear::Expression::Node::Raw.new(Clear::Expression.raw("(#{__template})", *__args))) end # Build SQL `where` condition using the Expression engine. # ```crystal # query.or_where { id == 1 } # ``` def or_where(&block) or_where(Clear::Expression.ensure_node!(with Clear::Expression.new yield)) end # Clear all the where clauses and return `self` def clear_wheres @wheres.clear change! end # :nodoc: protected def print_wheres {"WHERE ", @wheres.map(&.resolve).join(" AND ")}.join if @wheres.any? end end