tests/testthat/test-decompose.r

describe("decompose", {
  it("returns valid databases", {
    forall(
      list(
        gen_df(6, 7),
        gen.choice(gen.element(7:1), gen.pure(NA_integer_)),
        gen.element(c(FALSE, TRUE))
      ) |>
        gen.with(\(x) c(x[1], list(as.list(x[[1]])), x[2:3])),
      \(x, x2, digits, check) {
        fds <- discover(x, digits = digits)
        schema <- normalise(fds)
        db <- decompose(x, schema, digits = digits, check = check)
        expect_valid_database(db)
      },
      curry = TRUE
    )
  })
  it("removes extraneous dependencies", {
    df <- data.frame(a = integer(), b = integer(), c = integer())
    schema <- relation_schema(
      list(a = list(c("a", "b", "c"), list("a"))),
      attrs_order = c("a", "b", "c")
    ) |>
      database_schema(references = list())
    db <- decompose(df, schema)
    expect_identical(
      db,
      database(
        relation(
          list(a = list(
            df = df,
            keys = list("a")
          )),
          attrs_order = c("a", "b", "c")
        ),
        references = list()
      )
    )
  })
  it("resolves a simple bijection with no splits", {
    df <- data.frame(a = integer(), b = integer())
    schema <- relation_schema(
      list(a = list(c("a", "b"), list("a", "b"))),
      attrs_order = c("a", "b")
    ) |>
      database_schema(references = list())
    db <- decompose(df, schema)
    expect_identical(
      db,
      database(
        relation(list(a = list(df = df, keys = list("a", "b"))), c("a", "b")),
        list()
      )
    )
  })
  it("correctly splits example data.frame for original make_indexes() test", {
    df <- data.frame(
      id = c(
        0, 1, 2, 3, 4,
        5, 6, 7, 8, 9
      ),
      month = c(
        'dec', 'dec', 'jul', 'jul', 'dec',
        'jul', 'jul', 'jul', 'dec', 'jul'
      ),
      hemisphere = c(
        'N', 'N', 'N', 'N', 'S',
        'S', 'S', 'S', 'S', 'N'
      ),
      is_winter = c(
        TRUE, TRUE, FALSE, FALSE, FALSE,
        TRUE, TRUE, TRUE, FALSE, FALSE
      )
    )
    schema <- relation_schema(
      list(
        id = list(c("id", "month", "hemisphere"), list("id")),
        month_hemisphere = list(
          c("month", "hemisphere", "is_winter"),
          list(
            c("month", "hemisphere"),
            c("month", "is_winter"),
            c("hemisphere", "is_winter")
          )
        )
      ),
      attrs_order = c("id", "month", "hemisphere", "is_winter")
    ) |>
      database_schema(
        references = list(list(
          "id",
          c("month", "hemisphere"),
          "month_hemisphere",
          c("month", "hemisphere")
        ))
      )
    new_db <- decompose(df, schema)
    expected_db <- database(
      relation(
        list(
          id = list(
            df = df[, c("id", "month", "hemisphere")],
            keys = list("id")
          ),
          month_hemisphere = list(
            df = data.frame(
              month = c("dec", "jul", "dec", "jul"),
              hemisphere = c("N", "N", "S", "S"),
              is_winter = c(TRUE, FALSE, FALSE, TRUE),
              row.names = c(1L, 3L, 5:6)
            ),
            keys = list(
              c("month", "hemisphere"),
              c("month", "is_winter"),
              c("hemisphere", "is_winter")
            )
          )
        ),
        attrs_order = c("id", "month", "hemisphere", "is_winter")
      ),
      references = list(list(
        "id",
        c("month", "hemisphere"),
        "month_hemisphere",
        c("month", "hemisphere")
      ))
    )
    expect_identical(new_db, expected_db)
  })
  it("returns a error if data.frame doesn't satisfy FDs in the schema", {
    add_id_attribute <- function(df) {
      df2 <- cbind(df, a = seq.int(nrow(df)))
      names(df2) <- make.names(names(df2), unique = TRUE)
      df2
    }
    gen_fd_reduction_for_df <- function(df) {
      true_fds <- discover(df)
      nonempty_detsets <- which(lengths(detset(true_fds)) > 0L)
      if (length(nonempty_detsets) == 0)
        return(gen.pure(list(df, NULL, NULL)))
      gen.element(nonempty_detsets) |>
        gen.with(\(index) true_fds[[index]]) |>
        gen.and_then(\(fd) list(gen.pure(fd), gen.int(length(detset(fd))))) |>
        gen.with(\(lst) c(list(df), lst))
    }
    gen_df_and_fd_reduction <- function(nrow, ncol) {
      gen_df(nrow, ncol, minrow = 2L, mincol = 2L, remove_dup_rows = TRUE) |>
        gen.with(add_id_attribute) |>
        gen.and_then(gen_fd_reduction_for_df)
    }
    expect_decompose_error <- function(df, reduced_fd, removed_det) {
      if (nrow(df) <= 1)
        discard()
      flat_deps <- discover(df)
      reduced_index <- match(reduced_fd, flat_deps)
      if (is.na(reduced_index))
        stop("reduced_fd doesn't exist")
      reduced_deps <- flat_deps
      detset(reduced_deps)[[reduced_index]] <-
        detset(reduced_deps)[[reduced_index]][-removed_det]
      schema <- normalise(reduced_deps)
      expect_error(
        decompose(df, schema, check = TRUE),
        paste0(
          "\\A",
          "df doesn't satisfy functional dependencies in schema:",
          "(\\n *\\{.*\\} -> .*)+",
          "\\Z"
        ),
        perl = TRUE
      )
      element_regex <- ".+\\.\\{[^}]*\\}"
      expect_error(
        decompose(df, schema, check = FALSE),
        paste0(
          "\\A",
          "relations must satisfy their keys:",
          " element",
          paste0(
            "(",
            " ", element_regex,
            "|",
            "s ", element_regex, "(, ", element_regex, ")*",
            ")"
          ),
          "\\Z"
        ),
        perl = TRUE
      )
    }
    forall(
      gen_df_and_fd_reduction(6, 7),
      expect_decompose_error,
      discard.limit = 10L,
      curry = TRUE
    )
  })
  # it("returns a error if data.frame doesn't satisfy FKs in the schema", {
  #   gen_fk_reduction_for_df <- function(df) {
  #     true_dbs <- normalise(discover(df))
  #     true_fks <- references(true_dbs)
  #     true_fk_key_switch <- lapply(
  #       true_fks,
  #       \(fk) {
  #         len <- length(fk[[2]])
  #         # first key of correct length in each relation...
  #         new_tbs <- vapply(
  #           keys(true_dbs),
  #           \(ks) match(len, lengths(ks)),
  #           integer(1)
  #         )
  #         # ... that's not in the original FK
  #         valid_new_tbs <- new_tbs[
  #           !is.na(new_tbs) &
  #             names(new_tbs) != fk[[1]] &
  #             names(new_tbs) != fk[[3]]
  #         ]
  #         new_fks <- Map(
  #           \(new_key_index, new_parent) {
  #             new_fk <- c(
  #               fk[1:2],
  #               list(new_parent, keys(true_dbs)[[new_parent]][[new_key_index]])
  #             )
  #             stopifnot(length(new_fk) == 4)
  #             if (!is.element(list(new_fk[[4]]), keys(true_dbs)[[new_fk[[3]]]]))
  #               stop("argh")
  #             new_fk
  #           },
  #           valid_new_tbs,
  #           names(valid_new_tbs)
  #         ) |>
  #           Filter(f = \(fk) !is.element(list(fk), true_fks)) |>
  #           Filter(f = \(fk) {
  #             length(remove_violated_references(
  #               list(fk),
  #               decompose(df, true_dbs)
  #             )) == 0
  #           })
  #         new_fks
  #       }
  #     )
  #     if (all(lengths(true_fk_key_switch) == 0))
  #       return(gen.pure(list(df, NULL)))
  #     gen.element(which(lengths(true_fk_key_switch) > 0)) |>
  #       gen.and_then(\(index) list(
  #         gen.pure(df),
  #         gen.element(true_fk_key_switch[[index]]) |>
  #           gen.with(\(new_fk) {
  #             dbs <- true_dbs
  #             references(dbs)[[index]] <- new_fk
  #             dbs
  #           })
  #       ))
  #   }
  #   fac2char <- function(df) {
  #     facs <- vapply(df, is.factor, logical(1))
  #     df[, facs] <- lapply(df[, facs, drop = FALSE], as.character)
  #     df
  #   }
  #   gen_df_and_fk_reduction <- function(nrow, ncol) {
  #     gen_df(nrow, ncol, minrow = 4L, mincol = 4L, remove_dup_rows = TRUE) |>
  #       # change factors to characters, since they cause merge problems when
  #       # merged with non-factor non-character vectors that aren't in their
  #       # level set
  #       gen.with(fac2char) |>
  #       gen.and_then(gen_fk_reduction_for_df)
  #   }
  #   expect_fk_error <- function(df, dbs) {
  #     if (nrow(df) <= 1 || is.null(dbs))
  #       discard()
  #     name_regexp <- "[\\w\\. ]+"
  #     fk_half_regexp <- paste0(
  #       name_regexp,
  #       "\\.\\{", name_regexp, "(, ", name_regexp, ")*\\}"
  #     )
  #     expect_error(
  #       decompose(df, dbs, check = TRUE),
  #       paste0(
  #         "\\A",
  #         "relations must satisfy references in schema:",
  #         "(\\n", fk_half_regexp, " -> ", fk_half_regexp, ")+",
  #         "\\Z"
  #       ),
  #       perl = TRUE
  #     )
  #   }
  #   # gen_fk_reduction_for_df succeeds
  #   forall(
  #     gen_df(6, 7, minrow = 4, mincol = 4, remove_dup_rows = TRUE) |>
  #       gen.with(fac2char),
  #     \(x) expect_no_error(gen_fk_reduction_for_df(x))
  #   )
  #   forall(
  #     gen_df_and_fk_reduction(6, 7),
  #     expect_fk_error,
  #     discard.limit = 2*getOption("hedgehog.tests", 100),
  #     curry = TRUE
  #   )
  # })
  it("is equivalent to create >> insert for valid data", {
    forall(
      list(
        gen_df(6, 7, remove_dup_rows = TRUE),
        gen.element(c(7:1, NA)),
        gen.element(c(FALSE, TRUE))
      ) |>
        gen.with(uncurry(\(df, digits, check) {
          list(
            df = df,
            schema = normalise(
              discover(df, digits = digits),
              remove_avoidable = TRUE
            ),
            digits = digits,
            check = check
          )
        })),
      \(df, schema, digits, check) {
        expect_identical(
          decompose(df, schema, digits = digits,  check = check),
          create(schema) |> insert(df, digits = digits)
        )
      },
      curry = TRUE
    )
    forall(
      list(
        gen_df(6, 7, remove_dup_rows = TRUE),
        gen.element(c(7:1, NA)),
        gen.element(c(FALSE, TRUE))
      ) |>
        gen.with(uncurry(\(df, digits, check) {
          list(
            df = df,
            schema = normalise(
              discover(df, keep_rownames = TRUE, digits = digits),
              remove_avoidable = TRUE
            ),
            digits = digits,
            check = check
          )
        })),
      \(df, schema, digits, check) {
        expect_identical(
          decompose(df, schema, keep_rownames = TRUE, digits = digits,  check = check),
          create(schema) |> insert(df, keep_rownames = TRUE, digits = digits)
        )
      },
      curry = TRUE
    )
  })

  describe("Dependencies", {
    it("DepDF", {
      df <- data.frame(
        player_name = integer(),
        jersey_num = integer(),
        team = integer(),
        city = integer(),
        state = integer()
      )
      schema <- relation_schema(
        list(
          player_name_jersey_num = list(
            c("player_name", "jersey_num", "team"),
            list(
              c("player_name", "jersey_num"),
              c("player_name", "team"),
              c("team", "jersey_num")
            )
          ),
          city = list(
            c("city", "state"),
            list("city", "state")
          ),
          team = list(
            c("team", "city"),
            list("team")
          )
        ),
        attrs_order = c("player_name", "jersey_num", "team", "city", "state")
      ) |>
        database_schema(
          references = list(
            list("player_name_jersey_num", "team", "team", "team"),
            list("team", "city", "city", "city")
          )
        )
      db <- decompose(df, schema)
      expect_identical(length(db), 3L)
      expected_db <- database(
        relation(
          list(
            player_name_jersey_num = list(
              df = data.frame(
                player_name = integer(),
                jersey_num = integer(),
                team = integer()
              ),
              keys = list(
                c("player_name", "jersey_num"),
                c("player_name", "team"),
                c("jersey_num", "team")
              )
            ),
            city = list(
              df = data.frame(
                city = integer(),
                state = integer()
              ),
              keys = list("city", "state")
            ),
            team = list(
              df = data.frame(
                team = integer(),
                city = integer()
              ),
              keys = list("team")
            )
          ),
          attrs_order = c("player_name", "jersey_num", "team", "city", "state")
        ),
        references = list(
          list("player_name_jersey_num", "team", "team", "team"),
          list("team", "city", "city", "city")
        )
      )
      expect_identical(db, expected_db)
    })
  })
  it("correctly handles attributes with non-df-standard names", {
    df <- data.frame(1:3, c(1, 1, 2), c(1, 2, 2)) |>
      stats::setNames(c("A 1", "B 2", "C 3"))
    schema <- relation_schema(
      list(
        `A 1` = list(
          c("A 1", "B 2", "C 3"),
          list("A 1", c("B 2", "C 3"))
        )
      ),
      attrs_order = c("A 1", "B 2", "C 3")
    ) |>
      database_schema(references = list())
    db <- decompose(df, schema)
    expect_setequal(attrs(db)[[1]], c("A 1", "B 2", "C 3"))
  })
  it("links added key relations", {
    df <- data.frame(
      a = c(1L, 2L, 1L, 2L),
      b = c(1L, 2L, 1L, 2L),
      c = c(1L, 1L, 2L, 2L)
    )
    schema <- relation_schema(
      list(
        a = list(c("a", "b"), list("a", "b")),
        a_c = list(c("a", "c"), list(c("a", "c")))
      ),
      attrs_order = c("a", "b", "c")
    ) |>
      database_schema(references = list(list("a_c", "a", "a", "a")))
    db <- decompose(df, schema)
    expect_identical(
      records(db)$a_c,
      data.frame(a = 1:2, c = rep(1:2, each = 2), row.names = 1:4)
    )
    expect_identical(
      keys(db)$a_c,
      list(c("a", "c"))
    )
  })
})

describe("create_insert", {
  it("is equivalent to create >> insert", {
    forall(
      list(
        gen_df(6, 7, remove_dup_rows = TRUE),
        gen.element(c(7:1, NA_integer_))
      ) |>
        gen.with(uncurry(\(df, digits) list(
          df = df,
          schema = synthesise(discover(df, digits = digits)),
          digits = digits
        ))),
      \(df, schema, digits) {
        expect_identical(
          create_insert(df, schema, digits = digits),
          insert(create(schema), df, digits = digits)
        )
      },
      curry = TRUE
    )
  })
})

Try the autodb package in your browser

Any scripts or data that you put into this service are public.

autodb documentation built on Aug. 21, 2026, 5:14 p.m.