def flatten_nested_covariates(covariate_definitions):
    """
    Some covariates (e.g `categorised_as`) can define their own internal
    covariates which are used for calculating the column value but don't appear
    in the final output. Here we pull all these internal covariates out (which
    may be recursively nested) and assemble a flat list of covariates, adding a
    `hidden` flag to their arguments to indicate whether or not they belong in
    the final output

    We also check for any name clashes among covariates. (In future we could
    rewrite the names of internal covariates to avoid this but for now we just
    throw an error.)
    """
    flattened = {}
    hidden = set()
    items = list(covariate_definitions.items())
    while items:
        name, (query_type, query_args) = items.pop(0)
        if "extra_columns" in query_args:
            query_args = query_args.copy()
            # Pull out the extra columns
            extra_columns = query_args.pop("extra_columns")
            # Stick the query back on the stack
            items.insert(0, (name, (query_type, query_args)))
            # Mark the extra columns as hidden
            hidden.update(extra_columns.keys())
            # Add them to the start of the list of items to be processed
            items[:0] = extra_columns.items()
        else:
            if name in flattened:
                raise ValueError(f"Duplicate columns named '{name}'")
            flattened[name] = (query_type, query_args)
    for name, (query_type, query_args) in flattened.items():
        query_args["hidden"] = name in hidden
    return flattened