simple-sql 0.4.5 → 0.4.7
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/Gemfile.lock +1 -1
- data/lib/simple/sql/scope.rb +7 -102
- data/lib/simple/sql/scope/filters.rb +111 -0
- data/lib/simple/sql/scope/order.rb +21 -0
- data/lib/simple/sql/scope/pagination.rb +31 -0
- data/lib/simple/sql/version.rb +1 -1
- data/spec/simple/sql_scope_spec.rb +46 -1
- data/spec/support/001_database.rb +1 -1
- data/spec/support/003_factories.rb +6 -5
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a8c0b2ffb0ec662dd3a487855cac5b76d97a1970
|
4
|
+
data.tar.gz: 60a5c103b6d7970a4adca867a0f547923c5c03f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd80d308c336f2c5ecca6d49ec3f5fff516adb3adb59048bccce9ac13c89996f21821be1795e6b925a7f58b1ec3198965ceeffa5f90bb4786193d415cc0efa18
|
7
|
+
data.tar.gz: 7930a7c788dca9588bc56ef8ba4a65bd5b42fb6aaef018bbcad6dd1ff1baacc2b1608b6526548a4553c654f265ca44a74481ace1c535be82d6f496347d68b588
|
data/Gemfile.lock
CHANGED
data/lib/simple/sql/scope.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
# rubocop:disable Style/Not
|
2
1
|
# rubocop:disable Style/MultipleComparison
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
2
|
+
|
3
|
+
require_relative "scope/filters.rb"
|
4
|
+
require_relative "scope/order.rb"
|
5
|
+
require_relative "scope/pagination.rb"
|
8
6
|
|
9
7
|
# The Simple::SQL::Scope class helps building scopes; i.e. objects
|
10
8
|
# that start as a quite basic SQL query, and allow one to add
|
@@ -14,7 +12,6 @@ class Simple::SQL::Scope
|
|
14
12
|
|
15
13
|
attr_reader :args
|
16
14
|
attr_reader :per, :page
|
17
|
-
attr_reader :order_by_fragment
|
18
15
|
|
19
16
|
# Build a scope object
|
20
17
|
def initialize(sql)
|
@@ -38,106 +35,14 @@ class Simple::SQL::Scope
|
|
38
35
|
|
39
36
|
public
|
40
37
|
|
41
|
-
# scope = Scope.new("SELECT * FROM tablename")
|
42
|
-
# scope = scope.where(id: 12)
|
43
|
-
# scope = scope.where("id > ?", 12)
|
44
|
-
#
|
45
|
-
# In the second form the placeholder (usually a '?') is being replaced
|
46
|
-
# with the numbered argument (since postgres is using $1, $2, etc.)
|
47
|
-
# If your SQL fragment uses '?' as part of some fixed text you must
|
48
|
-
# use an alternative placeholder symbol:
|
49
|
-
#
|
50
|
-
# scope = scope.where("foo | '?' = '^'", match, placeholder: '^')
|
51
|
-
#
|
52
|
-
def where(sql_fragment, arg = :__dummy__no__arg, placeholder: "?")
|
53
|
-
duplicate.send(:where!, sql_fragment, arg, placeholder: placeholder)
|
54
|
-
end
|
55
|
-
|
56
|
-
def where!(first_arg, arg = :__dummy__no__arg, placeholder: "?")
|
57
|
-
if arg != :__dummy__no__arg
|
58
|
-
where_sql_with_argument!(first_arg, arg, placeholder: placeholder)
|
59
|
-
elsif first_arg.is_a?(Hash)
|
60
|
-
where_hash!(first_arg)
|
61
|
-
else
|
62
|
-
where_sql!(first_arg)
|
63
|
-
end
|
64
|
-
|
65
|
-
self
|
66
|
-
end
|
67
|
-
|
68
|
-
def where_sql!(sql_fragment)
|
69
|
-
@filters << sql_fragment
|
70
|
-
end
|
71
|
-
|
72
|
-
def where_sql_with_argument!(sql_fragment, arg, placeholder:)
|
73
|
-
@args << arg
|
74
|
-
@filters << sql_fragment.gsub(placeholder, "$#{@args.length}")
|
75
|
-
end
|
76
|
-
|
77
|
-
def where_hash!(hsh)
|
78
|
-
hsh.each do |key, value|
|
79
|
-
raise ArgumentError, "condition key must be a Symbol or a String" unless key.is_a?(Symbol) || key.is_a?(String)
|
80
|
-
|
81
|
-
@args << value
|
82
|
-
|
83
|
-
case value
|
84
|
-
when Array
|
85
|
-
@filters << "#{key} = ANY($#{@args.length})"
|
86
|
-
else
|
87
|
-
@filters << "#{key} = $#{@args.length}"
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Set pagination
|
93
|
-
def paginate(per:, page:)
|
94
|
-
duplicate.send(:paginate!, per: per, page: page)
|
95
|
-
end
|
96
|
-
|
97
|
-
def paginate!(per:, page:)
|
98
|
-
@per = per
|
99
|
-
@page = page
|
100
|
-
|
101
|
-
self
|
102
|
-
end
|
103
|
-
|
104
|
-
# Adjust sort order
|
105
|
-
def order_by!(sql_fragment)
|
106
|
-
@order_by_fragment = sql_fragment
|
107
|
-
self
|
108
|
-
end
|
109
|
-
|
110
|
-
def order_by(sql_fragment)
|
111
|
-
duplicate.order_by!(sql_fragment)
|
112
|
-
end
|
113
|
-
|
114
|
-
public
|
115
|
-
|
116
|
-
# Is this a paginated scope?
|
117
|
-
def paginated?
|
118
|
-
not @per.nil?
|
119
|
-
end
|
120
|
-
|
121
38
|
# generate a sql query
|
122
39
|
def to_sql(pagination: :auto)
|
123
40
|
raise ArgumentError unless pagination == :auto || pagination == false
|
124
41
|
|
125
42
|
sql = @sql
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
end
|
130
|
-
|
131
|
-
if order_by_fragment
|
132
|
-
sql += " ORDER BY #{order_by_fragment}"
|
133
|
-
end
|
134
|
-
|
135
|
-
if pagination == :auto && @per && @page
|
136
|
-
raise ArgumentError, "per must be > 0" unless @per > 0
|
137
|
-
raise ArgumentError, "page must be > 0" unless @page > 0
|
138
|
-
|
139
|
-
sql += " LIMIT #{@per} OFFSET #{(@page - 1) * @per}"
|
140
|
-
end
|
43
|
+
sql = apply_filters(sql)
|
44
|
+
sql = apply_order(sql)
|
45
|
+
sql = apply_pagination(sql, pagination: pagination)
|
141
46
|
|
142
47
|
sql
|
143
48
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# rubocop:disable Style/IfUnlessModifier
|
2
|
+
# rubocop:disable Style/GuardClause
|
3
|
+
|
4
|
+
class Simple::SQL::Scope
|
5
|
+
# scope = Scope.new("SELECT * FROM tablename")
|
6
|
+
# scope = scope.where(id: 12)
|
7
|
+
# scope = scope.where("id > ?", 12)
|
8
|
+
#
|
9
|
+
# In the second form the placeholder (usually a '?') is being replaced
|
10
|
+
# with the numbered argument (since postgres is using $1, $2, etc.)
|
11
|
+
# If your SQL fragment uses '?' as part of some fixed text you must
|
12
|
+
# use an alternative placeholder symbol:
|
13
|
+
#
|
14
|
+
# scope = scope.where("foo | '?' = '^'", match, placeholder: '^')
|
15
|
+
#
|
16
|
+
# If a hash is passed in as a search condition and the value to match is
|
17
|
+
# a hash, this is translated into a JSONB query, which matches each of
|
18
|
+
# the passed in keys against one of the passed in values.
|
19
|
+
#
|
20
|
+
# scope = scope.where(metadata: { uid: 1, type: ["foo", "bar", "baz"] })
|
21
|
+
#
|
22
|
+
# This feature can be disabled using the `jsonb: false` option.
|
23
|
+
#
|
24
|
+
# scope = scope.where(metadata: { uid: 1 }, jsonb: false)
|
25
|
+
#
|
26
|
+
def where(sql_fragment, arg = :__dummy__no__arg, placeholder: "?", jsonb: true)
|
27
|
+
duplicate.send(:where!, sql_fragment, arg, placeholder: placeholder, jsonb: jsonb)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def where!(first_arg, arg = :__dummy__no__arg, placeholder: "?", jsonb: true)
|
33
|
+
if arg != :__dummy__no__arg
|
34
|
+
where_sql_with_argument!(first_arg, arg, placeholder: placeholder)
|
35
|
+
elsif first_arg.is_a?(Hash)
|
36
|
+
where_hash!(first_arg, jsonb: jsonb)
|
37
|
+
else
|
38
|
+
where_sql!(first_arg)
|
39
|
+
end
|
40
|
+
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def where_sql!(sql_fragment)
|
45
|
+
@filters << sql_fragment
|
46
|
+
end
|
47
|
+
|
48
|
+
def where_sql_with_argument!(sql_fragment, arg, placeholder:)
|
49
|
+
@args << arg
|
50
|
+
@filters << sql_fragment.gsub(placeholder, "$#{@args.length}")
|
51
|
+
end
|
52
|
+
|
53
|
+
def where_hash!(hsh, jsonb:)
|
54
|
+
hsh.each do |column, value|
|
55
|
+
validate_column! column
|
56
|
+
if value.is_a?(Hash) && jsonb
|
57
|
+
where_jsonb_condition!(column, value)
|
58
|
+
else
|
59
|
+
where_plain_condition!(column, value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
ID_REGEXP = /\A[A-Za-z0-9_\.]+\z/
|
65
|
+
|
66
|
+
def validate_column!(column)
|
67
|
+
unless column.is_a?(Symbol) || column.is_a?(String)
|
68
|
+
raise ArgumentError, "condition key #{column.inspect} must be a Symbol or a String"
|
69
|
+
end
|
70
|
+
unless column.to_sym =~ ID_REGEXP
|
71
|
+
raise ArgumentError, "condition key #{column.inspect} must match #{ID_REGEXP}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def jsonb_condition(column, key, value)
|
76
|
+
if !value.is_a?(Array)
|
77
|
+
"#{column} @> '#{::JSON.generate(key => value)}'"
|
78
|
+
elsif value.empty?
|
79
|
+
"FALSE"
|
80
|
+
else
|
81
|
+
individual_conditions = value.map do |v|
|
82
|
+
jsonb_condition(column, key, v)
|
83
|
+
end
|
84
|
+
"(#{individual_conditions.join(' OR ')})"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def where_jsonb_condition!(column, hsh)
|
89
|
+
hsh.each do |key, value|
|
90
|
+
@filters << jsonb_condition(column, key, value)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def where_plain_condition!(key, value)
|
95
|
+
@args << value
|
96
|
+
|
97
|
+
case value
|
98
|
+
when Array
|
99
|
+
@filters << "#{key} = ANY($#{@args.length})"
|
100
|
+
else
|
101
|
+
@filters << "#{key} = $#{@args.length}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def apply_filters(sql)
|
106
|
+
active_filters = @filters.compact
|
107
|
+
return sql if active_filters.empty?
|
108
|
+
|
109
|
+
"#{sql} WHERE (" + active_filters.join(") AND (") + ")"
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Simple::SQL::Scope
|
4
|
+
def order_by(sql_fragment)
|
5
|
+
duplicate.send(:order_by!, sql_fragment)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Adjust sort order
|
11
|
+
def order_by!(sql_fragment)
|
12
|
+
@order_by_fragment = sql_fragment
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
# called from to_sql
|
17
|
+
def apply_order(sql)
|
18
|
+
return sql unless @order_by_fragment
|
19
|
+
"#{sql} ORDER BY #{@order_by_fragment}"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# rubocop:disable Style/Not
|
2
|
+
|
3
|
+
class Simple::SQL::Scope
|
4
|
+
# Set pagination
|
5
|
+
def paginate(per:, page:)
|
6
|
+
duplicate.send(:paginate!, per: per, page: page)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is this a paginated scope?
|
10
|
+
def paginated?
|
11
|
+
not @per.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def paginate!(per:, page:)
|
17
|
+
@per = per
|
18
|
+
@page = page
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply_pagination(sql, pagination:)
|
24
|
+
return sql unless pagination == :auto && @per && @page
|
25
|
+
|
26
|
+
raise ArgumentError, "per must be > 0" unless @per > 0
|
27
|
+
raise ArgumentError, "page must be > 0" unless @page > 0
|
28
|
+
|
29
|
+
"#{sql} LIMIT #{@per} OFFSET #{(@page - 1) * @per}"
|
30
|
+
end
|
31
|
+
end
|
data/lib/simple/sql/version.rb
CHANGED
@@ -5,7 +5,9 @@ describe "Simple::SQL::Scope" do
|
|
5
5
|
expect(SQL.ask(sql, *args)).to eq(expected_result)
|
6
6
|
end
|
7
7
|
|
8
|
-
let!(:users)
|
8
|
+
let!(:users) do
|
9
|
+
1.upto(2).map { |id| create(:user, id: id) }
|
10
|
+
end
|
9
11
|
|
10
12
|
it 'allows chaining of scopes' do
|
11
13
|
scope1 = SQL::Scope.new "SELECT 1, 2 FROM users"
|
@@ -156,6 +158,49 @@ describe "Simple::SQL::Scope" do
|
|
156
158
|
expect(SQL.ask(scope)).to be_nil
|
157
159
|
end
|
158
160
|
end
|
161
|
+
|
162
|
+
describe "hash matches" do
|
163
|
+
let(:scope) { SQL::Scope.new("SELECT id FROM users") }
|
164
|
+
|
165
|
+
it 'validates hash keys' do
|
166
|
+
expect {
|
167
|
+
scope.where("foo bar" => "baz")
|
168
|
+
}.to raise_error(ArgumentError)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "JSONB matches" do
|
173
|
+
before do
|
174
|
+
SQL.exec <<~SQL
|
175
|
+
UPDATE users SET metadata = '{"type": "user"}';
|
176
|
+
UPDATE users SET metadata = jsonb_set(metadata, '{uid}', to_json(id)::jsonb);
|
177
|
+
SQL
|
178
|
+
end
|
179
|
+
|
180
|
+
def ids_matching(condition)
|
181
|
+
scope = SQL::Scope.new("SELECT id FROM users")
|
182
|
+
scope = scope.where(condition)
|
183
|
+
SQL.all(scope)
|
184
|
+
end
|
185
|
+
|
186
|
+
it "runs with SQL.ask" do
|
187
|
+
# exact match
|
188
|
+
expect(ids_matching(metadata: { "uid" => 1 })).to contain_exactly(1)
|
189
|
+
|
190
|
+
# match against array
|
191
|
+
expect(ids_matching(metadata: { "uid" => [] })).to contain_exactly()
|
192
|
+
expect(ids_matching(metadata: { "uid" => [1, -1] })).to contain_exactly(1)
|
193
|
+
expect(ids_matching(metadata: { "uid" => [1, 2] })).to contain_exactly(1, 2)
|
194
|
+
|
195
|
+
# match against array of mixed types
|
196
|
+
expect(ids_matching(metadata: { "uid" => [1, "-1"] })).to contain_exactly(1)
|
197
|
+
|
198
|
+
# match against multiple conditions
|
199
|
+
expect(ids_matching(metadata: { "uid" => [1, "-1"], "type" => "foo" })).to contain_exactly()
|
200
|
+
expect(ids_matching(metadata: { "uid" => [1, "-1"], "type" => ["foo", "user"] })).to contain_exactly(1)
|
201
|
+
expect(ids_matching(metadata: { "uid" => [1, "-1"], "type" => [] })).to contain_exactly()
|
202
|
+
end
|
203
|
+
end
|
159
204
|
end
|
160
205
|
|
161
206
|
context "describe pagination" do
|
@@ -7,7 +7,7 @@ def sequence(pattern)
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
10
|
+
def table_attrs(table)
|
11
11
|
case table
|
12
12
|
when :user
|
13
13
|
{
|
@@ -15,19 +15,20 @@ def attrs(table)
|
|
15
15
|
first_name: sequence("First {{sequence}}"),
|
16
16
|
last_name: sequence("Last {{sequence}}"),
|
17
17
|
access_level: "viewable"
|
18
|
-
}
|
18
|
+
}.freeze
|
19
19
|
when :unique_user
|
20
20
|
{
|
21
21
|
first_name: sequence("First {{sequence}}"),
|
22
22
|
last_name: sequence("Last {{sequence}}")
|
23
|
-
}
|
23
|
+
}.freeze
|
24
24
|
else
|
25
25
|
raise ArgumentError, "Invalid table for factory: #{table.inspect}"
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
def create(table)
|
29
|
+
def create(table, attrs = {})
|
30
30
|
table_name = table.to_s.pluralize
|
31
|
-
|
31
|
+
attrs = table_attrs(table).merge(attrs)
|
32
|
+
id = Simple::SQL.insert(table_name, attrs)
|
32
33
|
Simple::SQL.ask("SELECT * FROM #{table_name} WHERE id=$1", id, into: OpenStruct)
|
33
34
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple-sql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- radiospiel
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-
|
12
|
+
date: 2018-06-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: pg_array_parser
|
@@ -178,6 +178,9 @@ files:
|
|
178
178
|
- lib/simple/sql/logging.rb
|
179
179
|
- lib/simple/sql/reflection.rb
|
180
180
|
- lib/simple/sql/scope.rb
|
181
|
+
- lib/simple/sql/scope/filters.rb
|
182
|
+
- lib/simple/sql/scope/order.rb
|
183
|
+
- lib/simple/sql/scope/pagination.rb
|
181
184
|
- lib/simple/sql/simple_transactions.rb
|
182
185
|
- lib/simple/sql/version.rb
|
183
186
|
- log/.gitkeep
|