diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..540b5aea --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# dbzero + +dbzero is a state management system for persisting process state without a database. It lets Python processes keep their in-memory state durable across restarts — no separate DB server, schemas, or ORM. The core is C++ with Python bindings. See https://docs.dbzero.io for user-facing documentation. + +## Development workflow + +### TDD is required + +When implementing new features, follow test-driven development: + +1. Write a failing test first (Python tests in `python_tests/`, C++ tests under `tests/` / `subprojects/`). +2. Implement the minimum code to make the test pass. +3. Refactor while keeping tests green. + +All tests must pass before a change is considered complete. + +### Building + +- Debug build: `./docker/dbzero-build.sh` +- Release build: `./docker/dbzero-build.sh -r` + +### Running tests + +- Python tests: `./scripts/run_tests.sh` +- If any C++ source under the native/core part of the project was modified, also run the C++ test suite (do not rely on the Python tests alone to cover native changes). + +Never mark a task done while tests are failing. + +## Implementation notes + +### MorphingBIndex: address and type can change on mutation + +A `MorphingBIndex` does not behave like a typical container. On mutation (`insert`, `erase`) it may morph into a different internal storage variant (itty / array_2..4 / vector / bindex), and the morph can change both its **address** and its **type**. + +Consequences for any code that mutates a `MorphingBIndex`: + +- Any externally stored `{address, type}` pair referring to the bindex is potentially invalidated after every `insert` or `erase` call. Lookups through a stale pair read pre-mutation storage and return wrong data. +- A live handle to the bindex remains valid across the mutation and reflects the new storage; prefer re-reading `bindex.getAddress()` / `bindex.getIndexType()` from the handle over trusting any previously captured copy. +- Destructive shortcuts (destroying and rebuilding the whole bindex, or erasing it entirely from its parent) avoid the issue since no stale reference remains. + +When adding a new mutating path that operates on a `MorphingBIndex`, treat re-syncing any externally held `{address, type}` as mandatory, not an optimization. Collection-specific handling (where these pairs live, which paths must re-sync) is documented at the top of the relevant `.cpp` files. diff --git a/dbzero/dbzero/dbzero.py b/dbzero/dbzero/dbzero.py index c9e4f4dc..6bf38564 100644 --- a/dbzero/dbzero/dbzero.py +++ b/dbzero/dbzero/dbzero.py @@ -10,7 +10,7 @@ def load_dynamic(name, path): def __bootstrap__(): global __bootstrap__, __loader__, __file__ - paths = [os.path.join(os.path.split(__file__)[0]), "/src/dev/build/release", "/usr/local/lib/python3/dist-packages/dbzero/"] + paths = [os.path.join(os.path.split(__file__)[0]), "/src/dev/build/", "/usr/local/lib/python3/dist-packages/dbzero/"] __file__ = None for path in paths: if os.path.isdir(path): diff --git a/dbzero/setup.py b/dbzero/setup.py index 22f059e1..7398bcb9 100644 --- a/dbzero/setup.py +++ b/dbzero/setup.py @@ -10,7 +10,7 @@ setup( name='dbzero', - version='0.1.12', + version='0.2.1', description='DBZero community edition', packages=['dbzero'], python_requires='>=3.9', diff --git a/docker/Dockerfile-claude b/docker/Dockerfile-claude new file mode 100644 index 00000000..5f6db0c5 --- /dev/null +++ b/docker/Dockerfile-claude @@ -0,0 +1,88 @@ +FROM python:3.12-bullseye + +ARG NODE_MAJOR=20 +ARG CLAUDE_CODE_VERSION=latest +ARG DEV_USER=claude +ARG DEV_UID=1000 +ARG DEV_GID=1000 + +ENV DEBIAN_FRONTEND=noninteractive +ENV HOME=/home/${DEV_USER} +ENV CLAUDE_CONFIG_DIR=/home/${DEV_USER}/.claude +ENV PATH=/home/${DEV_USER}/.local/bin:${PATH} + +# Install development dependencies, Node.js 20, and Claude Code. +RUN apt-get update && apt-get install -y \ + ca-certificates \ + cmake \ + curl \ + gdb \ + gettext-base \ + git \ + gnupg \ + jq \ + less \ + meson \ + ninja-build \ + psmisc \ + python3-dbg \ + python3-pip \ + python3-venv \ + ripgrep \ + rsync \ + screen \ + unzip \ + valgrind \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \ + > /etc/apt/sources.list.d/nodesource.list \ + && apt-get update && apt-get install -y nodejs \ + && npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ + && node "$(npm root -g)/@anthropic-ai/claude-code/install.cjs" \ + && npm cache clean --force \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --gid ${DEV_GID} ${DEV_USER} \ + && useradd --uid ${DEV_UID} --gid ${DEV_GID} --create-home --shell /bin/bash ${DEV_USER} + +# Install Python build tooling and project requirements. +RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel build + +COPY requirements.txt /usr/src/dbzero/ +RUN python3 -m pip install --no-cache-dir --upgrade -r /usr/src/dbzero/requirements.txt + +# Seed Claude Code defaults and make them available in interactive shells. +COPY docker/claude-settings.json /opt/claude/settings.json +COPY docker/claude-shell-init.sh /usr/local/bin/claude-shell-init +COPY docker/dbzero-build.sh /usr/local/bin/dbzero-build +COPY docker/dbzero-build-package.sh /usr/local/bin/dbzero-build-package +RUN chmod +x /usr/local/bin/claude-shell-init \ + && chmod +x /usr/local/bin/dbzero-build /usr/local/bin/dbzero-build-package \ + && mkdir -p ${CLAUDE_CONFIG_DIR} /etc/profile.d \ + && chmod 700 ${CLAUDE_CONFIG_DIR} \ + && cp /opt/claude/settings.json ${CLAUDE_CONFIG_DIR}/settings.json \ + && chmod 600 ${CLAUDE_CONFIG_DIR}/settings.json \ + && chown -R ${DEV_UID}:${DEV_GID} ${HOME} /opt/claude \ + && printf '%s\n' \ + 'export CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"' \ + > /etc/profile.d/claude-code.sh \ + && printf '%s\n' \ + 'export CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"' \ + >> ${HOME}/.bashrc \ + && claude --version + +RUN ulimit -c unlimited + +COPY --chown=${DEV_UID}:${DEV_GID} . /usr/src/dbzero +WORKDIR /usr/src/dbzero + +RUN python3 scripts/generate_meson.py ./src/dbzero/ core +RUN python3 scripts/generate_meson_tests.py tests/ + +USER ${DEV_USER} + +ENTRYPOINT ["/usr/local/bin/claude-shell-init"] +CMD ["/bin/bash"] \ No newline at end of file diff --git a/docker/claude-settings.json b/docker/claude-settings.json new file mode 100644 index 00000000..26d40dfc --- /dev/null +++ b/docker/claude-settings.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "autoUpdatesChannel": "stable", + "defaultShell": "bash", + "env": { + "DISABLE_AUTOUPDATER": "1" + } +} \ No newline at end of file diff --git a/docker/claude-shell-init.sh b/docker/claude-shell-init.sh new file mode 100644 index 00000000..0c5828e8 --- /dev/null +++ b/docker/claude-shell-init.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +export CLAUDE_CONFIG_DIR + +mkdir -p "$CLAUDE_CONFIG_DIR" +chmod 700 "$CLAUDE_CONFIG_DIR" + +if [[ ! -f "$CLAUDE_CONFIG_DIR/settings.json" && -f /opt/claude/settings.json ]]; then + cp /opt/claude/settings.json "$CLAUDE_CONFIG_DIR/settings.json" + chmod 600 "$CLAUDE_CONFIG_DIR/settings.json" +fi + +if [[ $# -eq 0 ]]; then + exec /bin/bash +fi + +exec "$@" \ No newline at end of file diff --git a/docker/dbzero-build-package.sh b/docker/dbzero-build-package.sh new file mode 100755 index 00000000..359ab929 --- /dev/null +++ b/docker/dbzero-build-package.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +show_help() { + echo "Build dbzero python package in the ./build directory and optionally install it" + echo "Use: dbzero-build-package [options]" + echo " -h, --help Shows this help screen." + echo " --install Install the package locally" + exit 0 +} + +if [[ ! -f setup.py || ! -d dbzero ]]; then + echo "Run dbzero-build-package from the repository root." >&2 + exit 1 +fi + +install_package="false" +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) show_help ;; + --install) install_package="true" ; shift ;; + --) shift ; break ;; + *) break ;; + esac +done + +pip_install_args=() +if python3 -m pip install --help 2>/dev/null | grep -q -- "--break-system-packages"; then + pip_install_args+=(--break-system-packages) +fi +if [[ "$(id -u)" -ne 0 && -z "${VIRTUAL_ENV:-}" ]]; then + pip_install_args+=(--user) +fi + +rm -rf ./.build +mkdir -p ./.build/dbzero +cp ./dbzero/* ./.build/dbzero +cp setup.py ./.build/setup.py +cp LICENSE ./.build/LICENSE +cp README.md ./.build/README.md + +cd ./.build +python3 setup.py sdist + +if [[ "$install_package" == "true" ]]; then + python3 -m pip install "${pip_install_args[@]}" "$(ls ./dist/*.tar.gz | sort | tail -n 1)" +fi + +cd .. +rm -rf ./.build \ No newline at end of file diff --git a/docker/dbzero-build.sh b/docker/dbzero-build.sh new file mode 100755 index 00000000..7d779ca1 --- /dev/null +++ b/docker/dbzero-build.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +show_help() { + echo "Builds dbzero for the Claude development container" + echo "Use: dbzero-build [options]" + echo " -h, --help Shows this help screen." + echo " -j, --jobs Threads number. Max by default." + echo " -r, --release Compile as release. Note: debug build is by default." + echo " -s, --sanitize Compile with sanitizers." + echo " -p, --prefix Install build under the specified prefix." + echo " -t, --tests Build tests." + echo " -e, --disable_debug_exceptions Disable debug exceptions." + echo "" + exit 0 +} + +if [[ ! -f scripts/generate_meson.py || ! -d dbzero ]]; then + echo "Run dbzero-build from the repository root." >&2 + exit 1 +fi + +export CPLUS_INCLUDE_PATH="${CPLUS_INCLUDE_PATH:-}:/usr/include/python3.9/" + +cores=$(grep -c ^processor /proc/cpuinfo) +build_type="debug" +sanitizer="false" +enable_debug_exceptions="true" +build_tests="false" + +default_prefix="/usr/local" +if [[ "$(id -u)" -ne 0 ]]; then + default_prefix="$HOME/.local" +fi +install_prefix="${DBZERO_INSTALL_PREFIX:-$default_prefix}" + +temp=$(getopt -o hj:rtsep: --long help,jobs:,release,tests,sanitize,disable_debug_exceptions,prefix: -n 'dbzero-build' -- "$@") +if [[ $? -ne 0 ]]; then + exit 1 +fi +eval set -- "$temp" + +while true; do + case "$1" in + -h|--help) show_help ;; + -s|--sanitize) sanitizer="true" ; shift ;; + -r|--release) build_type="release" ; shift ;; + -t|--tests) build_tests="true" ; shift ;; + -e|--disable_debug_exceptions) enable_debug_exceptions="false" ; shift ;; + -p|--prefix) install_prefix="$2" ; shift 2 ;; + -j|--jobs) cores="$2" ; shift 2 ;; + --) shift ; break ;; + *) echo "Argument parsing error: $1" >&2 ; exit 1 ;; + esac +done + +if [[ "$cores" -lt 1 ]]; then + echo "Argument parsing error: Wrong jobs number: $cores" >&2 + exit 1 +fi + +python3 scripts/generate_meson.py ./src/dbzero/ core +python3 scripts/generate_meson_tests.py tests/ +python3 scripts/generate_meson_dbzero.py dbzero/ + +mkdir -p build + +build_dir="build/debug" +if [[ "$build_type" == "release" ]]; then + build_dir="build/release" +fi + +options=( + "-Denable_debug_exceptions=$enable_debug_exceptions" + "-Denable_sanitizers=$sanitizer" + "-Dbuild_tests=$build_tests" +) + +if [[ -f "$build_dir/meson-private/coredata.dat" ]]; then + meson setup --reconfigure --prefix="$install_prefix" --buildtype="$build_type" "${options[@]}" "$build_dir" +else + meson setup --prefix="$install_prefix" --buildtype="$build_type" "${options[@]}" "$build_dir" +fi + +ninja -C "$build_dir" -j "$cores" +meson install -C "$build_dir" + +cd dbzero +envsubst < dbzero/dbzero.template > dbzero/dbzero.py +dbzero-build-package --install +echo "$build_type" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b89ee86a..5aeb6f0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ['meson-python'] [project] name = 'dbzero' -version = '0.1.12' +version = '0.2.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' diff --git a/python_tests/test_auto_weak_proxy.py b/python_tests/test_auto_weak_proxy.py index 12563095..18982d3f 100644 --- a/python_tests/test_auto_weak_proxy.py +++ b/python_tests/test_auto_weak_proxy.py @@ -53,7 +53,6 @@ def test_auto_wrap_expires_when_source_deleted(db0_fixture): _ = obj_2.value.value - def test_auto_wrap_on_cross_prefix_assignment(db0_fixture): """Cross-prefix assignment is silently wrapped as weak_proxy by default.""" px_1 = db0.get_current_prefix().name diff --git a/python_tests/test_set.py b/python_tests/test_set.py index 846ac077..e488cf37 100644 --- a/python_tests/test_set.py +++ b/python_tests/test_set.py @@ -658,3 +658,36 @@ def test_db0_set_iterator_type_valid(db0_fixture): s = db0.set() it = iter(s) assert type(type(it)) is type + + +def test_db0_set_remove_with_hash_collision(db0_fixture): + # The TUPLE hash in PyHash.cpp is xor-of-element-hashes, so any + # two permutations of the same elements collide. That puts them + # in the same m_index bucket, which is the only path that exercises + # Set::remove's bindex.erase(*it) branch (size > 1). + # + # If that branch fails to re-sync m_index after the underlying + # bindex morphs to a smaller container, the bucket's stale + # {address, type} will keep reporting pre-erase data on later + # lookups — wrong len(), wrong `in`, stale iteration. + a = (1, 2) + b = (2, 1) + + s = db0.set() + s.add(a) + s.add(b) + assert len(s) == 2 + assert a in s + assert b in s + + s.remove(a) + assert a not in s + assert b in s + assert len(s) == 1 + # Iteration should see only b. + assert list(s) == [b] + + # Removing the last colliding element must fully clean the bucket. + s.remove(b) + assert b not in s + assert len(s) == 0 diff --git a/python_tests/test_weak_refs.py b/python_tests/test_weak_refs.py index f902be59..300959a5 100644 --- a/python_tests/test_weak_refs.py +++ b/python_tests/test_weak_refs.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: LGPL-2.1-or-later # Copyright (c) 2025 DBZero Software sp. z o.o. +import random + import pytest import dbzero as db0 from .memo_test_types import MemoTestPxClass @@ -178,7 +180,7 @@ def test_long_weak_ref_inside_set(db0_fixture): obj_1 = MemoTestPxClass(123, prefix=px_1) set_1 = db0.set([db0.weak_proxy(obj_1)]) for obj in set_1: - assert obj == obj_1 + assert obj == obj_1 def test_long_weak_ref_inside_dict(db0_fixture): diff --git a/python_tests/test_weak_set.py b/python_tests/test_weak_set.py new file mode 100644 index 00000000..ea85fa7c --- /dev/null +++ b/python_tests/test_weak_set.py @@ -0,0 +1,301 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# Copyright (c) 2025 DBZero Software sp. z o.o. + +import pytest +import dbzero as db0 +from .memo_test_types import MemoTestClass, MemoTestPxClass, MemoScopedSingleton +from .conftest import DB0_DIR + + +def test_weak_set_created_empty(db0_fixture): + ws = db0.weak_set() + assert ws is not None + assert len(ws) == 0 + + +def test_weak_set_from_iterable(db0_fixture): + obj_1 = MemoTestClass(1) + obj_2 = MemoTestClass(2) + ws = db0.weak_set([obj_1, obj_2]) + assert len(ws) == 2 + + +def test_weak_set_primitives_rejected(db0_fixture): + with pytest.raises((TypeError, Exception)): + db0.weak_set([1]) + + +def test_weak_set_contains_by_actual_instance_same_prefix(db0_fixture): + obj_1 = MemoTestClass(1) + obj_2 = MemoTestClass(2) + ws = db0.weak_set([obj_1]) + assert obj_1 in ws + assert obj_2 not in ws + + +def test_weak_set_contains_by_actual_instance_cross_prefix(db0_fixture): + px_1 = db0.get_current_prefix().name + px_2 = "some-other-prefix" + db0.open(px_2, "rw") + obj_1 = MemoTestPxClass(123, prefix=px_1) + ws = MemoTestPxClass(db0.weak_set([obj_1]), prefix=px_2) + assert obj_1 in ws.value + + +def test_weak_set_iter_dereferences(db0_fixture): + obj_1 = MemoTestClass(1) + obj_2 = MemoTestClass(2) + ws = db0.weak_set([obj_1, obj_2]) + seen = list(ws) + assert len(seen) == 2 + assert obj_1 in seen + assert obj_2 in seen + + +def test_weak_set_len_after_del(db0_fixture): + px_1 = db0.get_current_prefix().name + px_2 = "some-other-prefix" + db0.open(px_2, "rw") + obj_1 = MemoTestPxClass(123, prefix=px_1) + ws = MemoTestPxClass(db0.weak_set([obj_1]), prefix=px_2) + assert len(ws.value) == 1 + del obj_1 + db0.commit() + assert len(ws.value) == 0 + + +def test_weak_set_iter_skips_expired(db0_fixture): + px_1 = db0.get_current_prefix().name + px_2 = "some-other-prefix" + db0.open(px_2, "rw") + obj_1 = MemoTestPxClass(1, prefix=px_1) + obj_2 = MemoTestPxClass(2, prefix=px_1) + ws = MemoTestPxClass(db0.weak_set([obj_1, obj_2]), prefix=px_2) + del obj_1 + db0.commit() + seen = list(ws.value) + assert len(seen) == 1 + assert seen[0] == obj_2 + + +def test_weak_set_add_discard_remove(db0_fixture): + obj_1 = MemoTestClass(1) + obj_2 = MemoTestClass(2) + ws = db0.weak_set() + ws.add(obj_1) + ws.add(obj_2) + assert len(ws) == 2 + ws.discard(obj_1) + assert len(ws) == 1 + assert obj_1 not in ws + ws.remove(obj_2) + assert len(ws) == 0 + with pytest.raises(KeyError): + ws.remove(obj_1) + + +def test_weak_set_does_not_increment_refcount(db0_fixture): + px_1 = db0.get_current_prefix().name + px_2 = "some-other-prefix" + db0.open(px_2, "rw") + obj_1 = MemoTestPxClass(123, prefix=px_1) + cnt_before = db0.getrefcount(obj_1) + ws = MemoTestPxClass(db0.weak_set([obj_1]), prefix=px_2) + assert db0.getrefcount(obj_1) == cnt_before + + +def test_weak_set_copy(db0_fixture): + obj_1 = MemoTestClass(1) + obj_2 = MemoTestClass(2) + ws = db0.weak_set([obj_1, obj_2]) + ws_copy = ws.copy() + assert len(ws_copy) == 2 + assert obj_1 in ws_copy + assert obj_2 in ws_copy + + +def test_weak_set_persistence(db0_fixture): + obj_1 = MemoTestClass(1) + holder = MemoTestClass(db0.weak_set([obj_1])) + db0.commit() + assert len(holder.value) == 1 + assert obj_1 in holder.value + + +def test_weak_set_discard_mixed_prefix(db0_fixture): + # Minimal reproducer: a weak_set that holds exactly one object + # from the current prefix (short weak ref) and one from another + # prefix (long weak ref). discard() on either element is expected + # to remove it; today it silently does nothing and len stays at 2. + px_current = db0.get_current_prefix().name + px_other = "some-other-prefix" + db0.open(px_other, "rw") + + obj_curr = MemoTestPxClass(1, prefix=px_current) + obj_other = MemoTestPxClass(2, prefix=px_other) + + ws = db0.weak_set() + ws.add(obj_curr) + ws.add(obj_other) + assert len(ws) == 2 + assert obj_curr in ws + assert obj_other in ws + + # Discarding the current-prefix (short weak ref) member: the + # long weak ref from the other prefix must remain. + ws.discard(obj_curr) + assert obj_curr not in ws + assert obj_other in ws + assert len(ws) == 1 + + # And discarding the remaining cross-prefix member empties the set. + ws.discard(obj_other) + assert obj_other not in ws + assert len(ws) == 0 + + +def test_weak_set_bulk_add_discard_contains(db0_fixture): + import random + + # Mix objects from two prefixes so the weak_set holds both + # same-prefix weak refs and cross-prefix "long" weak refs. + px_current = db0.get_current_prefix().name + px_other = "some-other-prefix" + db0.open(px_other, "rw") + + n = 128 + objs = [ + MemoTestPxClass(i, prefix=px_current if i % 2 == 0 else px_other) + for i in range(n) + ] + + # Populate ws and verify every element is a member. + ws = db0.weak_set() + for o in objs: + ws.add(o) + assert len(ws) == n + for o in objs: + assert o in ws + + # Objects not added — from either prefix — must not be reported as members. + assert MemoTestPxClass(9998, prefix=px_current) not in ws + assert MemoTestPxClass(9999, prefix=px_other) not in ws + + # Discard a random half, then confirm membership on both sides. + # Split by id() so memo value-equality cannot move objects between groups. + rng = random.Random(42) + to_discard = rng.sample(objs, n // 2) + discard_ids = {id(o) for o in to_discard} + remaining = [o for o in objs if id(o) not in discard_ids] + + for o in to_discard: + ws.discard(o) + + assert len(ws) == n - len(to_discard) + for o in to_discard: + assert o not in ws + for o in remaining: + assert o in ws + + +def test_weak_set_persistence_across_close(db0_fixture): + px_current = db0.get_current_prefix().name + px_other = "some-other-prefix" + db0.open(px_other, "rw") + + n = 120 + objs = [ + MemoTestPxClass(i, prefix=px_current if i % 2 == 0 else px_other) + for i in range(n) + ] + + holder = MemoTestPxClass(db0.weak_set(objs), prefix=px_other) + assert len(holder.value) == n + + # Per-prefix singletons anchor the objects and the holder so they + # survive close/reopen and the weak refs in the set stay resolvable. + MemoScopedSingleton( + value=[o for o in objs if o.value % 2 == 0], prefix=px_current + ) + MemoScopedSingleton( + value=[holder] + [o for o in objs if o.value % 2 == 1], + prefix=px_other, + ) + + obj_values = sorted(o.value for o in objs) + + del holder, objs + db0.commit() + db0.close() + + db0.init(DB0_DIR) + db0.open(px_current, "r") + db0.open(px_other, "r") + + root_other = db0.fetch(MemoScopedSingleton, prefix=px_other) + holder2 = root_other.value[0] + ws = holder2.value + assert len(ws) == n + + root_current = db0.fetch(MemoScopedSingleton, prefix=px_current) + all_objs = list(root_current.value) + list(root_other.value[1:]) + assert sorted(o.value for o in all_objs) == obj_values + for o in all_objs: + assert o in ws + + +def test_weak_set_persistence_only_holder_prefix_opened(db0_fixture): + # After close, reopen ONLY the prefix that owns the weak_set. + # Half the members are long weak refs into a prefix that is not loaded. + # Documents the current behavior: every set-wide operation on such a + # weak_set raises because the foreign prefix cannot be resolved. + px_current = db0.get_current_prefix().name + px_other = "some-other-prefix" + db0.open(px_other, "rw") + + n = 120 + objs = [ + MemoTestPxClass(i, prefix=px_current if i % 2 == 0 else px_other) + for i in range(n) + ] + + holder = MemoTestPxClass(db0.weak_set(objs), prefix=px_other) + assert len(holder.value) == n + + MemoScopedSingleton( + value=[o for o in objs if o.value % 2 == 0], prefix=px_current + ) + MemoScopedSingleton( + value=[holder] + [o for o in objs if o.value % 2 == 1], + prefix=px_other, + ) + + del holder, objs + db0.commit() + db0.close() + + db0.init(DB0_DIR) + # Deliberately open only the holder's prefix — the other one stays closed. + db0.open(px_other, "r") + + root_other = db0.fetch(MemoScopedSingleton, prefix=px_other) + holder2 = root_other.value[0] + ws = holder2.value + same_prefix_objs = list(root_other.value[1:]) + # Sanity: same-prefix anchors are fully reachable on their own. + assert len(same_prefix_objs) == n // 2 + + # len(), iteration, and membership all traverse the full underlying + # storage and hit the unresolvable long weak refs into px_current. + with pytest.raises(Exception): + len(ws) + with pytest.raises(RuntimeError, match="Prefix:"): + list(ws) + with pytest.raises(RuntimeError, match="Prefix:"): + _ = same_prefix_objs[0] in ws + + # Opening the missing prefix heals the set. + db0.open(px_current, "r") + assert len(ws) == n + for o in same_prefix_objs: + assert o in ws diff --git a/src/dbzero/bindings/TypeId.hpp b/src/dbzero/bindings/TypeId.hpp index 26240bf6..6aee9403 100644 --- a/src/dbzero/bindings/TypeId.hpp +++ b/src/dbzero/bindings/TypeId.hpp @@ -58,8 +58,9 @@ namespace db0::bindings // Python type decorated as memo MEMO_TYPE = 118, MEMO_IMMUTABLE_OBJECT = 119, + DB0_WEAK_SET = 120, // COUNT determines size of the type operator arrays - COUNT = 120, + COUNT = 121, // unrecognized type UNKNOWN = std::numeric_limits::max() }; diff --git a/src/dbzero/bindings/python/PyHash.hpp b/src/dbzero/bindings/python/PyHash.hpp index b836f2ad..8ec85086 100644 --- a/src/dbzero/bindings/python/PyHash.hpp +++ b/src/dbzero/bindings/python/PyHash.hpp @@ -25,7 +25,7 @@ namespace db0::python template std::int64_t getPyHashImpl(db0::swine_ptr &, PyObject *); - // NOTE: in rare cases type may be hashable but hash cannot be calculate if instance does not exist + // NOTE: in rare cases type may be hashable but hash cannot be calculated if an instance does not exist // e.g. EnumValueRepr without actual EnumValue materialized yet // in such cases this function will not raise any exception but return std::nullopt std::optional > getPyHashIfExists( diff --git a/src/dbzero/bindings/python/PyToolkit.cpp b/src/dbzero/bindings/python/PyToolkit.cpp index 0fc3360b..3d341d24 100644 --- a/src/dbzero/bindings/python/PyToolkit.cpp +++ b/src/dbzero/bindings/python/PyToolkit.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -462,7 +463,7 @@ namespace db0::python return shared_py_cast(std::move(py_index)); } - PyToolkit::ObjectSharedPtr PyToolkit::unloadSet(db0::swine_ptr fixture, + PyToolkit::ObjectSharedPtr PyToolkit::unloadSet(db0::swine_ptr fixture, Address address, std::uint16_t, AccessFlags access_mode) { // try pulling from cache first @@ -472,17 +473,35 @@ namespace db0::python // return from cache return object_ptr; } - + auto set_object = SetDefaultObject_new(); // retrieve actual dbzero instance set_object->unload(fixture, address, access_mode); - + // add list object to cache if (!set_object->ext().isNoCache()) { lang_cache.add(address, set_object.get()); } return shared_py_cast(std::move(set_object)); } + + PyToolkit::ObjectSharedPtr PyToolkit::unloadWeakSet(db0::swine_ptr fixture, + Address address, std::uint16_t, AccessFlags access_mode) + { + auto &lang_cache = fixture->getLangCache(); + auto object_ptr = lang_cache.get(address); + if (object_ptr.get()) { + return object_ptr; + } + + auto weak_set_object = WeakSetDefaultObject_new(); + weak_set_object->unload(fixture, address, access_mode); + + if (!weak_set_object->ext().isNoCache()) { + lang_cache.add(address, weak_set_object.get()); + } + return shared_py_cast(std::move(weak_set_object)); + } PyToolkit::ObjectSharedPtr PyToolkit::unloadDict(db0::swine_ptr fixture, Address address, std::uint16_t, AccessFlags access_mode) diff --git a/src/dbzero/bindings/python/PyToolkit.hpp b/src/dbzero/bindings/python/PyToolkit.hpp index 06702d57..6b918e11 100644 --- a/src/dbzero/bindings/python/PyToolkit.hpp +++ b/src/dbzero/bindings/python/PyToolkit.hpp @@ -129,6 +129,7 @@ namespace db0::python static ObjectSharedPtr unloadList(db0::swine_ptr, Address, std::uint16_t instance_id = 0, AccessFlags = {}); static ObjectSharedPtr unloadIndex(db0::swine_ptr, Address, std::uint16_t instance_id = 0, AccessFlags = {}); static ObjectSharedPtr unloadSet(db0::swine_ptr, Address, std::uint16_t instance_id = 0, AccessFlags = {}); + static ObjectSharedPtr unloadWeakSet(db0::swine_ptr, Address, std::uint16_t instance_id = 0, AccessFlags = {}); static ObjectSharedPtr unloadDict(db0::swine_ptr, Address, std::uint16_t instance_id = 0, AccessFlags = {}); static ObjectSharedPtr unloadTuple(db0::swine_ptr, Address, std::uint16_t instance_id = 0, AccessFlags = {}); // Unload dbzero block instance diff --git a/src/dbzero/bindings/python/PyTypeManager.cpp b/src/dbzero/bindings/python/PyTypeManager.cpp index df5969f4..a86662f3 100644 --- a/src/dbzero/bindings/python/PyTypeManager.cpp +++ b/src/dbzero/bindings/python/PyTypeManager.cpp @@ -9,6 +9,7 @@ #include "PyWeakProxy.hpp" #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +88,7 @@ namespace db0::python addStaticdbzeroType(&IndexObjectType, TypeId::DB0_INDEX); addStaticdbzeroType(&ListObjectType, TypeId::DB0_LIST); addStaticdbzeroType(&SetObjectType, TypeId::DB0_SET); + addStaticdbzeroType(&WeakSetObjectType, TypeId::DB0_WEAK_SET); addStaticdbzeroType(&DictObjectType, TypeId::DB0_DICT); addStaticdbzeroType(&TupleObjectType, TypeId::DB0_TUPLE); addStaticdbzeroType(&ClassObjectType, TypeId::DB0_CLASS); @@ -282,6 +285,22 @@ namespace db0::python } return reinterpret_cast(set_ptr)->modifyExt(); } + + const db0::object_model::WeakSet &PyTypeManager::extractWeakSet(ObjectPtr set_ptr) const + { + if (!WeakSetObject_Check(set_ptr)) { + THROWF(db0::InputException) << "Expected a weak set object" << THROWF_END; + } + return reinterpret_cast(set_ptr)->ext(); + } + + db0::object_model::WeakSet &PyTypeManager::extractMutableWeakSet(ObjectPtr set_ptr) const + { + if (!WeakSetObject_Check(set_ptr)) { + THROWF(db0::InputException) << "Expected a weak set object" << THROWF_END; + } + return reinterpret_cast(set_ptr)->modifyExt(); + } db0::object_model::ByteArray &PyTypeManager::extractMutableByteArray(ObjectPtr py_obj) const { diff --git a/src/dbzero/bindings/python/PyTypeManager.hpp b/src/dbzero/bindings/python/PyTypeManager.hpp index 31315172..f7394dfd 100644 --- a/src/dbzero/bindings/python/PyTypeManager.hpp +++ b/src/dbzero/bindings/python/PyTypeManager.hpp @@ -31,6 +31,7 @@ namespace db0::object_model { class Class; class List; class Set; + class WeakSet; class Tuple; class Dict; class TagSet; @@ -74,6 +75,7 @@ namespace db0::python using ObjectAnyImpl = db0::object_model::ObjectAnyImpl; using List = db0::object_model::List; using Set = db0::object_model::Set; + using WeakSet = db0::object_model::WeakSet; using Tuple = db0::object_model::Tuple; using Dict = db0::object_model::Dict; using TagSet = db0::object_model::TagSet; @@ -127,6 +129,8 @@ namespace db0::python List &extractMutableList(ObjectPtr list_ptr) const; const Set &extractSet(ObjectPtr set_ptr) const; Set &extractMutableSet(ObjectPtr set_ptr) const; + const WeakSet &extractWeakSet(ObjectPtr set_ptr) const; + WeakSet &extractMutableWeakSet(ObjectPtr set_ptr) const; std::int64_t extractInt64(ObjectPtr int_ptr) const; std::uint64_t extractUInt64(ObjectPtr) const; std::uint64_t extractUInt64(TypeId, ObjectPtr) const; diff --git a/src/dbzero/bindings/python/collections/PyWeakSet.cpp b/src/dbzero/bindings/python/collections/PyWeakSet.cpp new file mode 100644 index 00000000..bb495b00 --- /dev/null +++ b/src/dbzero/bindings/python/collections/PyWeakSet.cpp @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include "PyWeakSet.hpp" +#include "PyIterator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace db0::python + +{ + + using ObjectSharedPtr = PyTypes::ObjectSharedPtr; + using WeakSetIteratorObject = PySharedWrapper; + + PyTypeObject WeakSetIteratorObjectType = GetIteratorType("dbzero.WeakSetObjectIterator", + "dbzero weak-set object iterator"); + + WeakSetIteratorObject *tryWeakSetObject_iter(WeakSetObject *self) + { + return makeIterator( + WeakSetIteratorObjectType, self->ext().begin(), &self->ext(), self + ); + } + + WeakSetIteratorObject *PyAPI_WeakSetObject_iter(WeakSetObject *self) + { + PY_API_FUNC + return runSafe(tryWeakSetObject_iter, self); + } + + int tryWeakSetObject_HasItem(WeakSetObject *self, PyObject *key) + { + PY_API_FUNC + auto fixture = self->ext().getFixture(); + auto maybe_hash_pair = getPyHashIfExists(fixture, key); + if (!maybe_hash_pair) { + return 0; + } + return self->ext().hasItem(maybe_hash_pair->first, *maybe_hash_pair->second); + } + + int PyAPI_WeakSetObject_HasItem(WeakSetObject *self, PyObject *key) + { + PY_API_FUNC + return runSafe<-1>(tryWeakSetObject_HasItem, self, key); + } + + Py_ssize_t tryWeakSetObject_len(WeakSetObject *self) + { + self->ext().getFixture()->refreshIfUpdated(); + return self->ext().size(); + } + + Py_ssize_t PyAPI_WeakSetObject_len(WeakSetObject *self) + { + PY_API_FUNC + return runSafe(tryWeakSetObject_len, self); + } + + PyObject *tryWeakSetObject_add(WeakSetObject *self, PyObject *const *args, Py_ssize_t) + { + auto fixture = self->ext().getFixture(); + auto hash = getPyHash(fixture, args[0]); + db0::FixtureLock lock(fixture); + self->modifyExt().append(lock, hash, ObjectSharedPtr(args[0])); + Py_RETURN_NONE; + } + + PyObject *PyAPI_WeakSetObject_add(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs) + { + PY_API_FUNC + if (nargs != 1) { + PyErr_SetString(PyExc_TypeError, "add() takes exactly one argument"); + return NULL; + } + return runSafe(tryWeakSetObject_add, self, args, nargs); + } + + PyObject *tryWeakSetObject_remove(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs, bool throw_ex) + { + if (nargs != 1) { + PyErr_SetString(PyExc_TypeError, "remove() takes exactly one argument"); + return NULL; + } + + auto fixture = self->ext().getFixture(); + auto maybe_hash = getPyHashIfExists(fixture, args[0]); + if (maybe_hash) { + db0::FixtureLock lock(fixture); + if (self->modifyExt().remove(lock, maybe_hash->first, args[0])) { + Py_RETURN_NONE; + } + } + + if (throw_ex) { + PyErr_SetString(PyExc_KeyError, "Element not found"); + return NULL; + } + Py_RETURN_NONE; + } + + PyObject *PyAPI_WeakSetObject_remove(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs) + { + PY_API_FUNC + return runSafe(tryWeakSetObject_remove, self, args, nargs, true); + } + + PyObject *PyAPI_WeakSetObject_discard(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs) + { + PY_API_FUNC + return runSafe(tryWeakSetObject_remove, self, args, nargs, false); + } + + PyObject *tryWeakSetObject_clear(WeakSetObject *self, PyObject *const *, Py_ssize_t) + { + db0::FixtureLock lock(self->ext().getFixture()); + self->modifyExt().clear(lock); + Py_RETURN_NONE; + } + + PyObject *PyAPI_WeakSetObject_clear(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs) + { + PY_API_FUNC + return runSafe(tryWeakSetObject_clear, self, args, nargs); + } + + WeakSetObject *tryWeakSetObject_copyInternal(WeakSetObject *src) + { + db0::FixtureLock lock(src->ext().getFixture()); + auto py_set = WeakSetDefaultObject_new(); + auto &set = py_set->makeNew(*lock); + set.insert(src->ext()); + lock->getLangCache().add(set.getAddress(), py_set.get()); + return py_set.steal(); + } + + PyObject *PyAPI_WeakSetObject_copy(WeakSetObject *src) + { + PY_API_FUNC + return runSafe(tryWeakSetObject_copyInternal, src); + } + + PyObject *tryWeakSetObject_str(WeakSetObject *self) + { + std::stringstream str; + str << "weak_set({"; + auto iterator = Py_OWN(PyObject_GetIter(reinterpret_cast(self))); + if (!iterator) { + return nullptr; + } + bool first = true; + ObjectSharedPtr elem; + Py_FOR(elem, iterator) { + if (!first) { + str << ", "; + } else { + first = false; + } + auto str_value = Py_OWN(PyObject_Repr(*elem)); + if (!str_value) { + return nullptr; + } + str << PyUnicode_AsUTF8(*str_value); + } + str << "})"; + return PyUnicode_FromString(str.str().c_str()); + } + + PyObject *PyAPI_WeakSetObject_str(WeakSetObject *self) + { + PY_API_FUNC + return runSafe(tryWeakSetObject_str, self); + } + + static PySequenceMethods WeakSetObject_sq = { + .sq_length = (lenfunc)PyAPI_WeakSetObject_len, + .sq_contains = (objobjproc)PyAPI_WeakSetObject_HasItem + }; + + static PyMethodDef WeakSetObject_methods[] = + { + {"add", (PyCFunction)PyAPI_WeakSetObject_add, METH_FASTCALL, "Add an item to the weak set."}, + {"remove", (PyCFunction)PyAPI_WeakSetObject_remove, METH_FASTCALL, "Remove an item from the weak set; raises KeyError if not present."}, + {"discard", (PyCFunction)PyAPI_WeakSetObject_discard, METH_FASTCALL, "Discard an item from the weak set."}, + {"clear", (PyCFunction)PyAPI_WeakSetObject_clear, METH_FASTCALL, "Clear all items from the weak set."}, + {"copy", (PyCFunction)PyAPI_WeakSetObject_copy, METH_NOARGS, "Returns copy of weak set."}, + {NULL} + }; + + PyTypeObject WeakSetObjectType = { + PYVAROBJECT_HEAD_INIT_DESIGNATED, + .tp_name = "WeakSet", + .tp_basicsize = WeakSetObject::sizeOf(), + .tp_itemsize = 0, + .tp_dealloc = (destructor)WeakSetObject_del, + .tp_repr = (reprfunc)PyAPI_WeakSetObject_str, + .tp_as_sequence = &WeakSetObject_sq, + .tp_str = (reprfunc)PyAPI_WeakSetObject_str, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "dbzero weak-set collection object", + .tp_iter = (getiterfunc)PyAPI_WeakSetObject_iter, + .tp_methods = WeakSetObject_methods, + .tp_alloc = PyType_GenericAlloc, + .tp_new = (newfunc)WeakSetObject_new, + .tp_free = PyObject_Free, + }; + + WeakSetObject *WeakSetObject_new(PyTypeObject *type, PyObject *, PyObject *) { + return reinterpret_cast(type->tp_alloc(type, 0)); + } + + shared_py_object WeakSetDefaultObject_new() { + return { WeakSetObject_new(&WeakSetObjectType, NULL, NULL), false }; + } + + void WeakSetObject_del(WeakSetObject *self) + { + PY_API_FUNC + self->destroy(); + Py_TYPE(self)->tp_free((PyObject*)self); + } + + shared_py_object tryMake_DB0WeakSet( + db0::swine_ptr &fixture, PyObject *const *args, Py_ssize_t nargs, AccessFlags access_mode) + { + auto py_set = WeakSetDefaultObject_new(); + if (!py_set) { + PyErr_SetString(PyExc_RuntimeError, "Failed to create new weak set"); + return nullptr; + } + + db0::FixtureLock lock(fixture); + auto &set = py_set->makeNew(*lock); + if (nargs == 1) { + auto iterator = Py_OWN(PyObject_GetIter(args[0])); + if (!iterator) { + return nullptr; + } + ObjectSharedPtr item; + Py_FOR(item, iterator) { + auto hash = getPyHash(fixture, *item); + set.append(lock, hash, item); + } + } + + fixture->getLangCache().add(set.getAddress(), *py_set); + return py_set; + } + + WeakSetObject *tryMake_WeakSet(PyObject *, PyObject *const *args, Py_ssize_t nargs) + { + auto fixture = PyToolkit::getPyWorkspace().getWorkspace().getCurrentFixture(); + return tryMake_DB0WeakSet(fixture, args, nargs, {}).steal(); + } + + WeakSetObject *PyAPI_makeWeakSet(PyObject *obj, PyObject *const *args, Py_ssize_t nargs) + { + PY_API_FUNC + return runSafe(tryMake_WeakSet, obj, args, nargs); + } + + bool WeakSetObject_Check(PyObject *object) { + return Py_TYPE(object) == &WeakSetObjectType; + } + +} diff --git a/src/dbzero/bindings/python/collections/PyWeakSet.hpp b/src/dbzero/bindings/python/collections/PyWeakSet.hpp new file mode 100644 index 00000000..b65e818b --- /dev/null +++ b/src/dbzero/bindings/python/collections/PyWeakSet.hpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#pragma once + +#include +#include + +namespace db0::python + +{ + + using WeakSetObject = PyWrapper; + using AccessFlags = db0::AccessFlags; + + WeakSetObject *WeakSetObject_new(PyTypeObject *type, PyObject *, PyObject *); + shared_py_object WeakSetDefaultObject_new(); + void WeakSetObject_del(WeakSetObject *self); + + Py_ssize_t PyAPI_WeakSetObject_len(WeakSetObject *); + PyObject *PyAPI_WeakSetObject_add(WeakSetObject *, PyObject *const *args, Py_ssize_t nargs); + PyObject *PyAPI_WeakSetObject_remove(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs); + PyObject *PyAPI_WeakSetObject_discard(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs); + PyObject *PyAPI_WeakSetObject_copy(WeakSetObject *self); + PyObject *PyAPI_WeakSetObject_clear(WeakSetObject *self, PyObject *const *args, Py_ssize_t nargs); + int PyAPI_WeakSetObject_HasItem(WeakSetObject *self, PyObject *key); + + extern PyTypeObject WeakSetObjectType; + extern PyTypeObject WeakSetIteratorObjectType; + + shared_py_object tryMake_DB0WeakSet(db0::swine_ptr &, PyObject *const *args, + Py_ssize_t nargs, AccessFlags); + WeakSetObject *PyAPI_makeWeakSet(PyObject *, PyObject *const *args, Py_ssize_t nargs); + + bool WeakSetObject_Check(PyObject *); + +} diff --git a/src/dbzero/bindings/python/dbzero.cpp b/src/dbzero/bindings/python/dbzero.cpp index f59ef586..3606e68d 100644 --- a/src/dbzero/bindings/python/dbzero.cpp +++ b/src/dbzero/bindings/python/dbzero.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,7 @@ static PyMethodDef dbzero_methods[] = {"index", (PyCFunction)&py::PyAPI_makeIndex, METH_FASTCALL, "Create a new dbzero index instance"}, {"tuple", (PyCFunction)&py::PyAPI_makeTuple, METH_FASTCALL, "Create a new dbzero tuple instance"}, {"set", (PyCFunction)&py::PyAPI_makeSet, METH_FASTCALL, "Create a new dbzero set instance"}, + {"weak_set", (PyCFunction)&py::PyAPI_makeWeakSet, METH_FASTCALL, "Create a new dbzero weak set instance"}, {"dict", (PyCFunction)&py::PyAPI_makeDict, METH_VARARGS | METH_KEYWORDS, "Create a new dbzero dict instance"}, {"bytearray", (PyCFunction)&py::PyAPI_makeByteArray, METH_FASTCALL, "Create a new dbzero bytearray instance"}, {"tags", (PyCFunction)&py::makeObjectTagManager, METH_FASTCALL, ""}, @@ -186,8 +188,10 @@ PyMODINIT_FUNC PyInit_dbzero(void) &py::ListObjectType, &py::ListIteratorObjectType, &py::IndexObjectType, - &py::SetObjectType, + &py::SetObjectType, &py::SetIteratorObjectType, + &py::WeakSetObjectType, + &py::WeakSetIteratorObjectType, &py::TupleObjectType, &py::TupleIteratorObjectType, &py::DictObjectType, diff --git a/src/dbzero/object_model/ObjectModel.cpp b/src/dbzero/object_model/ObjectModel.cpp index ae4d9c45..4d9401b6 100644 --- a/src/dbzero/object_model/ObjectModel.cpp +++ b/src/dbzero/object_model/ObjectModel.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +38,7 @@ namespace db0::object_model return [](db0::swine_ptr &fixture, bool is_new, bool read_only, bool is_snapshot) { // static GC0 bindings initialization - GC0::registerTypes(); + GC0::registerTypes(); auto &oc = fixture->getObjectCatalogue(); if (is_new) { assert(!is_snapshot); diff --git a/src/dbzero/object_model/dict/Dict.cpp b/src/dbzero/object_model/dict/Dict.cpp index 07fcf594..6f6e2fff 100644 --- a/src/dbzero/object_model/dict/Dict.cpp +++ b/src/dbzero/object_model/dict/Dict.cpp @@ -11,6 +11,18 @@ #include #include +// Dict stores m_index: a top-level bindex keyed by key hash, whose bucket +// value is a TypedIndexAddr { m_index_address, m_type } pointing at an inner +// MorphingBIndex (DictIndex) that holds the (key, value) pairs sharing that +// hash. +// +// MorphingBIndex may morph on insert/erase — changing its address and/or type +// (see AGENTS.md). Every path here that calls bindex.insert() or bindex.erase() +// on a bucket retrieved from m_index must re-sync the parent entry afterwards, +// otherwise later lookups go through a stale {address, type} and read +// pre-mutation storage. The size==1 erase branch is exempt because the whole +// bucket is removed from m_index and the bindex is destroyed. + namespace db0::object_model { diff --git a/src/dbzero/object_model/set/Set.cpp b/src/dbzero/object_model/set/Set.cpp index a677eccc..51223a98 100644 --- a/src/dbzero/object_model/set/Set.cpp +++ b/src/dbzero/object_model/set/Set.cpp @@ -11,6 +11,17 @@ #include #include +// Set stores m_index: a top-level bindex keyed by element hash, whose bucket +// value is a TypedIndexAddr { m_index_address, m_type } pointing at an inner +// MorphingBIndex (SetIndex) that holds the elements sharing that hash. +// +// MorphingBIndex may morph on insert/erase — changing its address and/or type +// (see AGENTS.md). Every path here that calls bindex.insert() or bindex.erase() +// on a bucket retrieved from m_index must re-sync the parent entry afterwards, +// otherwise later lookups go through a stale {address, type} and read +// pre-mutation storage. The size==1 erase branch is exempt because the whole +// bucket is removed from m_index and the bindex is destroyed. + namespace db0::object_model { @@ -162,10 +173,18 @@ namespace db0::object_model if (LangToolkit::compare(key_value, member.get())) { if (bindex.size() == 1) { m_index.erase(iter); - unrefMember(fixture, storage_class, value); + unrefMember(fixture, storage_class, value); bindex.destroy(); } else { bindex.erase(*it); + // Erasing may have morphed the bindex to a smaller container, + // changing its address/type. Re-point the m_index entry so the + // bucket tracks the new storage — otherwise later lookups read + // through the stale {address, type} and see pre-erase data. + if (bindex.getAddress() != address.m_index_address) { + m_index.erase(iter); + m_index.insert({it_key, bindex}); + } } --modify().m_size; restoreIterators(); @@ -223,6 +242,12 @@ namespace db0::object_model bindex.destroy(); } else { bindex.erase(*it); + // Erasing may have morphed the bindex — re-point the m_index entry + // so later lookups see the new storage. + if (bindex.getAddress() != address.m_index_address) { + m_index.erase(iter); + m_index.insert({key, bindex}); + } } --modify().m_size; restoreIterators(); diff --git a/src/dbzero/object_model/set/WeakSet.cpp b/src/dbzero/object_model/set/WeakSet.cpp new file mode 100644 index 00000000..55028815 --- /dev/null +++ b/src/dbzero/object_model/set/WeakSet.cpp @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include "WeakSet.hpp" +#include "WeakSetIterator.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// WeakSet stores m_index: a top-level bindex keyed by element hash, whose +// bucket value is a TypedIndexAddr { m_index_address, m_type } pointing at +// an inner MorphingBIndex (SetIndex) that holds the (weak-referenced) +// elements sharing that hash. +// +// MorphingBIndex may morph on insert/erase — changing its address and/or type +// (see AGENTS.md). Every path here that calls bindex.insert() or bindex.erase() +// on a bucket retrieved from m_index must re-sync the parent entry afterwards, +// otherwise later lookups go through a stale {address, type} and read +// pre-mutation storage. The size==1 erase branch is exempt because the whole +// bucket is removed from m_index and the bindex is destroyed. + +namespace db0::object_model + +{ + + namespace py = db0::python; + GC0_Define(WeakSet) + + using TypeId = db0::bindings::TypeId; + + static bool isMemoLike(TypeId type_id) + { + return type_id == TypeId::MEMO_OBJECT + || type_id == TypeId::MEMO_IMMUTABLE_OBJECT + || type_id == TypeId::DB0_WEAK_PROXY + || type_id == TypeId::MEMO_EXPIRED_REF; + } + + // wrap a raw memo object as weak proxy and create a typed item with it + static o_typed_item createWeakTypedItem(db0::swine_ptr &fixture, + PyObject *lang_value, StorageClass storage_class, AccessFlags access_mode) + { + // wrap memo object as weak proxy (or pass-through if already a proxy) + auto wrapped = db0::python::PyTypes::ObjectSharedPtr( + db0::python::PyWeakProxy_Check(lang_value) ? (Py_INCREF(lang_value), lang_value) : db0::python::tryWeakProxy(lang_value), + false); + if (!wrapped) { + THROWF(db0::InputException) << "Failed to construct weak proxy" << THROWF_END; + } + auto value = createMember( + fixture, wrapped.get(), storage_class, access_mode); + return { storage_class, value }; + } + + static set_item createWeakSetItem(db0::swine_ptr &fixture, std::uint64_t key, + PyObject *lang_value, StorageClass storage_class, AccessFlags access_mode) + { + auto item = createWeakTypedItem(fixture, lang_value, storage_class, access_mode); + SetIndex bindex(*fixture, item); + return { key, bindex }; + } + + WeakSet::WeakSet(db0::swine_ptr &fixture, AccessFlags access_mode) + : super_t(fixture, access_mode) + , m_index(*fixture) + { + modify().m_index_ptr = m_index.getAddress(); + } + + WeakSet::WeakSet(tag_no_gc, db0::swine_ptr &fixture, const WeakSet &set) + : super_t(tag_no_gc(), fixture) + , m_index(*fixture) + { + modify().m_index_ptr = m_index.getAddress(); + std::uint64_t count = 0; + for(auto [hash, address] : set) { + auto bindex = address.getIndex(this->getMemspace()); + auto bindex_copy = SetIndex(bindex); + m_index.insert(set_item(hash, bindex_copy)); + ++count; + } + modify().m_size = count; + } + + WeakSet::WeakSet(db0::swine_ptr &fixture, Address address, AccessFlags access_mode) + : super_t(super_t::tag_from_address(), fixture, address, access_mode) + , m_index(myPtr((*this)->m_index_ptr)) + { + } + + WeakSet::~WeakSet() + { + unregister(); + } + + void WeakSet::operator=(WeakSet &&other) + { + unrefMembers(); + super_t::operator=(std::move(other)); + m_index = std::move(other.m_index); + assert(!other.hasInstance()); + restoreIterators(); + } + + void WeakSet::insert(const WeakSet &other) + { + for (auto [key, address] : other) { + auto fixture = this->getFixture(); + auto bindex = address.getIndex(*fixture); + auto it = bindex.beginJoin(1); + while (!it.is_end()) { + auto [storage_class, value] = (*it); + auto member = unloadMember(fixture, storage_class, value, 0, getMemberFlags()); + // skip expired references when copying + if (!db0::python::MemoExpiredRef_Check(member.get())) { + append(fixture, key, member.get()); + } + ++it; + } + } + restoreIterators(); + } + + void WeakSet::append(db0::FixtureLock &lock, std::size_t key, ObjectSharedPtr lang_value) + { + append(*lock, key, *lang_value); + restoreIterators(); + } + + void WeakSet::append(db0::swine_ptr &fixture, std::size_t key, ObjectPtr lang_value) + { + auto type_id = LangToolkit::getTypeManager().getTypeId(lang_value); + if (!isMemoLike(type_id)) { + THROWF(db0::InputException) << "weak_set only accepts memo object instances" << THROWF_END; + } + + // resolve short vs long weak ref based on target's prefix + const auto &target_obj = LangToolkit::getTypeManager().extractAnyObject(lang_value); + auto storage_class = (*target_obj.getFixture() != *fixture.get()) + ? StorageClass::OBJECT_LONG_WEAK_REF + : StorageClass::OBJECT_WEAK_REF; + + auto iter = m_index.find(key); + bool is_modified = false; + if (iter == m_index.end()) { + auto set_it = createWeakSetItem(fixture, key, lang_value, storage_class, getMemberFlags()); + m_index.insert(set_it); + ++modify().m_size; + is_modified = true; + } else { + // check if it's already there (by actual instance match) + if (!hasItem(static_cast(key), lang_value)) { + auto [it_key, address] = *iter; + auto bindex = address.getIndex(*fixture); + auto item = createWeakTypedItem(fixture, lang_value, storage_class, getMemberFlags()); + bindex.insert(item); + if (bindex.getAddress() != address.m_index_address) { + m_index.erase(iter); + m_index.insert({key, bindex}); + } + ++modify().m_size; + is_modified = true; + } + } + + if (is_modified) { + restoreIterators(); + } + } + + bool WeakSet::remove(FixtureLock &, std::size_t key, ObjectPtr key_value) + { + auto iter = m_index.find(key); + if (iter == m_index.end()) { + return false; + } + auto [it_key, address] = *iter; + auto bindex = address.getIndex(this->getMemspace()); + + auto it = bindex.beginJoin(1); + auto fixture = this->getFixture(); + while (!it.is_end()) { + auto [storage_class, value] = *it; + auto member = unloadMember(fixture, storage_class, value, 0, getMemberFlags()); + if (!db0::python::MemoExpiredRef_Check(member.get()) + && LangToolkit::compare(key_value, member.get())) { + if (bindex.size() == 1) { + m_index.erase(iter); + bindex.destroy(); + } else { + bindex.erase(*it); + // Erasing may have morphed the bindex to a smaller container, + // changing its address/type. Re-point the m_index entry so the + // bucket tracks the new storage — otherwise later lookups read + // through the stale {address, type} and see pre-erase data. + if (bindex.getAddress() != address.m_index_address) { + m_index.erase(iter); + m_index.insert({it_key, bindex}); + } + } + --modify().m_size; + restoreIterators(); + return true; + } + ++it; + } + return false; + } + + void WeakSet::destroy() + { + unrefMembers(); + m_index.destroy(); + super_t::destroy(); + } + + bool WeakSet::hasItem(std::int64_t hash_key, ObjectPtr key_value) const + { + auto iter = m_index.find(hash_key); + if (iter == m_index.end()) { + return false; + } + + auto [key, address] = *iter; + auto fixture = this->getFixture(); + auto bindex = address.getIndex(*fixture); + auto it = bindex.beginJoin(1); + while (!it.is_end()) { + auto [storage_class, value] = *it; + auto member = unloadMember(fixture, storage_class, value, 0, getMemberFlags()); + if (!db0::python::MemoExpiredRef_Check(member.get()) + && LangToolkit::compare(key_value, member.get())) { + return true; + } + ++it; + } + return false; + } + + void WeakSet::moveTo(db0::swine_ptr &fixture) + { + assert(hasInstance()); + if ((*this)->m_size > 0) { + THROWF(db0::InputException) << "WeakSet with items cannot be moved to another fixture"; + } + super_t::moveTo(fixture); + } + + std::size_t WeakSet::size() const + { + // full scan - skip expired entries + auto fixture = this->getFixture(); + std::size_t count = 0; + for (auto [_, address] : m_index) { + auto bindex = address.getIndex(this->getMemspace()); + auto it = bindex.beginJoin(1); + while (!it.is_end()) { + auto [storage_class, value] = *it; + auto member = unloadMember(fixture, storage_class, value, 0, getMemberFlags()); + if (!db0::python::MemoExpiredRef_Check(member.get())) { + ++count; + } + ++it; + } + } + return count; + } + + void WeakSet::clear(FixtureLock &) + { + unrefMembers(); + m_index.clear(); + modify().m_size = 0; + restoreIterators(); + } + + WeakSet::const_iterator WeakSet::begin() const { + return m_index.begin(); + } + + WeakSet::const_iterator WeakSet::end() const { + return m_index.end(); + } + + void WeakSet::commit() const + { + m_index.commit(); + super_t::commit(); + } + + void WeakSet::detach() const + { + commit(); + m_index.detach(); + m_iterators.forEach([](WeakSetIterator &iter) { + iter.detach(); + }); + super_t::detach(); + } + + void WeakSet::unrefMembers() const + { + // Weak references do not increment ref-counts, so no unref needed for items. + // For OBJECT_LONG_WEAK_REF, the LongWeakRef storage will be cleaned up when + // the WeakSet's underlying memory is freed (m_index.destroy()). + } + + std::shared_ptr WeakSet::getIterator(ObjectPtr lang_set) const + { + auto iter = std::shared_ptr(new WeakSetIterator(m_index.begin(), this, lang_set)); + m_iterators.push_back(iter); + return iter; + } + + void WeakSet::restoreIterators() + { + if (m_iterators.cleanup()) { + return; + } + m_iterators.forEach([](WeakSetIterator &iter) { + iter.restore(); + }); + } + + WeakSet::const_iterator WeakSet::find(std::uint64_t key_hash) const { + return m_index.find(key_hash); + } + +} diff --git a/src/dbzero/object_model/set/WeakSet.hpp b/src/dbzero/object_model/set/WeakSet.hpp new file mode 100644 index 00000000..98d1bc32 --- /dev/null +++ b/src/dbzero/object_model/set/WeakSet.hpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Set.hpp" +#include + +namespace db0 { + + class Fixture; + +} + +namespace db0::object_model + +{ + + class WeakSetIterator; + +DB0_PACKED_BEGIN + struct DB0_PACKED_ATTR o_weak_set: public db0::o_fixed_versioned + { + o_unique_header m_header; + Address m_index_ptr = {}; + std::uint64_t m_size = 0; + std::array m_reserved = {0, 0}; + + bool hasRefs() const { + return m_header.hasRefs(); + } + }; +DB0_PACKED_END + + class WeakSet: public db0::ObjectBase, StorageClass::DB0_WEAK_SET> + { + GC0_Declare + public: + using super_t = db0::ObjectBase, StorageClass::DB0_WEAK_SET>; + friend class db0::ObjectBase, StorageClass::DB0_WEAK_SET>; + using LangToolkit = db0::python::PyToolkit; + using ObjectPtr = typename LangToolkit::ObjectPtr; + using ObjectSharedPtr = typename LangToolkit::ObjectSharedPtr; + using const_iterator = typename db0::v_bindex::const_iterator; + + WeakSet() = default; + explicit WeakSet(db0::swine_ptr &, AccessFlags = {}); + explicit WeakSet(tag_no_gc, db0::swine_ptr &, const WeakSet &); + explicit WeakSet(db0::swine_ptr &, Address, AccessFlags = {}); + ~WeakSet(); + + void operator=(WeakSet &&); + + // Insert a memo object as a weak reference. Hash is the target's hash. + void append(FixtureLock &, std::size_t key, ObjectSharedPtr lang_value); + bool remove(FixtureLock &, std::size_t key, ObjectPtr key_value); + // contains by actual instance + bool hasItem(std::int64_t hash, ObjectPtr lang_key) const; + + void clear(FixtureLock &); + void insert(const WeakSet &); + void moveTo(db0::swine_ptr &); + + // Reports only non-expired references (full scan). + std::size_t size() const; + + void commit() const; + void detach() const; + + void destroy(); + + const_iterator begin() const; + const_iterator end() const; + + void unrefMembers() const; + + std::shared_ptr getIterator(ObjectPtr lang_set) const; + + protected: + friend class WeakSetIterator; + const_iterator find(std::uint64_t key_hash) const; + + private: + db0::v_bindex m_index; + mutable db0::weak_vector m_iterators; + + void append(db0::swine_ptr &, std::size_t key, ObjectPtr lang_value); + + void restoreIterators(); + }; + +} diff --git a/src/dbzero/object_model/set/WeakSetIterator.cpp b/src/dbzero/object_model/set/WeakSetIterator.cpp new file mode 100644 index 00000000..5712cb66 --- /dev/null +++ b/src/dbzero/object_model/set/WeakSetIterator.cpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#include "WeakSetIterator.hpp" +#include "WeakSet.hpp" +#include +#include + +namespace db0::object_model + +{ + + WeakSetIterator::WeakSetIterator(WeakSet::const_iterator iterator, const WeakSet *ptr, ObjectPtr lang_set_ptr) + : BaseIterator(iterator, ptr, lang_set_ptr) + { + setJoinIterator(); + prefetchNext(); + } + + void WeakSetIterator::setJoinIterator() + { + assureAttached(); + if (m_iterator != m_collection->end()) { + auto [key, address] = *m_iterator; + m_current_hash = key; + m_index = address.getIndex(m_collection->getMemspace()); + m_join_iterator = m_index.beginJoin(1); + assert(!m_join_iterator.is_end()); + m_current_key = *m_join_iterator; + } else { + m_inner_end = true; + } + } + + void WeakSetIterator::prefetchNext() + { + m_pending.reset(); + auto fixture = m_collection->getFixture(); + while (!m_inner_end && m_iterator != m_collection->end()) { + auto item = *m_join_iterator; + auto [storage_class, value] = item; + auto member = unloadMember(fixture, storage_class, value, 0, m_member_flags); + // step to next underlying entry + ++m_join_iterator; + if (m_join_iterator.is_end()) { + ++m_iterator; + setJoinIterator(); + } else { + m_current_key = *m_join_iterator; + } + if (!db0::python::MemoExpiredRef_Check(member.get())) { + m_pending = member; + return; + } + } + } + + bool WeakSetIterator::is_end() const + { + return !m_pending; + } + + WeakSetIterator::ObjectSharedPtr WeakSetIterator::next() + { + assureAttached(); + auto result = m_pending; + prefetchNext(); + return result; + } + + void WeakSetIterator::restore() + { + if (m_inner_end) { + m_iterator = m_collection->end(); + return; + } + m_iterator = m_collection->find(m_current_hash); + if (m_iterator == m_collection->end()) { + m_inner_end = true; + m_pending.reset(); + return; + } + + auto [key, address] = *m_iterator; + m_current_hash = key; + m_index = address.getIndex(m_collection->getMemspace()); + m_join_iterator = m_index.beginJoin(1); + if (m_join_iterator.join(m_current_key)) { + m_current_key = *m_join_iterator; + } else { + ++m_iterator; + setJoinIterator(); + } + } + +} diff --git a/src/dbzero/object_model/set/WeakSetIterator.hpp b/src/dbzero/object_model/set/WeakSetIterator.hpp new file mode 100644 index 00000000..c0dcc649 --- /dev/null +++ b/src/dbzero/object_model/set/WeakSetIterator.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (c) 2025 DBZero Software sp. z o.o. + +#pragma once + +#include +#include "WeakSet.hpp" +#include +#include +#include + +namespace db0::object_model + +{ + + class WeakSetIterator : public BaseIterator + { + public: + ObjectSharedPtr next() override; + // hides BaseIterator::is_end - we report end when no live element is pending + bool is_end() const; + + protected: + friend class WeakSet; + WeakSetIterator(WeakSet::const_iterator iterator, const WeakSet *ptr, ObjectPtr lang_set_ptr); + + void restore() override; + + private: + SetIndex m_index; + SetIndex::joinable_const_iterator m_join_iterator; + std::uint64_t m_current_hash = 0; + o_typed_item m_current_key; + bool m_inner_end = false; + // pre-fetched next live element (nullptr if iterator is exhausted) + ObjectSharedPtr m_pending; + + void setJoinIterator(); + // Advance underlying iterator and store next non-expired item in m_pending. + void prefetchNext(); + }; + +} diff --git a/src/dbzero/object_model/value/Member.cpp b/src/dbzero/object_model/value/Member.cpp index 96f22d3e..397bdbb3 100644 --- a/src/dbzero/object_model/value/Member.cpp +++ b/src/dbzero/object_model/value/Member.cpp @@ -120,6 +120,14 @@ namespace db0::object_model return resolveForFixture(fixture, set, obj_ptr, storage_class, access_flags); } + // WEAK_SET specialization + template <> Value createMember(db0::swine_ptr &fixture, + PyObjectPtr obj_ptr, StorageClass storage_class, AccessFlags access_flags) + { + auto &weak_set = PyToolkit::getTypeManager().extractMutableWeakSet(obj_ptr); + return resolveForFixture(fixture, weak_set, obj_ptr, storage_class, access_flags); + } + // DB0 DICT specialization template <> Value createMember(db0::swine_ptr &fixture, PyObjectPtr obj_ptr, StorageClass storage_class, AccessFlags access_flags) @@ -361,6 +369,7 @@ namespace db0::object_model functions[static_cast(TypeId::DB0_LIST)] = createMember; functions[static_cast(TypeId::DB0_INDEX)] = createMember; functions[static_cast(TypeId::DB0_SET)] = createMember; + functions[static_cast(TypeId::DB0_WEAK_SET)] = createMember; functions[static_cast(TypeId::DB0_DICT)] = createMember; functions[static_cast(TypeId::DB0_TUPLE)] = createMember; functions[static_cast(TypeId::LIST)] = createMember; @@ -439,6 +448,13 @@ namespace db0::object_model { return PyToolkit::unloadSet(fixture, value.asAddress(), 0, access_mode); } + + // DB0_WEAK_SET specialization + template <> typename PyToolkit::ObjectSharedPtr unloadMember( + db0::swine_ptr &fixture, Value value, unsigned int, AccessFlags access_mode) + { + return PyToolkit::unloadWeakSet(fixture, value.asAddress(), 0, access_mode); + } // DB0_DICT specialization template <> typename PyToolkit::ObjectSharedPtr unloadMember( @@ -655,6 +671,7 @@ namespace db0::object_model functions[static_cast(StorageClass::DB0_LIST)] = unloadMember; functions[static_cast(StorageClass::DB0_INDEX)] = unloadMember; functions[static_cast(StorageClass::DB0_SET)] = unloadMember; + functions[static_cast(StorageClass::DB0_WEAK_SET)] = unloadMember; functions[static_cast(StorageClass::DB0_DICT)] = unloadMember; functions[static_cast(StorageClass::DB0_TUPLE)] = unloadMember; functions[static_cast(StorageClass::DB0_BYTES)] = unloadMember; @@ -751,11 +768,17 @@ namespace db0::object_model } template <> void unrefMember( - db0::swine_ptr &fixture, Value value) + db0::swine_ptr &fixture, Value value) { unrefObjectBase(fixture, value.asAddress()); } + template <> void unrefMember( + db0::swine_ptr &fixture, Value value) + { + unrefObjectBase(fixture, value.asAddress()); + } + template <> void unrefMember( db0::swine_ptr &fixture, Value value) { @@ -803,6 +826,7 @@ namespace db0::object_model functions[static_cast(StorageClass::DB0_LIST)] = unrefMember; functions[static_cast(StorageClass::DB0_INDEX)] = unrefMember; functions[static_cast(StorageClass::DB0_SET)] = unrefMember; + functions[static_cast(StorageClass::DB0_WEAK_SET)] = unrefMember; functions[static_cast(StorageClass::DB0_DICT)] = unrefMember; functions[static_cast(StorageClass::DB0_TUPLE)] = unrefMember; functions[static_cast(StorageClass::DB0_BYTES_ARRAY)] = unrefMember; diff --git a/src/dbzero/object_model/value/Member.hpp b/src/dbzero/object_model/value/Member.hpp index f865c165..64376a12 100644 --- a/src/dbzero/object_model/value/Member.hpp +++ b/src/dbzero/object_model/value/Member.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include diff --git a/src/dbzero/object_model/value/StorageClass.cpp b/src/dbzero/object_model/value/StorageClass.cpp index 0aabc257..4c8e1932 100644 --- a/src/dbzero/object_model/value/StorageClass.cpp +++ b/src/dbzero/object_model/value/StorageClass.cpp @@ -39,6 +39,7 @@ namespace db0::object_model addMapping(TypeId::DB0_LIST, PreStorageClass::DB0_LIST); addMapping(TypeId::DB0_DICT, PreStorageClass::DB0_DICT); addMapping(TypeId::DB0_SET, PreStorageClass::DB0_SET); + addMapping(TypeId::DB0_WEAK_SET, PreStorageClass::DB0_WEAK_SET); addMapping(TypeId::DB0_TUPLE, PreStorageClass::DB0_TUPLE); addMapping(TypeId::DB0_INDEX, PreStorageClass::DB0_INDEX); addMapping(TypeId::OBJECT_ITERABLE, PreStorageClass::DB0_SERIALIZED); @@ -150,6 +151,7 @@ namespace std case StorageClass::DB0_LIST: return os << "DB0_LIST"; case StorageClass::DB0_DICT: return os << "DB0_DICT"; case StorageClass::DB0_SET: return os << "DB0_SET"; + case StorageClass::DB0_WEAK_SET: return os << "DB0_WEAK_SET"; case StorageClass::DB0_TUPLE: return os << "DB0_TUPLE"; case StorageClass::STR64: return os << "STR64"; case StorageClass::DB0_CLASS: return os << "DB0_CLASS"; diff --git a/src/dbzero/object_model/value/StorageClass.hpp b/src/dbzero/object_model/value/StorageClass.hpp index 05b3a3b6..920309fc 100644 --- a/src/dbzero/object_model/value/StorageClass.hpp +++ b/src/dbzero/object_model/value/StorageClass.hpp @@ -63,7 +63,8 @@ namespace db0::object_model // deleted value (placeholder) DELETED = 31, CALLABLE = 32, - + DB0_WEAK_SET = 33, + COUNT = std::numeric_limits::max() - 32, // invalid / reserved value, never used in objects INVALID = std::numeric_limits::max() @@ -114,6 +115,7 @@ namespace db0::object_model OBJECT_WEAK_REF = static_cast(PreStorageClass::OBJECT_WEAK_REF), DELETED = static_cast(PreStorageClass::DELETED), CALLABLE = static_cast(PreStorageClass::CALLABLE), + DB0_WEAK_SET = static_cast(PreStorageClass::DB0_WEAK_SET), // weak reference to other (Memo) instance from a foreign prefix OBJECT_LONG_WEAK_REF = static_cast(PreStorageClass::COUNT), // COUNT used to determine size of the StorageClass associated arrays @@ -159,10 +161,8 @@ namespace db0 using PreStorageClass = db0::object_model::PreStorageClass; using StorageClass = db0::object_model::StorageClass; - // This version is valid for all cases except for OBJECT_WEAK_REF - inline StorageClass getStorageClass(PreStorageClass pre_storage_class) + inline StorageClass getStorageClass(PreStorageClass pre_storage_class) { - assert(pre_storage_class != PreStorageClass::OBJECT_WEAK_REF); return static_cast(static_cast(pre_storage_class)); }