Skip to content

Commit 683f2fe

Browse files
tomerothblelump
authored andcommitted
Add pipeline example
1 parent b6ba364 commit 683f2fe

10 files changed

Lines changed: 376 additions & 3 deletions

examples/interpreter/collector.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
require File.expand_path('../../interpreter', __FILE__)
4+
5+
class Interpreter
6+
class Collector
7+
def call(param_builder)
8+
{
9+
where: [Interpreter.new(ast: param_builder.filters.to_ast).to_sql],
10+
order: param_builder.order.first.to_a.join(' '),
11+
from: param_builder.from.first,
12+
limit: param_builder.limit,
13+
offset: param_builder.offset,
14+
search_query: param_builder.search_query
15+
}
16+
end
17+
end
18+
end

examples/interpreter/to_sql/expression_factory.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def visit_course_category_ids(value, _left, right)
3232
<<~SQL
3333
EXISTS(
3434
SELECT TRUE FROM category_courses
35-
WHERE category_courses.category_id #{determine_values(value, right)})
35+
WHERE category_courses.category_id #{determine_values(value, right)}
3636
AND courses.id = category_courses.course_id
3737
)
3838
SQL

examples/sql_builder.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
class SqlBuilder
4+
attr_reader :sql_struct_parts, :model_name
5+
6+
def initialize(sql_struct_parts:, model_name:)
7+
@sql_struct_parts = sql_struct_parts
8+
@model_name = model_name
9+
end
10+
11+
def preloads
12+
ssp.preloads
13+
end
14+
15+
def sql_query
16+
<<~SQL
17+
SELECT #{select} FROM #{from} #{ssp.joins.join(' ')}
18+
WHERE #{ssp.where.join(' AND ')}
19+
ORDER BY #{ssp.order}
20+
LIMIT #{ssp.limit}
21+
OFFSET #{ssp.offset}
22+
SQL
23+
end
24+
25+
def count_query
26+
<<~SQL
27+
SELECT #{count_select} FROM #{from} #{ssp.joins.join(' ')}
28+
WHERE #{ssp.where.join(' AND ')}
29+
SQL
30+
end
31+
32+
def count_select
33+
"COUNT(#{select})"
34+
end
35+
36+
def select
37+
return '*' if ssp.select.empty?
38+
ssp.select
39+
end
40+
41+
def from
42+
return model_name if ssp.from.empty?
43+
ssp.from
44+
end
45+
46+
def to_s
47+
to_sql.to_s
48+
end
49+
50+
alias ssp sql_struct_parts
51+
end

examples/sql_struct_parts.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
class SqlStructParts
4+
PARTS = %w[select from joins where search_query order limit offset preload].freeze
5+
6+
attr_reader :deps
7+
8+
def initialize(**opts)
9+
@deps = opts[:deps] || {}
10+
end
11+
12+
def with(deps)
13+
self.class.new(deps: initialize_dependencies(deps))
14+
end
15+
16+
PARTS.each do |sql_part|
17+
define_method sql_part do
18+
@deps[sql_part.to_sym] || []
19+
end
20+
end
21+
22+
# @api private
23+
def initialize_dependencies(deps)
24+
params = {}
25+
params = initialize_arrays(params, deps)
26+
params = initialize_constants(params, deps)
27+
params
28+
end
29+
30+
# @api private
31+
def initialize_arrays(params, deps)
32+
params[:select] = select.to_a | deps[:select].to_a
33+
params[:from] = deps[:from].to_s
34+
params[:joins] = joins.to_a | deps[:joins].to_a
35+
params[:where] = where | deps[:where].to_a
36+
params
37+
end
38+
39+
# @api private
40+
def initialize_constants(params, deps)
41+
params[:search_query] = deps[:search_query].to_s
42+
params[:order] = deps[:order].to_s
43+
params[:limit] = deps[:limit].to_i
44+
params[:offset] = deps[:offset].to_i
45+
params[:preload] = preload | deps[:preload].to_a
46+
params
47+
end
48+
49+
alias << with
50+
end

examples/user_pipeline.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
require 'filterly'
4+
require_relative 'interpreter/collector'
5+
require_relative 'user_query_pipeline'
6+
7+
class UserPipeline
8+
attr_reader :params, :param_builder
9+
10+
def initialize(params:, param_builder: ::Filterly::Pipeline::ParamBuilder.new)
11+
@params = params
12+
@param_builder = param_builder
13+
end
14+
15+
def call
16+
build_params
17+
18+
count, query = UserQueryPipeline.new(params: interpret_to_sql).call
19+
puts '---------- SQL COUNT query -----------'
20+
puts
21+
puts count
22+
puts
23+
puts
24+
puts '---------- SQL main query ------------'
25+
puts query
26+
puts
27+
end
28+
29+
# @api private
30+
def build_params
31+
param_builder << filters
32+
param_builder << some_other_filters
33+
end
34+
35+
# @api private
36+
def interpret_to_sql
37+
Interpreter::Collector.new.call(param_builder)
38+
end
39+
40+
# @api private
41+
def filters
42+
params.to_h
43+
end
44+
45+
def some_other_filters
46+
{
47+
filters: {
48+
course_ids: [12, 34, 54]
49+
},
50+
select: ['courses.new_column as new_one'],
51+
params: { search_query: 'adam' }
52+
}
53+
end
54+
end
55+
56+
UserPipeline.new(
57+
params: {
58+
filters: {
59+
category_ids: [12, 34, 54],
60+
course_annual_id: 23,
61+
course_semester_id: 123
62+
},
63+
from: 'courses',
64+
order: { 'courses.id': 'asc' },
65+
params: {
66+
limit: 10,
67+
offset: 0,
68+
not_supported: 'omitted'
69+
}
70+
}
71+
).call

examples/user_query_pipeline.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'sql_struct_parts'
4+
require_relative 'sql_builder'
5+
6+
class UserQueryPipeline
7+
attr_reader :sql_struct_parts
8+
9+
def initialize(sql_struct_parts:)
10+
@sql_struct_parts = sql_struct_parts
11+
end
12+
13+
def self.new(params:)
14+
super(sql_struct_parts: SqlStructParts.new(deps: params))
15+
end
16+
17+
def call
18+
sql_struct_parts << something_special
19+
20+
sql_builder = SqlBuilder.new(sql_struct_parts: sql_struct_parts, model_name: 'users')
21+
22+
[sql_builder.count_query, sql_builder.sql_query]
23+
end
24+
25+
def something_special
26+
{
27+
where: ["user.ethnicity IN('celts', 'slavic')"]
28+
}
29+
end
30+
end

spec/examples/interpreter_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
(course_id='23' OR (course_id='7' OR course_id='56')) AND annual='2017-2018'
129129
AND EXISTS(
130130
SELECT TRUE FROM category_courses
131-
WHERE category_courses.category_id IN('67','32','34'))
131+
WHERE category_courses.category_id IN('67','32','34')
132132
AND courses.id = category_courses.course_id
133133
)
134134
SQL

spec/examples/sql_builder_spec.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
require 'examples/sql_builder'
6+
require 'examples/sql_struct_parts'
7+
8+
RSpec.describe SqlBuilder do
9+
subject do
10+
described_class.new(sql_struct_parts: sql_struct, model_name: 'users')
11+
end
12+
13+
let(:sql_struct) do
14+
SqlStructParts.new(
15+
deps: {
16+
select: 'users.*',
17+
from: '(SELECT users WHERE id IN(1,2,3)) as users',
18+
where: ['(users.name = "Pszemek" OR users.age > 24)', 'users.status = "adult"'],
19+
joins: ['LEFT JOIN courses c ON(c.id=users.course_id)'],
20+
limit: 10,
21+
order: 'users.id ASC',
22+
offset: 5,
23+
preload: %i[courses addresses]
24+
}
25+
)
26+
end
27+
28+
let(:sql_struct_no_from_or_select) do
29+
SqlStructParts.new(
30+
deps: {
31+
select: [],
32+
from: nil,
33+
where: ['users.name = "Pszemek" AND users.age > 24'],
34+
joins: ['LEFT JOIN courses c ON(c.id=users.course_id)'],
35+
limit: 10,
36+
order: 'users.id ASC',
37+
offset: 5,
38+
preload: %i[courses addresses]
39+
}
40+
)
41+
end
42+
43+
describe '#sql_query' do
44+
it 'returns sql query' do
45+
expect(subject.sql_query.tr("\n", ' ')).to eql(
46+
<<~SQL.tr("\n", ' ')
47+
SELECT users.* FROM (SELECT users WHERE id IN(1,2,3)) as users
48+
LEFT JOIN courses c ON(c.id=users.course_id)
49+
WHERE (users.name = "Pszemek" OR users.age > 24) AND users.status = "adult"
50+
ORDER BY users.id ASC
51+
LIMIT 10
52+
OFFSET 5
53+
SQL
54+
)
55+
end
56+
57+
it 'uses model_name when from is upsent' do
58+
result = described_class.new(
59+
sql_struct_parts: sql_struct_no_from_or_select,
60+
model_name: 'users'
61+
).sql_query
62+
63+
expect(result.tr("\n", ' ')).to eql(
64+
<<~SQL.tr("\n", ' ')
65+
SELECT * FROM users
66+
LEFT JOIN courses c ON(c.id=users.course_id)
67+
WHERE users.name = "Pszemek" AND users.age > 24
68+
ORDER BY users.id ASC
69+
LIMIT 10
70+
OFFSET 5
71+
SQL
72+
)
73+
end
74+
end
75+
76+
describe '#count_query' do
77+
it 'returns sql count query' do
78+
expect(subject.count_query.tr("\n", ' ')).to eql(
79+
<<~SQL.tr("\n", ' ')
80+
SELECT COUNT(users.*) FROM (SELECT users WHERE id IN(1,2,3)) as users
81+
LEFT JOIN courses c ON(c.id=users.course_id)
82+
WHERE (users.name = "Pszemek" OR users.age > 24) AND users.status = "adult"
83+
SQL
84+
)
85+
end
86+
end
87+
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require './examples/sql_struct_parts'
5+
6+
RSpec.describe SqlStructParts do
7+
subject do
8+
described_class.new(deps: deps)
9+
end
10+
11+
let(:deps) do
12+
{
13+
select: ['users.*'],
14+
where: ['user.id IN(1,2,3)'],
15+
limit: 20,
16+
joins: ['LEFT JOIN courses c ON(c.id=b.lk)']
17+
}
18+
end
19+
20+
describe '#new' do
21+
it 'creates deps with default values' do
22+
expect(subject.deps).to eql(
23+
select: ['users.*'],
24+
limit: 20,
25+
where: ['user.id IN(1,2,3)'],
26+
joins: ['LEFT JOIN courses c ON(c.id=b.lk)']
27+
)
28+
end
29+
end
30+
31+
describe '#with' do
32+
it 'merges new params with the old ones' do
33+
result = subject
34+
.with(
35+
limit: 10,
36+
offset: 0,
37+
joins: ['INNER JOIN addresses a ON(a.id=b.address_id)']
38+
)
39+
.with(from: 'users', select: ['id, surname'], where: ['user.age > 45'])
40+
41+
expect(result.deps).to eql(
42+
from: 'users',
43+
joins: [
44+
'LEFT JOIN courses c ON(c.id=b.lk)',
45+
'INNER JOIN addresses a ON(a.id=b.address_id)'
46+
],
47+
limit: 0,
48+
offset: 0,
49+
order: '',
50+
preload: [],
51+
search_query: '',
52+
select: ['users.*', 'id, surname'],
53+
where: ['user.id IN(1,2,3)', 'user.age > 45']
54+
)
55+
end
56+
end
57+
58+
describe '#initialize_dependencies' do
59+
it 'allows to call attributes by methods' do
60+
result = subject.with(limit: 11)
61+
62+
expect(result.limit).to eql(11)
63+
expect(result.where).to eql(['user.id IN(1,2,3)'])
64+
end
65+
end
66+
end

0 commit comments

Comments
 (0)