From 95ab6ed79c7956bcf6ad7e18ad6241f0c9c90f23 Mon Sep 17 00:00:00 2001 From: Albert N Date: Sat, 25 Oct 2025 13:45:57 -0400 Subject: [PATCH] perf tuning of `BaseRow` common cases are a bit faster overall Python version | | python opt | python main | py opt / main | | -------------------- | ---------- | ----------- | ------------- | | base_row_new | 0.84959 | 0.86417 | 0.9831283197 | | row_new | 0.85698 | 0.85543 | 1.001811954 | | base_row_new_proc | 2.77489 | 2.81795 | 0.9847193882 | | row_new_proc | 2.77881 | 2.86206 | 0.9709125595 | | brow_new_proc_none | 1.6311 | 1.65773 | 0.9839358641 | | row_new_proc_none | 1.65048 | 1.69896 | 0.9714648962 | | row_dumps | 0.15279 | 0.14519 | 1.052345203 | | row_loads | 0.91471 | 0.91358 | 1.001236892 | | row_values_impl | 0.1818 | 0.17773 | 1.022899904 | | row_iter | 0.44042 | 0.4554 | 0.967105841 | | row_len | 0.13858 | 0.13079 | 1.059561129 | | row_hash | 0.28614 | 0.29172 | 0.9808720691 | | getitem | 0.23159 | 0.23138 | 1.000907598 | | getitem_slice | 0.38393 | 0.38765 | 0.9904037147 | | get_by_key | 0.33692 | 0.33913 | 0.993483325 | | get_by_key2 | 0.35105 | 0.33915 | 1.035087719 | | getattr | 0.46809 | 0.46911 | 0.9978256699 | | get_by_key_recreate | 1.08137 | 1.18224 | 0.9146789146 | | get_by_key_recreate2 | 0.98543 | 0.99272 | 0.9926565396 | | getattr_recreate | 0.74 | 0.7407 | 0.999054948 | | contains | 0.49312 | 0.74477 | 0.6621104502 | Cython version | | cython opt | cython main | cy opt / main | | -------------------- | ---------- | ----------- | ------------- | | base_row_new | 0.12933 | 0.1347 | 0.9601336303 | | row_new | 0.15755 | 0.16489 | 0.9554854752 | | base_row_new_proc | 0.87398 | 1.08483 | 0.8056377497 | | row_new_proc | 0.88592 | 1.13255 | 0.7822347799 | | brow_new_proc_none | 0.2664 | 0.41703 | 0.6388029638 | | row_new_proc_none | 0.27526 | 0.42483 | 0.6479297601 | | row_dumps | 0.37526 | 0.20093 | 1.867615588 | | row_loads | 0.53037 | 0.3607 | 1.470390907 | | row_values_impl | 0.36886 | 0.22218 | 1.660185435 | | row_iter | 0.25249 | 0.25553 | 0.9881031581 | | row_len | 0.05194 | 0.05412 | 0.9597191426 | | row_hash | 0.18864 | 0.18726 | 1.007369433 | | getitem | 0.13876 | 0.13934 | 0.9958375197 | | getitem_slice | 0.23181 | 0.2318 | 1.000043141 | | get_by_key | 0.17031 | 0.17857 | 0.9537436299 | | get_by_key2 | 0.58172 | 0.31651 | 1.837919813 | | getattr | 0.21275 | 1.16669 | 0.1823534958 | | get_by_key_recreate | 0.80161 | 0.8836 | 0.9072091444 | | get_by_key_recreate2 | 1.14649 | 0.98709 | 1.161484768 | | getattr_recreate | 0.55927 | 1.09921 | 0.5087926784 | | contains | 0.35338 | 0.5494 | 0.6432107754 | Closes: #12919 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/12919 Pull-request-sha: b13204b7416083ccff09a45291b11fd606ff0fbd Change-Id: I5ff2b951f9840e99abdad3ae286ecb8becc70e4b --- lib/sqlalchemy/engine/_row_cy.py | 111 +++++++++++++++++++++------ lib/sqlalchemy/engine/row.py | 17 ---- lib/sqlalchemy/util/cython.py | 17 ++++ setup.py | 5 ++ test/aaa_profiling/test_resultset.py | 3 + test/perf/compiled_extensions/row.py | 41 +++++++++- test/profiles.txt | 20 ++--- test/sql/test_resultset.py | 22 ++++++ 8 files changed, 185 insertions(+), 51 deletions(-) diff --git a/lib/sqlalchemy/engine/_row_cy.py b/lib/sqlalchemy/engine/_row_cy.py index 87cf5bfa39..29d8f6548c 100644 --- a/lib/sqlalchemy/engine/_row_cy.py +++ b/lib/sqlalchemy/engine/_row_cy.py @@ -4,13 +4,15 @@ # # This module is part of SQLAlchemy and is released under # the MIT License: https://www.opensource.org/licenses/mit-license.php -# mypy: disable-error-code="misc" +# mypy: disable-error-code="misc,no-redef,valid-type,no-untyped-call" +# mypy: disable-error-code="index,no-any-return,arg-type,assignment" from __future__ import annotations from typing import Any from typing import Dict from typing import Iterator from typing import List +from typing import NoReturn from typing import Optional from typing import Sequence from typing import Tuple @@ -60,13 +62,15 @@ class BaseRow: data: Sequence[Any], ) -> None: """Row objects are constructed by CursorResult objects.""" - - data_tuple: Tuple[Any, ...] = ( - _apply_processors(processors, data) - if processors is not None - else tuple(data) + self._set_attrs( + parent, + key_to_index, + ( + _apply_processors(processors, data) + if processors is not None + else data if isinstance(data, tuple) else tuple(data) + ), ) - self._set_attrs(parent, key_to_index, data_tuple) @cython.cfunc @cython.inline @@ -121,34 +125,97 @@ class BaseRow: return self._get_by_key_impl(key, False) @cython.cfunc + @cython.inline def _get_by_key_impl(self, key: _KeyType, attr_err: cython.bint) -> object: - index: Optional[int] = self._key_to_index.get(key) + # NOTE: don't type index since there is no advantage in making cython + # do a type check + index = self._key_to_index.get(key) if index is not None: return self._data[index] self._parent._key_not_found(key, attr_err) + if cython.compiled: + + @cython.annotation_typing(False) + def __getattribute__(self, name: str) -> Any: + # this optimizes getattr access on cython, that's otherwise + # quite slow compared with python. The assumption is that + # most columns will not start with _. If they do they will + # fallback on __getattr__ in any case. + if name != "" and name[0] != "_": + # inline of _get_by_key_impl. Attribute on the class + # take precedence over column names. + index = self._key_to_index.get(name) + if index is not None and not hasattr(type(self), name): + return self._data[index] + + return object.__getattribute__(self, name) + @cython.annotation_typing(False) def __getattr__(self, name: str) -> Any: return self._get_by_key_impl(name, True) + def __setattr__(self, name: str, value: Any) -> NoReturn: + raise AttributeError("can't set attribute") + + def __delattr__(self, name: str) -> NoReturn: + raise AttributeError("can't delete attribute") + def _to_tuple_instance(self) -> Tuple[Any, ...]: return self._data + def __contains__(self, key: Any) -> cython.bint: + return key in self._data + + +if cython.compiled: -@cython.inline -@cython.cfunc -def _apply_processors( - proc: _ProcessorsType, data: Sequence[Any] -) -> Tuple[Any, ...]: - res: List[Any] = list(data) - proc_size: cython.Py_ssize_t = len(proc) - # TODO: would be nice to do this only on the fist row - assert len(res) == proc_size - for i in range(proc_size): - p = proc[i] - if p is not None: - res[i] = p(res[i]) - return tuple(res) + from cython.cimports.cpython import PyTuple_New + from cython.cimports.cpython import Py_INCREF + from cython.cimports.cpython import PyTuple_SET_ITEM + + @cython.inline + @cython.cfunc + @cython.wraparound(False) + @cython.boundscheck(False) + @cython.locals( + res=tuple, + proc_size=cython.Py_ssize_t, + i=cython.Py_ssize_t, + p=object, + value=object, + ) + def _apply_processors( + proc: Sequence[Any], data: Sequence[Any] + ) -> Tuple[Any, ...]: + proc_size = len(proc) + # TODO: would be nice to do this only on the fist row + assert len(data) == proc_size + res = PyTuple_New(proc_size) + for i in range(proc_size): + p = proc[i] + if p is not None: + value = p(data[i]) + else: + value = data[i] + Py_INCREF(value) + PyTuple_SET_ITEM(res, i, value) + return res + +else: + + def _apply_processors( + proc: _ProcessorsType, data: Sequence[Any] + ) -> Tuple[Any, ...]: + res: List[Any] = list(data) + proc_size = len(proc) + # TODO: would be nice to do this only on the fist row + assert len(res) == proc_size + for i in range(proc_size): + p = proc[i] + if p is not None: + res[i] = p(res[i]) + return tuple(res) # This reconstructor is necessary so that pickles with the Cy extension or diff --git a/lib/sqlalchemy/engine/row.py b/lib/sqlalchemy/engine/row.py index 6c5db5b49d..0744569f83 100644 --- a/lib/sqlalchemy/engine/row.py +++ b/lib/sqlalchemy/engine/row.py @@ -18,9 +18,7 @@ from typing import Callable from typing import Dict from typing import Generic from typing import Iterator -from typing import List from typing import Mapping -from typing import NoReturn from typing import Optional from typing import Sequence from typing import Tuple @@ -75,12 +73,6 @@ class Row(BaseRow, _RowBase[Unpack[_Ts]], Generic[Unpack[_Ts]]): __slots__ = () - def __setattr__(self, name: str, value: Any) -> NoReturn: - raise AttributeError("can't set attribute") - - def __delattr__(self, name: str) -> NoReturn: - raise AttributeError("can't delete attribute") - @deprecated( "2.1.0", "The :meth:`.Row._tuple` method is deprecated, :class:`.Row` " @@ -222,9 +214,6 @@ class Row(BaseRow, _RowBase[Unpack[_Ts]], Generic[Unpack[_Ts]]): count = _special_name_accessor("count") index = _special_name_accessor("index") - def __contains__(self, key: Any) -> bool: - return key in self._data - def _op(self, other: Any, op: Callable[[Any, Any], bool]) -> bool: return ( op(self._to_tuple_instance(), other._to_tuple_instance()) @@ -374,15 +363,9 @@ class RowMapping(BaseRow, typing.Mapping["_KeyType", Any]): else: __getitem__ = BaseRow._get_by_key_impl_mapping - def _values_impl(self) -> List[Any]: - return list(self._data) - def __iter__(self) -> Iterator[str]: return (k for k in self._parent.keys if k is not None) - def __len__(self) -> int: - return len(self._data) - def __contains__(self, key: object) -> bool: return self._parent._has_key(key) diff --git a/lib/sqlalchemy/util/cython.py b/lib/sqlalchemy/util/cython.py index 0d796313d8..288e497650 100644 --- a/lib/sqlalchemy/util/cython.py +++ b/lib/sqlalchemy/util/cython.py @@ -30,6 +30,7 @@ uint = int float = float # noqa: A001 double = float void = Any +NULL = None # functions @@ -59,3 +60,19 @@ def exceptval(value: Any = None, *, check: bool = False) -> _NO_OP[_T]: def cast(type_: Type[_T], value: Any, *, typecheck: bool = False) -> _T: return value # type: ignore[no-any-return] + + +def returns(_: type) -> _NO_OP[_T]: + return _no_op + + +def locals(**kwargs: Any) -> _NO_OP[_T]: # noqa: A001 + return _no_op + + +def wraparound(value: bool) -> _NO_OP[_T]: + return _no_op + + +def boundscheck(value: bool) -> _NO_OP[_T]: + return _no_op diff --git a/setup.py b/setup.py index 4f73c4e58b..2adbae3222 100644 --- a/setup.py +++ b/setup.py @@ -46,9 +46,14 @@ CYTHON_MODULES = ( if HAS_CYTHON and IS_CPYTHON and not DISABLE_EXTENSION: assert _cy_Extension is not None assert _cy_build_ext is not None + from Cython.Compiler import Options + + Options.docstrings = False + Options.clear_to_none = False cython_directives: Dict[str, Any] = { "language_level": "3", + # "initializedcheck": False, } if sys.version_info >= (3, 13): diff --git a/test/aaa_profiling/test_resultset.py b/test/aaa_profiling/test_resultset.py index 049e4a45e5..5c6ef2b1dd 100644 --- a/test/aaa_profiling/test_resultset.py +++ b/test/aaa_profiling/test_resultset.py @@ -266,4 +266,7 @@ class RowTest(fixtures.TestBase): def __iter__(self): return iter(self.data) + def __len__(self): + return len(self.data) + self._test_getitem_value_refcounts_new(CustomSeq) diff --git a/test/perf/compiled_extensions/row.py b/test/perf/compiled_extensions/row.py index 227bc8915b..f739b6e6c2 100644 --- a/test/perf/compiled_extensions/row.py +++ b/test/perf/compiled_extensions/row.py @@ -111,8 +111,12 @@ class BaseRow(Case): class Row(self.impl): pass + class RowMap(self.impl): + __getitem__ = self.impl._get_by_key_impl_mapping + self.Row = Row - self.row_sub = Row(*self.row_args) + self.row_map = RowMap(*self.row_args) + self.row_long_map = RowMap(*self.row_long_args) self.row_state = self.row.__getstate__() self.row_long_state = self.row_long.__getstate__() @@ -223,6 +227,14 @@ class BaseRow(Case): @test_case def get_by_key(self): + self.row_map["a"] + self.row_map["b"] + self.row_long_map["s"] + self.row_long_map["a"] + + @test_case + def get_by_key2(self): + # NOTE: this is not representative of real usage self.row._get_by_key_impl_mapping("a") self.row._get_by_key_impl_mapping("b") self.row_long._get_by_key_impl_mapping("s") @@ -235,8 +247,26 @@ class BaseRow(Case): self.row_long.x self.row_long.y - @test_case(number=25_000) + @test_case(number=15_000) def get_by_key_recreate(self): + self.init_objects() + row = self.row_map + for _ in range(25): + row["a"] + l_row = self.row_long_map + for _ in range(25): + l_row["f"] + l_row["o"] + l_row["r"] + l_row["t"] + l_row["y"] + l_row["t"] + l_row["w"] + l_row["o"] + + @test_case(number=15_000) + def get_by_key_recreate2(self): + # NOTE: this is not representative of real usage self.init_objects() row = self.row for _ in range(25): @@ -268,3 +298,10 @@ class BaseRow(Case): l_row.t l_row.w l_row.o + + @test_case + def contains(self): + 1 in self.row + -1 in self.row + 1 in self.row_long + -1 in self.row_long diff --git a/test/profiles.txt b/test/profiles.txt index 50da808a17..a40db06e4e 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -528,7 +528,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_6 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.13_postgresql_psycopg2_dbapiunicode_cextensions 1632 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.13_postgresql_psycopg2_dbapiunicode_nocextensions 35643 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_cextensions 1598 -test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_nocextensions 35609 +test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_nocextensions 37599 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_mariadb_mysqldb_dbapiunicode_cextensions 1648 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_mariadb_mysqldb_dbapiunicode_nocextensions 35650 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_mssql_pyodbc_dbapiunicode_cextensions 1652 @@ -538,7 +538,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_6 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_cextensions 1632 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_nocextensions 35634 test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_cextensions 1598 -test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 35609 +test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 37598 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-0] @@ -574,7 +574,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.13_postgresql_psycopg2_dbapiunicode_cextensions 14 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.13_postgresql_psycopg2_dbapiunicode_nocextensions 16 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_cextensions 14 -test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_nocextensions 16 +test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_nocextensions 17 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_mariadb_mysqldb_dbapiunicode_cextensions 18 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_mariadb_mysqldb_dbapiunicode_nocextensions 20 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_mssql_pyodbc_dbapiunicode_cextensions 14 @@ -584,7 +584,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_cextensions 14 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_nocextensions 16 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_cextensions 14 -test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 16 +test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 17 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] @@ -597,7 +597,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.13_postgresql_psycopg2_dbapiunicode_cextensions 14 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.13_postgresql_psycopg2_dbapiunicode_nocextensions 16 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_cextensions 14 -test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_nocextensions 16 +test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.13_sqlite_pysqlite_dbapiunicode_nocextensions 17 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_mariadb_mysqldb_dbapiunicode_cextensions 18 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_mariadb_mysqldb_dbapiunicode_nocextensions 20 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_mssql_pyodbc_dbapiunicode_cextensions 14 @@ -607,7 +607,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_cextensions 14 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_nocextensions 16 test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_cextensions 14 -test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 16 +test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 17 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] @@ -653,7 +653,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpy test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_cextensions 299 test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_nocextensions 4301 test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_cextensions 271 -test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 4273 +test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 5264 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode @@ -676,7 +676,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cp test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_cextensions 299 test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_nocextensions 4301 test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_cextensions 271 -test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 4273 +test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 5264 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_string @@ -699,7 +699,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_cextensions 623 test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_nocextensions 4625 test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_cextensions 589 -test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 4600 +test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 5591 # TEST: test.aaa_profiling.test_resultset.ResultSetTest.test_unicode @@ -722,4 +722,4 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpytho test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_cextensions 623 test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.14_postgresql_psycopg2_dbapiunicode_nocextensions 4625 test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_cextensions 589 -test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 4600 +test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.14_sqlite_pysqlite_dbapiunicode_nocextensions 5591 diff --git a/test/sql/test_resultset.py b/test/sql/test_resultset.py index ea9ce57a1f..1acf10c615 100644 --- a/test/sql/test_resultset.py +++ b/test/sql/test_resultset.py @@ -1905,6 +1905,7 @@ class CursorResultTest(fixtures.TablesTest): eq_(row.key, "kv") eq_(row.count, "cv") eq_(row.index, "iv") + eq_(row.foo, "f") eq_(row._mapping["foo"], "f") eq_(row._mapping["count"], "cv") @@ -1927,6 +1928,27 @@ class CursorResultTest(fixtures.TablesTest): eq_(row.count("cv"), 1) eq_(row.count("x"), 0) + def test_row_precedence_normal_names(self): + f = ("_fields", "_asdict", "_mapping", "as_tuple") + v = ["ff", "ad", "mm", "at"] + metadata = SimpleResultMetaData(f) + + class SubRow(Row): + # use subclass to ensure there is always a public method + @property + def as_tuple(self): + return tuple(self) + + row = SubRow(metadata, None, metadata._key_to_index, v) + + eq_(row._fields, f) + eq_(row._asdict(), dict(zip(f, v))) + eq_(row._mapping, dict(zip(f, v))) + eq_(row.as_tuple, tuple(v)) + + with expect_raises(AttributeError): + getattr(row, "") # test cython getattr edge case + def test_new_row_no_dict_behaviors(self): """This mode is not used currently but will be once we are in 2.0.""" metadata = SimpleResultMetaData(["a", "b", "count"]) -- 2.47.3