Query Metadata
picopyn.query_metadata
Picodata's query metadata protocol extension: distribution key metadata for a prepared statement, and stale-plan invalidation.
Based on the Picodata pgproto metadata mechanism.
Requires Picodata 26.1.1 or newer -- older Picodata silently ignores special startup options (only logs a warning server-side), so on an older cluster this whole mechanism just quietly never produces any query-meta or stale-plan recovery, with no error on the client side.
The async and sync drivers receive a query's meta from a Picodata Notice,
caches it, and detects the stale-plan signal that means it needs re-fetching
(see async and
sync usage).
When cached metadata is available, async Pool.execute can use it to compute
bucket_id and route the query to the bucket master's pooled connection.
QUERY_METADATA_OPTION = 'pico_query_metadata=true'
module-attribute
Makes Picodata send a Notice with query distribution meta. Only worth requesting on a connection that actually forwards it -- the meta doesn't depend on which connection asked.
STMT_INVALIDATED_SQLSTATE = '42999'
module-attribute
The stale-plan signal: the SQLSTATE Picodata raises (instead of silently using a stale plan) when a prepared statement's schema has changed since it was Parsed, requiring the caller to close it and re-Parse. Requires special connection option. See the DDL invalidation.
STMT_INVALIDATION_OPTION = 'pico_stmt_invalidation=true'
module-attribute
Makes Picodata return the stale-plan signal instead of silently running a stale plan, once a prepared statement's schema has changed. Requested on every connection, not just the one doing metadata discovery, so ordinary queries can detect and recover from stale plans too.
PreparedStatementMetadata
dataclass
Distribution key metadata for a prepared statement.
Sent by Picodata as a Notice right before ParseComplete, when the
connection enables Notice receiving via the
special connection option.
Note
Currently meta
is only populated for 1-row INSERT statements: anything else (SELECT/UPDATE/...)
always gets an empty dk_meta, even a simple equality filter on
the distribution key like WHERE id = $1. Also empty when Picodata
couldn't resolve a single bucket_id even for a supported INSERT (see
Unbounded queries).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The exact SQL text Picodata echoes back, matching what the
client sent in the |
required |
tier
|
str | None
|
The tier the query's tables belong to, or None if it cannot be estimated. |
required |
dk_meta
|
list[tuple[int, int]]
|
Can be empty if Picodata cannot locate a single |
required |
Source code in picopyn/query_metadata.py
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 | |
QueryMetadataCache
Bounded, least-recently-used cache of PreparedStatementMetadata.
async Pool and
sync Pool use this cache, controlled by their
QueryMetadataService.
It also remembers the queries Picodata parsed but sent no metadata Notice for. Both kinds of entry share the same bound and are dropped together.
Source code in picopyn/query_metadata.py
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 | |
__init__(max_size)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_size
|
int
|
Maximum number of distinct queries to cache. The least recently used entry is evicted once this is exceeded. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in picopyn/query_metadata.py
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
clear()
Remove all cached entries.
Source code in picopyn/query_metadata.py
265 266 267 | |
evict(query)
Drop a single cached entry, e.g. after a stale-plan error (see
is_stmt_invalidated_error).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The SQL query text whose cached entry should be dropped. A no-op if nothing is cached for it. |
required |
Source code in picopyn/query_metadata.py
255 256 257 258 259 260 261 262 263 | |
get(query)
Look up cached metadata for query, marking it as most recently used.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The SQL query text to look up. |
required |
Returns:
| Type | Description |
|---|---|
PreparedStatementMetadata | None
|
The cached metadata, or None on a cache miss and for a query known |
PreparedStatementMetadata | None
|
to have no metadata. |
Source code in picopyn/query_metadata.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
is_known_without_metadata(query)
True if query is the one Picodata already parsed (sent ParseComplete)
without sending any metadata Notice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The SQL query text to look up. |
required |
Source code in picopyn/query_metadata.py
246 247 248 249 250 251 252 253 | |
resize(max_size)
Change the cache limit, immediately evicting excess LRU entries.
Entries for queries without metadata count towards the same limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_size
|
int
|
Maximum number of distinct queries to cache. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in picopyn/query_metadata.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | |
set(metadata)
Cache query metadata, keyed by its own query text.
Evicts the least recently used entry if the cache is now over its max size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metadata
|
PreparedStatementMetadata
|
The metadata to cache. |
required |
Source code in picopyn/query_metadata.py
215 216 217 218 219 220 221 222 223 224 | |
set_no_metadata(query)
Remember that Picodata sent no metadata Notice for query, so
asking again is pointless until the schema changes.
Picodata answers such a Parse as usual (the statement is prepared, no error -- ParseComplete), it just does not answer with the Notice. The reason can be:
- Picodata does not support the mechanism (<26.1.1), so it never sends the Notice for any query
- Picodata parses the query without answering anything about it (DDL)
The entry lives by the same rules as a cached metadata, so it can be evict/cleared.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
The SQL query text Picodata sent no metadata Notice for. |
required |
Source code in picopyn/query_metadata.py
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | |
is_stmt_invalidated_error(exc)
True if the exception is a Postgres error with Picodata's stale-plan SQLSTATE.
Connection wrappers (async Connection.execute/fetch/fetchrow/prepare)
wrap the original driver error in a RuntimeError, which drops the sqlstate
attribute but keeps the original exception (as __cause__). So we simply use
the same check to see whether it's the invalidation signal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exc
|
BaseException
|
The exception to check -- either the original driver error, or a
|
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the exception carries the stale-plan SQLSTATE. |
Source code in picopyn/query_metadata.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
parse_query_metadata_notice(message)
Parse a Notice's text as Picodata query metadata. See the Picodata message contract.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
The raw text of a Postgres Notice message. |
required |
Returns:
| Type | Description |
|---|---|
PreparedStatementMetadata | None
|
The parsed metadata, or None if |
Source code in picopyn/query_metadata.py
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 | |