Skip to content

Latest commit

 

History

History
62 lines (45 loc) · 13 KB

AGENTS.md

File metadata and controls

62 lines (45 loc) · 13 KB

AGENTS.md

This file provides guidance to AI coding agents when working with code in this repository.

Commands

All make targets use (and create if needed, via python3 -m venv) the virtual environment in .venv from requirements.txt.

  • make depend — set up venv and download the ObjectBox C shared libraries (download-c-lib.py) into objectbox_clib/lib/<arch>/. Required once after checkout and whenever the C library version changes.
  • make depend-sync — download the Sync variant of the C libraries into objectbox_sync_clib/lib/<arch>/; if present, it is loaded instead (needed for the Sync tests).
  • make test — run all tests (python -m pytest --capture=no --verbose)
  • make build — clean and build the wheels into dist/: objectbox-clib (one per platform), objectbox-api and the objectbox metapackage; make build-sync adds the objectbox-sync-clib wheels and the objectbox-sync metapackage
  • make test-fast-flatbuffers-builder — all tests with the experimental fast FlatBuffers builder in verify mode (pytest --fast-flatbuffers-builder or OBX_TEST_FAST_FLATBUFFERS_BUILDER=1): every object put by any test is compared byte by byte with the reference
  • make test-flatbuffers-codegen — the same with the experimental FlatBuffers codegen (pytest --flatbuffers-codegen or OBX_TEST_FLATBUFFERS_CODEGEN=1)
  • make benchmark — CRUD benchmark (benchmark.py; BENCHMARK_ARGS=--fast-flatbuffers-builder or --flatbuffers-codegen for the experimental builders)
  • Single test: .venv/bin/python -m pytest tests/test_query.py::test_name -s -v (the venv gets pytest etc. from requirements.txt via any make target, e.g. make depend)

Run pytest from the repository root: tests import both common (via tests/conftest.py) and tests.model.

There is no linter configured; CONTRIBUTING.md asks for PEP-8 formatting.

Architecture

Python binding for the ObjectBox C API (objectbox-c) via ctypes; objects are serialized with FlatBuffers.

  • objectbox/c.py — all C bindings. Loads the library at import from the objectbox_sync_clib package if it has one for the platform, else from objectbox_clib (lib/<platform.machine()>/, or macos-universal; lib_package(); ImportError if neither has one) and asserts the core version equals required_version. required_version is the single source of the C library version (see Packaging). Functions are declared with helpers: c_fn (checks result/pointer), c_fn_rc (returns obx_err, raises via check_obx_err), c_fn_qb_cond, c_fn_nocheck.
  • objectbox/exceptions.py — DbError base class plus one subclass per DbErrorCode; create_db_error(code) maps C error codes to exception classes (message taken from obx_last_error_message()). Python code raises them with a message too, e.g. IllegalArgumentError("...") (also a ValueError) for invalid values and IllegalStateError("...") (also a RuntimeError) e.g. for closed objects; TypeError for wrong types.
  • Model definition (objectbox/model/):
    • @Entity() (entity.py) wraps a user class into an _Entity (handles FlatBuffers _marshal/_unmarshal; _marshal_builder is the generic flatbuffers.Builder path) and registers it in the global obx_models_by_name under a model name (default "default"). Property classes used as bare class members (e.g. name = String) are instantiated by the decorator. A property's name is the Python attribute name (used by all Python code); _db_name is its name in the database and model JSON: camelCase (camel_case(), e.g. dateCreated) unless @Entity(python_names=True).
    • properties.py — typed Property classes (scalars, vectors/lists, Date, Flex, Relation (to-one: an int ID with the target entity; created via obx_model_property_relation, relationTarget in the model JSON), HnswIndex for vector search; Index/Unique arguments are checked by Property._index_arg()/_unique_arg(), Property._check_args() rejects unknown arguments). Checks needing the whole model (relation targets, unique REPLACE rules) are in Model._validate(). Property operators/methods (==, starts_with, nearest_neighbors, …) produce PropertyQueryConditions.
    • fast_builder.py — experimental (objectbox/experimental.py, off by default): FastBuilder produces the same bytes as _marshal_builder with the layout precomputed per entity (by alignment residue); it mirrors the Builder's layout rules, so keep it byte-identical (tests/test_fast_flatbuffers_builder.py). A property set to None is left out of the object (null; the reference skips it), so objects with None values go through marshal_with_nulls() (layout per null combination); reading still returns defaults for missing fields.
    • codegen.py — experimental, the alternative to fast_builder.py (only one is active: experimental._flatbuffers_mode): CodegenBuilder generates and execs a marshal function per entity that does what FastBuilder.marshal does, unrolled; it uses a FastBuilder's layouts. Changes to FastBuilder.marshal must be mirrored in the generated code; the same tests check both. It also generates an unmarshal function, used by _Entity._unmarshal in codegen mode, that must return the same values as _unmarshal_table (flatbuffers.Table, the reference), so changes to reading go into both. Each function is compiled at its first use; with cache_dir (enable_flatbuffers_codegen), via a .py file in that directory (named by the source hash) and Python's bytecode cache.
    • idsync.py — assigns IDs/UIDs by syncing the model against an objectbox-model.json file (read, matched by UID then name, rewritten from scratch). This is how renames/removals are tracked across schema changes. Matching by name uses _db_name, falling back to the other form of the name (e.g. the Python name stored before 5.0), which renames the property and keeps its data. JSON entries not matched (removed) are retired (retired*Uids; also never generated or accepted as user UIDs); fields Python does not manage (standalone relations of other languages, lastRelationId, lastSequenceId, modelVersion, version) are copied from the loaded JSON; unassigned "last" IDs are written like the loaded file ("" for objectbox-generator files, else "0:0").
  • Store (store.py) — opening a store: resolve the model (a Model or a model name string → collect registered entities), locate objectbox-model.json (explicit model_json_file, else the calling module's directory found via the call stack, else __main__'s directory, else CWD), run sync_model (only once per Model and JSON file in a process: Model._synced_json_file; models for names are cached in _models_by_name), then build StoreOptions and call obx_store_open. Once closed, Store._c_store (a property; the C store is in _c_store_or_none) and Box._c_box raise IllegalStateError("Store is closed"). skip_model_sync=True opens an existing store without passing the model (schema from the database). Directory prefix memory: opens an in-memory DB.
  • Box (box.py) — CRUD per entity. box.query(condition) returns a QueryBuilder; conditions (condition.py, combinable with &/|) are applied onto the C query builder, then .build() yields a Query and closes the builder. Queries and builders free their C objects via close() (also __del__; a Query is a context manager); the store tracks the open ones (Store._native_resources, a WeakSet) and closes them before the C store; once closed, _c_query/_c_builder raise IllegalStateError. Query.set_parameter(s)() choose the C function by the property type (_parameter_functions in query.py); for set_parameter(s)_alias(), QueryBuilder records the entity and property of each condition alias (alias() uses the property of the last condition, which the condition methods record via _condition_property_id(); order() does not). QueryBuilder.link()/backlink() return a builder of the linked entity (_linked(): shares the C query builder's lifetime and the aliases; build(), order() and close() raise on it); parameters of linked conditions use the property's entity (prop._entity, set by @Entity()).
  • Sync (sync.py) — SyncClient (created only via Sync.client(); its constructor raises) wraps the C sync client (obx_sync_*; ctypes callbacks passed to C, also replaced ones, are kept referenced until the client is closed). The store keeps its open client alive (Store.sync_client(), like Java and Dart) and closes it (close listener) before the store is closed. @SyncEntity() sets the entity's Sync flags.
  • Deprecated API: ObjectBox and Builder are thin wrappers around Store kept for backward compatibility (tests/test_deprecated.py).
  • numpy and flatbuffers (which imports numpy) are imported lazily, only where needed (vector/flex properties, the reference FlatBuffers paths); no module-level imports of either (tests/test_lazy_imports.py). Use is_ndarray() (c.py) for type checks and prop._fb_format (struct format) instead of prop._fb_type outside the reference paths.
  • Public API is the flat export list in objectbox/__init__.py (the Sync names via a module __getattr__, importing objectbox.sync on first use: it takes a few ms); the binding version (version = Version(...)) is also defined there and used by setup.py and setup-meta.py (via build_info.py).

Packaging

Five pip packages are built from this repository:

  • objectbox-api (setup.py): the Python binding (import package objectbox); no C library dependency.
  • objectbox (setup-meta.py): metapackage without code, the one users install; depends on objectbox-api (exact binding version) and objectbox-clib (==<C version>.*). Its PyPI description is README.md.
  • objectbox-sync (setup-meta.py --sync): metapackage like objectbox, but depends on objectbox-sync-clib instead of objectbox-clib. Sync users install it instead of objectbox.
  • objectbox-clib (setup-clib.py, package objectbox_clib/): only the C libraries, no Python code. Built as one wheel per platform (wheel platform tags in PLATFORMS, so pip picks the platform's library). There is no fallback wheel: other platforms, e.g. Linux armv6l (no manylinux support), are not supported. Versioned like the C library.
  • objectbox-sync-clib (setup-clib.py --sync, package objectbox_sync_clib/): like objectbox-clib, but the Sync variant of the C libraries. objectbox/c.py (lib_package()) prefers its library over objectbox_clib's (e.g. if both metapackages are installed).

Upgrade caveat: up to 4.x, the distribution objectbox contained the code. An in-place pip install -U objectbox from such a version breaks the installation (pip removes the old objectbox files, now owned by objectbox-api, after installing it); README.md and CHANGELOG.md document uninstalling first. make publish uploads the C libraries first, then objectbox-api, then the metapackages.

build_info.py reads the binding version (objectbox/__init__.py) and the C library version (required_version in objectbox/c.py) as text for the build scripts and download-c-lib.py; it also holds the metadata shared by setup.py and setup-meta.py. Build scripts must not import objectbox: that loads the C library. To bump the C library, only change required_version, run make depend, and re-check the platform tags in setup-clib.py against the new libraries' minimum glibc/macOS versions.

Tests

  • tests/model.py defines test entities; tests/common.py has create_test_store() which deletes the DB dir (testdata by default) and tests/objectbox-model.json before opening a fresh store. The test_store fixture in conftest.py wraps it.
  • Sync tests (tests/test_sync.py, marker sync) run only if the loaded library supports Sync (make depend-sync). Tests needing a Sync server use OBX_SYNC_SERVER_URL (a running server with the model tests/objectbox-model-sync.json, e.g. via Docker with tests/sync_server_config.json), or download and start it in GitLab CI; otherwise they are skipped.
  • Tests write DB directories and model JSON files (e.g. testdata/, tests/test-model*.json) into the working tree; these are artifacts, not fixtures to commit.

CI

GitHub Actions (.github/workflows/test.yaml) runs make depend && make test && make test-fast-flatbuffers-builder && make test-flatbuffers-codegen on Linux/Windows/macOS for Python 3.7–3.14 (plus Windows ARM64 with Python 3.12), so code must stay compatible with Python 3.7; its job test-sync runs make depend-sync && make test with OBX_TEST_REQUIRE_SYNC=1 (fails if the Sync library is not loaded; Sync server tests skip) on the same OSes. GitLab CI (.gitlab-ci.yml) builds the wheels with make depend test build on Linux x64, then tests the built objectbox wheel (also with --fast-flatbuffers-builder and --flatbuffers-codegen; installed in place of the source packages, which are deleted; the platform's objectbox-clib wheel is picked via --find-links dist) on Linux x64 (Python 3.7–3.12 Docker images), Linux armv7hf and aarch64, macOS (arm64 runner; the macOS library is universal) and Windows x64.