Skip to content

graflo.architecture.schema.namespace

Physical graph namespace: the database, graph or space a schema deploys into.

Schema.metadata.name is a label. It is free-form on purpose: merges fold two names into left+right, agents write prose into it, and it is excluded from every content hash. It is not an identifier, and handing it verbatim to CREATE DATABASE / CREATE GRAPH / CREATE SPACE fails on the first character a backend does not accept.

The namespace a schema actually deploys into is resolved here, in one place, with one precedence:

  1. an explicit override (a call argument or connection config), validated;
  2. db_profile.target_namespace, validated -- an explicit value is refused when the flavor would reject it, never silently rewritten;
  3. :func:sanitize_namespace over metadata.name -- a deterministic, idempotent projection into the flavor's identifier rules.

The derived name is deliberately not stored back onto the profile. db_profile is part of the schema's content hash and metadata is not, so a stored mirror would make renaming a schema move its content address, and merging two schemas would have two auto-filled mirrors to reconcile.

Attributes

FALLBACK_NAMESPACE = 'graph' module-attribute

__all__ = ['FALLBACK_NAMESPACE', 'InvalidNamespaceError', 'namespace_problem', 'resolve_namespace', 'sanitize_namespace', 'validate_namespace'] module-attribute

logger = logging.getLogger(__name__) module-attribute

Classes

InvalidNamespaceError

Bases: ValueError

An explicit namespace the target flavor would reject.

Source code in graflo/architecture/schema/namespace.py
class InvalidNamespaceError(ValueError):
    """An explicit namespace the target flavor would reject."""

Functions:

namespace_problem(name, flavor)

Why flavor would reject name as a namespace, or None if it would not.

Source code in graflo/architecture/schema/namespace.py
def namespace_problem(name: str, flavor: DBType) -> str | None:
    """Why *flavor* would reject *name* as a namespace, or ``None`` if it would not."""
    flavor = DBType(flavor)
    if not name:
        return "namespace cannot be empty"
    limit = _MAX_LENGTH.get(flavor)
    if limit is not None and len(name) > limit:
        return f"longer than {limit} characters"
    if flavor == DBType.NEO4J:
        if len(name) < _NEO4J_MIN_LENGTH:
            return f"shorter than {_NEO4J_MIN_LENGTH} characters"
        if not _NEO4J_VALID.fullmatch(name):
            return "must start with a letter and hold only letters, digits, '.' and '-'"
        if name.lower().startswith("system"):
            return "must not start with 'system'"
        return None
    if flavor == DBType.ARANGO:
        if not _ARANGO_VALID.fullmatch(name):
            return "must start with a letter and hold only letters, digits, '_' and '-'"
        return None
    if flavor in (DBType.TIGERGRAPH, DBType.NEBULA):
        if not _WORD_VALID.fullmatch(name):
            return (
                "must start with a letter or '_' and hold only letters, digits and '_'"
            )
        if flavor == DBType.TIGERGRAPH:
            from graflo.db.tigergraph.name_validation import (
                validate_tigergraph_schema_name,
            )

            try:
                validate_tigergraph_schema_name(name, "graph")
            except ValueError as exc:
                return str(exc)
        return None
    if _PATH_UNSAFE.search(name):
        return "must not contain path separators or control characters"
    return None

resolve_namespace(schema, flavor=None, override=None)

The namespace schema deploys into on flavor.

Precedence: override, then db_profile.target_namespace (both validated), then the sanitized metadata.name.

Parameters:

Name Type Description Default
schema Schema

The schema being deployed.

required
flavor DBType | None

Target backend; defaults to schema.db_profile.db_flavor.

None
override str | None

An explicit namespace from the caller, e.g. a call argument.

None

Raises:

Type Description
InvalidNamespaceError

An explicit namespace flavor would reject.

Source code in graflo/architecture/schema/namespace.py
def resolve_namespace(
    schema: Schema,
    flavor: DBType | None = None,
    override: str | None = None,
) -> str:
    """The namespace *schema* deploys into on *flavor*.

    Precedence: *override*, then ``db_profile.target_namespace`` (both
    validated), then the sanitized ``metadata.name``.

    Args:
        schema: The schema being deployed.
        flavor: Target backend; defaults to ``schema.db_profile.db_flavor``.
        override: An explicit namespace from the caller, e.g. a call argument.

    Raises:
        InvalidNamespaceError: An explicit namespace *flavor* would reject.
    """
    target = DBType(flavor if flavor is not None else schema.db_profile.db_flavor)
    explicit = override if override is not None else schema.db_profile.target_namespace
    if explicit is not None:
        validate_namespace(explicit, target)
        return explicit
    label = schema.metadata.name
    resolved = sanitize_namespace(label, target)
    if resolved != label:
        logger.info(
            "Schema name %r is not a valid %s namespace; deploying into %r "
            "(set db_profile.target_namespace to choose one)",
            label,
            target.value,
            resolved,
        )
    return resolved

sanitize_namespace(name, flavor)

Project a free-form schema label onto a namespace flavor accepts.

Only what flavor would reject is rewritten: every run of disallowed characters becomes one separator, a leading character the flavor refuses gets a g_ prefix, and TigerGraph reserved words and forbidden prefixes are escaped. Neo4j names are additionally lowercased, - separated and padded to three characters. Over-long results keep a stable hash of the full name as a suffix, so two long labels sharing a prefix do not collide. Flavors without documented rules keep letters, digits, _ and -.

Deterministic and idempotent: sanitize_namespace(sanitize_namespace(x, f), f) equals sanitize_namespace(x, f).

Parameters:

Name Type Description Default
name str

The schema label, typically Schema.metadata.name.

required
flavor DBType

Target backend.

required

Returns:

Type Description
str

A namespace that passes :func:validate_namespace for flavor.

Source code in graflo/architecture/schema/namespace.py
def sanitize_namespace(name: str, flavor: DBType) -> str:
    """Project a free-form schema label onto a namespace *flavor* accepts.

    Only what *flavor* would reject is rewritten: every run of disallowed
    characters becomes one separator, a leading character the flavor refuses
    gets a ``g_`` prefix, and TigerGraph reserved words and forbidden prefixes
    are escaped. Neo4j names are additionally lowercased, ``-`` separated and
    padded to three characters. Over-long results keep a stable hash of the
    full name as a suffix, so two long labels sharing a prefix do not collide.
    Flavors without documented rules keep letters, digits, ``_`` and ``-``.

    Deterministic and idempotent: ``sanitize_namespace(sanitize_namespace(x, f), f)``
    equals ``sanitize_namespace(x, f)``.

    Args:
        name: The schema label, typically ``Schema.metadata.name``.
        flavor: Target backend.

    Returns:
        A namespace that passes :func:`validate_namespace` for *flavor*.
    """
    flavor = DBType(flavor)
    ascii_name = _ascii(name)
    if flavor == DBType.NEO4J:
        return _sanitize_neo4j(ascii_name)
    return _sanitize_word(ascii_name, flavor)

validate_namespace(name, flavor)

Refuse an explicit namespace flavor would reject.

Raises:

Type Description
InvalidNamespaceError

naming the problem and the sanitized spelling.

Source code in graflo/architecture/schema/namespace.py
def validate_namespace(name: str, flavor: DBType) -> None:
    """Refuse an explicit namespace *flavor* would reject.

    Raises:
        InvalidNamespaceError: naming the problem and the sanitized spelling.
    """
    flavor = DBType(flavor)
    problem = namespace_problem(name, flavor)
    if problem is not None:
        raise InvalidNamespaceError(
            f"Invalid {flavor.value} namespace {name!r}: {problem}. "
            f"A valid spelling is {sanitize_namespace(name, flavor)!r}."
        )