Skip to content

Commit eaa2d29

Browse files
authored
Extract QueryExecutor to eliminate duplication (#182)
Create new QueryExecutor class to encapsulate the pattern of executing database queries and mapping results to domain objects.
1 parent 6bf8e2c commit eaa2d29

File tree

7 files changed

+128
-54
lines changed

7 files changed

+128
-54
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ changelog, see the [commits] for each version via the version links.
2020
all schemas (#177)
2121
- Drop Rails 7.1 due to EOL (#176)
2222
- YARD documentation improvements (#179)
23+
- Extract `QueryExecutor` to remove duplication (#182)
2324

2425
## [0.9.0]
2526

lib/fx/adapters/postgres/functions.rb

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "fx/function"
2+
require "fx/adapters/postgres/query_executor"
23

34
module Fx
45
module Adapters
@@ -28,31 +29,12 @@ class Functions
2829
# Wraps #all as a static facade.
2930
#
3031
# @return [Array<Fx::Function>]
31-
def self.all(...)
32-
new(...).all
33-
end
34-
35-
def initialize(connection)
36-
@connection = connection
37-
end
38-
39-
# All of the functions that this connection has defined.
40-
#
41-
# @return [Array<Fx::Function>]
42-
def all
43-
functions_from_postgres.map { |function| to_fx_function(function) }
44-
end
45-
46-
private
47-
48-
attr_reader :connection
49-
50-
def functions_from_postgres
51-
connection.execute(FUNCTIONS_WITH_DEFINITIONS_QUERY)
52-
end
53-
54-
def to_fx_function(result)
55-
Fx::Function.new(result)
32+
def self.all(connection)
33+
QueryExecutor.call(
34+
connection: connection,
35+
query: FUNCTIONS_WITH_DEFINITIONS_QUERY,
36+
model_class: Fx::Function
37+
)
5638
end
5739
end
5840
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module Fx
2+
module Adapters
3+
class Postgres
4+
# Executes database queries and maps results to domain objects.
5+
# @api private
6+
class QueryExecutor
7+
def self.call(...)
8+
new(...).call
9+
end
10+
11+
def initialize(connection:, query:, model_class:)
12+
@connection = connection
13+
@query = query
14+
@model_class = model_class
15+
end
16+
17+
# Executes the query and maps results to domain objects.
18+
#
19+
# @return [Array] Array of domain objects (Functions or Triggers)
20+
def call
21+
results_from_postgres.map { |result| model_class.new(result) }
22+
end
23+
24+
private
25+
26+
attr_reader :connection, :query, :model_class
27+
28+
def results_from_postgres
29+
connection.execute(query)
30+
end
31+
end
32+
end
33+
end
34+
end

lib/fx/adapters/postgres/triggers.rb

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "fx/trigger"
2+
require "fx/adapters/postgres/query_executor"
23

34
module Fx
45
module Adapters
@@ -28,31 +29,12 @@ class Triggers
2829
# Wraps #all as a static facade.
2930
#
3031
# @return [Array<Fx::Trigger>]
31-
def self.all(...)
32-
new(...).all
33-
end
34-
35-
def initialize(connection)
36-
@connection = connection
37-
end
38-
39-
# All of the triggers that this connection has defined.
40-
#
41-
# @return [Array<Fx::Trigger>]
42-
def all
43-
triggers_from_postgres.map { |trigger| to_fx_trigger(trigger) }
44-
end
45-
46-
private
47-
48-
attr_reader :connection
49-
50-
def triggers_from_postgres
51-
connection.execute(TRIGGERS_WITH_DEFINITIONS_QUERY)
52-
end
53-
54-
def to_fx_trigger(result)
55-
Fx::Trigger.new(result)
32+
def self.all(connection)
33+
QueryExecutor.call(
34+
connection: connection,
35+
query: TRIGGERS_WITH_DEFINITIONS_QUERY,
36+
model_class: Fx::Trigger
37+
)
5638
end
5739
end
5840
end

spec/fx/adapters/postgres/functions_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
$$ LANGUAGE plpgsql;
1414
SQL
1515

16-
functions = Fx::Adapters::Postgres::Functions.new(connection).all
16+
functions = Fx::Adapters::Postgres::Functions.all(connection)
1717

1818
first = functions.first
1919
expect(functions.size).to eq(1)
@@ -32,7 +32,7 @@
3232
connection.execute "CREATE SCHEMA IF NOT EXISTS other;"
3333
connection.execute "SET search_path = 'other';"
3434

35-
functions = Fx::Adapters::Postgres::Functions.new(connection).all
35+
functions = Fx::Adapters::Postgres::Functions.all(connection)
3636

3737
expect(functions).to be_empty
3838
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
require "spec_helper"
2+
3+
RSpec.describe Fx::Adapters::Postgres::QueryExecutor, :db do
4+
it "executes the query and maps results to objects" do
5+
connection = ActiveRecord::Base.connection
6+
query = "SELECT 'Hello World' as message, 'english' as language"
7+
greeter = Class.new do
8+
attr_reader :message, :language
9+
10+
def initialize(row)
11+
@message = row.fetch("message")
12+
@language = row.fetch("language")
13+
end
14+
end
15+
16+
results = described_class.call(
17+
connection: connection,
18+
query: query,
19+
model_class: greeter
20+
)
21+
22+
expect(results.size).to eq(1)
23+
expect(results.first).to be_a(greeter)
24+
expect(results.first.message).to eq("Hello World")
25+
expect(results.first.language).to eq("english")
26+
end
27+
28+
it "executes query with multiple results" do
29+
connection = ActiveRecord::Base.connection
30+
query = <<~SQL
31+
SELECT 'first' as name
32+
UNION ALL
33+
SELECT 'second' as name
34+
ORDER BY name
35+
SQL
36+
simple_name = Class.new do
37+
attr_reader :name
38+
39+
def initialize(row)
40+
@name = row.fetch("name")
41+
end
42+
end
43+
44+
results = described_class.call(
45+
connection: connection,
46+
query: query,
47+
model_class: simple_name
48+
)
49+
50+
expect(results.size).to eq(2)
51+
expect(results).to all(be_a(simple_name))
52+
expect(results.first.name).to eq("first")
53+
expect(results.last.name).to eq("second")
54+
end
55+
56+
it "returns an empty array when query returns no results" do
57+
connection = ActiveRecord::Base.connection
58+
query = "SELECT 'test' as name WHERE 1 = 0"
59+
simple_name = Class.new do
60+
attr_reader :name
61+
62+
def initialize(row)
63+
@name = row.fetch("name")
64+
end
65+
end
66+
67+
results = described_class.call(
68+
connection: connection,
69+
query: query,
70+
model_class: simple_name
71+
)
72+
73+
expect(results).to eq([])
74+
end
75+
end

spec/fx/adapters/postgres/triggers_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
EXECUTE FUNCTION uppercase_users_name();
2828
SQL
2929

30-
triggers = Fx::Adapters::Postgres::Triggers.new(connection).all
30+
triggers = Fx::Adapters::Postgres::Triggers.all(connection)
3131

3232
first = triggers.first
3333
expect(triggers.size).to eq(1)
@@ -40,7 +40,7 @@
4040
connection.execute "CREATE SCHEMA IF NOT EXISTS other;"
4141
connection.execute "SET search_path = 'other';"
4242

43-
triggers = Fx::Adapters::Postgres::Triggers.new(connection).all
43+
triggers = Fx::Adapters::Postgres::Triggers.all(connection)
4444

4545
expect(triggers).to be_empty
4646
end

0 commit comments

Comments
 (0)