Skip to content

feat: Add Apache Doris 4.0 vector database connector - #1

Closed
Chi-Song-Owen wants to merge 4 commits into
mainfrom
feat/doris-connector
Closed

feat: Add Apache Doris 4.0 vector database connector#1
Chi-Song-Owen wants to merge 4 commits into
mainfrom
feat/doris-connector

Conversation

@Chi-Song-Owen

@Chi-Song-Owen Chi-Song-Owen commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Adds native support for Apache Doris (and VeloDB Cloud) as a CocoIndex target, enabling vector similarity search and full-text search capabilities.

Features

Vector Index Support

  • HNSW (Hierarchical Navigable Small World) - default algorithm with configurable m and ef_construction parameters
  • IVF (Inverted File) - cluster-based indexing with configurable lists parameter
  • Metrics: L2 distance (Euclidean), Inner product similarity

Full-Text Search

  • Inverted index support for text columns
  • Configurable parser options: unicode, english, chinese
  • MATCH_ANY, MATCH_ALL, MATCH_PHRASE operators

Data Ingestion

  • Stream Load API for high-throughput bulk ingestion
  • Batch processing with configurable batch_size (default: 10,000 rows)
  • Upsert and delete operations for incremental updates

Schema Management

  • Extend mode (default): Safely add missing columns, preserve extra columns
  • Strict mode: Enforce exact schema match, raise errors on drift
  • Automatic table creation with DUPLICATE KEY model (required for vector indexes)

Reliability

  • Retry logic with exponential backoff for transient errors
  • Covers both HTTP (Stream Load) and MySQL protocol (DDL) operations
  • Configurable max_retries, retry_base_delay, retry_max_delay

Query Helpers

  • connect_async() - Async MySQL connection helper with proper cleanup
  • build_vector_search_query() - SQL builder for vector similarity search

Test Coverage

Type Count Status
Unit tests 67 ✅ Pass
Integration tests 60 ✅ Pass

Integration tests verified against VeloDB Cloud.

Documentation

  • Complete target documentation at docs/docs/targets/doris.md
  • Added to targets index page

Example

New examples/text_embedding_doris/ following the same pattern as text_embedding_lancedb:

  • Markdown file ingestion with chunking
  • SentenceTransformer embeddings
  • Vector and full-text index creation
  • Query handler with similarity search

Requirements

  • Apache Doris 4.0+ or VeloDB Cloud (for vector index support)
  • Python packages: aiohttp, aiomysql

Breaking Changes

None - this is a new target connector.

Summary by CodeRabbit

  • New Features

    • Added Apache Doris support, including vector, full-text, hybrid search, schema evolution, retries, and asynchronous operations.
    • Added examples for Doris-backed text search and slide-to-speech processing.
    • Added documentation for Doris configuration and usage.
    • Added documentation version selection and GitHub star display.
  • Bug Fixes

    • Query responses now serialize binary data in a portable format.
    • Improved dark-mode styling and documentation navigation behavior.
  • Chores

    • Updated automated build, validation, and documentation checks.

ZhiHanZ and others added 4 commits January 12, 2026 16:52
Add native support for Apache Doris (and VeloDB Cloud) as a target with:

- Vector indexes (HNSW, IVF) with L2 distance and inner product metrics
- Full-text search with inverted indexes
- Stream Load for bulk data ingestion
- Schema evolution modes (extend/strict)
- Retry logic with exponential backoff for transient errors
- Query helpers (connect_async, build_vector_search_query)

Test coverage:
- 67 unit tests
- 60 integration tests (verified against VeloDB Cloud)

Documentation and example included.
- Implement JSON fallback for vectors without fixed dimension (like Postgres/Qdrant)
- Add aiohttp to optional dependencies in pyproject.toml
- Make schema_change_timeout and index_build_timeout configurable
- Fix flaky test_remove_index_from_existing_table with retry loop
- Clean up redundant unit tests
- Add timeout config propagation unit test
- Fix type annotation in test_doris_integration.py (rename shadowed variable)
- Add return type annotation to get_doris_config() in example
- Remove unused imports from test_doris_integration.py
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds an Apache Doris connector with examples, tests, and documentation. It also adds documentation navigation components, query byte serialization, updated example dependency bounds, and revised CI workflows.

Changes

CI and validation

Layer / File(s) Summary
CI and validation workflow updates
.github/workflows/*, .pre-commit-config.yaml
CI now targets v1, uses pre-commit build-test hooks, adds fast and mypy checks, and updates the mypy-check hook.

Apache Doris support

Layer / File(s) Summary
Doris connector and public target support
python/cocoindex/targets/doris.py, python/cocoindex/targets/__init__.py, python/cocoindex/tests/targets/test_doris_unit.py, docs/docs/targets/doris.md, pyproject.toml
Adds Doris configuration, retries, schema and index management, ingestion, deletion, query helpers, optional dependencies, documentation, and unit tests.
Doris example and integration validation
examples/text_embedding_doris/*, python/cocoindex/tests/targets/test_doris_integration.py
Adds a Doris text-embedding example and integration coverage for lifecycle, mutation, indexing, search, schema evolution, errors, and example execution.

Serialization and documentation

Layer / File(s) Summary
Byte serialization for query responses
python/cocoindex/engine_object.py, python/cocoindex/flow.py
Query responses now encode byte values as tagged base64 objects.
Documentation navigation components
docs/docusaurus.config.ts, docs/src/components/GitHubStar/*, docs/src/components/VersionSelector/*, docs/src/theme/Navbar/*, docs/src/css/custom.css
Adds a responsive navbar, GitHub star display, version selector, theme-aware navbar colors, and updated search configuration.
Documentation styling and formatting cleanup
docs/src/components/*, docs/src/css/*
Applies formatting cleanup and scopes dark-mode color variables to html[data-theme='dark'].

Examples

Layer / File(s) Summary
Slides-to-Speech example
examples/slides_to_speech/*
Adds a Google Drive PDF pipeline with slide rendering, BAML transcription, Piper speech synthesis, embeddings, LanceDB storage, vector search, setup files, and documentation.
Example dependency bounds
examples/*/pyproject.toml, examples/fastapi_server_docker/requirements.txt
Updates CocoIndex requirements to >=0.3.26,<1 across the affected examples.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant CocoIndex
  participant Doris
  Client->>CocoIndex: submit query
  CocoIndex->>Doris: execute vector-search SQL
  Doris-->CocoIndex: return matching rows
  CocoIndex-->Client: return serialized query output
Loading
sequenceDiagram
  participant GoogleDrive
  participant SlidesToSpeech
  participant BAML
  participant Piper
  participant LanceDB
  GoogleDrive->>SlidesToSpeech: provide presentation PDF
  SlidesToSpeech->>BAML: extract slide transcript
  SlidesToSpeech->>Piper: synthesize audio
  SlidesToSpeech->>LanceDB: store slide records and embeddings
  LanceDB-->SlidesToSpeech: return search matches
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding an Apache Doris 4.0 vector database connector.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/doris-connector

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 19

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (15)
examples/slides_to_speech/README.md-30-36 (1)

30-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the configuration instructions.

Line 34 uses .env.exmaple, so the copy command fails. The instructions also omit GEMINI_API_KEY, although baml_src/slides.baml requires it. Tell the user to set the Google Drive values and the Gemini API key.

Proposed fix
-3. Create `.env` file with your credential file and folder IDs.
-    Starting from copying the `.env.example`, and then edit it to fill in your credential file path and folder IDs.
+3. Create a `.env` file from `.env.example`.
+   Set the Google Drive credential path, folder IDs, and `GEMINI_API_KEY`.
 
     ```sh
-    cp .env.exmaple .env
+    cp .env.example .env
     $EDITOR .env
     ```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/slides_to_speech/README.md` around lines 30 - 36, Update the
environment setup instructions in the README to copy from the correctly named
.env.example file, and explicitly instruct users to configure the Google Drive
credential and folder ID values plus GEMINI_API_KEY required by slides.baml.
examples/slides_to_speech/main.py-195-227 (1)

195-227: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a consistent LanceDB metric. This export creates no vector index, so table.search() uses L2 distance while QueryInfo declares cosine similarity. Add .metric("cosine") or configure a cosine vector index, then keep score = 1.0 - row["_distance"].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/slides_to_speech/main.py` around lines 195 - 227, Update the vector
search in search to explicitly use the cosine metric by adding the LanceDB
metric configuration before limiting and materializing results. Keep QueryInfo’s
COSINE_SIMILARITY declaration and the existing score calculation unchanged.
python/cocoindex/engine_object.py-86-87 (1)

86-87: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Recurse after converting NumPy arrays to lists.

np.ndarray.tolist() preserves bytes values in object and byte-string arrays. When bytes_to_base64=True, returning the list directly bypasses recursive conversion. Wrap v.tolist() in dump_engine_object(..., bytes_to_base64=bytes_to_base64) and add a regression test for np.array([b"payload"], dtype=object).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/engine_object.py` around lines 86 - 87, Update the
np.ndarray branch in dump_engine_object to recursively process the result of
v.tolist(), forwarding the bytes_to_base64 option so nested bytes are converted
when enabled. Add a regression test covering np.array([b"payload"],
dtype=object) with bytes_to_base64=True.
examples/text_embedding_doris/README.md-7-7 (1)

7-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the product name spelling.

Write "GitHub" with a capital "H".

✏️ Proposed fix
-We appreciate a star at [CocoIndex Github](https://github.com/cocoindex-io/cocoindex) if this is helpful.
+We appreciate a star at [CocoIndex GitHub](https://github.com/cocoindex-io/cocoindex) if this is helpful.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/text_embedding_doris/README.md` at line 7, Update the CocoIndex
GitHub link text in the README sentence to capitalize the product name as
“GitHub,” leaving the URL and surrounding wording unchanged.

Source: Linters/SAST tools

python/cocoindex/tests/targets/test_doris_integration.py-675-681 (1)

675-681: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

test_upsert_row asserts nothing about the upsert.

The query uses LIMIT 1 on a table that already contains at least one row, so len(query_result) >= 1 always holds. The test does not verify that the second Stream Load updated the value. test_upsert_idempotent_no_duplicates at line 736 already covers the real contract.

Either assert the resulting content, or remove this test as redundant.

💚 Proposed fix
-        # Note: DUPLICATE KEY keeps all versions, so we check latest
-        assert len(query_result) >= 1
+        assert len(query_result) == 1
+        assert query_result[0]["content"] == "Updated"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 675 -
681, Update test_upsert_row so it verifies the upserted row’s content from
query_result rather than only asserting a non-empty result; alternatively remove
the test if its behavior is fully redundant with
test_upsert_idempotent_no_duplicates. Preserve the existing latest-version query
and ensure the assertion confirms the value written by the second Stream Load.
examples/text_embedding_doris/main.py-138-147 (1)

138-147: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove math.sqrt from the Doris L2 distance. l2_distance_approximate returns the Euclidean distance, not its square, so math.sqrt(result[3]) reports an incorrect score. Use result[3] directly and use float("inf") or omit score when the distance column is absent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/text_embedding_doris/main.py` around lines 138 - 147, Update the
result mapping in the Doris query output to use result[3] directly for the
score, without applying math.sqrt, because l2_distance_approximate already
returns Euclidean distance. Preserve the fallback for results without a distance
column by using float("inf") or omitting the score.
python/cocoindex/tests/targets/test_doris_integration.py-308-312 (1)

308-312: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The assertion cannot fail.

SHOW DATABASES returns at least one row on any reachable instance. The or len(db_names) > 0 branch therefore always holds, and the information_schema check never applies. The test only proves that _execute_ddl did not raise.

💚 Proposed fix
         result = await _execute_ddl(doris_spec, "SHOW DATABASES")
         assert isinstance(result, list)
-        # Should have at least some system databases
         db_names = [r.get("Database") for r in result]
-        assert "information_schema" in db_names or len(db_names) > 0
+        assert "information_schema" in db_names, (
+            f"Expected information_schema in databases, got: {db_names}"
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 308 -
312, Update the assertion in the SHOW DATABASES test to validate the expected
system database explicitly, removing the `or len(db_names) > 0` fallback. Keep
the existing `db_names` extraction and assert that `information_schema` is
present.
examples/text_embedding_doris/README.md-34-35 (1)

34-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the VeloDB documentation link. The 5.x-preview URL permanently redirects through 26.x to the unversioned quick-start page. It does not preserve the requested preview version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/text_embedding_doris/README.md` around lines 34 - 35, Update the
VeloDB Cloud quick-start link in the README's “Option B: VeloDB Cloud” entry to
use the current documentation URL that directly targets the intended version
without redirecting through 26.x or the unversioned page; leave the surrounding
setup text unchanged.
docs/src/components/VersionSelector/styles.module.css-93-105 (1)

93-105: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename the keyframe to kebab-case.

Stylelint flags fadeIn as violating keyframes-name-pattern. Rename it to fade-in to keep the pipeline lint check green.

🔧 Proposed fix
   animation: fadeIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
 }

-@keyframes fadeIn {
+@keyframes fade-in {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/components/VersionSelector/styles.module.css` around lines 93 - 105,
Rename the fadeIn keyframe declaration and its animation reference to fade-in,
preserving the existing animation timing and keyframe behavior.

Source: Linters/SAST tools

.github/workflows/e2e_type_check.yml-6-17 (1)

6-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Trigger this workflow when the mypy hook configuration changes.

build-test and fast-check skip mypy-check. A PR that changes only .pre-commit-config.yaml does not run the changed hook because these path filters exclude that file. Add .pre-commit-config.yaml to both filters.

Proposed fix
   pull_request:
     paths:
       - python/**
       - examples*/**
       - "*.toml"
+      - ".pre-commit-config.yaml"
       - ".github/workflows/e2e_type_check.yml"
   push:
     paths:
       - python/**
       - examples*/**
       - "*.toml"
+      - ".pre-commit-config.yaml"
       - ".github/workflows/e2e_type_check.yml"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e_type_check.yml around lines 6 - 17, Add
.pre-commit-config.yaml to the paths filters for both the pull_request and push
triggers in the e2e_type_check workflow, preserving all existing path entries so
changes to the mypy hook configuration trigger the workflow.
python/cocoindex/targets/doris.py-558-563 (1)

558-563: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Log a warning when a TEXT field maps to a VARCHAR column.

The branch at Lines 561-563 accepts a VARCHAR(n) column for a schema field that requires TEXT. Values longer than n will then fail or truncate at write time, and the failure surfaces later as a Stream Load error. The inline comment already identifies the case. Emit a warning so operators can find the cause.

Note that key columns are unaffected, because _convert_to_key_column_type converts the expected type before comparison at Lines 1710-1711.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 558 - 563, Update the
expected-TEXT-to-actual-VARCHAR branch in the schema compatibility logic to emit
a warning before returning True, including the expected and actual type details
and clearly indicating possible truncation or write failures. Leave the
key-column conversion behavior and other compatibility branches unchanged.
docs/docs/targets/doris.md-59-67 (1)

59-67: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the timeout options.

DorisTarget declares schema_change_timeout (default 60) and index_build_timeout (default 300) at python/cocoindex/targets/doris.py Lines 114-115. Both are public spec fields, and the PR objectives list them as a feature. This Behavior section does not mention them.

📝 Proposed addition
 * `auto_create_table` (`bool`, default: `True`): Whether to automatically create the table if it doesn't exist.
 
+* `schema_change_timeout` (`int`, default: `60`): Timeout in seconds for `ALTER TABLE` operations to reach the `NORMAL` state.
+
+* `index_build_timeout` (`int`, default: `300`): Timeout in seconds for `BUILD INDEX` operations to complete.
+
 * `schema_evolution` (`"extend"` | `"strict"`, default: `"extend"`): Schema evolution strategy.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/targets/doris.md` around lines 59 - 67, Update the Doris target
options documentation near stream_load_timeout to include schema_change_timeout
(int, default 60) and index_build_timeout (int, default 300), describing each
timeout’s purpose in seconds. Keep the existing option descriptions unchanged.
python/cocoindex/targets/doris.py-1856-1864 (1)

1856-1864: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Add strict=True to the zip call.

Ruff reports B905 at Line 1861. If the key tuple length does not match key_field_names, zip truncates silently. The resulting key_dict then identifies the wrong row, and both the delete and the Stream Load write operate on incorrect keys. strict=True converts that into an immediate error.

🐛 Proposed fix
                     key_dict = {
                         name: _convert_value_for_doris(k)
-                        for name, k in zip(key_field_names, key)
+                        for name, k in zip(key_field_names, key, strict=True)
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 1856 - 1864, Update the zip
call in the key_dict comprehension within the mutations loop to use strict=True,
ensuring mismatched key tuples and key_field_names raise immediately instead of
being silently truncated.

Source: Linters/SAST tools

python/cocoindex/targets/doris.py-2042-2043 (1)

2042-2043: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-finite values in the query vector.

float(v) renders nan and inf as the literals nan and inf. Those are not valid Doris array literals, so the generated SQL fails at the server with an unclear parse error. Validate the values and raise ValueError in this function, where the cause is clear.

🐛 Proposed fix
+    if not all(math.isfinite(float(v)) for v in query_vector):
+        raise ValueError("query_vector must contain only finite float values")
+
     # Format vector as array literal (safe - only floats)
     vector_literal = "[" + ", ".join(str(float(v)) for v in query_vector) + "]"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 2042 - 2043, Validate each
converted value in the query-vector formatting logic before constructing
vector_literal, rejecting NaN and positive or negative infinity with a clear
ValueError. Keep finite values formatted as the existing Doris array literal.
docs/docs/targets/doris.md-102-109 (1)

102-109: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the Doris index examples to use the public API.

cocoindex.VectorIndex and cocoindex.FtsIndex are not public symbols. Use VectorIndexDef(field_name=..., metric=..., method=...) and FtsIndexDef(field_name=..., parameters=...). Pass them through vector_indexes= and fts_indexes=, and use primary_key_fields= in flow_builder.export.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/targets/doris.md` around lines 102 - 109, Update the Doris
documentation examples to use the public index definitions: replace
cocoindex.VectorIndex with VectorIndexDef(field_name=..., metric=...,
method=...) and cocoindex.FtsIndex with FtsIndexDef(field_name=...,
parameters=...). Pass these definitions through vector_indexes= and
fts_indexes=, and update flow_builder.export to use primary_key_fields=.
🧹 Nitpick comments (21)
python/cocoindex/tests/targets/test_doris_integration.py (8)

1028-1041: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use the retry loop here as well.

test_remove_index_from_existing_table replaced a fixed sleep with a bounded retry loop, because Doris applies index changes asynchronously. This test still uses a single asyncio.sleep(5) before asserting. Index recreation has the same timing behaviour, so this test keeps the flakiness the sibling test removed.

♻️ Proposed refactor
-        # Wait longer for schema change to complete (Doris needs time for index operations)
-        await asyncio.sleep(5)
-
-        # Index should still exist (was dropped and recreated)
-        result = await _execute_ddl(
-            doris_spec,
-            f"SHOW CREATE TABLE {doris_spec.database}.{doris_spec.table}",
-        )
-        create_stmt = result[0].get("Create Table", "")
-        assert "idx_content_inv" in create_stmt, (
-            "Index should exist after parameter change"
-        )
+        # Wait for the index to be recreated (Doris async operation)
+        create_stmt = ""
+        for _ in range(ASYNC_OPERATION_TIMEOUT):
+            await asyncio.sleep(1)
+            result = await _execute_ddl(
+                doris_spec,
+                f"SHOW CREATE TABLE {doris_spec.database}.{doris_spec.table}",
+            )
+            create_stmt = result[0].get("Create Table", "")
+            if "idx_content_inv" in create_stmt:
+                break
+        else:
+            pytest.fail(
+                f"Index was not recreated after {ASYNC_OPERATION_TIMEOUT}s. "
+                f"Current schema: {create_stmt}"
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 1028 -
1041, Replace the fixed asyncio.sleep(5) in the index recreation flow of
test_remove_index_from_existing_table with the same bounded retry loop used by
the sibling test, retrying the SHOW CREATE TABLE check until idx_content_inv
appears or the retry limit is reached.

974-988: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the unused loop variable and initialize create_stmt.

Ruff reports B007 for i at line 975. The variable is not used in the body. Line 987 also reads create_stmt in the else branch. If a user sets DORIS_ASYNC_TIMEOUT=0, range(0) is empty, the loop body never runs, and line 987 raises NameError instead of reporting the intended failure message.

♻️ Proposed fix
         # Wait for index removal with retry (Doris async operation)
-        for i in range(ASYNC_OPERATION_TIMEOUT):
+        create_stmt = ""
+        for _ in range(ASYNC_OPERATION_TIMEOUT):
             await asyncio.sleep(1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 974 -
988, Update the retry loop around _execute_ddl to use an underscore for the
unused iteration variable, and initialize create_stmt before the loop so the
failure branch remains valid when ASYNC_OPERATION_TIMEOUT is zero. Preserve the
existing retry, success break, and pytest.fail behavior.

Source: Linters/SAST tools


1920-1939: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These two tests duplicate each other.

test_default_config_values and test_default_config_values_match_docs construct the same spec and assert the same defaults. The second adds schema_evolution and assertion messages; every other check repeats the first. Keep one and move the schema_evolution assertion into it.

Also applies to: 3010-3045

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 1920 -
1939, Remove the duplicate test between test_default_config_values and
test_default_config_values_match_docs, retaining a single spec construction and
set of default assertions. Move the schema_evolution assertion into the retained
test, including its assertion message if desired, and delete the redundant test.

1557-1565: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not swallow every BUILD INDEX error.

The bare except Exception: pass hides all failures, not only "index already built". A DDL syntax error, an auth failure, or a dropped connection passes silently. The later assertions then fail with a message that does not point at the real cause. Ruff reports this as S110 at lines 1564-1565, and the same pattern repeats at lines 1631-1632, 1827-1828, and 3161-3162.

Log the exception so the cause appears in the test output.

♻️ Proposed refactor
         try:
             await _execute_ddl(
                 doris_spec,
                 f"BUILD INDEX idx_embedding_ivf ON `{doris_spec.database}`.`{doris_spec.table}`",
             )
             await asyncio.sleep(5)  # Wait for index build
-        except Exception:
-            pass  # Index may already be built
+        except Exception as e:
+            print(f"BUILD INDEX skipped or failed (may already be built): {e}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 1557 -
1565, Update the BUILD INDEX error-handling blocks in the affected test flows to
log the caught exception instead of silently passing, while preserving the
ability to continue when the index is already built. Apply this consistently to
the blocks surrounding the visible BUILD INDEX call and the repeated occurrences
identified in the comment.

Source: Linters/SAST tools


1976-2003: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two tests assert only the values they just set.

test_batch_size_respected creates a real table and then asserts spec.batch_size == 10. It never performs a mutation, so it does not verify that batching happens. The docstring states this. test_retry_config_from_spec_used has the same shape: it constructs a spec and asserts the three retry attributes it passed in, which test_custom_config_values at line 1941 already covers.

Both names claim behaviour that the bodies do not verify, and test_batch_size_respected costs a network round trip and a table. Either assert the real behaviour, or remove both tests.

To verify batching, load more rows than batch_size through _Connector.mutate and count the resulting rows.

Also applies to: 2095-2115

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 1976 -
2003, Remove the redundant tests test_batch_size_respected and
test_retry_config_from_spec_used, since they only reassert values assigned
during construction and do not verify runtime behavior; alternatively, make
test_batch_size_respected exercise _Connector.mutate with more rows than
batch_size and verify the resulting row count, while retaining existing coverage
for retry configuration.

1566-1578: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

These tests do not exercise the vector index.

Both queries call the exact l2_distance function. The comment states that the index accelerates ORDER BY, but the result set is identical with or without an index on a three-row and five-row table. The assertion query_result[0]["content"] == "Apple" therefore passes even if index creation silently failed.

test_build_vector_search_query_l2_distance at line 2876 shows the connector emits l2_distance_approximate. Use that function here so the tests fail when the index is missing or unusable.

Also applies to: 1633-1645

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 1566 -
1578, Update both vector-search queries in the Doris integration test, including
the query near the l2_distance test and the additional occurrence around the
second referenced range, to call l2_distance_approximate instead of l2_distance.
Preserve the existing ordering, limits, and result assertions so the tests
exercise the vector index path.

239-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consolidate _mock_state and _state_with_creds.

_state_with_creds at line 2232 duplicates the credential-copying logic in _mock_state. The two helpers also differ silently: _state_with_creds sets replication_num=1 and buckets=1, while _mock_state leaves the defaults. Tests that switch between the helpers get different table properties without stating why.

Move _state_with_creds next to _mock_state and let _mock_state build its field schemas then delegate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 239 -
276, Move _state_with_creds next to _mock_state, then update _mock_state to
build its key and value schemas and delegate state construction to
_state_with_creds when credentials/spec are provided. Consolidate the shared
credential-copying logic and preserve the helper’s replication_num=1 and
buckets=1 settings consistently, while retaining the existing default path when
no spec is supplied.

2139-2154: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Narrow the expected exception type.

_execute_ddl raises aiomysql.OperationalError for authentication failure and DorisConnectionError after retryable connection failures. Use pytest.raises((DorisConnectionError, aiomysql.OperationalError)). aiomysql is already imported at module scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 2139 -
2154, Update test_invalid_credentials_raises_auth_error to replace the broad
Exception expectation with pytest.raises((DorisConnectionError,
aiomysql.OperationalError)), preserving the existing _execute_ddl call and
accepting only the documented authentication or connection failure types.

Source: Linters/SAST tools

examples/text_embedding_doris/main.py (1)

121-136: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider reusing a connection pool for the query handler.

Each search() call opens and closes a new MySQL protocol connection. For an example this is acceptable, but it adds connect latency to every query. A module-level aiomysql pool, or a lazily created shared connection, would show a better pattern to readers who copy this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/text_embedding_doris/main.py` around lines 121 - 136, Update the
query handling around search() to reuse a module-level or lazily initialized
aiomysql connection pool instead of calling coco_doris.connect_async for every
request. Acquire a connection from the pool for each query, release it after
cursor use, and close the shared pool only during application shutdown rather
than in the per-query finally block.
docs/src/theme/Navbar/index.tsx (1)

22-23: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Account for the default left position.

Docusaurus navbar items default to position: 'left' when the field is omitted. This filter only matches items with an explicit 'left' value, so any navbar item added later without an explicit position silently drops out of both leftItems and rightItems, instead of appearing on the left as Docusaurus would normally render it.

All current items in docusaurus.config.ts set position explicitly, so this is not an active failure today, but it is a latent trap for future navbar items.

♻️ Proposed fix
-  const leftItems = items.filter(item => item.position === 'left');
+  const leftItems = items.filter(item => (item.position ?? 'left') === 'left');
   const rightItems = items.filter(item => item.position === 'right');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/theme/Navbar/index.tsx` around lines 22 - 23, Update the leftItems
filter to include navbar items whose position is omitted, treating undefined
position as the default left placement; keep rightItems matching only explicit
'right' positions.
docs/src/components/VersionSelector/index.tsx (2)

46-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate the version-navigation logic.

The keyboard handler (lines 64-81) reimplements the same newPath resolution and navigation as handleVersionChange (lines 102-119). Two independent copies of this logic increase the risk of them diverging as new versions or path rules are added.

Use a ref to hold the latest handleVersionChange so the effect can call it without adding it as a dependency, instead of duplicating the logic inline.

♻️ Proposed refactor
+  const handleVersionChangeRef = useRef<(value: string) => void>();
+
+  const handleVersionChange = (value: string) => {
+    const option = options.find(opt => opt.value === value);
+    if (option?.disabled) {
+      return;
+    }
+    if (value === currentVersion) {
+      setIsOpen(false);
+      return;
+    }
+    let newPath = '';
+    if (value === 'v0') {
+      if (location.pathname.includes('/examples')) {
+        newPath = '/examples';
+      } else if (location.pathname.includes('/docs')) {
+        newPath = '/docs';
+      }
+    }
+    setIsOpen(false);
+    window.location.href = newPath;
+  };
+  handleVersionChangeRef.current = handleVersionChange;
+
   useEffect(() => {
     if (!isOpen) return;
     const handleKeyDown = (event: KeyboardEvent) => {
       if (event.key === 'Escape') {
         setIsOpen(false);
         triggerRef.current?.focus();
       } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
         event.preventDefault();
         const enabledOptions = options.filter(opt => !opt.disabled);
         const enabledValues = enabledOptions.map(opt => opt.value);
         const currentIndex = enabledValues.indexOf(currentVersion);
         const nextIndex = event.key === 'ArrowDown'
           ? (currentIndex + 1) % enabledValues.length
           : (currentIndex - 1 + enabledValues.length) % enabledValues.length;
-        const newValue = enabledValues[nextIndex];
-
-        // Handle version change inline to avoid dependency issues
-        if (newValue === currentVersion) {
-          setIsOpen(false);
-          return;
-        }
-
-        let newPath = '';
-        if (newValue === 'v0') {
-          if (location.pathname.includes('/examples')) {
-            newPath = '/examples';
-          } else if (location.pathname.includes('/docs')) {
-            newPath = '/docs';
-          }
-        }
-
-        setIsOpen(false);
-        window.location.href = newPath;
+        handleVersionChangeRef.current?.(enabledValues[nextIndex]);
       }
     };
     document.addEventListener('keydown', handleKeyDown);
     return () => {
       document.removeEventListener('keydown', handleKeyDown);
     };
-  }, [isOpen, currentVersion, location.pathname]);
-
-  const handleVersionChange = (value: string) => {
-    // Check if the option is disabled
-    const option = options.find(opt => opt.value === value);
-    if (option?.disabled) {
-      return; // Don't allow clicking on disabled options
-    }
-
-    if (value === currentVersion) {
-      setIsOpen(false);
-      return;
-    }
-
-    let newPath = '';
-
-    // Handle v0 selection
-    if (value === 'v0') {
-      // If URL contains /examples (with or without version), go to /examples base path
-      if (location.pathname.includes('/examples')) {
-        newPath = '/examples';
-      }
-      // If URL contains /docs (with or without version), go to /docs base path
-      else if (location.pathname.includes('/docs')) {
-        newPath = '/docs';
-      }
-    }
-
-    setIsOpen(false);
-    // Navigate to new path (base path only, no sub-paths preserved)
-    window.location.href = newPath;
-  };
+  }, [isOpen, currentVersion]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/components/VersionSelector/index.tsx` around lines 46 - 119,
Deduplicate version navigation by having the keyboard handler invoke the latest
handleVersionChange through a ref, rather than resolving newPath and navigating
inline. Update the ref whenever handleVersionChange changes, and keep the effect
dependencies focused on keyboard-state values so it can call the ref without
adding the callback itself.

102-119: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Handle the case where no path pattern matches.

If location.pathname does not contain /examples or /docs, newPath stays '', and window.location.href = '' triggers a full reload of the current page instead of a deliberate navigation. Since VersionSelector renders in the site-wide navbar, a user can trigger this from any page, such as the homepage or a blog post.

Fall back to a sensible default (for example, the site root) when no pattern matches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/components/VersionSelector/index.tsx` around lines 102 - 119, Update
the v0 navigation logic in VersionSelector so newPath defaults to a deliberate
fallback such as the site root when the current pathname matches neither
/examples nor /docs. Preserve the existing base-path selection for matching
pages and ensure window.location.href never receives an empty string.
docs/src/theme/Navbar/styles.module.css (1)

212-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove dead CSS for the retired GitHub iframe link.

.navbar-github-link styled the previous iframe-based GitHub star button. That navbar item is replaced by the "Enterprise" link in docusaurus.config.ts, and the new GitHubStar component uses its own styles.module.css classes, not .navbar-github-link. This block is now dead code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/theme/Navbar/styles.module.css` around lines 212 - 222, Remove the
obsolete .navbarItemsRight :global(.navbar-github-link) rules and their
media-query override from the navbar stylesheet, since the retired GitHub iframe
link no longer uses them. Preserve all unrelated navbar styling.
docs/src/components/GitHubStar/index.tsx (1)

22-36: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Harden the GitHub star fetch against failures and rate limits.

The fetch call has three gaps:

  • It does not check response.ok before calling response.json(). A non-2xx response (for example, a rate-limited 403) still attempts to parse JSON and silently produces an incorrect count.
  • It does not cache the result. The unauthenticated GitHub API allows 60 requests per hour per IP. Repeated visits or shared corporate IPs can exhaust this quickly, showing "0" stars.
  • It has no timeout. A stalled request leaves loading true forever with no user feedback.

Add a caching layer with a TTL and validate response.ok, and give fetch a timeout via AbortController.

♻️ Proposed fix
   useEffect(() => {
     async function fetchStars() {
       try {
-        const response = await fetch('https://api.github.com/repos/cocoindex-io/cocoindex');
+        const cached = sessionStorage.getItem('cocoindex-github-stars');
+        if (cached) {
+          const { count, ts } = JSON.parse(cached);
+          if (Date.now() - ts < 60 * 60 * 1000) {
+            setStars(count);
+            setLoading(false);
+            return;
+          }
+        }
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 5000);
+        const response = await fetch('https://api.github.com/repos/cocoindex-io/cocoindex', {
+          signal: controller.signal,
+        });
+        clearTimeout(timeoutId);
+        if (!response.ok) {
+          throw new Error(`GitHub API responded with ${response.status}`);
+        }
         const data = await response.json();
         setStars(data.stargazers_count);
+        sessionStorage.setItem(
+          'cocoindex-github-stars',
+          JSON.stringify({ count: data.stargazers_count, ts: Date.now() }),
+        );
       } catch (error) {
         console.error('Error fetching GitHub stars:', error);
       } finally {
         setLoading(false);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/components/GitHubStar/index.tsx` around lines 22 - 36, Harden the
fetchStars flow in the existing useEffect by adding TTL-based caching for the
GitHub star count, validating response.ok before parsing or updating state, and
using an AbortController with a timeout that is always cleared. Preserve the
loading cleanup on success, failure, rate limits, and timeouts, and avoid
replacing a valid cached count with invalid response data.
python/cocoindex/targets/doris.py (4)

55-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chain the re-raised ImportError to the original exception.

Ruff flags B904 at this site and at Lines 760-763, 805-808, 869-872, 987-990, and 1959-1962. Add from e (or from None) so the original cause is preserved in tracebacks.

♻️ Proposed fix for this occurrence
-    except ImportError:
+    except ImportError as e:
         raise ImportError(
             "aiohttp is required for Doris connector. "
             "Install it with: pip install aiohttp"
-        )
+        ) from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 55 - 59, Update the
ImportError handlers in the Doris connector, including the shown handler and the
corresponding handlers near the other reported locations, to explicitly chain
the raised ImportError with the caught exception using the appropriate from
clause. Preserve the existing error message and behavior while satisfying Ruff
B904.

Source: Linters/SAST tools


325-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid the function call in the argument default.

Ruff reports B008 here. RetryConfig is a mutable dataclass, so the default instance is shared across all calls. Use None and build the default inside the function, or read a module-level constant.

♻️ Proposed fix
+_DEFAULT_RETRY_CONFIG = RetryConfig()
+
+
 async def with_retry(
     operation: Callable[[], Awaitable[T]],
-    config: RetryConfig = RetryConfig(),
+    config: RetryConfig | None = None,
     operation_name: str = "operation",
     retryable_errors: tuple[type[Exception], ...] | None = None,
 ) -> T:
@@
+    if config is None:
+        config = _DEFAULT_RETRY_CONFIG
     if retryable_errors is None:
         retryable_errors = _get_retryable_errors()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 325 - 330, Update the
with_retry function signature to avoid constructing RetryConfig() as an argument
default; accept None for config, then instantiate a fresh RetryConfig inside
with_retry when no configuration is provided, preserving the existing behavior
for callers that pass a config.

Source: Linters/SAST tools


1044-1048: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Note on the Ruff S608 findings.

Ruff reports possible SQL injection at Lines 1022 and 1046. Both are false positives. spec.database, spec.table, and every entry in key_field_names pass _validate_identifier at Lines 993-996, and all values are parameterized. Add a # noqa: S608 with a short justification so the finding does not block the new Ruff check in CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 1044 - 1048, Add targeted
Ruff S608 suppressions with a brief justification to the SQL construction sites
around the validated identifier checks and parameterized DELETE template,
including the statement using spec.database, spec.table, and key_field_names.
Keep the existing _validate_identifier validation and parameter placeholders
unchanged.

Source: Linters/SAST tools


49-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the repeated lazy-import blocks.

aiomysql is imported lazily in four separate places with near-identical error messages, and aiohttp and pymysql each have their own helper. Extract one _require(module_name, purpose) helper and call it from every site. That removes the duplication and keeps the installation guidance consistent.

Also applies to: 283-302, 802-808, 866-872, 984-990, 1956-1962

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 49 - 59, Consolidate the lazy
dependency imports by adding a shared _require(module_name, purpose) helper that
imports the requested module and raises a consistent installation-guidance
ImportError when unavailable. Replace _get_aiohttp and every repeated aiomysql
and pymysql import block, including the sites around _get_aiohttp and the
referenced ranges, with calls to this helper while preserving each caller’s
purpose context.
python/cocoindex/tests/targets/test_doris_unit.py (2)

176-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for bytes conversion.

TestValueConversion covers UUID, NaN, None, lists, and dicts. It does not cover bytes, which _convert_value_for_doris handles at python/cocoindex/targets/doris.py Lines 432-433. That branch is lossy for non-UTF-8 input. A test would pin the intended behavior once the encoding is settled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_unit.py` around lines 176 - 190,
Add a bytes-conversion test to TestValueConversion.test_special_value_handling,
covering valid UTF-8 and non-UTF-8 input according to the intended
_convert_value_for_doris behavior. Assert the expected decoded or fallback
result so the bytes branch’s encoding behavior is explicitly preserved.

262-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for build_vector_search_query.

build_vector_search_query is a public helper that assembles SQL and performs its own injection validation. The suite covers _validate_identifier directly but never exercises the helper. Add cases for the database.table form, an invalid table name, an invalid select_columns entry, a non-positive limit, and the l2_distance versus inner_product ordering.

_types_compatible is also untested. It gates schema evolution and decides whether an existing column is accepted, so a regression there can corrupt data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_unit.py` around lines 262 - 274,
Extend TestIdentifierValidation or an appropriate nearby test class with
coverage for build_vector_search_query: verify database.table targets, rejection
of invalid table names and select_columns entries, rejection of non-positive
limits, and distinct l2_distance versus inner_product ordering. Add focused
tests for _types_compatible covering compatible and incompatible existing-column
types, including schema-evolution decisions.
docs/docs/targets/doris.md (1)

267-275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reference the packaging extra.

pyproject.toml Line 73 defines a doris extra that installs aiohttp, aiomysql, and pymysql. Point users at pip install 'cocoindex[doris]' so the dependency set stays in one place.

📝 Proposed change
 Install dependencies:
 ```bash
-pip install aiohttp aiomysql
+pip install 'cocoindex[doris]'
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/docs/targets/doris.md around lines 267 - 275, Update the Doris
documentation’s dependency installation command to use the packaging extra
defined by the doris extra, replacing the direct aiohttp and aiomysql
installation with pip install 'cocoindex[doris]'. Keep the requirements list
aligned with the documented dependency set.


</details>

<!-- cr-comment:v1:4cf247a08ceafec6321d64eb -->

</blockquote></details>

</blockquote></details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro Plus

**Run ID**: `a8f5325a-6b05-4a92-80b9-a469f29c9760`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ceda273d17636f44051e66ed08ec4f2f9d2a0bf7 and aaa4c1691747297553e39f998d9ce88dd56783eb.

</details>

<details>
<summary>⛔ Files ignored due to path filters (5)</summary>

* `docs/package-lock.json` is excluded by `!**/package-lock.json`
* `docs/static/img/logo-dark.svg` is excluded by `!**/*.svg`
* `docs/static/img/logo.svg` is excluded by `!**/*.svg`
* `docs/yarn.lock` is excluded by `!**/yarn.lock`, `!**/*.lock`
* `uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (89)</summary>

* `.github/workflows/CI.yml`
* `.github/workflows/_test.yml`
* `.github/workflows/docs_test.yml`
* `.github/workflows/e2e_type_check.yml`
* `.github/workflows/fast_check.yml`
* `.github/workflows/format.yml`
* `.github/workflows/links.yml`
* `.pre-commit-config.yaml`
* `docs/docs/targets/doris.md`
* `docs/docs/targets/index.md`
* `docs/docusaurus.config.ts`
* `docs/src/components/ActionButtons/index.tsx`
* `docs/src/components/Badge/index.tsx`
* `docs/src/components/Badge/styles.module.css`
* `docs/src/components/Button/index.tsx`
* `docs/src/components/Button/styles.module.css`
* `docs/src/components/Card/index.tsx`
* `docs/src/components/Card/styles.module.css`
* `docs/src/components/Color/colors.css`
* `docs/src/components/Color/index.ts`
* `docs/src/components/GitHubButton/index.tsx`
* `docs/src/components/GitHubStar/index.tsx`
* `docs/src/components/GitHubStar/styles.module.css`
* `docs/src/components/ImageCard/index.tsx`
* `docs/src/components/ImageCard/styles.module.css`
* `docs/src/components/Inset/index.tsx`
* `docs/src/components/Inset/styles.module.css`
* `docs/src/components/LastReviewed/index.tsx`
* `docs/src/components/LastReviewed/styles.module.css`
* `docs/src/components/RadioCards/index.tsx`
* `docs/src/components/RadioCards/styles.module.css`
* `docs/src/components/VersionSelector/index.tsx`
* `docs/src/components/VersionSelector/styles.module.css`
* `docs/src/css/custom.css`
* `docs/src/css/doc.css`
* `docs/src/css/scrollbar.css`
* `docs/src/css/sidebar.css`
* `docs/src/theme/Footer/index.tsx`
* `docs/src/theme/Footer/styles.module.css`
* `docs/src/theme/Navbar/index.tsx`
* `docs/src/theme/Navbar/styles.module.css`
* `examples/amazon_s3_embedding/pyproject.toml`
* `examples/azure_blob_embedding/pyproject.toml`
* `examples/code_embedding/pyproject.toml`
* `examples/custom_output_files/pyproject.toml`
* `examples/custom_source_hn/pyproject.toml`
* `examples/docs_to_knowledge_graph/pyproject.toml`
* `examples/face_recognition/pyproject.toml`
* `examples/fastapi_server_docker/requirements.txt`
* `examples/gdrive_text_embedding/pyproject.toml`
* `examples/hn_trending_topics/pyproject.toml`
* `examples/image_search/pyproject.toml`
* `examples/live_updates/pyproject.toml`
* `examples/manuals_llm_extraction/pyproject.toml`
* `examples/meeting_notes_graph/pyproject.toml`
* `examples/multi_format_indexing/pyproject.toml`
* `examples/paper_metadata/pyproject.toml`
* `examples/patient_intake_extraction/pyproject.toml`
* `examples/patient_intake_extraction_baml/pyproject.toml`
* `examples/patient_intake_extraction_dspy/pyproject.toml`
* `examples/pdf_elements_embedding/pyproject.toml`
* `examples/pdf_embedding/pyproject.toml`
* `examples/postgres_source/pyproject.toml`
* `examples/product_recommendation/pyproject.toml`
* `examples/slides_to_speech/.env.example`
* `examples/slides_to_speech/.gitignore`
* `examples/slides_to_speech/LICENSE`
* `examples/slides_to_speech/README.md`
* `examples/slides_to_speech/baml_src/generators.baml`
* `examples/slides_to_speech/baml_src/slides.baml`
* `examples/slides_to_speech/main.py`
* `examples/slides_to_speech/pyproject.toml`
* `examples/text_embedding/pyproject.toml`
* `examples/text_embedding_doris/.env`
* `examples/text_embedding_doris/.gitignore`
* `examples/text_embedding_doris/README.md`
* `examples/text_embedding_doris/main.py`
* `examples/text_embedding_doris/markdown_files/rfc8259.md`
* `examples/text_embedding_doris/pyproject.toml`
* `examples/text_embedding_lancedb/pyproject.toml`
* `examples/text_embedding_qdrant/pyproject.toml`
* `pyproject.toml`
* `python/cocoindex/engine_object.py`
* `python/cocoindex/flow.py`
* `python/cocoindex/targets/__init__.py`
* `python/cocoindex/targets/doris.py`
* `python/cocoindex/tests/targets/__init__.py`
* `python/cocoindex/tests/targets/test_doris_integration.py`
* `python/cocoindex/tests/targets/test_doris_unit.py`

</details>

<details>
<summary>💤 Files with no reviewable changes (18)</summary>

* docs/src/components/LastReviewed/styles.module.css
* docs/src/components/Inset/styles.module.css
* docs/src/css/scrollbar.css
* docs/src/components/Button/styles.module.css
* docs/src/components/ImageCard/styles.module.css
* docs/src/components/Card/styles.module.css
* docs/src/theme/Footer/index.tsx
* docs/src/components/Badge/index.tsx
* .github/workflows/format.yml
* docs/src/components/Button/index.tsx
* docs/src/components/Badge/styles.module.css
* docs/src/components/Inset/index.tsx
* docs/src/components/ImageCard/index.tsx
* docs/src/theme/Footer/styles.module.css
* docs/src/css/sidebar.css
* docs/src/components/RadioCards/styles.module.css
* docs/src/css/doc.css
* docs/src/components/Card/index.tsx

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +6 to +92
.githubStarButton {
display: inline-flex;
align-items: center;
gap: 4px;
height: 24px;
padding: 0 8px;
background-color: #f3f3f3;
color: #333;
border: 1px solid #d5d5d5;
border-radius: 3px;
text-decoration: none;
font-size: 12px;
font-weight: 600;
line-height: 24px;
transition: background-color 0.2s;
}

.githubStarButton:hover {
background-color: #e6e6e6;
text-decoration: none;
color: #333;
}

.githubStarText {
display: none;
}

@media (min-width: 640px) {
.githubStarText {
display: inline;
}
}

.githubStarCount {
width: 40px;
min-width: 40px;
height: 24px;
background-color: #f3f3f3;
color: #333;
position: relative;
margin-left: 5px;
padding: 0 5px;
font-size: 12px;
font-weight: 600;
line-height: 24px;
border: 1px solid #d5d5d5;
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
text-decoration: none;
transition: background-color 0.2s;
}

.githubStarCount:hover {
background-color: #e6e6e6;
text-decoration: none;
color: #333;
}

/* Arrow left pointing to the button */
.githubStarCount::after,
.githubStarCount::before {
content: "";
position: absolute;
right: 100%;
top: 50%;
height: 0;
width: 0;
pointer-events: none;
border-style: solid;
border-color: transparent;
border-left: 0;
}

.githubStarCount::after {
border-right-color: #f3f3f3;
border-width: 4px;
margin-top: -4px;
}

.githubStarCount::before {
border-right-color: #d5d5d5;
border-width: 5px;
margin-top: -5px;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add dark-mode styling for the GitHub star button.

The colors (#f3f3f3, #333, #d5d5d5) are hardcoded, with no [data-theme='dark'] overrides. VersionSelector/styles.module.css and Navbar/styles.module.css both adapt to dark mode using CSS custom properties. Without equivalent handling here, the GitHub star button and count badge keep a light background and border when the site is in dark mode, next to a dark navbar background.

Use the existing --navbar-* variables from custom.css, or add [data-theme='dark'] overrides consistent with the other navbar components.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/components/GitHubStar/styles.module.css` around lines 6 - 92, Update
the GitHub star styles around .githubStarButton and .githubStarCount to support
dark mode using the existing --navbar-* custom properties or matching
[data-theme='dark'] overrides. Ensure the button, count badge, hover states,
text colors, borders, and count-arrow colors switch from the hardcoded light
palette to colors consistent with the dark navbar theme.

@@ -1,4 +1,4 @@
cocoindex[embeddings]>=0.3.9
cocoindex[embeddings]>=0.3.26,<1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 2 '\b(starlette|fastapi|cocoindex)\b' examples/fastapi_server_docker/requirements.txt || true
fd -a -t f . | rg '(^|/)(uv\.lock|poetry\.lock|Pipfile\.lock|requirements[^/]*\.txt)$' \
  | xargs -r rg -n -C 2 '\bstarlette\b' || true

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 308


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- dependency files ---'
git ls-files \
  | rg '(^|/)(pyproject\.toml|requirements[^/]*\.txt|uv\.lock|poetry\.lock|Pipfile(\.lock)?|setup\.cfg|setup\.py)$' \
  | sort

printf '%s\n' '--- example requirements ---'
cat -n examples/fastapi_server_docker/requirements.txt

printf '%s\n' '--- FastAPI and Starlette constraints in repository ---'
rg -n -C 3 '\b(starlette|fastapi|cocoindex)(\[|[<>=!~]|[[:space:]]|$)' \
  --glob '*.toml' --glob '*.txt' --glob '*.lock' --glob '*.cfg' --glob '*.py' . \
  || true

printf '%s\n' '--- relevant package metadata from PyPI ---'
python3 - <<'PY'
import json
import urllib.request

for name, version in (("fastapi", "0.115.12"), ("starlette", "0.46.2")):
    url = f"https://pypi.org/pypi/{name}/{version}/json"
    try:
        with urllib.request.urlopen(url, timeout=15) as response:
            data = json.load(response)
        info = data["info"]
        print(f"{name}=={version}")
        print("requires_dist:")
        for requirement in info.get("requires_dist") or []:
            print(f"  {requirement}")
    except Exception as exc:
        print(f"{name}=={version}: {type(exc).__name__}: {exc}")
PY

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 46869


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- lockfile package entries ---'
rg -n -C 8 '^name = "(fastapi|starlette)"$' uv.lock

printf '%s\n' '--- lockfile dependency references ---'
rg -n -C 3 '\b(fastapi|starlette)\b' uv.lock

printf '%s\n' '--- PyPI metadata ---'
for spec in 'fastapi/0.115.12' 'starlette/0.46.2'; do
  printf '\n%s\n' "$spec"
  curl -fsSLk --max-time 20 "https://pypi.org/pypi/$spec/json" \
    | python3 -c '
import json, sys
data = json.load(sys.stdin)
info = data["info"]
print("version:", info["version"])
print("requires_dist:")
for req in info.get("requires_dist") or []:
    print(" ", req)
'
done

printf '%s\n' '--- Starlette release list ---'
curl -fsSLk --max-time 20 https://pypi.org/pypi/starlette/json \
  | python3 -c '
import json, sys
data = json.load(sys.stdin)
for version in sorted(data["releases"], key=lambda x: tuple(int(p) if p.isdigit() else p for p in x.replace("-", ".").split("."))):
    if version.startswith("0.4") or version.startswith("0.5"):
        print(version)
'

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 199


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- lockfile header and package count ---'
sed -n '1,80p' uv.lock
printf 'fastapi entries: '
rg -c '^name = "fastapi"$' uv.lock || true
printf 'starlette entries: '
rg -c '^name = "starlette"$' uv.lock || true

printf '%s\n' '--- PyPI metadata ---'
for spec in 'fastapi/0.115.12' 'starlette/0.46.2'; do
  printf '\n%s\n' "$spec"
  if ! curl -fsSLk --max-time 20 "https://pypi.org/pypi/$spec/json" \
    | python3 -c '
import json, sys
data = json.load(sys.stdin)
info = data["info"]
print("version:", info["version"])
print("requires_dist:")
for req in info.get("requires_dist") or []:
    print(" ", req)
'; then
    echo "metadata query failed"
  fi
done

printf '%s\n' '--- Starlette releases at and after 0.46.2 ---'
curl -fsSLk --max-time 20 https://pypi.org/pypi/starlette/json \
  | python3 -c '
import json, sys
from packaging.version import Version
data = json.load(sys.stdin)
for version in sorted(data["releases"], key=Version):
    if Version(version) >= Version("0.46.2"):
        print(version)
' || true

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 14102


🌐 Web query:

Starlette 0.46.2 vulnerability fixed version CVE advisory FastAPI compatibility

💡 Result:

Starlette version 0.46.2 is affected by multiple security vulnerabilities [1][2][3]. Users of this version should upgrade to a newer, patched version to mitigate these risks [3][4]. Key Security Findings for Starlette 0.46.2: - Host Header Validation: Version 0.46.2 is vulnerable to CVE-2026-48710 (also tracked as GHSA-86qp-5c8j-p5mr), where missing validation of the HTTP Host header can allow attackers to manipulate request.url.path, potentially bypassing security checks [3][4][5]. - SSRF via UNC Paths: It is vulnerable to CVE-2026-48818, which involves Server-Side Request Forgery (SSRF) and NTLM credential theft on Windows systems using UNC paths in StaticFiles [1][6]. - Resource Exhaustion/Throttling: The version is documented to be vulnerable to resource exhaustion (e.g., via request.form or multipart file uploads) and potential authentication bypass issues related to unsafe reflection [2]. Compatibility and Mitigation: - Upgrade Path: These issues have been addressed in later versions of Starlette [1][3]. Security advisories generally recommend upgrading to a version that includes the respective fixes (e.g., 1.0.1 or higher for the Host header and SSRF issues) [1][3][4]. - FastAPI Compatibility: FastAPI depends on Starlette; therefore, using an older, vulnerable version of Starlette may leave a FastAPI application exposed. FastAPI versions are typically updated to support newer Starlette ranges [7][8]. For example, FastAPI has historically updated its supported Starlette version ranges (e.g., >=0.40.0, <0.47.0; later increased to support >=0.48.0) to maintain compatibility while allowing for the necessary security updates [7][8]. It is strongly advised to check your project dependencies and upgrade to the latest stable version of Starlette to ensure both security and compatibility with the current FastAPI ecosystem [3][4][8].

Citations:


🌐 Web query:

FastAPI release Starlette 0.47.0 0.47.2 dependency constraint

💡 Result:

In the historical development of FastAPI, the dependency constraints for Starlette evolved to accommodate the releases of Starlette 0.47.0 and 0.47.2. FastAPI initially managed its Starlette dependency using version ranges to ensure compatibility [1]. When Starlette 0.47.0 was released, FastAPI updated its dependency constraints to starlette>=0.40.0,<0.48.0 [2]. This ensured that users could utilize the new features and fixes provided in Starlette 0.47.x while remaining within a range verified by FastAPI’s test suite [2]. Starlette 0.47.2 was subsequently released on July 20, 2025, primarily to fix an issue where UploadFile did not properly check for future rollovers [3][4]. Following this and subsequent Starlette releases, FastAPI continued to adjust its constraints [5]. By February 2026, FastAPI had further relaxed its dependency pin to starlette>=0.40.0,<1.0.0 to accommodate ongoing Starlette developments [6]. Users are generally advised not to manually pin the Starlette version [1], as FastAPI automatically specifies the compatible range for the version of FastAPI being used. As of August 3, 2026, FastAPI supports much newer versions of Starlette (having since moved to the 1.x.x series) [7].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- container build files ---'
git ls-files examples/fastapi_server_docker \
  | sort
for file in $(git ls-files examples/fastapi_server_docker | rg '(^|/)(Dockerfile|docker-compose[^/]*|requirements[^/]*\.txt)$'); do
  printf '\n--- %s ---\n' "$file"
  cat -n "$file"
done

printf '%s\n' '--- OSV advisory records ---'
for id in CVE-2026-48710 CVE-2026-48818 GHSA-86QP-5C8J-P5MR; do
  printf '\n%s\n' "$id"
  curl -fsSLk --max-time 20 "https://api.osv.dev/v1/vulns/$id" \
    | python3 -c '
import json, sys
data = json.load(sys.stdin)
print("summary:", data.get("summary"))
for affected in data.get("affected", []):
    print("package:", affected.get("package"))
    for r in affected.get("ranges", []):
        print(" range:", r.get("type"), r.get("events"))
    print("versions:", affected.get("versions"))
' || echo "advisory query failed"
done

printf '%s\n' '--- FastAPI releases that accept Starlette 0.47.2 or newer ---'
curl -fsSLk --max-time 20 https://pypi.org/pypi/fastapi/json \
  | python3 -c '
import json, sys, re
data = json.load(sys.stdin)
versions = []
for version, files in data["releases"].items():
    if not files or "post" in version:
        continue
    parts = version.split(".")
    if all(p.isdigit() for p in parts):
        versions.append((tuple(map(int, parts)), version))
for _, version in sorted(versions)[-40:]:
    try:
        raw = __import__("urllib.request", fromlist=["urlopen"]).urlopen(
            f"https://pypi.org/pypi/fastapi/{version}/json", timeout=10
        ).read()
        info = json.loads(raw)["info"]
        reqs = info.get("requires_dist") or []
        starlette = [r for r in reqs if r.lower().startswith("starlette")]
        if starlette:
            print(version, "=>", starlette[0])
    except Exception:
        pass
' || true

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 5734


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- container build file ---'
cat -n examples/fastapi_server_docker/dockerfile

printf '%s\n' '--- FastAPI releases and Starlette constraints ---'
versions=$(
  curl -fsSLk --max-time 20 https://pypi.org/pypi/fastapi/json \
  | python3 -c '
import json, sys
data = json.load(sys.stdin)
def key(v):
    out = []
    for part in v.split("."):
        digits = ""
        for char in part:
            if char.isdigit():
                digits += char
            else:
                break
        out.append(int(digits or 0))
    return tuple(out)
items = []
for version, files in data["releases"].items():
    if files and all(part.isdigit() for part in version.split(".")):
        items.append((key(version), version))
print("\n".join(v for _, v in sorted(items)[-20:]))
'
)
for version in $versions; do
  metadata=$(curl -fsSLk --max-time 20 "https://pypi.org/pypi/fastapi/$version/json" || true)
  [ -n "$metadata" ] || continue
  printf '%s: ' "$version"
  printf '%s' "$metadata" | python3 -c '
import json, sys
info = json.load(sys.stdin)["info"]
print(" | ".join(r for r in (info.get("requires_dist") or []) if r.lower().startswith("starlette")) or "no Starlette requirement")
' || true
done

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 1284


Update the FastAPI/Starlette dependency set before merging. The Dockerfile installs this file, and fastapi==0.115.12 permits affected starlette==0.46.2. Upgrade FastAPI as needed and constrain Starlette to a patched compatible version.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[HIGH] 1-1: starlette 0.46.2: BadHost: Missing Host header validation poisons request.url.path, bypassing path-based security checks

(PYSEC-2026-161)


[HIGH] 1-1: starlette 0.46.2: Starlette has possible denial-of-service vector when parsing large files in multipart forms

(PYSEC-2026-1941)


[HIGH] 1-1: starlette 0.46.2: Starlette vulnerable to O(n^2) DoS via Range header merging in starlette.responses.FileResponse

(PYSEC-2026-1942)


[HIGH] 1-1: starlette 0.46.2: undefined

(PYSEC-2026-2280)


[HIGH] 1-1: starlette 0.46.2: undefined

(PYSEC-2026-2281)


[HIGH] 1-1: starlette 0.46.2: undefined

(PYSEC-2026-248)


[HIGH] 1-1: starlette 0.46.2: undefined

(PYSEC-2026-249)


[HIGH] 1-1: starlette 0.46.2: Starlette has possible denial-of-service vector when parsing large files in multipart forms

(GHSA-2c2j-9gv5-cj73)


[HIGH] 1-1: starlette 0.46.2: Starlette vulnerable to O(n^2) DoS via Range header merging in starlette.responses.FileResponse

(GHSA-7f5h-v6xp-fcq8)


[HIGH] 1-1: starlette 0.46.2: Starlette: request.form() limits silently ignored for application/x-www-form-urlencoded enable DoS

(GHSA-82w8-qh3p-5jfq)


[HIGH] 1-1: starlette 0.46.2: Starlette has missing Host header validation that poisons request.url.path, bypassing path-based security checks

(GHSA-86qp-5c8j-p5mr)


[HIGH] 1-1: starlette 0.46.2: Starlette: Unvalidated request path concatenated into authority poisons request.url.hostname

(GHSA-jp82-jpqv-5vv3)


[HIGH] 1-1: starlette 0.46.2: Starlette: SSRF and NTLM credential theft via UNC paths in StaticFiles on Windows

(GHSA-wqp7-x3pw-xc5r)


[HIGH] 1-1: starlette 0.46.2: Starlette: Arbitrary HTTP method dispatched to HTTPEndpoint attributes via getattr

(GHSA-x746-7m8f-x49c)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/fastapi_server_docker/requirements.txt` at line 1, Update the
dependency entries in requirements.txt used by the FastAPI Docker build: upgrade
FastAPI as needed and add or update a Starlette constraint to a patched version
compatible with it, ensuring the resolved set cannot select affected
starlette==0.46.2.

Source: Linters/SAST tools

Comment on lines +99 to +109
# Convert PCM to MP3 using pydub
# Get audio parameters from first chunk
first_chunk = chunks[0]
audio = AudioSegment(
data=pcm_bytes,
sample_width=first_chunk.sample_width,
frame_rate=first_chunk.sample_rate,
channels=first_chunk.sample_channels,
)
mp3_data = io.BytesIO()
audio.export(mp3_data, format="mp3", bitrate="64k")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For pydub 0.25.1, does AudioSegment.export(..., format="mp3") require FFmpeg or Libav?

💡 Result:

Yes, for pydub 0.25.1, the AudioSegment.export(..., format="mp3") method requires FFmpeg or Libav to be installed and available on your system [1][2]. While pydub can natively open and save WAV files using Python's built-in libraries [3][2], it relies on external tools (FFmpeg or Libav) to perform encoding and decoding for most other formats, including MP3 [3][1]. When you export an AudioSegment to MP3, pydub internally invokes these tools to handle the conversion process [3][4]. If FFmpeg or Libav is not installed or not correctly configured in your system's PATH, this operation will fail [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file ---'
sed -n '1,150p' examples/slides_to_speech/main.py

printf '%s\n' '--- dependency and documentation references ---'
fd -a -t f 'pyproject.toml|README.*' . | sort
rg -n -i 'pydub|ffmpeg|libav|slides_to_speech|audio' pyproject.toml README* examples/slides_to_speech 2>/dev/null || true

printf '%s\n' '--- pydub-related configuration in repository ---'
rg -n -i 'AudioSegment|audio.export|ffmpeg|avconv|FFMPEG_BINARY|PATH' . -g '*.py' -g '*.toml' -g '*.md' -g '*.yaml' -g '*.yml' 2>/dev/null || true

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 30472


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- example dependency manifest ---'
cat -n examples/slides_to_speech/pyproject.toml

printf '%s\n' '--- example setup documentation ---'
cat -n examples/slides_to_speech/README.md

printf '%s\n' '--- project-level setup references ---'
rg -n -i 'ffmpeg|libav|apt|brew|choco|pydub' . -g 'README.md' -g '*.md' -g '*.toml' -g '*.yml' -g '*.yaml' 2>/dev/null || true

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 11369


Document and validate the FFmpeg requirement.

pydub requires FFmpeg or Libav for MP3 export. Add it to the example prerequisites and fail before processing when no encoder is available. Otherwise, installation succeeds and the first audio.export(..., format="mp3") call fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/slides_to_speech/main.py` around lines 99 - 109, Update the
prerequisites for the slide-to-speech example to document the required FFmpeg or
Libav encoder, and add an availability check before audio processing begins. Use
the existing startup or main flow and fail with a clear installation message
when no encoder is available, ensuring audio.export is only reached after
validation.

Comment on lines +5 to +12
requires-python = ">=3.11"
dependencies = [
"cocoindex[lancedb,embeddings]>=0.3.26,<1",
"python-dotenv>=1.0.1",
"baml-py==0.217.0,<0.218.0",
"pymupdf>=1.24.0",
"piper-tts>=1.2.0",
"pydub>=0.25.1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does pydub 0.25.1 support importing AudioSegment on Python 3.13, and what supported dependency resolves audioop removal?

💡 Result:

Pydub version 0.25.1 does not natively support Python 3.13 [1][2][3]. Attempting to import AudioSegment on Python 3.13 results in a ModuleNotFoundError because the audioop module was removed from the Python standard library in version 3.13 [1][4][3], and pydub's internal fallback (pyaudioop) fails to resolve correctly due to import issues within the library [1][5][6]. To resolve this dependency issue on Python 3.13, you can install the audioop-lts package, which provides a replacement for the removed audioop module [5][7][8]. It can be added as a dependency with a version marker to ensure it is only installed on Python 3.13 and newer [7]: audioop-lts; python_version >= '3.13' Alternatively, some users have reported success by fixing the internal import path within pydub's utils.py (changing import pyaudioop as audioop to from. import pyaudioop as audioop), which allows the library to locate its bundled fallback without requiring the external audioop-lts package [5][6][9].

Citations:


🏁 Script executed:

set -eu
printf '%s\n' '--- pyproject ---'
sed -n '1,80p' examples/slides_to_speech/pyproject.toml
printf '%s\n' '--- pydub imports and usage ---'
rg -n -C 3 'pydub|AudioSegment|audioop' examples/slides_to_speech
printf '%s\n' '--- repository Python-version constraints ---'
rg -n -C 2 '3\.13|requires-python|python_version' examples/slides_to_speech pyproject.toml 2>/dev/null || true

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 3512


Do not advertise Python 3.13 support.

main.py imports AudioSegment from pydub at startup. pydub>=0.25.1 does not support Python 3.13 because Python removed audioop. Restrict this example to Python 3.11–3.12, or add and test audioop-lts; python_version >= "3.13".

Proposed fix
-requires-python = ">=3.11"
+requires-python = ">=3.11,<3.13"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
requires-python = ">=3.11"
dependencies = [
"cocoindex[lancedb,embeddings]>=0.3.26,<1",
"python-dotenv>=1.0.1",
"baml-py==0.217.0,<0.218.0",
"pymupdf>=1.24.0",
"piper-tts>=1.2.0",
"pydub>=0.25.1",
requires-python = ">=3.11,<3.13"
dependencies = [
"cocoindex[lancedb,embeddings]>=0.3.26,<1",
"python-dotenv>=1.0.1",
"baml-py==0.217.0,<0.218.0",
"pymupdf>=1.24.0",
"piper-tts>=1.2.0",
"pydub>=0.25.1",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/slides_to_speech/pyproject.toml` around lines 5 - 12, Update the
requires-python setting in the project metadata to restrict the example to
Python 3.11–3.12, unless adding and validating the audioop-lts dependency for
Python 3.13 support. Keep the existing dependency declarations unchanged
otherwise.

Comment on lines +4 to +9
dependencies = [
"cocoindex",
"sentence-transformers",
"aiohttp",
"aiomysql",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare the Doris example dependency bounds with other examples and the declared optional extras.
set -uo pipefail

echo "== root optional-dependencies / extras =="
fd -t f 'pyproject.toml' -d 2 --exec sh -c 'echo "--- $1"; sed -n "1,200p" "$1"' _ {} \; | rg -n -A 20 'optional-dependencies|\[project\]' | head -120

echo
echo "== cocoindex bound in every example =="
fd -t f 'pyproject.toml' examples --exec rg -n -H 'cocoindex' {} \;

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 6520


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Doris example manifest =="
cat -n examples/text_embedding_doris/pyproject.toml

echo
echo "== Doris target files and imports =="
fd -t f -i 'doris' . | sort
rg -n -C 3 'targets\.doris|Doris|class Doris|def .*doris' --glob '*.py' --glob '*.toml' .

echo
echo "== Project version metadata =="
sed -n '1,110p' pyproject.toml
rg -n -C 3 'version|targets' cocoindex --glob '*.py' | head -160

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Example imports =="
rg -n 'sentence_transformers|cocoindex\.targets\.doris|aiohttp|aiomysql' examples/text_embedding_doris/main.py

echo
echo "== Declared extras =="
sed -n '68,84p' pyproject.toml

echo
echo "== Version bounds in nearby example manifests =="
for f in examples/text_embedding_doris/pyproject.toml examples/text_embedding/pyproject.toml examples/text_embedding_lancedb/pyproject.toml; do
  echo "--- $f"
  sed -n '1,14p' "$f"
done

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 1663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Doris example embedding usage =="
sed -n '35,90p' examples/text_embedding_doris/main.py

echo
echo "== Embedding helper definitions =="
rg -n -C 4 'SentenceTransformer|sentence.transformer|sentence_transformers|TextEmbedding' python/cocoindex --glob '*.py' | head -120

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 8432


Use the doris and embeddings extras.

Change the dependency to cocoindex[doris,embeddings]>=0.3.26,<1. Remove sentence-transformers, aiohttp, and aiomysql; both extras already declare these requirements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/text_embedding_doris/pyproject.toml` around lines 4 - 9, Update the
dependencies list to replace the bare cocoindex dependency with
cocoindex[doris,embeddings]>=0.3.26,<1, and remove the separately declared
sentence-transformers, aiohttp, and aiomysql entries.

Comment on lines +1614 to +1625
spec = DorisTarget(
fe_host=key.fe_host,
database=key.database,
table=key.table,
fe_http_port=state.fe_http_port,
query_port=state.query_port,
username=state.username,
password=state.password,
max_retries=state.max_retries,
retry_base_delay=state.retry_base_delay,
retry_max_delay=state.retry_max_delay,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Configured timeouts are dropped and never take effect.

This spec is the only DorisTarget used for DDL in apply_setup_change and _sync_indexes. It does not set schema_change_timeout or index_build_timeout, so both fall back to the class defaults of 60 and 300.

Every call site reads them from this object: Lines 1276, 1293, 1315, 1338, and 1801. get_setup_state stores the user values in _State at Lines 1547-1548, but nothing copies them back. The result is that schema_change_timeout and index_build_timeout are silently ignored at runtime.

test_timeout_config_propagated in python/cocoindex/tests/targets/test_doris_unit.py at Lines 341-359 only asserts the _State fields, so it passes while the feature is broken.

🐛 Proposed fix
         spec = DorisTarget(
             fe_host=key.fe_host,
             database=key.database,
             table=key.table,
             fe_http_port=state.fe_http_port,
             query_port=state.query_port,
             username=state.username,
             password=state.password,
             max_retries=state.max_retries,
             retry_base_delay=state.retry_base_delay,
             retry_max_delay=state.retry_max_delay,
+            schema_change_timeout=state.schema_change_timeout,
+            index_build_timeout=state.index_build_timeout,
+            auto_create_table=state.auto_create_table,
+            schema_evolution=state.schema_evolution,
         )

Extend the unit test to assert the effective timeout reaching the DDL path, not only the _State field.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
spec = DorisTarget(
fe_host=key.fe_host,
database=key.database,
table=key.table,
fe_http_port=state.fe_http_port,
query_port=state.query_port,
username=state.username,
password=state.password,
max_retries=state.max_retries,
retry_base_delay=state.retry_base_delay,
retry_max_delay=state.retry_max_delay,
)
spec = DorisTarget(
fe_host=key.fe_host,
database=key.database,
table=key.table,
fe_http_port=state.fe_http_port,
query_port=state.query_port,
username=state.username,
password=state.password,
max_retries=state.max_retries,
retry_base_delay=state.retry_base_delay,
retry_max_delay=state.retry_max_delay,
schema_change_timeout=state.schema_change_timeout,
index_build_timeout=state.index_build_timeout,
auto_create_table=state.auto_create_table,
schema_evolution=state.schema_evolution,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 1614 - 1625, Propagate
schema_change_timeout and index_build_timeout from _State into the DorisTarget
spec created for apply_setup_change and _sync_indexes, so configured values are
used instead of class defaults. Extend test_timeout_config_propagated to verify
the configured timeouts reach the DDL path, not only the stored _State fields.

Comment on lines +1789 to +1813
try:
await _execute_ddl(
spec,
f"ALTER TABLE `{key.database}`.`{key.table}` "
f"ADD COLUMN `{field.name}` {doris_type} {nullable}{default_clause}",
)
_logger.info("Added column %s to table %s", field.name, key.table)

# Wait for schema change to complete before proceeding
# Doris tables go through SCHEMA_CHANGE state during ALTER TABLE
# and reject writes to newly added columns until complete
await _wait_for_schema_change(
spec, key, timeout=spec.schema_change_timeout
)

# Update actual_schema with the new column
actual_schema[field.name] = _ColumnInfo(
name=field.name,
doris_type=doris_type,
nullable=nullable == "NULL",
is_key=False,
dimension=_extract_vector_dimension(field.value_type),
)
except Exception as e: # pylint: disable=broad-except
_logger.warning("Failed to add column %s: %s", field.name, e)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

A failed ADD COLUMN is swallowed and never reported.

The except at Line 1812 logs a warning and continues. The column is then still absent from actual_schema. The verification loop at Lines 1816-1825 skips any field in missing_columns, so it cannot detect the failure either. Setup therefore reports success while the target table lacks a required column, and the failure only appears later as Stream Load errors or dropped field data.

The _wait_for_schema_change result at Line 1800 is also discarded. Every other call site raises DorisSchemaError on timeout. A timeout here leaves the table in SCHEMA_CHANGE state while the code proceeds to create indexes.

Raise DorisSchemaError in both cases.

🐛 Proposed fix
-                    await _wait_for_schema_change(
-                        spec, key, timeout=spec.schema_change_timeout
-                    )
+                    if not await _wait_for_schema_change(
+                        spec, key, timeout=spec.schema_change_timeout
+                    ):
+                        raise DorisSchemaError(
+                            f"Timeout waiting for ADD COLUMN {field.name} on table {key.table}"
+                        )
@@
-                except Exception as e:  # pylint: disable=broad-except
-                    _logger.warning("Failed to add column %s: %s", field.name, e)
+                except DorisSchemaError:
+                    raise
+                except Exception as e:  # pylint: disable=broad-except
+                    raise DorisSchemaError(
+                        f"Failed to add column {field.name} to table {key.table}: {e}",
+                        field_name=field.name,
+                    ) from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
await _execute_ddl(
spec,
f"ALTER TABLE `{key.database}`.`{key.table}` "
f"ADD COLUMN `{field.name}` {doris_type} {nullable}{default_clause}",
)
_logger.info("Added column %s to table %s", field.name, key.table)
# Wait for schema change to complete before proceeding
# Doris tables go through SCHEMA_CHANGE state during ALTER TABLE
# and reject writes to newly added columns until complete
await _wait_for_schema_change(
spec, key, timeout=spec.schema_change_timeout
)
# Update actual_schema with the new column
actual_schema[field.name] = _ColumnInfo(
name=field.name,
doris_type=doris_type,
nullable=nullable == "NULL",
is_key=False,
dimension=_extract_vector_dimension(field.value_type),
)
except Exception as e: # pylint: disable=broad-except
_logger.warning("Failed to add column %s: %s", field.name, e)
try:
await _execute_ddl(
spec,
f"ALTER TABLE `{key.database}`.`{key.table}` "
f"ADD COLUMN `{field.name}` {doris_type} {nullable}{default_clause}",
)
_logger.info("Added column %s to table %s", field.name, key.table)
# Wait for schema change to complete before proceeding
# Doris tables go through SCHEMA_CHANGE state during ALTER TABLE
# and reject writes to newly added columns until complete
if not await _wait_for_schema_change(
spec, key, timeout=spec.schema_change_timeout
):
raise DorisSchemaError(
f"Timeout waiting for ADD COLUMN {field.name} on table {key.table}"
)
# Update actual_schema with the new column
actual_schema[field.name] = _ColumnInfo(
name=field.name,
doris_type=doris_type,
nullable=nullable == "NULL",
is_key=False,
dimension=_extract_vector_dimension(field.value_type),
)
except DorisSchemaError:
raise
except Exception as e: # pylint: disable=broad-except
raise DorisSchemaError(
f"Failed to add column {field.name} to table {key.table}: {e}",
field_name=field.name,
) from e
🧰 Tools
🪛 Ruff (0.16.0)

[warning] 1812-1812: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 1789 - 1813, Update the ADD
COLUMN handling around _execute_ddl and _wait_for_schema_change so any failed
ALTER TABLE or schema-change timeout raises DorisSchemaError instead of being
swallowed. Preserve the existing actual_schema update only after both operations
complete successfully, and remove or replace the broad warning-only exception
path so setup cannot proceed with a missing column or incomplete schema change.

Comment on lines +1878 to +1913
async with context.lock:
# For DUPLICATE KEY tables, we must delete existing rows before inserting
# to prevent accumulating duplicate rows with the same key.
# This ensures idempotent upsert behavior.
#
# WARNING: This is NOT atomic. If stream load fails after deletes,
# the deleted rows are lost. Use smaller batch_size to minimize risk.
if upserts:
# Extract keys from upserts for deletion
upsert_keys = [
{name: row[name] for name in key_field_names} for row in upserts
]

# Delete existing rows first
deleted_count = 0
for i in range(0, len(upsert_keys), context.spec.batch_size):
batch = upsert_keys[i : i + context.spec.batch_size]
deleted_count += await _execute_delete(
context.spec, key_field_names, batch
)

# Process inserts in batches via Stream Load
# If this fails, deleted rows cannot be recovered
try:
for i in range(0, len(upserts), context.spec.batch_size):
batch = upserts[i : i + context.spec.batch_size]
await _stream_load(context.session, context.spec, batch)
except Exception as e:
if deleted_count > 0:
_logger.error(
"Stream Load failed after deleting %d rows. "
"Data loss may have occurred. Error: %s",
deleted_count,
e,
)
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The delete-then-insert upsert can lose committed data.

The inline comment records the risk. The impact deserves a stronger mitigation than a smaller batch_size.

The deletes at Lines 1893-1897 commit immediately, because autocommit=True. If the process crashes between the delete loop and the Stream Load, the rows are gone permanently and no retry recovers them. Reducing batch_size narrows the window but does not remove it.

Two options limit the exposure:

  • Reverse the order. Insert the new rows first, then delete only the older duplicates. That leaves transient duplicates instead of missing rows, and a re-run converges.
  • Interleave per batch, so a failure affects one batch rather than the full mutation set.

Also document the failure mode in docs/docs/targets/doris.md, because it changes the durability guarantee that users expect from a target.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/targets/doris.py` around lines 1878 - 1913, The current
upsert flow in the context.lock block deletes all existing rows before any
Stream Load, risking permanent data loss on interruption. Change the
implementation to avoid the full-set delete-first window by either inserting
each batch before deleting its older duplicates or interleaving deletion and
insertion per batch, while preserving retry convergence; update the related
Doris target documentation to describe the remaining failure mode and durability
guarantee.

Comment on lines +127 to +171
@pytest.fixture(scope="module")
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
"""Create event loop for async tests."""
loop = asyncio.new_event_loop()
yield loop
loop.close()


@pytest.fixture
def unique_table_name() -> str:
"""Generate unique table name for each test."""
return f"test_{int(time.time())}_{uuid.uuid4().hex[:8]}"


@pytest.fixture
def doris_spec(test_config: dict[str, Any], unique_table_name: str) -> DorisTarget:
"""Create DorisTarget spec for testing."""
return DorisTarget(
fe_host=test_config["fe_host"],
fe_http_port=test_config["fe_http_port"],
query_port=test_config["query_port"],
username=test_config["username"],
password=test_config["password"],
database=test_config["database"],
table=unique_table_name,
replication_num=1,
buckets=1, # Small for testing
)


@pytest.fixture
def cleanup_table(
doris_spec: DorisTarget, event_loop: asyncio.AbstractEventLoop
) -> Generator[None, None, None]:
"""Cleanup table after test."""
yield
try:
event_loop.run_until_complete(
_execute_ddl(
doris_spec,
f"DROP TABLE IF EXISTS {doris_spec.database}.{doris_spec.table}",
)
)
except Exception as e:
print(f"Cleanup failed: {e}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find the pinned pytest-asyncio version and its configured mode.
set -uo pipefail

echo "== pytest-asyncio pins =="
fd -t f -e toml -e txt -e cfg -e ini . -d 3 --exec rg -n -H 'pytest-asyncio|pytest_asyncio' {} \;

echo
echo "== asyncio_mode configuration =="
fd -t f 'pyproject.toml|pytest.ini|setup.cfg|tox.ini' -d 3 --exec rg -n -H -A 6 'pytest\.ini_options|\[pytest\]|asyncio_mode' {} \;

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== dependency configuration =="
sed -n '60,105p' pyproject.toml

echo
echo "== test module imports and fixtures =="
sed -n '1,220p' python/cocoindex/tests/targets/test_doris_integration.py

echo
echo "== pytest-asyncio usage and fixture configuration =="
rg -n -H 'pytest_asyncio|pytest\.fixture|pytestmark|asyncio_mode|event_loop|cleanup_table|ensure_database' \
  pyproject.toml python/cocoindex/tests python 2>/dev/null | head -240

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 23060


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pytest configuration and lock files =="
rg -n -H -A 12 -B 4 'pytest|pytest-asyncio|asyncio_mode|requires-python' \
  pyproject.toml uv.lock poetry.lock pdm.lock requirements*.txt setup.cfg tox.ini pytest.ini 2>/dev/null || true

echo
echo "== async test declarations in the Doris module =="
rg -n -H -A 3 -B 2 'pytest\.mark\.asyncio|async def test_|class Test' \
  python/cocoindex/tests/targets/test_doris_integration.py | head -240

echo
echo "== fixture dependencies and imports =="
rg -n -H 'from pytest_asyncio|import pytest_asyncio|`@pytest_asyncio`|cleanup_table|ensure_database' \
  python pyproject.toml

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 29365


🌐 Web query:

pytest-asyncio official documentation overriding event_loop fixture removed 1.0 0.23

💡 Result:

In pytest-asyncio, the practice of overriding the event_loop fixture was deprecated starting in version 0.22 and discouraged in version 0.23 [1][2]. While the fixture was not outright removed in 0.23, the library introduced fundamental architectural changes that made overriding it problematic and largely unnecessary [3][4][5]. Key details regarding this change include: Why it was deprecated: Historically, users overrode the event_loop fixture to change its scope (e.g., to module or session scope) or to add custom setup/teardown logic [6]. This was frequently identified as a source of bugs, code duplication, and complexity, as it forced users to manually manage the event loop lifecycle [6][3]. The shift in version 0.23: As of version 0.23, pytest-asyncio attaches an event loop to each item in the test suite (session, package, module, class, and function) [4][5]. Instead of manually overriding the fixture, users are encouraged to define the scope of the event loop directly using the loop_scope keyword argument in the @pytest.mark.asyncio decorator for tests or @pytest_asyncio.fixture for fixtures [7][8][9][10]. Migration path: If your test suite previously relied on overriding the event_loop fixture, the recommended migration path is: 1. Remove the custom event_loop fixture implementation [8][11]. 2. Configure the desired loop_scope for your tests and fixtures using markers or the asyncio_default_fixture_loop_scope configuration option [7][8][9]. 3. If you need to change the event loop policy (rather than the fixture itself), use the event_loop_policy fixture instead [12]. For complex test suites that cannot easily adopt the new scoping model, documentation suggests maintaining the v0.21 release until these architectural concerns are fully addressed [4][5]. Comprehensive migration guides are available in the official documentation for moving from v0.21 [8][13] and v0.23 [7][9].

Citations:


🌐 Web query:

site:pytest-asyncio.readthedocs.io/en/v1.0.0 event_loop fixture removed override pytest-asyncio

💡 Result:

The event_loop fixture has been removed in pytest-asyncio 1.0.0 [1]. Previously, it was common to override this fixture to control the event loop's scope or policy; however, this is no longer supported or necessary [2]. To migrate away from an overridden event_loop fixture, use the following approaches supported by modern pytest-asyncio: 1. Use the loop_scope Argument: Instead of manually creating an event loop fixture, specify the desired scope (e.g., "session", "module", "class", "package", "function") using the loop_scope argument in the @pytest_asyncio.fixture decorator or the @pytest.mark.asyncio marker [2][3][4]. Example: @pytest_asyncio.fixture(loop_scope="module") async def my_fixture:... @pytest.mark.asyncio(loop_scope="module") async def test_example(my_fixture):... 2. Configure Default Loop Scope: You can set a project-wide default for fixture loop scopes by adding the asyncio_default_fixture_loop_scope option to your pytest configuration file (e.g., pytest.ini, pyproject.toml) [5][6]. Example (pytest.ini): [pytest] asyncio_default_fixture_loop_scope = function 3. Accessing the Current Loop: Within async tests or fixtures, simply use asyncio.get_running_loop to access the active event loop, as pytest-asyncio now manages the loop automatically based on the configured scope [2]. If your project previously relied on a custom event loop policy, you should now use the event_loop_policy fixture provided by pytest-asyncio to handle non-default or multiple event loop requirements [1].

Citations:


🌐 Web query:

site:pytest-asyncio.readthedocs.io/en/v1.3.0 event_loop fixture override fixture no longer supported

💡 Result:

The event_loop fixture has been deprecated in modern versions of pytest-asyncio (as of v1.3.0 and preceding versions) [1]. Overriding it is no longer the recommended or supported way to manage event loop scope or configuration [2][3]. Instead of re-implementing or overriding the event_loop fixture, you should use the following modern mechanisms provided by pytest-asyncio: 1. Configuring Event Loop Scope: If you need tests or fixtures to share a specific event loop (e.g., class, module, or session scope), use the loop_scope argument [2][4][5]. - For tests: Use the pytest.mark.asyncio marker: @pytest.mark.asyncio(loop_scope="module") [5]. - For fixtures: Use the @pytest_asyncio.fixture decorator: @pytest_asyncio.fixture(loop_scope="module") [4]. - Global default: You can set the default scope for all fixtures via the asyncio_default_fixture_loop_scope configuration option in your pytest.ini or pyproject.toml [2][6]. 2. Customizing Event Loop Policy: If your goal is to change the event loop policy (e.g., using a different loop implementation), override the event_loop_policy fixture instead [7][8]. This fixture remains the supported way to provide a custom policy [7]. If you currently have a custom event_loop fixture, the migration path is to remove it [2] and adopt the loop_scope and policy-based configuration methods described above [3]. If you still see deprecation warnings, ensure your configuration does not have re-implementations of the event_loop fixture and consider setting asyncio_default_fixture_loop_scope to "function" (or your preferred scope) [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact pytest-asyncio lock entry =="
sed -n '1808,1830p' uv.lock

echo
echo "== all Doris fixture definitions and test markers =="
sed -n '115,205p' python/cocoindex/tests/targets/test_doris_integration.py
sed -n '275,325p' python/cocoindex/tests/targets/test_doris_integration.py

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 6270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

lock = Path("uv.lock").read_text()
test = Path("python/cocoindex/tests/targets/test_doris_integration.py").read_text()

match = re.search(
    r'\[\[package\]\]\s*name = "pytest-asyncio"\s*version = "([^"]+)"',
    lock,
)
assert match, "pytest-asyncio is not present in uv.lock"
version = match.group(1)

event_loop_fixture = re.search(
    r'`@pytest`\.fixture\(scope="module"\)\s+def event_loop\b', test
)
event_loop_consumers = len(re.findall(r'\bevent_loop:\s*asyncio\.AbstractEventLoop', test))
asyncio_import = "import pytest_asyncio" in test or "from pytest_asyncio" in test

print(f"locked pytest-asyncio version: {version}")
print(f"custom event_loop fixture present: {bool(event_loop_fixture)}")
print(f"event_loop type-annotated consumers: {event_loop_consumers}")
print(f"pytest_asyncio import present: {asyncio_import}")
PY

Repository: LLM-CR-EVAL/cocoindex-94-cr1

Length of output: 319


Remove the custom event_loop fixture. uv.lock selects pytest-asyncio 1.3.0, which no longer provides this fixture. Convert both cleanup_table and ensure_database to @pytest_asyncio.fixture fixtures and await _execute_ddl on the active test loop.

🧰 Tools
🪛 Ruff (0.16.0)

[warning] 170-170: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_integration.py` around lines 127 -
171, Remove the custom event_loop fixture, then convert cleanup_table and
ensure_database to pytest_asyncio.fixture fixtures. Make their teardown/setup
paths asynchronous and await _execute_ddl directly on the active test loop,
eliminating event_loop injection and run_until_complete usage.

Comment on lines +341 to +359
def test_timeout_config_propagated(self) -> None:
"""Test that timeout configs are propagated from DorisTarget to _State."""
spec = DorisTarget(
fe_host="localhost",
database="test",
table="test_table",
schema_change_timeout=120, # Non-default
index_build_timeout=600, # Non-default
)
key_fields = [_mock_field("id", "Int64")]
value_fields = [_mock_field("content", "Str")]
index_options = IndexOptions(primary_key_fields=["id"])

state = _Connector.get_setup_state(
spec, key_fields, value_fields, index_options
)

assert state.schema_change_timeout == 120
assert state.index_build_timeout == 600

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

This test does not cover the timeout behavior.

The assertions only confirm that get_setup_state copies the values into _State. The values are never read from _State at runtime. apply_setup_change builds a separate DorisTarget at python/cocoindex/targets/doris.py Lines 1614-1625 and omits both timeout fields, so the DDL path always uses the defaults.

Extend the test to assert the timeout that reaches the DDL path. See the related comment on python/cocoindex/targets/doris.py Lines 1614-1625.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/cocoindex/tests/targets/test_doris_unit.py` around lines 341 - 359,
The test test_timeout_config_propagated only verifies values copied into _State,
not their use by apply_setup_change. Extend it to exercise the DDL setup path
and assert the reconstructed DorisTarget receives schema_change_timeout=120 and
index_build_timeout=600, preserving the existing state assertions.

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.

3 participants