-
Notifications
You must be signed in to change notification settings - Fork 50
FEAT: Add to_dict(), keys(), values(), items(), __contains__ to Row #613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a4852ad
d21846e
757a550
a4804ae
9b96304
75998e2
31f3d47
89c3b2f
b7cb1a8
0917d1f
93d9f37
c2c6927
ae22ff8
e6142d0
04111b9
10292eb
a4b4ac2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,14 +16,21 @@ class Row: | |
| A row of data from a cursor fetch operation. Provides both tuple-like indexing | ||
| and attribute access to column values. | ||
|
|
||
| Row supports dict-like access via keys(), values(), items(), and to_dict(). | ||
| However, iteration (for x in row) yields values, not keys — consistent with | ||
| pyodbc.Row and sqlite3.Row. Use row.keys() to iterate column names. | ||
|
|
||
| Column attribute access behavior depends on the global 'lowercase' setting: | ||
| - When enabled: Case-insensitive attribute access | ||
| - When disabled (default): Case-sensitive attribute access matching original column names | ||
|
|
||
| Example: | ||
| row = cursor.fetchone() | ||
| print(row[0]) # Access by index | ||
| print(row.column_name) # Access by column name (case sensitivity varies) | ||
| print(row[0]) # Access by index | ||
| print(row.column_name) # Access by column name | ||
| print(row.to_dict()) # Convert to dict | ||
| for value in row: # Iterates values, not keys | ||
| print(value) | ||
| """ | ||
|
|
||
| def __init__( | ||
|
|
@@ -73,6 +80,7 @@ def __init__( | |
| # Lowercase map is pre-built once per result set in the cursor and shared | ||
| # across all rows. None when lowercase is off (the default) — zero cost. | ||
| self._column_map_lower = column_map_lower | ||
| self._column_names = None # Lazy-computed on first access via _get_column_names() | ||
|
|
||
| def _stringify_uuids(self, indices): | ||
| """ | ||
|
|
@@ -209,6 +217,41 @@ def __getattr__(self, name: str) -> Any: | |
|
|
||
| raise AttributeError(f"Row has no attribute '{name}'") | ||
|
|
||
| def _get_column_names(self) -> tuple: | ||
| """Lazy-compute and cache deduplicated column names on first access.""" | ||
| if self._column_names is not None: | ||
| return self._column_names | ||
|
|
||
| if self._cursor and hasattr(self._cursor, "description") and self._cursor.description: | ||
| column_names = tuple(desc[0] for desc in self._cursor.description) | ||
| elif self._column_map: | ||
| idx_to_name: dict = {} | ||
| for name, idx in self._column_map.items(): | ||
| if idx not in idx_to_name: | ||
| idx_to_name[idx] = name | ||
| column_names = tuple(idx_to_name[i] for i in sorted(idx_to_name)) | ||
| else: | ||
| column_names = () | ||
|
|
||
| self._column_names = column_names | ||
| return column_names | ||
|
|
||
| def keys(self) -> tuple: | ||
|
jahnvi480 marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just checked during testing, adding cursor.execute("SELECT 'timeout' AS keys, '30' AS [values]")
row = cursor.fetchone()
row.keys # before PR: 'timeout' (via __getattr__)
row.keys # after PR: <bound method Row.keys>
row["keys"] # still works: 'timeout'class methods win over |
||
| """Return column names, like dict.keys().""" | ||
| return self._get_column_names() | ||
|
|
||
| def values(self) -> tuple: | ||
| """Return column values as a tuple, like dict.values().""" | ||
| return tuple(self._values) | ||
|
|
||
| def items(self) -> list: | ||
| """Return (column_name, value) pairs, like dict.items().""" | ||
| return list(zip(self._get_column_names(), self._values)) | ||
|
|
||
| def to_dict(self) -> dict: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
cursor.execute("""
SELECT e.id, e.name, m.id, m.name
FROM employees e LEFT JOIN employees m ON e.manager_id = m.id
""")
row = cursor.fetchone()
list(row) # [1, 'Alice', 2, 'Bob'] — 4 values
row.to_dict() # {'id': 2, 'name': 'Bob'} — 2 entries, Alice gonelets discuss approaching this |
||
| """Return the row as a plain dict mapping column names to values.""" | ||
| return dict(zip(self._get_column_names(), self._values)) | ||
|
|
||
| def __eq__(self, other: Any) -> bool: | ||
| """ | ||
| Support comparison with lists for test compatibility. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regression caused from previous suggestion:
_get_column_names()readsself._cursor.descriptionlazily on first call. if the cursor re-executes beforekeys()/to_dict()is called on an old row, the old row picks up the new query's column names. repro:fix: snapshot
cursor.descriptionat construction time instead of reading it live. one pointer assignment, no computation:still lazy, still zero-cost if
keys()/to_dict()is never called, but safe on cursor reuse.