require_relative 'spec_helper'

describe 'Prepared statements' do
  let(:pool_size) { 5 }
  let(:processes) { Helpers::PgDoorman.single_instance_setup("example_db", pool_size) }
  let(:prepared_statements_cache_size) { 100 }
  let(:server_round_robin) { false }

  before do
    new_configs = processes.pg_doorman.current_config
    new_configs["pools"]["example_db"]["prepared_statements_cache_size"] = prepared_statements_cache_size
    new_configs["pools"]["example_db"]["users"]["0"]["pool_size"] =  pool_size
    processes.pg_doorman.update_config(new_configs)
    processes.pg_doorman.reload_config
  end

  context 'when trying prepared statements' do
    it 'it allows unparameterized statements to succeed' do
      conn1 = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))
      conn2 = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      prepared_query = "SELECT 1"

      # prepare query on server 1 and client 1
      conn1.prepare('statement1', prepared_query)
      conn1.exec_prepared('statement1')

      conn2.transaction do
        # Claim server 1 with client 2
        conn2.exec("SELECT 2")

        # Client 1 now runs the prepared query, and it's automatically
        # prepared on server 2
        conn1.prepare('statement2', prepared_query)
        conn1.exec_prepared('statement2')

        # Client 2 now prepares the same query that was already
        # prepared on server 1. And PgBouncer reuses that already
        # prepared query for this different client.
        conn2.prepare('statement3', prepared_query)
        conn2.exec_prepared('statement3')
        end
    ensure
        conn1.close if conn1
        conn2.close if conn2
    end

    it 'it allows parameterized statements to succeed' do
      conn1 = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))
      conn2 = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      prepared_query = "SELECT $1"

      # prepare query on server 1 and client 1
      conn1.prepare('statement1', prepared_query)
      conn1.exec_prepared('statement1', [1])

      conn2.transaction do
        # Claim server 1 with client 2
        conn2.exec("SELECT 2")

        # Client 1 now runs the prepared query, and it's automatically
        # prepared on server 2
        conn1.prepare('statement2', prepared_query)
        conn1.exec_prepared('statement2', [1])

        # Client 2 now prepares the same query that was already
        # prepared on server 1. And PgBouncer reuses that already
        # prepared query for this different client.
        conn2.prepare('statement3', prepared_query)
        conn2.exec_prepared('statement3', [1])
      end
    ensure
       conn1.close if conn1
       conn2.close if conn2

    end
  end

  context 'when trying large packets' do
    it "works with large parse" do
      conn1 = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      long_string = "1" * 4096 * 10
      prepared_query = "SELECT '#{long_string}'"


      # prepare query on server 1 and client 1
      conn1.prepare('statement1', prepared_query)
      result = conn1.exec_prepared('statement1')

      # assert result matches long_string
      expect(result.getvalue(0, 0)).to eq(long_string)
    ensure
      conn1.close if conn1
    end

    it "works with large bind" do
      conn1 = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      long_string = "1" * 4096 * 10
      prepared_query = "SELECT $1::text"

      # prepare query on server 1 and client 1
      conn1.prepare('statement1', prepared_query)
      result = conn1.exec_prepared('statement1', [long_string])

      # assert result matches long_string
      expect(result.getvalue(0, 0)).to eq(long_string)
    ensure
      conn1.close if conn1
    end
  end

  context 'when statement cache is smaller than set of unqiue statements' do
    let(:prepared_statements_cache_size) { 1 }
    let(:pool_size) { 1 }

    it "evicts all but 1 statement from the server cache" do
      conn = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      5.times do |i|
        prepared_query = "SELECT '#{i}'"
        conn.prepare("statement#{i}", prepared_query)
        result = conn.exec_prepared("statement#{i}")
        expect(result.getvalue(0, 0)).to eq(i.to_s)
      end

      # Check number of prepared statements (expected: 1)
      n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
      expect(n_statements).to eq(1)
    end
  end

  context 'when statement cache is larger than set of unqiue statements' do
    let(:pool_size) { 1 }

    it "does not evict any of the statements from the cache" do
      # cache size 5
      conn = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      5.times do |i|
        prepared_query = "SELECT '#{i}'"
        conn.prepare("statement#{i}", prepared_query)
        result = conn.exec_prepared("statement#{i}")
        expect(result.getvalue(0, 0)).to eq(i.to_s)
      end

      # Check number of prepared statements (expected: 1)
      n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
      expect(n_statements).to eq(5)
    end
  end

  context 'when preparing the same query' do
    let(:prepared_statements_cache_size) { 5 }
    let(:pool_size) { 5 }

    it "reuses statement cache when there are different statement names on the same connection" do
      conn = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      10.times do |i|
        statement_name = "statement_#{i}"
        conn.prepare(statement_name, 'SELECT $1::int')
        conn.exec_prepared(statement_name, [1])
      end

      # Check number of prepared statements (expected: 1)
      n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
      expect(n_statements).to eq(1)
    end

    it "reuses statement cache when there are different statement names on different connections" do
      10.times do |i|
        conn = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))
        statement_name = "statement_#{i}"
        conn.prepare(statement_name, 'SELECT $1::int')
        conn.exec_prepared(statement_name, [1])
      end

      # Check number of prepared statements (expected: 1)
      conn = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))
      n_statements = conn.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
      expect(n_statements).to eq(1)
    end
  end

  context 'when reloading config' do
    let(:pool_size) { 1 }

    it "test_reload_config" do
      conn = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))

      # prepare query
      conn.prepare('statement1', 'SELECT 1')
      conn.exec_prepared('statement1')

      # Reload config which triggers pool recreation
      new_configs = processes.pg_doorman.current_config
      new_configs["pools"]["example_db"]["prepared_statements_cache_size"] = prepared_statements_cache_size + 1
      processes.pg_doorman.update_config(new_configs)
      processes.pg_doorman.reload_config
      sleep 11

      # check that we're starting with no prepared statements on the server
      conn_check = PG.connect(processes.pg_doorman.connection_string('example_db', 'example_user_1', 'test'))
      n_statements = conn_check.exec("SELECT count(*) FROM pg_prepared_statements").getvalue(0, 0).to_i
      expect(n_statements).to eq(0)

      # still able to run prepared query
      conn.exec_prepared('statement1')
    end
  end
end