From 4b127618f710ceb0947ff2e912664f864189d097 Mon Sep 17 00:00:00 2001 From: juankx-bodo Date: Tue, 9 Sep 2025 10:55:37 -0600 Subject: [PATCH 1/2] Improve metadata error messages and validations Improve metadata error messages Improve metadata validations Check for reserved python keywords on metadata names Fix metadata for broker Check for Pydough and SQL reserved words Add validation for SQL names in TablePath & ColumnName Apply code review observations --- pydough/errors/error_utils.py | 187 ++++++++++- .../collections/collection_metadata.py | 4 +- .../collections/simple_table_metadata.py | 6 +- pydough/metadata/graphs/graph_metadata.py | 4 +- .../metadata/properties/property_metadata.py | 4 +- .../properties/table_column_metadata.py | 2 + tests/test_metadata/defog_graphs.json | 2 +- tests/test_metadata/invalid_graphs.json | 314 +++++++++++++++++- tests/test_metadata/mysql_defog_graphs.json | 2 +- .../test_metadata/snowflake_defog_graphs.json | 2 +- tests/test_metadata_errors.py | 82 ++++- 11 files changed, 586 insertions(+), 23 deletions(-) diff --git a/pydough/errors/error_utils.py b/pydough/errors/error_utils.py index b2956ce53..098ff223c 100644 --- a/pydough/errors/error_utils.py +++ b/pydough/errors/error_utils.py @@ -27,11 +27,15 @@ "is_positive_int", "is_string", "is_valid_name", + "is_valid_sql_name", "simple_join_keys_predicate", "unique_properties_predicate", ] +import builtins +import keyword +import re from abc import ABC, abstractmethod import numpy as np @@ -97,11 +101,189 @@ class ValidName(PyDoughPredicate): as the name of a PyDough graph/collection/property. """ + def __init__(self): + self.error_messages: dict[str, str] = { + "identifier": "must be a string that is a valid Python identifier", + "python_keyword": "must be a string that is not a Python reserved word or built-in name", + "pydough_keyword": "must be a string that is not a PyDough reserved word", + "sql_keyword": "must be a string that is not a SQL reserved word", + } + + def _error_code(self, obj: object) -> str | None: + """Return an error code if invalid, or None if valid.""" + ret_val: str | None = None + # Check that obj is a string + if isinstance(obj, str): + # Check that obj is a valid Python identifier + if not obj.isidentifier(): + ret_val = "identifier" + # Check that obj is not a Python reserved word or built-in name + elif keyword.iskeyword(obj) or hasattr(builtins, obj): + ret_val = "python_keyword" + # Check that obj is not a PyDough reserved word + elif self._is_pydough_keyword(obj): + ret_val = "pydough_keyword" + else: + ret_val = "identifier" + + return ret_val + + def _is_pydough_keyword(self, name: str) -> bool: + """ + helper: Verifies if name is a PyDough reserved word. + Extend with new PyDough reserved words if required. + """ + # Dictionary of all registered operators pre-built from the PyDough source + from pydough.pydough_operators import builtin_registered_operators + + # Set of collection operators + PYDOUGH_RESERVED: set[str] = { + "CALCULATE", + "WHERE", + "ORDER_BY", + "TOP_K", + "PARTITION", + "SINGULAR", + "BEST", + "CROSS", + } + return (name in PYDOUGH_RESERVED) or (name in builtin_registered_operators()) + def accept(self, obj: object) -> bool: - return isinstance(obj, str) and obj.isidentifier() + return self._error_code(obj) is None def error_message(self, error_name: str) -> str: - return f"{error_name} must be a string that is a Python identifier" + # Generic fallback (since we don't have the object here) + return f"{error_name} must be a valid identifier and not a reserved word" + + def verify(self, obj: object, error_name: str) -> None: + code: str | None = self._error_code(obj) + if code is not None: + raise PyDoughMetadataException(f"{error_name} {self.error_messages[code]}") + + +class ValidSQLName(PyDoughPredicate): + """Predicate class to check that an object is a string that can be used + as the name for a SQL table path/column name. + """ + + # Regex for unquoted SQL identifiers + _UNQUOTED_SQL_IDENTIFIER = re.compile( + r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$" + ) + + def __init__(self): + self.error_messages: dict[str, str] = { + "identifier": "must have a SQL name that is a valid SQL identifier", + "sql_keyword": "must have a SQL name that is not a reserved word", + } + + def _error_code(self, obj: object) -> str | None: + """Return an error code if invalid, or None if valid.""" + ret_val: str | None = None + # Check that obj is a string + if isinstance(obj, str): + # Check that obj is a valid SQL identifier + if not self.is_valid_sql_identifier(obj): + ret_val = "identifier" + # Check that obj is not a SQL reserved word + elif self._is_sql_keyword(obj): + ret_val = "sql_keyword" + else: + ret_val = "identifier" + + return ret_val + + def is_valid_sql_identifier(self, name: str) -> bool: + """ + Check if a string is a valid SQL identifier. + + - Unquoted: starts with letter/underscore, then letters, digits, + underscores. + - Double-quoted: allows any chars, but " "" " is the only valid way to + include a double-quote char. + - Backtick-quoted: allows any chars, but `` `` `` is the only valid + way to include a backtick char. + """ + if not name: + return False + + # Case 1: unquoted + if self._UNQUOTED_SQL_IDENTIFIER.match(name): + return True + + # Case 2: double quoted + if name.startswith('"') and name.endswith('"'): + inner = name[1:-1] + # Any " must be escaped as "" + return '"' not in inner.replace('""', "") + + # Case 3: backtick quoted + if name.startswith("`") and name.endswith("`"): + inner = name[1:-1] + # Any ` must be escaped as `` + return "`" not in inner.replace("``", "") + + return False + + # fmt: off + SQL_RESERVED_KEYWORDS: set[str] = { + # Query & DML + "select", "from", "where", "group", "having", "distinct", "as", + "join", "inner", "union", "intersect", "except", + + # DDL & schema + "create", "alter", "drop", "table", "view", "index", "sequence", + "trigger", "schema", "database", "column", "constraint", + + # DML + "insert", "update", "delete", "into", "values", "set", + + # Control flow & logical + "and", "or", "not", "in", "is", "like", "between", "case", "when", + "then", "else", "end", "exists", + + # Transaction & session + "begin", "commit", "rollback", "savepoint", "transaction", + "lock", "grant", "revoke", + + # Data types + "int", "integer", "bigint", "smallint", "decimal", "numeric", + "float", "real", "double", "char", "varchar", "text", + "timestamp", "boolean", "null", + + # Functions + "cast", + } + """ + Set of SQL reserved keywords that may cause conflicts when used as table or + column names. This list was compiled from commonly reserved terms across + multiple SQL dialects (e.g., PostgreSQL, SQLite, MySQL), with emphasis on + keywords that are likely to appear in generated SQL statements. + If any of these are used as identifiers, they must be properly escaped to + avoid syntax errors. + """ + # fmt: on + + def _is_sql_keyword(self, name: str) -> bool: + """ + helper: Verifies if name is a SQL reserved word. + Uses SQL_RESERVED_KEYWORDS set. + Extend with new SQL reserved words if required. + """ + return name.lower() in self.SQL_RESERVED_KEYWORDS + + def accept(self, obj: object) -> bool: + return self._error_code(obj) is None + + def error_message(self, error_name: str) -> str: + # Generic fallback (since we don't have the object here) + return f"{error_name} must be a valid SQL identifier and not a reserved word" + + def verify(self, obj: object, error_name: str) -> None: + code: str | None = self._error_code(obj) + if code is not None: + raise PyDoughMetadataException(f"{error_name} {self.error_messages[code]}") class NoExtraKeys(PyDoughPredicate): @@ -304,6 +486,7 @@ def error_message(self, error_name: str) -> str: ############################################################################### is_valid_name: PyDoughPredicate = ValidName() +is_valid_sql_name: PyDoughPredicate = ValidSQLName() is_integer = HasType(int, "integer") is_string = HasType(str, "string") is_bool = HasType(bool, "boolean") diff --git a/pydough/metadata/collections/collection_metadata.py b/pydough/metadata/collections/collection_metadata.py index ffe31624f..a612d63e1 100644 --- a/pydough/metadata/collections/collection_metadata.py +++ b/pydough/metadata/collections/collection_metadata.py @@ -48,8 +48,8 @@ def __init__( PropertyMetadata, ) - is_valid_name.verify(name, "name") - HasType(GraphMetadata).verify(graph, "graph") + is_valid_name.verify(name, f"collection name {name!r}") + HasType(GraphMetadata).verify(graph, f"graph {name!r}") self._graph: GraphMetadata = graph self._name: str = name diff --git a/pydough/metadata/collections/simple_table_metadata.py b/pydough/metadata/collections/simple_table_metadata.py index deadb374a..14940646b 100644 --- a/pydough/metadata/collections/simple_table_metadata.py +++ b/pydough/metadata/collections/simple_table_metadata.py @@ -10,6 +10,7 @@ extract_array, extract_string, is_string, + is_valid_sql_name, unique_properties_predicate, ) from pydough.metadata.graphs import GraphMetadata @@ -146,6 +147,7 @@ def parse_from_json( # Extract the relevant properties from the JSON to build the new # collection, then add it to the graph. table_path: str = extract_string(collection_json, "table path", error_name) + is_valid_sql_name.verify(table_path, error_name) HasPropertyWith("unique properties", unique_properties_predicate).verify( collection_json, error_name ) @@ -161,8 +163,6 @@ def parse_from_json( ) # Parse the optional common semantic properties like the description. new_collection.parse_optional_properties(collection_json) - properties: list = extract_array( - collection_json, "properties", new_collection.error_name - ) + properties: list = extract_array(collection_json, "properties", error_name) new_collection.add_properties_from_json(properties) graph.add_collection(new_collection) diff --git a/pydough/metadata/graphs/graph_metadata.py b/pydough/metadata/graphs/graph_metadata.py index b23e6f2be..a47b4eb5e 100644 --- a/pydough/metadata/graphs/graph_metadata.py +++ b/pydough/metadata/graphs/graph_metadata.py @@ -43,7 +43,7 @@ def __init__( synonyms: list[str] | None, extra_semantic_info: dict | None, ): - is_valid_name.verify(name, "graph name") + is_valid_name.verify(name, f"graph name {name!r}") self._additional_definitions: list[str] | None = additional_definitions self._verified_pydough_analysis: list[dict] | None = verified_pydough_analysis self._name: str = name @@ -179,7 +179,7 @@ def add_function(self, name: str, function: "ExpressionFunctionOperator") -> Non `PyDoughMetadataException`: if `function` cannot be inserted into the graph because of a name collision. """ - is_valid_name.verify(name, "function name") + is_valid_name.verify(name, f"function name {name!r}") if name == self.name: raise PyDoughMetadataException( f"Function name {name!r} cannot be the same as the graph name {self.name!r}" diff --git a/pydough/metadata/properties/property_metadata.py b/pydough/metadata/properties/property_metadata.py index b5a077878..8a8e47c22 100644 --- a/pydough/metadata/properties/property_metadata.py +++ b/pydough/metadata/properties/property_metadata.py @@ -45,8 +45,8 @@ def __init__( synonyms: list[str] | None, extra_semantic_info: dict | None, ): - is_valid_name.verify(name, "name") - HasType(CollectionMetadata).verify(collection, "collection") + is_valid_name.verify(name, f"property name {name!r}") + HasType(CollectionMetadata).verify(collection, f"collection {name}") self._name: str = name self._collection: CollectionMetadata = collection super().__init__(description, synonyms, extra_semantic_info) diff --git a/pydough/metadata/properties/table_column_metadata.py b/pydough/metadata/properties/table_column_metadata.py index d296f3cd9..da6c49098 100644 --- a/pydough/metadata/properties/table_column_metadata.py +++ b/pydough/metadata/properties/table_column_metadata.py @@ -11,6 +11,7 @@ NoExtraKeys, extract_string, is_string, + is_valid_sql_name, ) from pydough.metadata.collections import CollectionMetadata from pydough.types import PyDoughType, parse_type_from_string @@ -99,6 +100,7 @@ def parse_from_json( except PyDoughTypeException as e: raise PyDoughMetadataException(*e.args) column_name: str = extract_string(property_json, "column name", error_name) + is_valid_sql_name.verify(column_name, error_name) NoExtraKeys(TableColumnMetadata.allowed_fields).verify( property_json, error_name diff --git a/tests/test_metadata/defog_graphs.json b/tests/test_metadata/defog_graphs.json index c2ed72368..09c2860db 100644 --- a/tests/test_metadata/defog_graphs.json +++ b/tests/test_metadata/defog_graphs.json @@ -223,7 +223,7 @@ "synonyms": ["record datetime", "price update date"] }, { - "name": "open", + "name": "_open", "type": "table column", "column name": "sbDpOpen", "data type": "numeric", diff --git a/tests/test_metadata/invalid_graphs.json b/tests/test_metadata/invalid_graphs.json index bde86d9b0..2ba692036 100644 --- a/tests/test_metadata/invalid_graphs.json +++ b/tests/test_metadata/invalid_graphs.json @@ -52,7 +52,7 @@ "version": "V2", "collections": [ { - "name": "0", + "name": "Invalid name", "type": "simple table", "table path": "TableName", "unique properties": ["key"], @@ -66,7 +66,7 @@ "version": "V2", "collections": [ { - "name": "0", + "name": "table_name", "type": "simple table", "table path": "TableName", "unique properties": ["key"], @@ -84,7 +84,7 @@ { "name": "0", "type": "simple table", - "table path": "TableName", + "table path": "`Table Name`", "unique properties": ["key"], "properties": [ {"name": true} @@ -93,6 +93,314 @@ ], "relationships": [] }, + { + "name": "BAD_TABLE_NAME", + "version": "V2", + "collections": [ + { + "name": "invalid table_name", + "type": "simple table", + "table path": "TableName", + "unique properties": ["column_name"], + "properties": [ + { + "name": "column_name", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "BAD_COLUMN_NAME", + "version": "V2", + "collections": [ + { + "name": "collection", + "type": "simple table", + "table path": "TableName", + "unique properties": ["key"], + "properties": [ + { + "name": "invalid column name", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "BAD_UNIQUE_PROPERTY", + "version": "V2", + "collections": [ + { + "name": "collection", + "type": "simple table", + "table path": "TableName", + "unique properties": ["invalid column name"], + "properties": [ + { + "name": "invalid column name", + "type": "table column", + "column name": "\"column name\"", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "RESERVED_PROPERTY_KEYWORD_1", + "version": "V2", + "collections": [ + { + "name": "collection", + "type": "simple table", + "table path": "\"Table Name\"", + "unique properties": ["id"], + "properties": [ + { + "name": "id", + "type": "table column", + "column name": "id", + "data type": "numeric", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "RESERVED_PROPERTY_KEYWORD_2", + "version": "V2", + "collections": [ + { + "name": "table_name", + "type": "simple table", + "table path": "`Table Name`", + "unique properties": ["CALCULATE"], + "properties": [ + { + "name": "CALCULATE", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "RESERVED_PROPERTY_KEYWORD_3", + "version": "V2", + "collections": [ + { + "name": "table_name", + "type": "simple table", + "table path": "table_name", + "unique properties": ["MONOTONIC"], + "properties": [ + { + "name": "MONOTONIC", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "COLUMN_NAME_RESERVED_KEYWORD", + "version": "V2", + "collections": [ + { + "name": "table_name", + "type": "simple table", + "table path": "TableName", + "unique properties": ["FROM_RESERVED_SQL_WORD"], + "properties": [ + { + "name": "FROM_RESERVED_SQL_WORD", + "type": "table column", + "column name": "FROM", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "TABLE_PATH_RESERVED_KEYWORD", + "version": "V2", + "collections": [ + { + "name": "cast_reserved_sql_word", + "type": "simple table", + "table path": "cast", + "unique properties": ["_id"], + "properties": [ + { + "name": "_id", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "COLUMN_NAME_INVALID_NAME_1", + "version": "V2", + "collections": [ + { + "name": "table_name", + "type": "simple table", + "table path": "TableName", + "unique properties": ["FROM_RESERVED_SQL_WORD"], + "properties": [ + { + "name": "invalid_column_name", + "type": "table column", + "column name": "column name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "COLUMN_NAME_INVALID_NAME_2", + "version": "V2", + "collections": [ + { + "name": "table_name", + "type": "simple table", + "table path": "TableName", + "unique properties": ["FROM_RESERVED_SQL_WORD"], + "properties": [ + { + "name": "invalid_column_name", + "type": "table column", + "column name": "\"column\"name\"", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "COLUMN_NAME_INVALID_NAME_3", + "version": "V2", + "collections": [ + { + "name": "table_name", + "type": "simple table", + "table path": "TableName", + "unique properties": ["FROM_RESERVED_SQL_WORD"], + "properties": [ + { + "name": "invalid_column_name", + "type": "table column", + "column name": "`column`name`", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "TABLE_PATH_INVALID_NAME_1", + "version": "V2", + "collections": [ + { + "name": "invalid_table_path", + "type": "simple table", + "table path": "table name", + "unique properties": ["_id"], + "properties": [ + { + "name": "_id", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "TABLE_PATH_INVALID_NAME_2", + "version": "V2", + "collections": [ + { + "name": "invalid_table_path", + "type": "simple table", + "table path": "\"table\"name\"", + "unique properties": ["_id"], + "properties": [ + { + "name": "_id", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, + { + "name": "TABLE_PATH_INVALID_NAME_3", + "version": "V2", + "collections": [ + { + "name": "invalid_table_path", + "type": "simple table", + "table path": "`table`name`", + "unique properties": ["_id"], + "properties": [ + { + "name": "_id", + "type": "table column", + "column name": "column_name", + "data type": "string", + "description": "column description" + } + ] + } + ], + "relationships": [] + }, { "name": "BAD_RELATIONSHIP_NAME", "version": "V2", diff --git a/tests/test_metadata/mysql_defog_graphs.json b/tests/test_metadata/mysql_defog_graphs.json index 7b58c3cf5..159682847 100644 --- a/tests/test_metadata/mysql_defog_graphs.json +++ b/tests/test_metadata/mysql_defog_graphs.json @@ -223,7 +223,7 @@ "synonyms": ["record datetime", "price update date"] }, { - "name": "open", + "name": "_open", "type": "table column", "column name": "sbDpOpen", "data type": "numeric", diff --git a/tests/test_metadata/snowflake_defog_graphs.json b/tests/test_metadata/snowflake_defog_graphs.json index 3a1007b13..97568aead 100644 --- a/tests/test_metadata/snowflake_defog_graphs.json +++ b/tests/test_metadata/snowflake_defog_graphs.json @@ -223,7 +223,7 @@ "synonyms": ["record datetime", "price update date"] }, { - "name": "open", + "name": "_open", "type": "table column", "column name": "sbDpOpen", "data type": "numeric", diff --git a/tests/test_metadata_errors.py b/tests/test_metadata_errors.py index 85793a47f..77a572171 100644 --- a/tests/test_metadata_errors.py +++ b/tests/test_metadata_errors.py @@ -53,7 +53,7 @@ def test_missing_property(get_sample_graph: graph_fetcher) -> None: ), pytest.param( "#BadGraphName", - "graph name must be a string that is a Python identifier", + "graph name '#BadGraphName' must be a string that is a valid Python identifier", id="#BadGraphName", ), pytest.param( @@ -85,24 +85,94 @@ def test_missing_property(get_sample_graph: graph_fetcher) -> None: ), pytest.param( "BAD_COLLECTION_NAME_1", - "name must be a string that is a Python identifier", + "collection name '0' must be a string that is a valid Python identifier", id="BAD_COLLECTION_NAME_1", ), pytest.param( "BAD_COLLECTION_NAME_2", - "name must be a string that is a Python identifier", + "collection name 'Invalid name' must be a string that is a valid Python identifier", id="BAD_COLLECTION_NAME_2", ), pytest.param( "BAD_PROPERTY_NAME_1", - "name must be a string that is a Python identifier", + "property 'bad name' of simple table collection 'table_name' in graph 'BAD_PROPERTY_NAME_1' must be a JSON object containing a field 'type' and field 'type' must be a string", id="BAD_PROPERTY_NAME_1", ), pytest.param( "BAD_PROPERTY_NAME_2", - "name must be a string that is a Python identifier", + "collection name '0' must be a string that is a valid Python identifier", id="BAD_PROPERTY_NAME_2", ), + pytest.param( + "BAD_TABLE_NAME", + "collection name 'invalid table_name' must be a string that is a valid Python identifier", + id="BAD_TABLE_NAME", + ), + pytest.param( + "BAD_COLUMN_NAME", + "property name 'invalid column name' must be a string that is a valid Python identifier", + id="BAD_COLUMN_NAME", + ), + pytest.param( + "BAD_UNIQUE_PROPERTY", + "property name 'invalid column name' must be a string that is a valid Python identifier", + id="BAD_UNIQUE_PROPERTY", + ), + pytest.param( + "RESERVED_PROPERTY_KEYWORD_1", + "property name 'id' must be a string that is not a Python reserved word or built-in name", + id="RESERVED_PROPERTY_KEYWORD_1", + ), + pytest.param( + "RESERVED_PROPERTY_KEYWORD_2", + "property name 'CALCULATE' must be a string that is not a PyDough reserved word", + id="RESERVED_PROPERTY_KEYWORD_2", + ), + pytest.param( + "RESERVED_PROPERTY_KEYWORD_3", + "property name 'MONOTONIC' must be a string that is not a PyDough reserved word", + id="RESERVED_PROPERTY_KEYWORD_3", + ), + pytest.param( + "COLUMN_NAME_RESERVED_KEYWORD", + "table column property 'FROM_RESERVED_SQL_WORD' of simple table collection 'table_name' in graph 'COLUMN_NAME_RESERVED_KEYWORD' must have a SQL name that is not a reserved word", + id="COLUMN_NAME_RESERVED_KEYWORD", + ), + pytest.param( + "TABLE_PATH_RESERVED_KEYWORD", + "simple table collection 'cast_reserved_sql_word' in graph 'TABLE_PATH_RESERVED_KEYWORD' must have a SQL name that is not a reserved word", + id="TABLE_PATH_RESERVED_KEYWORD", + ), + pytest.param( + "COLUMN_NAME_INVALID_NAME_1", + "table column property 'invalid_column_name' of simple table collection 'table_name' in graph 'COLUMN_NAME_INVALID_NAME_1' must have a SQL name that is a valid SQL identifier", + id="COLUMN_NAME_INVALID_NAME_1", + ), + pytest.param( + "COLUMN_NAME_INVALID_NAME_2", + "table column property 'invalid_column_name' of simple table collection 'table_name' in graph 'COLUMN_NAME_INVALID_NAME_2' must have a SQL name that is a valid SQL identifier", + id="COLUMN_NAME_INVALID_NAME_2", + ), + pytest.param( + "COLUMN_NAME_INVALID_NAME_3", + "table column property 'invalid_column_name' of simple table collection 'table_name' in graph 'COLUMN_NAME_INVALID_NAME_3' must have a SQL name that is a valid SQL identifier", + id="COLUMN_NAME_INVALID_NAME_3", + ), + pytest.param( + "TABLE_PATH_INVALID_NAME_1", + "simple table collection 'invalid_table_path' in graph 'TABLE_PATH_INVALID_NAME_1' must have a SQL name that is a valid SQL identifier", + id="TABLE_PATH_INVALID_NAME_1", + ), + pytest.param( + "TABLE_PATH_INVALID_NAME_2", + "simple table collection 'invalid_table_path' in graph 'TABLE_PATH_INVALID_NAME_2' must have a SQL name that is a valid SQL identifier", + id="TABLE_PATH_INVALID_NAME_2", + ), + pytest.param( + "TABLE_PATH_INVALID_NAME_3", + "simple table collection 'invalid_table_path' in graph 'TABLE_PATH_INVALID_NAME_3' must have a SQL name that is a valid SQL identifier", + id="TABLE_PATH_INVALID_NAME_3", + ), pytest.param( "BAD_RELATIONSHIP_NAME", "metadata for relationships within graph 'BAD_RELATIONSHIP_NAME' must be a JSON object containing a field 'type' and field 'type' must be a string", @@ -463,7 +533,7 @@ def test_missing_property(get_sample_graph: graph_fetcher) -> None: ), pytest.param( "BAD_FUNCTION_BAD_NAME_2", - "function name must be a string that is a Python identifier", + "function name '' must be a string that is a valid Python identifier", id="BAD_FUNCTION_BAD_NAME_2", ), pytest.param( From d96e49f166d5ff424115cc5a534a927750505d40 Mon Sep 17 00:00:00 2001 From: juankx-bodo Date: Wed, 24 Sep 2025 23:37:44 -0600 Subject: [PATCH 2/2] [RUN CI]