Skip to content

Commit 457e12b

Browse files
authored
Python: fix(sql_server): validate collection field types before table creation (#14697)
## Summary The SQL Server vector store's type mapping (`_python_type_to_sql`) returned `None` for data fields with unmapped Python types — e.g. `datetime.date`, `Optional[str]`, `UUID` — and the CREATE TABLE builder interpolated that `None` directly into the DDL, so `ensure_collection_exists` failed with an opaque pyodbc syntax error instead of telling the user what was wrong. Unmapped field types now raise `VectorStoreModelValidationError` from the pure type-mapping helper, before any connection or SQL is built — the same behavior Postgres got in #14611. The error names the field and its unsupported type, and points at the escape hatch: ``` SQL Server field type 'datetime.date' for field 'created' is not supported for automatic table creation. Use a supported Python type, or create the table before using the collection. ``` ## Behavior notes - `Optional[str]` and similar optional-of-unmapped types are now rejected up front — the same trade-off #14611 accepted for Postgres, where a clean early error beats a database-side failure with no actionable message. - No new type mappings were added (`date` → `DATE` etc. would be a feature decision); if maintainers want them they can layer them on top of the validation. ## Testing - `python/tests/unit/connectors/memory/test_sql_server.py`: parametrized unmapped-type cases (`datetime.date`, `Optional[str]`, `uuid.UUID`, `list[datetime.date]`) and an unsupported key-field case, each asserting the validation error names the field and the type; the pre-existing exact-DDL happy-path tests are unchanged and pass. - Red on main: the generated DDL for a `datetime.date` field reads `[created] None NULL`, and the new tests fail with `DID NOT RAISE`. Green with the fix: the whole `connectors/memory` unit directory passes (546 tests), plus ruff and mypy clean on the changed file.
1 parent 87eadc7 commit 457e12b

2 files changed

Lines changed: 45 additions & 7 deletions

File tree

‎python/semantic_kernel/connectors/sql_server.py‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from semantic_kernel.exceptions import (
3636
VectorSearchExecutionException,
3737
VectorStoreInitializationException,
38+
VectorStoreModelValidationError,
3839
VectorStoreOperationException,
3940
)
4041
from semantic_kernel.kernel_pydantic import KernelBaseSettings
@@ -799,15 +800,20 @@ def get_collection(
799800
# region: Query Build Functions
800801

801802

802-
def _python_type_to_sql(python_type_str: str | None, is_key: bool = False) -> str | None:
803+
def _python_type_to_sql(python_type_str: str | None, is_key: bool = False, field_name: str | None = None) -> str:
803804
"""Convert a string representation of a Python type to a SQL data type.
804805
805806
Args:
806807
python_type_str: The string representation of the Python type (e.g., "int", "List[str]").
807808
is_key: Whether the type is a key field.
809+
field_name: Name of the field the type belongs to, used in error messages.
808810
809811
Returns:
810-
Corresponding SQL data type as a string, if found. If the type is not found, return None.
812+
Corresponding SQL data type as a string.
813+
814+
Raises:
815+
VectorStoreOperationException: If the type is None.
816+
VectorStoreModelValidationError: If the type is not supported for automatic table creation.
811817
"""
812818
if python_type_str is None:
813819
raise VectorStoreOperationException("property type cannot be None")
@@ -830,14 +836,18 @@ def _python_type_to_sql(python_type_str: str | None, is_key: bool = False) -> st
830836
if match:
831837
# Extract the inner type of the list and convert it to a SQL array type
832838
element_type_str = match.group(1)
833-
sql_element_type = _python_type_to_sql(element_type_str)
839+
sql_element_type = _python_type_to_sql(element_type_str, field_name=field_name)
834840
return f"{sql_element_type}[]"
835841

836842
# Handle basic types
837843
if python_type_str in type_mapping:
838844
return type_mapping[python_type_str]
839845

840-
return None
846+
field_context = f" for field '{field_name}'" if field_name else ""
847+
raise VectorStoreModelValidationError(
848+
f"SQL Server field type {python_type_str!r}{field_context} is not supported for automatic table creation. "
849+
"Use a supported Python type, or create the table before using the collection."
850+
)
841851

842852

843853
def _cast_value(value: Any) -> str:
@@ -886,13 +896,13 @@ def _build_create_table_query(
886896
# add the key field
887897
command.query.append(
888898
f"{QueryBuilder.escape_identifier(key_field.storage_name or key_field.name)} "
889-
f"{_python_type_to_sql(key_field.type_, is_key=True)} NOT NULL,\n"
899+
f"{_python_type_to_sql(key_field.type_, is_key=True, field_name=key_field.name)} NOT NULL,\n"
890900
)
891901
# add the data fields
892902
for field in data_fields:
893903
command.query.append(
894904
f"{QueryBuilder.escape_identifier(field.storage_name or field.name)}"
895-
f" {_python_type_to_sql(field.type_)} NULL,\n"
905+
f" {_python_type_to_sql(field.type_, field_name=field.name)} NULL,\n"
896906
)
897907
# add the vector fields
898908
for field in vector_fields:
@@ -993,7 +1003,8 @@ def _build_merge_query(
9931003
command = SqlCommand()
9941004
# Declare a temp table to store the keys that are updated
9951005
command.query.append(
996-
f"DECLARE @UpsertedKeys TABLE (KeyColumn {_python_type_to_sql(key_field.type_ or 'str', is_key=True)});\n"
1006+
f"DECLARE @UpsertedKeys TABLE (KeyColumn "
1007+
f"{_python_type_to_sql(key_field.type_ or 'str', is_key=True, field_name=key_field.name)});\n"
9971008
)
9981009
# start the MERGE statement
9991010
command.query.append_table_name(schema, table, prefix="MERGE INTO", suffix="AS t", newline=True)

‎python/tests/unit/connectors/memory/test_sql_server.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from semantic_kernel.data.vector import DistanceFunction, IndexKind, VectorSearchOptions, VectorStoreField
2525
from semantic_kernel.exceptions.vector_store_exceptions import (
2626
VectorStoreInitializationException,
27+
VectorStoreModelValidationError,
2728
VectorStoreOperationException,
2829
)
2930

@@ -159,6 +160,32 @@ def test_build_create_table_query_uses_storage_name_for_primary_key(self):
159160
assert "[pk_id] nvarchar" in cmd_str
160161
assert "PRIMARY KEY ([pk_id])" in cmd_str
161162

163+
@mark.parametrize(
164+
("field_type", "unsupported_type"),
165+
[
166+
("datetime.date", "datetime.date"),
167+
("Optional[str]", "Optional[str]"),
168+
("uuid.UUID", "uuid.UUID"),
169+
("list[datetime.date]", "datetime.date"),
170+
],
171+
)
172+
def test_build_create_table_query_unsupported_field_type(self, field_type, unsupported_type):
173+
key_field = VectorStoreField("key", name="id", type="str")
174+
data_fields = [VectorStoreField("data", name="created", type=field_type)]
175+
with raises(VectorStoreModelValidationError) as exc_info:
176+
_build_create_table_query("dbo", "Test", key_field, data_fields, [])
177+
message = str(exc_info.value)
178+
assert f"SQL Server field type {unsupported_type!r} for field 'created' is not supported" in message
179+
assert "Use a supported Python type, or create the table before using the collection." in message
180+
181+
def test_build_create_table_query_unsupported_key_field_type(self):
182+
key_field = VectorStoreField("key", name="id", type="uuid.UUID")
183+
with raises(VectorStoreModelValidationError) as exc_info:
184+
_build_create_table_query("dbo", "Test", key_field, [], [])
185+
message = str(exc_info.value)
186+
assert "SQL Server field type 'uuid.UUID' for field 'id' is not supported" in message
187+
assert "Use a supported Python type, or create the table before using the collection." in message
188+
162189
def test_build_merge_query_output_uses_storage_name(self):
163190
key_field = VectorStoreField("key", name="id", type="str", storage_name="pk_id")
164191
records = [{"pk_id": "1"}]

0 commit comments

Comments
 (0)