simple-sql 0.4.1 → 0.4.2

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: 4aa26b5a85a0c280fbabaeb6496a48b4e4f614b2
4
- data.tar.gz: b81cffbc2356054f595383a72b72b49580e2fef5
3
+ metadata.gz: 54dcb90e07a18f552fcaa0f48a29ca7e60dd3888
4
+ data.tar.gz: ea2093eb587de387ecff564f46f4c86857cd5e5c
5
5
  SHA512:
6
- metadata.gz: fb5ed3c2ae8b584013c8bf3c6e5b658037f7a17b650058b2556029724f4ee01fcad775a46a7cdbc5296a1b6eca44caafafabce119466e2c71f4111e9e5fb00d6
7
- data.tar.gz: 098003ee4e34f9b29003a96ba4d479af1054685320f12a9cb9c93eebd7668cdacdd903e2dc76c992e558b038386cc09c93715200d1bc7f85f3ff517c11e19dd3
6
+ metadata.gz: 35493961c28b9140d4a18508acc4d7bfe28c638fff760418173cfee3f583ba643d427ca4670eb5c4dd2d812ea7c63347b9d37bb4c82ed560d73f1651a9845199
7
+ data.tar.gz: 5978e812fec0928e0ff5dcf020409775c55406d20b635244a71a238a74fe094368bc02453106028d848594affbbcfde068982fb1b1a3b5db90c83e211cdc9d17
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- simple-sql (0.4.1)
4
+ simple-sql (0.4.2)
5
5
  pg (~> 0.20)
6
6
  pg_array_parser (~> 0)
7
7
 
data/lib/simple/sql.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "sql/encoder.rb"
7
7
  require_relative "sql/config.rb"
8
8
  require_relative "sql/logging.rb"
9
9
  require_relative "sql/simple_transactions.rb"
10
+ require_relative "sql/scope.rb"
10
11
  require_relative "sql/connection_adapter.rb"
11
12
  require_relative "sql/connection.rb"
12
13
  require_relative "sql/reflection.rb"
@@ -1,3 +1,5 @@
1
+ # rubocop:disable Style/IfUnlessModifier
2
+ # rubocop:disable Metrics/AbcSize
1
3
  # rubocop:disable Metrics/MethodLength
2
4
 
3
5
  # This module implements an adapter between the Simple::SQL interface
@@ -6,9 +8,10 @@
6
8
  # This module can be mixed onto objects that implement a raw_connection
7
9
  # method, which must return a Pg::Connection.
8
10
  module Simple::SQL::ConnectionAdapter
9
- Logging = Simple::SQL::Logging
10
- Encoder = Simple::SQL::Encoder
11
- Decoder = Simple::SQL::Decoder
11
+ Logging = ::Simple::SQL::Logging
12
+ Encoder = ::Simple::SQL::Encoder
13
+ Decoder = ::Simple::SQL::Decoder
14
+ Scope = ::Simple::SQL::Scope
12
15
 
13
16
  # execute one or more sql statements. This method does not allow to pass in
14
17
  # arguments - since the pg client does not support this - but it allows to
@@ -35,7 +38,11 @@ module Simple::SQL::ConnectionAdapter
35
38
 
36
39
  def all(sql, *args, into: nil, &block)
37
40
  result = exec_logged(sql, *args)
38
- enumerate(result, into: into, &block)
41
+ result = enumerate(result, into: into, &block)
42
+ if sql.is_a?(Scope) && sql.paginated?
43
+ add_page_info(sql, result)
44
+ end
45
+ result
39
46
  end
40
47
 
41
48
  # Runs a query and returns the first result row of a query.
@@ -53,7 +60,30 @@ module Simple::SQL::ConnectionAdapter
53
60
  end
54
61
  end
55
62
 
56
- def exec_logged(sql, *args)
63
+ def add_page_info(scope, results)
64
+ raise ArgumentError, "expect Array but get a #{results.class.name}" unless results.is_a?(Array)
65
+ raise ArgumentError, "per must be > 0" unless scope.per > 0
66
+
67
+ # optimization: add empty case (page <= 1 && results.empty?)
68
+ if scope.page <= 1 && results.empty?
69
+ Scope::PageInfo.attach(results, total_count: 0, per: scope.per, page: scope.page)
70
+ else
71
+ sql = "SELECT COUNT(*) FROM (#{scope.to_sql(pagination: false)}) simple_sql_count"
72
+ total_count = ask(sql, *scope.args)
73
+ Scope::PageInfo.attach(results, total_count: total_count, per: scope.per, page: scope.page)
74
+ end
75
+ end
76
+
77
+ def exec_logged(sql_or_scope, *args)
78
+ if sql_or_scope.is_a?(Scope)
79
+ raise ArgumentError, "You cannot call .all with a scope and additional arguments" unless args.empty?
80
+
81
+ sql = sql_or_scope.to_sql
82
+ args = sql_or_scope.args
83
+ else
84
+ sql = sql_or_scope
85
+ end
86
+
57
87
  Logging.yield_logged sql, *args do
58
88
  raw_connection.exec_params(sql, Encoder.encode_args(raw_connection, args))
59
89
  end
@@ -0,0 +1,120 @@
1
+ # rubocop:disable Style/Not
2
+ # 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
8
+
9
+ # The Simple::SQL::Scope class helps building scopes; i.e. objects
10
+ # that start as a quite basic SQL query, and allow one to add
11
+ # sql_fragments as where conditions.
12
+ class Simple::SQL::Scope
13
+ SELF = self
14
+
15
+ attr_reader :args
16
+ attr_reader :per, :page
17
+
18
+ # Build a scope object
19
+ def initialize(sql)
20
+ @sql = sql
21
+ @args = []
22
+ @filters = []
23
+ end
24
+
25
+ private
26
+
27
+ def duplicate
28
+ dupe = SELF.new(@sql)
29
+ dupe.instance_variable_set :@args, @args.dup
30
+ dupe.instance_variable_set :@filters, @filters.dup
31
+ dupe.instance_variable_set :@per, @per
32
+ dupe.instance_variable_set :@page, @page
33
+ dupe
34
+ end
35
+
36
+ public
37
+
38
+ # scope = Scope.new("SELECT * FROM tablename")
39
+ # scope = scope.where("id > ?", 12)
40
+ #
41
+ # The placeholder (usually a '?') is being replaced with the numbered
42
+ # argument (since postgres is using $1, $2, etc.) If your SQL fragment
43
+ # uses '?' as part of some fixed text you must use an alternative
44
+ # placeholder symbol.
45
+ #
46
+ # TODO: Add support for hash arguments, i.e.
47
+ # scope = scope.where(title: "foobar")
48
+ def where(sql_fragment, arg = :__dummy__no__arg, placeholder: "?")
49
+ duplicate.send(:where!, sql_fragment, arg, placeholder: placeholder)
50
+ end
51
+
52
+ private
53
+
54
+ def where!(sql_fragment, arg = :__dummy__no__arg, placeholder: "?")
55
+ if arg == :__dummy__no__arg
56
+ @filters << sql_fragment
57
+ else
58
+ @args << arg
59
+ @filters << sql_fragment.gsub(placeholder, "$#{@args.length}")
60
+ end
61
+
62
+ self
63
+ end
64
+
65
+ public
66
+
67
+ # Set pagination
68
+ def paginate(per:, page:)
69
+ duplicate.send(:paginate!, per: per, page: page)
70
+ end
71
+
72
+ private
73
+
74
+ def paginate!(per:, page:)
75
+ @per = per
76
+ @page = page
77
+
78
+ self
79
+ end
80
+
81
+ public
82
+
83
+ # Is this a paginated scope?
84
+ def paginated?
85
+ not @per.nil?
86
+ end
87
+
88
+ # generate a sql query
89
+ def to_sql(pagination: :auto)
90
+ raise ArgumentError unless pagination == :auto || pagination == false
91
+
92
+ sql = @sql
93
+ active_filters = @filters.compact
94
+ unless active_filters.empty?
95
+ sql += " WHERE (" + active_filters.join(") AND (") + ")"
96
+ end
97
+ if pagination == :auto && @per && @page
98
+ raise ArgumentError, "per must be > 0" unless @per > 0
99
+ raise ArgumentError, "page must be > 0" unless @page > 0
100
+
101
+ sql += "LIMIT #{@per} OFFSET #{(@page - 1) * @per}"
102
+ end
103
+
104
+ sql
105
+ end
106
+
107
+ # The Scope::PageInfo module can be mixed into other objects to
108
+ # hold total_count, total_pages, and current_page.
109
+ module PageInfo
110
+ attr_reader :total_count, :total_pages, :current_page
111
+
112
+ def self.attach(results, total_count:, per:, page:)
113
+ results.extend(self)
114
+ results.instance_variable_set :@total_count, total_count
115
+ results.instance_variable_set :@total_pages, (total_count + (per - 1)) / per
116
+ results.instance_variable_set :@current_page, page
117
+ results
118
+ end
119
+ end
120
+ end
@@ -1,5 +1,5 @@
1
1
  module Simple
2
2
  module SQL
3
- VERSION = "0.4.1"
3
+ VERSION = "0.4.2"
4
4
  end
5
5
  end
@@ -0,0 +1,163 @@
1
+ require "spec_helper"
2
+
3
+ describe "Simple::SQL::Scope" do
4
+ def expects(expected_result, sql, *args)
5
+ expect(SQL.ask(sql, *args)).to eq(expected_result)
6
+ end
7
+
8
+ let!(:users) { 1.upto(2).map { create(:user) } }
9
+
10
+ it 'allows chaining of scopes' do
11
+ scope1 = SQL::Scope.new "SELECT 1, 2 FROM users"
12
+ scope2 = scope1.where("FALSE")
13
+ expect(scope1.to_sql).not_to eq(scope2.to_sql)
14
+ end
15
+
16
+ context "without conditions" do
17
+ let(:scope) { SQL::Scope.new "SELECT 1, 2 FROM users" }
18
+
19
+ it "runs with SQL.ask" do
20
+ expect(SQL.ask(scope)).to eq([1, 2])
21
+ end
22
+
23
+ it "runs with SQL.all" do
24
+ expect(SQL.all(scope)).to eq([[1, 2], [1, 2]])
25
+ end
26
+ end
27
+
28
+ context "with non-argument conditions" do
29
+ context "that do not match" do
30
+ let(:scope) do
31
+ scope = SQL::Scope.new "SELECT 1, 2 FROM users"
32
+ scope = scope.where("id < 0")
33
+ scope.where("TRUE")
34
+ end
35
+
36
+ it "runs with SQL.ask" do
37
+ expect(SQL.ask(scope)).to be_nil
38
+ end
39
+
40
+ it "runs with SQL.all" do
41
+ expect(SQL.all(scope)).to eq([])
42
+ end
43
+ end
44
+
45
+ context "that do match" do
46
+ let(:scope) do
47
+ scope = SQL::Scope.new "SELECT 1, 2 FROM users"
48
+ scope = scope.where("id >= 0")
49
+ scope.where("TRUE")
50
+ end
51
+
52
+ it "runs with SQL.ask" do
53
+ expect(SQL.ask(scope)).to eq([1, 2])
54
+ end
55
+
56
+ it "runs with SQL.all" do
57
+ expect(SQL.all(scope)).to eq([[1, 2], [1, 2]])
58
+ end
59
+ end
60
+ end
61
+
62
+ context "with argument conditions" do
63
+ context "that do not match" do
64
+ let(:scope) do
65
+ scope = SQL::Scope.new "SELECT 1, 2 FROM users"
66
+ scope = scope.where("first_name NOT LIKE ?", "First%")
67
+ scope.where("id < ?", 0)
68
+ end
69
+
70
+ it "runs with SQL.ask" do
71
+ expect(SQL.ask(scope)).to be_nil
72
+ end
73
+
74
+ it "runs with SQL.all" do
75
+ expect(SQL.all(scope)).to eq([])
76
+ end
77
+ end
78
+
79
+ context "where both match" do
80
+ let(:scope) do
81
+ scope = SQL::Scope.new "SELECT 1, 2 FROM users"
82
+ scope = scope.where("first_name LIKE ?", "First%")
83
+ scope.where("id >= ?", 0)
84
+ end
85
+
86
+ it "runs with SQL.ask" do
87
+ expect(SQL.ask(scope)).to eq([1,2])
88
+ end
89
+
90
+ it "runs with SQL.all" do
91
+ expect(SQL.all(scope)).to eq([[1,2], [1,2]])
92
+ end
93
+ end
94
+
95
+ context "where first condition matches" do
96
+ let(:scope) do
97
+ scope = SQL::Scope.new "SELECT 1, 2 FROM users"
98
+ scope = scope.where("first_name LIKE ?", "First%")
99
+ scope.where("id < ?", 0)
100
+ end
101
+
102
+ it "runs with SQL.ask" do
103
+ expect(SQL.ask(scope)).to be_nil
104
+ end
105
+ end
106
+
107
+ context "where second condition matches" do
108
+ let(:scope) do
109
+ scope = SQL::Scope.new "SELECT 1, 2 FROM users"
110
+ scope = scope.where("first_name LIKE ?", "Boo%")
111
+ scope.where("id >= ?", 0)
112
+ end
113
+
114
+ it "runs with SQL.ask" do
115
+ expect(SQL.ask(scope)).to be_nil
116
+ end
117
+ end
118
+ end
119
+
120
+ context "describe pagination" do
121
+ let(:scope) do
122
+ scope = SQL::Scope.new "SELECT 1, 2 FROM users"
123
+ scope = scope.where("first_name LIKE ?", "First%")
124
+ scope.where("id > ?", 0)
125
+ end
126
+
127
+ it "sets paginated?" do
128
+ s = scope.paginate(per: 1, page: 1)
129
+ expect(s.paginated?).to eq(true)
130
+ end
131
+
132
+ context "with per=1" do
133
+ it "adds pagination info to the .all return value" do
134
+ result = SQL.all(scope.paginate(per: 1, page: 1))
135
+
136
+ expect(result).to eq([[1, 2]])
137
+ expect(result.total_pages).to eq(2)
138
+ expect(result.current_page).to eq(1)
139
+ expect(result.total_count).to eq(2)
140
+ end
141
+ end
142
+
143
+ context "with per=2" do
144
+ it "returns an empty array after the last page" do
145
+ result = SQL.all(scope.paginate(per: 2, page: 2))
146
+
147
+ expect(result).to eq([])
148
+ expect(result.total_pages).to eq(1)
149
+ expect(result.current_page).to eq(2)
150
+ expect(result.total_count).to eq(2)
151
+ end
152
+
153
+ it "adds pagination info to the .all return value" do
154
+ result = SQL.all(scope.paginate(per: 2, page: 1))
155
+
156
+ expect(result).to eq([[1, 2], [1, 2]])
157
+ expect(result.total_pages).to eq(1)
158
+ expect(result.current_page).to eq(1)
159
+ expect(result.total_count).to eq(2)
160
+ end
161
+ end
162
+ end
163
+ 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.1
4
+ version: 0.4.2
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-04-17 00:00:00.000000000 Z
12
+ date: 2018-04-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pg_array_parser
@@ -177,6 +177,7 @@ files:
177
177
  - lib/simple/sql/insert.rb
178
178
  - lib/simple/sql/logging.rb
179
179
  - lib/simple/sql/reflection.rb
180
+ - lib/simple/sql/scope.rb
180
181
  - lib/simple/sql/simple_transactions.rb
181
182
  - lib/simple/sql/version.rb
182
183
  - log/.gitkeep
@@ -193,6 +194,7 @@ files:
193
194
  - spec/simple/sql_duplicate_unique_spec.rb
194
195
  - spec/simple/sql_insert_spec.rb
195
196
  - spec/simple/sql_reflection_spec.rb
197
+ - spec/simple/sql_scope_spec.rb
196
198
  - spec/spec_helper.rb
197
199
  - spec/support/001_database.rb
198
200
  - spec/support/002_database_cleaner.rb
@@ -235,6 +237,7 @@ test_files:
235
237
  - spec/simple/sql_duplicate_unique_spec.rb
236
238
  - spec/simple/sql_insert_spec.rb
237
239
  - spec/simple/sql_reflection_spec.rb
240
+ - spec/simple/sql_scope_spec.rb
238
241
  - spec/spec_helper.rb
239
242
  - spec/support/001_database.rb
240
243
  - spec/support/002_database_cleaner.rb