chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
@@ -0,0 +1,340 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.mongodb._client import (get_index_stats, get_indexes,
|
||||
get_validator, is_view,
|
||||
iter_documents, iter_inserts)
|
||||
|
||||
|
||||
class _AsyncIter:
|
||||
|
||||
def __init__(self, items):
|
||||
self._items = list(items)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if not self._items:
|
||||
raise StopAsyncIteration
|
||||
return self._items.pop(0)
|
||||
|
||||
|
||||
def _build_mock_client(docs):
|
||||
cursor = MagicMock()
|
||||
cursor.sort = MagicMock(return_value=cursor)
|
||||
cursor.batch_size = MagicMock(return_value=cursor)
|
||||
cursor.__aiter__ = lambda self: _AsyncIter(docs).__aiter__()
|
||||
col = MagicMock()
|
||||
col.find = MagicMock(return_value=cursor)
|
||||
db = MagicMock()
|
||||
db.__getitem__.return_value = col
|
||||
client = MagicMock()
|
||||
client.__getitem__.return_value = db
|
||||
return client, col, cursor
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_iter_documents_yields_each_doc_in_order():
|
||||
docs = [{"_id": i, "v": i * 10} for i in range(3)]
|
||||
client, col, cursor = _build_mock_client(docs)
|
||||
out = []
|
||||
async for doc in iter_documents(client, "db1", "coll1", batch_size=100):
|
||||
out.append(doc)
|
||||
assert out == docs
|
||||
col.find.assert_called_once_with({}, None)
|
||||
cursor.batch_size.assert_called_once_with(100)
|
||||
cursor.sort.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_iter_documents_applies_filter_projection_and_sort():
|
||||
docs = [{"_id": 1, "x": 5}]
|
||||
client, col, cursor = _build_mock_client(docs)
|
||||
out = []
|
||||
async for doc in iter_documents(client,
|
||||
"db1",
|
||||
"coll1",
|
||||
filter={"x": {
|
||||
"$gt": 0
|
||||
}},
|
||||
projection={"x": 1},
|
||||
sort=[("_id", -1)],
|
||||
batch_size=50):
|
||||
out.append(doc)
|
||||
assert out == docs
|
||||
col.find.assert_called_once_with({"x": {"$gt": 0}}, {"x": 1})
|
||||
cursor.sort.assert_called_once_with([("_id", -1)])
|
||||
cursor.batch_size.assert_called_once_with(50)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_iter_documents_empty_yields_nothing():
|
||||
client, _, _ = _build_mock_client([])
|
||||
out = []
|
||||
async for doc in iter_documents(client, "db1", "coll1"):
|
||||
out.append(doc)
|
||||
assert out == []
|
||||
|
||||
|
||||
def _build_indexes_client(spec, indexes):
|
||||
spec_cursor = MagicMock()
|
||||
spec_cursor.__aiter__ = lambda self: _AsyncIter([spec]
|
||||
if spec else []).__aiter__(
|
||||
)
|
||||
idx_cursor = MagicMock()
|
||||
idx_cursor.__aiter__ = lambda self: _AsyncIter(indexes).__aiter__()
|
||||
col = MagicMock()
|
||||
col.list_indexes = AsyncMock(return_value=idx_cursor)
|
||||
db = MagicMock()
|
||||
db.list_collections = AsyncMock(return_value=spec_cursor)
|
||||
db.__getitem__.return_value = col
|
||||
client = MagicMock()
|
||||
client.__getitem__.return_value = db
|
||||
return client, col, db
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_view_true_when_spec_type_view():
|
||||
client, _, db = _build_indexes_client(
|
||||
spec={
|
||||
"name": "myview",
|
||||
"type": "view"
|
||||
},
|
||||
indexes=[],
|
||||
)
|
||||
assert await is_view(client, "db1", "myview") is True
|
||||
db.list_collections.assert_awaited_once_with(filter={"name": "myview"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_view_false_for_regular_collection():
|
||||
client, _, _ = _build_indexes_client(
|
||||
spec={
|
||||
"name": "coll1",
|
||||
"type": "collection"
|
||||
},
|
||||
indexes=[],
|
||||
)
|
||||
assert await is_view(client, "db1", "coll1") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_view_false_when_collection_absent():
|
||||
client, _, _ = _build_indexes_client(spec=None, indexes=[])
|
||||
assert await is_view(client, "db1", "missing") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_indexes_returns_indexes_for_collection():
|
||||
indexes = [{"name": "_id_", "key": {"_id": 1}}]
|
||||
client, col, db = _build_indexes_client(
|
||||
spec={
|
||||
"name": "coll1",
|
||||
"type": "collection"
|
||||
},
|
||||
indexes=indexes,
|
||||
)
|
||||
out = await get_indexes(client, "db1", "coll1")
|
||||
assert out == indexes
|
||||
db.list_collections.assert_awaited_once_with(filter={"name": "coll1"})
|
||||
col.list_indexes.assert_awaited_once_with()
|
||||
|
||||
|
||||
def _build_validator_client(spec):
|
||||
spec_cursor = MagicMock()
|
||||
spec_cursor.__aiter__ = lambda self: _AsyncIter([spec]
|
||||
if spec else []).__aiter__(
|
||||
)
|
||||
db = MagicMock()
|
||||
db.list_collections = AsyncMock(return_value=spec_cursor)
|
||||
client = MagicMock()
|
||||
client.__getitem__.return_value = db
|
||||
return client, db
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_validator_returns_json_schema_when_present():
|
||||
spec = {
|
||||
"name": "movies",
|
||||
"options": {
|
||||
"validator": {
|
||||
"$jsonSchema": {
|
||||
"bsonType": "object",
|
||||
"required": ["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
client, db = _build_validator_client(spec)
|
||||
out = await get_validator(client, "db1", "movies")
|
||||
assert out == {"bsonType": "object", "required": ["title"]}
|
||||
db.list_collections.assert_awaited_once_with(filter={"name": "movies"})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_validator_returns_none_without_validator():
|
||||
client, _ = _build_validator_client({"name": "movies", "options": {}})
|
||||
assert await get_validator(client, "db1", "movies") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_validator_returns_none_when_collection_missing():
|
||||
client, _ = _build_validator_client(None)
|
||||
assert await get_validator(client, "db1", "ghost") is None
|
||||
|
||||
|
||||
def _build_indexstats_client(rows):
|
||||
cursor = MagicMock()
|
||||
cursor.__aiter__ = lambda self: _AsyncIter(rows).__aiter__()
|
||||
col = MagicMock()
|
||||
col.aggregate = AsyncMock(return_value=cursor)
|
||||
db = MagicMock()
|
||||
db.__getitem__.return_value = col
|
||||
client = MagicMock()
|
||||
client.__getitem__.return_value = db
|
||||
return client, col
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_index_stats_returns_map_keyed_by_name():
|
||||
rows = [
|
||||
{
|
||||
"name": "_id_",
|
||||
"accesses": {
|
||||
"ops": 1234,
|
||||
"since": "2026-01-01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "title_text",
|
||||
"accesses": {
|
||||
"ops": 5678,
|
||||
"since": "2026-02-01"
|
||||
}
|
||||
},
|
||||
]
|
||||
client, col = _build_indexstats_client(rows)
|
||||
out = await get_index_stats(client, "db1", "coll1")
|
||||
assert out["title_text"] == {"ops": 5678, "since": "2026-02-01"}
|
||||
assert out["_id_"] == {"ops": 1234, "since": "2026-01-01"}
|
||||
col.aggregate.assert_awaited_once_with([{"$indexStats": {}}])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_index_stats_empty_when_view():
|
||||
client, _ = _build_indexstats_client([])
|
||||
out = await get_index_stats(client, "db1", "myview")
|
||||
assert out == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_indexes_returns_empty_for_view_without_listing():
|
||||
client, col, db = _build_indexes_client(
|
||||
spec={
|
||||
"name": "myview",
|
||||
"type": "view"
|
||||
},
|
||||
indexes=[],
|
||||
)
|
||||
out = await get_indexes(client, "db1", "myview")
|
||||
assert out == []
|
||||
db.list_collections.assert_awaited_once_with(filter={"name": "myview"})
|
||||
col.list_indexes.assert_not_called()
|
||||
|
||||
|
||||
class _AsyncChangeStream:
|
||||
|
||||
def __init__(self, changes):
|
||||
self._iter = _AsyncIter(changes)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
def __aiter__(self):
|
||||
return self._iter
|
||||
|
||||
|
||||
def _build_watch_client(changes):
|
||||
stream = _AsyncChangeStream(changes)
|
||||
col = MagicMock()
|
||||
col.watch = AsyncMock(return_value=stream)
|
||||
db = MagicMock()
|
||||
db.__getitem__.return_value = col
|
||||
client = MagicMock()
|
||||
client.__getitem__.return_value = db
|
||||
return client, col
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_iter_inserts_yields_full_documents():
|
||||
changes = [
|
||||
{
|
||||
"operationType": "insert",
|
||||
"fullDocument": {
|
||||
"_id": 1,
|
||||
"v": "a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"operationType": "insert",
|
||||
"fullDocument": {
|
||||
"_id": 2,
|
||||
"v": "b"
|
||||
}
|
||||
},
|
||||
]
|
||||
client, col = _build_watch_client(changes)
|
||||
out = []
|
||||
async for doc in iter_inserts(client, "db1", "coll1"):
|
||||
out.append(doc)
|
||||
assert out == [{"_id": 1, "v": "a"}, {"_id": 2, "v": "b"}]
|
||||
col.watch.assert_awaited_once_with([{
|
||||
"$match": {
|
||||
"operationType": "insert"
|
||||
}
|
||||
}])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_iter_inserts_skips_changes_without_full_document():
|
||||
changes = [
|
||||
{
|
||||
"operationType": "insert",
|
||||
"fullDocument": {
|
||||
"_id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"operationType": "drop"
|
||||
},
|
||||
{
|
||||
"operationType": "insert",
|
||||
"fullDocument": {
|
||||
"_id": 2
|
||||
}
|
||||
},
|
||||
]
|
||||
client, _ = _build_watch_client(changes)
|
||||
out = []
|
||||
async for doc in iter_inserts(client, "db1", "coll1"):
|
||||
out.append(doc)
|
||||
assert out == [{"_id": 1}, {"_id": 2}]
|
||||
@@ -0,0 +1,182 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from bson import ObjectId
|
||||
|
||||
from mirage.accessor.mongodb import MongoDBAccessor
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.mongodb.read import read
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
DOCS_PATH = "/sample_mflix/collections/movies/documents.jsonl"
|
||||
SCHEMA_PATH = "/sample_mflix/collections/movies/schema.json"
|
||||
DBJSON_PATH = "/sample_mflix/database.json"
|
||||
|
||||
|
||||
async def _gen(items):
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
return RAMIndexCacheStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return MongoDBAccessor(config=MongoDBConfig(
|
||||
uri="mongodb://localhost:27017"))
|
||||
|
||||
|
||||
def _patched_iter(docs):
|
||||
return patch("mirage.core.mongodb.stream.iter_documents",
|
||||
new=lambda *args, **kwargs: _gen(docs))
|
||||
|
||||
|
||||
def _path(s: str) -> PathSpec:
|
||||
return PathSpec(virtual=s, directory=s, resource_path=s.strip("/"))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_existence_checks():
|
||||
with patch(
|
||||
"mirage.core.mongodb.read.database_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
), patch(
|
||||
"mirage.core.mongodb.read.entity_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_documents_returns_extended_json_jsonl(accessor, index):
|
||||
oid = ObjectId()
|
||||
docs = [
|
||||
{
|
||||
"_id": oid,
|
||||
"title": "Movie 1"
|
||||
},
|
||||
{
|
||||
"_id": ObjectId(),
|
||||
"title": "Movie 2"
|
||||
},
|
||||
]
|
||||
with _patched_iter(docs):
|
||||
result = await read(accessor, _path(DOCS_PATH), index)
|
||||
lines = result.decode().strip().split("\n")
|
||||
assert len(lines) == 2
|
||||
first = json.loads(lines[0])
|
||||
assert first["title"] == "Movie 1"
|
||||
assert first["_id"] == {"$oid": str(oid)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_documents_no_doc_cap(accessor, index):
|
||||
docs = [{"_id": ObjectId(), "x": i} for i in range(50)]
|
||||
with _patched_iter(docs):
|
||||
result = await read(accessor, _path(DOCS_PATH), index)
|
||||
lines = result.decode().strip().split("\n")
|
||||
assert len(lines) == 50
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_documents_empty(accessor, index):
|
||||
with _patched_iter([]):
|
||||
result = await read(accessor, _path(DOCS_PATH), index)
|
||||
assert result == b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_schema_json_returns_jsonschema_payload(accessor, index):
|
||||
payload = {
|
||||
"database": "sample_mflix",
|
||||
"name": "movies",
|
||||
"kind": "collection",
|
||||
"validator": None,
|
||||
"fields": [],
|
||||
"primary_key": "_id",
|
||||
"indexes": [],
|
||||
"document_count": 0,
|
||||
"sampled": 100,
|
||||
}
|
||||
with patch("mirage.core.mongodb.read.build_collection_schema_json",
|
||||
new=AsyncMock(return_value=payload)):
|
||||
result = await read(accessor, _path(SCHEMA_PATH), index)
|
||||
parsed = json.loads(result.decode())
|
||||
assert parsed == payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_database_json_returns_payload(accessor, index):
|
||||
payload = {
|
||||
"database": "sample_mflix",
|
||||
"collections": [{
|
||||
"name": "movies",
|
||||
"document_count": 100
|
||||
}],
|
||||
"views": [{
|
||||
"name": "top_rated"
|
||||
}],
|
||||
}
|
||||
with patch("mirage.core.mongodb.read.build_database_json",
|
||||
new=AsyncMock(return_value=payload)):
|
||||
result = await read(accessor, _path(DBJSON_PATH), index)
|
||||
parsed = json.loads(result.decode())
|
||||
assert parsed == payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_unknown_path_raises(accessor, index):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await read(accessor, _path("/garbage/path"), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_kind_dir_path_raises(accessor, index):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await read(accessor, _path("/sample_mflix/collections"), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_documents_missing_collection_raises(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.read.entity_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await read(
|
||||
accessor,
|
||||
_path("/sample_mflix/collections/ghost/documents.jsonl"),
|
||||
index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_database_json_missing_db_raises(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.read.database_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await read(accessor, _path("/ghost/database.json"), index)
|
||||
@@ -0,0 +1,184 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.mongodb import MongoDBAccessor
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.mongodb.readdir import is_dir_name, readdir
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
return RAMIndexCacheStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return MongoDBAccessor(config=MongoDBConfig(
|
||||
uri="mongodb://localhost:27017"))
|
||||
|
||||
|
||||
def _path(s: str) -> PathSpec:
|
||||
return PathSpec(virtual=s, directory=s, resource_path=s.strip("/"))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_existence_checks():
|
||||
with patch(
|
||||
"mirage.core.mongodb.readdir.database_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
), patch(
|
||||
"mirage.core.mongodb.readdir.entity_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root_lists_databases(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.readdir.list_databases",
|
||||
new_callable=AsyncMock,
|
||||
return_value=["db1", "db2"],
|
||||
):
|
||||
result = await readdir(accessor, _path("/"), index)
|
||||
assert "/db1" in result
|
||||
assert "/db2" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_database_returns_fixed_children(accessor, index):
|
||||
result = await readdir(accessor, _path("/sample_mflix"), index)
|
||||
assert result == [
|
||||
"/sample_mflix/database.json",
|
||||
"/sample_mflix/collections",
|
||||
"/sample_mflix/views",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_collections_dir_lists_collections_only(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.readdir.list_collections",
|
||||
new_callable=AsyncMock,
|
||||
return_value=["movies", "users"],
|
||||
) as mock_list:
|
||||
result = await readdir(accessor, _path("/sample_mflix/collections"),
|
||||
index)
|
||||
assert "/sample_mflix/collections/movies" in result
|
||||
assert "/sample_mflix/collections/users" in result
|
||||
mock_list.assert_awaited_once_with(accessor.client,
|
||||
"sample_mflix",
|
||||
kind="collection")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_views_dir_lists_views_only(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.readdir.list_collections",
|
||||
new_callable=AsyncMock,
|
||||
return_value=["top_rated"],
|
||||
) as mock_list:
|
||||
result = await readdir(accessor, _path("/sample_mflix/views"), index)
|
||||
assert result == ["/sample_mflix/views/top_rated"]
|
||||
mock_list.assert_awaited_once_with(accessor.client,
|
||||
"sample_mflix",
|
||||
kind="view")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_collection_entity_lists_schema_and_documents(
|
||||
accessor, index):
|
||||
result = await readdir(accessor, _path("/sample_mflix/collections/movies"),
|
||||
index)
|
||||
assert result == [
|
||||
"/sample_mflix/collections/movies/schema.json",
|
||||
"/sample_mflix/collections/movies/documents.jsonl",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_view_entity_lists_schema_and_documents(accessor, index):
|
||||
result = await readdir(accessor, _path("/sample_mflix/views/top_rated"),
|
||||
index)
|
||||
assert result == [
|
||||
"/sample_mflix/views/top_rated/schema.json",
|
||||
"/sample_mflix/views/top_rated/documents.jsonl",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_unknown_path_raises(accessor, index):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await readdir(accessor, _path("/db/something/extra"), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root_index_caches_databases(accessor, index):
|
||||
mock_list = AsyncMock(return_value=["db1"])
|
||||
with patch("mirage.core.mongodb.readdir.list_databases", new=mock_list):
|
||||
first = await readdir(accessor, _path("/"), index)
|
||||
second = await readdir(accessor, _path("/"), index)
|
||||
assert first == second
|
||||
assert mock_list.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_database_raises_when_db_missing(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.readdir.database_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await readdir(accessor, _path("/ghost"), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_entity_raises_when_collection_missing(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.readdir.entity_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await readdir(accessor, _path("/sample_mflix/collections/ghost"),
|
||||
index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_prefix_carries_through(accessor, index):
|
||||
p = PathSpec(resource_path=mount_key("/mongo/sample_mflix", "/mongo"),
|
||||
virtual="/mongo/sample_mflix",
|
||||
directory="/mongo/sample_mflix")
|
||||
result = await readdir(accessor, p, index)
|
||||
assert result == [
|
||||
"/mongo/sample_mflix/database.json",
|
||||
"/mongo/sample_mflix/collections",
|
||||
"/mongo/sample_mflix/views",
|
||||
]
|
||||
|
||||
|
||||
def test_is_dir_name_classifies_by_extension():
|
||||
assert is_dir_name("/db/collections") is True
|
||||
assert is_dir_name("/db/database.json") is False
|
||||
assert is_dir_name("/db/collections/books/documents.jsonl") is False
|
||||
@@ -0,0 +1,122 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from bson import Decimal128, ObjectId
|
||||
|
||||
from mirage.core.mongodb._sampler import sample_field_types
|
||||
|
||||
|
||||
class _AsyncIter:
|
||||
|
||||
def __init__(self, items):
|
||||
self._items = list(items)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if not self._items:
|
||||
raise StopAsyncIteration
|
||||
return self._items.pop(0)
|
||||
|
||||
|
||||
def _col(docs):
|
||||
col = MagicMock()
|
||||
col.aggregate = AsyncMock(return_value=_AsyncIter(docs))
|
||||
return col
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sample_field_types_tallies_types_and_presence():
|
||||
docs = [
|
||||
{
|
||||
"_id": 1,
|
||||
"title": "Hi",
|
||||
"year": 2020,
|
||||
"tags": ["a", "b"]
|
||||
},
|
||||
{
|
||||
"_id": 2,
|
||||
"title": "Bye",
|
||||
"year": "2021",
|
||||
"tags": ["c"]
|
||||
},
|
||||
{
|
||||
"_id": 3,
|
||||
"title": "Yo"
|
||||
},
|
||||
]
|
||||
out = await sample_field_types(_col(docs), sample_size=3)
|
||||
by_path = {f["path"]: f for f in out}
|
||||
assert "_id" not in by_path
|
||||
assert by_path["title"]["presence"] == 1.0
|
||||
assert by_path["title"]["types"] == {"string": 1.0}
|
||||
assert by_path["year"]["presence"] == pytest.approx(2 / 3)
|
||||
assert set(by_path["year"]["types"].keys()) == {"int", "string"}
|
||||
assert by_path["tags"]["types"] == {"array<string>": pytest.approx(2 / 3)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sample_field_types_recognizes_fixed_numeric_arrays():
|
||||
docs = [{"_id": i, "embedding": [0.1] * 1024} for i in range(5)]
|
||||
out = await sample_field_types(_col(docs), sample_size=5)
|
||||
by_path = {f["path"]: f for f in out}
|
||||
assert by_path["embedding"]["types"] == {"array<double>(1024)": 1.0}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sample_field_types_walks_nested_objects():
|
||||
docs = [
|
||||
{
|
||||
"_id": 1,
|
||||
"metadata": {
|
||||
"tag": "a",
|
||||
"ratings": [1.0, 2.0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"_id": 2,
|
||||
"metadata": {
|
||||
"tag": "b",
|
||||
"extra": ObjectId()
|
||||
}
|
||||
},
|
||||
]
|
||||
out = await sample_field_types(_col(docs), sample_size=2)
|
||||
paths = {f["path"] for f in out}
|
||||
assert "metadata.tag" in paths
|
||||
assert "metadata.ratings" in paths
|
||||
assert "metadata.extra" in paths
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sample_field_types_handles_bson_scalars():
|
||||
docs = [{
|
||||
"_id": 1,
|
||||
"dec": Decimal128("1.23"),
|
||||
"oid": ObjectId("65f0000000000000000000a1")
|
||||
}]
|
||||
out = await sample_field_types(_col(docs), sample_size=1)
|
||||
by_path = {f["path"]: f for f in out}
|
||||
assert by_path["dec"]["types"] == {"decimal": 1.0}
|
||||
assert by_path["oid"]["types"] == {"objectId": 1.0}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sample_field_types_empty_sample_returns_empty_list():
|
||||
out = await sample_field_types(_col([]), sample_size=5)
|
||||
assert out == []
|
||||
@@ -0,0 +1,119 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.mongodb import MongoDBAccessor
|
||||
from mirage.core.mongodb._schema_json import build_collection_schema_json
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return MongoDBAccessor(config=MongoDBConfig(
|
||||
uri="mongodb://localhost:27017"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_collection_schema_json_assembles_all_sections(accessor):
|
||||
fields = [{"path": "title", "presence": 1.0, "types": {"string": 1.0}}]
|
||||
indexes = [{"name": "_id_", "key": {"_id": 1}}]
|
||||
stats = {"_id_": {"ops": 42, "since": "2026-03-01T00:00:00Z"}}
|
||||
with (
|
||||
patch("mirage.core.mongodb._schema_json.get_validator",
|
||||
new=AsyncMock(return_value={"bsonType": "object"})),
|
||||
patch("mirage.core.mongodb._schema_json.sample_field_types",
|
||||
new=AsyncMock(return_value=fields)),
|
||||
patch("mirage.core.mongodb._schema_json.get_indexes",
|
||||
new=AsyncMock(return_value=indexes)),
|
||||
patch("mirage.core.mongodb._schema_json.get_index_stats",
|
||||
new=AsyncMock(return_value=stats)),
|
||||
patch("mirage.core.mongodb._schema_json.count_documents",
|
||||
new=AsyncMock(return_value=999)),
|
||||
patch("mirage.core.mongodb._schema_json.is_view",
|
||||
new=AsyncMock(return_value=False)),
|
||||
):
|
||||
out = await build_collection_schema_json(accessor, "db1", "movies")
|
||||
assert out["database"] == "db1"
|
||||
assert out["name"] == "movies"
|
||||
assert out["kind"] == "collection"
|
||||
assert out["validator"] == {"bsonType": "object"}
|
||||
assert out["fields"] == fields
|
||||
assert out["primary_key"] == "_id"
|
||||
assert out["document_count"] == 999
|
||||
assert out["sampled"] == 100
|
||||
assert len(out["indexes"]) == 1
|
||||
enriched = out["indexes"][0]
|
||||
assert enriched["name"] == "_id_"
|
||||
assert enriched["keys"] == {"_id": 1}
|
||||
assert enriched["type"] == "btree"
|
||||
assert enriched["stats"] == {"ops": 42, "since": "2026-03-01T00:00:00Z"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_collection_schema_json_text_index_tagged(accessor):
|
||||
indexes = [{
|
||||
"name": "title_text",
|
||||
"key": {
|
||||
"_fts": "text",
|
||||
"_ftsx": 1
|
||||
},
|
||||
"textIndexVersion": 3,
|
||||
}]
|
||||
with (
|
||||
patch("mirage.core.mongodb._schema_json.get_validator",
|
||||
new=AsyncMock(return_value=None)),
|
||||
patch("mirage.core.mongodb._schema_json.sample_field_types",
|
||||
new=AsyncMock(return_value=[])),
|
||||
patch("mirage.core.mongodb._schema_json.get_indexes",
|
||||
new=AsyncMock(return_value=indexes)),
|
||||
patch("mirage.core.mongodb._schema_json.get_index_stats",
|
||||
new=AsyncMock(return_value={})),
|
||||
patch("mirage.core.mongodb._schema_json.count_documents",
|
||||
new=AsyncMock(return_value=0)),
|
||||
patch("mirage.core.mongodb._schema_json.is_view",
|
||||
new=AsyncMock(return_value=False)),
|
||||
):
|
||||
out = await build_collection_schema_json(accessor, "db1", "articles")
|
||||
assert out["indexes"][0]["type"] == "text"
|
||||
assert out["indexes"][0]["stats"] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_collection_schema_json_view_skips_indexes(accessor):
|
||||
fields = [{"path": "title", "presence": 1.0, "types": {"string": 1.0}}]
|
||||
with (
|
||||
patch("mirage.core.mongodb._schema_json.get_validator",
|
||||
new=AsyncMock(return_value=None)),
|
||||
patch("mirage.core.mongodb._schema_json.sample_field_types",
|
||||
new=AsyncMock(return_value=fields)),
|
||||
patch("mirage.core.mongodb._schema_json.get_indexes",
|
||||
new=AsyncMock(side_effect=AssertionError(
|
||||
"get_indexes must not be called for views"))),
|
||||
patch("mirage.core.mongodb._schema_json.get_index_stats",
|
||||
new=AsyncMock(side_effect=AssertionError(
|
||||
"get_index_stats must not be called for views"))),
|
||||
patch("mirage.core.mongodb._schema_json.count_documents",
|
||||
new=AsyncMock(return_value=40)),
|
||||
patch("mirage.core.mongodb._schema_json.is_view",
|
||||
new=AsyncMock(return_value=True)),
|
||||
):
|
||||
out = await build_collection_schema_json(accessor, "db1", "myview")
|
||||
assert out["kind"] == "view"
|
||||
assert out["indexes"] == []
|
||||
assert out["validator"] is None
|
||||
assert out["document_count"] == 40
|
||||
assert out["fields"] == fields
|
||||
@@ -0,0 +1,92 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from mirage.accessor.mongodb import MongoDBAccessor
|
||||
from mirage.core.mongodb._schema_json import build_collection_schema_json
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("MIRAGE_RUN_INTEGRATION_MONGO") != "1",
|
||||
reason="integration test (set MIRAGE_RUN_INTEGRATION_MONGO=1 to enable)",
|
||||
)
|
||||
|
||||
|
||||
def _load_env() -> str:
|
||||
repo_root = Path(__file__).resolve().parents[4]
|
||||
load_dotenv(repo_root / ".env.development")
|
||||
uri = os.environ.get("MONGODB_URI")
|
||||
if not uri:
|
||||
pytest.skip("MONGODB_URI not set in .env.development")
|
||||
return uri
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return MongoDBAccessor(config=MongoDBConfig(uri=_load_env()))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_captures_jsonschema_validator(accessor):
|
||||
s = await build_collection_schema_json(accessor, "mirage_test",
|
||||
"with_validator")
|
||||
assert s["kind"] == "collection"
|
||||
assert s["validator"]["bsonType"] == "object"
|
||||
assert "title" in s["validator"]["required"]
|
||||
assert "year" in s["validator"]["required"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_recognizes_fixed_length_embedding_array(accessor):
|
||||
s = await build_collection_schema_json(accessor, "mirage_test",
|
||||
"embeddings")
|
||||
vec = next(f for f in s["fields"] if f["path"] == "vector")
|
||||
assert vec["types"] == {"array<double>(1024)": 1.0}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_tags_text_index_and_returns_indexstats(accessor):
|
||||
s = await build_collection_schema_json(accessor, "mirage_test",
|
||||
"text_indexed")
|
||||
by_name = {idx["name"]: idx for idx in s["indexes"]}
|
||||
assert by_name["title_body_text"]["type"] == "text"
|
||||
assert "ops" in by_name["title_body_text"]["stats"]
|
||||
assert by_name["_id_"]["type"] == "btree"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_view_marks_kind_and_skips_indexes(accessor):
|
||||
s = await build_collection_schema_json(accessor, "mirage_test",
|
||||
"high_rated_films")
|
||||
assert s["kind"] == "view"
|
||||
assert s["indexes"] == []
|
||||
assert s["document_count"] == 40
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_heterogeneous_collection_surfaces_mixed_types(accessor):
|
||||
s = await build_collection_schema_json(accessor,
|
||||
"mirage_test",
|
||||
"heterogeneous",
|
||||
sample_size=200)
|
||||
by_path = {f["path"]: f for f in s["fields"]}
|
||||
assert "metadata.tag" in by_path
|
||||
score = by_path["score"]
|
||||
assert set(score["types"].keys()).issubset({"string", "int", "null"})
|
||||
assert sum(score["types"].values()) == pytest.approx(score["presence"])
|
||||
@@ -0,0 +1,138 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from mirage.core.mongodb.scope import detect_scope
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
def test_root():
|
||||
scope = detect_scope("/")
|
||||
assert scope.level == "root"
|
||||
assert scope.database is None
|
||||
assert scope.kind is None
|
||||
assert scope.name is None
|
||||
|
||||
|
||||
def test_database():
|
||||
scope = detect_scope("/sample_mflix")
|
||||
assert scope.level == "database"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind is None
|
||||
assert scope.name is None
|
||||
|
||||
|
||||
def test_database_json():
|
||||
scope = detect_scope("/sample_mflix/database.json")
|
||||
assert scope.level == "database_json"
|
||||
assert scope.database == "sample_mflix"
|
||||
|
||||
|
||||
def test_collections_kind_dir():
|
||||
scope = detect_scope("/sample_mflix/collections")
|
||||
assert scope.level == "kind_dir"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind == "collection"
|
||||
|
||||
|
||||
def test_views_kind_dir():
|
||||
scope = detect_scope("/sample_mflix/views")
|
||||
assert scope.level == "kind_dir"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind == "view"
|
||||
|
||||
|
||||
def test_collection_entity_dir():
|
||||
scope = detect_scope("/sample_mflix/collections/movies")
|
||||
assert scope.level == "entity"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind == "collection"
|
||||
assert scope.name == "movies"
|
||||
|
||||
|
||||
def test_view_entity_dir():
|
||||
scope = detect_scope("/sample_mflix/views/top_rated")
|
||||
assert scope.level == "entity"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind == "view"
|
||||
assert scope.name == "top_rated"
|
||||
|
||||
|
||||
def test_collection_schema_json():
|
||||
scope = detect_scope("/sample_mflix/collections/movies/schema.json")
|
||||
assert scope.level == "schema_json"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind == "collection"
|
||||
assert scope.name == "movies"
|
||||
|
||||
|
||||
def test_collection_documents_jsonl():
|
||||
scope = detect_scope("/sample_mflix/collections/movies/documents.jsonl")
|
||||
assert scope.level == "documents"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind == "collection"
|
||||
assert scope.name == "movies"
|
||||
|
||||
|
||||
def test_view_documents_jsonl():
|
||||
scope = detect_scope("/sample_mflix/views/top_rated/documents.jsonl")
|
||||
assert scope.level == "documents"
|
||||
assert scope.kind == "view"
|
||||
assert scope.name == "top_rated"
|
||||
|
||||
|
||||
def test_unknown_leaf_under_entity():
|
||||
scope = detect_scope("/sample_mflix/collections/movies/weird.txt")
|
||||
assert scope.level == "unknown"
|
||||
|
||||
|
||||
def test_unknown_top_segment_under_db():
|
||||
scope = detect_scope("/sample_mflix/randomdir")
|
||||
assert scope.level == "unknown"
|
||||
|
||||
|
||||
def test_pathspec_with_prefix_root():
|
||||
p = PathSpec(
|
||||
resource_path=mount_key("/mongo/", "/mongo"),
|
||||
virtual="/mongo/",
|
||||
directory="/mongo/",
|
||||
)
|
||||
scope = detect_scope(p)
|
||||
assert scope.level == "root"
|
||||
|
||||
|
||||
def test_pathspec_with_prefix_database():
|
||||
p = PathSpec(
|
||||
resource_path=mount_key("/mongo/sample_mflix", "/mongo"),
|
||||
virtual="/mongo/sample_mflix",
|
||||
directory="/mongo/",
|
||||
)
|
||||
scope = detect_scope(p)
|
||||
assert scope.level == "database"
|
||||
assert scope.database == "sample_mflix"
|
||||
|
||||
|
||||
def test_pathspec_with_prefix_documents():
|
||||
p = PathSpec(
|
||||
resource_path=mount_key(
|
||||
"/mongo/sample_mflix/collections/movies/documents.jsonl",
|
||||
"/mongo"),
|
||||
virtual="/mongo/sample_mflix/collections/movies/documents.jsonl",
|
||||
directory="/mongo/sample_mflix/collections/movies/",
|
||||
)
|
||||
scope = detect_scope(p)
|
||||
assert scope.level == "documents"
|
||||
assert scope.database == "sample_mflix"
|
||||
assert scope.kind == "collection"
|
||||
assert scope.name == "movies"
|
||||
@@ -0,0 +1,139 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.core.mongodb.search import search_collection, search_database
|
||||
|
||||
|
||||
class _AsyncIter:
|
||||
|
||||
def __init__(self, items):
|
||||
self._items = list(items)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
if not self._items:
|
||||
raise StopAsyncIteration
|
||||
return self._items.pop(0)
|
||||
|
||||
|
||||
def _agg_iter(items):
|
||||
return _AsyncIter(items)
|
||||
|
||||
|
||||
def _build_search_client(sampled_docs, matched_docs):
|
||||
col = MagicMock()
|
||||
col.aggregate = AsyncMock(return_value=_agg_iter(sampled_docs))
|
||||
cursor = MagicMock()
|
||||
cursor.limit = MagicMock(return_value=cursor)
|
||||
cursor.to_list = AsyncMock(return_value=matched_docs)
|
||||
col.find = MagicMock(return_value=cursor)
|
||||
db = MagicMock()
|
||||
db.__getitem__.return_value = col
|
||||
client = MagicMock()
|
||||
client.__getitem__.return_value = db
|
||||
return client, col
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_collection_unions_string_fields_across_sampled_docs():
|
||||
sampled = [
|
||||
{
|
||||
"_id": 1,
|
||||
"title": "Hello"
|
||||
},
|
||||
{
|
||||
"_id": 2,
|
||||
"body": "World"
|
||||
},
|
||||
{
|
||||
"_id": 3,
|
||||
"metadata": {
|
||||
"tag": "x"
|
||||
}
|
||||
},
|
||||
]
|
||||
matched = [{"_id": 2, "body": "World matches"}]
|
||||
client, col = _build_search_client(sampled, matched)
|
||||
with patch("mirage.core.mongodb.search.get_indexes",
|
||||
new=AsyncMock(return_value=[])):
|
||||
out = await search_collection(client,
|
||||
"db1",
|
||||
"coll1",
|
||||
"World",
|
||||
limit=10)
|
||||
assert out == matched
|
||||
filter_arg = col.find.call_args[0][0]
|
||||
or_fields = {list(clause.keys())[0] for clause in filter_arg["$or"]}
|
||||
assert {"title", "body", "metadata.tag"}.issubset(or_fields)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_collection_uses_text_when_textIndexVersion_present():
|
||||
indexes = [{
|
||||
"name": "title_text",
|
||||
"key": {
|
||||
"_fts": "text",
|
||||
"_ftsx": 1
|
||||
},
|
||||
"weights": {
|
||||
"title": 1
|
||||
},
|
||||
"textIndexVersion": 3,
|
||||
}]
|
||||
client, col = _build_search_client([], [{"_id": 1}])
|
||||
with patch("mirage.core.mongodb.search.get_indexes",
|
||||
new=AsyncMock(return_value=indexes)):
|
||||
await search_collection(client, "db1", "coll1", "query", limit=10)
|
||||
assert col.find.call_args[0][0] == {"$text": {"$search": "query"}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_collection_no_string_fields_returns_no_results():
|
||||
sampled = [{"_id": 1, "n": 42}, {"_id": 2, "n": 7}]
|
||||
client, col = _build_search_client(sampled, [])
|
||||
with patch("mirage.core.mongodb.search.get_indexes",
|
||||
new=AsyncMock(return_value=[])):
|
||||
out = await search_collection(client,
|
||||
"db1",
|
||||
"coll1",
|
||||
"anything",
|
||||
limit=10)
|
||||
assert out == []
|
||||
assert col.find.call_args is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_database_runs_collections_concurrently():
|
||||
barrier = asyncio.Barrier(3)
|
||||
|
||||
async def slow_search(client, database, col, pattern, limit):
|
||||
await barrier.wait()
|
||||
return [{"_id": col, "v": col}]
|
||||
|
||||
with patch("mirage.core.mongodb.search.list_collections",
|
||||
new=AsyncMock(return_value=["a", "b", "c"])):
|
||||
with patch("mirage.core.mongodb.search.search_collection",
|
||||
new=slow_search):
|
||||
out = await asyncio.wait_for(
|
||||
search_database(None, "db", "p", 10),
|
||||
timeout=2.0,
|
||||
)
|
||||
assert {col for _, col, _ in out} == {"a", "b", "c"}
|
||||
@@ -0,0 +1,200 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.accessor.mongodb import MongoDBAccessor
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.mongodb.stat import stat
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
from mirage.types import FileType, PathSpec
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
return RAMIndexCacheStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return MongoDBAccessor(config=MongoDBConfig(
|
||||
uri="mongodb://localhost:27017"))
|
||||
|
||||
|
||||
def _path(s: str) -> PathSpec:
|
||||
return PathSpec(virtual=s, directory=s, resource_path=s.strip("/"))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_existence_checks():
|
||||
with patch(
|
||||
"mirage.core.mongodb.stat.database_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
), patch(
|
||||
"mirage.core.mongodb.stat.entity_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_root(accessor, index):
|
||||
result = await stat(accessor, _path("/"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "/"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_database(accessor, index):
|
||||
result = await stat(accessor, _path("/sample_mflix"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "sample_mflix"
|
||||
assert result.extra["database"] == "sample_mflix"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_collections_kind_dir(accessor, index):
|
||||
result = await stat(accessor, _path("/sample_mflix/collections"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "collections"
|
||||
assert result.extra["kind"] == "collection"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_views_kind_dir(accessor, index):
|
||||
result = await stat(accessor, _path("/sample_mflix/views"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "views"
|
||||
assert result.extra["kind"] == "view"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_entity_collection(accessor, index):
|
||||
with patch("mirage.core.mongodb.stat.count_documents",
|
||||
new=AsyncMock(return_value=23519)):
|
||||
result = await stat(accessor,
|
||||
_path("/sample_mflix/collections/movies"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "movies"
|
||||
assert result.extra["kind"] == "collection"
|
||||
assert result.extra["document_count"] == 23519
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_documents_collection_full_metadata(accessor, index):
|
||||
fake_indexes = [{"name": "_id_", "key": {"_id": 1}}]
|
||||
with (
|
||||
patch("mirage.core.mongodb.stat.is_view",
|
||||
new=AsyncMock(return_value=False)),
|
||||
patch("mirage.core.mongodb.stat.count_documents",
|
||||
new=AsyncMock(return_value=42)),
|
||||
patch("mirage.core.mongodb.stat.get_indexes",
|
||||
new=AsyncMock(return_value=fake_indexes)),
|
||||
):
|
||||
result = await stat(
|
||||
accessor,
|
||||
_path("/sample_mflix/collections/movies/documents.jsonl"), index)
|
||||
assert result.type == FileType.TEXT
|
||||
assert result.name == "documents.jsonl"
|
||||
assert result.extra["kind"] == "collection"
|
||||
assert result.extra["document_count"] == 42
|
||||
assert result.extra["indexes"] == [{"name": "_id_", "keys": {"_id": 1}}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_documents_view_skips_indexes(accessor, index):
|
||||
with (
|
||||
patch(
|
||||
"mirage.core.mongodb.stat.count_documents",
|
||||
new=AsyncMock(return_value=17),
|
||||
),
|
||||
patch(
|
||||
"mirage.core.mongodb.stat.get_indexes",
|
||||
new=AsyncMock(side_effect=AssertionError(
|
||||
"get_indexes must not be called for views")),
|
||||
),
|
||||
):
|
||||
result = await stat(
|
||||
accessor, _path("/sample_mflix/views/my_view/documents.jsonl"),
|
||||
index)
|
||||
assert result.type == FileType.TEXT
|
||||
assert result.extra["kind"] == "view"
|
||||
assert result.extra["indexes"] == []
|
||||
assert result.extra["document_count"] == 17
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_schema_json(accessor, index):
|
||||
result = await stat(accessor,
|
||||
_path("/sample_mflix/collections/movies/schema.json"),
|
||||
index)
|
||||
assert result.type == FileType.TEXT
|
||||
assert result.name == "schema.json"
|
||||
assert result.extra["kind"] == "collection"
|
||||
assert result.extra["name"] == "movies"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_database_json(accessor, index):
|
||||
result = await stat(accessor, _path("/sample_mflix/database.json"), index)
|
||||
assert result.type == FileType.TEXT
|
||||
assert result.name == "database.json"
|
||||
assert result.extra["database"] == "sample_mflix"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_unknown_path_raises(accessor, index):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(accessor, _path("/db/something/extra/leaf"), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_database_missing_raises(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.stat.database_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(accessor, _path("/ghost"), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_collection_missing_raises(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.stat.entity_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(accessor, _path("/sample_mflix/collections/ghost"),
|
||||
index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_documents_under_missing_collection_raises(accessor, index):
|
||||
with patch(
|
||||
"mirage.core.mongodb.stat.entity_exists",
|
||||
new_callable=AsyncMock,
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await stat(
|
||||
accessor,
|
||||
_path("/sample_mflix/collections/ghost/documents.jsonl"),
|
||||
index)
|
||||
@@ -0,0 +1,342 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from bson import Decimal128, ObjectId
|
||||
|
||||
from mirage.accessor.mongodb import MongoDBAccessor
|
||||
from mirage.cache.index.ram import RAMIndexCacheStore
|
||||
from mirage.core.mongodb.stream import read_stream, read_tail, watch_stream
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
DOCS_PATH = "/db1/collections/coll1/documents.jsonl"
|
||||
VIEW_DOCS_PATH = "/db1/views/myview/documents.jsonl"
|
||||
|
||||
|
||||
async def _gen(items):
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def index():
|
||||
return RAMIndexCacheStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
return MongoDBAccessor(config=MongoDBConfig(
|
||||
uri="mongodb://localhost:27017"))
|
||||
|
||||
|
||||
def _patched_iter(docs):
|
||||
return patch("mirage.core.mongodb.stream.iter_documents",
|
||||
new=lambda *args, **kwargs: _gen(docs))
|
||||
|
||||
|
||||
def _patched_watch(docs):
|
||||
return patch("mirage.core.mongodb.stream.iter_inserts",
|
||||
new=lambda *args, **kwargs: _gen(docs))
|
||||
|
||||
|
||||
def _path(s: str) -> PathSpec:
|
||||
return PathSpec(virtual=s, directory=s, resource_path=s.strip("/"))
|
||||
|
||||
|
||||
async def _collect(gen):
|
||||
chunks = []
|
||||
async for chunk in gen:
|
||||
chunks.append(chunk)
|
||||
return b"".join(chunks)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_yields_one_jsonl_line_per_doc(accessor, index):
|
||||
oid_a, oid_b = ObjectId(), ObjectId()
|
||||
docs = [{"_id": oid_a, "title": "A"}, {"_id": oid_b, "title": "B"}]
|
||||
with _patched_iter(docs):
|
||||
data = await _collect(read_stream(accessor, _path(DOCS_PATH), index))
|
||||
lines = [line for line in data.decode().split("\n") if line]
|
||||
assert len(lines) == 2
|
||||
first = json.loads(lines[0])
|
||||
assert first["title"] == "A"
|
||||
assert first["_id"] == {"$oid": str(oid_a)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_preserves_bson_types_via_extended_json(
|
||||
accessor, index):
|
||||
docs = [{
|
||||
"_id":
|
||||
ObjectId("65f0000000000000000000a1"),
|
||||
"date":
|
||||
dt.datetime(2026, 5, 15, 12, 30, 45, tzinfo=dt.timezone.utc),
|
||||
"decimal":
|
||||
Decimal128("123.456"),
|
||||
}]
|
||||
with _patched_iter(docs):
|
||||
data = await _collect(read_stream(accessor, _path(DOCS_PATH), index))
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["_id"] == {"$oid": "65f0000000000000000000a1"}
|
||||
assert "$date" in parsed["date"]
|
||||
assert parsed["decimal"] == {"$numberDecimal": "123.456"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_empty_yields_nothing(accessor, index):
|
||||
with _patched_iter([]):
|
||||
chunks = []
|
||||
async for chunk in read_stream(accessor, _path(DOCS_PATH), index):
|
||||
chunks.append(chunk)
|
||||
assert chunks == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_short_circuits_when_consumer_closes(
|
||||
accessor, index):
|
||||
consumed: list[int] = []
|
||||
|
||||
async def _instrumented(*_args, **_kwargs):
|
||||
for i in range(1000):
|
||||
consumed.append(i)
|
||||
yield {"_id": ObjectId(), "i": i}
|
||||
|
||||
with patch("mirage.core.mongodb.stream.iter_documents", new=_instrumented):
|
||||
gen = read_stream(accessor, _path(DOCS_PATH), index)
|
||||
await gen.__anext__()
|
||||
await gen.aclose()
|
||||
assert len(consumed) <= 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_works_on_view_documents_path(accessor, index):
|
||||
oid = ObjectId()
|
||||
docs = [{"_id": oid, "v": 1}]
|
||||
with _patched_iter(docs):
|
||||
data = await _collect(
|
||||
read_stream(accessor, _path(VIEW_DOCS_PATH), index))
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["v"] == 1
|
||||
assert parsed["_id"] == {"$oid": str(oid)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_directory_path_raises(accessor, index):
|
||||
with _patched_iter([]):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
async for _ in read_stream(accessor, _path("/db1"), index):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_schema_json_path_raises(accessor, index):
|
||||
with _patched_iter([]):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
async for _ in read_stream(
|
||||
accessor, _path("/db1/collections/coll1/schema.json"),
|
||||
index):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_elides_configured_top_level_field(index):
|
||||
cfg = MongoDBConfig(
|
||||
uri="mongodb://localhost:27017",
|
||||
elide_fields={"db1.coll1": ["vector"]},
|
||||
)
|
||||
acc = MongoDBAccessor(config=cfg)
|
||||
oid = ObjectId()
|
||||
docs = [{"_id": oid, "title": "hi", "vector": [0.1, 0.2, 0.3, 0.4, 0.5]}]
|
||||
with _patched_iter(docs):
|
||||
data = await _collect(read_stream(acc, _path(DOCS_PATH), index))
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["title"] == "hi"
|
||||
assert "vector" not in parsed
|
||||
assert parsed["_id"] == {"$oid": str(oid)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_elides_configured_nested_path(index):
|
||||
cfg = MongoDBConfig(
|
||||
uri="mongodb://localhost:27017",
|
||||
elide_fields={"db1.coll1": ["metadata.embedding"]},
|
||||
)
|
||||
acc = MongoDBAccessor(config=cfg)
|
||||
docs = [{
|
||||
"_id": ObjectId(),
|
||||
"metadata": {
|
||||
"tag": "alpha",
|
||||
"embedding": [0.1] * 1024,
|
||||
},
|
||||
}]
|
||||
with _patched_iter(docs):
|
||||
data = await _collect(read_stream(acc, _path(DOCS_PATH), index))
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["metadata"]["tag"] == "alpha"
|
||||
assert "embedding" not in parsed["metadata"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_elision_isolated_to_configured_collection(index):
|
||||
cfg = MongoDBConfig(
|
||||
uri="mongodb://localhost:27017",
|
||||
elide_fields={"db1.other_coll": ["vector"]},
|
||||
)
|
||||
acc = MongoDBAccessor(config=cfg)
|
||||
docs = [{"_id": ObjectId(), "vector": [1.0, 2.0]}]
|
||||
with _patched_iter(docs):
|
||||
data = await _collect(read_stream(acc, _path(DOCS_PATH), index))
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["vector"] == [1.0, 2.0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_stream_yields_one_jsonl_line_per_insert(accessor, index):
|
||||
oid_a, oid_b = ObjectId(), ObjectId()
|
||||
docs = [{"_id": oid_a, "title": "A"}, {"_id": oid_b, "title": "B"}]
|
||||
with _patched_watch(docs):
|
||||
data = await _collect(watch_stream(accessor, _path(DOCS_PATH), index))
|
||||
lines = [line for line in data.decode().split("\n") if line]
|
||||
assert len(lines) == 2
|
||||
assert json.loads(lines[0])["_id"] == {"$oid": str(oid_a)}
|
||||
assert json.loads(lines[1])["title"] == "B"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_stream_preserves_bson_types(accessor, index):
|
||||
docs = [{
|
||||
"_id":
|
||||
ObjectId("65f0000000000000000000a1"),
|
||||
"date":
|
||||
dt.datetime(2026, 5, 15, 12, 30, 45, tzinfo=dt.timezone.utc),
|
||||
"decimal":
|
||||
Decimal128("123.456"),
|
||||
}]
|
||||
with _patched_watch(docs):
|
||||
data = await _collect(watch_stream(accessor, _path(DOCS_PATH), index))
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["_id"] == {"$oid": "65f0000000000000000000a1"}
|
||||
assert "$date" in parsed["date"]
|
||||
assert parsed["decimal"] == {"$numberDecimal": "123.456"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_stream_empty_yields_nothing(accessor, index):
|
||||
with _patched_watch([]):
|
||||
chunks = []
|
||||
async for chunk in watch_stream(accessor, _path(DOCS_PATH), index):
|
||||
chunks.append(chunk)
|
||||
assert chunks == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_stream_short_circuits_when_consumer_closes(
|
||||
accessor, index):
|
||||
consumed: list[int] = []
|
||||
|
||||
async def _instrumented(*_args, **_kwargs):
|
||||
for i in range(1000):
|
||||
consumed.append(i)
|
||||
yield {"_id": ObjectId(), "i": i}
|
||||
|
||||
with patch("mirage.core.mongodb.stream.iter_inserts", new=_instrumented):
|
||||
gen = watch_stream(accessor, _path(DOCS_PATH), index)
|
||||
await gen.__anext__()
|
||||
await gen.aclose()
|
||||
assert len(consumed) <= 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_stream_directory_path_raises(accessor, index):
|
||||
with _patched_watch([]):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
async for _ in watch_stream(accessor, _path("/db1"), index):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_tail_returns_docs_in_ascending_order(accessor):
|
||||
docs = [{"_id": 5, "name": "e"}, {"_id": 4, "name": "d"}]
|
||||
with patch(
|
||||
"mirage.core.mongodb.stream.find_documents",
|
||||
new_callable=AsyncMock,
|
||||
return_value=list(docs),
|
||||
) as fake:
|
||||
data = await read_tail(accessor, _path(DOCS_PATH), 2)
|
||||
lines = data.decode().splitlines()
|
||||
assert '"_id": 4' in lines[0]
|
||||
assert '"_id": 5' in lines[1]
|
||||
assert data.endswith(b"\n")
|
||||
assert fake.await_args.kwargs["limit"] == 2
|
||||
assert fake.await_args.kwargs["sort"] == [("_id", -1)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_tail_caps_limit_at_max_doc_limit(accessor):
|
||||
with patch(
|
||||
"mirage.core.mongodb.stream.find_documents",
|
||||
new_callable=AsyncMock,
|
||||
return_value=[],
|
||||
) as fake:
|
||||
data = await read_tail(accessor, _path(DOCS_PATH), 10**9)
|
||||
assert data == b""
|
||||
assert fake.await_args.kwargs["limit"] == accessor.config.max_doc_limit
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_tail_applies_elision(index):
|
||||
cfg = MongoDBConfig(
|
||||
uri="mongodb://localhost:27017",
|
||||
elide_fields={"db1.coll1": ["vector"]},
|
||||
)
|
||||
acc = MongoDBAccessor(config=cfg)
|
||||
docs = [{"_id": 1, "title": "hi", "vector": [0.1, 0.2]}]
|
||||
with patch(
|
||||
"mirage.core.mongodb.stream.find_documents",
|
||||
new_callable=AsyncMock,
|
||||
return_value=docs,
|
||||
):
|
||||
data = await read_tail(acc, _path(DOCS_PATH), 1)
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["title"] == "hi"
|
||||
assert "vector" not in parsed
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_tail_rejects_non_documents_path(accessor):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await read_tail(accessor, _path("/db1/collections/coll1/schema.json"),
|
||||
5)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_stream_applies_elision(index):
|
||||
cfg = MongoDBConfig(
|
||||
uri="mongodb://localhost:27017",
|
||||
elide_fields={"db1.coll1": ["vector"]},
|
||||
)
|
||||
acc = MongoDBAccessor(config=cfg)
|
||||
oid = ObjectId()
|
||||
docs = [{"_id": oid, "title": "live", "vector": [0.1, 0.2, 0.3]}]
|
||||
with _patched_watch(docs):
|
||||
data = await _collect(watch_stream(acc, _path(DOCS_PATH), index))
|
||||
parsed = json.loads(data.decode().strip())
|
||||
assert parsed["title"] == "live"
|
||||
assert "vector" not in parsed
|
||||
assert parsed["_id"] == {"$oid": str(oid)}
|
||||
@@ -0,0 +1,102 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from mirage.accessor.mongodb import MongoDBAccessor
|
||||
from mirage.core.mongodb.stream import read_stream
|
||||
from mirage.resource.mongodb.config import MongoDBConfig
|
||||
from mirage.types import PathSpec
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("MIRAGE_RUN_INTEGRATION_MONGO") != "1",
|
||||
reason="integration test (set MIRAGE_RUN_INTEGRATION_MONGO=1 to enable)",
|
||||
)
|
||||
|
||||
|
||||
def _load_env() -> str:
|
||||
repo_root = Path(__file__).resolve().parents[4]
|
||||
load_dotenv(repo_root / ".env.development")
|
||||
uri = os.environ.get("MONGODB_URI")
|
||||
if not uri:
|
||||
pytest.skip("MONGODB_URI not set in .env.development")
|
||||
return uri
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def accessor():
|
||||
uri = _load_env()
|
||||
return MongoDBAccessor(config=MongoDBConfig(uri=uri))
|
||||
|
||||
|
||||
async def _collect_lines(gen) -> list[str]:
|
||||
chunks = []
|
||||
async for chunk in gen:
|
||||
chunks.append(chunk)
|
||||
text = b"".join(chunks).decode()
|
||||
return [line for line in text.split("\n") if line]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extended_json_for_bson_types(accessor):
|
||||
path = PathSpec(resource_path="mirage_test/bson_types.jsonl",
|
||||
virtual="/mirage_test/bson_types.jsonl",
|
||||
directory="/mirage_test/bson_types.jsonl")
|
||||
lines = await _collect_lines(read_stream(accessor, path))
|
||||
assert len(lines) == 5
|
||||
by_label = {json.loads(line)["label"]: json.loads(line) for line in lines}
|
||||
scalars = by_label["scalars"]
|
||||
assert scalars["_id"] == {"$oid": "65f0000000000000000000a1"}
|
||||
assert scalars["decimal"] == {"$numberDecimal": "123456789.987654321"}
|
||||
assert scalars["int64"] == 1099511627776
|
||||
assert scalars["null"] is None
|
||||
temporal = by_label["temporal"]
|
||||
assert "$date" in temporal["date_utc"]
|
||||
binary_and_regex = by_label["binary_and_regex"]
|
||||
assert "$binary" in binary_and_regex["binary"]
|
||||
nested = by_label["nested"]
|
||||
assert nested["metadata"]["nested"]["leaf"] == {
|
||||
"$oid": "65f0000000000000000000b1"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streams_full_5000_docs_without_cap(accessor):
|
||||
path = PathSpec(resource_path="mirage_test/streaming_large.jsonl",
|
||||
virtual="/mirage_test/streaming_large.jsonl",
|
||||
directory="/mirage_test/streaming_large.jsonl")
|
||||
count = 0
|
||||
async for chunk in read_stream(accessor, path, batch_size=500):
|
||||
count += chunk.count(b"\n")
|
||||
assert count == 5000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_circuit_when_only_first_doc_consumed(accessor):
|
||||
path = PathSpec(resource_path="mirage_test/streaming_large.jsonl",
|
||||
virtual="/mirage_test/streaming_large.jsonl",
|
||||
directory="/mirage_test/streaming_large.jsonl")
|
||||
start = time.monotonic()
|
||||
gen = read_stream(accessor, path, batch_size=100)
|
||||
first = await gen.__anext__()
|
||||
await gen.aclose()
|
||||
elapsed = time.monotonic() - start
|
||||
assert first
|
||||
assert elapsed < 5.0
|
||||
Reference in New Issue
Block a user