Skip to content
Merged
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
2 changes: 1 addition & 1 deletion dbzero/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name='dbzero',
version='0.6.0',
version='0.6.1',
description='DBZero community edition',
packages=['dbzero'],
python_requires='>=3.9',
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ requires = ['meson-python']

[project]
name = 'dbzero'
version = '0.6.0'
version = '0.6.1'
description = 'A state management system for Python 3.x that unifies your applications business logic, data persistence, and caching into a single, efficient layer.'
readme = 'README.md'
requires-python = '>=3.9'
Expand Down
11 changes: 11 additions & 0 deletions python_tests/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ def test_index_can_store_nulls(db0_fixture):
assert values == [2, 0, 3, 4, 1]


def test_find_with_negated_tag_and_empty_subquery_returns_empty(db0_fixture):
index = db0.index()
contact = MemoTestClass("not-overdue")
index.add(datetime(2026, 1, 2), contact)

overdue_contacts = db0.find(MemoTestClass, index.select(None, datetime(2026, 1, 1)))
assert not overdue_contacts

assert list(db0.find(MemoTestClass, db0.no("archived"), overdue_contacts)) == []


def test_index_can_sort_by_multiple_criteria(db0_fixture):
index_1 = db0.index()
index_2 = db0.index()
Expand Down
67 changes: 67 additions & 0 deletions python_tests/test_tag_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,73 @@ def __init__(self):
assert list(db0.find(MixedRuntimeFields, "manual")) == [obj]


def test_passive_tag_field_then_regular_foreign_tag_preserves_regular_target(db0_fixture):
@db0.memo
class Scope:
pass

@db0.memo
@db0.tag_fields("scope")
class PassiveRecord:
def __init__(self, scope):
self.scope = scope

@db0.memo
class Error:
pass

def add_error(scope):
error = Error()
db0.tags(error).add(db0.as_tag(scope))

scope = Scope()

PassiveRecord(scope)
add_error(scope)

assert len(list(db0.find(Error, db0.as_tag(scope)))) == 1


def test_mixed_passive_regular_foreign_tag_remove_releases_regular_target(db0_fixture):
@db0.memo
class Scope:
pass

@db0.memo
@db0.tag_fields("scope")
class PassiveRecord:
def __init__(self, scope):
self.scope = scope

@db0.memo
class RegularTarget:
def __init__(self):
pass

scope = Scope()
tag = db0.as_tag(scope)
PassiveRecord(scope)

target = RegularTarget()
target_uuid = db0.uuid(target)
db0.tags(target).add(tag)
db0.commit()

assert list(db0.find(RegularTarget, tag)) == [target]
assert db0.getrefcount(target) == 1

db0.tags(target).remove(tag)
db0.commit()

assert list(db0.find(RegularTarget, tag)) == []
assert db0.getrefcount(target) == 0

del target
db0.commit()

assert not db0.exists(target_uuid)


def test_tag_field_removal_preserves_manual_collision_semantics(db0_fixture):
@db0.memo
@db0.tag_fields("status")
Expand Down
50 changes: 46 additions & 4 deletions python_tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,22 +124,41 @@ def test_passive_tags_can_be_added_and_removed_from_find_results(db0_fixture):
assert {item.value for item in db0.find("other")} == {3}


def test_passive_first_then_regular_tag_remains_non_durable(db0_fixture):
def test_passive_first_then_regular_tag_upgrades_to_durable(db0_fixture):
object_1 = MemoNoDefTags(1)
db0.tags(object_1, passive=True).add("passive-tag")
db0.commit()
db0.tags(object_1).add("passive-tag")
db0.commit()
assert db0.getrefcount(object_1) == 0
assert db0.getrefcount(object_1) == 1


def test_passive_first_then_regular_tag_in_same_transaction_remains_non_durable(db0_fixture):
def test_passive_first_then_regular_tag_in_same_transaction_upgrades_to_durable(db0_fixture):
object_1 = MemoNoDefTags(1)
db0.tags(object_1, passive=True).add("passive-tag")
db0.tags(object_1).add("passive-tag")
db0.commit()
assert db0.getrefcount(object_1) == 1


def test_passive_first_then_regular_tag_remove_releases_upgrade(db0_fixture):
object_1 = MemoNoDefTags(1)
object_uuid = db0.uuid(object_1)

db0.tags(object_1, passive=True).add("passive-tag")
db0.commit()
db0.tags(object_1).add("passive-tag")
db0.commit()
assert db0.getrefcount(object_1) == 1

db0.tags(object_1).remove("passive-tag")
db0.commit()
assert db0.getrefcount(object_1) == 0

del object_1
db0.commit()
assert not db0.exists(object_uuid)


def test_regular_first_then_passive_tag_remains_durable(db0_fixture):
object_1 = MemoNoDefTags(1)
Expand Down Expand Up @@ -224,7 +243,7 @@ def test_passive_foreign_tag_remove_uses_regular_remove(db0_fixture):
assert list(db0.find(MemoClassForTags, foreign_tag)) == []


def test_passive_foreign_tag_first_then_regular_remains_non_durable(db0_fixture):
def test_passive_foreign_tag_first_then_regular_upgrades_to_durable(db0_fixture):
foreign_tag_source = MemoScopedClass(2)
foreign_tag = db0.as_tag(foreign_tag_source)
db0.open("passive-long-tag-prefix", "rw")
Expand All @@ -235,6 +254,29 @@ def test_passive_foreign_tag_first_then_regular_remains_non_durable(db0_fixture)
db0.commit()
db0.tags(local_object).add(foreign_tag)
db0.commit()
assert db0.getrefcount(local_object) == 1

del local_object
db0.commit()
assert db0.exists(local_uuid)
assert [item.value for item in db0.find(MemoNoDefTags, foreign_tag)] == [1]


def test_passive_foreign_tag_first_then_regular_remove_releases_upgrade(db0_fixture):
foreign_tag_source = MemoScopedClass(2)
foreign_tag = db0.as_tag(foreign_tag_source)
db0.open("passive-long-tag-prefix", "rw")
local_object = MemoNoDefTags(1)
local_uuid = db0.uuid(local_object)

db0.tags(local_object, passive=True).add(foreign_tag)
db0.commit()
db0.tags(local_object).add(foreign_tag)
db0.commit()
assert db0.getrefcount(local_object) == 1

db0.tags(local_object).remove(foreign_tag)
db0.commit()
assert db0.getrefcount(local_object) == 0

del local_object
Expand Down
97 changes: 97 additions & 0 deletions src/dbzero/core/collections/b_index/bindex_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ namespace db0::bindex::interface
using bulkInsertUniquePtr = std::pair<std::uint32_t, std::uint32_t>(*)
(void *this_ptr, typename DefinitionT::Containers::IInputRange&, std::function<void(typename DefinitionT::item_t)> *callback_ptr);

/**
* Heteromorphic inserts are for item types whose comparator/equality identity can match even
* when the stored bit pattern differs. On such duplicates, resolver_ptr decides whether the
* incoming representation should replace the stored one.
*/
template<typename DefinitionT>
using bulkInsertUniqueHeteromorphicPtr = std::pair<std::uint32_t, std::uint32_t>(*)
(void *this_ptr, typename DefinitionT::Containers::IInputRange&,
std::function<void(typename DefinitionT::item_t)> *callback_ptr,
typename DefinitionT::HeteromorphicResolverT *resolver_ptr);

template <typename item_t> using insertPtr = void (*)(void *this_ptr, const item_t &item);

template<typename DefinitionT>
Expand Down Expand Up @@ -97,6 +108,8 @@ namespace db0::bindex::interface

template <typename DefinitionT, typename T> struct BulkInsertUniqueFunctor {};

template <typename DefinitionT, typename T> struct BulkInsertUniqueHeteromorphicFunctor {};

template <typename item_t, typename T> struct InsertFunctor {};

template <typename DefinitionT, typename T> struct BulkEraseFunctor {};
Expand Down Expand Up @@ -139,6 +152,17 @@ namespace db0::bindex::interface
return static_cast<InputRangeT&>(input).insert(index, callback_ptr);
}

template<typename DefinitionT, typename ContainerT>
std::pair<std::uint32_t, std::uint32_t>
insertHeteromorphicGenericImpl(void *self, typename DefinitionT::Containers::IInputRange &input,
std::function<void(typename DefinitionT::item_t)> *callback_ptr,
typename DefinitionT::HeteromorphicResolverT *resolver_ptr)
{
ContainerT &index = *reinterpret_cast<ContainerT*>(self);
using InputRangeT = typename DefinitionT::template IContainerInputRange<ContainerT>;
return static_cast<InputRangeT&>(input).insertHeteromorphic(index, callback_ptr, resolver_ptr);
}

template<typename DefinitionT, typename ContainerT>
std::size_t eraseGenericImpl(void *self, typename DefinitionT::Containers::IInputRange &input,
std::function<void(typename DefinitionT::item_t)> *callback_ptr)
Expand Down Expand Up @@ -179,6 +203,18 @@ namespace db0::bindex::interface
}
};

template<typename DefinitionT, typename... T>
struct BulkInsertUniqueHeteromorphicFunctor<DefinitionT, db0::v_bindex<T...>> {
using CallbackT = std::function<void(typename DefinitionT::item_t)>;
static std::pair<std::uint32_t, std::uint32_t>
execute(void *this_ptr, typename DefinitionT::Containers::IInputRange &input, CallbackT *callback_ptr,
typename DefinitionT::HeteromorphicResolverT *resolver_ptr)
{
return insertHeteromorphicGenericImpl<DefinitionT, db0::v_bindex<T...>>(
this_ptr, input, callback_ptr, resolver_ptr);
}
};

template <typename item_t, typename... T> struct InsertFunctor<item_t, db0::v_bindex<T...> > {
static void execute(void *this_ptr, const item_t &item) {
reinterpret_cast<db0::v_bindex<T...>*>(this_ptr)->insert(item);
Expand Down Expand Up @@ -333,6 +369,20 @@ namespace db0::bindex::interface
return insertGenericImpl<DefinitionT, ContainerT>(this_ptr, input, callback_ptr);
}
};

template<typename DefinitionT, int N, typename... T>
struct BulkInsertUniqueHeteromorphicFunctor<DefinitionT, db0::v_sorted_sequence<typename DefinitionT::item_t, N, T...>>
{
using CallbackT = std::function<void(typename DefinitionT::item_t)>;
static std::pair<std::uint32_t, std::uint32_t>
execute(void *this_ptr, typename DefinitionT::Containers::IInputRange &input, CallbackT *callback_ptr,
typename DefinitionT::HeteromorphicResolverT *resolver_ptr)
{
using ContainerT = db0::v_sorted_sequence<typename DefinitionT::item_t, N, T...>;
return insertHeteromorphicGenericImpl<DefinitionT, ContainerT>(
this_ptr, input, callback_ptr, resolver_ptr);
}
};

template <typename item_t, int N, typename... T> struct InsertFunctor<item_t, db0::v_sorted_sequence<item_t, N, T...> >
{
Expand Down Expand Up @@ -492,6 +542,18 @@ namespace db0::bindex::interface
}
};

template<typename DefinitionT, typename... T>
struct BulkInsertUniqueHeteromorphicFunctor<DefinitionT, db0::v_sorted_vector<T...>> {
using CallbackT = std::function<void(typename DefinitionT::item_t)>;
static std::pair<std::uint32_t, std::uint32_t>
execute(void *this_ptr, typename DefinitionT::Containers::IInputRange &input, CallbackT *callback_ptr,
typename DefinitionT::HeteromorphicResolverT *resolver_ptr)
{
return insertHeteromorphicGenericImpl<DefinitionT, db0::v_sorted_vector<T...>>(
this_ptr, input, callback_ptr, resolver_ptr);
}
};

template <typename item_t, typename... T> struct InsertFunctor<item_t, db0::v_sorted_vector<item_t, T...> > {
static void execute(void *this_ptr, const item_t &item) {
reinterpret_cast<db0::v_sorted_vector<item_t, T...>*>(this_ptr)->insert(item);
Expand Down Expand Up @@ -650,6 +712,18 @@ namespace db0::bindex::interface
return insertGenericImpl<DefinitionT, db0::IttyIndex<T...>>(this_ptr, input, callback_ptr);
}
};

template<typename DefinitionT, typename... T>
struct BulkInsertUniqueHeteromorphicFunctor<DefinitionT, db0::IttyIndex<T...> > {
using CallbackT = std::function<void(typename DefinitionT::item_t)>;
static std::pair<std::uint32_t, std::uint32_t>
execute(void *this_ptr, typename DefinitionT::Containers::IInputRange &input, CallbackT *callback_ptr,
typename DefinitionT::HeteromorphicResolverT *resolver_ptr)
{
return insertHeteromorphicGenericImpl<DefinitionT, db0::IttyIndex<T...>>(
this_ptr, input, callback_ptr, resolver_ptr);
}
};

template <typename item_t, typename... T> struct InsertFunctor<item_t, db0::IttyIndex<item_t, T...> >
{
Expand Down Expand Up @@ -799,6 +873,19 @@ namespace db0::bindex::interface
return insertGenericImpl<DefinitionT, db0::empty_index<T...> >(this_ptr, input, callback_ptr);
}
};

template<typename DefinitionT, typename... T>
struct BulkInsertUniqueHeteromorphicFunctor<DefinitionT, db0::empty_index<T...> >
{
using CallbackT = std::function<void(typename DefinitionT::item_t)>;
static std::pair<std::uint32_t, std::uint32_t>
execute(void *this_ptr, typename DefinitionT::Containers::IInputRange &input, CallbackT *callback_ptr,
typename DefinitionT::HeteromorphicResolverT *resolver_ptr)
{
return insertHeteromorphicGenericImpl<DefinitionT, db0::empty_index<T...> >(
this_ptr, input, callback_ptr, resolver_ptr);
}
};

template <typename item_t, typename... T>
struct InsertFunctor<item_t, db0::empty_index<T...> >
Expand Down Expand Up @@ -928,6 +1015,7 @@ namespace db0::bindex::interface
: m_ref(ref)
, m_ptr(m_ref.get())
, m_bulk_insert_unique_ptr(BulkInsertUniqueFunctor<DefinitionT, T>::execute)
, m_bulk_insert_unique_heteromorphic_ptr(BulkInsertUniqueHeteromorphicFunctor<DefinitionT, T>::execute)
, m_insert_ptr(InsertFunctor<item_t, T>::execute)
, m_bulk_erase_ptr(BulkEraseFunctor<DefinitionT, T>::execute)
, m_empty_ptr(EmptyFunctor<item_t, T>::execute)
Expand Down Expand Up @@ -956,6 +1044,14 @@ namespace db0::bindex::interface
db0::bindex::GenericInputRange<InputIterator, DefinitionT> input(begin, end);
return m_bulk_insert_unique_ptr(m_ptr, input, callback_ptr);
}

template <typename ItemT, typename InputIterator>
std::pair<std::uint32_t, std::uint32_t> bulkInsertUniqueHeteromorphic(InputIterator begin, InputIterator end,
std::function<void(ItemT)> *callback_ptr, typename DefinitionT::HeteromorphicResolverT *resolver_ptr)
{
db0::bindex::GenericInputRange<InputIterator, DefinitionT> input(begin, end);
return m_bulk_insert_unique_heteromorphic_ptr(m_ptr, input, callback_ptr, resolver_ptr);
}

/**
* Insert single item
Expand Down Expand Up @@ -1068,6 +1164,7 @@ namespace db0::bindex::interface
// pointer to actual data collection (persisted in m_ref)
void *m_ptr = nullptr;
bulkInsertUniquePtr<DefinitionT> m_bulk_insert_unique_ptr = nullptr;
bulkInsertUniqueHeteromorphicPtr<DefinitionT> m_bulk_insert_unique_heteromorphic_ptr = nullptr;
insertPtr<item_t> m_insert_ptr = nullptr;
bulkErasePtr<DefinitionT> m_bulk_erase_ptr = nullptr;
emptyPtr<item_t> m_empty_ptr = nullptr;
Expand Down
Loading
Loading