simple-sql 0.4.9 → 0.4.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f24d06442bae77c5c1a51362e9ef1ac83e50034e6ce1b47b480ed8290f40e2b3
|
4
|
+
data.tar.gz: b2b0a88578e3c977ecf88739512d94a39bcbc8ce9eafac7225e02a925300ac9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ebd2eda811287cf9dbc8ec9deefb512aa7e5b02fbeb3d8c789d254958ff2d7148c16a65fb529ef672a7626f2e8871822493bb8444e602c112070b5be66f6aa5
|
7
|
+
data.tar.gz: bdfff2c6a865217aad9507ba6550b8122bc9f449a7cb8a09cc393ce97d75a60eec79a21f0edef183addc73e4378656e099b2ce84871f5a7e2b9721bbff670926
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
simple-sql (0.4.
|
4
|
+
simple-sql (0.4.10)
|
5
|
+
expectation (~> 1)
|
5
6
|
pg (~> 0.20)
|
6
7
|
pg_array_parser (~> 0)
|
7
8
|
|
@@ -29,6 +30,7 @@ GEM
|
|
29
30
|
database_cleaner (1.6.2)
|
30
31
|
diff-lcs (1.3)
|
31
32
|
docile (1.1.5)
|
33
|
+
expectation (1.1.1)
|
32
34
|
i18n (0.9.1)
|
33
35
|
concurrent-ruby (~> 1.0)
|
34
36
|
json (2.1.0)
|
data/lib/simple/sql.rb
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
require "forwardable"
|
2
2
|
require "logger"
|
3
|
-
|
4
|
-
|
5
|
-
require_relative "sql/
|
6
|
-
require_relative "sql/
|
7
|
-
|
8
|
-
require_relative "sql/
|
9
|
-
require_relative "sql/
|
10
|
-
require_relative "sql/
|
11
|
-
require_relative "sql/
|
12
|
-
require_relative "sql/
|
13
|
-
require_relative "sql/
|
14
|
-
require_relative "sql/
|
15
|
-
require_relative "sql/
|
3
|
+
require "expectation"
|
4
|
+
|
5
|
+
require_relative "sql/version"
|
6
|
+
require_relative "sql/helpers"
|
7
|
+
|
8
|
+
require_relative "sql/result"
|
9
|
+
require_relative "sql/config"
|
10
|
+
require_relative "sql/logging"
|
11
|
+
require_relative "sql/simple_transactions"
|
12
|
+
require_relative "sql/scope"
|
13
|
+
require_relative "sql/connection_adapter"
|
14
|
+
require_relative "sql/connection"
|
15
|
+
require_relative "sql/reflection"
|
16
|
+
require_relative "sql/insert"
|
17
|
+
require_relative "sql/duplicate"
|
16
18
|
|
17
19
|
module Simple
|
18
20
|
# The Simple::SQL module
|
data/lib/simple/sql/config.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# rubocop:disable Metrics/AbcSize
|
2
2
|
# rubocop:disable Metrics/CyclomaticComplexity
|
3
|
-
# rubocop:disable Metrics/MethodLength
|
4
3
|
# rubocop:disable Metrics/PerceivedComplexity
|
5
4
|
|
6
5
|
# private
|
@@ -9,11 +8,9 @@ module Simple::SQL::Config
|
|
9
8
|
|
10
9
|
# parse a DATABASE_URL, return PG::Connection settings.
|
11
10
|
def parse_url(url)
|
12
|
-
|
13
|
-
|
14
|
-
raise ArgumentError, "Invalid URL #{url.inspect}" unless url.is_a?(String)
|
15
|
-
raise ArgumentError, "Invalid URL #{url.inspect}" unless url =~ /^postgres(ql)?s?:\/\//
|
11
|
+
expect! url => /^postgres(ql)?s?:\/\//
|
16
12
|
|
13
|
+
require "uri"
|
17
14
|
uri = URI.parse(url)
|
18
15
|
raise ArgumentError, "Invalid URL #{url}" unless uri.hostname && uri.path
|
19
16
|
|
@@ -1,16 +1,13 @@
|
|
1
1
|
# rubocop:disable Style/IfUnlessModifier
|
2
|
-
# rubocop:disable Metrics/AbcSize
|
3
|
-
# rubocop:disable Metrics/MethodLength
|
4
2
|
|
5
3
|
# This module implements an adapter between the Simple::SQL interface
|
6
4
|
# (i.e. ask, all, first, transaction) and a raw connection.
|
7
5
|
#
|
8
6
|
# This module can be mixed onto objects that implement a raw_connection
|
9
7
|
# method, which must return a Pg::Connection.
|
8
|
+
|
10
9
|
module Simple::SQL::ConnectionAdapter
|
11
10
|
Logging = ::Simple::SQL::Logging
|
12
|
-
Encoder = ::Simple::SQL::Encoder
|
13
|
-
Decoder = ::Simple::SQL::Decoder
|
14
11
|
Scope = ::Simple::SQL::Scope
|
15
12
|
|
16
13
|
# execute one or more sql statements. This method does not allow to pass in
|
@@ -37,12 +34,29 @@ module Simple::SQL::ConnectionAdapter
|
|
37
34
|
# end
|
38
35
|
|
39
36
|
def all(sql, *args, into: nil, &block)
|
40
|
-
|
41
|
-
|
37
|
+
pg_result = exec_logged(sql, *args)
|
38
|
+
|
39
|
+
# enumerate the rows in pg_result. This returns either an Array of Hashes
|
40
|
+
# (if into is set), or an array of row arrays or of singular values.
|
41
|
+
#
|
42
|
+
# Even if into is set to something different than a Hash, we'll convert
|
43
|
+
# each row into a Hash initially, and only later convert it to the final
|
44
|
+
# target type (via RowConverter.convert). This is to allow to fill in
|
45
|
+
# more entries later on.
|
46
|
+
records = enumerate(pg_result, into: into)
|
47
|
+
|
48
|
+
# optimization: If we wouldn't clear here the GC would do this later.
|
49
|
+
pg_result.clear unless pg_result.autoclear?
|
50
|
+
|
51
|
+
# [TODO] - resolve associations. Note that this is only possible if the type
|
52
|
+
# is not an Array (i.e. into is nil)
|
53
|
+
|
42
54
|
if sql.is_a?(Scope) && sql.paginated?
|
43
|
-
|
55
|
+
records.send(:set_pagination_info, sql)
|
44
56
|
end
|
45
|
-
|
57
|
+
|
58
|
+
records.each(&block) if block
|
59
|
+
records
|
46
60
|
end
|
47
61
|
|
48
62
|
# Runs a query and returns the first result row of a query.
|
@@ -62,19 +76,7 @@ module Simple::SQL::ConnectionAdapter
|
|
62
76
|
|
63
77
|
private
|
64
78
|
|
65
|
-
|
66
|
-
raise ArgumentError, "expect Array but get a #{results.class.name}" unless results.is_a?(Array)
|
67
|
-
raise ArgumentError, "per must be > 0" unless scope.per > 0
|
68
|
-
|
69
|
-
# optimization: add empty case (page <= 1 && results.empty?)
|
70
|
-
if scope.page <= 1 && results.empty?
|
71
|
-
Scope::PageInfo.attach(results, total_count: 0, per: scope.per, page: 1)
|
72
|
-
else
|
73
|
-
sql = "SELECT COUNT(*) FROM (#{scope.order_by(nil).to_sql(pagination: false)}) simple_sql_count"
|
74
|
-
total_count = ask(sql, *scope.args)
|
75
|
-
Scope::PageInfo.attach(results, total_count: total_count, per: scope.per, page: scope.page)
|
76
|
-
end
|
77
|
-
end
|
79
|
+
Encoder = ::Simple::SQL::Helpers::Encoder
|
78
80
|
|
79
81
|
def exec_logged(sql_or_scope, *args)
|
80
82
|
if sql_or_scope.is_a?(Scope)
|
@@ -91,19 +93,20 @@ module Simple::SQL::ConnectionAdapter
|
|
91
93
|
end
|
92
94
|
end
|
93
95
|
|
94
|
-
|
95
|
-
|
96
|
+
Result = ::Simple::SQL::Result
|
97
|
+
Decoder = ::Simple::SQL::Helpers::Decoder
|
96
98
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
ary
|
99
|
+
def enumerate(pg_result, into:)
|
100
|
+
records = []
|
101
|
+
pg_source_oid = nil
|
102
|
+
|
103
|
+
if pg_result.ntuples > 0 && pg_result.nfields > 0
|
104
|
+
decoder = Decoder.new(self, pg_result, into: (into ? Hash : nil))
|
105
|
+
pg_result.each_row { |row| records << decoder.decode(row) }
|
106
|
+
pg_source_oid = pg_result.ftable(0)
|
106
107
|
end
|
108
|
+
|
109
|
+
Result.build(records, target_type: into, pg_source_oid: pg_source_oid)
|
107
110
|
end
|
108
111
|
|
109
112
|
public
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ::Simple::SQL::Helpers
|
2
|
+
end
|
3
|
+
|
4
|
+
require_relative "helpers/decoder.rb"
|
5
|
+
require_relative "helpers/encoder.rb"
|
6
|
+
require_relative "helpers/row_converter.rb"
|
7
|
+
|
8
|
+
module ::Simple::SQL::Helpers
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def stable_group_by_key(ary, key)
|
12
|
+
hsh = Hash.new { |h, k| h[k] = [] }
|
13
|
+
ary.each do |entity|
|
14
|
+
group = entity.fetch(key)
|
15
|
+
hsh[group] << entity
|
16
|
+
end
|
17
|
+
hsh
|
18
|
+
end
|
19
|
+
|
20
|
+
def pluck(ary, key)
|
21
|
+
ary.map { |rec| rec.fetch(key) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def by_key(ary, key)
|
25
|
+
hsh = {}
|
26
|
+
ary.each do |entity|
|
27
|
+
group = entity.fetch(key)
|
28
|
+
hsh[group] = entity
|
29
|
+
end
|
30
|
+
hsh
|
31
|
+
end
|
32
|
+
end
|
@@ -1,18 +1,9 @@
|
|
1
1
|
require "time"
|
2
2
|
|
3
3
|
# private
|
4
|
-
module Simple::SQL::Decoder
|
4
|
+
module Simple::SQL::Helpers::Decoder
|
5
5
|
extend self
|
6
6
|
|
7
|
-
def new(connection, result, into:)
|
8
|
-
if into == Hash then HashRecord.new(connection, result)
|
9
|
-
elsif into == :struct then StructRecord.new(connection, result)
|
10
|
-
elsif into then Record.new(connection, result, into: into)
|
11
|
-
elsif result.nfields == 1 then SingleColumn.new(connection, result)
|
12
|
-
else MultiColumns.new(connection, result)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
7
|
def parse_timestamp(s)
|
17
8
|
r = ::Time.parse(s)
|
18
9
|
return r if r.utc_offset == 0
|
@@ -21,7 +12,6 @@ module Simple::SQL::Decoder
|
|
21
12
|
|
22
13
|
# rubocop:disable Metrics/AbcSize
|
23
14
|
# rubocop:disable Metrics/CyclomaticComplexity
|
24
|
-
# rubocop:disable Metrics/MethodLength
|
25
15
|
def decode_value(type, s)
|
26
16
|
case type
|
27
17
|
when :unknown then s
|
@@ -81,68 +71,50 @@ module Simple::SQL::Decoder
|
|
81
71
|
end
|
82
72
|
end
|
83
73
|
|
84
|
-
|
85
|
-
def
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
def decode(row)
|
91
|
-
value = row.first
|
92
|
-
value && Simple::SQL::Decoder.decode_value(@field_type, value)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
class Simple::SQL::Decoder::MultiColumns
|
97
|
-
def initialize(connection, result)
|
98
|
-
@field_types = 0.upto(result.fields.length - 1).map do |idx|
|
99
|
-
typename = connection.resolve_type(result.ftype(idx), result.fmod(idx))
|
100
|
-
typename.to_sym
|
74
|
+
module Simple::SQL::Helpers::Decoder
|
75
|
+
def self.new(connection, result, into:)
|
76
|
+
if into == Hash then HashRecord.new(connection, result)
|
77
|
+
elsif result.nfields == 1 then SingleColumn.new(connection, result)
|
78
|
+
else MultiColumns.new(connection, result)
|
101
79
|
end
|
102
80
|
end
|
103
81
|
|
104
|
-
|
105
|
-
|
106
|
-
|
82
|
+
class SingleColumn
|
83
|
+
def initialize(connection, result)
|
84
|
+
typename = connection.resolve_type(result.ftype(0), result.fmod(0))
|
85
|
+
@field_type = typename.to_sym
|
107
86
|
end
|
108
|
-
end
|
109
|
-
end
|
110
87
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
116
|
-
|
117
|
-
def decode(row)
|
118
|
-
decoded_row = super(row)
|
119
|
-
Hash[@field_names.zip(decoded_row)]
|
88
|
+
def decode(row)
|
89
|
+
value = row.first
|
90
|
+
value && Simple::SQL::Helpers::Decoder.decode_value(@field_type, value)
|
91
|
+
end
|
120
92
|
end
|
121
|
-
end
|
122
93
|
|
123
|
-
class
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
94
|
+
class MultiColumns
|
95
|
+
def initialize(connection, result)
|
96
|
+
@field_types = 0.upto(result.fields.length - 1).map do |idx|
|
97
|
+
typename = connection.resolve_type(result.ftype(idx), result.fmod(idx))
|
98
|
+
typename.to_sym
|
99
|
+
end
|
100
|
+
end
|
128
101
|
|
129
|
-
|
130
|
-
|
102
|
+
def decode(row)
|
103
|
+
@field_types.zip(row).map do |field_type, value|
|
104
|
+
value && Simple::SQL::Helpers::Decoder.decode_value(field_type, value)
|
105
|
+
end
|
106
|
+
end
|
131
107
|
end
|
132
|
-
end
|
133
|
-
|
134
|
-
class Simple::SQL::Decoder::StructRecord < Simple::SQL::Decoder::MultiColumns
|
135
|
-
@@struct_cache = {}
|
136
|
-
|
137
|
-
def initialize(connection, result)
|
138
|
-
super(connection, result)
|
139
108
|
|
140
|
-
|
141
|
-
|
142
|
-
|
109
|
+
class HashRecord < MultiColumns
|
110
|
+
def initialize(connection, result)
|
111
|
+
super(connection, result)
|
112
|
+
@field_names = result.fields.map(&:to_sym)
|
113
|
+
end
|
143
114
|
|
144
|
-
|
145
|
-
|
146
|
-
|
115
|
+
def decode(row)
|
116
|
+
decoded_row = super(row)
|
117
|
+
Hash[@field_names.zip(decoded_row)]
|
118
|
+
end
|
147
119
|
end
|
148
120
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Simple::SQL::Helpers::RowConverter
|
2
|
+
# returns an array of converted records
|
3
|
+
def self.convert(records, into:)
|
4
|
+
hsh = records.first
|
5
|
+
return records unless hsh
|
6
|
+
|
7
|
+
if into == :struct
|
8
|
+
converter = StructConverter.for(attributes: hsh.keys)
|
9
|
+
records.map { |record| converter.convert(record) }
|
10
|
+
else
|
11
|
+
records.map { |record| into.new(record) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class StructConverter # :nodoc:
|
16
|
+
def self.for(attributes:)
|
17
|
+
@cache ||= {}
|
18
|
+
@cache[attributes] ||= new(attributes)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def initialize(attributes)
|
24
|
+
@klass = Struct.new(*attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
public
|
28
|
+
|
29
|
+
def convert(hsh)
|
30
|
+
values = hsh.values_at(*@klass.members)
|
31
|
+
@klass.new(*values)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative "helpers"
|
2
|
+
|
3
|
+
class ::Simple::SQL::Result < Array
|
4
|
+
end
|
5
|
+
|
6
|
+
require_relative "result/rows"
|
7
|
+
require_relative "result/records"
|
8
|
+
|
9
|
+
# The result of SQL.all
|
10
|
+
#
|
11
|
+
# This class implements the interface of a Result.
|
12
|
+
class ::Simple::SQL::Result < Array
|
13
|
+
# A Result object is requested via ::Simple::SQL::Result.build, which then
|
14
|
+
# chooses the correct implementation, based on the <tt>target_type:</tt>
|
15
|
+
# parameter.
|
16
|
+
def self.build(records, target_type:, pg_source_oid:) # :nodoc:
|
17
|
+
if target_type.nil?
|
18
|
+
Rows.new(records)
|
19
|
+
else
|
20
|
+
Records.new(records, target_type: target_type, pg_source_oid: pg_source_oid)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :total_count
|
25
|
+
attr_reader :total_pages
|
26
|
+
attr_reader :current_page
|
27
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize
|
2
|
+
# rubocop:disable Metrics/MethodLength
|
3
|
+
# rubocop:disable Metrics/ParameterLists
|
4
|
+
|
5
|
+
#
|
6
|
+
# This module implements a pretty generic AssociationLoader.
|
7
|
+
#
|
8
|
+
module ::Simple::SQL::Result::AssociationLoader # :nodoc:
|
9
|
+
extend self
|
10
|
+
|
11
|
+
SQL = ::Simple::SQL
|
12
|
+
H = ::Simple::SQL::Helpers
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Assuming association refers to a table, what would the table name be?
|
17
|
+
#
|
18
|
+
# For example, find_associated_table(:user, schema: "foo") could return
|
19
|
+
# "foo.users", if such a table exists.
|
20
|
+
#
|
21
|
+
# Raises an ArgumentError if no matching table can be found.
|
22
|
+
def find_associated_table(association, schema:)
|
23
|
+
association = association.to_s
|
24
|
+
|
25
|
+
tables_in_schema = ::Simple::SQL::Reflection.table_info(schema: schema).keys
|
26
|
+
|
27
|
+
return "#{schema}.#{association}" if tables_in_schema.include?(association)
|
28
|
+
return "#{schema}.#{association.singularize}" if tables_in_schema.include?(association.singularize)
|
29
|
+
return "#{schema}.#{association.pluralize}" if tables_in_schema.include?(association.pluralize)
|
30
|
+
|
31
|
+
raise ArgumentError, "Don't know how to find foreign table for association #{association.inspect}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Given two tables returns a structure which describes a potential association
|
35
|
+
# between these tables, based on foreign key descriptions found in the database.
|
36
|
+
#
|
37
|
+
# The returned struct looks something like this:
|
38
|
+
#
|
39
|
+
# #<struct
|
40
|
+
# belonging_table="public.users",
|
41
|
+
# belonging_column="organization_id",
|
42
|
+
# having_table="public.organizations",
|
43
|
+
# having_column="id"
|
44
|
+
# >
|
45
|
+
#
|
46
|
+
# Raises an ArgumentError if no association can be found between these tables.
|
47
|
+
#
|
48
|
+
def find_matching_relation(host_table, associated_table)
|
49
|
+
sql = <<~SQL
|
50
|
+
WITH foreign_keys AS(
|
51
|
+
SELECT
|
52
|
+
tc.table_schema || '.' || tc.table_name AS belonging_table,
|
53
|
+
kcu.column_name AS belonging_column,
|
54
|
+
ccu.table_schema || '.' || ccu.table_name AS having_table,
|
55
|
+
ccu.column_name AS having_column
|
56
|
+
FROM
|
57
|
+
information_schema.table_constraints AS tc
|
58
|
+
JOIN information_schema.key_column_usage AS kcu
|
59
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
60
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
61
|
+
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
62
|
+
WHERE constraint_type = 'FOREIGN KEY'
|
63
|
+
)
|
64
|
+
SELECT * FROM foreign_keys
|
65
|
+
WHERE (belonging_table=$1 AND having_table=$2)
|
66
|
+
OR (belonging_table=$2 AND having_table=$1)
|
67
|
+
SQL
|
68
|
+
|
69
|
+
relations = SQL.all(sql, host_table, associated_table, into: :struct)
|
70
|
+
raise ArgumentError, "Found two potential matches for the #{association.inspect} relation" if relations.length > 1
|
71
|
+
raise ArgumentError, "Found no potential match for the #{association.inspect} relation" if relations.empty?
|
72
|
+
|
73
|
+
relations.first
|
74
|
+
end
|
75
|
+
|
76
|
+
# preloads a belongs_to association.
|
77
|
+
def preload_belongs_to(records, relation, as:)
|
78
|
+
belonging_column = relation.belonging_column.to_sym
|
79
|
+
having_column = relation.having_column.to_sym
|
80
|
+
|
81
|
+
foreign_ids = H.pluck(records, belonging_column).uniq.compact
|
82
|
+
|
83
|
+
scope = SQL::Scope.new(table: relation.having_table)
|
84
|
+
scope = scope.where(having_column => foreign_ids)
|
85
|
+
|
86
|
+
recs = SQL.all(scope, into: Hash)
|
87
|
+
recs_by_id = H.by_key(recs, having_column)
|
88
|
+
|
89
|
+
records.each do |model|
|
90
|
+
model[as] = recs_by_id[model.fetch(belonging_column)]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# preloads a has_one or has_many association.
|
95
|
+
def preload_has_one_or_many(records, relation, as:, order_by:, limit:)
|
96
|
+
if limit
|
97
|
+
# To really make sense limit must be implemented using window
|
98
|
+
# functions, because one (or, at lieast, I) would expect this code
|
99
|
+
#
|
100
|
+
# organizations = SQL.all "SELECT * FROM organizations", into: Hash
|
101
|
+
# organizations.preload :users, limit: 2, order_by: "id DESC"
|
102
|
+
#
|
103
|
+
# to return up to two users **per organization**.
|
104
|
+
#
|
105
|
+
raise "Support for limit: is not implemented yet!"
|
106
|
+
end
|
107
|
+
|
108
|
+
belonging_column = relation.belonging_column.to_sym
|
109
|
+
having_column = relation.having_column.to_sym
|
110
|
+
|
111
|
+
host_ids = H.pluck(records, having_column).uniq.compact
|
112
|
+
|
113
|
+
scope = SQL::Scope.new(table: relation.belonging_table)
|
114
|
+
scope = scope.where(belonging_column => host_ids)
|
115
|
+
scope = scope.order_by(order_by) if order_by
|
116
|
+
|
117
|
+
recs = SQL.all(scope, into: Hash)
|
118
|
+
|
119
|
+
if as.to_s.singularize == as.to_s
|
120
|
+
recs_by_id = H.by_key(recs, belonging_column) # has_one
|
121
|
+
else
|
122
|
+
recs_by_id = H.stable_group_by_key(recs, belonging_column) # has_many
|
123
|
+
end
|
124
|
+
|
125
|
+
records.each do |model|
|
126
|
+
model[as] = recs_by_id[model.fetch(having_column)]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
public
|
131
|
+
|
132
|
+
# Preloads a association into the records array.
|
133
|
+
#
|
134
|
+
# Parameters:
|
135
|
+
#
|
136
|
+
# - records: an Array of hashes.
|
137
|
+
# - association: the name of the association
|
138
|
+
# - host_table: the name of the table \a records has been loaded from.
|
139
|
+
# - schema: the schema name in the database.
|
140
|
+
# - as: the name to sue for the association. Defaults to +association+
|
141
|
+
def preload(records, association, host_table:, schema:, as:, order_by:, limit:)
|
142
|
+
return records if records.empty?
|
143
|
+
|
144
|
+
expect! records.first => Hash
|
145
|
+
|
146
|
+
as = association if as.nil?
|
147
|
+
fq_host_table = "#{schema}.#{host_table}"
|
148
|
+
|
149
|
+
associated_table = find_associated_table(association, schema: schema)
|
150
|
+
relation = find_matching_relation(fq_host_table, associated_table)
|
151
|
+
|
152
|
+
if fq_host_table == relation.belonging_table
|
153
|
+
if order_by || limit
|
154
|
+
raise ArgumentError, "#{association.inspect} is a singular association, w/o support for order_by: and limit:"
|
155
|
+
end
|
156
|
+
preload_belongs_to records, relation, as: as
|
157
|
+
else
|
158
|
+
preload_has_one_or_many records, relation, as: as, order_by: order_by, limit: limit
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|