Permalink
Browse files

reset prepared statement when schema changes imapact statement result…

…s. fixes #3335
  • Loading branch information...
1 parent d44702c commit 6a28c512e358a95bbabd24af9d2b0608df787158 @tenderlove tenderlove committed Oct 18, 2011
View
54 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -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[$$]
@@ -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.
View
8 activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -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

4 comments on commit 6a28c51

@joevandyk

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

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

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

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.