simple-sql 0.4.1 → 0.4.2

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