query-composer 1.0.0
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 +7 -0
- data/LICENSE +20 -0
- data/README.md +408 -0
- data/Rakefile +17 -0
- data/examples/library.rb +200 -0
- data/lib/query/base.rb +55 -0
- data/lib/query/composer.rb +315 -0
- data/lib/query/composer/version.rb +11 -0
- data/lib/query/wrapper.rb +7 -0
- data/query-composer.gemspec +28 -0
- data/test/query/base_test.rb +62 -0
- data/test/query/composer_test.rb +102 -0
- data/test/query/wrapper_test.rb +10 -0
- data/test/test_helper.rb +46 -0
- metadata +133 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "query/composer/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.version = Query::Composer::Version::STRING
|
7
|
+
gem.name = "query-composer"
|
8
|
+
gem.authors = ["Jamis Buck"]
|
9
|
+
gem.email = ["jamis@jamisbuck.org"]
|
10
|
+
gem.homepage = "http://github.com/jamis/query-composer"
|
11
|
+
gem.summary = "Modularly construct complex SQL queries"
|
12
|
+
gem.description = "Build complex SQL queries by defining each subquery separately. Query::Composer will then compose those subqueries together, nesting as needed, to produce the final SQL query."
|
13
|
+
gem.license = 'MIT'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($\)
|
16
|
+
gem.test_files = gem.files.grep(%r{^test/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
|
19
|
+
##
|
20
|
+
# Development dependencies
|
21
|
+
#
|
22
|
+
gem.add_development_dependency "rake"
|
23
|
+
gem.add_development_dependency "minitest"
|
24
|
+
gem.add_development_dependency "activerecord", ">= 4.0"
|
25
|
+
gem.add_development_dependency "rubygems-tasks", "~> 0"
|
26
|
+
|
27
|
+
gem.add_dependency "arel", "~> 6.0"
|
28
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Query
|
4
|
+
class BaseTest < Minitest::Test
|
5
|
+
def test_should_interpret_symbol_argument_as_table_name
|
6
|
+
q = Query::Base.new(:people)
|
7
|
+
assert_equal Arel::Table.new(:people), q.primary_table
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_interpret_string_argument_as_table_name
|
11
|
+
q = Query::Base.new("people")
|
12
|
+
assert_equal Arel::Table.new(:people), q.primary_table
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_should_use_Arel_Table_argument_as_primary_table
|
16
|
+
q = Query::Base.new(Arel::Table.new(:people))
|
17
|
+
assert_equal Arel::Table.new(:people), q.primary_table
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_should_use_Arel_Nodes_TableAlias_argument_as_primary_table
|
21
|
+
arel = Person.all.arel.as('people')
|
22
|
+
q = Query::Base.new(arel)
|
23
|
+
assert_equal arel, q.primary_table
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_should_treat_Arel_Nodes_TableAlias_as_query_source
|
27
|
+
arel = Person.all.arel.as('folks')
|
28
|
+
q = Query::Base.new(arel).all
|
29
|
+
assert_equal "SELECT folks.* FROM (SELECT \"people\".* FROM \"people\") folks", q.to_sql
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_all_should_project_all_table_attributes
|
33
|
+
q = Query::Base.new(:people).all
|
34
|
+
assert_match /"people"\.\*/, q.to_sql
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_reproject_should_change_attributes_in_projection
|
38
|
+
q = Query::Base.new(:people).all
|
39
|
+
q.reproject(q.primary_table[:id], q.primary_table[:first_name])
|
40
|
+
refute_match /"people"\.\*/, q.to_sql
|
41
|
+
assert_match /"people"\."id", "people"\."first_name"/, q.to_sql
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_as_should_return_Arel_node_instance
|
45
|
+
q = Query::Base.new(:people).as('scooby')
|
46
|
+
assert_instance_of Arel::Nodes::TableAlias, q
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_to_sql_should_convert_query_to_a_string_of_SQL
|
50
|
+
q = Query::Base.new(:people).all
|
51
|
+
assert_equal "SELECT \"people\".* FROM \"people\"", q.to_sql
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_unrecognized_methods_should_be_delegated_to_arel
|
55
|
+
people = Arel::Table.new(:people)
|
56
|
+
companies = Arel::Table.new(:companies)
|
57
|
+
|
58
|
+
q = Query::Base.new(people).join(companies).on(people[:company_id].eq(companies[:id]))
|
59
|
+
assert_equal "SELECT FROM \"people\" INNER JOIN \"companies\" ON \"people\".\"company_id\" = \"companies\".\"id\"", q.to_sql
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Query
|
4
|
+
class ComposerTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@composer = Query::Composer.new
|
7
|
+
|
8
|
+
@composer.use(:companies_set) { Company.all }
|
9
|
+
@composer.use(:people_set) { Query::Wrapper.new(:people, Person.all.arel) }
|
10
|
+
|
11
|
+
@composer.use(:joined_set) do |people_set, companies_set|
|
12
|
+
Query::Base.new(people_set).
|
13
|
+
project(
|
14
|
+
people_set[:first_name].as("first_name"),
|
15
|
+
companies_set[:name].as("name")).
|
16
|
+
join(companies_set).
|
17
|
+
on(people_set[:company_id].eq(companies_set[:id])).
|
18
|
+
order(people_set[:first_name])
|
19
|
+
end
|
20
|
+
|
21
|
+
@composer.use(:dep_a) { |dep_b| Person.all }
|
22
|
+
@composer.use(:dep_b) { |dep_a| Company.all }
|
23
|
+
|
24
|
+
@composer.use(:unknown_dep) { |bogus| Person.all }
|
25
|
+
@composer.use(:invalid_query) { 15 }
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_composer_should_accept_ActiveRecord_scope
|
29
|
+
results = Company.find_by_sql(@composer.build(:companies_set))
|
30
|
+
expected = Company.all.to_a
|
31
|
+
assert_equal(expected, results)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_composer_should_accept_scope_quack_alikes
|
35
|
+
results = Person.find_by_sql(@composer.build(:people_set))
|
36
|
+
expected = Person.all.to_a
|
37
|
+
assert_equal(expected, results)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_composer_should_resolve_dependencies
|
41
|
+
results = Person.find_by_sql(@composer.build(:joined_set))
|
42
|
+
assert_equal(results.first.first_name, "Harry")
|
43
|
+
assert_equal(results.first.name, "Big Company")
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_composer_should_support_derived_tables
|
47
|
+
results = Person.find_by_sql(@composer.build(:joined_set, use_cte: false))
|
48
|
+
assert_equal(results.first.first_name, "Harry")
|
49
|
+
assert_equal(results.first.name, "Big Company")
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_composer_should_support_common_table_expressions
|
53
|
+
results = Person.find_by_sql(@composer.build(:joined_set, use_cte: true))
|
54
|
+
assert_equal(results.first.first_name, "Harry")
|
55
|
+
assert_equal(results.first.name, "Big Company")
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_composer_should_detect_circular_dependencies
|
59
|
+
assert_raises Query::Composer::CircularDependency do
|
60
|
+
@composer.build(:dep_a)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_composer_should_error_when_a_dependency_isnt_recognized
|
65
|
+
assert_raises Query::Composer::UnknownQuery do
|
66
|
+
@composer.build(:unknown_dep)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_composer_should_ensure_component_returns_querylike
|
71
|
+
assert_raises Query::Composer::InvalidQuery do
|
72
|
+
@composer.build(:invalid_query)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_composer_use_should_overwrite_existing_component
|
77
|
+
@composer.use(:companies_set) { Company.where("id = 1") }
|
78
|
+
results = Company.find_by_sql(@composer.build(:companies_set))
|
79
|
+
assert_equal 1, results.length
|
80
|
+
assert_equal 1, results.first.id
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_composer_alias_should_copy_named_component
|
84
|
+
@composer.alias(:companies_again, :companies_set)
|
85
|
+
@composer.use(:companies_set) { Company.where("id = 1") }
|
86
|
+
|
87
|
+
results = Company.find_by_sql(@composer.build(:companies_set))
|
88
|
+
assert_equal 1, results.length
|
89
|
+
|
90
|
+
results = Company.find_by_sql(@composer.build(:companies_again))
|
91
|
+
assert_equal Company.all, results
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_composer_delete_should_remove_named_component
|
95
|
+
@composer.delete(:companies_set)
|
96
|
+
|
97
|
+
assert_raises Query::Composer::UnknownQuery do
|
98
|
+
@composer.build(:companies_set)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Query
|
4
|
+
class WrapperTest < Minitest::Test
|
5
|
+
def test_should_allow_arbitrary_query_to_be_used_as_a_query_object
|
6
|
+
wrapper = Query::Wrapper.new(:people, Person.where("id=1"))
|
7
|
+
assert_equal "SELECT \"people\".* FROM \"people\" WHERE (id=1)", wrapper.to_sql
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
require 'query/composer'
|
5
|
+
require 'query/base'
|
6
|
+
require 'query/wrapper'
|
7
|
+
|
8
|
+
ActiveRecord::Base.logger = Logger.new(File.open("test.log", "w"))
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(
|
11
|
+
adapter: "sqlite3",
|
12
|
+
database: ":memory:"
|
13
|
+
)
|
14
|
+
|
15
|
+
ActiveRecord::Schema.verbose = false
|
16
|
+
ActiveRecord::Schema.define do
|
17
|
+
create_table :people do |t|
|
18
|
+
t.string :first_name
|
19
|
+
t.integer :company_id
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table :companies do |t|
|
23
|
+
t.string :name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
28
|
+
INSERT INTO people (id, first_name, company_id)
|
29
|
+
VALUES (1, 'Harry', 1),
|
30
|
+
(2, 'Margaret', 1),
|
31
|
+
(3, 'Jesse', 2);
|
32
|
+
SQL
|
33
|
+
|
34
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
35
|
+
INSERT INTO companies (id, name)
|
36
|
+
VALUES (1, 'Big Company'),
|
37
|
+
(2, 'Little Company');
|
38
|
+
SQL
|
39
|
+
|
40
|
+
class Person < ActiveRecord::Base
|
41
|
+
belongs_to :company
|
42
|
+
end
|
43
|
+
|
44
|
+
class Company < ActiveRecord::Base
|
45
|
+
has_many :people
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: query-composer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jamis Buck
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubygems-tasks
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: arel
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '6.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '6.0'
|
83
|
+
description: Build complex SQL queries by defining each subquery separately. Query::Composer
|
84
|
+
will then compose those subqueries together, nesting as needed, to produce the final
|
85
|
+
SQL query.
|
86
|
+
email:
|
87
|
+
- jamis@jamisbuck.org
|
88
|
+
executables: []
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- LICENSE
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- examples/library.rb
|
96
|
+
- lib/query/base.rb
|
97
|
+
- lib/query/composer.rb
|
98
|
+
- lib/query/composer/version.rb
|
99
|
+
- lib/query/wrapper.rb
|
100
|
+
- query-composer.gemspec
|
101
|
+
- test/query/base_test.rb
|
102
|
+
- test/query/composer_test.rb
|
103
|
+
- test/query/wrapper_test.rb
|
104
|
+
- test/test_helper.rb
|
105
|
+
homepage: http://github.com/jamis/query-composer
|
106
|
+
licenses:
|
107
|
+
- MIT
|
108
|
+
metadata: {}
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 2.5.1
|
126
|
+
signing_key:
|
127
|
+
specification_version: 4
|
128
|
+
summary: Modularly construct complex SQL queries
|
129
|
+
test_files:
|
130
|
+
- test/query/base_test.rb
|
131
|
+
- test/query/composer_test.rb
|
132
|
+
- test/query/wrapper_test.rb
|
133
|
+
- test/test_helper.rb
|