feat: Add Apache Doris 4.0 vector database connector - #1
Conversation
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
📝 WalkthroughWalkthroughThis 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. ChangesCI and validation
Apache Doris support
Serialization and documentation
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
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 winFix the configuration instructions.
Line 34 uses
.env.exmaple, so the copy command fails. The instructions also omitGEMINI_API_KEY, althoughbaml_src/slides.bamlrequires 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 winUse a consistent LanceDB metric. This export creates no vector index, so
table.search()uses L2 distance whileQueryInfodeclares cosine similarity. Add.metric("cosine")or configure a cosine vector index, then keepscore = 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 winRecurse after converting NumPy arrays to lists.
np.ndarray.tolist()preservesbytesvalues in object and byte-string arrays. Whenbytes_to_base64=True, returning the list directly bypasses recursive conversion. Wrapv.tolist()indump_engine_object(..., bytes_to_base64=bytes_to_base64)and add a regression test fornp.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 winFix 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_rowasserts nothing about the upsert.The query uses
LIMIT 1on a table that already contains at least one row, solen(query_result) >= 1always holds. The test does not verify that the second Stream Load updated the value.test_upsert_idempotent_no_duplicatesat 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 winRemove
math.sqrtfrom the Doris L2 distance.l2_distance_approximatereturns the Euclidean distance, not its square, somath.sqrt(result[3])reports an incorrect score. Useresult[3]directly and usefloat("inf")or omitscorewhen 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 winThe assertion cannot fail.
SHOW DATABASESreturns at least one row on any reachable instance. Theor len(db_names) > 0branch therefore always holds, and theinformation_schemacheck never applies. The test only proves that_execute_ddldid 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 winUpdate the VeloDB documentation link. The
5.x-previewURL permanently redirects through26.xto 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 winRename the keyframe to kebab-case.
Stylelint flags
fadeInas violatingkeyframes-name-pattern. Rename it tofade-into 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 winTrigger this workflow when the mypy hook configuration changes.
build-testandfast-checkskipmypy-check. A PR that changes only.pre-commit-config.yamldoes not run the changed hook because these path filters exclude that file. Add.pre-commit-config.yamlto 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 winLog 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 thannwill 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_typeconverts 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 winDocument the timeout options.
DorisTargetdeclaresschema_change_timeout(default60) andindex_build_timeout(default300) atpython/cocoindex/targets/doris.pyLines 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 winAdd
strict=Trueto thezipcall.Ruff reports B905 at Line 1861. If the key tuple length does not match
key_field_names,ziptruncates silently. The resultingkey_dictthen identifies the wrong row, and both the delete and the Stream Load write operate on incorrect keys.strict=Trueconverts 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 winReject non-finite values in the query vector.
float(v)rendersnanandinfas the literalsnanandinf. Those are not valid Doris array literals, so the generated SQL fails at the server with an unclear parse error. Validate the values and raiseValueErrorin 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 winUpdate the Doris index examples to use the public API.
cocoindex.VectorIndexandcocoindex.FtsIndexare not public symbols. UseVectorIndexDef(field_name=..., metric=..., method=...)andFtsIndexDef(field_name=..., parameters=...). Pass them throughvector_indexes=andfts_indexes=, and useprimary_key_fields=inflow_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 winUse the retry loop here as well.
test_remove_index_from_existing_tablereplaced a fixed sleep with a bounded retry loop, because Doris applies index changes asynchronously. This test still uses a singleasyncio.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 valueRename the unused loop variable and initialize
create_stmt.Ruff reports B007 for
iat line 975. The variable is not used in the body. Line 987 also readscreate_stmtin theelsebranch. If a user setsDORIS_ASYNC_TIMEOUT=0,range(0)is empty, the loop body never runs, and line 987 raisesNameErrorinstead 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 valueThese two tests duplicate each other.
test_default_config_valuesandtest_default_config_values_match_docsconstruct the same spec and assert the same defaults. The second addsschema_evolutionand assertion messages; every other check repeats the first. Keep one and move theschema_evolutionassertion 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 winDo not swallow every
BUILD INDEXerror.The bare
except Exception: passhides 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 winTwo tests assert only the values they just set.
test_batch_size_respectedcreates a real table and then assertsspec.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_usedhas the same shape: it constructs a spec and asserts the three retry attributes it passed in, whichtest_custom_config_valuesat line 1941 already covers.Both names claim behaviour that the bodies do not verify, and
test_batch_size_respectedcosts a network round trip and a table. Either assert the real behaviour, or remove both tests.To verify batching, load more rows than
batch_sizethrough_Connector.mutateand 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 winThese tests do not exercise the vector index.
Both queries call the exact
l2_distancefunction. The comment states that the index acceleratesORDER BY, but the result set is identical with or without an index on a three-row and five-row table. The assertionquery_result[0]["content"] == "Apple"therefore passes even if index creation silently failed.
test_build_vector_search_query_l2_distanceat line 2876 shows the connector emitsl2_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 valueConsolidate
_mock_stateand_state_with_creds.
_state_with_credsat line 2232 duplicates the credential-copying logic in_mock_state. The two helpers also differ silently:_state_with_credssetsreplication_num=1andbuckets=1, while_mock_stateleaves the defaults. Tests that switch between the helpers get different table properties without stating why.Move
_state_with_credsnext to_mock_stateand let_mock_statebuild 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 winNarrow the expected exception type.
_execute_ddlraisesaiomysql.OperationalErrorfor authentication failure andDorisConnectionErrorafter retryable connection failures. Usepytest.raises((DorisConnectionError, aiomysql.OperationalError)).aiomysqlis 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 valueConsider 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-levelaiomysqlpool, 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 winAccount for the default
leftposition.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 explicitpositionsilently drops out of bothleftItemsandrightItems, instead of appearing on the left as Docusaurus would normally render it.All current items in
docusaurus.config.tssetpositionexplicitly, 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 winDeduplicate the version-navigation logic.
The keyboard handler (lines 64-81) reimplements the same
newPathresolution and navigation ashandleVersionChange(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
handleVersionChangeso 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 winHandle the case where no path pattern matches.
If
location.pathnamedoes not contain/examplesor/docs,newPathstays'', andwindow.location.href = ''triggers a full reload of the current page instead of a deliberate navigation. SinceVersionSelectorrenders 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 valueRemove dead CSS for the retired GitHub iframe link.
.navbar-github-linkstyled the previous iframe-based GitHub star button. That navbar item is replaced by the "Enterprise" link indocusaurus.config.ts, and the newGitHubStarcomponent uses its ownstyles.module.cssclasses, 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 winHarden the GitHub star fetch against failures and rate limits.
The fetch call has three gaps:
- It does not check
response.okbefore callingresponse.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
loadingtrue forever with no user feedback.Add a caching layer with a TTL and validate
response.ok, and givefetcha timeout viaAbortController.♻️ 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 winChain the re-raised
ImportErrorto 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(orfrom 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 winAvoid the function call in the argument default.
Ruff reports B008 here.
RetryConfigis a mutable dataclass, so the default instance is shared across all calls. UseNoneand 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 valueNote 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 inkey_field_namespass_validate_identifierat Lines 993-996, and all values are parameterized. Add a# noqa: S608with 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 winConsolidate the repeated lazy-import blocks.
aiomysqlis imported lazily in four separate places with near-identical error messages, andaiohttpandpymysqleach 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 winAdd a test for
bytesconversion.
TestValueConversioncovers UUID, NaN,None, lists, and dicts. It does not coverbytes, which_convert_value_for_dorishandles atpython/cocoindex/targets/doris.pyLines 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 winAdd tests for
build_vector_search_query.
build_vector_search_queryis a public helper that assembles SQL and performs its own injection validation. The suite covers_validate_identifierdirectly but never exercises the helper. Add cases for thedatabase.tableform, an invalid table name, an invalidselect_columnsentry, a non-positivelimit, and thel2_distanceversusinner_productordering.
_types_compatibleis 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 winReference the packaging extra.
pyproject.tomlLine 73 defines adorisextra that installsaiohttp,aiomysql, andpymysql. Point users atpip 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.mdaround 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 -->
| .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; | ||
| } |
There was a problem hiding this comment.
🎯 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 | |||
There was a problem hiding this comment.
🔒 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' || trueRepository: 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}")
PYRepository: 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)
' || trueRepository: 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:
- 1: https://osv.dev/vulnerability/CVE-2026-48818
- 2: https://security.snyk.io/package/pip/starlette/0.46.2
- 3: https://osv.dev/vulnerability/CVE-2026-48710
- 4: GHSA-86qp-5c8j-p5mr
- 5: https://osv.dev/vulnerability/PYSEC-2026-161
- 6: https://osv.dev/vulnerability/PYSEC-2026-2281
- 7: ⬆️ Bump Starlette to allow up to 0.46.0:
>=0.40.0,<0.47.0fastapi/fastapi#13426 - 8: ⬆️ Upgrade Starlette supported version range to >=0.40.0,<0.49.0 fastapi/fastapi#14077
🌐 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:
- 1: https://fastapi.tiangolo.com/deployment/versions/
- 2: ⬆️ Bump Starlette to allow up to 0.47.0:
>=0.40.0,<0.48.0fastapi/fastapi#13743 - 3: https://github.com/encode/starlette/releases/tag/0.47.2
- 4: Bump starlette from 0.27.0 to 0.47.2 in the pip group across 1 directory mlz-cisx/eln-backend#2
- 5: ⬆️ Upgrade Starlette supported version range to >=0.40.0,<0.49.0 fastapi/fastapi#14077
- 6: ⬆️ Upgrade Starlette supported version range to
starlette>=0.40.0,<1.0.0fastapi/fastapi#14853 - 7: https://fastapi.tiangolo.com/release-notes/
🏁 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
' || trueRepository: 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
doneRepository: 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
[HIGH] 1-1: starlette 0.46.2: Starlette vulnerable to O(n^2) DoS via Range header merging in starlette.responses.FileResponse
[HIGH] 1-1: starlette 0.46.2: Starlette: request.form() limits silently ignored for application/x-www-form-urlencoded enable DoS
[HIGH] 1-1: starlette 0.46.2: Starlette has missing Host header validation that poisons request.url.path, bypassing path-based security checks
[HIGH] 1-1: starlette 0.46.2: Starlette: Unvalidated request path concatenated into authority poisons request.url.hostname
[HIGH] 1-1: starlette 0.46.2: Starlette: SSRF and NTLM credential theft via UNC paths in StaticFiles on Windows
[HIGH] 1-1: starlette 0.46.2: Starlette: Arbitrary HTTP method dispatched to HTTPEndpoint attributes via getattr
🤖 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
| # 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") |
There was a problem hiding this comment.
🩺 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:
- 1: https://github.com/jiaaro/pydub
- 2: https://github.com/jiaaro/pydub/blob/86210baf951613eb748f960dbb7471858606e26a/README.markdown
- 3: https://deepwiki.com/jiaaro/pydub/2.4-exporting-audio
- 4: https://github.com/jiaaro/pydub/blob/master/pydub/audio_segment.py
- 5: https://stackoverflow.com/questions/32073278/python-convert-mp3-to-wav-with-pydub
- 6: MP3 exports unreadable/corrupted jiaaro/pydub#806
🏁 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 || trueRepository: 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 || trueRepository: 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.
| 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", |
There was a problem hiding this comment.
🩺 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:
- 1: ImportError: No module named 'pyaudioop' on Python 3.13 – audioop removed, no pip package available jiaaro/pydub#863
- 2: https://bugs.debian.org/1082254
- 3: ModuleNotFoundError: No module named 'pyaudioop' jiaaro/pydub#839
- 4: Adapt
pydubto Python 3.13 (audioop removed) jiaaro/pydub#867 - 5: audioop deprecation - Python 3.12 jiaaro/pydub#725
- 6: https://gitlab.archlinux.org/archlinux/packaging/packages/python-pydub/-/issues/1
- 7: https://pypi.org/project/audioop-lts/
- 8: 🐛 ModuleNotFoundError: No module named 'audioop' no Python 3.13.3 jiaaro/pydub#842
- 9: Adding audioop-lts for python >=3.13 jiaaro/pydub#816
🏁 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 || trueRepository: 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.
| 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.
| dependencies = [ | ||
| "cocoindex", | ||
| "sentence-transformers", | ||
| "aiohttp", | ||
| "aiomysql", | ||
| ] |
There was a problem hiding this comment.
🗄️ 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 -160Repository: 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"
doneRepository: 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 -120Repository: 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.
| 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, | ||
| ) |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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) |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| 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 |
There was a problem hiding this comment.
🗄️ 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.
| @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}") |
There was a problem hiding this comment.
🩺 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 -240Repository: 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.tomlRepository: 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:
- 1: https://pytest-asyncio.readthedocs.io/en/v0.23.0/reference/changelog.html
- 2: https://pytest-asyncio.readthedocs.io/en/v0.23.2/reference/changelog.html
- 3: Breaking change in 0.23.* pytest-dev/pytest-asyncio#706
- 4: https://pytest-asyncio.readthedocs.io/en/v0.23.5/reference/changelog.html
- 5: https://pytest-asyncio.readthedocs.io/en/v0.23.7/reference/changelog.html
- 6: Deprecate event_loop fixture overrides pytest-dev/pytest-asyncio#631
- 7: https://pytest-asyncio.readthedocs.io/en/stable/how-to-guides/migrate_from_0_23.html
- 8: https://pytest-asyncio.readthedocs.io/en/stable/how-to-guides/migrate_from_0_21.html
- 9: https://pytest-asyncio.readthedocs.io/en/v1.0.0/how-to-guides/migrate_from_0_23.html
- 10: Understand the changes of event-loop when moving from 0.21 pytest-dev/pytest-asyncio#1171
- 11: https://pytest-asyncio.readthedocs.io/en/v1.0.0/how-to-guides/migrate_from_0_21.html
- 12: https://github.com/pytest-dev/pytest-asyncio/blob/v0.23.0/docs/source/reference/fixtures/index.rst
- 13: https://pytest-asyncio.readthedocs.io/en/v1.3.0/how-to-guides/migrate%5Ffrom%5F0%5F21.html
🌐 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:
- 1: https://pytest-asyncio.readthedocs.io/en/v1.0.0/reference/changelog.html
- 2: https://pytest-asyncio.readthedocs.io/en/v1.0.0/how-to-guides/migrate_from_0_21.html
- 3: https://pytest-asyncio.readthedocs.io/en/v1.0.0/how-to-guides/change_fixture_loop.html
- 4: https://pytest-asyncio.readthedocs.io/en/v1.0.0/reference/decorators/
- 5: https://pytest-asyncio.readthedocs.io/en/v1.0.0/how-to-guides/change_default_fixture_loop.html
- 6: https://pytest-asyncio.readthedocs.io/en/v1.0.0/reference/configuration.html
🌐 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:
- 1: https://pytest-asyncio.readthedocs.io/en/v1.3.0/reference/changelog.html
- 2: https://pytest-asyncio.readthedocs.io/en/v1.3.0/how-to-guides/migrate%5Ffrom%5F0%5F21.html
- 3: https://pytest-asyncio.readthedocs.io/en/v1.3.0/how-to-guides/migrate_from_0_23.html
- 4: https://pytest-asyncio.readthedocs.io/en/v1.3.0/how-to-guides/change_fixture_loop.html
- 5: https://pytest-asyncio.readthedocs.io/en/v1.3.0/concepts.html
- 6: https://pytest-asyncio.readthedocs.io/en/v1.3.0/reference/configuration.html
- 7: https://pytest-asyncio.readthedocs.io/en/v1.3.0/reference/fixtures/index.html
- 8: https://pytest-asyncio.readthedocs.io/en/v1.3.0/how-to-guides/multiple_loops.html
🏁 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.pyRepository: 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}")
PYRepository: 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.
| 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 |
There was a problem hiding this comment.
🎯 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.
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
mandef_constructionparameterslistsparameterFull-Text Search
unicode,english,chineseData Ingestion
batch_size(default: 10,000 rows)Schema Management
Reliability
max_retries,retry_base_delay,retry_max_delayQuery Helpers
connect_async()- Async MySQL connection helper with proper cleanupbuild_vector_search_query()- SQL builder for vector similarity searchTest Coverage
Integration tests verified against VeloDB Cloud.
Documentation
docs/docs/targets/doris.mdExample
New
examples/text_embedding_doris/following the same pattern astext_embedding_lancedb:Requirements
aiohttp,aiomysqlBreaking Changes
None - this is a new target connector.
Summary by CodeRabbit
New Features
Bug Fixes
Chores