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 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