sqb 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/sqb.rb +6 -1
- data/lib/sqb/base.rb +25 -0
- data/lib/sqb/columns.rb +38 -0
- data/lib/sqb/delete.rb +35 -0
- data/lib/sqb/distinct.rb +10 -0
- data/lib/sqb/error.rb +3 -0
- data/lib/sqb/escaping.rb +56 -0
- data/lib/sqb/filtering.rb +109 -0
- data/lib/sqb/grouping.rb +17 -0
- data/lib/sqb/insert.rb +43 -0
- data/lib/sqb/joins.rb +55 -0
- data/lib/sqb/limiting.rb +14 -0
- data/lib/sqb/offsetting.rb +14 -0
- data/lib/sqb/ordering.rb +39 -0
- data/lib/sqb/query.rb +2 -371
- data/lib/sqb/safe_string.rb +0 -1
- data/lib/sqb/select.rb +66 -0
- data/lib/sqb/update.rb +52 -0
- data/lib/sqb/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3c57f511a3e18b3718cbbcd5e6703b52bd0b3d6e92ead8aca02a04ef46b087a
|
4
|
+
data.tar.gz: 2fd0e2d396c722bf2d0fcac632a8322be95ab3aa65b64ba70d86b62ca5859d98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba298f55cf459a29818711e146c78b58911a1673c277879d013f2333e31729b733fa686af429931797aad1b1d5dfb0ea43c0b31364d4ac3c1f68f99ded75d4d9
|
7
|
+
data.tar.gz: 8e4c5e633e78f78689a2d60e8574bb5e86de9d0c0305ace9e4c46e64daadc9c971a3615da18fa5825e295081e2b3eabd6c61b43d9f185b18de10f98ebdfa83e0
|
data/lib/sqb.rb
CHANGED
data/lib/sqb/base.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'sqb/error'
|
2
|
+
require 'sqb/escaping'
|
3
|
+
|
4
|
+
module SQB
|
5
|
+
class Base
|
6
|
+
|
7
|
+
include SQB::Escaping
|
8
|
+
|
9
|
+
attr_reader :prepared_arguments
|
10
|
+
|
11
|
+
def initialize(table_name, options = {}, &block)
|
12
|
+
@table_name = table_name
|
13
|
+
@options = options
|
14
|
+
@prepared_arguments = []
|
15
|
+
block.call(self) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Generate the full SQL query for this query.
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
def to_sql
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/sqb/columns.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
module SQB
|
2
|
+
module Columns
|
3
|
+
|
4
|
+
# Add a column to the query
|
5
|
+
#
|
6
|
+
# @param column [String, Symbol, Hash] the column name (or a hash with table & column name)
|
7
|
+
# @option options [String] :function a function to wrap around the column
|
8
|
+
# @options options [String] :as the name to return this column as
|
9
|
+
# @return [Query] returns the query
|
10
|
+
def column(column, options = {})
|
11
|
+
@columns ||= []
|
12
|
+
with_table_and_column(column) do |table, column|
|
13
|
+
@columns << [].tap do |query|
|
14
|
+
if options[:function]
|
15
|
+
query << "#{escape_function(options[:function])}("
|
16
|
+
end
|
17
|
+
query << escape_and_join(table, column)
|
18
|
+
if options[:function]
|
19
|
+
query << ")"
|
20
|
+
end
|
21
|
+
if options[:as]
|
22
|
+
query << "AS"
|
23
|
+
query << escape(options[:as])
|
24
|
+
end
|
25
|
+
end.join(' ')
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Replace all existing columns with the given column
|
31
|
+
def column!(*args)
|
32
|
+
@columns = []
|
33
|
+
column(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/sqb/delete.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'sqb/base'
|
2
|
+
require 'sqb/filtering'
|
3
|
+
require 'sqb/ordering'
|
4
|
+
require 'sqb/limiting'
|
5
|
+
|
6
|
+
module SQB
|
7
|
+
class Delete < Base
|
8
|
+
|
9
|
+
include SQB::Filtering
|
10
|
+
include SQB::Ordering
|
11
|
+
include SQB::Limiting
|
12
|
+
|
13
|
+
def to_sql
|
14
|
+
[].tap do |query|
|
15
|
+
query << "DELETE FROM"
|
16
|
+
query << escape_and_join(@options[:database_name], @table_name)
|
17
|
+
|
18
|
+
if @where && !@where.empty?
|
19
|
+
query << "WHERE"
|
20
|
+
query << @where.join(' AND ')
|
21
|
+
end
|
22
|
+
|
23
|
+
if @orders && !@orders.empty?
|
24
|
+
query << "ORDER BY"
|
25
|
+
query << @orders.join(', ')
|
26
|
+
end
|
27
|
+
|
28
|
+
if @limit
|
29
|
+
query << "LIMIT #{@limit.to_i}"
|
30
|
+
end
|
31
|
+
end.join(' ')
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/sqb/distinct.rb
ADDED
data/lib/sqb/error.rb
CHANGED
data/lib/sqb/escaping.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module SQB
|
2
|
+
module Escaping
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def escape(name)
|
7
|
+
if name.is_a?(SafeString)
|
8
|
+
name
|
9
|
+
else
|
10
|
+
"`#{name.to_s.gsub('`', '``')}`"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def escape_function(name)
|
15
|
+
if name.is_a?(SafeString)
|
16
|
+
name
|
17
|
+
else
|
18
|
+
name.to_s.gsub(/[^a-z0-9\_]/i, '').upcase
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def value_escape(value)
|
23
|
+
if value == true
|
24
|
+
1
|
25
|
+
elsif value == false
|
26
|
+
0
|
27
|
+
elsif value.nil?
|
28
|
+
'NULL'
|
29
|
+
elsif value.is_a?(Integer)
|
30
|
+
value.to_i
|
31
|
+
else
|
32
|
+
@prepared_arguments << value.to_s
|
33
|
+
'?'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_table_and_column(input, &block)
|
38
|
+
if input.is_a?(Hash)
|
39
|
+
input.each { |table, column| block.call(table, column) }
|
40
|
+
else
|
41
|
+
block.call(@table_name, input)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def escape_and_join(*parts)
|
46
|
+
if parts.last.is_a?(SafeString)
|
47
|
+
# If a safe string is provided as a column name, we'll
|
48
|
+
# always use this even if a table name is provided too.
|
49
|
+
parts.last
|
50
|
+
else
|
51
|
+
parts.compact.map { |part| escape(part) }.join('.')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module SQB
|
2
|
+
module Filtering
|
3
|
+
|
4
|
+
# Add a condition to the query by providing a hash of keys and values.
|
5
|
+
#
|
6
|
+
# @param hash [Hash]
|
7
|
+
# @return [Query]
|
8
|
+
def where(hash)
|
9
|
+
if @where_within_or && @where_within_or.last
|
10
|
+
@where_within_or.last << hash
|
11
|
+
else
|
12
|
+
@where ||= []
|
13
|
+
@where << hash_to_sql(hash, @table_name)
|
14
|
+
end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set that all conditions added in this block should be joined using OR
|
19
|
+
# rather than AND.
|
20
|
+
def or(&block)
|
21
|
+
@where_within_or ||= []
|
22
|
+
# Start by making an array within the OR block for this calling
|
23
|
+
@where_within_or << []
|
24
|
+
# Execute the block. All queries to 'where' will be added to the last
|
25
|
+
# array in the chain (created above)
|
26
|
+
block.call
|
27
|
+
ensure
|
28
|
+
# Start work on a full array of SQL fragments for all OR queries
|
29
|
+
@where_within_or_sql ||= []
|
30
|
+
# After each OR call, store up the SQL fragment for all where queries
|
31
|
+
# executed within the block.
|
32
|
+
if w = @where_within_or.pop
|
33
|
+
@where_within_or_sql << w.map do |w|
|
34
|
+
hash_to_sql(w, @table_name)
|
35
|
+
end.join(' OR ')
|
36
|
+
end
|
37
|
+
|
38
|
+
# When there are no fragments in the chain left, add it to the main
|
39
|
+
# where chain for the query.
|
40
|
+
if @where_within_or.empty?
|
41
|
+
@where ||= []
|
42
|
+
@where << "(#{@where_within_or_sql.flatten.join(' OR ')})"
|
43
|
+
@where_within_or_sql = nil
|
44
|
+
end
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def hash_to_sql(hash, table, joiner = ' AND ')
|
51
|
+
sql = hash.map do |key, value|
|
52
|
+
if key.is_a?(Hash)
|
53
|
+
table = key.first[0]
|
54
|
+
key = key.first[1]
|
55
|
+
end
|
56
|
+
|
57
|
+
key = escape_and_join(table, key)
|
58
|
+
|
59
|
+
if value.is_a?(Array)
|
60
|
+
escaped_values = value.map { |v| value_escape(v) }.join(', ')
|
61
|
+
"#{key} IN (#{escaped_values})"
|
62
|
+
elsif value.is_a?(Hash)
|
63
|
+
sql = []
|
64
|
+
value.each do |operator, value|
|
65
|
+
case operator
|
66
|
+
when :not_equal
|
67
|
+
if value.nil?
|
68
|
+
sql << "#{key} IS NOT NULL"
|
69
|
+
else
|
70
|
+
sql << "#{key} != #{value_escape(value)}"
|
71
|
+
end
|
72
|
+
when :equal
|
73
|
+
if value.nil?
|
74
|
+
sql << "#{key} IS NULL"
|
75
|
+
else
|
76
|
+
sql << "#{key} = #{value_escape(value)}"
|
77
|
+
end
|
78
|
+
when :less_than
|
79
|
+
sql << "#{key} < #{value_escape(value)}"
|
80
|
+
when :greater_than
|
81
|
+
sql << "#{key} > #{value_escape(value)}"
|
82
|
+
when :less_than_or_equal_to
|
83
|
+
sql << "#{key} <= #{value_escape(value)}"
|
84
|
+
when :greater_than_or_equal_to
|
85
|
+
sql << "#{key} >= #{value_escape(value)}"
|
86
|
+
when :in, :not_in
|
87
|
+
escaped_values = value.map { |v| value_escape(v) }.join(', ')
|
88
|
+
op = operator == :in ? "IN" : "NOT IN"
|
89
|
+
sql << "#{key} #{op} (#{escaped_values})"
|
90
|
+
when :like
|
91
|
+
sql << "#{key} LIKE #{value_escape(value)}"
|
92
|
+
when :not_like
|
93
|
+
sql << "#{key} NOT LIKE #{value_escape(value)}"
|
94
|
+
else
|
95
|
+
raise InvalidOperatorError, "Invalid operator '#{operator}'"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
sql.empty? ? "1=0" : sql.join(joiner)
|
99
|
+
elsif value == nil
|
100
|
+
"#{key} IS NULL"
|
101
|
+
else
|
102
|
+
"#{key} = #{value_escape(value)}"
|
103
|
+
end
|
104
|
+
end.join(joiner)
|
105
|
+
"(#{sql})"
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
data/lib/sqb/grouping.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module SQB
|
2
|
+
module Grouping
|
3
|
+
|
4
|
+
# Add a grouping
|
5
|
+
#
|
6
|
+
# @param column [String, Symbol, Hash]
|
7
|
+
# @return [Query]
|
8
|
+
def group_by(column)
|
9
|
+
@groups ||= []
|
10
|
+
with_table_and_column(column) do |table, column|
|
11
|
+
@groups << escape_and_join(table, column)
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
data/lib/sqb/insert.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'sqb/base'
|
2
|
+
|
3
|
+
module SQB
|
4
|
+
class Insert < Base
|
5
|
+
|
6
|
+
def to_sql
|
7
|
+
[].tap do |query|
|
8
|
+
query << "INSERT INTO"
|
9
|
+
query << escape_and_join(@options[:database_name], @table_name)
|
10
|
+
if @values.nil? || @values.empty?
|
11
|
+
raise NoValuesError, "No values have been specified. Use `value` to add values to the query."
|
12
|
+
end
|
13
|
+
query << "(#{columns.join(', ')})"
|
14
|
+
query << "VALUES"
|
15
|
+
query << "(#{values.join(', ')})"
|
16
|
+
end.join(' ')
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set a value to be inserted
|
20
|
+
#
|
21
|
+
# @param key [String]
|
22
|
+
# @param value [String, nil]
|
23
|
+
def value(hash)
|
24
|
+
@values ||= {}
|
25
|
+
hash.each do |key, value|
|
26
|
+
@values[key] = value
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
alias_method :values, :value
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def columns
|
35
|
+
@values.keys.map { |k| escape(k) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def values
|
39
|
+
@values.values.map { |v| value_escape(v) }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
data/lib/sqb/joins.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module SQB
|
2
|
+
module Joins
|
3
|
+
|
4
|
+
# Add a join
|
5
|
+
#
|
6
|
+
# @param table_name [String, Symbol]
|
7
|
+
# @param foreign_key [String, Symbol]
|
8
|
+
# @option options [Hash] :where
|
9
|
+
# @option options [Array] :select
|
10
|
+
# @return [Query]
|
11
|
+
def join(table_name, foreign_key, options = {})
|
12
|
+
@joins ||= []
|
13
|
+
@joins_name_mapping ||= {}
|
14
|
+
|
15
|
+
if options[:name]
|
16
|
+
join_name = options[:name]
|
17
|
+
else
|
18
|
+
@joins_name_mapping[table_name] ||= 0
|
19
|
+
join_name= "#{table_name}_#{@joins_name_mapping[table_name]}"
|
20
|
+
@joins_name_mapping[table_name] += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
@joins << [].tap do |query|
|
24
|
+
query << "INNER JOIN"
|
25
|
+
query << escape_and_join(@options[:database_name], table_name)
|
26
|
+
query << "AS"
|
27
|
+
query << escape(join_name)
|
28
|
+
query << "ON"
|
29
|
+
query << escape_and_join(@table_name, 'id')
|
30
|
+
query << "="
|
31
|
+
query << escape_and_join(join_name, foreign_key)
|
32
|
+
end.join(' ')
|
33
|
+
|
34
|
+
if options[:where]
|
35
|
+
join_where = options[:where].each_with_object({}) do |(column, value), hash|
|
36
|
+
hash[{join_name => column}] = value
|
37
|
+
end
|
38
|
+
where(join_where)
|
39
|
+
end
|
40
|
+
|
41
|
+
if columns = options[:columns]
|
42
|
+
for field in columns
|
43
|
+
column({join_name => field}, :as => "#{join_name}_#{field}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if g = options[:group_by]
|
48
|
+
group_by(join_name => g.is_a?(Symbol) ? g : :id)
|
49
|
+
end
|
50
|
+
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
data/lib/sqb/limiting.rb
ADDED
data/lib/sqb/ordering.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module SQB
|
2
|
+
module Ordering
|
3
|
+
|
4
|
+
VALID_ORDERS = ['ASC', 'DESC']
|
5
|
+
|
6
|
+
# Add an order column
|
7
|
+
#
|
8
|
+
# @param column [String, Symbol, Hash]
|
9
|
+
# @param direction [String] 'ASC' or 'DESC' (default 'ASC')
|
10
|
+
# @return [Query]
|
11
|
+
def order(column, direction = nil)
|
12
|
+
direction = direction ? direction.to_s.upcase : 'ASC'
|
13
|
+
|
14
|
+
unless VALID_ORDERS.include?(direction)
|
15
|
+
raise InvalidOrderDirectionError, "Invalid order direction #{direction}"
|
16
|
+
end
|
17
|
+
|
18
|
+
@orders ||= []
|
19
|
+
|
20
|
+
with_table_and_column(column) do |table, column|
|
21
|
+
@orders << [escape_and_join(table, column), direction].join(' ')
|
22
|
+
end
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add an order replacing all previous ones
|
28
|
+
def order!(*args)
|
29
|
+
@orders = []
|
30
|
+
order(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Remove all ordering for this query
|
34
|
+
def no_order!
|
35
|
+
@orders = []
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/sqb/query.rb
CHANGED
@@ -1,375 +1,6 @@
|
|
1
|
-
require 'sqb/
|
1
|
+
require 'sqb/select'
|
2
2
|
|
3
3
|
module SQB
|
4
|
-
class Query
|
5
|
-
|
6
|
-
attr_reader :prepared_arguments
|
7
|
-
|
8
|
-
VALID_ORDERS = ['ASC', 'DESC']
|
9
|
-
|
10
|
-
def initialize(table_name, options = {}, &escape_block)
|
11
|
-
@table_name = table_name
|
12
|
-
@columns = []
|
13
|
-
@joins = []
|
14
|
-
@joins_name_mapping = {}
|
15
|
-
@where = []
|
16
|
-
@orders = []
|
17
|
-
@groups = []
|
18
|
-
@limit = nil
|
19
|
-
@offset = nil
|
20
|
-
@distinct = false
|
21
|
-
@where_within_or = []
|
22
|
-
@options = options
|
23
|
-
@prepared_arguments = []
|
24
|
-
|
25
|
-
if @options[:prepared] == false && escape_block.nil?
|
26
|
-
raise EscapeBlockMissingError, "An escape block must be provided if prepared statements are disabled."
|
27
|
-
else
|
28
|
-
@escape_block = escape_block
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# Generate the full SQL query for this query.
|
33
|
-
#
|
34
|
-
# @return [String]
|
35
|
-
def to_sql
|
36
|
-
[].tap do |query|
|
37
|
-
query << "SELECT"
|
38
|
-
query << "DISTINCT" if @distinct
|
39
|
-
if @columns.empty?
|
40
|
-
query << escape_and_join(@table_name, '*')
|
41
|
-
else
|
42
|
-
query << @columns.join(', ')
|
43
|
-
end
|
44
|
-
query << "FROM"
|
45
|
-
query << escape_and_join(@options[:database_name], @table_name)
|
46
|
-
|
47
|
-
unless @joins.empty?
|
48
|
-
query << @joins.join(' ')
|
49
|
-
end
|
50
|
-
|
51
|
-
unless @where.empty?
|
52
|
-
query << "WHERE"
|
53
|
-
query << @where.join(' AND ')
|
54
|
-
end
|
55
|
-
|
56
|
-
unless @groups.empty?
|
57
|
-
query << "GROUP BY"
|
58
|
-
query << @groups.join(', ')
|
59
|
-
end
|
60
|
-
|
61
|
-
unless @orders.empty?
|
62
|
-
query << "ORDER BY"
|
63
|
-
query << @orders.join(', ')
|
64
|
-
end
|
65
|
-
|
66
|
-
if @limit
|
67
|
-
query << "LIMIT #{@limit.to_i}"
|
68
|
-
end
|
69
|
-
|
70
|
-
if @offset
|
71
|
-
query << "OFFSET #{@offset.to_i}"
|
72
|
-
end
|
73
|
-
end.join(' ')
|
74
|
-
end
|
75
|
-
|
76
|
-
# Add a column to the query
|
77
|
-
#
|
78
|
-
# @param column [String, Symbol, Hash] the column name (or a hash with table & column name)
|
79
|
-
# @option options [String] :function a function to wrap around the column
|
80
|
-
# @options options [String] :as the name to return this column as
|
81
|
-
# @return [Query] returns the query
|
82
|
-
def column(column, options = {})
|
83
|
-
with_table_and_column(column) do |table, column|
|
84
|
-
@columns << [].tap do |query|
|
85
|
-
if options[:function]
|
86
|
-
query << "#{escape_function(options[:function])}("
|
87
|
-
end
|
88
|
-
query << escape_and_join(table, column)
|
89
|
-
if options[:function]
|
90
|
-
query << ")"
|
91
|
-
end
|
92
|
-
if options[:as]
|
93
|
-
query << "AS"
|
94
|
-
query << escape(options[:as])
|
95
|
-
end
|
96
|
-
end.join(' ')
|
97
|
-
end
|
98
|
-
self
|
99
|
-
end
|
100
|
-
|
101
|
-
# Replace all existing columns with the given column
|
102
|
-
def column!(*args)
|
103
|
-
@columns = []
|
104
|
-
column(*args)
|
105
|
-
end
|
106
|
-
|
107
|
-
# Add a condition to the query by providing a hash of keys and values.
|
108
|
-
#
|
109
|
-
# @param hash [Hash]
|
110
|
-
# @return [Query]
|
111
|
-
def where(hash)
|
112
|
-
if @where_within_or.last
|
113
|
-
@where_within_or.last << hash
|
114
|
-
else
|
115
|
-
@where << hash_to_sql(hash, @table_name)
|
116
|
-
end
|
117
|
-
self
|
118
|
-
end
|
119
|
-
|
120
|
-
# Set that all conditions added in this block should be joined using OR
|
121
|
-
# rather than AND.
|
122
|
-
def or(&block)
|
123
|
-
# Start by making an array within the OR block for this calling
|
124
|
-
@where_within_or << []
|
125
|
-
# Execute the block. All queries to 'where' will be added to the last
|
126
|
-
# array in the chain (created above)
|
127
|
-
block.call
|
128
|
-
ensure
|
129
|
-
# Start work on a full array of SQL fragments for all OR queries
|
130
|
-
@where_within_or_sql ||= []
|
131
|
-
# After each OR call, store up the SQL fragment for all where queries
|
132
|
-
# executed within the block.
|
133
|
-
if w = @where_within_or.pop
|
134
|
-
@where_within_or_sql << w.map do |w|
|
135
|
-
hash_to_sql(w, @table_name)
|
136
|
-
end.join(' OR ')
|
137
|
-
end
|
138
|
-
|
139
|
-
# When there are no fragments in the chain left, add it to the main
|
140
|
-
# where chain for the query.
|
141
|
-
if @where_within_or.empty?
|
142
|
-
@where << "(#{@where_within_or_sql.flatten.join(' OR ')})"
|
143
|
-
@where_within_or_sql = nil
|
144
|
-
end
|
145
|
-
self
|
146
|
-
end
|
147
|
-
|
148
|
-
# Limit the number of records return
|
149
|
-
#
|
150
|
-
# @param number [Integer]
|
151
|
-
# @return [Query]
|
152
|
-
def limit(number)
|
153
|
-
@limit = number&.to_i
|
154
|
-
self
|
155
|
-
end
|
156
|
-
|
157
|
-
# Set the offset
|
158
|
-
#
|
159
|
-
# @param number [Integer]
|
160
|
-
# @return [Query]
|
161
|
-
def offset(number)
|
162
|
-
@offset = number&.to_i
|
163
|
-
self
|
164
|
-
end
|
165
|
-
|
166
|
-
# Add an order column
|
167
|
-
#
|
168
|
-
# @param column [String, Symbol, Hash]
|
169
|
-
# @param direction [String] 'ASC' or 'DESC' (default 'ASC')
|
170
|
-
# @return [Query]
|
171
|
-
def order(column, direction = nil)
|
172
|
-
direction = direction ? direction.to_s.upcase : 'ASC'
|
173
|
-
|
174
|
-
unless VALID_ORDERS.include?(direction)
|
175
|
-
raise InvalidOrderDirectionError, "Invalid order direction #{direction}"
|
176
|
-
end
|
177
|
-
|
178
|
-
with_table_and_column(column) do |table, column|
|
179
|
-
@orders << [escape_and_join(table, column), direction].join(' ')
|
180
|
-
end
|
181
|
-
|
182
|
-
self
|
183
|
-
end
|
184
|
-
|
185
|
-
# Add an order replacing all previous ones
|
186
|
-
def order!(*args)
|
187
|
-
@orders = []
|
188
|
-
order(*args)
|
189
|
-
end
|
190
|
-
|
191
|
-
# Remove all ordering for this query
|
192
|
-
def no_order!
|
193
|
-
@orders = []
|
194
|
-
end
|
195
|
-
|
196
|
-
# Add a grouping
|
197
|
-
#
|
198
|
-
# @param column [String, Symbol, Hash]
|
199
|
-
# @return [Query]
|
200
|
-
def group_by(column)
|
201
|
-
with_table_and_column(column) do |table, column|
|
202
|
-
@groups << escape_and_join(table, column)
|
203
|
-
end
|
204
|
-
self
|
205
|
-
end
|
206
|
-
|
207
|
-
# Add a join
|
208
|
-
#
|
209
|
-
# @param table_name [String, Symbol]
|
210
|
-
# @param foreign_key [String, Symbol]
|
211
|
-
# @option options [Hash] :where
|
212
|
-
# @option options [Array] :select
|
213
|
-
# @return [Query]
|
214
|
-
def join(table_name, foreign_key, options = {})
|
215
|
-
|
216
|
-
if options[:name]
|
217
|
-
join_name = options[:name]
|
218
|
-
else
|
219
|
-
@joins_name_mapping[table_name] ||= 0
|
220
|
-
join_name= "#{table_name}_#{@joins_name_mapping[table_name]}"
|
221
|
-
@joins_name_mapping[table_name] += 1
|
222
|
-
end
|
223
|
-
|
224
|
-
@joins << [].tap do |query|
|
225
|
-
query << "INNER JOIN"
|
226
|
-
query << escape_and_join(@options[:database_name], table_name)
|
227
|
-
query << "AS"
|
228
|
-
query << escape(join_name)
|
229
|
-
query << "ON"
|
230
|
-
query << escape_and_join(@table_name, 'id')
|
231
|
-
query << "="
|
232
|
-
query << escape_and_join(join_name, foreign_key)
|
233
|
-
end.join(' ')
|
234
|
-
|
235
|
-
if options[:where]
|
236
|
-
join_where = options[:where].each_with_object({}) do |(column, value), hash|
|
237
|
-
hash[{join_name => column}] = value
|
238
|
-
end
|
239
|
-
where(join_where)
|
240
|
-
end
|
241
|
-
|
242
|
-
if columns = options[:columns]
|
243
|
-
for field in columns
|
244
|
-
column({join_name => field}, :as => "#{join_name}_#{field}")
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
if g = options[:group_by]
|
249
|
-
group_by(join_name => g.is_a?(Symbol) ? g : :id)
|
250
|
-
end
|
251
|
-
|
252
|
-
self
|
253
|
-
end
|
254
|
-
|
255
|
-
def distinct
|
256
|
-
@distinct = true
|
257
|
-
self
|
258
|
-
end
|
259
|
-
|
260
|
-
private
|
261
|
-
|
262
|
-
def hash_to_sql(hash, table, joiner = ' AND ')
|
263
|
-
sql = hash.map do |key, value|
|
264
|
-
if key.is_a?(Hash)
|
265
|
-
table = key.first[0]
|
266
|
-
key = key.first[1]
|
267
|
-
end
|
268
|
-
|
269
|
-
key = escape_and_join(table, key)
|
270
|
-
|
271
|
-
if value.is_a?(Array)
|
272
|
-
escaped_values = value.map { |v| value_escape(v) }.join(', ')
|
273
|
-
"#{key} IN (#{escaped_values})"
|
274
|
-
elsif value.is_a?(Hash)
|
275
|
-
sql = []
|
276
|
-
value.each do |operator, value|
|
277
|
-
case operator
|
278
|
-
when :not_equal
|
279
|
-
if value.nil?
|
280
|
-
sql << "#{key} IS NOT NULL"
|
281
|
-
else
|
282
|
-
sql << "#{key} != #{value_escape(value)}"
|
283
|
-
end
|
284
|
-
when :equal
|
285
|
-
if value.nil?
|
286
|
-
sql << "#{key} IS NULL"
|
287
|
-
else
|
288
|
-
sql << "#{key} = #{value_escape(value)}"
|
289
|
-
end
|
290
|
-
when :less_than
|
291
|
-
sql << "#{key} < #{value_escape(value)}"
|
292
|
-
when :greater_than
|
293
|
-
sql << "#{key} > #{value_escape(value)}"
|
294
|
-
when :less_than_or_equal_to
|
295
|
-
sql << "#{key} <= #{value_escape(value)}"
|
296
|
-
when :greater_than_or_equal_to
|
297
|
-
sql << "#{key} >= #{value_escape(value)}"
|
298
|
-
when :in, :not_in
|
299
|
-
escaped_values = value.map { |v| value_escape(v) }.join(', ')
|
300
|
-
op = operator == :in ? "IN" : "NOT IN"
|
301
|
-
sql << "#{key} #{op} (#{escaped_values})"
|
302
|
-
when :like
|
303
|
-
sql << "#{key} LIKE #{value_escape(value)}"
|
304
|
-
when :not_like
|
305
|
-
sql << "#{key} NOT LIKE #{value_escape(value)}"
|
306
|
-
else
|
307
|
-
raise InvalidOperatorError, "Invalid operator '#{operator}'"
|
308
|
-
end
|
309
|
-
end
|
310
|
-
sql.empty? ? "1=0" : sql.join(joiner)
|
311
|
-
elsif value == nil
|
312
|
-
"#{key} IS NULL"
|
313
|
-
else
|
314
|
-
"#{key} = #{value_escape(value)}"
|
315
|
-
end
|
316
|
-
end.join(joiner)
|
317
|
-
"(#{sql})"
|
318
|
-
end
|
319
|
-
|
320
|
-
def escape(name)
|
321
|
-
if name.is_a?(SafeString)
|
322
|
-
name
|
323
|
-
else
|
324
|
-
"`#{name.to_s.gsub('`', '``')}`"
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
|
-
def escape_function(name)
|
329
|
-
if name.is_a?(SafeString)
|
330
|
-
name
|
331
|
-
else
|
332
|
-
name.to_s.gsub(/[^a-z0-9\_]/i, '').upcase
|
333
|
-
end
|
334
|
-
end
|
335
|
-
|
336
|
-
def value_escape(value)
|
337
|
-
if value == true
|
338
|
-
1
|
339
|
-
elsif value == false
|
340
|
-
0
|
341
|
-
elsif value.nil?
|
342
|
-
'NULL'
|
343
|
-
elsif value.is_a?(Integer)
|
344
|
-
value.to_i
|
345
|
-
else
|
346
|
-
if @options[:prepared] == false
|
347
|
-
escaped_value = @escape_block ? @escape_block.call(value.to_s) : value.to_s
|
348
|
-
"'" + escaped_value + "'"
|
349
|
-
else
|
350
|
-
@prepared_arguments << value.to_s
|
351
|
-
'?'
|
352
|
-
end
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
def with_table_and_column(input, &block)
|
357
|
-
if input.is_a?(Hash)
|
358
|
-
input.each { |table, column| block.call(table, column) }
|
359
|
-
else
|
360
|
-
block.call(@table_name, input)
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
def escape_and_join(*parts)
|
365
|
-
if parts.last.is_a?(SafeString)
|
366
|
-
# If a safe string is provided as a column name, we'll
|
367
|
-
# always use this even if a table name is provided too.
|
368
|
-
parts.last
|
369
|
-
else
|
370
|
-
parts.compact.map { |part| escape(part) }.join('.')
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
4
|
+
class Query < Select
|
374
5
|
end
|
375
6
|
end
|
data/lib/sqb/safe_string.rb
CHANGED
data/lib/sqb/select.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'sqb/base'
|
2
|
+
require 'sqb/distinct'
|
3
|
+
require 'sqb/columns'
|
4
|
+
require 'sqb/filtering'
|
5
|
+
require 'sqb/joins'
|
6
|
+
require 'sqb/ordering'
|
7
|
+
require 'sqb/grouping'
|
8
|
+
require 'sqb/limiting'
|
9
|
+
require 'sqb/offsetting'
|
10
|
+
|
11
|
+
module SQB
|
12
|
+
class Select < Base
|
13
|
+
|
14
|
+
include SQB::Distinct
|
15
|
+
include SQB::Columns
|
16
|
+
include SQB::Filtering
|
17
|
+
include SQB::Joins
|
18
|
+
include SQB::Ordering
|
19
|
+
include SQB::Grouping
|
20
|
+
include SQB::Limiting
|
21
|
+
include SQB::Offsetting
|
22
|
+
|
23
|
+
def to_sql
|
24
|
+
[].tap do |query|
|
25
|
+
query << "SELECT"
|
26
|
+
query << "DISTINCT" if @distinct
|
27
|
+
if @columns.nil? || @columns.empty?
|
28
|
+
query << escape_and_join(@table_name, '*')
|
29
|
+
else
|
30
|
+
query << @columns.join(', ')
|
31
|
+
end
|
32
|
+
|
33
|
+
query << "FROM"
|
34
|
+
query << escape_and_join(@options[:database_name], @table_name)
|
35
|
+
|
36
|
+
if @joins && !@joins.empty?
|
37
|
+
query << @joins.join(' ')
|
38
|
+
end
|
39
|
+
|
40
|
+
if @where && !@where.empty?
|
41
|
+
query << "WHERE"
|
42
|
+
query << @where.join(' AND ')
|
43
|
+
end
|
44
|
+
|
45
|
+
if @groups && !@groups.empty?
|
46
|
+
query << "GROUP BY"
|
47
|
+
query << @groups.join(', ')
|
48
|
+
end
|
49
|
+
|
50
|
+
if @orders && !@orders.empty?
|
51
|
+
query << "ORDER BY"
|
52
|
+
query << @orders.join(', ')
|
53
|
+
end
|
54
|
+
|
55
|
+
if @limit
|
56
|
+
query << "LIMIT #{@limit.to_i}"
|
57
|
+
end
|
58
|
+
|
59
|
+
if @offset
|
60
|
+
query << "OFFSET #{@offset.to_i}"
|
61
|
+
end
|
62
|
+
end.join(' ')
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
data/lib/sqb/update.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'sqb/base'
|
2
|
+
require 'sqb/filtering'
|
3
|
+
require 'sqb/limiting'
|
4
|
+
|
5
|
+
module SQB
|
6
|
+
class Update < Base
|
7
|
+
|
8
|
+
include SQB::Filtering
|
9
|
+
include SQB::Limiting
|
10
|
+
|
11
|
+
def to_sql
|
12
|
+
[].tap do |query|
|
13
|
+
query << "UPDATE"
|
14
|
+
query << escape_and_join(@options[:database_name], @table_name)
|
15
|
+
query << "SET"
|
16
|
+
if @sets && !@sets.empty?
|
17
|
+
query << @sets.map do |key, value|
|
18
|
+
"#{escape_and_join(@table_name, key)} = #{value_escape(value)}"
|
19
|
+
end.join(', ')
|
20
|
+
else
|
21
|
+
raise NoValuesError, "No values have been updated. Use `set` to set the values to update."
|
22
|
+
end
|
23
|
+
|
24
|
+
if @where && !@where.empty?
|
25
|
+
query << "WHERE"
|
26
|
+
query << @where.join(' AND ')
|
27
|
+
end
|
28
|
+
|
29
|
+
if @limit
|
30
|
+
query << "LIMIT #{@limit.to_i}"
|
31
|
+
end
|
32
|
+
|
33
|
+
if @offset
|
34
|
+
query << "OFFSET #{@offset.to_i}"
|
35
|
+
end
|
36
|
+
end.join(' ')
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set a value to be updated
|
40
|
+
#
|
41
|
+
# @param key [String]
|
42
|
+
# @param value [String, nil]
|
43
|
+
def set(hash)
|
44
|
+
@sets ||= {}
|
45
|
+
hash.each do |key, value|
|
46
|
+
@sets[key] = value
|
47
|
+
end
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/sqb/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sqb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Cooke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-02-
|
11
|
+
date: 2018-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A friendly SQL builder for MySQL.
|
14
14
|
email:
|
@@ -18,9 +18,23 @@ extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
20
|
- lib/sqb.rb
|
21
|
+
- lib/sqb/base.rb
|
22
|
+
- lib/sqb/columns.rb
|
23
|
+
- lib/sqb/delete.rb
|
24
|
+
- lib/sqb/distinct.rb
|
21
25
|
- lib/sqb/error.rb
|
26
|
+
- lib/sqb/escaping.rb
|
27
|
+
- lib/sqb/filtering.rb
|
28
|
+
- lib/sqb/grouping.rb
|
29
|
+
- lib/sqb/insert.rb
|
30
|
+
- lib/sqb/joins.rb
|
31
|
+
- lib/sqb/limiting.rb
|
32
|
+
- lib/sqb/offsetting.rb
|
33
|
+
- lib/sqb/ordering.rb
|
22
34
|
- lib/sqb/query.rb
|
23
35
|
- lib/sqb/safe_string.rb
|
36
|
+
- lib/sqb/select.rb
|
37
|
+
- lib/sqb/update.rb
|
24
38
|
- lib/sqb/version.rb
|
25
39
|
homepage: https://github.com/adamcooke/sqb
|
26
40
|
licenses:
|