torque-postgresql 1.1.4 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/lib/torque/postgresql.rb +0 -2
  3. data/lib/torque/postgresql/adapter.rb +0 -1
  4. data/lib/torque/postgresql/adapter/database_statements.rb +4 -15
  5. data/lib/torque/postgresql/adapter/schema_creation.rb +13 -23
  6. data/lib/torque/postgresql/adapter/schema_definitions.rb +7 -21
  7. data/lib/torque/postgresql/adapter/schema_dumper.rb +74 -11
  8. data/lib/torque/postgresql/adapter/schema_statements.rb +2 -12
  9. data/lib/torque/postgresql/associations.rb +0 -3
  10. data/lib/torque/postgresql/associations/association_scope.rb +18 -60
  11. data/lib/torque/postgresql/associations/belongs_to_many_association.rb +2 -1
  12. data/lib/torque/postgresql/associations/preloader.rb +0 -24
  13. data/lib/torque/postgresql/associations/preloader/association.rb +13 -9
  14. data/lib/torque/postgresql/auxiliary_statement.rb +1 -13
  15. data/lib/torque/postgresql/coder.rb +1 -2
  16. data/lib/torque/postgresql/config.rb +0 -4
  17. data/lib/torque/postgresql/inheritance.rb +13 -17
  18. data/lib/torque/postgresql/reflection/abstract_reflection.rb +19 -25
  19. data/lib/torque/postgresql/relation.rb +11 -16
  20. data/lib/torque/postgresql/relation/auxiliary_statement.rb +2 -8
  21. data/lib/torque/postgresql/relation/distinct_on.rb +1 -1
  22. data/lib/torque/postgresql/version.rb +1 -1
  23. data/spec/en.yml +19 -0
  24. data/spec/factories/authors.rb +6 -0
  25. data/spec/factories/comments.rb +13 -0
  26. data/spec/factories/posts.rb +6 -0
  27. data/spec/factories/tags.rb +5 -0
  28. data/spec/factories/texts.rb +5 -0
  29. data/spec/factories/users.rb +6 -0
  30. data/spec/factories/videos.rb +5 -0
  31. data/spec/mocks/cache_query.rb +16 -0
  32. data/spec/mocks/create_table.rb +35 -0
  33. data/spec/models/activity.rb +3 -0
  34. data/spec/models/activity_book.rb +4 -0
  35. data/spec/models/activity_post.rb +7 -0
  36. data/spec/models/activity_post/sample.rb +4 -0
  37. data/spec/models/author.rb +4 -0
  38. data/spec/models/author_journalist.rb +4 -0
  39. data/spec/models/comment.rb +3 -0
  40. data/spec/models/course.rb +2 -0
  41. data/spec/models/geometry.rb +2 -0
  42. data/spec/models/guest_comment.rb +4 -0
  43. data/spec/models/post.rb +6 -0
  44. data/spec/models/tag.rb +2 -0
  45. data/spec/models/text.rb +2 -0
  46. data/spec/models/time_keeper.rb +2 -0
  47. data/spec/models/user.rb +8 -0
  48. data/spec/models/video.rb +2 -0
  49. data/spec/schema.rb +141 -0
  50. data/spec/spec_helper.rb +59 -0
  51. data/spec/tests/arel_spec.rb +72 -0
  52. data/spec/tests/auxiliary_statement_spec.rb +593 -0
  53. data/spec/tests/belongs_to_many_spec.rb +240 -0
  54. data/spec/tests/coder_spec.rb +367 -0
  55. data/spec/tests/collector_spec.rb +59 -0
  56. data/spec/tests/distinct_on_spec.rb +65 -0
  57. data/spec/tests/enum_set_spec.rb +306 -0
  58. data/spec/tests/enum_spec.rb +628 -0
  59. data/spec/tests/geometric_builder_spec.rb +221 -0
  60. data/spec/tests/has_many_spec.rb +390 -0
  61. data/spec/tests/interval_spec.rb +167 -0
  62. data/spec/tests/lazy_spec.rb +24 -0
  63. data/spec/tests/period_spec.rb +954 -0
  64. data/spec/tests/quoting_spec.rb +24 -0
  65. data/spec/tests/range_spec.rb +36 -0
  66. data/spec/tests/relation_spec.rb +57 -0
  67. data/spec/tests/table_inheritance_spec.rb +416 -0
  68. metadata +103 -16
  69. data/lib/torque/postgresql/associations/join_dependency/join_association.rb +0 -15
  70. data/lib/torque/postgresql/schema_dumper.rb +0 -88
@@ -0,0 +1,7 @@
1
+ require_relative 'activity'
2
+
3
+ class ActivityPost < Activity
4
+ belongs_to :post
5
+ end
6
+
7
+ require_relative 'activity_post/sample'
@@ -0,0 +1,4 @@
1
+ class ActivityPost < Activity
2
+ class Sample < ActivityPost
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ class Author < ActiveRecord::Base
2
+ has_many :activities, -> { cast_records }
3
+ has_many :posts
4
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'author'
2
+
3
+ class AuthorJournalist < Author
4
+ end
@@ -0,0 +1,3 @@
1
+ class Comment < ActiveRecord::Base
2
+ belongs_to :user
3
+ end
@@ -0,0 +1,2 @@
1
+ class Course < ActiveRecord::Base
2
+ end
@@ -0,0 +1,2 @@
1
+ class Geometry < ActiveRecord::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'comment'
2
+
3
+ class GuestComment < Comment
4
+ end
@@ -0,0 +1,6 @@
1
+ class Post < ActiveRecord::Base
2
+ belongs_to :author
3
+ belongs_to :activity
4
+
5
+ scope :test_scope, -> { where('1=1') }
6
+ end
@@ -0,0 +1,2 @@
1
+ class Tag < ActiveRecord::Base
2
+ end
@@ -0,0 +1,2 @@
1
+ class Text < ActiveRecord::Base
2
+ end
@@ -0,0 +1,2 @@
1
+ class TimeKeeper < ActiveRecord::Base
2
+ end
@@ -0,0 +1,8 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :comments
3
+
4
+ auxiliary_statement :last_comment do |cte|
5
+ cte.query Comment.distinct_on(:user_id).order(:user_id, id: :desc)
6
+ cte.attributes id: :comment_id, content: :comment_content
7
+ end
8
+ end
@@ -0,0 +1,2 @@
1
+ class Video < ActiveRecord::Base
2
+ end
@@ -0,0 +1,141 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your
6
+ # database schema. If you need to create the application database on another
7
+ # system, you should be using db:schema:load, not running all the migrations
8
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ begin
14
+ version = 61
15
+
16
+ raise SystemExit if ActiveRecord::Migrator.current_version == version
17
+ ActiveRecord::Schema.define(version: version) do
18
+ self.verbose = false
19
+
20
+ # These are extensions that must be enabled in order to support this database
21
+ enable_extension "plpgsql"
22
+
23
+ # These are user-defined types used on this database
24
+ create_enum "content_status", ["created", "draft", "published", "archived"], force: :cascade
25
+ create_enum "specialties", ["books", "movies", "plays"], force: :cascade
26
+ create_enum "roles", ["visitor", "assistant", "manager", "admin"], force: :cascade
27
+ create_enum "conflicts", ["valid", "invalid", "untrusted"], force: :cascade
28
+ create_enum "types", ["A", "B", "C", "D"], force: :cascade
29
+
30
+ create_table "geometries", force: :cascade do |t|
31
+ t.point "point"
32
+ t.line "line"
33
+ t.lseg "lseg"
34
+ t.box "box"
35
+ t.path "closed_path"
36
+ t.path "open_path"
37
+ t.polygon "polygon"
38
+ t.circle "circle"
39
+ end
40
+
41
+ create_table "time_keepers", force: :cascade do |t|
42
+ t.daterange "available"
43
+ t.tsrange "period"
44
+ t.tstzrange "tzperiod"
45
+ t.interval "th"
46
+ end
47
+
48
+ create_table "tags", force: :cascade do |t|
49
+ t.string "name"
50
+ end
51
+
52
+ create_table "videos", force: :cascade do |t|
53
+ t.bigint "tag_ids", array: true
54
+ t.string "title"
55
+ t.string "url"
56
+ t.enum "type", subtype: :types
57
+ t.enum "conflicts", subtype: :conflicts, array: true
58
+ t.datetime "created_at", null: false
59
+ t.datetime "updated_at", null: false
60
+ end
61
+
62
+ create_table "authors", force: :cascade do |t|
63
+ t.string "name"
64
+ t.string "type"
65
+ t.enum "specialty", subtype: :specialties
66
+ end
67
+
68
+ create_table "texts", force: :cascade do |t|
69
+ t.integer "user_id"
70
+ t.string "content"
71
+ t.enum "conflict", subtype: :conflicts
72
+ end
73
+
74
+ create_table "comments", force: :cascade do |t|
75
+ t.integer "user_id", null: false
76
+ t.integer "comment_id"
77
+ t.text "content", null: false
78
+ t.string "kind"
79
+ t.index ["user_id"], name: "index_comments_on_user_id", using: :btree
80
+ t.index ["comment_id"], name: "index_comments_on_comment_id", using: :btree
81
+ end
82
+
83
+ create_table "courses", force: :cascade do |t|
84
+ t.string "title", null: false
85
+ t.interval "duration"
86
+ t.enum "types", subtype: :types, array: true, default: [:A, :B]
87
+ t.datetime "created_at", null: false
88
+ t.datetime "updated_at", null: false
89
+ end
90
+
91
+ create_table "images", force: :cascade, id: false do |t|
92
+ t.string "file"
93
+ end
94
+
95
+ create_table "posts", force: :cascade do |t|
96
+ t.integer "author_id"
97
+ t.integer "activity_id"
98
+ t.string "title"
99
+ t.text "content"
100
+ t.enum "status", subtype: :content_status
101
+ t.index ["author_id"], name: "index_posts_on_author_id", using: :btree
102
+ end
103
+
104
+ create_table "users", force: :cascade do |t|
105
+ t.string "name", null: false
106
+ t.enum "role", subtype: :roles, default: :visitor
107
+ t.datetime "created_at", null: false
108
+ t.datetime "updated_at", null: false
109
+ end
110
+
111
+ create_table "activities", force: :cascade do |t|
112
+ t.integer "author_id"
113
+ t.string "title"
114
+ t.boolean "active"
115
+ t.enum "kind", subtype: :types
116
+ t.datetime "created_at", null: false
117
+ t.datetime "updated_at", null: false
118
+ end
119
+
120
+ create_table "activity_books", force: :cascade, inherits: :activities do |t|
121
+ t.text "description"
122
+ t.string "url"
123
+ t.boolean "activated"
124
+ end
125
+
126
+ create_table "activity_posts", force: :cascade, inherits: [:activities, :images] do |t|
127
+ t.integer "post_id"
128
+ t.string "url"
129
+ t.integer "activated"
130
+ end
131
+
132
+ create_table "activity_post_samples", force: :cascade, inherits: :activity_posts
133
+
134
+ # create_table "activity_blanks", force: :cascade, inherits: :activities
135
+
136
+ # create_table "activity_images", force: :cascade, inherits: [:activities, :images]
137
+
138
+ add_foreign_key "posts", "authors"
139
+ end
140
+ rescue SystemExit
141
+ end
@@ -0,0 +1,59 @@
1
+ require 'torque-postgresql'
2
+ require 'database_cleaner'
3
+ require 'factory_bot'
4
+ require 'dotenv'
5
+ require 'faker'
6
+ require 'rspec'
7
+ require 'byebug'
8
+
9
+ Dotenv.load
10
+
11
+ ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
12
+ cache = ActiveRecord::Base.connection.schema_cache
13
+
14
+ cleaner = ->() do
15
+ cache.instance_variable_set(:@inheritance_loaded, false)
16
+ cache.instance_variable_set(:@inheritance_dependencies, {})
17
+ cache.instance_variable_set(:@inheritance_associations, {})
18
+ end
19
+
20
+ load File.join('schema.rb')
21
+ Dir.glob(File.join('spec', '{models,factories,mocks}', '*.rb')) do |file|
22
+ require file[5..-4]
23
+ end
24
+
25
+ cleaner.call
26
+ I18n.load_path << Pathname.pwd.join('spec', 'en.yml')
27
+ RSpec.configure do |config|
28
+ config.extend Mocks::CreateTable
29
+ config.include Mocks::CacheQuery
30
+
31
+ config.formatter = :documentation
32
+ config.color = true
33
+ config.tty = true
34
+
35
+ # Handles acton before rspec initialize
36
+ config.before(:suite) do
37
+ DatabaseCleaner.clean_with(:truncation)
38
+ end
39
+
40
+ config.before(:each) do
41
+ DatabaseCleaner.strategy = :transaction
42
+ end
43
+
44
+ config.before(:each, js: true) do
45
+ DatabaseCleaner.strategy = :truncation
46
+ end
47
+
48
+ config.before(:each) do
49
+ DatabaseCleaner.start
50
+ end
51
+
52
+ config.after(:each) do
53
+ DatabaseCleaner.clean
54
+ end
55
+
56
+ config.before(:each) do
57
+ cleaner.call
58
+ end
59
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Arel' do
4
+ context 'on inflix operation' do
5
+ let(:list) { Torque::PostgreSQL::Arel::INFLIX_OPERATION }
6
+ let(:collector) { ::Arel::Collectors::SQLString }
7
+ let(:attribute) { ::Arel::Table.new('a')['sample'] }
8
+ let(:conn) { ActiveRecord::Base.connection }
9
+ let(:visitor) { ::Arel::Visitors::PostgreSQL.new(conn) }
10
+
11
+ [
12
+ [:overlaps, [1, 2], "ARRAY[1, 2]"],
13
+ [:contains, [3, 4], "ARRAY[3, 4]"],
14
+ [:contained_by, [5, 6], "ARRAY[5, 6]"],
15
+ [:has_key, ::Arel.sql("'a'"), "'a'"],
16
+ [:has_all_keys, ['b', 'c'], "ARRAY['b', 'c']"],
17
+ [:has_any_keys, ['d', 'e'], "ARRAY['d', 'e']"],
18
+
19
+ [:strictly_left, ::Arel.sql('numrange(1, 2)'), 'numrange(1, 2)'],
20
+ [:strictly_right, ::Arel.sql('numrange(3, 4)'), 'numrange(3, 4)'],
21
+ [:doesnt_right_extend, ::Arel.sql('numrange(5, 6)'), 'numrange(5, 6)'],
22
+ [:doesnt_left_extend, ::Arel.sql('numrange(7, 8)'), 'numrange(7, 8)'],
23
+ [:adjacent_to, ::Arel.sql('numrange(9, 0)'), 'numrange(9, 0)'],
24
+ ].each do |(operator, value, quoted_value)|
25
+ klass_name = operator.to_s.camelize
26
+
27
+ context "##{operator}" do
28
+ let(:instance) { attribute.public_send(operator, value) }
29
+
30
+ context 'for attribute' do
31
+ let(:klass) { ::Arel::Nodes.const_get(klass_name) }
32
+
33
+ it "returns a new #{klass_name}" do
34
+ expect(instance).to be_a(klass)
35
+ end
36
+ end
37
+
38
+ context 'for visitor' do
39
+ let(:result) { visitor.accept(instance, collector.new).value }
40
+
41
+ it 'returns a formatted operation' do
42
+ expect(result).to be_eql("\"a\".\"sample\" #{list[klass_name]} #{quoted_value}")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ context 'on cast' do
50
+ it 'provides an array method' do
51
+ sample1 = ::Arel.array(1, 2, 3, 4)
52
+ sample2 = ::Arel.array([1, 2, 3, 4])
53
+ sample3 = ::Arel.array(1, 2, 3, 4, cast: 'bigint')
54
+ sample4 = ::Arel.array([1, 2, 3, 4], [5, 6, 7, 8], cast: 'integer')
55
+
56
+ expect(sample1.to_sql).to be_eql('ARRAY[1, 2, 3, 4]')
57
+ expect(sample2.to_sql).to be_eql('ARRAY[1, 2, 3, 4]')
58
+ expect(sample3.to_sql).to be_eql('ARRAY[1, 2, 3, 4]::bigint[]')
59
+ expect(sample4.to_sql).to be_eql('ARRAY[ARRAY[1, 2, 3, 4], ARRAY[5, 6, 7, 8]]::integer[]')
60
+ end
61
+
62
+ it 'provides a cast method' do
63
+ attribute = ::Arel::Table.new('a')['sample']
64
+ quoted = ::Arel::Nodes::build_quoted([1])
65
+ casted = ::Arel::Nodes::build_quoted(1, attribute)
66
+
67
+ expect(attribute.cast('text').to_sql).to be_eql('"a"."sample"::text')
68
+ expect(quoted.cast('bigint', true).to_sql).to be_eql('ARRAY[1]::bigint[]')
69
+ expect(casted.cast('string').to_sql).to be_eql("1::string")
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,593 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'AuxiliaryStatement' do
4
+ before :each do
5
+ User.auxiliary_statements_list = {}
6
+ end
7
+
8
+ context 'on relation' do
9
+ let(:klass) { User }
10
+ let(:true_value) { 'TRUE' }
11
+ subject { klass.unscoped }
12
+
13
+ it 'has its method' do
14
+ expect(subject).to respond_to(:with)
15
+ end
16
+
17
+ it 'can perform simple queries' do
18
+ klass.send(:auxiliary_statement, :comments) do |cte|
19
+ cte.query Comment.all
20
+ cte.attributes content: :comment_content
21
+ end
22
+
23
+ result = 'WITH "comments" AS'
24
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content FROM "comments")'
25
+ result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
26
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
27
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
28
+ end
29
+
30
+ it 'can perform more complex queries' do
31
+ klass.send(:auxiliary_statement, :comments) do |cte|
32
+ cte.query Comment.distinct_on(:user_id).order(:user_id, id: :desc)
33
+ cte.attributes content: :last_comment
34
+ end
35
+
36
+ result = 'WITH "comments" AS (SELECT DISTINCT ON ( "comments"."user_id" )'
37
+ result << ' "comments"."user_id", "comments"."content" AS last_comment'
38
+ result << ' FROM "comments" ORDER BY "comments"."user_id" ASC,'
39
+ result << ' "comments"."id" DESC) SELECT "users".*,'
40
+ result << ' "comments"."last_comment" FROM "users" INNER JOIN "comments"'
41
+ result << ' ON "comments"."user_id" = "users"."id"'
42
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
43
+ end
44
+
45
+ it 'accepts extra select columns' do
46
+ klass.send(:auxiliary_statement, :comments) do |cte|
47
+ cte.query Comment.all
48
+ cte.attributes content: :comment_content
49
+ end
50
+
51
+ result = 'WITH "comments" AS'
52
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content, "comments"."slug" AS comment_slug FROM "comments")'
53
+ result << ' SELECT "users".*, "comments"."comment_content", "comments"."comment_slug" FROM "users"'
54
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
55
+ expect(subject.with(:comments, select: {slug: :comment_slug}).arel.to_sql).to eql(result)
56
+ end
57
+
58
+ it 'accepts extra join columns' do
59
+ klass.send(:auxiliary_statement, :comments) do |cte|
60
+ cte.query Comment.all
61
+ cte.attributes content: :comment_content
62
+ end
63
+
64
+ result = 'WITH "comments" AS'
65
+ result << ' (SELECT "comments"."user_id", "comments"."active", "comments"."content" AS comment_content FROM "comments")'
66
+ result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
67
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id" AND "comments"."active" = "users"."active"'
68
+ expect(subject.with(:comments, join: {active: :active}).arel.to_sql).to eql(result)
69
+ end
70
+
71
+ it 'accepts extra conditions' do
72
+ klass.send(:auxiliary_statement, :comments) do |cte|
73
+ cte.query Comment.all
74
+ cte.attributes content: :comment_content
75
+ end
76
+
77
+ result = 'WITH "comments" AS'
78
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content'
79
+ result << ' FROM "comments" WHERE "comments"."active" = $1)'
80
+ result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
81
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
82
+ expect(subject.with(:comments, where: {active: true}).arel.to_sql).to eql(result)
83
+ end
84
+
85
+ it 'accepts scopes from both sides' do
86
+ klass.send(:auxiliary_statement, :comments) do |cte|
87
+ cte.query Comment.where(id: 1).all
88
+ cte.attributes content: :comment_content
89
+ end
90
+
91
+ query = subject.where(id: 2).with(:comments)
92
+
93
+ result = 'WITH "comments" AS'
94
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content FROM "comments"'
95
+ result << ' WHERE "comments"."id" = $1)'
96
+ result << ' SELECT "users".*, "comments"."comment_content" FROM "users"'
97
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
98
+ result << ' WHERE "users"."id" = $2'
99
+
100
+ expect(query.arel.to_sql).to eql(result)
101
+ expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1, 2])
102
+ end
103
+
104
+ it 'accepts string as attributes' do
105
+ klass.send(:auxiliary_statement, :comments) do |cte|
106
+ cte.query Comment.all
107
+ cte.attributes sql('MAX(id)') => :comment_id
108
+ end
109
+
110
+ result = 'WITH "comments" AS'
111
+ result << ' (SELECT "comments"."user_id", MAX(id) AS comment_id FROM "comments")'
112
+ result << ' SELECT "users".*, "comments"."comment_id" FROM "users"'
113
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
114
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
115
+ end
116
+
117
+ it 'accepts complex string as attributes' do
118
+ klass.send(:auxiliary_statement, :comments) do |cte|
119
+ cte.query Comment.all
120
+ cte.attributes sql('ROW_NUMBER() OVER (PARTITION BY ORDER BY "comments"."id")') => :comment_id
121
+ end
122
+
123
+ result = 'WITH "comments" AS'
124
+ result << ' (SELECT "comments"."user_id", ROW_NUMBER() OVER (PARTITION BY ORDER BY "comments"."id") AS comment_id FROM "comments")'
125
+ result << ' SELECT "users".*, "comments"."comment_id" FROM "users"'
126
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
127
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
128
+ end
129
+
130
+ it 'accepts arel attribute as attributes' do
131
+ klass.send(:auxiliary_statement, :comments) do |cte|
132
+ cte.query Comment.all
133
+ cte.attributes col(:id).minimum => :comment_id
134
+ end
135
+
136
+ result = 'WITH "comments" AS'
137
+ result << ' (SELECT "comments"."user_id", MIN("comments"."id") AS comment_id FROM "comments")'
138
+ result << ' SELECT "users".*, "comments"."comment_id" FROM "users"'
139
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
140
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
141
+ end
142
+
143
+ it 'accepts custom join properties' do
144
+ klass.send(:auxiliary_statement, :comments) do |cte|
145
+ cte.query Comment.all
146
+ cte.attributes content: :comment_content
147
+ cte.join name: :id, 'a.col' => :col
148
+ end
149
+
150
+ result = 'WITH "comments" AS (SELECT "comments"."id", "comments"."col",'
151
+ result << ' "comments"."content" AS comment_content FROM "comments") SELECT "users".*,'
152
+ result << ' "comments"."comment_content" FROM "users" INNER JOIN "comments"'
153
+ result << ' ON "comments"."id" = "users"."name" AND "comments"."col" = "a"."col"'
154
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
155
+ end
156
+
157
+ it 'can perform other types of joins' do
158
+ klass.send(:auxiliary_statement, :comments) do |cte|
159
+ cte.query Comment.all
160
+ cte.attributes content: :comment_content
161
+ cte.join_type :left
162
+ end
163
+
164
+ result = 'WITH "comments" AS (SELECT "comments"."user_id",'
165
+ result << ' "comments"."content" AS comment_content FROM "comments") SELECT "users".*,'
166
+ result << ' "comments"."comment_content" FROM "users" LEFT OUTER JOIN "comments"'
167
+ result << ' ON "comments"."user_id" = "users"."id"'
168
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
169
+ end
170
+
171
+ it 'can manually define the association' do
172
+ klass.has_many :sample_comment, class_name: 'Comment', foreign_key: :a_user_id
173
+ klass.send(:auxiliary_statement, :comments) do |cte|
174
+ cte.query Comment.all
175
+ cte.through :sample_comment
176
+ cte.attributes content: :sample_content
177
+ end
178
+
179
+ result = 'WITH "comments" AS'
180
+ result << ' (SELECT "comments"."a_user_id", "comments"."content" AS sample_content FROM "comments")'
181
+ result << ' SELECT "users".*, "comments"."sample_content" FROM "users"'
182
+ result << ' INNER JOIN "comments" ON "comments"."a_user_id" = "users"."id"'
183
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
184
+ end
185
+
186
+ it 'accepts complex scopes from dependencies' do
187
+ klass.send(:auxiliary_statement, :comments1) do |cte|
188
+ cte.query Comment.where(id: 1).all
189
+ cte.attributes content: :comment_content1
190
+ end
191
+
192
+ klass.send(:auxiliary_statement, :comments2) do |cte|
193
+ cte.requires :comments1
194
+ cte.query Comment.where(id: 2).all
195
+ cte.attributes content: :comment_content2
196
+ end
197
+
198
+ query = subject.where(id: 3).with(:comments2)
199
+
200
+ result = 'WITH '
201
+ result << '"comments1" AS (SELECT "comments"."user_id", "comments"."content" AS comment_content1 FROM "comments" WHERE "comments"."id" = $1), '
202
+ result << '"comments2" AS (SELECT "comments"."user_id", "comments"."content" AS comment_content2 FROM "comments" WHERE "comments"."id" = $2)'
203
+ result << ' SELECT "users".*, "comments1"."comment_content1", "comments2"."comment_content2" FROM "users"'
204
+ result << ' INNER JOIN "comments1" ON "comments1"."user_id" = "users"."id"'
205
+ result << ' INNER JOIN "comments2" ON "comments2"."user_id" = "users"."id"'
206
+ result << ' WHERE "users"."id" = $3'
207
+
208
+ expect(query.arel.to_sql).to eql(result)
209
+ expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1, 2, 3])
210
+ end
211
+
212
+ context 'with dependency' do
213
+ before :each do
214
+ klass.send(:auxiliary_statement, :comments1) do |cte|
215
+ cte.query Comment.all
216
+ cte.attributes content: :comment_content1
217
+ end
218
+
219
+ klass.send(:auxiliary_statement, :comments2) do |cte|
220
+ cte.requires :comments1
221
+ cte.query Comment.all
222
+ cte.attributes content: :comment_content2
223
+ end
224
+ end
225
+
226
+ it 'can requires another statement as dependency' do
227
+ result = 'WITH '
228
+ result << '"comments1" AS (SELECT "comments"."user_id", "comments"."content" AS comment_content1 FROM "comments"), '
229
+ result << '"comments2" AS (SELECT "comments"."user_id", "comments"."content" AS comment_content2 FROM "comments")'
230
+ result << ' SELECT "users".*, "comments1"."comment_content1", "comments2"."comment_content2" FROM "users"'
231
+ result << ' INNER JOIN "comments1" ON "comments1"."user_id" = "users"."id"'
232
+ result << ' INNER JOIN "comments2" ON "comments2"."user_id" = "users"."id"'
233
+ expect(subject.with(:comments2).arel.to_sql).to eql(result)
234
+ end
235
+
236
+ it 'can uses already already set dependent' do
237
+ result = 'WITH '
238
+ result << '"comments1" AS (SELECT "comments"."user_id", "comments"."content" AS comment_content1 FROM "comments"), '
239
+ result << '"comments2" AS (SELECT "comments"."user_id", "comments"."content" AS comment_content2 FROM "comments")'
240
+ result << ' SELECT "users".*, "comments1"."comment_content1", "comments2"."comment_content2" FROM "users"'
241
+ result << ' INNER JOIN "comments1" ON "comments1"."user_id" = "users"."id"'
242
+ result << ' INNER JOIN "comments2" ON "comments2"."user_id" = "users"."id"'
243
+ expect(subject.with(:comments1, :comments2).arel.to_sql).to eql(result)
244
+ end
245
+
246
+ it 'raises an error if the dependent does not exist' do
247
+ klass.send(:auxiliary_statement, :comments2) do |cte|
248
+ cte.requires :comments3
249
+ cte.query Comment.all
250
+ cte.attributes content: :comment_content2
251
+ end
252
+ expect{ subject.with(:comments2).arel.to_sql }.to raise_error(ArgumentError)
253
+ end
254
+ end
255
+
256
+ context 'query as string' do
257
+ it 'performs correctly' do
258
+ klass.send(:auxiliary_statement, :comments) do |cte|
259
+ cte.query :comments, 'SELECT * FROM comments'
260
+ cte.attributes content: :comment
261
+ cte.join id: :user_id
262
+ end
263
+
264
+ result = 'WITH "comments" AS (SELECT * FROM comments)'
265
+ result << ' SELECT "users".*, "comments"."comment" FROM "users"'
266
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
267
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
268
+ end
269
+
270
+ it 'accepts arguments to format the query' do
271
+ klass.send(:auxiliary_statement, :comments) do |cte|
272
+ cte.query :comments, 'SELECT * FROM comments WHERE active = %{active}'
273
+ cte.attributes content: :comment
274
+ cte.join id: :user_id
275
+ end
276
+
277
+ result = "WITH \"comments\" AS (SELECT * FROM comments WHERE active = #{true_value})"
278
+ result << ' SELECT "users".*, "comments"."comment" FROM "users"'
279
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
280
+ expect(subject.with(:comments, args: {active: true}).arel.to_sql).to eql(result)
281
+ end
282
+
283
+ it 'raises an error when join columns are not given' do
284
+ klass.send(:auxiliary_statement, :comments) do |cte|
285
+ cte.query :comments, 'SELECT * FROM comments'
286
+ cte.attributes content: :comment
287
+ end
288
+
289
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /join columns/)
290
+ end
291
+
292
+ it 'raises an error when not given the table name as first argument' do
293
+ klass.send(:auxiliary_statement, :comments) do |cte|
294
+ cte.query 'SELECT * FROM comments'
295
+ cte.attributes content: :comment
296
+ cte.join id: :user_id
297
+ end
298
+
299
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /table name/)
300
+ end
301
+ end
302
+
303
+ context 'query as proc' do
304
+ it 'performs correctly for result as relation' do
305
+ klass.send(:auxiliary_statement, :comments) do |cte|
306
+ cte.query :comments, -> { Comment.all }
307
+ cte.attributes content: :comment
308
+ cte.join id: :user_id
309
+ end
310
+
311
+ result = 'WITH "comments" AS'
312
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment FROM "comments")'
313
+ result << ' SELECT "users".*, "comments"."comment" FROM "users"'
314
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
315
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
316
+ end
317
+
318
+ it 'performs correctly for anything that has a call method' do
319
+ obj = Struct.new(:call, :arity).new('SELECT * FROM comments', 0)
320
+ klass.send(:auxiliary_statement, :comments) do |cte|
321
+ cte.query :comments, obj
322
+ cte.attributes content: :comment
323
+ cte.join id: :user_id
324
+ end
325
+
326
+ result = 'WITH "comments" AS (SELECT * FROM comments)'
327
+ result << ' SELECT "users".*, "comments"."comment" FROM "users"'
328
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
329
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
330
+ end
331
+
332
+ it 'performs correctly for result as string' do
333
+ klass.send(:auxiliary_statement, :comments) do |cte|
334
+ cte.query :comments, -> { 'SELECT * FROM comments' }
335
+ cte.attributes content: :comment
336
+ cte.join id: :user_id
337
+ end
338
+
339
+ result = 'WITH "comments" AS (SELECT * FROM comments)'
340
+ result << ' SELECT "users".*, "comments"."comment" FROM "users"'
341
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
342
+ expect(subject.with(:comments).arel.to_sql).to eql(result)
343
+ end
344
+
345
+ it 'performs correctly when the proc requires arguments' do
346
+ klass.send(:auxiliary_statement, :comments) do |cte|
347
+ cte.query :comments, -> (args) { Comment.where(id: args.id) }
348
+ cte.attributes content: :comment
349
+ cte.join id: :user_id
350
+ end
351
+
352
+ query = subject.with(:comments, args: {id: 1})
353
+
354
+ result = 'WITH "comments" AS'
355
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment'
356
+ result << ' FROM "comments" WHERE "comments"."id" = $1)'
357
+ result << ' SELECT "users".*, "comments"."comment" FROM "users"'
358
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
359
+
360
+ expect(query.arel.to_sql).to eql(result)
361
+ expect(query.send(:bound_attributes).map(&:value_before_type_cast)).to eql([1])
362
+ end
363
+
364
+ it 'raises an error when join columns are not given' do
365
+ klass.send(:auxiliary_statement, :comments) do |cte|
366
+ cte.query :comments, -> { Author.all }
367
+ cte.attributes content: :comment
368
+ end
369
+
370
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /join columns/)
371
+ end
372
+
373
+ it 'raises an error when not given the table name as first argument' do
374
+ klass.send(:auxiliary_statement, :comments) do |cte|
375
+ cte.query -> { Comment.all }
376
+ cte.attributes content: :comment
377
+ cte.join id: :user_id
378
+ end
379
+
380
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /table name/)
381
+ end
382
+
383
+ it 'raises an error when the result of the proc is an invalid type' do
384
+ klass.send(:auxiliary_statement, :comments) do |cte|
385
+ cte.query :comments, -> { false }
386
+ cte.attributes content: :comment
387
+ cte.join id: :user_id
388
+ end
389
+
390
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /query objects/)
391
+ end
392
+ end
393
+
394
+ context 'with inheritance' do
395
+ let(:base) { Activity }
396
+ let(:klass) { ActivityBook }
397
+
398
+ it 'accepts ancestors auxiliary statements' do
399
+ base.send(:auxiliary_statement, :authors) do |cte|
400
+ cte.query Author.all
401
+ cte.attributes name: :author_name
402
+ cte.join author_id: :id
403
+ end
404
+
405
+ result = 'WITH "authors" AS'
406
+ result << ' (SELECT "authors"."id", "authors"."name" AS author_name FROM "authors")'
407
+ result << ' SELECT "activity_books".*, "authors"."author_name" FROM "activity_books"'
408
+ result << ' INNER JOIN "authors" ON "authors"."id" = "activity_books"."author_id"'
409
+ expect(subject.with(:authors).arel.to_sql).to eql(result)
410
+ end
411
+
412
+ it 'can replace ancestors auxiliary statements' do
413
+ base.send(:auxiliary_statement, :authors) do |cte|
414
+ cte.query Author.all
415
+ cte.attributes name: :author_name
416
+ cte.join author_id: :id
417
+ end
418
+
419
+ klass.send(:auxiliary_statement, :authors) do |cte|
420
+ cte.query Author.all
421
+ cte.attributes type: :author_type
422
+ cte.join author_id: :id
423
+ end
424
+
425
+ result = 'WITH "authors" AS'
426
+ result << ' (SELECT "authors"."id", "authors"."type" AS author_type FROM "authors")'
427
+ result << ' SELECT "activity_books".*, "authors"."author_type" FROM "activity_books"'
428
+ result << ' INNER JOIN "authors" ON "authors"."id" = "activity_books"."author_id"'
429
+ expect(subject.with(:authors).arel.to_sql).to eql(result)
430
+ end
431
+
432
+ it 'raises an error when no class has the auxiliary statement' do
433
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError)
434
+ end
435
+ end
436
+
437
+ it 'works with count and does not add extra columns' do
438
+ klass.send(:auxiliary_statement, :comments) do |cte|
439
+ cte.query Comment.all
440
+ cte.attributes content: :comment_content
441
+ end
442
+
443
+ result = 'WITH "comments" AS'
444
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content FROM "comments")'
445
+ result << ' SELECT COUNT(*) FROM "users"'
446
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
447
+
448
+ query = get_last_executed_query{ subject.with(:comments).count }
449
+ expect(query).to eql(result)
450
+ end
451
+
452
+ it 'works with sum and does not add extra columns' do
453
+ klass.send(:auxiliary_statement, :comments) do |cte|
454
+ cte.query Comment.all
455
+ cte.attributes id: :value
456
+ end
457
+
458
+ result = 'WITH "comments" AS'
459
+ result << ' (SELECT "comments"."user_id", "comments"."id" AS value FROM "comments")'
460
+ result << ' SELECT SUM("comments"."value") FROM "users"'
461
+ result << ' INNER JOIN "comments" ON "comments"."user_id" = "users"."id"'
462
+
463
+ query = get_last_executed_query{ subject.with(:comments).sum(comments: :value) }
464
+ expect(query).to eql(result)
465
+ end
466
+
467
+ it 'raises an error when using an invalid type of object as query' do
468
+ klass.send(:auxiliary_statement, :comments) do |cte|
469
+ cte.query :string, String
470
+ end
471
+
472
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError, /object types/)
473
+ end
474
+
475
+ it 'raises an error when traying to use a statement that is not defined' do
476
+ expect{ subject.with(:does_not_exist).arel.to_sql }.to raise_error(ArgumentError)
477
+ end
478
+
479
+ it 'raises an error when using an invalid type of join' do
480
+ klass.send(:auxiliary_statement, :comments) do |cte|
481
+ cte.query Comment.all
482
+ cte.attributes content: :comment_content
483
+ cte.join_type :invalid
484
+ end
485
+
486
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(ArgumentError)
487
+ end
488
+ end
489
+
490
+ context 'on model' do
491
+ subject { User }
492
+
493
+ it 'has its configurator' do
494
+ expect(subject.protected_methods).to include(:cte)
495
+ expect(subject.protected_methods).to include(:auxiliary_statement)
496
+ end
497
+
498
+ it 'allows configurate new auxiliary statements' do
499
+ subject.send(:auxiliary_statement, :cte1)
500
+ expect(subject.auxiliary_statements_list).to include(:cte1)
501
+ expect(subject.const_defined?('Cte1_AuxiliaryStatement')).to be_truthy
502
+ end
503
+
504
+ it 'has its query method' do
505
+ expect(subject).to respond_to(:with)
506
+ end
507
+
508
+ it 'returns a relation when using the method' do
509
+ subject.send(:auxiliary_statement, :comments) do |cte|
510
+ cte.query Comment.all
511
+ cte.attributes content: :comment_content
512
+ end
513
+ expect(subject.with(:comments)).to be_a(ActiveRecord::Relation)
514
+ end
515
+ end
516
+
517
+ context 'on external' do
518
+ let(:klass) { Torque::PostgreSQL::AuxiliaryStatement }
519
+ subject { User }
520
+
521
+ it 'has the external method available' do
522
+ expect(klass).to respond_to(:create)
523
+ end
524
+
525
+ it 'accepts simple auxiliary statement definition' do
526
+ sample = klass.create(Comment.all)
527
+ query = subject.with(sample, select: {content: :comment_content}).arel.to_sql
528
+
529
+ result = 'WITH "comment" AS'
530
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content FROM "comments")'
531
+ result << ' SELECT "users".*, "comment"."comment_content" FROM "users"'
532
+ result << ' INNER JOIN "comment" ON "comment"."user_id" = "users"."id"'
533
+ expect(query).to eql(result)
534
+ end
535
+
536
+ it 'accepts a hash auxiliary statement definition' do
537
+ sample = klass.create(query: Comment.all, select: {content: :comment_content})
538
+ query = subject.with(sample).arel.to_sql
539
+
540
+ result = 'WITH "comment" AS'
541
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content FROM "comments")'
542
+ result << ' SELECT "users".*, "comment"."comment_content" FROM "users"'
543
+ result << ' INNER JOIN "comment" ON "comment"."user_id" = "users"."id"'
544
+ expect(query).to eql(result)
545
+ end
546
+
547
+ it 'accepts a block when creating the auxiliary statement' do
548
+ sample = klass.create(:all_comments) do |cte|
549
+ cte.query Comment.all
550
+ cte.select content: :comment_content
551
+ end
552
+
553
+ result = 'WITH "all_comments" AS'
554
+ result << ' (SELECT "comments"."user_id", "comments"."content" AS comment_content FROM "comments")'
555
+ result << ' SELECT "users".*, "all_comments"."comment_content" FROM "users"'
556
+ result << ' INNER JOIN "all_comments" ON "all_comments"."user_id" = "users"."id"'
557
+
558
+ query = subject.with(sample).arel.to_sql
559
+ expect(query).to eql(result)
560
+ end
561
+ end
562
+
563
+ context 'on settings' do
564
+ let(:base) { User }
565
+ let(:statement_klass) do
566
+ base.send(:auxiliary_statement, :statement)
567
+ base::Statement_AuxiliaryStatement
568
+ end
569
+
570
+ subject do
571
+ Torque::PostgreSQL::AuxiliaryStatement::Settings.new(base, statement_klass)
572
+ end
573
+
574
+ it 'has access to base' do
575
+ expect(subject.base).to eql(User)
576
+ expect(subject.base_table).to be_a(Arel::Table)
577
+ end
578
+
579
+ it 'has access to statement table' do
580
+ expect(subject.table_name).to eql('statement')
581
+ expect(subject.table).to be_a(Arel::Table)
582
+ end
583
+
584
+ it 'has access to the query arel table' do
585
+ subject.query Comment.all
586
+ expect(subject.query_table).to be_a(Arel::Table)
587
+ end
588
+
589
+ it 'raises an error when trying to access query table before defining the query' do
590
+ expect{ subject.with(:comments).arel.to_sql }.to raise_error(StandardError)
591
+ end
592
+ end
593
+ end