Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions mssql_python/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ 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
# Canonical column names in ordinal order, deduplicated.
# cursor.description is the authoritative source; _column_map may
# contain lowercase aliases injected by _prepare_metadata_result_set.
if cursor and hasattr(cursor, "description") and cursor.description:
self._column_names = tuple(desc[0] for desc in cursor.description)
elif column_map is not None:
# Fallback: deduplicate _column_map by keeping first name per index
idx_to_name: dict = {}
for name, idx in column_map.items():
if idx not in idx_to_name:
idx_to_name[idx] = name
self._column_names = tuple(idx_to_name[i] for i in sorted(idx_to_name))
else:
self._column_names = ()

def _stringify_uuids(self, indices):
"""
Expand Down Expand Up @@ -209,6 +223,31 @@ def __getattr__(self, name: str) -> Any:

raise AttributeError(f"Row has no attribute '{name}'")

def keys(self):
"""Return column names, like dict.keys()."""
return self._column_names

def values(self):
"""Return column values, like dict.values()."""
return self._values

def items(self):
"""Return (column_name, value) pairs, like dict.items()."""
return zip(self._column_names, self._values)

def to_dict(self):
"""Return the row as a plain dict mapping column names to values."""
return dict(zip(self._column_names, self._values))

def __contains__(self, key) -> bool:
"""Support 'col_name in row' membership testing."""
if isinstance(key, str):
if key in self._column_map:
return True
if self._column_map_lower is not None:
return key.lower() in self._column_map_lower
return False

def __eq__(self, other: Any) -> bool:
"""
Support comparison with lists for test compatibility.
Expand Down
101 changes: 101 additions & 0 deletions tests/test_001_globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -1059,3 +1059,104 @@ def test_row_string_key_case_insensitive_with_lowercase():
# Non-existent attribute raises AttributeError
with pytest.raises(AttributeError):
row.nonexistent


def test_row_to_dict():
"""Test Row.to_dict() returns a plain dict of column names to values."""
from mssql_python.row import Row

row = Row(
[1, "foo", 3.14],
{"ProductID": 0, "Name": 1, "Price": 2},
cursor=None,
)

d = row.to_dict()
assert d == {"ProductID": 1, "Name": "foo", "Price": 3.14}
assert isinstance(d, dict)


def test_row_keys_values_items():
"""Test Row.keys(), values(), and items() behave like dict counterparts."""
from mssql_python.row import Row

column_map = {"id": 0, "name": 1}
row = Row([42, "Alice"], column_map, cursor=None)

# keys()
assert list(row.keys()) == ["id", "name"]

# values()
assert list(row.values()) == [42, "Alice"]

# items()
assert list(row.items()) == [("id", 42), ("name", "Alice")]


def test_row_dict_methods_with_lowercase_aliases():
"""Test dict-like methods deduplicate when _column_map contains lowercase aliases.

The cursor's _prepare_metadata_result_set injects both original-cased and
lowercase entries (e.g. {"ProductID": 0, "productid": 0}). The dict methods
must return N entries (not 2N) and preserve original casing.
"""
from mssql_python.row import Row

# Simulate what _prepare_metadata_result_set produces:
# original name + lowercase alias per column
column_map = {"ProductID": 0, "productid": 0, "Name": 1, "name": 1}
row = Row([1, "foo"], column_map, cursor=None)

# keys() — N entries, original casing preserved
keys = list(row.keys())
assert len(keys) == 2
assert keys == ["ProductID", "Name"]

# values() — N entries matching the values
assert list(row.values()) == [1, "foo"]

# items() — N pairs
items = list(row.items())
assert len(items) == 2
assert items == [("ProductID", 1), ("Name", "foo")]

# to_dict() — N entries, original casing as keys
d = row.to_dict()
assert len(d) == 2
assert d == {"ProductID": 1, "Name": "foo"}

# len consistency: keys, values, items all match len(row)
assert len(keys) == len(row)
assert len(list(row.values())) == len(row)
assert len(items) == len(row)


def test_row_contains():
"""Test 'column_name in row' membership testing."""
from mssql_python.row import Row

row = Row(
[1, "foo"],
{"ProductID": 0, "Name": 1},
cursor=None,
)

assert "ProductID" in row
assert "Name" in row
assert "nonexistent" not in row
# Integer is not a column name
assert 0 not in row


def test_row_contains_case_insensitive():
"""Test 'in' operator is case-insensitive when column_map_lower is provided."""
from mssql_python.row import Row

column_map = {"productid": 0, "name": 1}
column_map_lower = {k.lower(): v for k, v in column_map.items()}
row = Row([1, "bar"], column_map, cursor=None, column_map_lower=column_map_lower)

assert "productid" in row
assert "ProductID" in row
assert "NAME" in row
assert "missing" not in row
Loading