98
98
stale, nor can it retry the statement as the PostgreSQL transaction is
99
99
invalidated when these errors occur.
100
100
101
+ .. _asyncpg_prepared_statement_name:
102
+
103
+ Prepared Statement Name
104
+ -----------------------
105
+
106
+ By default, asyncpg enumerates prepared statements in numeric order, which
107
+ can lead to errors if a name has already been taken for another prepared
108
+ statement. This issue can arise if your application uses database proxies
109
+ such as PgBouncer to handle connections. One possible workaround is to
110
+ use dynamic prepared statement names, which asyncpg now supports through
111
+ an optional name value for the statement name. This allows you to
112
+ generate your own unique names that won't conflict with existing ones.
113
+ To achieve this, you can provide a function that will be called every time
114
+ a prepared statement is prepared::
115
+
116
+ from uuid import uuid4
117
+
118
+ engine = create_async_engine(
119
+ "postgresql+asyncpg://user:pass@hostname/dbname",
120
+ poolclass=NullPool,
121
+ connect_args={
122
+ 'pepared_statement_name_func': lambda: f'__asyncpg_{uuid4()}__',
123
+ },
124
+ )
125
+
126
+ .. seealso::
127
+
128
+ https://github.com/MagicStack/asyncpg/issues/837
129
+
130
+ https://github.com/sqlalchemy/sqlalchemy/issues/6467
131
+
132
+ .. warning:: To prevent a buildup of useless prepared statements in
133
+ your application, it's important to use the NullPool poolclass and
134
+ PgBouncer with a configured `DISCARD https://www.postgresql.org/docs/current/sql-discard.html`_
135
+ setup. The DISCARD command is used to release resources held by the db connection,
136
+ including prepared statements. Without proper setup, prepared statements can
137
+ accumulate quickly and cause performance issues.
138
+
101
139
Disabling the PostgreSQL JIT to improve ENUM datatype handling
102
140
---------------------------------------------------------------
103
141
@@ -642,13 +680,20 @@ class AsyncAdapt_asyncpg_connection(AdaptedConnection):
642
680
"_transaction" ,
643
681
"_started" ,
644
682
"_prepared_statement_cache" ,
683
+ "_prepared_statement_name_func" ,
645
684
"_invalidate_schema_cache_asof" ,
646
685
"_execute_mutex" ,
647
686
)
648
687
649
688
await_ = staticmethod (await_only )
650
689
651
- def __init__ (self , dbapi , connection , prepared_statement_cache_size = 100 ):
690
+ def __init__ (
691
+ self ,
692
+ dbapi ,
693
+ connection ,
694
+ prepared_statement_cache_size = 100 ,
695
+ prepared_statement_name_func = None ,
696
+ ):
652
697
self .dbapi = dbapi
653
698
self ._connection = connection
654
699
self .isolation_level = self ._isolation_setting = "read_committed"
@@ -666,6 +711,11 @@ def __init__(self, dbapi, connection, prepared_statement_cache_size=100):
666
711
else :
667
712
self ._prepared_statement_cache = None
668
713
714
+ if prepared_statement_name_func :
715
+ self ._prepared_statement_name_func = prepared_statement_name_func
716
+ else :
717
+ self ._prepared_statement_name_func = self ._default_name_func
718
+
669
719
async def _check_type_cache_invalidation (self , invalidate_timestamp ):
670
720
if invalidate_timestamp > self ._invalidate_schema_cache_asof :
671
721
await self ._connection .reload_schema_state ()
@@ -676,7 +726,9 @@ async def _prepare(self, operation, invalidate_timestamp):
676
726
677
727
cache = self ._prepared_statement_cache
678
728
if cache is None :
679
- prepared_stmt = await self ._connection .prepare (operation )
729
+ prepared_stmt = await self ._connection .prepare (
730
+ operation , name = self ._prepared_statement_name_func ()
731
+ )
680
732
attributes = prepared_stmt .get_attributes ()
681
733
return prepared_stmt , attributes
682
734
@@ -692,7 +744,9 @@ async def _prepare(self, operation, invalidate_timestamp):
692
744
if cached_timestamp > invalidate_timestamp :
693
745
return prepared_stmt , attributes
694
746
695
- prepared_stmt = await self ._connection .prepare (operation )
747
+ prepared_stmt = await self ._connection .prepare (
748
+ operation , name = self ._prepared_statement_name_func ()
749
+ )
696
750
attributes = prepared_stmt .get_attributes ()
697
751
cache [operation ] = (prepared_stmt , attributes , time .time ())
698
752
@@ -792,6 +846,10 @@ def close(self):
792
846
def terminate (self ):
793
847
self ._connection .terminate ()
794
848
849
+ @staticmethod
850
+ def _default_name_func ():
851
+ return None
852
+
795
853
796
854
class AsyncAdaptFallback_asyncpg_connection (AsyncAdapt_asyncpg_connection ):
797
855
__slots__ = ()
@@ -809,17 +867,23 @@ def connect(self, *arg, **kw):
809
867
prepared_statement_cache_size = kw .pop (
810
868
"prepared_statement_cache_size" , 100
811
869
)
870
+ prepared_statement_name_func = kw .pop (
871
+ "prepared_statement_name_func" , None
872
+ )
873
+
812
874
if util .asbool (async_fallback ):
813
875
return AsyncAdaptFallback_asyncpg_connection (
814
876
self ,
815
877
await_fallback (self .asyncpg .connect (* arg , ** kw )),
816
878
prepared_statement_cache_size = prepared_statement_cache_size ,
879
+ prepared_statement_name_func = prepared_statement_name_func ,
817
880
)
818
881
else :
819
882
return AsyncAdapt_asyncpg_connection (
820
883
self ,
821
884
await_only (self .asyncpg .connect (* arg , ** kw )),
822
885
prepared_statement_cache_size = prepared_statement_cache_size ,
886
+ prepared_statement_name_func = prepared_statement_name_func ,
823
887
)
824
888
825
889
class Error (Exception ):
0 commit comments