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.
@@ -0,0 +1,11 @@
1
+ module Query
2
+ class Composer
3
+ module Version
4
+ MAJOR = 1
5
+ MINOR = 0
6
+ PATCH = 0
7
+
8
+ STRING = [MAJOR, MINOR, PATCH].join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module Query
2
+ class Wrapper < Base
3
+ def _configure(source)
4
+ @arel = source
5
+ end
6
+ end
7
+ end
@@ -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
@@ -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