diff --git a/cpp/perspective/src/cpp/context_zero.cpp b/cpp/perspective/src/cpp/context_zero.cpp index cce3dbae81..b8c646244a 100644 --- a/cpp/perspective/src/cpp/context_zero.cpp +++ b/cpp/perspective/src/cpp/context_zero.cpp @@ -381,20 +381,12 @@ t_ctx0::sidedness() const { return 0; } -/** - * @brief Handle additions and new data, calculating deltas along the way. - * - * @param flattened - * @param delta - * @param prev - * @param curr - * @param transitions - * @param existed - */ void t_ctx0::notify(const t_data_table& flattened, const t_data_table& delta, const t_data_table& prev, const t_data_table& curr, const t_data_table& transitions, const t_data_table& existed) { + // Notify the context with new data when the `t_gstate` master table is + // not empty, and being updated with new data. psp_log_time(repr() + " notify.enter"); t_uindex nrecs = flattened.size(); std::shared_ptr pkey_sptr = flattened.get_const_column("psp_pkey"); @@ -406,6 +398,7 @@ t_ctx0::notify(const t_data_table& flattened, const t_data_table& delta, const t_column* existed_col = existed_sptr.get(); bool delete_encountered = false; + if (m_config.has_filters()) { t_mask msk_prev = filter_table_for_config(prev, m_config); t_mask msk_curr = filter_table_for_config(curr, m_config); @@ -441,13 +434,16 @@ t_ctx0::notify(const t_data_table& flattened, const t_data_table& delta, default: { PSP_COMPLAIN_AND_ABORT("Unexpected OP"); } break; } - // add the pkey for updated rows + // add the pkey for row delta add_delta_pkey(pkey); } psp_log_time(repr() + " notify.has_filter_path.updated_traversal"); - // calculate deltas - calc_step_delta(flattened, prev, curr, transitions); + // calculate cell deltas if enabled + if (get_deltas_enabled()) { + calc_step_delta(flattened, prev, curr, transitions); + } + m_has_delta = m_deltas->size() > 0 || m_delta_pkeys.size() > 0 || delete_encountered; psp_log_time(repr() + " notify.has_filter_path.exit"); @@ -455,6 +451,7 @@ t_ctx0::notify(const t_data_table& flattened, const t_data_table& delta, return; } + // Context does not have filters applied for (t_uindex idx = 0; idx < nrecs; ++idx) { t_tscalar pkey = m_symtable.get_interned_tscalar(pkey_col->get_scalar(idx)); std::uint8_t op_ = *(op_col->get_nth(idx)); @@ -476,26 +473,25 @@ t_ctx0::notify(const t_data_table& flattened, const t_data_table& delta, default: { PSP_COMPLAIN_AND_ABORT("Unexpected OP"); } break; } - // add the pkey for updated rows + // add the pkey for row delta add_delta_pkey(pkey); } psp_log_time(repr() + " notify.no_filter_path.updated_traversal"); - // calculate deltas - calc_step_delta(flattened, prev, curr, transitions); + // calculate cell deltas if enabled + if (get_deltas_enabled()) { + calc_step_delta(flattened, prev, curr, transitions); + } m_has_delta = m_deltas->size() > 0 || m_delta_pkeys.size() > 0 || delete_encountered; psp_log_time(repr() + " notify.no_filter_path.exit"); } -/** - * @brief Handle the addition of new data. - * - * @param flattened - */ void t_ctx0::notify(const t_data_table& flattened) { + // Notify the context with new data after the `t_gstate`'s master table + // has been updated for the first time with data. t_uindex nrecs = flattened.size(); std::shared_ptr pkey_sptr = flattened.get_const_column("psp_pkey"); std::shared_ptr op_sptr = flattened.get_const_column("psp_op"); @@ -518,11 +514,19 @@ t_ctx0::notify(const t_data_table& flattened) { m_traversal->add_row(m_gstate, m_config, pkey); } } break; - default: { - // pass - } break; + default: break; } + + // Add primary key to track row delta + add_delta_pkey(pkey); } + + // Calculate the step delta, if enabled in the context through an on_update + // callback with the "cell" or "row" mode set. + if (get_deltas_enabled()) { + calc_step_delta(flattened); + } + return; } @@ -535,13 +539,51 @@ t_ctx0::notify(const t_data_table& flattened) { case OP_INSERT: { m_traversal->add_row(m_gstate, m_config, pkey); } break; - default: { } break; } + default: break; + } + + // Add primary key to track row delta + add_delta_pkey(pkey); + } + + // Calculate the step delta, if enabled in the context through an on_update + // callback with the "cell" or "row" mode set. + if (get_deltas_enabled()) { + calc_step_delta(flattened); + } +} + +void +t_ctx0::calc_step_delta(const t_data_table& flattened) { + // Calculate step deltas when the `t_gstate` master table is updated with + // data for the first time, so every single row is a new delta. + t_uindex nrows = flattened.size(); + const auto& column_names = m_config.get_column_names(); + const t_column* pkey_col = flattened.get_const_column("psp_pkey").get(); + + // Add every row and every column to the delta + for (const auto& name : column_names) { + auto cidx = m_config.get_colidx(name); + const t_column* flattened_column = flattened.get_const_column(name).get(); + + for (t_uindex ridx = 0; ridx < nrows; ++ridx) { + m_deltas->insert( + t_zcdelta( + get_interned_tscalar(pkey_col->get_scalar(ridx)), + cidx, + mknone(), + get_interned_tscalar(flattened_column->get_scalar(ridx)) + ) + ); + } } } void t_ctx0::calc_step_delta(const t_data_table& flattened, const t_data_table& prev, const t_data_table& curr, const t_data_table& transitions) { + // Calculate step deltas when the `t_gstate` master table already has + // data, so we can take transitions into account. t_uindex nrows = flattened.size(); PSP_VERBOSE_ASSERT(prev.size() == nrows, "Shape violation detected"); @@ -580,6 +622,7 @@ t_ctx0::calc_step_delta(const t_data_table& flattened, const t_data_table& prev, } } + /** * @brief Mark a primary key as updated by adding it to the tracking set. * diff --git a/cpp/perspective/src/cpp/view.cpp b/cpp/perspective/src/cpp/view.cpp index 1cf144c212..523b129307 100644 --- a/cpp/perspective/src/cpp/view.cpp +++ b/cpp/perspective/src/cpp/view.cpp @@ -575,22 +575,12 @@ View::_get_deltas_enabled() const { return m_ctx->get_deltas_enabled(); } -template <> -bool -View::_get_deltas_enabled() const { - return true; -} - template void View::_set_deltas_enabled(bool enabled_state) { m_ctx->set_deltas_enabled(enabled_state); } -template <> -void -View::_set_deltas_enabled(bool enabled_state) {} - // Pivot table operations template std::int32_t diff --git a/cpp/perspective/src/include/perspective/context_zero.h b/cpp/perspective/src/include/perspective/context_zero.h index e56e2664fd..621781c5ff 100644 --- a/cpp/perspective/src/include/perspective/context_zero.h +++ b/cpp/perspective/src/include/perspective/context_zero.h @@ -47,11 +47,29 @@ class PERSPECTIVE_EXPORT t_ctx0 : public t_ctxbase { std::vector get_all_pkeys( const std::vector>& cells) const; + /** + * @brief During a call to `notify` when the master table is being updated + * with data for the first time (prev, curr, and transitions tables are + * empty), take all added rows in the traversal and store them in + * `m_deltas`. + * + * @param flattened + */ + void calc_step_delta(const t_data_table& flattened); + + /** + * @brief During a call to `notify` when the master table has data, + * calculate the deltas - both changed and added cells, and write them + * to `m_deltas`. + * + * @param flattened + * @param prev + * @param curr + * @param transitions + */ void calc_step_delta(const t_data_table& flattened, const t_data_table& prev, const t_data_table& curr, const t_data_table& transitions); - void calc_row_delta(const t_data_table& flattened, const t_data_table& transitions); - void add_delta_pkey(t_tscalar pkey); private: diff --git a/packages/perspective-bench/bench/perspective.benchmark.js b/packages/perspective-bench/bench/perspective.benchmark.js index 0769eec6b0..eefc4468af 100644 --- a/packages/perspective-bench/bench/perspective.benchmark.js +++ b/packages/perspective-bench/bench/perspective.benchmark.js @@ -92,16 +92,108 @@ describe("Update", async () => { const static_table = worker.table(data.arrow.slice()); const static_view = static_table.view(); - let table; + let table, view; afterEach(async () => { + if (view) { + await view.delete(); + } await table.delete(); }); for (const name of Object.keys(data)) { describe("mixed", async () => { - describe("update", async () => { + // Benchmark how long it takes the table to update without any + // linked contexts to notify. + describe("table_only", async () => { + table = worker.table(data.arrow.slice()); + + let test_data = await static_view[`to_${name}`]({end_row: 500}); + benchmark(name, async () => { + for (let i = 0; i < 5; i++) { + table.update(test_data.slice ? test_data.slice() : test_data); + await table.size(); + } + }); + }); + + describe("ctx0", async () => { table = worker.table(data.arrow.slice()); + view = table.view(); + + let test_data = await static_view[`to_${name}`]({end_row: 500}); + benchmark(name, async () => { + for (let i = 0; i < 5; i++) { + table.update(test_data.slice ? test_data.slice() : test_data); + await table.size(); + } + }); + }); + + describe("ctx1", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State"] + }); + + let test_data = await static_view[`to_${name}`]({end_row: 500}); + benchmark(name, async () => { + for (let i = 0; i < 5; i++) { + table.update(test_data.slice ? test_data.slice() : test_data); + await table.size(); + } + }); + }); + + describe("ctx1 deep", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State", "City"] + }); + let test_data = await static_view[`to_${name}`]({end_row: 500}); + benchmark(name, async () => { + for (let i = 0; i < 5; i++) { + table.update(test_data.slice ? test_data.slice() : test_data); + await table.size(); + } + }); + }); + + describe("ctx2", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State"], + column_pivots: ["Sub-Category"] + }); + let test_data = await static_view[`to_${name}`]({end_row: 500}); + benchmark(name, async () => { + for (let i = 0; i < 5; i++) { + table.update(test_data.slice ? test_data.slice() : test_data); + await table.size(); + } + }); + }); + + describe("ctx2 deep", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State", "City"], + column_pivots: ["Sub-Category"] + }); + let test_data = await static_view[`to_${name}`]({end_row: 500}); + benchmark(name, async () => { + for (let i = 0; i < 5; i++) { + table.update(test_data.slice ? test_data.slice() : test_data); + await table.size(); + } + }); + }); + + describe("ctx1.5", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + column_pivots: ["Sub-Category"] + }); let test_data = await static_view[`to_${name}`]({end_row: 500}); benchmark(name, async () => { for (let i = 0; i < 5; i++) { @@ -114,6 +206,124 @@ describe("Update", async () => { } }); +describe("Deltas", async () => { + // Generate update data from Perspective + const static_table = worker.table(data.arrow.slice()); + const static_view = static_table.view(); + + let table, view; + + afterEach(async () => { + await view.delete(); + await table.delete(); + }); + + describe("mixed", async () => { + describe("ctx0", async () => { + table = worker.table(data.arrow.slice()); + view = table.view(); + view.on_update(() => {}, {mode: "cell"}); + const test_data = await static_view.to_arrow({end_row: 500}); + benchmark("cell delta", async () => { + for (let i = 0; i < 3; i++) { + table.update(test_data.slice()); + await table.size(); + } + }); + }); + + describe("ctx0", async () => { + table = worker.table(data.arrow.slice()); + view = table.view(); + view.on_update(() => {}, {mode: "row"}); + const test_data = await static_view.to_arrow({end_row: 500}); + benchmark("row delta", async () => { + for (let i = 0; i < 3; i++) { + table.update(test_data.slice ? test_data.slice() : test_data); + await table.size(); + } + }); + }); + + describe("ctx1", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State"] + }); + view.on_update(() => {}, {mode: "row"}); + const test_data = await static_view.to_arrow({end_row: 500}); + benchmark("row delta", async () => { + for (let i = 0; i < 3; i++) { + table.update(test_data.slice()); + await table.size(); + } + }); + }); + + describe("ctx1 deep", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State", "City"] + }); + view.on_update(() => {}, {mode: "row"}); + const test_data = await static_view.to_arrow({end_row: 500}); + benchmark("row delta", async () => { + for (let i = 0; i < 3; i++) { + table.update(test_data.slice()); + await table.size(); + } + }); + }); + + describe("ctx2", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State"], + column_pivots: ["Sub-Category"] + }); + view.on_update(() => {}, {mode: "row"}); + const test_data = await static_view.to_arrow({end_row: 500}); + benchmark("row delta", async () => { + for (let i = 0; i < 3; i++) { + table.update(test_data.slice()); + await table.size(); + } + }); + }); + + describe("ctx2 deep", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + row_pivots: ["State", "City"], + column_pivots: ["Sub-Category"] + }); + view.on_update(() => {}, {mode: "row"}); + const test_data = await static_view.to_arrow({end_row: 500}); + benchmark("row delta", async () => { + for (let i = 0; i < 3; i++) { + table.update(test_data.slice()); + await table.size(); + } + }); + }); + + describe("ctx1.5", async () => { + table = worker.table(data.arrow.slice()); + view = table.view({ + column_pivots: ["Sub-Category"] + }); + view.on_update(() => {}, {mode: "row"}); + const test_data = await static_view.to_arrow({end_row: 500}); + benchmark("row delta", async () => { + for (let i = 0; i < 3; i++) { + table.update(test_data.slice()); + await table.size(); + } + }); + }); + }); +}); + describe("View", async () => { let table; diff --git a/packages/perspective-bench/bench/versions.js b/packages/perspective-bench/bench/versions.js index 9f190691ec..5d93bd8b01 100644 --- a/packages/perspective-bench/bench/versions.js +++ b/packages/perspective-bench/bench/versions.js @@ -34,7 +34,7 @@ const JPMC_VERSIONS = [ const FINOS_VERSIONS = ["0.3.1", "0.3.0", "0.3.0-rc.3", "0.3.0-rc.2", "0.3.0-rc.1"]; -const UMD_VERSIONS = ["0.4.8", "0.4.7", "0.4.6", "0.4.5", "0.4.4", "0.4.2", "0.4.1", "0.4.0", "0.3.9", "0.3.8", "0.3.7", "0.3.6"]; +const UMD_VERSIONS = ["0.5.2", "0.5.1", "0.5.0", "0.4.8", "0.4.7", "0.4.6", "0.4.5", "0.4.4", "0.4.2", "0.4.1", "0.4.0", "0.3.9", "0.3.8", "0.3.7", "0.3.6"]; async function run() { await PerspectiveBench.run("master", "bench/perspective.benchmark.js", `http://${process.env.PSP_DOCKER_PUPPETEER ? `localhost` : `host.docker.internal`}:8080/perspective.js`, { diff --git a/packages/perspective-bench/src/js/browser_runtime.js b/packages/perspective-bench/src/js/browser_runtime.js index b84ffb1ce2..a765a8d935 100644 --- a/packages/perspective-bench/src/js/browser_runtime.js +++ b/packages/perspective-bench/src/js/browser_runtime.js @@ -133,7 +133,7 @@ class Benchmark { this._table_printer( ...(OUTPUT_MODE === "tree" ? path : []), `{${color} ${completed}}{whiteBright /${total}}`, - `({${color} ${(100 * completed_per).toFixed(2)}}{whiteBright %)}`, + `{${color} ${(100 * completed_per).toFixed(2)}}{whiteBright %)}`, `{whiteBright ${time.toFixed(3)}}s`, `{whiteBright ${time_per.toFixed(2)}}secs/op`, `{${stddev_color} ${(100 * stddev).toFixed(2)}}{whiteBright %} σ/mean`, diff --git a/packages/perspective/src/js/perspective.js b/packages/perspective/src/js/perspective.js index 38ea84dc5a..f45eae4b71 100644 --- a/packages/perspective/src/js/perspective.js +++ b/packages/perspective/src/js/perspective.js @@ -819,15 +819,18 @@ export default function(Module) { */ view.prototype.on_update = function(callback, {mode = "none"} = {}) { _call_process(this.table.get_id()); + if (["none", "cell", "row"].indexOf(mode) === -1) { throw new Error(`Invalid update mode "${mode}" - valid modes are "none", "cell" and "row".`); } + if (mode === "cell" || mode === "row") { // Enable deltas only if needed by callback if (!this._View._get_deltas_enabled()) { this._View._set_deltas_enabled(true); } } + this.callbacks.push({ view: this, orig_callback: callback, diff --git a/packages/perspective/test/js/computed/deltas.js b/packages/perspective/test/js/computed/deltas.js index 28cc204e1c..7b64f96b4e 100644 --- a/packages/perspective/test/js/computed/deltas.js +++ b/packages/perspective/test/js/computed/deltas.js @@ -55,6 +55,43 @@ module.exports = perspective => { table.update({x: [1, 3], y: ["HELLO", "WORLD"]}); }); + it("Returns appended rows for normal and computed columns from schema", async function(done) { + const table = perspective.table({ + x: "integer", + y: "string" + }); + const view = table.view({ + computed_columns: [ + { + column: "uppercase", + computed_function_name: "Uppercase", + inputs: ["y"] + } + ] + }); + + view.on_update( + async function(updated) { + const expected = [ + {x: 1, y: "a", uppercase: "A"}, + {x: 2, y: "b", uppercase: "B"}, + {x: 3, y: "c", uppercase: "C"}, + {x: 4, y: "d", uppercase: "D"} + ]; + await match_delta(perspective, updated.delta, expected); + await view.delete(); + await table.delete(); + done(); + }, + {mode: "row"} + ); + + table.update({ + x: [1, 2, 3, 4], + y: ["a", "b", "c", "d"] + }); + }); + it("Returns partially updated step delta for normal and computed columns", async function(done) { let table = perspective.table( [ diff --git a/packages/perspective/test/js/delta.js b/packages/perspective/test/js/delta.js index f7dadf725f..13be147310 100644 --- a/packages/perspective/test/js/delta.js +++ b/packages/perspective/test/js/delta.js @@ -60,6 +60,99 @@ module.exports = perspective => { table.update(partial_change_y); }); + it("Should calculate step delta for 0-sided contexts from schema", async function(done) { + let table = perspective.table( + { + x: "integer", + y: "string", + z: "boolean" + }, + {index: "x"} + ); + let view = table.view(); + view.on_update( + function(updated) { + expect(updated.delta).toEqual(data); + view.delete(); + table.delete(); + done(); + }, + {mode: "cell"} + ); + table.update(data); + }); + + it("Should calculate step delta for added rows in 0-sided contexts from schema", async function(done) { + let table = perspective.table({ + x: "integer", + y: "string", + z: "boolean" + }); + let view = table.view(); + view.on_update( + function(updated) { + expect(updated.delta).toEqual(data); + view.delete(); + table.delete(); + done(); + }, + {mode: "cell"} + ); + table.update(data); + }); + + it("Should calculate step delta for added rows in 0-sided filtered contexts from schema", async function(done) { + let table = perspective.table({ + x: "integer", + y: "string", + z: "boolean" + }); + let view = table.view({ + filter: [["x", ">", 3]] + }); + view.on_update( + function(updated) { + expect(updated.delta).toEqual([ + { + x: 4, + y: "d", + z: false + } + ]); + view.delete(); + table.delete(); + done(); + }, + {mode: "cell"} + ); + table.update(data); + }); + + it("Should calculate step delta for added rows with partial nones in 0-sided contexts from schema", async function(done) { + let table = perspective.table({ + x: "integer", + y: "string", + z: "boolean" + }); + let view = table.view(); + view.on_update( + function(updated) { + expect(updated.delta).toEqual([ + {x: 1, y: "a", z: true}, + {x: 2, y: "b", z: null} + ]); + view.delete(); + table.delete(); + done(); + }, + {mode: "cell"} + ); + table.update([ + {x: 1, y: "a", z: true}, + {x: 2, y: "b"} + ]); + }); + it.skip("Should calculate step delta for 0-sided contexts during non-sequential updates", async function(done) { let table = perspective.table(data, {index: "x"}); let view = table.view(); @@ -100,6 +193,38 @@ module.exports = perspective => { table.update(partial_change_y); }); + it("returns changed rows from schema", async function(done) { + let table = perspective.table( + { + x: "integer", + y: "string", + z: "boolean" + }, + {index: "x"} + ); + let view = table.view(); + view.on_update( + async function(updated) { + const expected = [ + {x: 1, y: "d", z: false}, + {x: 2, y: "b", z: false}, + {x: 3, y: "c", z: true} + ]; + await match_delta(perspective, updated.delta, expected); + view.delete(); + table.delete(); + done(); + }, + {mode: "row"} + ); + table.update([ + {x: 1, y: "a", z: true}, + {x: 2, y: "b", z: false}, + {x: 3, y: "c", z: true}, + {x: 1, y: "d", z: false} + ]); + }); + it("returns added rows", async function(done) { let table = perspective.table(data); let view = table.view(); @@ -119,6 +244,25 @@ module.exports = perspective => { table.update(partial_change_y); }); + it("returns added rows from schema", async function(done) { + let table = perspective.table({ + x: "integer", + y: "string", + z: "boolean" + }); + let view = table.view(); + view.on_update( + async function(updated) { + await match_delta(perspective, updated.delta, data); + view.delete(); + table.delete(); + done(); + }, + {mode: "row"} + ); + table.update(data); + }); + it("returns deleted columns", async function(done) { let table = perspective.table(data, {index: "x"}); let view = table.view(); @@ -162,6 +306,63 @@ module.exports = perspective => { table.update(partial_change_y); }); + it("returns changed rows in sorted context from schema", async function(done) { + let table = perspective.table( + { + x: "integer", + y: "string", + z: "boolean" + }, + {index: "x"} + ); + let view = table.view({ + sort: [["x", "desc"]] + }); + view.on_update( + async function(updated) { + const expected = [ + {x: 4, y: "a", z: true}, + {x: 3, y: "c", z: true}, + {x: 2, y: "b", z: false}, + {x: 1, y: "d", z: false} + ]; + await match_delta(perspective, updated.delta, expected); + view.delete(); + table.delete(); + done(); + }, + {mode: "row"} + ); + table.update([ + {x: 1, y: "a", z: true}, + {x: 2, y: "b", z: false}, + {x: 3, y: "c", z: true}, + {x: 1, y: "d", z: false}, + {x: 4, y: "a", z: true} + ]); + }); + + it("returns added rows in filtered context from schema", async function(done) { + let table = perspective.table({ + x: "integer", + y: "string", + z: "boolean" + }); + let view = table.view({ + filter: [["x", ">", 3]] + }); + view.on_update( + async function(updated) { + await match_delta(perspective, updated.delta, [{x: 4, y: "d", z: false}]); + view.delete(); + table.delete(); + done(); + }, + {mode: "row"} + ); + table.update(data); + }); + it("returns changed rows in non-sequential update", async function(done) { let table = perspective.table(data, {index: "x"}); let view = table.view(); diff --git a/python/perspective/perspective/tests/table/test_view.py b/python/perspective/perspective/tests/table/test_view.py index 5167d21cab..f36ad866f2 100644 --- a/python/perspective/perspective/tests/table/test_view.py +++ b/python/perspective/perspective/tests/table/test_view.py @@ -842,6 +842,85 @@ def cb1(port_id, delta): view.on_update(cb1, mode="row") tbl.update(update_data) + def test_view_row_delta_zero_from_schema(self, util): + update_data = { + "a": [5], + "b": [6] + } + + def cb1(port_id, delta): + compare_delta(delta, update_data) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view() + view.on_update(cb1, mode="row") + tbl.update(update_data) + + def test_view_row_delta_zero_from_schema_filtered(self, util): + update_data = { + "a": [8, 9, 10, 11], + "b": [1, 2, 3, 4] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [11], + "b": [4] + }) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view(filter=[["a", ">", 10]]) + view.on_update(cb1, mode="row") + tbl.update(update_data) + + def test_view_row_delta_zero_from_schema_indexed(self, util): + update_data = { + "a": ["a", "b", "a"], + "b": [1, 2, 3] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": ["a", "b"], + "b": [3, 2] + }) + + tbl = Table({ + "a": str, + "b": int + }, index="a") + + view = tbl.view() + view.on_update(cb1, mode="row") + + tbl.update(update_data) + + def test_view_row_delta_zero_from_schema_indexed_filtered(self, util): + update_data = { + "a": [8, 9, 10, 11, 11], + "b": [1, 2, 3, 4, 5] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [11], + "b": [5] + }) + + tbl = Table({ + "a": int, + "b": int + }, index="a") + view = tbl.view(filter=[["a", ">", 10]]) + view.on_update(cb1, mode="row") + tbl.update(update_data) + def test_view_row_delta_one(self, util): data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}] update_data = { @@ -865,6 +944,155 @@ def cb1(port_id, delta): view.on_update(cb1, mode="row") tbl.update(update_data) + def test_view_row_delta_one_from_schema(self, util): + update_data = { + "a": [1, 2, 3, 4, 5], + "b": [6, 7, 8, 9, 10] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [15, 1, 2, 3, 4, 5], + "b": [40, 6, 7, 8, 9, 10] + }) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view(row_pivots=["a"]) + view.on_update(cb1, mode="row") + tbl.update(update_data) + + def test_view_row_delta_one_from_schema_sorted(self, util): + update_data = { + "a": [1, 2, 3, 4, 5], + "b": [6, 7, 8, 9, 10] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [15, 5, 4, 3, 2, 1], + "b": [40, 10, 9, 8, 7, 6] + }) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view(row_pivots=["a"], sort=[["a", "desc"]]) + view.on_update(cb1, mode="row") + tbl.update(update_data) + + def test_view_row_delta_one_from_schema_filtered(self, util): + update_data = { + "a": [1, 2, 3, 4, 5], + "b": [6, 7, 8, 9, 10] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [9, 4, 5], + "b": [19, 9, 10] + }) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view(row_pivots=["a"], filter=[["a", ">", 3]]) + view.on_update(cb1, mode="row") + tbl.update(update_data) + + def test_view_row_delta_one_from_schema_sorted_filtered(self, util): + update_data = { + "a": [1, 2, 3, 4, 5], + "b": [6, 7, 8, 9, 10] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [9, 5, 4], + "b": [19, 10, 9] + }) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view( + row_pivots=["a"], + sort=[["a", "desc"]], + filter=[["a", ">", 3]]) + view.on_update(cb1, mode="row") + tbl.update(update_data) + + def test_view_row_delta_one_from_schema_indexed(self, util): + update_data = { + "a": [1, 2, 3, 4, 5, 5, 4], + "b": [6, 7, 8, 9, 10, 11, 12] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [15, 1, 2, 3, 4, 5], + "b": [44, 6, 7, 8, 12, 11] + }) + + tbl = Table({ + "a": int, + "b": int + }, index="a") + + view = tbl.view(row_pivots=["a"]) + view.on_update(cb1, mode="row") + + tbl.update(update_data) + + def test_view_row_delta_one_from_schema_sorted_indexed(self, util): + update_data = { + "a": [1, 2, 3, 4, 5, 5, 4], + "b": [6, 7, 8, 9, 10, 11, 12] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [15, 4, 5, 3, 2, 1], + "b": [44, 12, 11, 8, 7, 6] + }) + + tbl = Table({ + "a": int, + "b": int + }, index="a") + + view = tbl.view(row_pivots=["a"], sort=[["b", "desc"]]) + view.on_update(cb1, mode="row") + + tbl.update(update_data) + + def test_view_row_delta_one_from_schema_filtered_indexed(self, util): + update_data = { + "a": [1, 2, 3, 4, 5, 5, 4], + "b": [6, 7, 8, 9, 10, 11, 12] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "a": [9, 4, 5], + "b": [23, 12, 11] + }) + + tbl = Table({ + "a": int, + "b": int + }, index="a") + + view = tbl.view(row_pivots=["a"], filter=[["a", ">", 3]]) + view.on_update(cb1, mode="row") + + tbl.update(update_data) + def test_view_row_delta_two(self, util): data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}] update_data = { @@ -894,6 +1122,44 @@ def cb1(port_id, delta): view.on_update(cb1, mode="row") tbl.update(update_data) + def test_view_row_delta_two_from_schema(self, util): + data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}] + + def cb1(port_id, delta): + compare_delta(delta, { + "2|a": [1, 1, None], + "2|b": [2, 2, None], + "4|a": [3, None, 3], + "4|b": [4, None, 4] + }) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view(row_pivots=["a"], column_pivots=["b"]) + view.on_update(cb1, mode="row") + tbl.update(data) + + def test_view_row_delta_two_from_schema_indexed(self, util): + data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 3, "b": 5}] + + def cb1(port_id, delta): + compare_delta(delta, { + "2|a": [1, 1, None], + "2|b": [2, 2, None], + "5|a": [3, None, 3], + "5|b": [5, None, 5] + }) + + tbl = Table({ + "a": int, + "b": int + }, index="a") + view = tbl.view(row_pivots=["a"], column_pivots=["b"]) + view.on_update(cb1, mode="row") + tbl.update(data) + def test_view_row_delta_two_column_only(self, util): data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}] update_data = { @@ -922,7 +1188,73 @@ def cb1(port_id, delta): view.on_update(cb1, mode="row") tbl.update(update_data) - # hidden rows + def test_view_row_delta_two_column_only_indexed(self, util): + data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 3, "b": 5}] + update_data = { + "a": [5], + "b": [6] + } + + def cb1(port_id, delta): + compare_delta(delta, { + "2|a": [1, None], + "2|b": [2, None], + "5|a": [3, None], + "5|b": [5, None], + "6|a": [5, 5], + "6|b": [6, 6] + }) + + tbl = Table(data, index="a") + view = tbl.view(column_pivots=["b"]) + assert view.to_dict() == { + "2|a": [1, None], + "2|b": [2, None], + "5|a": [None, 3], + "5|b": [None, 5], + } + view.on_update(cb1, mode="row") + tbl.update(update_data) + + def test_view_row_delta_two_column_only_from_schema(self, util): + data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}] + + def cb1(port_id, delta): + compare_delta(delta, { + "2|a": [1, 1, None], + "2|b": [2, 2, None], + "4|a": [3, None, 3], + "4|b": [4, None, 4] + }) + + tbl = Table({ + "a": int, + "b": int + }) + view = tbl.view(column_pivots=["b"]) + view.on_update(cb1, mode="row") + tbl.update(data) + + def test_view_row_delta_two_column_only_from_schema_indexed(self, util): + data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 3, "b": 5}] + + def cb1(port_id, delta): + compare_delta(delta, { + "2|a": [1, 1, None], + "2|b": [2, 2, None], + "5|a": [3, None, 3], + "5|b": [5, None, 5] + }) + + tbl = Table({ + "a": int, + "b": int + }, index="a") + view = tbl.view(column_pivots=["b"]) + view.on_update(cb1, mode="row") + tbl.update(data) + + # hidden cols def test_view_num_hidden_cols(self): data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]