From ba94ff12aaf777909da03682b7fd9f8a76e08dfa Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Fri, 16 Oct 2026 17:30:39 +0200 Subject: [PATCH 1/4] Python: validate PostgreSQL collection field types --- python/semantic_kernel/connectors/postgres.py | 103 ++++++++++-------- .../memory/postgres/test_postgres_int.py | 58 ++++++++++ .../connectors/memory/test_postgres_store.py | 82 +++++++++++++- 3 files changed, 194 insertions(+), 49 deletions(-) diff --git a/python/semantic_kernel/connectors/postgres.py b/python/semantic_kernel/connectors/postgres.py index 42994d75d042..131cec3c4c88 100644 --- a/python/semantic_kernel/connectors/postgres.py +++ b/python/semantic_kernel/connectors/postgres.py @@ -90,53 +90,58 @@ IndexKind.IVF_FLAT: "ivfflat", IndexKind.DEFAULT: "hnsw", } +POSTGRES_TYPE_ALIASES: Final[dict[str, str]] = { + "str": "TEXT", + "text": "TEXT", + "int": "INTEGER", + "integer": "INTEGER", + "float": "DOUBLE PRECISION", + "double precision": "DOUBLE PRECISION", + "bool": "BOOLEAN", + "boolean": "BOOLEAN", + "dict": "JSONB", + "jsonb": "JSONB", + "datetime": "TIMESTAMP", + "timestamp": "TIMESTAMP", + "bytes": "BYTEA", + "bytea": "BYTEA", +} +LIST_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile(r"list\s*\[(.+)\]", re.IGNORECASE) +DICT_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile(r"dict\s*\[(.+),\s*(.+)\]", re.IGNORECASE) +ARRAY_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile(r"(.+)\[\]") # region: Helpers -def _python_type_to_postgres(python_type_str: str) -> str | None: - """Convert a string representation of a Python type to a PostgreSQL data type. +def _get_postgres_type(type_name: str) -> sql.Composable: + """Convert a supported field type to a trusted PostgreSQL type expression. Args: - python_type_str: The string representation of the Python type (e.g., "int", "List[str]"). + type_name: The Python or PostgreSQL type name. Returns: - Corresponding PostgreSQL data type as a string, if found. If the type is not found, return None. + A trusted PostgreSQL type expression. + + Raises: + VectorStoreModelValidationError: If the type is not supported for automatic table creation. """ - # Basic type mapping from Python types (in string form) to PostgreSQL types - type_mapping = { - "str": "TEXT", - "int": "INTEGER", - "float": "DOUBLE PRECISION", - "bool": "BOOLEAN", - "dict": "JSONB", - "datetime": "TIMESTAMP", - "bytes": "BYTEA", - "NoneType": "NULL", - } - - # Regular expression to detect lists, e.g., "List[str]" or "List[int]" - list_pattern = re.compile(r"(?i)List\[(.*)\]") - - # Check if the type is a list - match = list_pattern.match(python_type_str) - if match: - # Extract the inner type of the list and convert it to a PostgreSQL array type - element_type_str = match.group(1) - postgres_element_type = _python_type_to_postgres(element_type_str) - return f"{postgres_element_type}[]" - - # Check if the type is a dictionary - dict_pattern = re.compile(r"(?i)Dict\[(.*), (.*)\]") - match = dict_pattern.match(python_type_str) - if match: - return "JSONB" - - # Handle basic types - if python_type_str in type_mapping: - return type_mapping[python_type_str] - - return None + normalized_type = " ".join(type_name.strip().split()) + if postgres_type := POSTGRES_TYPE_ALIASES.get(normalized_type.casefold()): + return sql.SQL(postgres_type) + + if match := LIST_TYPE_PATTERN.fullmatch(normalized_type): + return sql.SQL("{}[]").format(_get_postgres_type(match.group(1))) + + if DICT_TYPE_PATTERN.fullmatch(normalized_type): + return sql.SQL("JSONB") + + if match := ARRAY_TYPE_PATTERN.fullmatch(normalized_type): + return sql.SQL("{}[]").format(_get_postgres_type(match.group(1))) + + raise VectorStoreModelValidationError( + f"PostgreSQL field type {type_name!r} is not supported for automatic collection creation. " + "Use a supported Python or PostgreSQL type, or create the table before using the collection." + ) def _convert_row_to_dict(row: tuple[Any, ...], fields: Sequence[tuple[str, VectorStoreField | None]]) -> dict[str, Any]: @@ -562,12 +567,17 @@ def _deserialize_store_models_to_dicts(self, records: Sequence[Any], **kwargs: A @override async def ensure_collection_exists(self, **kwargs: Any) -> None: - """Create a PostgreSQL table based on a dictionary of VectorStoreRecordField. + """Create a PostgreSQL table based on the collection definition. + + Automatic creation supports built-in Python field types, their corresponding PostgreSQL aliases, + and arrays of those types. For custom domains, enums, extension types, or type modifiers, create the + table separately and use the collection without calling this method. Args: - table_name: Name of the table to be created - fields: A dictionary where keys are column names and values are VectorStoreRecordField instances - **kwargs: Additional arguments + **kwargs: Additional arguments. + + Raises: + VectorStoreModelValidationError: If a field type is not supported for automatic table creation. """ if self.connection_pool is None: raise VectorStoreOperationException( @@ -581,8 +591,7 @@ async def ensure_collection_exists(self, **kwargs: Any) -> None: if not field.type_: raise ValueError(f"Property type is not defined for field '{field.name}'") - # If the property type represents a Python type, convert it to a PostgreSQL type - property_type = _python_type_to_postgres(field.type_) or field.type_.upper() + property_type = _get_postgres_type(field.type_) # For Vector fields with dimensions, use pgvector's VECTOR type # Note that other vector types are supported in pgvector (e.g. halfvec), @@ -595,17 +604,15 @@ async def ensure_collection_exists(self, **kwargs: Any) -> None: ) ) elif field.field_type == FieldTypes.KEY: - # Use the property_type directly for key fields column_definitions.append( sql.SQL("{name} {col_type} PRIMARY KEY").format( - name=sql.Identifier(field.storage_name or field.name), col_type=sql.SQL(property_type) + name=sql.Identifier(field.storage_name or field.name), col_type=property_type ) ) else: - # Use the property_type directly for other types column_definitions.append( sql.SQL("{name} {col_type}").format( - name=sql.Identifier(field.storage_name or field.name), col_type=sql.SQL(property_type) + name=sql.Identifier(field.storage_name or field.name), col_type=property_type ) ) diff --git a/python/tests/integration/memory/postgres/test_postgres_int.py b/python/tests/integration/memory/postgres/test_postgres_int.py index cf9df99e967a..8f7e414e2d93 100644 --- a/python/tests/integration/memory/postgres/test_postgres_int.py +++ b/python/tests/integration/memory/postgres/test_postgres_int.py @@ -8,6 +8,7 @@ import pandas as pd import pytest import pytest_asyncio +from psycopg import sql from pydantic import BaseModel from semantic_kernel.connectors.postgres import PostgresCollection, PostgresSettings, PostgresStore @@ -19,6 +20,7 @@ VectorStoreField, vectorstoremodel, ) +from semantic_kernel.exceptions import VectorStoreModelValidationError from semantic_kernel.exceptions.memory_connector_exceptions import ( MemoryConnectorConnectionException, MemoryConnectorInitializationError, @@ -143,6 +145,62 @@ async def test_ensure_collection_exists_exists_and_delete(vector_store: Postgres assert does_exist_3 is False +async def test_ensure_collection_exists_rejects_unsupported_type_without_database_side_effects( + vector_store: PostgresStore, +): + suffix = str(uuid.uuid4()).replace("-", "")[:8] + sentinel_name = f"sentinel_{suffix}" + collection_name = f"test_collection_{suffix}" + connection_pool = vector_store.connection_pool + + async with connection_pool.connection() as conn, conn.cursor() as cur: + await cur.execute( + sql.SQL("CREATE TABLE {schema}.{table} (id INTEGER)").format( + schema=sql.Identifier(vector_store.db_schema), + table=sql.Identifier(sentinel_name), + ) + ) + await conn.commit() + + definition = VectorStoreCollectionDefinition( + fields=[ + VectorStoreField("key", name="id", type="int"), + VectorStoreField( + "data", + name="content", + type=f"TEXT); DROP TABLE {sentinel_name}; --", + ), + ] + ) + collection = vector_store.get_collection( + collection_name=collection_name, + record_type=dict, + definition=definition, + ) + + try: + with pytest.raises(VectorStoreModelValidationError, match="not supported for automatic collection creation"): + await collection.ensure_collection_exists() + + async with connection_pool.connection() as conn, conn.cursor() as cur: + await cur.execute( + "SELECT to_regclass(%s)", + (f"{vector_store.db_schema}.{sentinel_name}",), + ) + row = await cur.fetchone() + assert row is not None + assert row[0] == sentinel_name + finally: + async with connection_pool.connection() as conn, conn.cursor() as cur: + await cur.execute( + sql.SQL("DROP TABLE IF EXISTS {schema}.{table}").format( + schema=sql.Identifier(vector_store.db_schema), + table=sql.Identifier(sentinel_name), + ) + ) + await conn.commit() + + async def test_list_collection_names(vector_store): async with create_simple_collection(vector_store) as simple_collection: simple_collection_id = simple_collection.collection_name diff --git a/python/tests/unit/connectors/memory/test_postgres_store.py b/python/tests/unit/connectors/memory/test_postgres_store.py index ad33d864ca9f..b60e1ad59af7 100644 --- a/python/tests/unit/connectors/memory/test_postgres_store.py +++ b/python/tests/unit/connectors/memory/test_postgres_store.py @@ -24,10 +24,15 @@ DistanceFunction, IndexKind, VectorSearchOptions, + VectorStoreCollectionDefinition, VectorStoreField, vectorstoremodel, ) -from semantic_kernel.exceptions import VectorSearchExecutionException, VectorStoreOperationException +from semantic_kernel.exceptions import ( + VectorSearchExecutionException, + VectorStoreModelValidationError, + VectorStoreOperationException, +) from semantic_kernel.functions import KernelParameterMetadata @@ -198,6 +203,81 @@ class ModelWithImplicitTypes: ) +async def test_ensure_collection_exists_explicit_definition_with_supported_aliases( + vector_store: PostgresStore, mock_cursor: Mock +) -> None: + definition = VectorStoreCollectionDefinition( + fields=[ + VectorStoreField("key", name="id", type="int"), + VectorStoreField("data", name="title", type="text"), + VectorStoreField("data", name="count", type="INTEGER"), + VectorStoreField("data", name="score", type="double precision"), + VectorStoreField("data", name="enabled", type="BOOLEAN"), + VectorStoreField("data", name="metadata", type="jsonb"), + VectorStoreField("data", name="created_at", type="TIMESTAMP"), + VectorStoreField("data", name="payload", type="bytea"), + VectorStoreField("data", name="tags", type="list[str]"), + VectorStoreField("data", name="weights", type="DOUBLE PRECISION[]"), + VectorStoreField("data", name="items", type="Dict[str, Any]"), + ] + ) + collection = vector_store.get_collection( + collection_name="test_collection", + record_type=dict, + definition=definition, + ) + + await collection.ensure_collection_exists() + + mock_cursor.execute.assert_awaited_once() + statement = mock_cursor.execute.call_args.args[0] + assert statement.as_string() == ( + 'CREATE TABLE "public"."test_collection" ' + '("id" INTEGER PRIMARY KEY, "title" TEXT, "count" INTEGER, "score" DOUBLE PRECISION, ' + '"enabled" BOOLEAN, "metadata" JSONB, "created_at" TIMESTAMP, "payload" BYTEA, ' + '"tags" TEXT[], "weights" DOUBLE PRECISION[], "items" JSONB)' + ) + + +@pytest.mark.parametrize( + "unsupported_type", + [ + "TEXT); SELECT 1; --", + "TEXT -- comment", + "TEXT/*comment*/", + "TEXT\n); SELECT 1", + "list[int", + "list[]", + "INTEGER[", + "INTEGER[] trailing", + "list[custom_type]", + ], +) +async def test_ensure_collection_exists_rejects_unsupported_types_before_execution( + vector_store: PostgresStore, + mock_cursor: Mock, + mock_connection_pool, + unsupported_type: str, +) -> None: + definition = VectorStoreCollectionDefinition( + fields=[ + VectorStoreField("key", name="id", type="int"), + VectorStoreField("data", name="content", type=unsupported_type), + ] + ) + collection = vector_store.get_collection( + collection_name="test_collection", + record_type=dict, + definition=definition, + ) + + with pytest.raises(VectorStoreModelValidationError, match="not supported for automatic collection creation"): + await collection.ensure_collection_exists() + + mock_cursor.execute.assert_not_awaited() + mock_connection_pool[0].assert_not_called() + + async def test_upsert_records(vector_store: PostgresStore, mock_cursor: Mock) -> None: collection = vector_store.get_collection(collection_name="test_collection", record_type=SimpleDataModel) await collection.upsert([ From e2375863ab63b347a9c680343c8b9c7f3780cf40 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Fri, 16 Oct 2026 17:45:20 +0200 Subject: [PATCH 2/4] Python: validate PostgreSQL generic field types --- python/semantic_kernel/connectors/postgres.py | 51 +++++++++++++++---- .../connectors/memory/test_postgres_store.py | 8 ++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/python/semantic_kernel/connectors/postgres.py b/python/semantic_kernel/connectors/postgres.py index 131cec3c4c88..266c9ce2c81e 100644 --- a/python/semantic_kernel/connectors/postgres.py +++ b/python/semantic_kernel/connectors/postgres.py @@ -4,7 +4,6 @@ import json import logging import random -import re import string import sys from collections.abc import AsyncGenerator, MutableSequence, Sequence @@ -106,13 +105,45 @@ "bytes": "BYTEA", "bytea": "BYTEA", } -LIST_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile(r"list\s*\[(.+)\]", re.IGNORECASE) -DICT_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile(r"dict\s*\[(.+),\s*(.+)\]", re.IGNORECASE) -ARRAY_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile(r"(.+)\[\]") # region: Helpers +def _get_python_generic_arguments(type_name: str, generic_name: str, argument_count: int) -> tuple[str, ...] | None: + """Get validated arguments from a Python generic type annotation.""" + try: + expression = ast.parse(type_name, mode="eval").body + except SyntaxError: + return None + + if not isinstance(expression, ast.Subscript): + return None + + if isinstance(expression.value, ast.Name): + expression_name = expression.value.id + else: + return None + + arguments = expression.slice.elts if isinstance(expression.slice, ast.Tuple) else [expression.slice] + if expression_name.casefold() != generic_name or len(arguments) != argument_count: + return None + + def is_valid_annotation(node: ast.expr) -> bool: + if isinstance(node, ast.Name): + return True + if isinstance(node, ast.Attribute): + return is_valid_annotation(node.value) + if isinstance(node, ast.Subscript): + values = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice] + return is_valid_annotation(node.value) and all(is_valid_annotation(value) for value in values) + return False + + if not all(is_valid_annotation(argument) for argument in arguments): + return None + + return tuple(ast.unparse(argument) for argument in arguments) + + def _get_postgres_type(type_name: str) -> sql.Composable: """Convert a supported field type to a trusted PostgreSQL type expression. @@ -129,14 +160,14 @@ def _get_postgres_type(type_name: str) -> sql.Composable: if postgres_type := POSTGRES_TYPE_ALIASES.get(normalized_type.casefold()): return sql.SQL(postgres_type) - if match := LIST_TYPE_PATTERN.fullmatch(normalized_type): - return sql.SQL("{}[]").format(_get_postgres_type(match.group(1))) + if normalized_type.endswith("[]"): + return sql.SQL("{}[]").format(_get_postgres_type(normalized_type[:-2])) - if DICT_TYPE_PATTERN.fullmatch(normalized_type): - return sql.SQL("JSONB") + if list_arguments := _get_python_generic_arguments(normalized_type, "list", 1): + return sql.SQL("{}[]").format(_get_postgres_type(list_arguments[0])) - if match := ARRAY_TYPE_PATTERN.fullmatch(normalized_type): - return sql.SQL("{}[]").format(_get_postgres_type(match.group(1))) + if _get_python_generic_arguments(normalized_type, "dict", 2): + return sql.SQL("JSONB") raise VectorStoreModelValidationError( f"PostgreSQL field type {type_name!r} is not supported for automatic collection creation. " diff --git a/python/tests/unit/connectors/memory/test_postgres_store.py b/python/tests/unit/connectors/memory/test_postgres_store.py index b60e1ad59af7..feba65b04f5c 100644 --- a/python/tests/unit/connectors/memory/test_postgres_store.py +++ b/python/tests/unit/connectors/memory/test_postgres_store.py @@ -219,6 +219,8 @@ async def test_ensure_collection_exists_explicit_definition_with_supported_alias VectorStoreField("data", name="tags", type="list[str]"), VectorStoreField("data", name="weights", type="DOUBLE PRECISION[]"), VectorStoreField("data", name="items", type="Dict[str, Any]"), + VectorStoreField("data", name="item_history", type="Dict[str, Any][]"), + VectorStoreField("data", name="nested_item_history", type="dict[str, list[int]][][]"), ] ) collection = vector_store.get_collection( @@ -235,7 +237,8 @@ async def test_ensure_collection_exists_explicit_definition_with_supported_alias 'CREATE TABLE "public"."test_collection" ' '("id" INTEGER PRIMARY KEY, "title" TEXT, "count" INTEGER, "score" DOUBLE PRECISION, ' '"enabled" BOOLEAN, "metadata" JSONB, "created_at" TIMESTAMP, "payload" BYTEA, ' - '"tags" TEXT[], "weights" DOUBLE PRECISION[], "items" JSONB)' + '"tags" TEXT[], "weights" DOUBLE PRECISION[], "items" JSONB, "item_history" JSONB[], ' + '"nested_item_history" JSONB[][])' ) @@ -251,6 +254,9 @@ async def test_ensure_collection_exists_explicit_definition_with_supported_alias "INTEGER[", "INTEGER[] trailing", "list[custom_type]", + "Dict[str, Any]]", + "list[Dict[str, Any]]]", + "dict[str, Any -- comment]", ], ) async def test_ensure_collection_exists_rejects_unsupported_types_before_execution( From cd792cfe2123f82911c64eff2b197e4f859be831 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Mon, 19 Oct 2026 09:20:37 +0200 Subject: [PATCH 3/4] Python: preserve common PostgreSQL field types --- python/semantic_kernel/connectors/postgres.py | 32 ++++++++++- .../connectors/memory/test_postgres_store.py | 54 +++++++++++++++++-- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/python/semantic_kernel/connectors/postgres.py b/python/semantic_kernel/connectors/postgres.py index 266c9ce2c81e..48381f112659 100644 --- a/python/semantic_kernel/connectors/postgres.py +++ b/python/semantic_kernel/connectors/postgres.py @@ -4,6 +4,7 @@ import json import logging import random +import re import string import sys from collections.abc import AsyncGenerator, MutableSequence, Sequence @@ -94,17 +95,32 @@ "text": "TEXT", "int": "INTEGER", "integer": "INTEGER", + "bigint": "BIGINT", + "smallint": "SMALLINT", "float": "DOUBLE PRECISION", "double precision": "DOUBLE PRECISION", + "real": "REAL", "bool": "BOOLEAN", "boolean": "BOOLEAN", "dict": "JSONB", "jsonb": "JSONB", + "date": "DATE", "datetime": "TIMESTAMP", "timestamp": "TIMESTAMP", + "timestamptz": "TIMESTAMP WITH TIME ZONE", + "timestamp with time zone": "TIMESTAMP WITH TIME ZONE", + "uuid": "UUID", + "decimal": "DECIMAL", + "numeric": "NUMERIC", "bytes": "BYTEA", "bytea": "BYTEA", } +LENGTH_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile( + r"(varchar|character varying)\s*\(\s*(\d+)\s*\)", re.IGNORECASE +) +PRECISION_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile( + r"(decimal|numeric)\s*\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\)", re.IGNORECASE +) # region: Helpers @@ -163,6 +179,17 @@ def _get_postgres_type(type_name: str) -> sql.Composable: if normalized_type.endswith("[]"): return sql.SQL("{}[]").format(_get_postgres_type(normalized_type[:-2])) + if match := LENGTH_TYPE_PATTERN.fullmatch(normalized_type): + postgres_type = "VARCHAR" if match.group(1).casefold() == "varchar" else "CHARACTER VARYING" + return sql.SQL("{}({})").format(sql.SQL(postgres_type), sql.Literal(int(match.group(2)))) + + if match := PRECISION_TYPE_PATTERN.fullmatch(normalized_type): + postgres_type = POSTGRES_TYPE_ALIASES[match.group(1).casefold()] + modifiers = [sql.Literal(int(match.group(2)))] + if scale := match.group(3): + modifiers.append(sql.Literal(int(scale))) + return sql.SQL("{}({})").format(sql.SQL(postgres_type), sql.SQL(", ").join(modifiers)) + if list_arguments := _get_python_generic_arguments(normalized_type, "list", 1): return sql.SQL("{}[]").format(_get_postgres_type(list_arguments[0])) @@ -601,8 +628,9 @@ async def ensure_collection_exists(self, **kwargs: Any) -> None: """Create a PostgreSQL table based on the collection definition. Automatic creation supports built-in Python field types, their corresponding PostgreSQL aliases, - and arrays of those types. For custom domains, enums, extension types, or type modifiers, create the - table separately and use the collection without calling this method. + validated length and precision modifiers, and arrays of those types. For custom domains, enums, + extension types, or other type expressions, create the table separately and use the collection without + calling this method. Args: **kwargs: Additional arguments. diff --git a/python/tests/unit/connectors/memory/test_postgres_store.py b/python/tests/unit/connectors/memory/test_postgres_store.py index feba65b04f5c..1f6f6c378fd0 100644 --- a/python/tests/unit/connectors/memory/test_postgres_store.py +++ b/python/tests/unit/connectors/memory/test_postgres_store.py @@ -4,8 +4,11 @@ import asyncio from collections.abc import AsyncGenerator from dataclasses import dataclass +from datetime import date +from decimal import Decimal from typing import Annotated, Any from unittest.mock import AsyncMock, MagicMock, Mock, patch +from uuid import UUID import pytest import pytest_asyncio @@ -203,6 +206,32 @@ class ModelWithImplicitTypes: ) +async def test_ensure_collection_exists_model_with_common_postgres_types( + vector_store: PostgresStore, mock_cursor: Mock +) -> None: + @vectorstoremodel + @dataclass + class ModelWithCommonPostgresTypes: + name: Annotated[str, VectorStoreField("key")] + event_date: Annotated[date, VectorStoreField("data")] + external_id: Annotated[UUID, VectorStoreField("data")] + amount: Annotated[Decimal, VectorStoreField("data")] + + collection = vector_store.get_collection( + collection_name="test_collection", + record_type=ModelWithCommonPostgresTypes, + ) + + await collection.ensure_collection_exists() + + mock_cursor.execute.assert_awaited_once() + statement = mock_cursor.execute.call_args.args[0] + assert statement.as_string() == ( + 'CREATE TABLE "public"."test_collection" ' + '("name" TEXT PRIMARY KEY, "event_date" DATE, "external_id" UUID, "amount" DECIMAL)' + ) + + async def test_ensure_collection_exists_explicit_definition_with_supported_aliases( vector_store: PostgresStore, mock_cursor: Mock ) -> None: @@ -211,12 +240,21 @@ async def test_ensure_collection_exists_explicit_definition_with_supported_alias VectorStoreField("key", name="id", type="int"), VectorStoreField("data", name="title", type="text"), VectorStoreField("data", name="count", type="INTEGER"), + VectorStoreField("data", name="large_count", type="bigint"), + VectorStoreField("data", name="small_count", type="SMALLINT"), VectorStoreField("data", name="score", type="double precision"), + VectorStoreField("data", name="approximate_score", type="real"), VectorStoreField("data", name="enabled", type="BOOLEAN"), VectorStoreField("data", name="metadata", type="jsonb"), VectorStoreField("data", name="created_at", type="TIMESTAMP"), + VectorStoreField("data", name="processed_at", type="timestamptz"), VectorStoreField("data", name="payload", type="bytea"), + VectorStoreField("data", name="summary", type="varchar(255)"), + VectorStoreField("data", name="description", type="character varying ( 1024 )"), + VectorStoreField("data", name="amount", type="numeric(10, 2)"), + VectorStoreField("data", name="ratio", type="decimal(6)"), VectorStoreField("data", name="tags", type="list[str]"), + VectorStoreField("data", name="codes", type="varchar(12)[]"), VectorStoreField("data", name="weights", type="DOUBLE PRECISION[]"), VectorStoreField("data", name="items", type="Dict[str, Any]"), VectorStoreField("data", name="item_history", type="Dict[str, Any][]"), @@ -235,10 +273,12 @@ async def test_ensure_collection_exists_explicit_definition_with_supported_alias statement = mock_cursor.execute.call_args.args[0] assert statement.as_string() == ( 'CREATE TABLE "public"."test_collection" ' - '("id" INTEGER PRIMARY KEY, "title" TEXT, "count" INTEGER, "score" DOUBLE PRECISION, ' - '"enabled" BOOLEAN, "metadata" JSONB, "created_at" TIMESTAMP, "payload" BYTEA, ' - '"tags" TEXT[], "weights" DOUBLE PRECISION[], "items" JSONB, "item_history" JSONB[], ' - '"nested_item_history" JSONB[][])' + '("id" INTEGER PRIMARY KEY, "title" TEXT, "count" INTEGER, "large_count" BIGINT, ' + '"small_count" SMALLINT, "score" DOUBLE PRECISION, "approximate_score" REAL, "enabled" BOOLEAN, ' + '"metadata" JSONB, "created_at" TIMESTAMP, "processed_at" TIMESTAMP WITH TIME ZONE, "payload" BYTEA, ' + '"summary" VARCHAR(255), "description" CHARACTER VARYING(1024), "amount" NUMERIC(10, 2), ' + '"ratio" DECIMAL(6), "tags" TEXT[], "codes" VARCHAR(12)[], "weights" DOUBLE PRECISION[], ' + '"items" JSONB, "item_history" JSONB[], "nested_item_history" JSONB[][])' ) @@ -257,6 +297,12 @@ async def test_ensure_collection_exists_explicit_definition_with_supported_alias "Dict[str, Any]]", "list[Dict[str, Any]]]", "dict[str, Any -- comment]", + "varchar()", + "varchar(255, 1)", + "varchar(255); SELECT 1", + "numeric()", + "numeric(10, 2, 1)", + "numeric(10, scale)", ], ) async def test_ensure_collection_exists_rejects_unsupported_types_before_execution( From 09a64b5853f6028fd4ae2050f63c523af27a3f90 Mon Sep 17 00:00:00 2001 From: Jose Alvarez Date: Mon, 19 Oct 2026 09:21:17 +0200 Subject: [PATCH 4/4] Python: bound PostgreSQL type modifiers --- python/semantic_kernel/connectors/postgres.py | 4 ++-- python/tests/unit/connectors/memory/test_postgres_store.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/connectors/postgres.py b/python/semantic_kernel/connectors/postgres.py index 48381f112659..e5195bfd6ef5 100644 --- a/python/semantic_kernel/connectors/postgres.py +++ b/python/semantic_kernel/connectors/postgres.py @@ -116,10 +116,10 @@ "bytea": "BYTEA", } LENGTH_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile( - r"(varchar|character varying)\s*\(\s*(\d+)\s*\)", re.IGNORECASE + r"(varchar|character varying)\s*\(\s*(\d{1,8})\s*\)", re.IGNORECASE ) PRECISION_TYPE_PATTERN: Final[re.Pattern[str]] = re.compile( - r"(decimal|numeric)\s*\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\)", re.IGNORECASE + r"(decimal|numeric)\s*\(\s*(\d{1,4})(?:\s*,\s*(\d{1,4}))?\s*\)", re.IGNORECASE ) # region: Helpers diff --git a/python/tests/unit/connectors/memory/test_postgres_store.py b/python/tests/unit/connectors/memory/test_postgres_store.py index 1f6f6c378fd0..0ddf51ef2b72 100644 --- a/python/tests/unit/connectors/memory/test_postgres_store.py +++ b/python/tests/unit/connectors/memory/test_postgres_store.py @@ -300,9 +300,11 @@ async def test_ensure_collection_exists_explicit_definition_with_supported_alias "varchar()", "varchar(255, 1)", "varchar(255); SELECT 1", + "varchar(123456789)", "numeric()", "numeric(10, 2, 1)", "numeric(10, scale)", + "numeric(12345)", ], ) async def test_ensure_collection_exists_rejects_unsupported_types_before_execution(