# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ # ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ # ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ # ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ # ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ # ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ # ┃ Copyright (c) 2017, the Perspective Authors. ┃ # ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ # ┃ This file is part of the Perspective library, distributed under the terms ┃ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import numpy as np import pyarrow as pa import pandas as pd from datetime import date, datetime from pytest import mark import perspective as psp client = psp.Server().new_local_client() Table = client.table class TestUpdate(object): @mark.skip(reason="Fix for null values update yet to be implemented") def test_update_with_missing_or_null_values(self): tbl = Table([{"a": "1", "b": "2"}, {"a": "3", "b": "4"}], index="a") tbl.update([{"a": "1", "b": None}]) assert tbl.view().to_records() == [{"a": "1", "b": None}, {"a": "3", "b": "4"}] data = pd.DataFrame({"a": ["1"], "b": [None]}) arrow_table = pa.Table.from_pandas(data, preserve_index=False) tbl.update(arrow_table) assert tbl.size() == 2 assert tbl.view().to_records() == [{"a": "1", "b": ""}, {"a": "3", "b": "4"}] def test_update_from_schema(self): tbl = Table({"a": "string", "b": "integer"}) tbl.update([{"a": "abc", "b": 123}]) assert tbl.view().to_records() == [{"a": "abc", "b": 123}] def test_update_columnar_from_schema(self): tbl = Table({"a": "string", "b": "integer"}) tbl.update({"a": ["abc"], "b": [123]}) assert tbl.view().to_records() == [{"a": "abc", "b": 123}] def test_update_csv(self): tbl = Table({"a": "string", "b": "integer"}) view = tbl.view() tbl.update("a,b\nxyz,123\ndef,100000000") assert view.to_columns() == {"a": ["xyz", "def"], "b": [123, 100000000]} def test_update_csv_indexed(self): tbl = Table({"a": "string", "b": "float"}, index="a") view = tbl.view() tbl.update("a,b\nxyz,1.23456718\ndef,100000000.1") assert view.to_columns() == { "a": ["def", "xyz"], "b": [100000000.1, 1.23456718], } tbl.update("a,b\nxyz,0.00000001\ndef,1234.5678\nefg,100.2") assert view.to_columns() == { "a": ["def", "efg", "xyz"], "b": [1234.5678, 100.2, 0.00000001], } def test_update_append(self): tbl = Table([{"a": "abc", "b": 123}]) tbl.update([{"a": "def", "b": 456}]) assert tbl.view().to_records() == [ {"a": "abc", "b": 123}, {"a": "def", "b": 456}, ] def test_update_partial(self): tbl = Table([{"a": "abc", "b": 123}], index="a") tbl.update([{"a": "abc", "b": 456}]) assert tbl.view().to_records() == [{"a": "abc", "b": 456}] def test_update_partial_add_row(self): tbl = Table([{"a": "abc", "b": 123}], index="a") tbl.update([{"a": "abc", "b": 456}, {"a": "def"}]) assert tbl.view().to_records() == [ {"a": "abc", "b": 456}, {"a": "def", "b": None}, ] def test_update_partial_noop(self): tbl = Table([{"a": "abc", "b": 1, "c": 2}], index="a") tbl.update([{"a": "abc", "b": 456}]) assert tbl.view().to_records() == [{"a": "abc", "b": 456, "c": 2}] def test_update_partial_unset(self): tbl = Table( [{"a": "abc", "b": 1, "c": 2}, {"a": "def", "b": 3, "c": 4}], index="a" ) tbl.update([{"a": "abc"}, {"a": "def", "c": None}]) assert tbl.view().to_records() == [ {"a": "abc", "b": 1, "c": 2}, {"a": "def", "b": 3, "c": None}, ] def test_update_columnar_partial_add_row(self): tbl = Table([{"a": "abc", "b": 123}], index="a") tbl.update({"a": ["abc", "def"], "b": [456, None]}) assert tbl.view().to_records() == [ {"a": "abc", "b": 456}, {"a": "def", "b": None}, ] def test_update_columnar_partial_noop(self): tbl = Table([{"a": "abc", "b": 1, "c": 2}], index="a") # no-op because "c" is not in the update dataset tbl.update({"a": ["abc"], "b": [456]}) assert tbl.view().to_records() == [{"a": "abc", "b": 456, "c": 2}] def test_update_columnar_partial_unset(self): tbl = Table( [{"a": "abc", "b": 1, "c": 2}, {"a": "def", "b": 3, "c": 4}], index="a" ) tbl.update({"a": ["abc"], "b": [None]}) assert tbl.view().to_records() == [ {"a": "abc", "b": None, "c": 2}, {"a": "def", "b": 3, "c": 4}, ] def test_update_partial_subcolumn(self): tbl = Table([{"a": "abc", "b": 123, "c": 456}], index="a") tbl.update([{"a": "abc", "c": 1234}]) assert tbl.view().to_records() == [{"a": "abc", "b": 123, "c": 1234}] def test_update_partial_subcolumn_dict(self): tbl = Table([{"a": "abc", "b": 123, "c": 456}], index="a") tbl.update({"a": ["abc"], "c": [1234]}) assert tbl.view().to_records() == [{"a": "abc", "b": 123, "c": 1234}] def test_update_partial_cols_more_columns_than_table(self): tbl = Table([{"a": "abc", "b": 123}], index="a") tbl.update([{"a": "abc", "b": 456, "c": 789}]) assert tbl.view().to_records() == [{"a": "abc", "b": 456}] def test_update_columnar_append(self): tbl = Table({"a": ["abc"], "b": [123]}) tbl.update({"a": ["def"], "b": [456]}) assert tbl.view().to_records() == [ {"a": "abc", "b": 123}, {"a": "def", "b": 456}, ] def test_update_columnar_partial(self): tbl = Table({"a": ["abc"], "b": [123]}, index="a") tbl.update({"a": ["abc"], "b": [456]}) assert tbl.view().to_records() == [{"a": "abc", "b": 456}] # make sure already created views are notified properly def test_update_from_schema_notify(self): tbl = Table({"a": "string", "b": "integer"}) view = tbl.view() assert view.num_rows() == 0 tbl.update([{"a": "abc", "b": 123}]) assert view.to_records() == [{"a": "abc", "b": 123}] def test_update_columnar_from_schema_notify(self): tbl = Table({"a": "string", "b": "integer"}) view = tbl.view() assert view.num_rows() == 0 tbl.update({"a": ["abc"], "b": [123]}) assert view.to_records() == [{"a": "abc", "b": 123}] def test_update_append_notify(self): tbl = Table([{"a": "abc", "b": 123}]) view = tbl.view() assert view.num_rows() == 1 tbl.update([{"a": "def", "b": 456}]) assert view.to_records() == [{"a": "abc", "b": 123}, {"a": "def", "b": 456}] def test_update_partial_notify(self): tbl = Table([{"a": "abc", "b": 123}], index="a") view = tbl.view() assert view.num_rows() == 1 tbl.update([{"a": "abc", "b": 456}]) assert view.to_records() == [{"a": "abc", "b": 456}] def test_update_partial_cols_not_in_schema_notify(self): tbl = Table([{"a": "abc", "b": 123}], index="a") view = tbl.view() assert view.num_rows() == 1 tbl.update([{"a": "abc", "b": 456, "c": 789}]) assert view.to_records() == [{"a": "abc", "b": 456}] def test_update_columnar_append_notify(self): tbl = Table({"a": ["abc"], "b": [123]}) view = tbl.view() assert view.num_rows() == 1 tbl.update({"a": ["def"], "b": [456]}) assert view.to_records() == [{"a": "abc", "b": 123}, {"a": "def", "b": 456}] def test_update_columnar_partial_notify(self): tbl = Table({"a": ["abc"], "b": [123]}, index="a") view = tbl.view() assert view.num_rows() == 1 tbl.update({"a": ["abc"], "b": [456]}) assert view.to_records() == [{"a": "abc", "b": 456}] # bool def test_update_bool_from_schema(self): bool_data = [{"a": True, "b": False}, {"a": True, "b": True}] tbl = Table({"a": "boolean", "b": "boolean"}) tbl.update(bool_data) assert tbl.size() == 2 assert tbl.view().to_records() == bool_data def test_update_bool_str_from_schema(self): bool_data = [{"a": "True", "b": "False"}, {"a": "True", "b": "True"}] tbl = Table({"a": "boolean", "b": "boolean"}) tbl.update(bool_data) assert tbl.size() == 2 assert tbl.view().to_records() == [ {"a": True, "b": False}, {"a": True, "b": True}, ] @mark.skip(reason="We may not want to support this") def test_update_bool_str_all_formats_from_schema(self): bool_data = [ {"a": "True", "b": "False"}, {"a": "t", "b": "f"}, {"a": "true", "b": "false"}, {"a": 1, "b": 0}, {"a": "on", "b": "off"}, ] tbl = Table({"a": "boolean", "b": "boolean"}) tbl.update(bool_data) assert tbl.size() == 5 assert tbl.view().to_columns() == { "a": [True, True, True, True, True], "b": [False, False, False, False, False], } def test_update_bool_int_from_schema(self): bool_data = [{"a": 1, "b": 0}, {"a": 1, "b": 0}] tbl = Table({"a": "boolean", "b": "boolean"}) tbl.update(bool_data) assert tbl.size() == 2 assert tbl.view().to_columns() == {"a": [True, True], "b": [False, False]} # dates and datetimes def test_update_date(self, util): tbl = Table({"a": [date(2019, 7, 11)]}) tbl.update([{"a": date(2019, 7, 12)}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 11))}, {"a": util.to_timestamp(datetime(2019, 7, 12))}, ] @mark.skip # We do not support numpy. def test_update_date_np(self, util): tbl = Table({"a": [date(2019, 7, 11)]}) tbl.update([{"a": np.datetime64(date(2019, 7, 12))}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 11))}, {"a": util.to_timestamp(datetime(2019, 7, 12))}, ] def test_update_datetime(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": datetime(2019, 7, 12, 11, 0)}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))}, {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}, ] @mark.skip # We do not support numpy. def test_update_datetime_np(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": np.datetime64(datetime(2019, 7, 12, 11, 0))}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))}, {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}, ] @mark.skip # We do not support numpy. def test_update_datetime_np_ts(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": np.datetime64("2019-07-12T11:00")}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))}, {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}, ] def test_update_datetime_timestamp_seconds(self, util): ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0)) tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": ts}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))}, {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}, ] def test_update_datetime_timestamp_ms(self, util): ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0)) tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": ts}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 11, 11, 0))}, {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))}, ] # partial date & datetime updates def test_update_date_partial(self, util): tbl = Table({"a": [date(2019, 7, 11)], "b": [1]}, index="b") tbl.update([{"a": date(2019, 7, 12), "b": 1}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12)), "b": 1} ] @mark.skip # We do not support numpy anymore. def test_update_date_np_partial(self, util): tbl = Table({"a": [date(2019, 7, 11)], "b": [1]}, index="b") tbl.update([{"a": np.datetime64(date(2019, 7, 12)), "b": 1}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12)), "b": 1} ] def test_update_datetime_partial(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "b": [1]}, index="b") tbl.update([{"a": datetime(2019, 7, 12, 11, 0), "b": 1}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "b": 1} ] @mark.skip # We do not support numpy anymore. def test_update_datetime_np_partial(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "b": [1]}, index="b") tbl.update([{"a": np.datetime64(datetime(2019, 7, 12, 11, 0)), "b": 1}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "b": 1} ] @mark.skip # We do not support numpy anymore. def test_update_datetime_np_ts_partial(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "b": [1]}, index="b") tbl.update([{"a": np.datetime64("2019-07-12T11:00"), "b": 1}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "b": 1} ] def test_update_datetime_timestamp_seconds_partial(self, util): ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0)) tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "idx": [1]}, index="idx") tbl.update([{"a": ts, "idx": 1}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "idx": 1} ] def test_update_datetime_timestamp_ms_partial(self, util): ts = util.to_timestamp(datetime(2019, 7, 12, 11, 0, 0)) tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)], "idx": [1]}, index="idx") tbl.update([{"a": ts, "idx": 1}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0)), "idx": 1} ] # updating dates using implicit index def test_update_date_partial_implicit(self, util): tbl = Table({"a": [date(2019, 7, 11)]}) tbl.update([{"a": date(2019, 7, 12), "__INDEX__": 0}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12))} ] @mark.skip # We do not support numpy anymore. def test_update_date_np_partial_implicit(self, util): tbl = Table({"a": [date(2019, 7, 11)]}) tbl.update([{"a": np.datetime64(date(2019, 7, 12)), "__INDEX__": 0}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12))} ] def test_update_datetime_partial_implicit(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": datetime(2019, 7, 12, 11, 0), "__INDEX__": 0}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))} ] @mark.skip # We do not support numpy anymore. def test_update_datetime_np_partial_implicit(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": np.datetime64(datetime(2019, 7, 12, 11, 0)), "__INDEX__": 0}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))} ] @mark.skip # We do not support numpy anymore. def test_update_datetime_np_ts_partial_implicit(self, util): tbl = Table({"a": [datetime(2019, 7, 11, 11, 0)]}) tbl.update([{"a": np.datetime64("2019-07-12T11:00"), "__INDEX__": 0}]) assert tbl.view().to_records() == [ {"a": util.to_timestamp(datetime(2019, 7, 12, 11, 0))} ] # implicit index def test_update_implicit_index(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}] tbl = Table(data) view = tbl.view() tbl.update([{"__INDEX__": 0, "a": 3, "b": 15}]) assert view.to_records() == [{"a": 3, "b": 15}, {"a": 2, "b": 3}] def test_update_implicit_index_dict_noop(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}] tbl = Table(data) view = tbl.view() # "b" does not exist in dataset, so no-op tbl.update( { "__INDEX__": [0], "a": [3], } ) assert view.to_records() == [{"a": 3, "b": 2}, {"a": 2, "b": 3}] def test_update_implicit_index_dict_unset_with_null(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}] tbl = Table(data) view = tbl.view() # unset because "b" is null tbl.update({"__INDEX__": [0], "a": [3], "b": [None]}) assert view.to_records() == [{"a": 3, "b": None}, {"a": 2, "b": 3}] def test_update_implicit_index_multi(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 4, "b": 5}] tbl = Table(data) view = tbl.view() tbl.update( [ { "__INDEX__": 0, "a": 3, }, {"__INDEX__": 2, "a": 5}, ] ) assert view.to_records() == [ {"a": 3, "b": 2}, {"a": 2, "b": 3}, {"a": 5, "b": 5}, ] def test_update_implicit_index_symmetric(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}] tbl = Table(data) view = tbl.view() records = view.to_records(index=True) idx = records[0]["__INDEX__"][0] # XXX: How should we grab this? tbl.update([{"__INDEX__": idx, "a": 3}]) assert view.to_records() == [{"a": 3, "b": 2}, {"a": 2, "b": 3}] def test_update_explicit_index(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}] tbl = Table(data, index="a") view = tbl.view() tbl.update([{"a": 1, "b": 3}]) assert view.to_records() == [{"a": 1, "b": 3}, {"a": 2, "b": 3}] def test_update_explicit_index_multi(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 3, "b": 4}] tbl = Table(data, index="a") view = tbl.view() tbl.update([{"a": 1, "b": 3}, {"a": 3, "b": 5}]) assert view.to_records() == [ {"a": 1, "b": 3}, {"a": 2, "b": 3}, {"a": 3, "b": 5}, ] def test_update_explicit_index_multi_append(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 3, "b": 4}] tbl = Table(data, index="a") view = tbl.view() tbl.update([{"a": 1, "b": 3}, {"a": 12, "b": 5}]) assert view.to_records() == [ {"a": 1, "b": 3}, {"a": 2, "b": 3}, {"a": 3, "b": 4}, {"a": 12, "b": 5}, ] def test_update_explicit_index_multi_append_noindex(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}, {"a": 3, "b": 4}] tbl = Table(data, index="a") view = tbl.view() tbl.update([{"a": 1, "b": 3}, {"b": 5}]) assert view.to_records() == [ {"a": None, "b": 5}, {"a": 1, "b": 3}, {"a": 2, "b": 3}, {"a": 3, "b": 4}, ] def test_update_implicit_index_with_explicit_unset(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}] tbl = Table(data, index="a") view = tbl.view() tbl.update([{"__INDEX__": 1, "b": 3}]) assert view.to_records() == [{"a": 1, "b": 3}, {"a": 2, "b": 3}] def test_update_implicit_index_with_explicit_set(self): data = [{"a": 1, "b": 2}, {"a": 2, "b": 3}] tbl = Table(data, index="a") view = tbl.view() tbl.update( [{"__INDEX__": 1, "a": 1, "b": 3}] ) # should ignore re-specification of pkey assert view.to_records() == [{"a": 1, "b": 3}, {"a": 2, "b": 3}]