Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Tests for capped collection convertToCapped behaviors."""

from __future__ import annotations

import pytest

from documentdb_tests.framework.assertions import assertProperties, assertSuccess
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.property_checks import Eq


# Property [Convert Max Ignored]: convertToCapped does not store the max
# parameter in collection metadata.
@pytest.mark.collection_mgmt
def test_capped_convert_max_ignored(database_client, collection):
"""Test that convertToCapped ignores the max parameter."""
name = f"{collection.name}_conv"
database_client.create_collection(name)
database_client[name].insert_many([{"_id": i} for i in range(5)])
execute_command(database_client[name], {"convertToCapped": name, "size": 100_000, "max": 2})
result = execute_command(database_client[name], {"collStats": name})
assertProperties(
result,
{"max": Eq(0)},
msg="convertToCapped should not store the max parameter",
raw_res=True,
)


# Property [Convert Max Not Enforced]: the max parameter passed to
# convertToCapped is not enforced on subsequent inserts.
@pytest.mark.collection_mgmt
def test_capped_convert_max_not_enforced(database_client, collection):
"""Test that max from convertToCapped is not enforced on inserts."""
name = f"{collection.name}_conv"
database_client.create_collection(name)
database_client[name].insert_many([{"_id": i} for i in range(5)])
execute_command(database_client[name], {"convertToCapped": name, "size": 100_000, "max": 2})
database_client[name].insert_many([{"_id": i} for i in range(5, 15)])
result = execute_command(database_client[name], {"find": name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": i} for i in range(15)],
msg="convertToCapped max should not limit subsequent inserts",
)


# Property [Convert Drops Secondary Indexes]: all secondary indexes are dropped
# during conversion and only the _id index survives.
@pytest.mark.collection_mgmt
def test_capped_convert_index_drop(database_client, collection):
"""Test that convertToCapped drops secondary indexes."""
name = f"{collection.name}_conv"
database_client.create_collection(name)
coll = database_client[name]
coll.insert_many([{"_id": i, "x": i, "y": str(i)} for i in range(5)])
coll.create_index("x")
coll.create_index("y")
execute_command(coll, {"convertToCapped": name, "size": 100_000})
result = execute_command(coll, {"listIndexes": name})
assertSuccess(
result,
["_id_"],
msg="convertToCapped should drop all secondary indexes",
raw_res=True,
transform=lambda r: [idx["name"] for idx in r["cursor"]["firstBatch"]],
)


# Property [Convert Order Preservation]: insertion order is preserved for
# retained documents after conversion.
@pytest.mark.collection_mgmt
def test_capped_convert_order(database_client, collection):
"""Test that convertToCapped preserves insertion order."""
name = f"{collection.name}_conv"
database_client.create_collection(name)
database_client[name].insert_many([{"_id": 5}, {"_id": 2}, {"_id": 8}, {"_id": 1}, {"_id": 3}])
execute_command(database_client[name], {"convertToCapped": name, "size": 100_000})
result = execute_command(database_client[name], {"find": name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 5}, {"_id": 2}, {"_id": 8}, {"_id": 1}, {"_id": 3}],
msg="convertToCapped should preserve insertion order for retained documents",
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Tests for capped collection delete behaviors."""

from __future__ import annotations

import pytest

from documentdb_tests.framework.assertions import assertProperties, assertSuccess
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.property_checks import Eq
from documentdb_tests.framework.target_collection import CappedCollection


# Property [Delete Order Preservation]: after deletes, remaining documents
# maintain their relative insertion order.
@pytest.mark.collection_mgmt
def test_capped_delete_order(database_client, collection):
"""Test that deletes preserve relative insertion order."""
coll = CappedCollection(size=100_000).resolve(database_client, collection)
coll.insert_many([{"_id": 1}, {"_id": 2}, {"_id": 3}, {"_id": 4}, {"_id": 5}])
execute_command(coll, {"delete": coll.name, "deletes": [{"q": {"_id": 3}, "limit": 1}]})
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 1}, {"_id": 2}, {"_id": 4}, {"_id": 5}],
msg="Remaining documents should maintain relative insertion order after delete",
)


# Property [Delete Insert Append]: after deletes, new inserts append at the
# end of natural order and deleted positions are not reused.
@pytest.mark.collection_mgmt
def test_capped_delete_insert_append(database_client, collection):
"""Test that new inserts after delete append at the end."""
coll = CappedCollection(size=100_000).resolve(database_client, collection)
coll.insert_many([{"_id": 1}, {"_id": 2}, {"_id": 3}])
execute_command(coll, {"delete": coll.name, "deletes": [{"q": {"_id": 2}, "limit": 1}]})
coll.insert_one({"_id": 4})
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 1}, {"_id": 3}, {"_id": 4}],
msg="New inserts after delete should append at the end of natural order",
)


# Property [Delete Preserves Capped]: the collection remains capped after any
# delete operation.
@pytest.mark.collection_mgmt
def test_capped_delete_remains_capped(database_client, collection):
"""Test that the collection remains capped after deletes."""
coll = CappedCollection(size=100_000).resolve(database_client, collection)
coll.insert_many([{"_id": 1}, {"_id": 2}, {"_id": 3}])
execute_command(coll, {"delete": coll.name, "deletes": [{"q": {}, "limit": 0}]})
result = execute_command(coll, {"collStats": coll.name})
assertProperties(
result,
{"capped": Eq(True)},
msg="Collection should remain capped after delete",
raw_res=True,
)


# Property [Delete Natural Hint Forward]: $natural:1 hint in a delete targets
# the oldest document.
@pytest.mark.collection_mgmt
def test_capped_delete_hint_natural_forward(database_client, collection):
"""Test $natural:1 hint targets the oldest document for deletion."""
coll = CappedCollection(size=100_000).resolve(database_client, collection)
coll.insert_many([{"_id": 1}, {"_id": 2}, {"_id": 3}, {"_id": 4}, {"_id": 5}])
execute_command(
coll, {"delete": coll.name, "deletes": [{"q": {}, "limit": 1, "hint": {"$natural": 1}}]}
)
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 2}, {"_id": 3}, {"_id": 4}, {"_id": 5}],
msg="$natural:1 hint should target the oldest document for deletion",
)


# Property [Delete Natural Hint Reverse]: $natural:-1 hint in a delete targets
# the newest document.
@pytest.mark.collection_mgmt
def test_capped_delete_hint_natural_reverse(database_client, collection):
"""Test $natural:-1 hint targets the newest document for deletion."""
coll = CappedCollection(size=100_000).resolve(database_client, collection)
coll.insert_many([{"_id": 1}, {"_id": 2}, {"_id": 3}, {"_id": 4}, {"_id": 5}])
execute_command(
coll, {"delete": coll.name, "deletes": [{"q": {}, "limit": 1, "hint": {"$natural": -1}}]}
)
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 1}, {"_id": 2}, {"_id": 3}, {"_id": 4}],
msg="$natural:-1 hint should target the newest document for deletion",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""Tests for capped collection eviction behaviors."""

from __future__ import annotations

import pytest

from documentdb_tests.framework.assertions import assertProperties, assertSuccess
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.property_checks import Eq
from documentdb_tests.framework.target_collection import CappedCollection


# Property [Eviction Order Preservation]: surviving documents maintain their
# relative insertion order after eviction has occurred.
@pytest.mark.collection_mgmt
def test_capped_eviction_order(database_client, collection):
"""Test insertion order preservation after eviction."""
coll = CappedCollection(size=100_000, max=5).resolve(database_client, collection)
for doc in [{"_id": i} for i in [10, 7, 3, 8, 1, 5, 9, 2, 6, 4]]:
coll.insert_one(doc)
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 5}, {"_id": 9}, {"_id": 2}, {"_id": 6}, {"_id": 4}],
msg="Surviving documents should maintain relative insertion order after eviction",
)


# Property [Evicted _id Reuse]: after eviction removes a document, its _id
# value can be reused in a new insert.
@pytest.mark.collection_mgmt
def test_capped_duplicate_id_reuse(database_client, collection):
"""Test that evicted _id values can be reused."""
coll = CappedCollection(size=100_000, max=2).resolve(database_client, collection)
for doc in [{"_id": 1}, {"_id": 2}, {"_id": 3}]:
coll.insert_one(doc)
# _id:1 was evicted (max=2 keeps only last 2). Reinsert it.
coll.insert_one({"_id": 1})
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 3}, {"_id": 1}],
msg="An evicted _id value should be reusable in a new insert",
)


# Property [Eviction Removes Index Entries]: evicted documents are no longer
# findable via secondary indexes.
@pytest.mark.collection_mgmt
def test_capped_eviction_index_cleanup(database_client, collection):
"""Test that eviction removes entries from secondary indexes."""
coll = CappedCollection(size=100_000, max=3).resolve(database_client, collection)
coll.create_index("x")
coll.insert_many([{"_id": 1, "x": "findme"}, {"_id": 2, "x": "b"}, {"_id": 3, "x": "c"}])
# Trigger eviction by inserting beyond max.
coll.insert_one({"_id": 4, "x": "d"})
# The evicted document should no longer be findable via the index.
result = execute_command(
coll, {"find": coll.name, "filter": {"x": "findme"}, "projection": {"_id": 1}}
)
assertSuccess(result, [], msg="Evicted documents should not be findable via secondary index")


# Property [Size-Based Eviction]: when the total data size exceeds the byte
# size cap, the oldest documents are evicted to make room.
@pytest.mark.collection_mgmt
def test_capped_size_based_eviction(database_client, collection):
"""Test that size-based eviction removes oldest documents."""
coll = CappedCollection(size=4096).resolve(database_client, collection)
for i in range(50):
coll.insert_one({"_id": i, "data": "x" * 500})
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})

def validate_size_eviction(r):
ids = [d["_id"] for d in r["cursor"]["firstBatch"]]
# Eviction occurred, most recent survives, in insertion order
return len(ids) < 50 and ids[-1] == 49 and ids == list(range(ids[0], ids[-1] + 1))

assertSuccess(
result,
True,
msg="Size-based eviction should remove oldest docs, preserving insertion order",
raw_res=True,
transform=validate_size_eviction,
)


# Property [Dual Constraint Max First]: when both size and max are specified
# and max is the tighter constraint, eviction is triggered by max.
@pytest.mark.collection_mgmt
def test_capped_dual_constraint_max_triggers(database_client, collection):
"""Test that max triggers eviction when it is the tighter constraint."""
coll = CappedCollection(size=1_000_000, max=3).resolve(database_client, collection)
coll.insert_many([{"_id": i} for i in range(7)])
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 4}, {"_id": 5}, {"_id": 6}],
msg="max should trigger eviction when it is the tighter constraint",
)


# Property [Dual Constraint Size First]: when both size and max are specified
# and size is the tighter constraint, eviction is triggered by size.
@pytest.mark.collection_mgmt
def test_capped_dual_constraint_size_triggers(database_client, collection):
"""Test that size triggers eviction when it is the tighter constraint."""
coll = CappedCollection(size=4096, max=1000).resolve(database_client, collection)
for i in range(50):
coll.insert_one({"_id": i, "data": "x" * 500})
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})

def validate_size_triggers_first(r):
ids = [d["_id"] for d in r["cursor"]["firstBatch"]]
# Size triggered (count well below max=1000), most recent survives, in order
return len(ids) < 50 and ids[-1] == 49 and ids == list(range(ids[0], ids[-1] + 1))

assertSuccess(
result,
True,
msg="Size should trigger eviction before max is reached",
raw_res=True,
transform=validate_size_triggers_first,
)


# Property [Batch Insert Eviction]: a single insertMany that exceeds the max
# document limit evicts oldest documents, keeping only the last max documents.
@pytest.mark.collection_mgmt
def test_capped_batch_insert_eviction(database_client, collection):
"""Test eviction during a single insertMany call."""
coll = CappedCollection(size=100_000, max=5).resolve(database_client, collection)
coll.insert_many([{"_id": i} for i in range(10)])
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 5}, {"_id": 6}, {"_id": 7}, {"_id": 8}, {"_id": 9}],
msg="Batch insert should evict oldest documents when max is exceeded",
)


# Property [Count Reflects Eviction]: the count command returns only the
# number of surviving documents after eviction.
@pytest.mark.collection_mgmt
def test_capped_count_reflects_eviction(database_client, collection):
"""Test that count reflects eviction."""
coll = CappedCollection(size=100_000, max=3).resolve(database_client, collection)
coll.insert_many([{"_id": i} for i in range(10)])
result = execute_command(coll, {"count": coll.name})
assertProperties(
result,
{"n": Eq(3)},
msg="Count should reflect only surviving documents after eviction",
raw_res=True,
)


# Property [Oversize Document Accepted]: a single document larger than the
# declared size cap is accepted.
@pytest.mark.collection_mgmt
def test_capped_oversize_document_accepted(database_client, collection):
"""Test that a document larger than the size cap is accepted."""
coll = CappedCollection(size=4096).resolve(database_client, collection)
coll.insert_one({"_id": 1, "data": "x" * 10000})
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 1}],
msg="A document larger than the size cap should be accepted",
)


# Property [Oversize Document Evicted]: after accepting an oversize document,
# subsequent inserts evict it following normal oldest-first eviction.
@pytest.mark.collection_mgmt
def test_capped_oversize_document_evicted(database_client, collection):
"""Test that an oversize document is evicted by subsequent inserts."""
coll = CappedCollection(size=4096).resolve(database_client, collection)
coll.insert_one({"_id": 1, "data": "x" * 10000})
coll.insert_one({"_id": 2, "data": "y" * 50})
result = execute_command(coll, {"find": coll.name, "projection": {"_id": 1}})
assertSuccess(
result,
[{"_id": 2}],
msg="Oversize document should be evicted by subsequent inserts",
)
Loading
Loading