simple-sql 0.4.5 → 0.4.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a83b1690cf4e3b75f7e974df31e7c8b29680e198
4
- data.tar.gz: 333ca9858a0cc4eb06519d1eadee61fb875939f2
3
+ metadata.gz: a8c0b2ffb0ec662dd3a487855cac5b76d97a1970
4
+ data.tar.gz: 60a5c103b6d7970a4adca867a0f547923c5c03f9
5
5
  SHA512:
6
- metadata.gz: ce35aefb5d57370cb6fd88b1c25a5a9ebe3d767ce8bbb2e1b4433726e6968511ffa5a82859ae2b012b2f189b12761c20a266d7a21556f89303ac6f67c3ed4332
7
- data.tar.gz: 812bc3709085da01e1fcd91a10f6c5c864a34f5a5fddb3edea5885ba22fcd0d5b273a8d6d300482d0a03c41ed14120c7c710aaa9e4e33a6489774d44782c3847
6
+ metadata.gz: fd80d308c336f2c5ecca6d49ec3f5fff516adb3adb59048bccce9ac13c89996f21821be1795e6b925a7f58b1ec3198965ceeffa5f90bb4786193d415cc0efa18
7
+ data.tar.gz: 7930a7c788dca9588bc56ef8ba4a65bd5b42fb6aaef018bbcad6dd1ff1baacc2b1608b6526548a4553c654f265ca44a74481ace1c535be82d6f496347d68b588
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple-sql (0.4.5)
4
+ simple-sql (0.4.7)
5
5
  pg (~> 0.20)
6
6
  pg_array_parser (~> 0)
7
7
 
@@ -1,10 +1,8 @@
1
- # rubocop:disable Style/Not
2
1
  # rubocop:disable Style/MultipleComparison
3
- # rubocop:disable Style/IfUnlessModifier
4
- # rubocop:disable Metrics/AbcSize
5
- # rubocop:disable Metrics/CyclomaticComplexity
6
- # rubocop:disable Metrics/MethodLength
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
- active_filters = @filters.compact
127
- unless active_filters.empty?
128
- sql += " WHERE (" + active_filters.join(") AND (") + ")"
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
@@ -1,5 +1,5 @@
1
1
  module Simple
2
2
  module SQL
3
- VERSION = "0.4.5"
3
+ VERSION = "0.4.7"
4
4
  end
5
5
  end
@@ -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) { 1.upto(2).map { create(:user) } }
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
@@ -30,7 +30,7 @@ ActiveRecord::Schema.define do
30
30
  t.integer :role_id
31
31
  t.string :first_name
32
32
  t.string :last_name
33
- t.hstore :meta_data
33
+ t.jsonb :metadata
34
34
  t.column :access_level, :access_level
35
35
 
36
36
  t.timestamps null: true
@@ -7,7 +7,7 @@ def sequence(pattern)
7
7
  end
8
8
  end
9
9
 
10
- def attrs(table)
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
- id = Simple::SQL.insert(table_name, attrs(table))
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.5
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-05-30 00:00:00.000000000 Z
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