Skip to content

Commit

Permalink
reset prepared statement when schema changes imapact statement results.
Browse files Browse the repository at this point in the history
fixes #3335
  • Loading branch information
tenderlove committed Oct 18, 2011
1 parent d44702c commit 6a28c51
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ def clear
cache.clear
end

def delete(sql_key)
dealloc cache[sql_key]
cache.delete sql_key
end

private
def cache
@cache[$$]
Expand Down Expand Up @@ -1030,27 +1035,54 @@ def translate_exception(exception, message)
end

private
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:

def exec_no_cache(sql, binds)
@connection.async_exec(sql)
end

def exec_cache(sql, binds)
sql_key = "#{schema_search_path}-#{sql}"
begin
stmt_key = prepare_statement sql

# Clear the queue
@connection.get_last_result
@connection.send_query_prepared(stmt_key, binds.map { |col, val|
type_cast(val, col)
})
@connection.block
@connection.get_last_result
rescue PGError => e
# Get the PG code for the failure. Annoyingly, the code for
# prepared statements whose return value may have changed is
# FEATURE_NOT_SUPPORTED. Check here for more details:
# http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
if FEATURE_NOT_SUPPORTED == code
@statements.delete sql_key(sql)
retry
else
raise e
end
end
end

# Returns the statement identifier for the client side cache
# of statements
def sql_key(sql)
"#{schema_search_path}-#{sql}"
end

# Prepare the statement if it hasn't been prepared, return
# the statement key.
def prepare_statement(sql)
sql_key = sql_key(sql)
unless @statements.key? sql_key
nextkey = @statements.next_key
@connection.prepare nextkey, sql
@statements[sql_key] = nextkey
end

key = @statements[sql_key]

# Clear the queue
@connection.get_last_result
@connection.send_query_prepared(key, binds.map { |col, val|
type_cast(val, col)
})
@connection.block
@connection.get_last_result
@statements[sql_key]
end

# The internal PostgreSQL identifier of the money data type.
Expand Down
8 changes: 8 additions & 0 deletions activerecord/test/cases/adapters/postgresql/schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ def teardown
@connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
end

def test_schema_change_with_prepared_stmt
@connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
@connection.exec_query "alter table developers add column zomg int", 'sql', []
@connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
ensure
@connection.exec_query "alter table developers drop column if exists zomg", 'sql', []
end

def test_table_exists?
[Thing1, Thing2, Thing3, Thing4].each do |klass|
name = klass.table_name
Expand Down

4 comments on commit 6a28c51

@joevandyk
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno if this will work inside a transaction: https://img.skitch.com/20111019-nceg3b4uye3meaj4pryd9wktqe.png

If there's a postgresql error inside a transaction, it needs to be rolled back to the last savepoint/transaction.

@joevandyk
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was looking at this, I couldn't figure out a way to do it that didn't involve wrapping every single query inside a savepoint, which is probably a stupid thing to do.

@joevandyk
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The postgresql log from that test run:

LOG:  duration: 0.077 ms  statement: BEGIN
LOG:  duration: 0.288 ms  parse a2: select * from developers where id = $1
LOG:  duration: 0.025 ms  bind a2: select * from developers where id = $1
DETAIL:  parameters: $1 = '1'
LOG:  duration: 0.032 ms  execute a2: select * from developers where id = $1
DETAIL:  parameters: $1 = '1'
LOG:  duration: 0.140 ms  statement: alter table developers add column zomg int
ERROR:  cached plan must not change result type
STATEMENT:  select * from developers where id = $1
ERROR:  current transaction is aborted, commands ignored until end of transaction block
STATEMENT:  DEALLOCATE a2
LOG:  duration: 0.011 ms  statement: ROLLBACK
LOG:  duration: 0.092 ms  statement: alter table developers drop column if exists zomg

@yahonda
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fails if PostgreSQL version < 9.0.

ActiveRecord::StatementInvalid: PGError: ERROR:  syntax error at or near "exists"
LINE 1: alter table developers drop column if exists zomg

Please sign in to comment.