torque-postgresql 1.1.5 → 2.0.3

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  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 +16 -11
  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/autosave_association.rb +4 -4
  15. data/lib/torque/postgresql/auxiliary_statement.rb +1 -13
  16. data/lib/torque/postgresql/coder.rb +1 -2
  17. data/lib/torque/postgresql/config.rb +0 -4
  18. data/lib/torque/postgresql/inheritance.rb +13 -17
  19. data/lib/torque/postgresql/reflection/abstract_reflection.rb +19 -25
  20. data/lib/torque/postgresql/reflection/belongs_to_many_reflection.rb +16 -4
  21. data/lib/torque/postgresql/relation.rb +11 -16
  22. data/lib/torque/postgresql/relation/auxiliary_statement.rb +2 -8
  23. data/lib/torque/postgresql/relation/distinct_on.rb +1 -1
  24. data/lib/torque/postgresql/version.rb +1 -1
  25. data/spec/en.yml +19 -0
  26. data/spec/factories/authors.rb +6 -0
  27. data/spec/factories/comments.rb +13 -0
  28. data/spec/factories/posts.rb +6 -0
  29. data/spec/factories/tags.rb +5 -0
  30. data/spec/factories/texts.rb +5 -0
  31. data/spec/factories/users.rb +6 -0
  32. data/spec/factories/videos.rb +5 -0
  33. data/spec/mocks/cache_query.rb +16 -0
  34. data/spec/mocks/create_table.rb +35 -0
  35. data/spec/models/activity.rb +3 -0
  36. data/spec/models/activity_book.rb +4 -0
  37. data/spec/models/activity_post.rb +7 -0
  38. data/spec/models/activity_post/sample.rb +4 -0
  39. data/spec/models/author.rb +4 -0
  40. data/spec/models/author_journalist.rb +4 -0
  41. data/spec/models/comment.rb +3 -0
  42. data/spec/models/course.rb +2 -0
  43. data/spec/models/geometry.rb +2 -0
  44. data/spec/models/guest_comment.rb +4 -0
  45. data/spec/models/post.rb +6 -0
  46. data/spec/models/tag.rb +2 -0
  47. data/spec/models/text.rb +2 -0
  48. data/spec/models/time_keeper.rb +2 -0
  49. data/spec/models/user.rb +8 -0
  50. data/spec/models/video.rb +2 -0
  51. data/spec/schema.rb +141 -0
  52. data/spec/spec_helper.rb +59 -0
  53. data/spec/tests/arel_spec.rb +72 -0
  54. data/spec/tests/auxiliary_statement_spec.rb +593 -0
  55. data/spec/tests/belongs_to_many_spec.rb +246 -0
  56. data/spec/tests/coder_spec.rb +367 -0
  57. data/spec/tests/collector_spec.rb +59 -0
  58. data/spec/tests/distinct_on_spec.rb +65 -0
  59. data/spec/tests/enum_set_spec.rb +306 -0
  60. data/spec/tests/enum_spec.rb +628 -0
  61. data/spec/tests/geometric_builder_spec.rb +221 -0
  62. data/spec/tests/has_many_spec.rb +400 -0
  63. data/spec/tests/interval_spec.rb +167 -0
  64. data/spec/tests/lazy_spec.rb +24 -0
  65. data/spec/tests/period_spec.rb +954 -0
  66. data/spec/tests/quoting_spec.rb +24 -0
  67. data/spec/tests/range_spec.rb +36 -0
  68. data/spec/tests/relation_spec.rb +57 -0
  69. data/spec/tests/table_inheritance_spec.rb +416 -0
  70. metadata +102 -14
  71. data/lib/torque/postgresql/associations/join_dependency/join_association.rb +0 -15
  72. data/lib/torque/postgresql/schema_dumper.rb +0 -88
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :text do
3
+ content { Faker::Lorem.sentence }
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :user do
3
+ name { Faker::Name.name }
4
+ role { 'visitor' }
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :video do
3
+ title { Faker::Lorem.sentence }
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module Mocks
2
+ module CacheQuery
3
+ def get_last_executed_query(&block)
4
+ conn = ActiveRecord::Base.connection
5
+ conn.instance_variable_set(:@query_cache_enabled, true)
6
+
7
+ block.call
8
+ result = conn.query_cache.keys.first
9
+
10
+ conn.instance_variable_set(:@query_cache_enabled, false)
11
+ conn.instance_variable_get(:@query_cache).delete(result)
12
+
13
+ result
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ module Mocks
2
+ module CreateTable
3
+ def mock_create_table
4
+ path = ActiveRecord::Base.connection.method(:create_table).super_method.source_location[0]
5
+
6
+ before :all do
7
+ ActiveRecord::ConnectionAdapters::SchemaStatements.send(:define_method, :create_table) do |table_name, **options, &block|
8
+ td = create_table_definition(table_name, **options)
9
+
10
+ # Does things as the same as schema statements
11
+ if options[:id] != false && !options[:as]
12
+ pk = options.fetch(:primary_key) do
13
+ ActiveRecord::Base.get_primary_key table_name.to_s.singularize
14
+ end
15
+
16
+ if pk.is_a?(Array)
17
+ td.primary_keys pk
18
+ else
19
+ td.primary_key pk, options.fetch(:id, :primary_key), **options
20
+ end
21
+ end
22
+
23
+ block.call(td) if block.present?
24
+
25
+ # Now generate the SQL and return it
26
+ schema_creation.accept td
27
+ end
28
+ end
29
+
30
+ after :all do
31
+ load path
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ class Activity < ActiveRecord::Base
2
+ belongs_to :author
3
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'activity'
2
+
3
+ class ActivityBook < Activity
4
+ end
@@ -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