diff --git a/src/store/part.c b/src/store/part.c index ebe0ce15..6bc5dd69 100644 --- a/src/store/part.c +++ b/src/store/part.c @@ -27,6 +27,7 @@ #define _GNU_SOURCE #endif #include "part.h" +#include "core/runtime.h" #include "mem/sys.h" #include "ops/ops.h" #include "store/splay.h" @@ -560,7 +561,21 @@ static ray_t* empty_table_like(ray_t* tmpl) { for (int64_t c = 0; c < ncols; c++) { ray_t* col = ray_table_get_col_idx(tmpl, c); if (!col) { ray_release(out); return ray_error("type", "empty_table_like: null column"); } - ray_t* ecol = ray_vec_new(col->type, 0); + ray_t* ecol = NULL; + if (col->type == RAY_LIST) { + ecol = ray_list_new(0); + } else if (col->type == RAY_TABLE || col->type == RAY_DICT) { + int64_t name_id = ray_table_col_name(tmpl, c); + ray_t* name = ray_sym_str(name_id); + const char* name_ptr = name ? ray_str_ptr(name) : "?"; + int name_len = name ? (int)ray_str_len(name) : 1; + const char* type_name = col->type == RAY_TABLE ? "TABLE" : "DICT"; + ray_release(out); + return ray_error("nyi", "empty_table_like: column '%.*s' has unsupported nested type %s", + name_len, name_ptr, type_name); + } else { + ecol = ray_vec_new(col->type, 0); + } if (!ecol || RAY_IS_ERR(ecol)) { ray_release(out); return ecol ? ecol : ray_error("oom", NULL); } ray_t* nout = ray_table_add_col(out, ray_table_col_name(tmpl, c), ecol); ray_release(ecol); @@ -674,6 +689,7 @@ ray_t* ray_parted_fill(const char* db_root) { int64_t all_count = 0, all_cap = 0; uint8_t* fixed = (uint8_t*)ray_calloc_raw((size_t)((size_t)part_count) * (1)); ray_err_t err = fixed ? RAY_OK : RAY_ERR_OOM; + char cause[256] = {0}; for (int64_t p = 0; p < part_count && err == RAY_OK; p++) { char pdir[1024]; @@ -718,16 +734,23 @@ ray_t* ray_parted_fill(const char* db_root) { if (tn < 0 || (size_t)tn >= sizeof(tdir)) { err = RAY_ERR_RANGE; break; } ray_t* full = ray_read_splayed(tdir, sym_path); if (!full || RAY_IS_ERR(full)) { + err = full ? ray_err_from_obj(full) : RAY_ERR_OOM; + const char* detail = ray_error_msg(); + snprintf(cause, sizeof(cause), "table %s (partition %s): %s", + tname, part_dirs[templ], + detail && *detail ? detail : "template read failed"); if (full) ray_error_free(full); - err = RAY_ERR_IO; break; } empty_tbl = empty_table_like(full); ray_release(full); if (!empty_tbl || RAY_IS_ERR(empty_tbl)) { + err = empty_tbl ? ray_err_from_obj(empty_tbl) : RAY_ERR_OOM; + const char* detail = ray_error_msg(); + snprintf(cause, sizeof(cause), "table %s: %s", tname, + detail && *detail ? detail : "empty table construction failed"); if (empty_tbl) ray_error_free(empty_tbl); empty_tbl = NULL; - err = RAY_ERR_IO; break; } } @@ -737,7 +760,12 @@ ray_t* ray_parted_fill(const char* db_root) { db_root, part_dirs[p], tname); if (ptn < 0 || (size_t)ptn >= sizeof(ptdir)) { err = RAY_ERR_RANGE; break; } ray_err_t se = ray_splay_save(empty_tbl, ptdir, sym_path); - if (se != RAY_OK) { err = se; break; } + if (se != RAY_OK) { + err = se; + snprintf(cause, sizeof(cause), "table %s (partition %s): save failed", + tname, part_dirs[p]); + break; + } fixed[p] = 1; } if (empty_tbl) ray_release(empty_tbl); @@ -767,6 +795,9 @@ ray_t* ray_parted_fill(const char* db_root) { if (err != RAY_OK) { if (result && !RAY_IS_ERR(result)) ray_release(result); + if (cause[0]) + return ray_error(ray_err_code_str(err), "parted %s: fill failed: %s", + db_root, cause); return ray_error(ray_err_code_str(err), "parted %s: fill failed", db_root); } return result; diff --git a/test/rfl/system/db_parted_fill.rfl b/test/rfl/system/db_parted_fill.rfl index a1466b05..2574b271 100644 --- a/test/rfl/system/db_parted_fill.rfl +++ b/test/rfl/system/db_parted_fill.rfl @@ -40,7 +40,27 @@ ;; Idempotent: a second fill finds nothing missing → empty result. (.db.parted.fill "/tmp/rfl_fill/") -- [] +;; Regression #401: an empty copy of a LIST column must use the LIST +;; constructor, not the concrete-vector constructor (RAY_LIST == 0). +(set SCHED (table [acct sched] (list ['a 'b] (list (dict [0] [5.0]) (dict [0 250000] [15.0 10.0]))))) +(.db.splayed.set "/tmp/rfl_fill/2024.01.03/SCHED/" SCHED) +(.db.parted.fill "/tmp/rfl_fill/") -- ['2024.01.01 '2024.01.02] +(set RS (.db.parted.get "/tmp/rfl_fill/" 'SCHED)) +(count RS) -- 2 +(type (at RS 'sched)) -- 'LIST +(type (at (at RS 'sched) 0)) -- 'DICT +(at (at (at RS 'sched) 1) 250000) -- 10.0 +(.db.parted.fill "/tmp/rfl_fill/") -- [] + +;; Template errors retain their real code instead of being flattened to io. +(.sys.exec "rm -rf /tmp/rfl_fill_bad") -- 0 +(set TN (table [v] (list [1]))) +(.db.splayed.set "/tmp/rfl_fill_bad/2024.01.01/A/" TN) +(.db.splayed.set "/tmp/rfl_fill_bad/2024.01.02/BAD/" TN) +(.sys.exec "printf 'bad' > /tmp/rfl_fill_bad/2024.01.02/BAD/.d") -- 0 +(.db.parted.fill "/tmp/rfl_fill_bad/") !- corrupt + ;; Error paths: missing root and a non-parted (splayed) root. (.db.parted.fill "/tmp/rfl_fill_nope/") !- io -(.sys.exec "rm -rf /tmp/rfl_fill") +(.sys.exec "rm -rf /tmp/rfl_fill /tmp/rfl_fill_bad") diff --git a/test/test_store.c b/test/test_store.c index 03e37ad1..0903c56b 100644 --- a/test/test_store.c +++ b/test/test_store.c @@ -566,6 +566,55 @@ static test_result_t test_splay_dict_column_roundtrip(void) { PASS(); } +/* ---- test_splay_empty_list_column_roundtrip --------------------------- */ +static test_result_t test_splay_empty_list_column_roundtrip(void) { + (void)!system("rm -rf " TMP_SPLAY_DIR); + + ray_t* ids = ray_vec_new(RAY_I64, 0); + ray_t* who = ray_vec_new(RAY_SYM, 0); + ray_t* sched = ray_list_new(0); + TEST_ASSERT_FALSE(RAY_IS_ERR(ids)); + TEST_ASSERT_FALSE(RAY_IS_ERR(who)); + TEST_ASSERT_FALSE(RAY_IS_ERR(sched)); + + ray_t* tbl = ray_table_new(3); + tbl = ray_table_add_col(tbl, ray_sym_intern("id", 2), ids); + TEST_ASSERT_FALSE(RAY_IS_ERR(tbl)); + tbl = ray_table_add_col(tbl, ray_sym_intern("who", 3), who); + TEST_ASSERT_FALSE(RAY_IS_ERR(tbl)); + tbl = ray_table_add_col(tbl, ray_sym_intern("sched", 5), sched); + TEST_ASSERT_FALSE(RAY_IS_ERR(tbl)); + + const char* sym_path = TMP_SPLAY_DIR "/.sym"; + TEST_ASSERT_EQ_I(ray_splay_save(tbl, TMP_SPLAY_DIR, sym_path), RAY_OK); + ray_t* loaded = ray_read_splayed(TMP_SPLAY_DIR, sym_path); + TEST_ASSERT_NOT_NULL(loaded); + TEST_ASSERT_FALSE(RAY_IS_ERR(loaded)); + TEST_ASSERT_EQ_I(ray_table_ncols(loaded), 3); + TEST_ASSERT_EQ_I(ray_table_nrows(loaded), 0); + + ray_t* loaded_ids = ray_table_get_col(loaded, ray_sym_find("id", 2)); + ray_t* loaded_who = ray_table_get_col(loaded, ray_sym_find("who", 3)); + ray_t* loaded_sched = ray_table_get_col(loaded, ray_sym_find("sched", 5)); + TEST_ASSERT_NOT_NULL(loaded_ids); + TEST_ASSERT_NOT_NULL(loaded_who); + TEST_ASSERT_NOT_NULL(loaded_sched); + TEST_ASSERT_EQ_I(loaded_ids->type, RAY_I64); + TEST_ASSERT_EQ_I(loaded_who->type, RAY_SYM); + TEST_ASSERT_EQ_I(loaded_sched->type, RAY_LIST); + TEST_ASSERT_EQ_I(loaded_ids->len, 0); + TEST_ASSERT_EQ_I(loaded_who->len, 0); + TEST_ASSERT_EQ_I(loaded_sched->len, 0); + + ray_release(loaded); + ray_release(tbl); + ray_release(ids); + ray_release(who); + ray_release(sched); + (void)!system("rm -rf " TMP_SPLAY_DIR); + PASS(); +} + /* A deterministic unsupported column must be rejected before an earlier * column can replace the committed generation. */ static test_result_t test_splay_save_preflight_preserves_generation(void) { @@ -5163,6 +5212,7 @@ const test_entry_t store_entries[] = { { "store/splay_str_column_roundtrip", test_splay_str_column_roundtrip, store_setup, store_teardown }, { "store/splay_short_strv_roundtrip", test_splay_short_strv_roundtrip, store_setup, store_teardown }, { "store/splay_dict_column_roundtrip", test_splay_dict_column_roundtrip, store_setup, store_teardown }, + { "store/splay_empty_list_column_roundtrip", test_splay_empty_list_column_roundtrip, store_setup, store_teardown }, { "store/splay_save_preflight", test_splay_save_preflight_preserves_generation, store_setup, store_teardown }, { "store/parted_nrows", test_parted_nrows, store_setup, store_teardown }, { "store/table_nrows_parted", test_table_nrows_parted, store_setup, store_teardown },