/
where.cr
168 lines (150 loc) · 5.23 KB
/
where.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# 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