diff --git a/mssql_python/row.py b/mssql_python/row.py index b74e451e..368ec73e 100644 --- a/mssql_python/row.py +++ b/mssql_python/row.py @@ -156,8 +156,18 @@ def _apply_output_converters_optimized(self, values, converter_map): return converted_values - def __getitem__(self, index: int) -> Any: - """Allow accessing by numeric index: row[0]""" + def __getitem__(self, index) -> Any: + """Allow accessing by numeric index (row[0]) or column name (row["col"]).""" + if isinstance(index, str): + if index in self._column_map: + return self._values[self._column_map[index]] + # Case-insensitive lookup when lowercase is enabled + if get_settings().lowercase: + index_lower = index.lower() + for col_name in self._column_map: + if col_name.lower() == index_lower: + return self._values[self._column_map[col_name]] + raise KeyError(f"Row has no column '{index}'") return self._values[index] def __getattr__(self, name: str) -> Any: @@ -175,8 +185,8 @@ def __getattr__(self, name: str) -> Any: if name in self._column_map: return self._values[self._column_map[name]] - # If lowercase is enabled on the cursor, try case-insensitive lookup - if hasattr(self._cursor, "lowercase") and self._cursor.lowercase: + # If lowercase is enabled, try case-insensitive lookup + if get_settings().lowercase: name_lower = name.lower() for col_name in self._column_map: if col_name.lower() == name_lower: diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index 9e44e011..7a2a4745 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -996,3 +996,56 @@ def test_stringify_uuids_with_tuple_values(): assert row[2] == "hello" # Internal storage should now be a list (converted from tuple) assert isinstance(row._values, list) + + +def test_row_string_key_indexing(): + """Test Row supports string-key indexing via __getitem__ (row['col']).""" + from mssql_python.row import Row + + row = Row( + [1, "foo", 3.14], + {"ProductID": 0, "Name": 1, "Price": 2}, + cursor=None, + ) + + # String-key access + assert row["ProductID"] == 1 + assert row["Name"] == "foo" + assert row["Price"] == 3.14 + + # Integer index access still works + assert row[0] == 1 + assert row[1] == "foo" + assert row[2] == 3.14 + + # Slice access still works + assert row[0:2] == [1, "foo"] + + # Missing key raises KeyError + with pytest.raises(KeyError): + row["nonexistent"] + + +def test_row_string_key_case_insensitive_with_lowercase(): + """Test Row string-key indexing is case-insensitive when global lowercase is True.""" + from mssql_python.row import Row + from mssql_python.helpers import get_settings + + settings = get_settings() + original = settings.lowercase + try: + settings.lowercase = True + + row = Row( + [1, "bar"], + {"productid": 0, "name": 1}, + cursor=None, + ) + + # Exact match + assert row["productid"] == 1 + # Case-insensitive match + assert row["ProductID"] == 1 + assert row["NAME"] == "bar" + finally: + settings.lowercase = original diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index f4736107..a63c4d8f 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -2876,6 +2876,44 @@ def test_row_attribute_access(cursor, db_connection): db_connection.commit() +def test_row_string_key_indexing(cursor, db_connection): + """Test accessing row values by column name as string key: row['col']""" + try: + cursor.execute( + "CREATE TABLE #pytest_row_strkey (id INT PRIMARY KEY, name VARCHAR(50), age INT)" + ) + db_connection.commit() + + cursor.execute("INSERT INTO #pytest_row_strkey (id, name, age) VALUES (1, 'Alice', 25)") + db_connection.commit() + + cursor.execute("SELECT * FROM #pytest_row_strkey") + row = cursor.fetchone() + + # String-key access + assert row["id"] == 1, "Failed to access 'id' by string key" + assert row["name"] == "Alice", "Failed to access 'name' by string key" + assert row["age"] == 25, "Failed to access 'age' by string key" + + # Consistency with index and attribute access + assert row["id"] == row[0] == row.id + assert row["name"] == row[1] == row.name + assert row["age"] == row[2] == row.age + + # Non-existent key raises KeyError + with pytest.raises(KeyError): + row["nonexistent"] + + except Exception as e: + pytest.fail(f"Row string-key indexing test failed: {e}") + finally: + try: + cursor.execute("DROP TABLE IF EXISTS #pytest_row_strkey") + db_connection.commit() + except Exception: + pass + + def test_row_comparison_with_list(cursor, db_connection): """Test comparing Row objects with lists (__eq__ method)""" try: