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