cocoindex-io/cocoindex · error · ValueError

Invalid

Error message

Invalid {kind}: {name!r}

What it means

Table and schema identifiers used in generated SQL must be plain unquoted SQL identifiers (matching _IDENTIFIER_RE). Because identifier quoting alone cannot neutralize an embedded double-quote, anything that is not a plain identifier is rejected to prevent SQL injection (mirroring the Doris connector's CVE fix).

Solutions

  1. Use a plain identifier: letters, digits, underscores, not starting with a digit.
  2. Pass the schema separately via pg_schema_name instead of 'schema.table'.
  3. Sanitize or reject user-supplied names against a regex like ^[A-Za-z_][A-Za-z0-9_]*$ before calling the API.

Example fix

// before
table_target("orders-2024")
// after
table_target("orders_2024")
Defensive patterns

Strategy: validation

Validate before calling

import re
_ID = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
assert _ID.match(table_name), f"Invalid table name: {table_name!r}"

Type guard

import re
_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
def is_safe_identifier(name: object) -> bool:
    return isinstance(name, str) and bool(_IDENTIFIER_RE.match(name))

Try / catch

try:
    target = table_target(user_supplied_name)
except ValueError as e:
    if e.args and e.args[0].startswith("Invalid "):
        raise ValueError("Table name must be a plain identifier") from e

Prevention

When it happens

Trigger: Calling table_target (or constructing a PgTableTarget) with a table_name or pg_schema_name containing characters like quotes, spaces, dots, or hyphens, or passing a non-string.

Common situations: Building table names by string concatenation from user input; including schema in table_name like 'myschema.table' instead of using pg_schema_name; names copied from quoted DDL.

Related errors


AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08). Data as JSON: /api/errors/44e2327361b8b70d. Report an issue: GitHub.

Appendix: source

Thrown at python/cocoindex/connectors/postgres/_target.py:87

# asyncpg enforces a protocol limit of 32767 bind parameters per query.
_BIND_LIMIT: int = 32767


_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


def _validate_identifier(name: str, kind: str = "identifier") -> None:
    """Reject identifiers outside the unquoted-identifier allow-list.

    PostgreSQL identifiers are quoted with double quotes when interpolated, but
    quoting alone does not prevent injection if the input itself contains a
    double-quote character. Mirroring the Doris connector's approach
    (CVE-2026-28438), we error out immediately on anything that isn't a plain
    unquoted identifier.
    """
    if not isinstance(name, str) or not _IDENTIFIER_RE.match(name):
        raise ValueError(f"Invalid {kind}: {name!r}")


def _qualified_table_name(table_name: str, pg_schema_name: str | None) -> str:
    """Return a properly quoted (optionally schema-qualified) table name."""

    if pg_schema_name:
        return f'"{pg_schema_name}"."{table_name}"'
    return f'"{table_name}"'


class PgType(NamedTuple):
    """
    Annotation to specify a PostgreSQL column type.

    Use with `typing.Annotated` to override the default type mapping:

    ```python
    from typing import Annotated

View on GitHub (pinned to e84aa99b32)