| ################################################################################ |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| ################################################################################ |
| |
| """Unit tests for ``pypaimon.common.merge_engine_dispatch``. |
| |
| Pins down the table-option parsing the dispatch uses to decide whether |
| ``partial-update`` should run or be rejected. The key contract: strict |
| ``"true"``-only boolean parsing aligned with the table-option parser |
| used elsewhere in Paimon, so an option string the rest of the |
| toolchain treats as ``false`` is not silently elevated to ``true`` |
| here. |
| """ |
| |
| import unittest |
| |
| from pypaimon.common.merge_engine_dispatch import ( |
| _option_is_truthy, |
| build_merge_function, |
| partial_update_unsupported_options, |
| ) |
| from pypaimon.common.options.core_options import MergeEngine |
| from pypaimon.read.reader.partial_update_merge_function import \ |
| PartialUpdateMergeFunction |
| |
| |
| class OptionIsTruthyTest(unittest.TestCase): |
| """``_option_is_truthy`` accepts only ``"true"`` (case-insensitive) |
| as truthy; every other string -- including ``"1"``, ``"yes"``, |
| ``"on"`` -- is falsey. Matches the table-option parser used |
| elsewhere in Paimon. |
| """ |
| |
| def test_true_string_is_truthy(self): |
| self.assertTrue(_option_is_truthy("true")) |
| |
| def test_true_string_is_case_insensitive(self): |
| for v in ("TRUE", "True", "tRuE"): |
| self.assertTrue(_option_is_truthy(v), v) |
| |
| def test_true_string_tolerates_surrounding_whitespace(self): |
| self.assertTrue(_option_is_truthy(" true ")) |
| |
| def test_python_bool_true_is_truthy(self): |
| self.assertTrue(_option_is_truthy(True)) |
| |
| def test_python_bool_false_is_falsey(self): |
| self.assertFalse(_option_is_truthy(False)) |
| |
| def test_none_is_falsey(self): |
| self.assertFalse(_option_is_truthy(None)) |
| |
| def test_non_true_strings_are_falsey(self): |
| # The table-option parser elsewhere in Paimon returns false |
| # for every one of these. pypaimon must do the same so a |
| # user-set "yes" is not silently elevated to true here while |
| # the rest of the toolchain treats it as false. |
| for v in ("1", "yes", "on", "Yes", "ON", "y", "t", "0", "no", "off", |
| "false", "FALSE", ""): |
| self.assertFalse(_option_is_truthy(v), v) |
| |
| |
| class PartialUpdateUnsupportedOptionsTest(unittest.TestCase): |
| |
| def test_ignore_delete_yes_is_not_flagged(self): |
| # ``yes`` is falsey under the upstream table-option parser, |
| # so partial-update must NOT be blocked here. Pre-fix pypaimon |
| # rejected this; the fix aligns the dispatch with the parser. |
| unsupported = partial_update_unsupported_options( |
| {"partial-update.ignore-delete": "yes"}) |
| self.assertEqual(unsupported, set()) |
| |
| def test_ignore_delete_true_is_flagged(self): |
| unsupported = partial_update_unsupported_options( |
| {"partial-update.ignore-delete": "true"}) |
| self.assertEqual(unsupported, {"partial-update.ignore-delete"}) |
| |
| def test_sequence_group_is_flagged(self): |
| unsupported = partial_update_unsupported_options( |
| {"fields.a.sequence-group": "b"}) |
| self.assertEqual(unsupported, {"fields.a.sequence-group"}) |
| |
| def test_unrelated_options_are_not_flagged(self): |
| unsupported = partial_update_unsupported_options( |
| {"bucket": "1", "merge-engine": "partial-update"}) |
| self.assertEqual(unsupported, set()) |
| |
| |
| class BuildMergeFunctionTest(unittest.TestCase): |
| """``build_merge_function`` forwards ``value_field_names`` to |
| ``PartialUpdateMergeFunction`` so the NOT-NULL error message can |
| surface the offending column name. This is the only behavioural |
| contract the dispatch adds on top of routing. |
| """ |
| |
| def test_partial_update_forwards_field_names(self): |
| mf = build_merge_function( |
| engine=MergeEngine.PARTIAL_UPDATE, |
| raw_options={}, |
| key_arity=1, |
| value_arity=2, |
| value_field_nullables=[True, True], |
| value_field_names=['col_a', 'col_b'], |
| ) |
| self.assertIsInstance(mf, PartialUpdateMergeFunction) |
| self.assertEqual(mf._value_field_names, ['col_a', 'col_b']) |
| |
| def test_partial_update_without_field_names_keeps_none(self): |
| mf = build_merge_function( |
| engine=MergeEngine.PARTIAL_UPDATE, |
| raw_options={}, |
| key_arity=1, |
| value_arity=2, |
| value_field_nullables=[True, True], |
| ) |
| self.assertIsInstance(mf, PartialUpdateMergeFunction) |
| self.assertIsNone(mf._value_field_names) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |