Skip to content

fix(vdb-tidb-vector): swap FTS_MATCH_WORD argument order to match TiDB - #41836

Open
Taranum01 wants to merge 8 commits into
langgenius:mainfrom
Taranum01:fix/41822-tidb-fts-argument-order
Open

fix(vdb-tidb-vector): swap FTS_MATCH_WORD argument order to match TiDB#41836
Taranum01 wants to merge 8 commits into
langgenius:mainfrom
Taranum01:fix/41822-tidb-fts-argument-order

Conversation

@Taranum01

Copy link
Copy Markdown
Contributor

Fixes #41822

What problem does this PR solve?

TiDB's FTS_MATCH_WORD signature is FTS_MATCH_WORD(query, column), but the pre-fix code generated FTS_MATCH_WORD(text, :query) (column first, query second). TiDB rejected the bound-parameter form with This version of TiDB doesn't yet support 'match against a non-constant string', silently breaking Full-Text Search and Hybrid Search for every TiDB Vector user with TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH=true.

What is changed and how it works?

The two occurrences of the function call in search_by_full_text — the WHERE clause predicate and the score projection — both need the query as the first argument and the column as the second:

FTS_MATCH_WORD(:query, text)

How it was tested?

$ uv run pytest providers/vdb/vdb-tidb-vector/tests/unit_tests/test_tidb_vector.py -o addopts=
29 passed
  • The existing test_search_by_full_text_queries_tidb_fts_and_scores assertion is updated to the new argument order.
  • A new test_search_by_full_text_uses_query_first_argument_order pins the contract: both occurrences in the generated SQL must use FTS_MATCH_WORD(:query, text) (and never FTS_MATCH_WORD(text, :query)), so the swap can't regress silently.

Taranum01 and others added 7 commits September 2, 2026 11:00
…enius#41594)

`sqlalchemy.engine` (and its sub-loggers like `sqlalchemy.pool`) have
`propagate = False` set in ext_logging.init_app to avoid duplicate logs,
so the root logger's LOG_TZ-aware formatter never gets a chance to format
SQLAlchemy records. When SQLALCHEMY_ECHO is enabled, engine log timestamps
are emitted in the server's local timezone while the surrounding
application logs are already converted to LOG_TZ, producing inconsistent
timestamps within a single log stream.

This change adds `apply_timezone_to_sqlalchemy_loggers()` in
ext_logging.py and calls it from ext_database.init_app() right after the
SQLAlchemy engine is eagerly created. The helper walks the SQLAlchemy
logger hierarchy and applies the same LOG_TZ converter to the formatters
of any handlers SQLAlchemy has attached, so engine log timestamps agree
with the rest of the application logs.

The fix preserves the existing non-propagating behavior (no duplicate
logs) and is a no-op when LOG_OUTPUT_FORMAT is not "text" or when
LOG_TZ is unset.

Regression test covers:
- JSON output format: no-op
- LOG_TZ unset: no-op
- LOG_TZ set: converter patched on sqlalchemy.engine, .pool, and bare
  SQLAlchemy loggers
- Handler with no formatter: left alone, no exception
- Idempotency across repeated calls
…logging

The langgenius#41594 / langgenius#41629 fix (langgenius#41629) called
apply_timezone_to_sqlalchemy_loggers() from ext_database.init_app(),
which adds a new import edge extensions.ext_database -> extensions.ext_logging.

Combined with the pre-existing libs.oauth_bearer -> extensions.ext_database
-> extensions.ext_logging -> core.logging.* chain, importlinter's
backend-layers contract now reports libs transitively reaching core.

Add the new edge to the ignore_imports list, alongside the existing
extensions exceptions (libs.external_api -> extensions.ext_logging, etc.).
Siblings inside the extensions package are free to depend on each other;
this exception is purely to mark the new edge and keep the migration
baseline honest.
…lper (langgenius#41594 / langgenius#41629 follow-up)

- Add `-> None` to every test method.
- Add a small `_formatter_converter(formatter)` helper that asserts
  the formatter is not None and casts the converter to a typed
  `Callable[[float], time.struct_time]`, then route every
  `formatter.converter` access through it.
- All 6 tests still pass; pyrefly reports 0 diagnostics on the file.
…anggenius#41649)

The PUT/DELETE/auth endpoints for MCP providers accept a
``provider_id`` in the request body and pass it straight to
SQLAlchemy, which (via the ``StringUUID`` column type) sends it to
Postgres as a UUID. When a non-UUID value (e.g. ``"fast-mcp"``) is
sent — typically when a stale server identifier ends up in the
request body — Postgres raises
``psycopg2.errors.InvalidTextRepresentation: invalid input syntax
for type uuid`` and the user sees a generic 500. The API layer
should fail earlier with a clean 400 instead.

This change adds a ``_validate_uuid`` helper and a ``field_validator``
on each of the three payloads that take ``provider_id``
(``MCPProviderUpdatePayload``, ``MCPProviderDeletePayload``,
``MCPAuthPayload``). The helper accepts the existing UUID string
form (``xxxxxxxx-xxxx-...``) and rejects anything else with a
Pydantic ``ValidationError`` that the existing
``@model_validate`` decorator turns into a 400.

Tests (7 new, in
``api/tests/unit_tests/controllers/console/workspace/test_mcp_provider_id_uuid.py``):

- ``MCPProviderUpdatePayload`` accepts a valid UUID, rejects
  ``"fast-mcp"`` and ``""`` with a ValidationError on the
  ``provider_id`` field.
- ``MCPProviderDeletePayload`` and ``MCPAuthPayload`` accept a
  valid UUID and reject a non-UUID string.

All 32 pre-existing tests in ``test_tool_providers.py`` still pass.
ruff check / ruff format --check clean, pyrefly 0 diagnostics,
importlinter 25 kept / 0 broken.
…tests (langgenius#41649 follow-up)

mypy + pyrefly flag every test method in the new
test_mcp_provider_id_uuid.py as missing a return annotation. Add
`-> None` to all seven test methods. Pure test annotation, no
behavior change.
…langgenius#41714)

`_get_uuids` overrode the base ``VectorBase._get_uuids`` with
``uuid5(URL_NAMESPACE, page_content)`` (UUID v5) so the same content
inserted twice would dedupe to a single Weaviate object. That was
self-consistent, but the cleanup path
(``batch_clean_document_task`` → ``index_processor.clean`` →
``vector.delete_by_ids``) was passing the segment's ``index_node_id``
from the database — a random UUID v4. ``uuid5(content)`` never
equals ``index_node_id``, so ``delete_by_id`` silently no-op'd on
every Weaviate vector store and ``Cleaned documents ...`` was logged
as success while the objects remained as searchable orphans.

Use the same source the rest of the VDB stack uses
(``VectorBase._get_uuids``): the ``doc_id`` stored in each document's
metadata. Insert writes under that id, the cleanup path already
passes the same id, and ``delete_by_ids`` now actually targets the
object that was inserted. The deduplication property previously claimed
by the UUID5 path is already provided by the upstream
``_filter_duplicate_texts`` step on the same ``doc_id``, so removing
the UUID5 hash does not change semantics for any caller that already
provides a stable ``doc_id``.

The pre-fix UUID5 path is removed; the ``_get_uuids`` override now
matches the parent's parameter name (``texts``) so pyrefly stops
flagging the override as inconsistent.

Tests (2 new in
``providers/vdb/vdb-weaviate/tests/unit_tests/test_weaviate_vector.py``):

- ``test_get_uuids_uses_doc_id_to_match_cleanup_path`` — pins the
  contract: any document with a ``doc_id`` must round-trip through
  ``_get_uuids`` as that exact ``doc_id``, so insert and delete agree
  on the Weaviate object id.
- ``test_get_uuids_skips_documents_without_doc_id`` — documents without
  a ``doc_id`` are skipped, so ``add_texts`` falls back to a fresh
  uuid4 for them (inherited from the base class).

All 41 existing tests in the suite still pass.
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 63.14% 63.15% +0.01%
Strict coverage 62.75% 62.76% +0.01%
Typed symbols 44,671 44,692 +21
Untyped symbols 26,242 26,245 +3
Modules 3344 3346 +2

langgenius#41822)

TiDB's ``FTS_MATCH_WORD`` signature is ``FTS_MATCH_WORD(query, column)``,
but the pre-fix code generated ``FTS_MATCH_WORD(text, :query)`` (column
first, query second). TiDB rejected the bound-parameter form with
``This version of TiDB doesn't yet support 'match against a non-constant
string'``, silently breaking Full-Text Search and Hybrid Search for
every TiDB Vector user with ``TIDB_VECTOR_ENABLE_FULLTEXT_SEARCH=true``.

The two occurrences of the function call in
``search_by_full_text`` — the WHERE clause predicate and the score
projection — both need the query as the first argument and the column
as the second.

Tests:
- The existing ``test_search_by_full_text_queries_tidb_fts_and_scores``
  assertion is updated to the new argument order.
- A new ``test_search_by_full_text_uses_query_first_argument_order``
  pins the contract: both occurrences in the generated SQL must use
  ``FTS_MATCH_WORD(:query, text)`` (and never ``FTS_MATCH_WORD(text, :query)``),
  so the swap can't regress silently.

All 29 existing tests still pass. ``ruff check`` / ``ruff format --check``
clean, ``importlinter`` 25 kept / 0 broken.
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.

Bug: fix TiDB Vector full-text search argument order

1 participant