aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/adapters/postgresql/connection_test.rb
blob: f0daea42667e7c3ec14d3867411554ea1acf2613 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# frozen_string_literal: true

require "cases/helper"
require "support/connection_helper"

module ActiveRecord
  class PostgresqlConnectionTest < ActiveRecord::PostgreSQLTestCase
    include ConnectionHelper

    class NonExistentTable < ActiveRecord::Base
    end

    fixtures :comments

    def setup
      super
      @subscriber = SQLSubscriber.new
      @connection = ActiveRecord::Base.connection
      @connection.materialize_transactions
      @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
    end

    def teardown
      ActiveSupport::Notifications.unsubscribe(@subscription)
      super
    end

    def test_truncate
      count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
      assert_operator count, :>, 0
      ActiveRecord::Base.connection.truncate("comments")
      count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
      assert_equal 0, count
    end

    def test_encoding
      assert_queries(1) do
        assert_not_nil @connection.encoding
      end
    end

    def test_collation
      assert_queries(1) do
        assert_not_nil @connection.collation
      end
    end

    def test_ctype
      assert_queries(1) do
        assert_not_nil @connection.ctype
      end
    end

    def test_default_client_min_messages
      assert_equal "warning", @connection.client_min_messages
    end

    # Ensure, we can set connection params using the example of Generic
    # Query Optimizer (geqo). It is 'on' per default.
    def test_connection_options
      params = ActiveRecord::Base.connection_config.dup
      params[:options] = "-c geqo=off"
      NonExistentTable.establish_connection(params)

      # Verify the connection param has been applied.
      expect = NonExistentTable.connection.query("show geqo").first.first
      assert_equal "off", expect
    end

    def test_reset
      @connection.query("ROLLBACK")
      @connection.query("SET geqo TO off")

      # Verify the setting has been applied.
      expect = @connection.query("show geqo").first.first
      assert_equal "off", expect

      @connection.reset!

      # Verify the setting has been cleared.
      expect = @connection.query("show geqo").first.first
      assert_equal "on", expect
    end

    def test_reset_with_transaction
      @connection.query("ROLLBACK")
      @connection.query("SET geqo TO off")

      # Verify the setting has been applied.
      expect = @connection.query("show geqo").first.first
      assert_equal "off", expect

      @connection.query("BEGIN")
      @connection.reset!

      # Verify the setting has been cleared.
      expect = @connection.query("show geqo").first.first
      assert_equal "on", expect
    end

    def test_tables_logs_name
      @connection.tables
      assert_equal "SCHEMA", @subscriber.logged[0][1]
    end

    def test_indexes_logs_name
      @connection.indexes("items")
      assert_equal "SCHEMA", @subscriber.logged[0][1]
    end

    def test_table_exists_logs_name
      @connection.table_exists?("items")
      assert_equal "SCHEMA", @subscriber.logged[0][1]
    end

    def test_table_alias_length_logs_name
      @connection.instance_variable_set("@max_identifier_length", nil)
      @connection.table_alias_length
      assert_equal "SCHEMA", @subscriber.logged[0][1]
    end

    def test_current_database_logs_name
      @connection.current_database
      assert_equal "SCHEMA", @subscriber.logged[0][1]
    end

    def test_encoding_logs_name
      @connection.encoding
      assert_equal "SCHEMA", @subscriber.logged[0][1]
    end

    def test_schema_names_logs_name
      @connection.schema_names
      assert_equal "SCHEMA", @subscriber.logged[0][1]
    end

    if ActiveRecord::Base.connection.prepared_statements
      def test_statement_key_is_logged
        bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
        @connection.exec_query("SELECT $1::integer", "SQL", [bind], prepare: true)
        name = @subscriber.payloads.last[:statement_name]
        assert name
        res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
        plan = res.column_types["QUERY PLAN"].deserialize res.rows.first.first
        assert_operator plan.length, :>, 0
      end
    end

    # Must have PostgreSQL >= 9.2, or with_manual_interventions set to
    # true for this test to run.
    #
    # When prompted, restart the PostgreSQL server with the
    # "-m fast" option or kill the individual connection assuming
    # you know the incantation to do that.
    # To restart PostgreSQL 9.1 on macOS, installed via MacPorts, ...
    # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast"
    def test_reconnection_after_actual_disconnection_with_verify
      original_connection_pid = @connection.query("select pg_backend_pid()")

      # Sanity check.
      assert_predicate @connection, :active?

      if @connection.send(:postgresql_version) >= 90200
        secondary_connection = ActiveRecord::Base.connection_pool.checkout
        secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})")
        ActiveRecord::Base.connection_pool.checkin(secondary_connection)
      elsif ARTest.config["with_manual_interventions"]
        puts "Kill the connection now (e.g. by restarting the PostgreSQL " \
          'server with the "-m fast" option) and then press enter.'
        $stdin.gets
      else
        # We're not capable of terminating the backend ourselves, and
        # we're not allowed to seek assistance; bail out without
        # actually testing anything.
        return
      end

      @connection.verify!

      assert_predicate @connection, :active?

      # If we get no exception here, then either we re-connected successfully, or
      # we never actually got disconnected.
      new_connection_pid = @connection.query("select pg_backend_pid()")

      assert_not_equal original_connection_pid, new_connection_pid,
        "umm -- looks like you didn't break the connection, because we're still " \
        "successfully querying with the same connection pid."
    ensure
      # Repair all fixture connections so other tests won't break.
      @fixture_connections.each(&:verify!)
    end

    def test_set_session_variable_true
      run_without_connection do |orig_connection|
        ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: true }))
        set_true = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
        assert_equal set_true.rows, [["on"]]
      end
    end

    def test_set_session_variable_false
      run_without_connection do |orig_connection|
        ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: false }))
        set_false = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
        assert_equal set_false.rows, [["off"]]
      end
    end

    def test_set_session_variable_nil
      run_without_connection do |orig_connection|
        # This should be a no-op that does not raise an error
        ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: nil }))
      end
    end

    def test_set_session_variable_default
      run_without_connection do |orig_connection|
        # This should execute a query that does not raise an error
        ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: :default }))
      end
    end

    def test_set_session_timezone
      run_without_connection do |orig_connection|
        ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { timezone: "America/New_York" }))
        assert_equal "America/New_York", ActiveRecord::Base.connection.query_value("SHOW TIME ZONE")
      end
    end

    def test_get_and_release_advisory_lock
      lock_id = 5295901941911233559
      list_advisory_locks = <<~SQL
        SELECT locktype,
              (classid::bigint << 32) | objid::bigint AS lock_id
        FROM pg_locks
        WHERE locktype = 'advisory'
      SQL

      got_lock = @connection.get_advisory_lock(lock_id)
      assert got_lock, "get_advisory_lock should have returned true but it didn't"

      advisory_lock = @connection.query(list_advisory_locks).find { |l| l[1] == lock_id }
      assert advisory_lock,
        "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one"

      released_lock = @connection.release_advisory_lock(lock_id)
      assert released_lock, "expected release_advisory_lock to return true but it didn't"

      advisory_locks = @connection.query(list_advisory_locks).select { |l| l[1] == lock_id }
      assert_empty advisory_locks,
        "expected to have released advisory lock with lock_id #{lock_id} but it was still held"
    end

    def test_release_non_existent_advisory_lock
      fake_lock_id = 2940075057017742022
      with_warning_suppression do
        released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id)
        assert_equal released_non_existent_lock, false,
          "expected release_advisory_lock to return false when there was no lock to release"
      end
    end

    private

      def with_warning_suppression
        log_level = @connection.client_min_messages
        @connection.client_min_messages = "error"
        yield
        @connection.client_min_messages = log_level
      end
  end
end