Skip to content

cdm_data_loaders.converters.readers.dlt

Build typed schema trees from dlt's normalized table forest.

DltReader dataclass

Resolve filtering and ambiguous child-table shape at the reader boundary.

Source code in src/cdm_data_loaders/converters/readers/dlt.py
@dataclass(frozen=True, slots=True)
class DltReader:
    """Resolve filtering and ambiguous child-table shape at the reader boundary."""

    include_dlt_columns: bool = False
    include_variant_columns: bool = False
    child_table_mode: Literal["object", "array"] = "object"
    extension_registry: ExtensionRegistry = DEFAULT_EXTENSIONS

    def __post_init__(self) -> None:
        """Reject invalid policy configuration."""
        if self.child_table_mode not in {"object", "array"}:
            msg = "child_table_mode must be 'object' or 'array'"
            raise ValueError(msg)
        if type(self.include_dlt_columns) is not bool or type(self.include_variant_columns) is not bool:
            msg = "Column filters must be booleans"
            raise TypeError(msg)

    def read(self, source: Mapping[str, Any]) -> dict[str, SchemaDocument]:
        """Return ordered root documents from stored-schema or plain-table input."""
        if not isinstance(source, Mapping):
            msg = "Expected a dlt stored schema or tables mapping"
            raise ConversionError(msg)
        if "tables" in source and isinstance(source["tables"], Mapping):
            tables = source["tables"]
        elif all(isinstance(value, Mapping) and ("columns" in value or "parent" in value) for value in source.values()):
            tables = source
        else:
            msg = (
                "Input must be a dlt stored schema (with a 'tables' dict) or a "
                "{table_name: TTableSchema} mapping; no tables found."
            )
            raise ConversionError(msg)
        if not tables:
            msg = "The schema contains no tables."
            raise ConversionError(msg)
        roots = unflatten_tables(tables)
        envelope = {key: value for key, value in source.items() if key != "tables"} if "tables" in source else {}
        documents = {}
        for name, table in roots.items():
            root = self._table(table)
            documents[name] = SchemaDocument(
                root=root,
                name=name,
                provenance=Provenance(
                    "dlt",
                    (name,),
                    {
                        "envelope": envelope,
                        "child_table_mode": self.child_table_mode,
                        "include_dlt_columns": self.include_dlt_columns,
                        "include_variant_columns": self.include_variant_columns,
                    },
                ),
            )
        return documents

    def _columns(self, table: DltTableNode) -> dict[str, Any]:
        """Filter before scalar-value detection and required propagation."""
        return {
            name: column
            for name, column in table.table.get("columns", {}).items()
            if (self.include_dlt_columns or not name.startswith(DLT_INTERNAL_PREFIX))
            and (self.include_variant_columns or "__v_" not in name)
        }

    def _table(self, table: DltTableNode) -> TypedNode:
        """Build real child nodes while retaining isolated source table metadata.

        A child table's name always wins over a same-named scalar column
        (dlt's own naming can produce this when a column is later normalized
        into a nested table); the scalar column's `required` flag still
        contributes to the resulting field's `required` flag.
        """
        columns = self._columns(table)
        properties = {name: self._column(name, column, table.name) for name, column in columns.items()}
        required_names = [name for name, column in columns.items() if column.get("nullable", True) is False]
        for name, child in table.children.items():
            child_node = self._table(child)
            required = any(column.get("nullable", True) is False for column in self._columns(child).values())
            properties[name] = Field(name, child_node, required=required or name in required_names)
            if required:
                required_names.append(name)
        annotations = {"description": table.table["description"]} if "description" in table.table else {}
        provenance = Provenance("dlt", ("tables", table.name), table.table)
        object_node = TypedNode(
            type="object",
            nullable=False,
            properties=tuple(properties.values()),
            required_names=tuple(required_names),
            additional_properties=TypedNode(type="never", nullable=False),
            annotations=annotations,
            provenance=provenance,
        )
        if table.parent is None:
            return object_node
        scalar = not table.children and list(columns) == ["value"]
        if scalar or self.child_table_mode == "array":
            return TypedNode(
                type="array",
                nullable=False,
                items=properties["value"].node if scalar else replace(object_node, annotations={}),
                annotations=annotations,
                provenance=provenance,
            )
        return object_node

    def _column(self, name: str, column: Mapping[str, Any], table_name: str) -> Field:
        """Preserve source type and hints without adopting JSON target encodings."""
        data_type = column.get("data_type")
        kind = TYPES.get(data_type, "any" if data_type is None else "unknown")
        if kind == "unknown":
            logger.warning("Unknown dlt data_type %r at %s.%s retained in IR", data_type, table_name, name)
        provenance = Provenance("dlt", ("tables", table_name, "columns", name), column)
        nullable = column.get("nullable", True)
        hints = NodeHints(
            logical_type=data_type,
            precision=column.get("precision"),
            scale=column.get("scale"),
            bit_width=column.get("precision") if data_type == "bigint" else None,
            timezone=column.get("timezone"),
        )
        extensions = Extensions({"x-dlt": dict(column)}, self.extension_registry)
        node = TypedNode(
            type=kind,
            nullable=nullable,
            hints=hints,
            annotations={"description": column["description"]} if "description" in column else {},
            extensions=extensions,
            provenance=provenance,
        )
        return Field(name, node, required=nullable is False, provenance=provenance)

__post_init__

__post_init__() -> None

Reject invalid policy configuration.

Source code in src/cdm_data_loaders/converters/readers/dlt.py
def __post_init__(self) -> None:
    """Reject invalid policy configuration."""
    if self.child_table_mode not in {"object", "array"}:
        msg = "child_table_mode must be 'object' or 'array'"
        raise ValueError(msg)
    if type(self.include_dlt_columns) is not bool or type(self.include_variant_columns) is not bool:
        msg = "Column filters must be booleans"
        raise TypeError(msg)

read

read(
    source: Mapping[str, Any],
) -> dict[str, SchemaDocument]

Return ordered root documents from stored-schema or plain-table input.

Source code in src/cdm_data_loaders/converters/readers/dlt.py
def read(self, source: Mapping[str, Any]) -> dict[str, SchemaDocument]:
    """Return ordered root documents from stored-schema or plain-table input."""
    if not isinstance(source, Mapping):
        msg = "Expected a dlt stored schema or tables mapping"
        raise ConversionError(msg)
    if "tables" in source and isinstance(source["tables"], Mapping):
        tables = source["tables"]
    elif all(isinstance(value, Mapping) and ("columns" in value or "parent" in value) for value in source.values()):
        tables = source
    else:
        msg = (
            "Input must be a dlt stored schema (with a 'tables' dict) or a "
            "{table_name: TTableSchema} mapping; no tables found."
        )
        raise ConversionError(msg)
    if not tables:
        msg = "The schema contains no tables."
        raise ConversionError(msg)
    roots = unflatten_tables(tables)
    envelope = {key: value for key, value in source.items() if key != "tables"} if "tables" in source else {}
    documents = {}
    for name, table in roots.items():
        root = self._table(table)
        documents[name] = SchemaDocument(
            root=root,
            name=name,
            provenance=Provenance(
                "dlt",
                (name,),
                {
                    "envelope": envelope,
                    "child_table_mode": self.child_table_mode,
                    "include_dlt_columns": self.include_dlt_columns,
                    "include_variant_columns": self.include_variant_columns,
                },
            ),
        )
    return documents