simple-sql 0.4.9 → 0.4.10
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/.rubocop.yml +3 -0
- data/Gemfile.lock +3 -1
- data/lib/simple/sql.rb +15 -13
- data/lib/simple/sql/config.rb +2 -5
- data/lib/simple/sql/connection_adapter.rb +35 -32
- data/lib/simple/sql/helpers.rb +32 -0
- data/lib/simple/sql/{decoder.rb → helpers/decoder.rb} +35 -63
- data/lib/simple/sql/{encoder.rb → helpers/encoder.rb} +1 -1
- data/lib/simple/sql/helpers/row_converter.rb +34 -0
- data/lib/simple/sql/reflection.rb +0 -2
- data/lib/simple/sql/result.rb +27 -0
- data/lib/simple/sql/result/association_loader.rb +161 -0
- data/lib/simple/sql/result/records.rb +72 -0
- data/lib/simple/sql/result/rows.rb +30 -0
- data/lib/simple/sql/scope.rb +41 -18
- data/lib/simple/sql/scope/order.rb +15 -3
- data/lib/simple/sql/simple_transactions.rb +0 -1
- data/lib/simple/sql/version.rb +1 -1
- data/simple-sql.gemspec +1 -0
- data/spec/simple/sql_all_into_spec.rb +68 -9
- data/spec/simple/sql_associations_spec.rb +142 -0
- data/spec/simple/sql_scope_spec.rb +15 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/support/001_database.rb +25 -19
- data/spec/support/003_factories.rb +4 -0
- metadata +26 -8
- data/spec/simple/sql_ask_into_spec.rb +0 -27
- data/spec/simple/sql_ask_into_struct_spec.rb +0 -17
@@ -0,0 +1,72 @@
|
|
1
|
+
# xrubocop:disable Metrics/ParameterLists
|
2
|
+
|
3
|
+
require_relative "association_loader"
|
4
|
+
|
5
|
+
class ::Simple::SQL::Result::Records < ::Simple::SQL::Result::Rows
|
6
|
+
def initialize(records, target_type:, pg_source_oid:) # :nodoc:
|
7
|
+
expect! records.first => Hash unless records.empty?
|
8
|
+
|
9
|
+
super(records)
|
10
|
+
|
11
|
+
@hash_records = records
|
12
|
+
@target_type = target_type
|
13
|
+
@pg_source_oid = pg_source_oid
|
14
|
+
|
15
|
+
materialize
|
16
|
+
end
|
17
|
+
|
18
|
+
# -- preload associations -------------------------------------------------
|
19
|
+
|
20
|
+
AssociationLoader = ::Simple::SQL::Result::AssociationLoader
|
21
|
+
|
22
|
+
|
23
|
+
# Preloads an association.
|
24
|
+
#
|
25
|
+
# This can now be used as follows:
|
26
|
+
#
|
27
|
+
# scope = SQL::Scope.new("SELECT * FROM users")
|
28
|
+
# results = SQL.all scope, into: :struct
|
29
|
+
# results.preload(:organization)
|
30
|
+
#
|
31
|
+
# The preload method uses foreign key definitions in the database to figure out
|
32
|
+
# which table to load from.
|
33
|
+
#
|
34
|
+
# This method is only available if <tt>into:</tt> was set in the call to <tt>SQL.all</tt>.
|
35
|
+
# It raises an error otherwise.
|
36
|
+
#
|
37
|
+
# Parameters:
|
38
|
+
#
|
39
|
+
# - association: the name of the association.
|
40
|
+
# - as: the target name of the association.
|
41
|
+
# - order_by: if set describes ordering; see Scope#order_by.
|
42
|
+
# - limit: if set describes limits; see Scope#order_by.
|
43
|
+
def preload(association, as: nil, order_by: nil, limit: nil)
|
44
|
+
expect! association => Symbol
|
45
|
+
expect! as => [nil, Symbol]
|
46
|
+
|
47
|
+
# resolve oid into table and schema name.
|
48
|
+
schema, host_table = SQL.ask <<~SQL, @pg_source_oid
|
49
|
+
SELECT nspname AS schema, relname AS host_table
|
50
|
+
FROM pg_class
|
51
|
+
JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace
|
52
|
+
WHERE pg_class.oid=$1
|
53
|
+
SQL
|
54
|
+
|
55
|
+
AssociationLoader.preload @hash_records, association,
|
56
|
+
host_table: host_table, schema: schema, as: as,
|
57
|
+
order_by: order_by, limit: limit
|
58
|
+
materialize
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# convert the records into the target type.
|
64
|
+
RowConverter = ::Simple::SQL::Helpers::RowConverter
|
65
|
+
|
66
|
+
def materialize
|
67
|
+
records = @hash_records
|
68
|
+
records = RowConverter.convert(records, into: @target_type) if @target_type != Hash
|
69
|
+
|
70
|
+
replace(records)
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize
|
2
|
+
# rubocop:disable Naming/AccessorMethodName
|
3
|
+
|
4
|
+
class ::Simple::SQL::Result::Rows < ::Simple::SQL::Result
|
5
|
+
def initialize(records)
|
6
|
+
replace(records)
|
7
|
+
end
|
8
|
+
|
9
|
+
# -- pagination info ------------------------------------------------------
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def set_pagination_info(scope)
|
14
|
+
raise ArgumentError, "per must be > 0" unless scope.per > 0
|
15
|
+
|
16
|
+
if scope.page <= 1 && empty?
|
17
|
+
# This branch is an optimization: the call to the database to count is
|
18
|
+
# not necessary if we know that there are not even any results on the
|
19
|
+
# first page.
|
20
|
+
@total_count = 0
|
21
|
+
@current_page = 1
|
22
|
+
else
|
23
|
+
sql = "SELECT COUNT(*) FROM (#{scope.order_by(nil).to_sql(pagination: false)}) simple_sql_count"
|
24
|
+
@total_count = ::Simple::SQL.ask(sql, *scope.args)
|
25
|
+
@current_page = scope.page
|
26
|
+
end
|
27
|
+
|
28
|
+
@total_pages = (@total_count * 1.0 / scope.per).ceil
|
29
|
+
end
|
30
|
+
end
|
data/lib/simple/sql/scope.rb
CHANGED
@@ -14,15 +14,51 @@ class Simple::SQL::Scope
|
|
14
14
|
attr_reader :per, :page
|
15
15
|
|
16
16
|
# Build a scope object
|
17
|
+
#
|
18
|
+
# This call supports a few variants:
|
19
|
+
#
|
20
|
+
# Simple::SQL::Scope.new("SELECT * FROM mytable")
|
21
|
+
# Simple::SQL::Scope.new(table: "mytable", select: "*")
|
22
|
+
#
|
23
|
+
# The second option also allows one to pass in more options, like the following:
|
24
|
+
#
|
25
|
+
# Simple::SQL::Scope.new(table: "mytable", select: "*", where: { id: 1, foo: "bar" }, order_by: "id desc")
|
26
|
+
#
|
17
27
|
def initialize(sql)
|
18
|
-
|
19
|
-
|
28
|
+
expect! sql => [String, Hash]
|
29
|
+
|
30
|
+
@sql = nil
|
31
|
+
@args = []
|
20
32
|
@filters = []
|
21
|
-
|
33
|
+
|
34
|
+
case sql
|
35
|
+
when String then @sql = sql
|
36
|
+
when Hash then initialize_from_hash(sql)
|
37
|
+
end
|
22
38
|
end
|
23
39
|
|
24
40
|
private
|
25
41
|
|
42
|
+
# rubocop:disable Metrics/AbcSize
|
43
|
+
def initialize_from_hash(hsh)
|
44
|
+
actual_keys = hsh.keys
|
45
|
+
valid_keys = [:table, :select, :where, :order_by]
|
46
|
+
extra_keys = actual_keys - valid_keys
|
47
|
+
raise ArgumentError, "Extra keys #{extra_keys.inspect}; allowed are #{valid_keys.inspect}" unless extra_keys.empty?
|
48
|
+
|
49
|
+
# -- set table and select -------------------------------------------------
|
50
|
+
|
51
|
+
table = hsh[:table] || raise(ArgumentError, "Missing :table option")
|
52
|
+
select = hsh[:select] || "*"
|
53
|
+
|
54
|
+
@sql = "SELECT #{Array(select).join(', ')} FROM #{table}"
|
55
|
+
|
56
|
+
# -- apply conditions, if any ---------------------------------------------
|
57
|
+
|
58
|
+
where!(hsh[:where]) unless hsh[:where].nil?
|
59
|
+
order_by!(hsh[:order_by]) unless hsh[:order_by].nil?
|
60
|
+
end
|
61
|
+
|
26
62
|
def duplicate
|
27
63
|
dupe = SELF.new(@sql)
|
28
64
|
dupe.instance_variable_set :@args, @args.dup
|
@@ -30,6 +66,7 @@ class Simple::SQL::Scope
|
|
30
66
|
dupe.instance_variable_set :@per, @per
|
31
67
|
dupe.instance_variable_set :@page, @page
|
32
68
|
dupe.instance_variable_set :@order_by_fragment, @order_by_fragment
|
69
|
+
dupe.instance_variable_set :@limit, @limit
|
33
70
|
dupe
|
34
71
|
end
|
35
72
|
|
@@ -41,23 +78,9 @@ class Simple::SQL::Scope
|
|
41
78
|
|
42
79
|
sql = @sql
|
43
80
|
sql = apply_filters(sql)
|
44
|
-
sql =
|
81
|
+
sql = apply_order_and_limit(sql)
|
45
82
|
sql = apply_pagination(sql, pagination: pagination)
|
46
83
|
|
47
84
|
sql
|
48
85
|
end
|
49
|
-
|
50
|
-
# The Scope::PageInfo module can be mixed into other objects to
|
51
|
-
# hold total_count, total_pages, and current_page.
|
52
|
-
module PageInfo
|
53
|
-
attr_reader :total_count, :total_pages, :current_page
|
54
|
-
|
55
|
-
def self.attach(results, total_count:, per:, page:)
|
56
|
-
results.extend(self)
|
57
|
-
results.instance_variable_set :@total_count, total_count
|
58
|
-
results.instance_variable_set :@total_pages, (total_count + (per - 1)) / per
|
59
|
-
results.instance_variable_set :@current_page, page
|
60
|
-
results
|
61
|
-
end
|
62
|
-
end
|
63
86
|
end
|
@@ -5,6 +5,10 @@ class Simple::SQL::Scope
|
|
5
5
|
duplicate.send(:order_by!, sql_fragment)
|
6
6
|
end
|
7
7
|
|
8
|
+
def limit(count)
|
9
|
+
duplicate.send(:limit!, count)
|
10
|
+
end
|
11
|
+
|
8
12
|
private
|
9
13
|
|
10
14
|
# Adjust sort order
|
@@ -13,9 +17,17 @@ class Simple::SQL::Scope
|
|
13
17
|
self
|
14
18
|
end
|
15
19
|
|
20
|
+
# Adjust sort order
|
21
|
+
def limit!(count)
|
22
|
+
@limit = count
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
16
26
|
# called from to_sql
|
17
|
-
def
|
18
|
-
|
19
|
-
"#{sql}
|
27
|
+
def apply_order_and_limit(sql)
|
28
|
+
sql = "#{sql} ORDER BY #{@order_by_fragment}" if @order_by_fragment
|
29
|
+
sql = "#{sql} LIMIT #{@limit}" if @limit
|
30
|
+
|
31
|
+
sql
|
20
32
|
end
|
21
33
|
end
|
data/lib/simple/sql/version.rb
CHANGED
data/simple-sql.gemspec
CHANGED
@@ -1,17 +1,76 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
|
-
describe "Simple::SQL.
|
3
|
+
describe "Simple::SQL.ask into: :struct" do
|
4
4
|
let!(:users) { 1.upto(USER_COUNT).map { create(:user) } }
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
describe "all into: X" do
|
7
|
+
it "calls the database" do
|
8
|
+
r = SQL.all("SELECT id FROM users", into: Hash)
|
9
|
+
expect(r).to be_a(Array)
|
10
|
+
expect(r.length).to eq(USER_COUNT)
|
11
|
+
expect(r.map(&:class).uniq).to eq([Hash])
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns an empty array when there is no match" do
|
15
|
+
r = SQL.all("SELECT * FROM users WHERE FALSE", into: Hash)
|
16
|
+
expect(r).to eq([])
|
17
|
+
end
|
18
|
+
|
19
|
+
it "yields the results into a block" do
|
20
|
+
received = []
|
21
|
+
SQL.all("SELECT id FROM users", into: Hash) do |hsh|
|
22
|
+
received << hsh
|
23
|
+
end
|
24
|
+
expect(received.length).to eq(USER_COUNT)
|
25
|
+
expect(received.map(&:class).uniq).to eq([Hash])
|
26
|
+
end
|
27
|
+
|
28
|
+
it "does not yield if there is no match" do
|
29
|
+
received = []
|
30
|
+
SQL.all("SELECT id FROM users WHERE FALSE", into: Hash) do |hsh|
|
31
|
+
received << hsh
|
32
|
+
end
|
33
|
+
expect(received.length).to eq(0)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "into: :struct" do
|
38
|
+
it "calls the database" do
|
39
|
+
r = SQL.ask("SELECT COUNT(*) AS count FROM users", into: :struct)
|
40
|
+
expect(r.count).to eq(2)
|
41
|
+
expect(r.class.members).to eq([:count])
|
42
|
+
end
|
43
|
+
|
44
|
+
it "reuses the struct Class" do
|
45
|
+
r1 = SQL.ask("SELECT COUNT(*) AS count FROM users", into: :struct)
|
46
|
+
r2 = SQL.ask("SELECT COUNT(*) AS count FROM users", into: :struct)
|
47
|
+
expect(r1.class.object_id).to eq(r2.class.object_id)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "into: Hash" do
|
52
|
+
it "calls the database" do
|
53
|
+
r = SQL.ask("SELECT COUNT(*) AS count FROM users", into: Hash)
|
54
|
+
expect(r).to eq({count: 2})
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns nil when there is no match" do
|
58
|
+
r = SQL.ask("SELECT * FROM users WHERE FALSE", into: Hash)
|
59
|
+
expect(r).to be_nil
|
60
|
+
end
|
11
61
|
end
|
62
|
+
|
63
|
+
describe "into: OpenStruct" do
|
64
|
+
it "returns a OpenStruct with into: OpenStruct" do
|
65
|
+
r = SQL.ask("SELECT COUNT(*) AS count FROM users", into: OpenStruct)
|
66
|
+
expect(r).to be_a(OpenStruct)
|
67
|
+
expect(r).to eq(OpenStruct.new(count: 2))
|
68
|
+
end
|
12
69
|
|
13
|
-
|
14
|
-
|
15
|
-
|
70
|
+
it "supports the into: option even with parameters" do
|
71
|
+
r = SQL.ask("SELECT $1::integer AS count FROM users", 2, into: OpenStruct)
|
72
|
+
expect(r).to be_a(OpenStruct)
|
73
|
+
expect(r).to eq(OpenStruct.new(count: 2))
|
74
|
+
end
|
16
75
|
end
|
17
76
|
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module ArrayPluck
|
4
|
+
refine Array do
|
5
|
+
def pluck(key)
|
6
|
+
map { |e| e.fetch(key) }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
using ArrayPluck
|
12
|
+
|
13
|
+
describe "Simple::SQL::Result#preload" do
|
14
|
+
let!(:org1) { create(:organization) }
|
15
|
+
let!(:users1) { 1.upto(USER_COUNT).map { create(:user, organization_id: org1.id) } }
|
16
|
+
let!(:org2) { create(:organization) }
|
17
|
+
let!(:users2) { 1.upto(USER_COUNT).map { create(:user, organization_id: org2.id) } }
|
18
|
+
let!(:users) { 1.upto(USER_COUNT).map { create(:user) } }
|
19
|
+
|
20
|
+
# The block below checks that the factories are set up correctly.
|
21
|
+
describe "factories used in spec" do
|
22
|
+
it "builds correct objects" do
|
23
|
+
expect(org1.id).not_to eq(org2.id)
|
24
|
+
expect(users1.map(&:organization_id).uniq).to eq([org1.id])
|
25
|
+
expect(users2.map(&:organization_id).uniq).to eq([org2.id])
|
26
|
+
expect(users.map(&:organization_id).uniq).to eq([nil])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# describe "correctness" do
|
31
|
+
# it "resolves a belongs_to association" do
|
32
|
+
# users = SQL.all "SELECT * FROM users WHERE organization_id=$1", org1.id, into: Hash
|
33
|
+
# users.preload :organization
|
34
|
+
# expect(users.first[:organization]).to eq(org1.to_h)
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# it "resolves a has_many association" do
|
38
|
+
# organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
39
|
+
# organizations.preload :users
|
40
|
+
# expect(organizations.first[:users]).to eq(users1.map(&:to_h))
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# it "resolves a has_one association" do
|
44
|
+
# organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
45
|
+
# organizations.preload :user
|
46
|
+
#
|
47
|
+
# organization = organizations.first
|
48
|
+
# users_of_organization = SQL.all "SELECT * FROM users WHERE organization_id=$1", organization[:id], into: Hash
|
49
|
+
# expect(users_of_organization).to include(organization[:user])
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
|
53
|
+
describe "automatic detection via foreign keys" do
|
54
|
+
it "detects a belongs_to association" do
|
55
|
+
users = SQL.all "SELECT * FROM users WHERE organization_id=$1", org1.id, into: Hash
|
56
|
+
users.preload :organization
|
57
|
+
expect(users.first[:organization]).to eq(org1.to_h)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "detects a has_many association" do
|
61
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
62
|
+
organizations.preload :users
|
63
|
+
expect(organizations.first[:users]).to eq(users1.map(&:to_h))
|
64
|
+
end
|
65
|
+
|
66
|
+
it "detects a has_one association" do
|
67
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
68
|
+
organizations.preload :user
|
69
|
+
|
70
|
+
organization = organizations.first
|
71
|
+
users_of_organization = SQL.all "SELECT * FROM users WHERE organization_id=$1", organization[:id], into: Hash
|
72
|
+
expect(users_of_organization).to include(organization[:user])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe ":as option" do
|
77
|
+
it "renames a belongs_to association" do
|
78
|
+
users = SQL.all "SELECT * FROM users WHERE organization_id=$1", org1.id, into: Hash
|
79
|
+
users.preload :organization, as: :org
|
80
|
+
|
81
|
+
expect(users.first.keys).not_to include(:organization)
|
82
|
+
expect(users.first[:org]).to eq(org1.to_h)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "renames a has_many association" do
|
86
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
87
|
+
organizations.preload :users, as: :members
|
88
|
+
|
89
|
+
expect(organizations.first.keys).not_to include(:users)
|
90
|
+
expect(organizations.first[:members]).to eq(users1.map(&:to_h))
|
91
|
+
end
|
92
|
+
|
93
|
+
it "detects a has_one association" do
|
94
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
95
|
+
organizations.preload :user, as: :usr
|
96
|
+
expect(organizations.first.keys).not_to include(:user)
|
97
|
+
|
98
|
+
organization = organizations.first
|
99
|
+
users_of_organization = SQL.all "SELECT * FROM users WHERE organization_id=$1", organization[:id], into: Hash
|
100
|
+
expect(users_of_organization).to include(organization[:usr])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe ":order_by" do
|
105
|
+
it "supports order_by" do
|
106
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
107
|
+
organizations.preload :users, order_by: "id"
|
108
|
+
users = organizations.first[:users]
|
109
|
+
|
110
|
+
ordered_user_ids = SQL.all("SELECT id FROM users WHERE organization_id=$1 ORDER BY id", organizations.first[:id])
|
111
|
+
expect(users.pluck(:id)).to eq(ordered_user_ids)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "supports order_by DESC" do
|
115
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
116
|
+
organizations.preload :users, order_by: "id DESC"
|
117
|
+
users = organizations.first[:users]
|
118
|
+
|
119
|
+
ordered_user_ids = SQL.all("SELECT id FROM users WHERE organization_id=$1 ORDER BY id", organizations.first[:id])
|
120
|
+
expect(users.pluck(:id)).to eq(ordered_user_ids.reverse)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe ":limit" do
|
125
|
+
xit "limits the number of returned records" do
|
126
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
127
|
+
organizations.preload :users, limit: 1
|
128
|
+
|
129
|
+
expect(organizations.first[:users].length).to eq(1)
|
130
|
+
end
|
131
|
+
|
132
|
+
xit "limits the number of returned records" do
|
133
|
+
organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
134
|
+
organizations.preload :users, limit: 2, order_by: "id"
|
135
|
+
users = organizations.first[:users]
|
136
|
+
expect(users.length).to eq(2)
|
137
|
+
|
138
|
+
ordered_user_ids = SQL.all("SELECT id FROM users WHERE organization_id=$1 ORDER BY id", organizations.first[:id])
|
139
|
+
expect(users.pluck(:id)).to eq(ordered_user_ids)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|