rom-sql 1.3.1 → 1.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/lib/rom/sql/association.rb +1 -1
- data/lib/rom/sql/association/many_to_one.rb +1 -1
- data/lib/rom/sql/association/name.rb +1 -1
- data/lib/rom/sql/association/one_to_many.rb +1 -1
- data/lib/rom/sql/attribute.rb +23 -9
- data/lib/rom/sql/dsl.rb +13 -0
- data/lib/rom/sql/extensions/postgres/commands.rb +4 -4
- data/lib/rom/sql/extensions/postgres/types.rb +262 -110
- data/lib/rom/sql/function.rb +2 -2
- data/lib/rom/sql/projection_dsl.rb +1 -12
- data/lib/rom/sql/relation.rb +1 -1
- data/lib/rom/sql/relation/reading.rb +8 -4
- data/lib/rom/sql/restriction_dsl.rb +7 -1
- data/lib/rom/sql/types.rb +2 -0
- data/lib/rom/sql/version.rb +1 -1
- data/spec/extensions/postgres/attribute_spec.rb +78 -0
- data/spec/integration/association/many_to_many/custom_fks_spec.rb +8 -3
- data/spec/integration/association/many_to_many/from_view_spec.rb +9 -3
- data/spec/integration/association/many_to_many_spec.rb +8 -2
- data/spec/integration/association/many_to_one/custom_fks_spec.rb +8 -4
- data/spec/integration/association/many_to_one/from_view_spec.rb +10 -4
- data/spec/integration/association/many_to_one/self_ref_spec.rb +4 -2
- data/spec/integration/association/many_to_one_spec.rb +8 -4
- data/spec/integration/association/one_to_many/custom_fks_spec.rb +5 -2
- data/spec/integration/association/one_to_many/from_view_spec.rb +5 -2
- data/spec/integration/association/one_to_many/self_ref_spec.rb +4 -2
- data/spec/integration/association/one_to_many_spec.rb +1 -1
- data/spec/integration/commands/upsert_spec.rb +2 -2
- data/spec/integration/plugins/auto_wrap_spec.rb +1 -1
- data/spec/integration/sequel_api_spec.rb +3 -2
- data/spec/unit/function_spec.rb +1 -1
- data/spec/unit/order_dsl_spec.rb +4 -4
- data/spec/unit/projection_dsl_spec.rb +8 -0
- data/spec/unit/relation/dataset_spec.rb +3 -3
- data/spec/unit/relation/project_spec.rb +1 -1
- data/spec/unit/relation/qualified_columns_spec.rb +3 -2
- data/spec/unit/relation/where_spec.rb +20 -0
- data/spec/unit/restriction_dsl_spec.rb +2 -2
- metadata +2 -2
data/lib/rom/sql/function.rb
CHANGED
@@ -7,7 +7,7 @@ module ROM
|
|
7
7
|
class ProjectionDSL < DSL
|
8
8
|
# @api public
|
9
9
|
def `(value)
|
10
|
-
expr = ::Sequel
|
10
|
+
expr = ::Sequel.lit(value)
|
11
11
|
::ROM::SQL::Attribute.new(type(:string)).meta(sql_expr: expr)
|
12
12
|
end
|
13
13
|
|
@@ -18,17 +18,6 @@ module ROM
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
# @api private
|
22
|
-
def type(identifier)
|
23
|
-
type_name = ::Dry::Core::Inflector.classify(identifier)
|
24
|
-
types.const_get(type_name) if types.const_defined?(type_name)
|
25
|
-
end
|
26
|
-
|
27
|
-
# @api private
|
28
|
-
def types
|
29
|
-
::ROM::SQL::Types
|
30
|
-
end
|
31
|
-
|
32
21
|
# @api private
|
33
22
|
def method_missing(meth, *args, &block)
|
34
23
|
if schema.key?(meth)
|
data/lib/rom/sql/relation.rb
CHANGED
@@ -54,7 +54,7 @@ module ROM
|
|
54
54
|
|
55
55
|
if db.table_exists?(table)
|
56
56
|
if schema
|
57
|
-
select(*schema
|
57
|
+
select(*schema).order(*schema.project(*schema.primary_key_names).qualified)
|
58
58
|
else
|
59
59
|
select(*columns).order(*klass.primary_key_columns(db, table))
|
60
60
|
end
|
@@ -105,7 +105,7 @@ module ROM
|
|
105
105
|
#
|
106
106
|
# @api public
|
107
107
|
def qualified_columns
|
108
|
-
schema.qualified.map(&:
|
108
|
+
schema.qualified.map(&:to_sql_name)
|
109
109
|
end
|
110
110
|
|
111
111
|
# Map tuples from the relation
|
@@ -341,8 +341,10 @@ module ROM
|
|
341
341
|
where(*args).where(self.class.schema.restriction(&block))
|
342
342
|
elsif args.size == 1 && args[0].is_a?(Hash)
|
343
343
|
new(dataset.where(coerce_conditions(args[0])))
|
344
|
-
|
344
|
+
elsif !args.empty?
|
345
345
|
new(dataset.where(*args))
|
346
|
+
else
|
347
|
+
self
|
346
348
|
end
|
347
349
|
end
|
348
350
|
|
@@ -395,9 +397,9 @@ module ROM
|
|
395
397
|
# @api public
|
396
398
|
def having(*args, &block)
|
397
399
|
if block
|
398
|
-
new(dataset.having(*args
|
400
|
+
new(dataset.having(*args, *self.class.schema.restriction(&block)))
|
399
401
|
else
|
400
|
-
new(dataset.__send__(__method__, *args
|
402
|
+
new(dataset.__send__(__method__, *args))
|
401
403
|
end
|
402
404
|
end
|
403
405
|
|
@@ -835,6 +837,8 @@ module ROM
|
|
835
837
|
else
|
836
838
|
new(dataset.__send__(type, other.to_sym, join_cond, opts, &block))
|
837
839
|
end
|
840
|
+
elsif other.is_a?(Sequel::SQL::AliasedExpression)
|
841
|
+
new(dataset.__send__(type, other, join_cond, opts, &block))
|
838
842
|
elsif other.respond_to?(:name) && other.name.is_a?(Relation::Name)
|
839
843
|
associations[other.name.dataset].join(__registry__, type, self, other)
|
840
844
|
else
|
@@ -16,7 +16,13 @@ module ROM
|
|
16
16
|
if schema.key?(meth)
|
17
17
|
schema[meth]
|
18
18
|
else
|
19
|
-
|
19
|
+
type = type(meth)
|
20
|
+
|
21
|
+
if type
|
22
|
+
::ROM::SQL::Function.new(type)
|
23
|
+
else
|
24
|
+
::Sequel::VIRTUAL_ROW.__send__(meth, *args, &block)
|
25
|
+
end
|
20
26
|
end
|
21
27
|
end
|
22
28
|
end
|
data/lib/rom/sql/types.rb
CHANGED
data/lib/rom/sql/version.rb
CHANGED
@@ -125,4 +125,82 @@ RSpec.describe 'ROM::SQL::Attribute', :postgres do
|
|
125
125
|
{ result: jsonb_hash['age' => 25] }])
|
126
126
|
end
|
127
127
|
end
|
128
|
+
|
129
|
+
describe 'using array types' do
|
130
|
+
before do
|
131
|
+
conn.create_table :pg_people do
|
132
|
+
primary_key :id
|
133
|
+
String :name
|
134
|
+
column :emails, 'text[]'
|
135
|
+
column :bigids, 'bigint[]'
|
136
|
+
end
|
137
|
+
|
138
|
+
conf.commands(:people) do
|
139
|
+
define(:create)
|
140
|
+
define(:update)
|
141
|
+
end
|
142
|
+
|
143
|
+
create_person.(name: 'John Doe', emails: %w(john@doe.com john@example.com), bigids: [84])
|
144
|
+
create_person.(name: 'Jade Doe', emails: %w(jade@hotmail.com), bigids: [42])
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'filters by email inclusion' do
|
148
|
+
expect(people.select(:name).where { emails.contain(['john@doe.com']) }.one).
|
149
|
+
to eql(name: 'John Doe')
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'coerces values so that PG does not complain' do
|
153
|
+
expect(people.select(:name).where { bigids.contain([84]) }.one).
|
154
|
+
to eql(name: 'John Doe')
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'fetches element by index' do
|
158
|
+
expect(people.select { [name, emails.get(2).as(:second_email)] }.to_a).
|
159
|
+
to eql([{ name: 'John Doe', second_email: 'john@example.com' },
|
160
|
+
{ name: 'Jade Doe', second_email: nil }])
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'restricts with ANY' do
|
164
|
+
expect(people.select(:name).where { bigids.any(84)}.one).
|
165
|
+
to eql(name: 'John Doe')
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'restricts by <@' do
|
169
|
+
expect(people.select(:name).where { bigids.contained_by((30..50).to_a) }.one).
|
170
|
+
to eql(name: 'Jade Doe')
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'returns array length' do
|
174
|
+
expect(people.select { [name, emails.length.as(:size)] }.to_a).
|
175
|
+
to eql([{ name: 'John Doe', size: 2 }, { name: 'Jade Doe', size: 1 }])
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'restrict by overlapping with other array' do
|
179
|
+
expect(people.select(:name).where { emails.overlaps(%w(jade@hotmail.com)) }.one).
|
180
|
+
to eql(name: 'Jade Doe')
|
181
|
+
|
182
|
+
expect(people.select(:name).where { bigids.overlaps([42]) }.one).
|
183
|
+
to eql(name: 'Jade Doe')
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'removes element by value' do
|
187
|
+
expect(people.select { emails.remove_value('john@example.com').as(:emails) }.to_a).
|
188
|
+
to eq([{ emails: %w(john@doe.com) }, { emails: %w(jade@hotmail.com) }])
|
189
|
+
|
190
|
+
pending "doesn't have auto-casting yet"
|
191
|
+
expect(people.select(:name).where { bigids.remove_value(100).contains([42]) }.one).
|
192
|
+
to eql(name: 'Jade Doe')
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'joins values' do
|
196
|
+
expect(people.select { emails.join(',').as(:emails) }.to_a).
|
197
|
+
to eql([{ emails: 'john@doe.com,john@example.com' },
|
198
|
+
{ emails: 'jade@hotmail.com' }])
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'concatenates arrays' do
|
202
|
+
expect(people.select { (emails + %w(foo@bar.com)).as(:emails) }.where { name.is('Jade Doe') }.one).
|
203
|
+
to eq(emails: %w(jade@hotmail.com foo@bar.com))
|
204
|
+
end
|
205
|
+
end
|
128
206
|
end
|
@@ -9,6 +9,9 @@ RSpec.describe ROM::SQL::Association::ManyToMany, '#call' do
|
|
9
9
|
relations[:users].associations[:puzzles]
|
10
10
|
end
|
11
11
|
|
12
|
+
let(:puzzles) { relations[:puzzles] }
|
13
|
+
let(:puzzle_solvers) { relations[:puzzle_solvers] }
|
14
|
+
|
12
15
|
with_adapters do
|
13
16
|
before do
|
14
17
|
conn.create_table(:puzzles) do
|
@@ -52,10 +55,12 @@ RSpec.describe ROM::SQL::Association::ManyToMany, '#call' do
|
|
52
55
|
end
|
53
56
|
|
54
57
|
it 'prepares joined relations using custom FK' do
|
55
|
-
relation = assoc.call(relations).order(:
|
58
|
+
relation = assoc.call(relations).order(puzzles[:text].qualified, puzzle_solvers[:solver_id].qualified)
|
56
59
|
|
57
|
-
expect(relation.schema.map(&:
|
58
|
-
to eql(
|
60
|
+
expect(relation.schema.map(&:to_sql_name)).
|
61
|
+
to eql([Sequel.qualify(:puzzles, :id),
|
62
|
+
Sequel.qualify(:puzzles, :text),
|
63
|
+
Sequel.qualify(:puzzle_solvers, :solver_id)])
|
59
64
|
|
60
65
|
expect(relation.to_a).
|
61
66
|
to eql([
|
@@ -9,6 +9,9 @@ RSpec.describe ROM::SQL::Association::ManyToMany, '#call' do
|
|
9
9
|
relations[:users].associations[:solved_puzzles]
|
10
10
|
end
|
11
11
|
|
12
|
+
let(:puzzles) { relations[:puzzles] }
|
13
|
+
let(:puzzle_solvers) { relations[:puzzle_solvers] }
|
14
|
+
|
12
15
|
with_adapters do
|
13
16
|
before do
|
14
17
|
conn.create_table(:puzzles) do
|
@@ -67,10 +70,13 @@ RSpec.describe ROM::SQL::Association::ManyToMany, '#call' do
|
|
67
70
|
end
|
68
71
|
|
69
72
|
it 'prepares joined relations using custom FK' do
|
70
|
-
relation = assoc.call(relations).order(:
|
73
|
+
relation = assoc.call(relations).order(puzzles[:text].qualified, puzzle_solvers[:user_id].qualified)
|
71
74
|
|
72
|
-
expect(relation.schema.map(&:
|
73
|
-
to eql(
|
75
|
+
expect(relation.schema.map(&:to_sql_name)).
|
76
|
+
to eql([Sequel.qualify(:puzzles, :id),
|
77
|
+
Sequel.qualify(:puzzles, :text),
|
78
|
+
Sequel.qualify(:puzzles, :solved),
|
79
|
+
Sequel.qualify(:puzzle_solvers, :user_id)])
|
74
80
|
|
75
81
|
expect(relation.to_a).
|
76
82
|
to eql([
|
@@ -46,7 +46,10 @@ RSpec.describe ROM::SQL::Association::ManyToMany do
|
|
46
46
|
it 'prepares joined relations' do
|
47
47
|
relation = assoc.call(container.relations)
|
48
48
|
|
49
|
-
expect(relation.schema.map(&:
|
49
|
+
expect(relation.schema.map(&:to_sql_name)).
|
50
|
+
to eql([Sequel.qualify(:tags, :id),
|
51
|
+
Sequel.qualify(:tags, :name),
|
52
|
+
Sequel.qualify(:task_tags, :task_id)])
|
50
53
|
expect(relation.to_a).to eql([id: 1, name: 'important', task_id: 1])
|
51
54
|
end
|
52
55
|
end
|
@@ -59,7 +62,10 @@ RSpec.describe ROM::SQL::Association::ManyToMany do
|
|
59
62
|
it 'prepares joined relations through other association' do
|
60
63
|
relation = assoc.call(container.relations)
|
61
64
|
|
62
|
-
expect(relation.schema.map(&:
|
65
|
+
expect(relation.schema.map(&:to_sql_name)).
|
66
|
+
to eql([Sequel.qualify(:tags, :id),
|
67
|
+
Sequel.qualify(:tags, :name),
|
68
|
+
Sequel.qualify(:tasks, :user_id)])
|
63
69
|
expect(relation.to_a).to eql([id: 1, name: 'important', user_id: 2])
|
64
70
|
end
|
65
71
|
end
|
@@ -42,15 +42,19 @@ RSpec.describe ROM::SQL::Association::ManyToOne, '#call' do
|
|
42
42
|
it 'prepares joined relations using correct FKs based on association aliases' do
|
43
43
|
relation = assoc_from.call(relations)
|
44
44
|
|
45
|
-
expect(relation.schema.map(&:
|
46
|
-
to eql(
|
45
|
+
expect(relation.schema.map(&:to_sql_name)).
|
46
|
+
to eql([Sequel.qualify(:destinations, :id),
|
47
|
+
Sequel.qualify(:destinations, :name),
|
48
|
+
Sequel.qualify(:flights, :id).as(:flight_id)])
|
47
49
|
|
48
50
|
expect(relation.first).to eql(id: 1, name: 'FROM', flight_id: 1)
|
49
51
|
|
50
52
|
relation = assoc_to.call(relations)
|
51
53
|
|
52
|
-
expect(relation.schema.map(&:
|
53
|
-
to eql(
|
54
|
+
expect(relation.schema.map(&:to_sql_name)).
|
55
|
+
to eql([Sequel.qualify(:destinations, :id),
|
56
|
+
Sequel.qualify(:destinations, :name),
|
57
|
+
Sequel.qualify(:flights, :id).as(:flight_id)])
|
54
58
|
|
55
59
|
expect(relation.first).to eql(id: 2, name: 'TO', flight_id: 1)
|
56
60
|
end
|
@@ -60,16 +60,22 @@ RSpec.describe ROM::SQL::Association::ManyToOne, '#call' do
|
|
60
60
|
it 'prepares joined relations using custom view in target relation' do
|
61
61
|
relation = assoc_inter.call(relations)
|
62
62
|
|
63
|
-
expect(relation.schema.map(&:
|
64
|
-
to eql(
|
63
|
+
expect(relation.schema.map(&:to_sql_name)).
|
64
|
+
to eql([Sequel.qualify(:destinations, :id),
|
65
|
+
Sequel.qualify(:destinations, :name),
|
66
|
+
Sequel.qualify(:destinations, :intermediate),
|
67
|
+
Sequel.qualify(:flights, :id).as(:flight_id)])
|
65
68
|
|
66
69
|
expect(relation.first).to eql(id: 2, intermediate: db_true, name: 'Intermediate', flight_id: 1)
|
67
70
|
expect(relation.count).to be(1)
|
68
71
|
|
69
72
|
relation = assoc_final.call(relations)
|
70
73
|
|
71
|
-
expect(relation.schema.map(&:
|
72
|
-
to eql(
|
74
|
+
expect(relation.schema.map(&:to_sql_name)).
|
75
|
+
to eql([Sequel.qualify(:destinations, :id),
|
76
|
+
Sequel.qualify(:destinations, :name),
|
77
|
+
Sequel.qualify(:destinations, :intermediate),
|
78
|
+
Sequel.qualify(:flights, :id).as(:flight_id)])
|
73
79
|
|
74
80
|
expect(relation.first).to eql(id: 1, intermediate: db_false, name: 'Final', flight_id: 2)
|
75
81
|
expect(relation.count).to be(1)
|
@@ -37,8 +37,10 @@ RSpec.describe ROM::SQL::Association::OneToMany, '#call' do
|
|
37
37
|
it 'prepares joined relations using custom FK for a self-ref association' do
|
38
38
|
relation = assoc.call(relations)
|
39
39
|
|
40
|
-
expect(relation.schema.map(&:
|
41
|
-
to eql(
|
40
|
+
expect(relation.schema.map(&:to_sql_name)).
|
41
|
+
to eql([Sequel.qualify(:categories, :id),
|
42
|
+
Sequel.qualify(:categories, :parent_id),
|
43
|
+
Sequel.qualify(:categories, :name)])
|
42
44
|
|
43
45
|
expect(relation.to_a).
|
44
46
|
to eql([
|
@@ -41,8 +41,10 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
|
|
41
41
|
it 'prepares joined relations' do
|
42
42
|
relation = assoc.call(container.relations)
|
43
43
|
|
44
|
-
expect(relation.schema.map(&:
|
45
|
-
|
44
|
+
expect(relation.schema.map(&:to_sql_name)).
|
45
|
+
to eql([Sequel.qualify(:users, :id),
|
46
|
+
Sequel.qualify(:users, :name),
|
47
|
+
Sequel.qualify(:tasks, :id).as(:task_id)])
|
46
48
|
|
47
49
|
expect(relation.where(user_id: 1).one).to eql(id: 1, task_id: 2, name: 'Jane')
|
48
50
|
|
@@ -109,8 +111,10 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
|
|
109
111
|
it 'prepares joined relations' do
|
110
112
|
relation = assoc.call(container.relations)
|
111
113
|
|
112
|
-
expect(relation.schema.map(&:
|
113
|
-
|
114
|
+
expect(relation.schema.map(&:to_sql_name)).
|
115
|
+
to eql([Sequel.qualify(:users, :id),
|
116
|
+
Sequel.qualify(:users, :name),
|
117
|
+
Sequel.qualify(:posts, :post_id)])
|
114
118
|
|
115
119
|
expect(relation.order(:id).to_a).to eql([
|
116
120
|
{ id: 1, name: 'Jane', post_id: 2 },
|
@@ -40,8 +40,11 @@ RSpec.describe ROM::SQL::Association::OneToMany, '#call' do
|
|
40
40
|
it 'prepares joined relations using custom FK' do
|
41
41
|
relation = assoc.call(relations)
|
42
42
|
|
43
|
-
expect(relation.schema.map(&:
|
44
|
-
to eql(
|
43
|
+
expect(relation.schema.map(&:to_sql_name)).
|
44
|
+
to eql([Sequel.qualify(:puzzles, :id),
|
45
|
+
Sequel.qualify(:puzzles, :author_id),
|
46
|
+
Sequel.qualify(:puzzles, :solver_id),
|
47
|
+
Sequel.qualify(:puzzles, :text)])
|
45
48
|
|
46
49
|
expect(relation.first).to eql(id: 2, author_id: 2, solver_id: 1, text: 'P2')
|
47
50
|
end
|
@@ -44,8 +44,11 @@ RSpec.describe ROM::SQL::Association::OneToMany, '#call' do
|
|
44
44
|
it 'prepares joined relations using custom view' do
|
45
45
|
relation = assoc.call(relations)
|
46
46
|
|
47
|
-
expect(relation.schema.map(&:
|
48
|
-
to eql(
|
47
|
+
expect(relation.schema.map(&:to_sql_name)).
|
48
|
+
to eql([Sequel.qualify(:puzzles, :id),
|
49
|
+
Sequel.qualify(:puzzles, :user_id),
|
50
|
+
Sequel.qualify(:puzzles, :text),
|
51
|
+
Sequel.qualify(:puzzles, :solved)])
|
49
52
|
|
50
53
|
expect(relation.count).to be(1)
|
51
54
|
expect(relation.first).to eql(id: 2, user_id: 2, solved: db_true, text: 'P2')
|
@@ -38,8 +38,10 @@ RSpec.describe ROM::SQL::Association::OneToMany, '#call' do
|
|
38
38
|
it 'prepares joined relations using custom FK for a self-ref association' do
|
39
39
|
relation = assoc.call(relations)
|
40
40
|
|
41
|
-
expect(relation.schema.map(&:
|
42
|
-
to eql(
|
41
|
+
expect(relation.schema.map(&:to_sql_name)).
|
42
|
+
to eql([Sequel.qualify(:categories, :id),
|
43
|
+
Sequel.qualify(:categories, :parent_id),
|
44
|
+
Sequel.qualify(:categories, :name)])
|
43
45
|
|
44
46
|
expect(relation.to_a).
|
45
47
|
to eql([
|
@@ -26,7 +26,7 @@ RSpec.describe ROM::SQL::Association::OneToMany do
|
|
26
26
|
|
27
27
|
expect(relation.schema.map(&:name)).to eql(%i[id user_id title])
|
28
28
|
|
29
|
-
expect(relation.order(:
|
29
|
+
expect(relation.order(tasks[:id].qualified).to_a).to eql([
|
30
30
|
{ id: 1, user_id: 2, title: "Joe's task" },
|
31
31
|
{ id: 2, user_id: 1, title: "Jane's task" }
|
32
32
|
])
|
@@ -56,7 +56,7 @@ RSpec.describe 'Commands / Postgres / Upsert', :postgres, seeds: false do
|
|
56
56
|
let(:command_config) do
|
57
57
|
-> do
|
58
58
|
constraint :tasks_title_key
|
59
|
-
update_statement user_id: :
|
59
|
+
update_statement user_id: Sequel.qualify(:excluded, :user_id)
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
@@ -70,7 +70,7 @@ RSpec.describe 'Commands / Postgres / Upsert', :postgres, seeds: false do
|
|
70
70
|
-> do
|
71
71
|
conflict_target :title
|
72
72
|
update_statement user_id: nil
|
73
|
-
update_where
|
73
|
+
update_where Sequel.qualify(:tasks, :id) => 2
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|