You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Connection-level defaults for statement properties (starting with local_time_zone)
Motivation
Unlike psycopg/asyncpg/mysql, this driver has no persistent session. Those drivers hold a live
connection to the server, so a session parameter like timezone is set once and the server carries
it across every subsequent statement. Our Connection is a client-side simulation: each statement is
an independent REST submission carrying its own spec.properties, and nothing server-side remembers
anything between them. There is no session GUC to set.
So the only way to get session-like behavior -- the same local_time_zone on every statement a
workload runs -- is for the driver to re-apply the value on each submission. This isn't a
convenience layered on top of a session; in the absence of a session it is the mechanism. The proof
is already in the codebase: sql.current-catalog and sql.current-database are re-stamped from
connection state onto every statement for exactly this reason. This issue generalizes that
established move to caller-facing, caller-overridable defaults, with local_time_zone first.
Proposed API
A connect() / Connection() keyword argument that seeds a read/writeConnection.local_time_zone
property. The kwarg is the familiar, eagerly-validated, discoverable entry point -- mirroring the
true siblings database and compute_pool_id, the other connection-level inputs stamped onto every
statement submission (database becomes sql.current-database exactly as local_time_zone would
become sql.local-time-zone). The property lets a caller read the current value back and, if they
want, change it for later statements:
conn=connect(..., local_time_zone="America/New_York")
cursor.execute("SELECT LOCALTIMESTAMP") # renders in America/New_York, no per-call propertyconn.local_time_zone="America/Chicago"# affects statements submitted after this line
Unset by default (None) -> nothing emitted, so behavior is unchanged for callers who never touch it
(server default of UTC still applies).
On having both a kwarg and a writeable property: this is not two competing spellings of one
value. The epic's "exactly one way to spell a property" guard is about extra colliding with a typed
field inside a single construction -- a genuine ambiguity we raise on. Here there is one piece of
connection state with an init path (the kwarg) and a read/mutate path (the property); last write wins,
unambiguously. The connect-vs-mutable question that matters for session-holding drivers is moot for us
anyway: with no session, connect-time carries no server semantics, so both paths reduce to the same
"stamp onto future submissions" behavior. The one caveat to document: mutating the property affects
only statements submitted afterward -- there is no server-side session setting that changes
retroactively.
Blending / precedence
During statement-property determination, layers combine low-to-high, each overriding the last:
Server defaults -- implicit; nothing emitted.
Connection-level defaults (local_time_zone, ...) -- emitted only when set on the connection.
Per-call properties (the properties= dict or StatementProperties) -- the caller's explicit
per-statement intent; overrides the connection default for that key.
So a connection default fills in only where the per-call properties are silent, and never displaces
either an explicit per-call value or the driver overlay. The reserved-key rejection continues to
apply to the per-call (caller) layer only.
Constraints
Only non-driver-owned properties are eligible for connection-level defaults. The reserved trio
(catalog/database/snapshot.mode) is already connection/execution-derived and stays in the system
overlay; a connection default must never be able to set one.
Start with local_time_zone only. The mechanism should be shaped to generalize, but each
additional default (candidates: state_ttl, scan_startup_mode) needs its own judgment that a
connection-wide value is actually meaningful for it -- don't blanket-promote every field.
Open questions
Shape of the store: individual typed properties (Connection.local_time_zone: str | None) as
sketched, versus the connection holding a single StatementProperties of defaults and blending its to_properties_dict() in at layer 2. The latter reuses Frozen dataclass StatementProperties #163 wholesale and generalizes for free;
the former is a smaller, more discoverable surface. Lean: start with the individual property, keep
the door open to the held-object form if more defaults arrive.
Eager vs post-construction?Resolved (see Proposed API): a connect() kwarg that seeds a
read/write Connection.local_time_zone property -- both, init + read/mutate over one piece of
state, not competing spellings. Matches the database/compute_pool_id siblings for the kwarg.
Forcing the server default per statement: with None meaning "unset", a caller who wants one
statement to ignore the connection default falls back to the explicit value (local_time_zone="UTC")
rather than a sentinel. Confirm that's sufficient and no "reset to server default" sentinel is
needed.
Mutability / thread-safety: the property is mutable across the connection's life and affects
only subsequently submitted statements. That's consistent with threadsafety = 1 (connections are
not shared across threads); note it in the docstring.
Prior art in other Python DB drivers
Session parameters like timezone are configured at connect time across the ecosystem; none of
these expose a mutable post-construction attribute as the primary knob (post-connection changes are
done by running SET/set_config SQL):
mysql-connector-python -- dedicated per-property connect kwarg: connect(time_zone="PST")
issues SET time_zone at connection time (alongside init_command, sql_mode). Precedent for the individual-typed-kwarg form.
asyncpg -- connect(server_settings={"timezone": "UTC", ...}), a dict of arbitrary PostgreSQL
GUCs applied at connect. Precedent for the held-defaults-object form (a connection carrying a
bag of statement-property defaults, i.e. the StatementProperties-of-defaults alternative above).
psycopg 3 -- no dedicated timezone setter; you pass libpq options="-c timezone=..." at connect
or run SELECT set_config('TimeZone', ..., false). Notably it exposes the effective, resolved
value read-only as conn.info.timezone (a zoneinfo.ZoneInfo) -- setting and introspection are
separate concerns.
Takeaways for this issue:
Favor a connect() kwarg as the primary surface (matches mysql/asyncpg/psycopg); a mutable Connection.local_time_zone property (our http_user_agent pattern) can ride along as a
convenience but isn't the ecosystem norm.
The held-defaults form generalizes exactly like asyncpg's server_settings -- worth keeping in
view if more than one default lands.
These drivers have no per-statement "blend" because they don't need one: a real session holds the
GUC server-side, and a one-off override is a SQL SET LOCAL inside a transaction. We have no
session (see Motivation), so the connection-default-plus-per-call blend isn't us going beyond the
pattern -- it's us reconstructing, client-side and per submission, the session state those drivers
get for free. Borrow their connect-time surface, not their assumption of a persistent session.
Testing
Unit: connection default reaches the wire when the call omits the key; a per-call value overrides
the connection default; the driver overlay still wins; unset connection default emits nothing.
Integration: the local_time_zone round-trip (as in test_local_time_zone_shifts_naive_timestamp)
but driven by Connection.local_time_zone instead of a per-call StatementProperties.
Sequenced after [v0.4.x] Snapshot mode GA #164 -- the blend belongs in the consolidated statement-property determination
logic that [v0.4.x] Snapshot mode GA #164 relocates out of Connection; landing it first would mean reworking the merge
twice.
Connection-level defaults for statement properties (starting with
local_time_zone)Motivation
Unlike psycopg/asyncpg/mysql, this driver has no persistent session. Those drivers hold a live
connection to the server, so a session parameter like
timezoneis set once and the server carriesit across every subsequent statement. Our
Connectionis a client-side simulation: each statement isan independent REST submission carrying its own
spec.properties, and nothing server-side remembersanything between them. There is no session GUC to set.
So the only way to get session-like behavior -- the same
local_time_zoneon every statement aworkload runs -- is for the driver to re-apply the value on each submission. This isn't a
convenience layered on top of a session; in the absence of a session it is the mechanism. The proof
is already in the codebase:
sql.current-catalogandsql.current-databaseare re-stamped fromconnection state onto every statement for exactly this reason. This issue generalizes that
established move to caller-facing, caller-overridable defaults, with
local_time_zonefirst.Proposed API
A
connect()/Connection()keyword argument that seeds a read/writeConnection.local_time_zoneproperty. The kwarg is the familiar, eagerly-validated, discoverable entry point -- mirroring the
true siblings
databaseandcompute_pool_id, the other connection-level inputs stamped onto everystatement submission (
databasebecomessql.current-databaseexactly aslocal_time_zonewouldbecome
sql.local-time-zone). The property lets a caller read the current value back and, if theywant, change it for later statements:
Unset by default (
None) -> nothing emitted, so behavior is unchanged for callers who never touch it(server default of UTC still applies).
On having both a kwarg and a writeable property: this is not two competing spellings of one
value. The epic's "exactly one way to spell a property" guard is about
extracolliding with a typedfield inside a single construction -- a genuine ambiguity we raise on. Here there is one piece of
connection state with an init path (the kwarg) and a read/mutate path (the property); last write wins,
unambiguously. The connect-vs-mutable question that matters for session-holding drivers is moot for us
anyway: with no session, connect-time carries no server semantics, so both paths reduce to the same
"stamp onto future submissions" behavior. The one caveat to document: mutating the property affects
only statements submitted afterward -- there is no server-side session setting that changes
retroactively.
Blending / precedence
During statement-property determination, layers combine low-to-high, each overriding the last:
local_time_zone, ...) -- emitted only when set on the connection.properties=dict orStatementProperties) -- the caller's explicitper-statement intent; overrides the connection default for that key.
sql.current-catalog/-database/snapshot.mode) -- alwaysapplied, non-overridable, unchanged.
So a connection default fills in only where the per-call properties are silent, and never displaces
either an explicit per-call value or the driver overlay. The reserved-key rejection continues to
apply to the per-call (caller) layer only.
Constraints
(catalog/database/snapshot.mode) is already connection/execution-derived and stays in the system
overlay; a connection default must never be able to set one.
local_time_zoneonly. The mechanism should be shaped to generalize, but eachadditional default (candidates:
state_ttl,scan_startup_mode) needs its own judgment that aconnection-wide value is actually meaningful for it -- don't blanket-promote every field.
Open questions
Connection.local_time_zone: str | None) assketched, versus the connection holding a single
StatementPropertiesof defaults and blending itsto_properties_dict()in at layer 2. The latter reuses Frozen dataclassStatementProperties#163 wholesale and generalizes for free;the former is a smaller, more discoverable surface. Lean: start with the individual property, keep
the door open to the held-object form if more defaults arrive.
Eager vs post-construction?Resolved (see Proposed API): aconnect()kwarg that seeds aread/write
Connection.local_time_zoneproperty -- both, init + read/mutate over one piece ofstate, not competing spellings. Matches the
database/compute_pool_idsiblings for the kwarg.Nonemeaning "unset", a caller who wants onestatement to ignore the connection default falls back to the explicit value (
local_time_zone="UTC")rather than a sentinel. Confirm that's sufficient and no "reset to server default" sentinel is
needed.
only subsequently submitted statements. That's consistent with
threadsafety = 1(connections arenot shared across threads); note it in the docstring.
Prior art in other Python DB drivers
Session parameters like timezone are configured at connect time across the ecosystem; none of
these expose a mutable post-construction attribute as the primary knob (post-connection changes are
done by running
SET/set_configSQL):connect(time_zone="PST")issues
SET time_zoneat connection time (alongsideinit_command,sql_mode). Precedent for theindividual-typed-kwarg form.
connect(server_settings={"timezone": "UTC", ...}), a dict of arbitrary PostgreSQLGUCs applied at connect. Precedent for the held-defaults-object form (a connection carrying a
bag of statement-property defaults, i.e. the
StatementProperties-of-defaults alternative above).options="-c timezone=..."at connector run
SELECT set_config('TimeZone', ..., false). Notably it exposes the effective, resolvedvalue read-only as
conn.info.timezone(azoneinfo.ZoneInfo) -- setting and introspection areseparate concerns.
Takeaways for this issue:
connect()kwarg as the primary surface (matches mysql/asyncpg/psycopg); a mutableConnection.local_time_zoneproperty (ourhttp_user_agentpattern) can ride along as aconvenience but isn't the ecosystem norm.
server_settings-- worth keeping inview if more than one default lands.
GUC server-side, and a one-off override is a SQL
SET LOCALinside a transaction. We have nosession (see Motivation), so the connection-default-plus-per-call blend isn't us going beyond the
pattern -- it's us reconstructing, client-side and per submission, the session state those drivers
get for free. Borrow their connect-time surface, not their assumption of a persistent session.
Testing
the connection default; the driver overlay still wins; unset connection default emits nothing.
local_time_zoneround-trip (as intest_local_time_zone_shifts_naive_timestamp)but driven by
Connection.local_time_zoneinstead of a per-callStatementProperties.Relationships
StatementProperties#163 (StatementProperties/local_time_zonefield)logic that [v0.4.x] Snapshot mode GA #164 relocates out of
Connection; landing it first would mean reworking the mergetwice.