Skip to content

feat(v2): add validate_schema option and skip embedding column in similarity search - #330

Closed
Yuki Harada (yukiharada1228) wants to merge 1 commit into
langchain-ai:mainfrom
yukiharada1228:vectorstore-skip-validation-and-column-select
Closed

Yuki Harada (yukiharada1228) wants to merge 1 commit into
langchain-ai:mainfrom
yukiharada1228:vectorstore-skip-validation-and-column-select

Conversation

@yukiharada1228

Copy link
Copy Markdown
Contributor

Fixes #326.

What

Two small, additive/opt-in changes to AsyncPGVectorStore (and the PGVectorStore sync wrapper, which just forwards to it):

  1. create() gets a validate_schema: bool = True option. When False, it skips the information_schema.columns round trip used to validate id_column/content_column/embedding_column/metadata_columns, trusting the caller-provided configuration instead. This matters for callers that construct a short-lived store per request (e.g. a serverless/edge function that can't hold a long-lived instance across invocations) against a table whose schema is already known to be valid — today that pays for a full schema introspection query on every single request in addition to the actual similarity search query. Can't be combined with ignore_metadata_columns, since resolving that option requires the full column list from information_schema.

  2. __query_collection gets an internal include_embedding flag (default True), and asimilarity_search_with_score_by_vector opts out of it. The embedding column was always included in the SELECT list, even though plain similarity search never reads the embedding value back off the row (the returned Document never carries it) — only amax_marginal_relevance_search_with_score_by_vector actually uses row[self.embedding_column]. For a high-dimensional embedding (e.g. 1536-dim OpenAI) and a typical k, that's several vectors of serialized floats transferred and immediately discarded per plain search. A caller can still opt back in with include_embedding=True.

Neither change touches the SQL for the MMR path, and both preserve existing default behavior everywhere else.

Testing

  • uv sync --group test, then ran the full tests/unit_tests/v2/ suite against a local pgvector/pgvector-equivalent Postgres (POSTGRES_PORT=<local>): 585 passed (582 pre-existing + 3 new).
  • Added test_create_with_validate_schema_false / test_create_with_validate_schema_false_and_ignore_metadata_columns to test_async_pg_vectorstore.py.
  • Added test_query_collection_include_embedding to test_async_pg_vectorstore_search.py.
  • ruff check / ruff format --check / mypy langchain_postgres all clean on the changed files.

Context

I filed #326 after noticing these while porting this package to TypeScript (@yukiharada1228/langchain-postgres, a faithful hand-translation) for use in a Cloudflare Workers app, where a per-request store is unavoidable and the extra round trip is pure overhead on every request. Happy to adjust the API shape (naming, whether include_embedding should be a public kwarg vs. stay internal-only) if you'd prefer something different.

…ilarity search

AsyncPGVectorStore.create() always queried information_schema.columns to
validate the configured columns, with no way to skip it for a caller that
already knows the schema is valid (e.g. a short-lived store constructed per
request in a serverless/edge function). Add validate_schema: bool = True to
opt out of that round trip; it can't be combined with
ignore_metadata_columns, since resolving that option requires the full
column list.

__query_collection also always selected the embedding column, even though
plain similarity search never reads it back (only MMR does, via a separate
call). Add an internal include_embedding flag (default True) and have
asimilarity_search_with_score_by_vector opt out of it by default, since the
returned Document never carries the embedding either way.

Both changes are additive/opt-in and preserve existing default behavior.

Fixes langchain-ai#326

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTbEx2hGwCZyQQtXZ2pDck

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new validate_schema=False option removes the only guard that prevented unsanitized column-name strings from being interpolated directly into SQL identifiers; applications that derive any of the store's column or table configuration from external input when using this flag are vulnerable to SQL injection under the configured Postgres role.

metadata_json_column = (
None if metadata_json_column not in columns else metadata_json_column
)
if validate_schema:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validate_schema=False branch skips the information_schema-backed check that was the only guard preventing unsanitized column names from reaching SQL interpolation. With this flag set, any value passed as id_column, content_column, embedding_column, metadata_columns, metadata_json_column, or hybrid_search_config.tsv_column is stored verbatim and later interpolated into f-string queries (INSERT at line 330, SELECT at line 701, DELETE at line 491, hybrid-search at line 742) with only double-quote wrapping — wrapping a " in the payload trivially breaks.

if not validate_schema and ignore_metadata_columns:
    raise ValueError(
        "Can not use both validate_schema=False and ignore_metadata_columns, "
        "since resolving ignore_metadata_columns requires querying the full "
        "column list. Pass metadata_columns explicitly instead."
    )

if validate_schema:
    # Get field type information

Remediation: Apply an unconditional identifier-format check (e.g. ^[A-Za-z_][A-Za-z0-9_]{0,62}$) to every column and table name before they are stored, independent of validate_schema. The flag should only skip the database round-trip existence/type probe, not local format validation.

Attack Path
  1. Application constructs the store per request with validate_schema=False, reading column names from tenant config or a request parameter.
  2. Attacker supplies a payload like col", NULL); DROP TABLE docs; -- as a metadata column name.
  3. create() stores it verbatim — no information_schema check runs.
  4. aadd_embeddings (line 330) interpolates it into the INSERT statement, breaking out of the quoted-identifier context.
  5. The injected SQL executes under the application's Postgres role.

For more details, see the finding in Corridor.

Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.

@yukiharada1228
Yuki Harada (yukiharada1228) deleted the vectorstore-skip-validation-and-column-select branch September 12, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Perf: AsyncPGVectorStore re-validates schema and fetches the embedding column on every similarity search

1 participant