Dev Docs CNID Database System

CNID Database System

Audited against Netatalk d63155e13 on 2026-10-10. This describes the current source checkout; older releases may differ.

Role and supported backends

Catalog Node IDs identify files and directories independently of their current names. The server reconciles parent/name and device/inode information with persistent records; IDs should survive supported AFP renames and moves, but persistence is not guaranteed after database resets or arbitrary external replacement of files.

Backend Availability Access model Source
sqlite Always built; default Embedded connection in the AFP worker libatalk/cnid/sqlite/cnid_sqlite.c
mysql Optional Meson with-cnid-backends selection Worker connects to external MySQL/MariaDB libatalk/cnid/mysql/cnid_mysql.c

There is no dbd/Berkeley DB backend in this checkout. cnid_metad, cnid_dbd, their RPC protocol, and BDB environment parameters are obsolete for this source tree. The command named dbd remains as an offline maintenance utility.

Dispatch layer

include/atalk/cnid.h defines cnid_db, cnid_module, and cnid_open_args. libatalk/cnid/cnid_init.c registers SQLite unconditionally and MySQL under CNID_BACKEND_MYSQL. Modules are statically linked implementations with function-pointer dispatch, rather than dynamically loaded plugins.

libatalk/cnid/cnid.c validates inputs and dispatches to a backend. cnid_scheme_registered() checks a configured scheme without opening it.

API Operation
cnid_open() / cnid_close() Open/close a volume’s backend handle
cnid_add() Find or allocate an ID, optionally using a valid metadata hint
cnid_get() Lookup by parent DID and name
cnid_lookup() Reconcile stat identity with parent DID and name
cnid_resolve() Resolve an ID to its parent DID and name in the caller’s buffer
cnid_update() / cnid_delete() Change or remove a record
cnid_getstamp() Read the database stamp used to validate stored hints
cnid_find() / cnid_find_scoped() Name search, optionally limited to a directory subtree
cnid_wipe() Reset persistent records

cnid_find() and cnid_find_scoped() take bool *more_available in addition to the result buffer. The backend cnid_find callback also takes scope_did. Callers must distinguish truncation from a complete result set. The wrapper requires at least CNID_FIND_MIN_RESULTS (100) result slots.

CNIDs and parent DIDs passed through the AFP-facing API are in network byte order; SQL row IDs are host-order numeric values. CNID_INVALID is 0, CNID_START is 17, and directory IDs 1 and 2 have reserved root roles. Current capability flags are CNID_FLAG_PERSISTENT and CNID_FLAG_NODEV; older flags and cnid_rebuild_add/cnid_nextid callbacks are absent.

Error contract

CNID_ERR_* values occupy the unsigned high range stored in errno. Compare them through CNID_ERRNO(), not a signed literal comparison.

Condition Constant Caller implication
No matching record CNID_ERR_NOTFOUND May mean a genuinely absent object
Temporary contention CNID_ERR_BUSY Backend did not establish existence
Corrupt/unusable result CNID_ERR_CORRUPT Backend failure, not an absent object
Database unavailable CNID_ERR_DB Backend failure
Table reset CNID_ERR_RESET Existing IDs are stale; session must be ended
Invalid parameter/path or closed database CNID_ERR_PARAM, CNID_ERR_PATH, CNID_ERR_CLOSE Check arguments and lifecycle
Exhausted ID range CNID_ERR_MAX ID allocation failure

CNID_ERRNO_IS_BACKEND_FAILURE() groups busy, corrupt, and database failures. AFP callers use retryable AFPERR_MISC for these cases rather than permanent AFPERR_NOOBJ/AFPERR_NOID replies. See the handling in etc/afpd/directory.c, etc/afpd/file.c, and etc/afpd/catsearch.c.

SQLite storage and concurrency

cnid_sqlite_open() uses vol->v_dbpath, or the state directory’s CNID/<volume name> fallback, and creates <volume name>.sqlite inside it. The per-volume table is named by the volume UUID with dashes removed. A volumes table stores UUID, path, stamp, and depletion state. A missing UUID prevents opening the backend.

The CNID table has Id, Name, Did, DevNo, and InodeNo columns. Id is an autoincrement primary key, with uniqueness constraints on (Did, Name) and (DevNo, InodeNo). Sequence seeding reserves IDs below 17.

Current connection settings are WAL journaling, synchronous=NORMAL, and a 3,000 ms busy timeout. Each session has its own connection and prepared statements. WAL permits concurrent readers, while write-lock contention still requires handling; embedded does not mean one connection shared between processes.

Mutation paths use explicit transaction ownership and rollback so cleanup cannot accidentally roll back a caller’s transaction. Update operations reconcile conflicting name and inode keys before inserting the requested identity. Add retries are bounded (CNID_SQLITE_ADD_ATTEMPTS = 16); database failures must not become an endless lookup/insert loop.

SQLite rows, UUIDs, and metadata hints are validated. Hints near the top of the 32-bit range are excluded by a reserved range so user-writable AppleDouble metadata cannot trivially force depletion. On actual depletion, the backend resets records and reports CNID_ERR_RESET.

Database path handling checks file type, links, and ownership before changing modes. Normal multi-user service state and owner-only single-user state follow different permission rules; do not document one universal database mode. Details are in cnid_sqlite_fchmod_regular(), cnid_sqlite_fchmod_companions(), and cnid_sqlite_open().

MySQL

The optional MySQL backend uses per-volume UUID tables and a shared volume registry. Connection parameters come from [Global]:

[Global]
cnid mysql host = localhost
cnid mysql user = netatalk
cnid mysql pw = example-password
cnid mysql db = netatalk_cnid

[Shared]
path = /srv/share
cnid scheme = mysql

Inspect libatalk/cnid/mysql/cnid_mysql.c and include/atalk/cnid_mysql_private.h for connection timeouts, prepared statements, reconnection, and query behavior. A central CNID database alone does not provide distributed file locking or cross-host cache invalidation; backend availability is not a claim that a multi-server deployment is coherent.

Database resets and cache hints

Persistent records and in-memory directory caches are separate. Ordinary AFP mutations emit best-effort hints to sibling workers. A table reset invalidates the entire identity space, so cnid_volume_reset() sends CACHE_HINT_VOLUME_RESET and terminates the originating session.

cnid_volume_tag() identifies the volume using its UUID. The reset tag travels in the hint payload’s cnid field; receivers match open volumes by tag instead of their process-local volume ID. A matching receiver raises SIGTERM so its client reconnects and obtains current IDs. The relay has separate reset rate limits and bounded delivery handling. See libatalk/util/cnid.c, libatalk/util/server_ipc.c, and etc/afpd/dircache.c.

Maintenance utility

bin/dbd/cmd_dbd.c loads the configured volume and checks cnid_scheme_registered(). bin/dbd/cmd_dbd_scanvol.c scans filesystem entries and reconciles CNID/AppleDouble information.

Option Purpose
-F path Select configuration file
-s Treat the volume as read-only; suppress filesystem modifications
-f Wipe and recreate CNID records
-i Invalidate AppleDouble CNID hints
-c Convert legacy AppleDouble v2 metadata to EA format
-t, -v, -V Statistics, verbosity, version
-u username Resolve volumes using the user variable

The supplied path must be the configured volume root. Run maintenance with appropriate permissions and without active clients. -s is not a promise that opening SQLite and scanning have no database side effects. -f can change IDs and invalidate aliases or stored search references; it is a reset operation, not a routine first diagnostic step.

Verification sources

test/afpd/subtests_cnid.c covers backend errors, transactions, record validation, reset behavior, and search boundaries. test/testsuite/FPResolveID.c, test/testsuite/FPCatSearch.c, and test/testsuite/FPCatSearchExt.c exercise AFP-facing behavior. Build and execution instructions are in test/README.md.

Footnotes

Generated from the Netatalk GitHub Wiki

Last updated 2026-10-21