| # 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. | |
| import json | |
| import os | |
| import shutil | |
| import tempfile | |
| import unittest | |
| from io import StringIO | |
| from unittest.mock import patch | |
| import pyarrow as pa | |
| from pypaimon import CatalogFactory, Schema | |
| from pypaimon.cli.cli import main | |
| class CliDbTest(unittest.TestCase): | |
| """Integration tests for CLI database commands with real catalog operations.""" | |
| @classmethod | |
| def setUpClass(cls): | |
| """Set up test catalog and configuration.""" | |
| cls.tempdir = tempfile.mkdtemp() | |
| cls.warehouse = os.path.join(cls.tempdir, 'warehouse') | |
| # Create catalog | |
| cls.catalog = CatalogFactory.create({ | |
| 'warehouse': cls.warehouse | |
| }) | |
| # Create catalog config file | |
| cls.config_file = os.path.join(cls.tempdir, 'paimon.yaml') | |
| with open(cls.config_file, 'w') as f: | |
| f.write(f"metastore: filesystem\nwarehouse: {cls.warehouse}\n") | |
| @classmethod | |
| def tearDownClass(cls): | |
| """Clean up temporary directory.""" | |
| shutil.rmtree(cls.tempdir, ignore_errors=True) | |
| def test_cli_db_create_basic(self): | |
| """Test basic database create via CLI.""" | |
| with patch('sys.argv', ['paimon', '-c', self.config_file, 'db', 'create', 'new_db']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('created successfully', output) | |
| # Verify database was created | |
| database = self.catalog.get_database('new_db') | |
| self.assertEqual(database.name, 'new_db') | |
| def test_cli_db_create_ignore_if_exists(self): | |
| """Test database create with ignore-if-exists flag.""" | |
| # Create database first | |
| self.catalog.create_database('existing_db', True) | |
| # Try to create again with ignore-if-exists flag | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'create', 'existing_db', '-i']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('created successfully', output) | |
| def test_cli_db_create_already_exists_error(self): | |
| """Test database create raises error when database already exists.""" | |
| self.catalog.create_database('dup_db', True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'create', 'dup_db']): | |
| with patch('sys.stderr', new_callable=StringIO) as mock_stderr: | |
| with self.assertRaises(SystemExit) as context: | |
| main() | |
| self.assertEqual(context.exception.code, 1) | |
| error_output = mock_stderr.getvalue() | |
| self.assertIn('Error', error_output) | |
| def test_cli_db_get_basic(self): | |
| """Test basic database get via CLI.""" | |
| self.catalog.create_database('get_db', True) | |
| with patch('sys.argv', ['paimon', '-c', self.config_file, 'db', 'get', 'get_db']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| # Verify output is valid JSON | |
| result = json.loads(output) | |
| self.assertEqual(result['name'], 'get_db') | |
| self.assertIn('options', result) | |
| def test_cli_db_get_nonexistent(self): | |
| """Test CLI error handling for nonexistent database.""" | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'get', 'nonexistent_db']): | |
| with patch('sys.stderr', new_callable=StringIO) as mock_stderr: | |
| with self.assertRaises(SystemExit) as context: | |
| main() | |
| self.assertEqual(context.exception.code, 1) | |
| error_output = mock_stderr.getvalue() | |
| self.assertIn('Error', error_output) | |
| def test_cli_db_drop_basic(self): | |
| """Test basic database drop via CLI.""" | |
| self.catalog.create_database('drop_db', True) | |
| with patch('sys.argv', ['paimon', '-c', self.config_file, 'db', 'drop', 'drop_db']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('dropped successfully', output) | |
| # Verify database was dropped | |
| from pypaimon.catalog.catalog_exception import DatabaseNotExistException | |
| with self.assertRaises(DatabaseNotExistException): | |
| self.catalog.get_database('drop_db') | |
| def test_cli_db_drop_nonexistent_error(self): | |
| """Test CLI error handling for dropping nonexistent database.""" | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'drop', 'nonexistent_drop_db']): | |
| with patch('sys.stderr', new_callable=StringIO) as mock_stderr: | |
| with self.assertRaises(SystemExit) as context: | |
| main() | |
| self.assertEqual(context.exception.code, 1) | |
| error_output = mock_stderr.getvalue() | |
| self.assertIn('Error', error_output) | |
| def test_cli_db_drop_ignore_if_not_exists(self): | |
| """Test database drop with ignore-if-not-exists flag.""" | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'drop', | |
| 'nonexistent_ignore_db', '-i']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('dropped successfully', output) | |
| def test_cli_db_drop_cascade(self): | |
| """Test database drop with cascade flag.""" | |
| # Create database with a table | |
| self.catalog.create_database('cascade_db', True) | |
| pa_schema = pa.schema([ | |
| ('id', pa.int32()), | |
| ('name', pa.string()) | |
| ]) | |
| schema = Schema.from_pyarrow_schema(pa_schema) | |
| self.catalog.create_table('cascade_db.test_table', schema, False) | |
| # Drop with cascade | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'drop', 'cascade_db', '--cascade']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('dropped successfully', output) | |
| # Verify database was dropped | |
| from pypaimon.catalog.catalog_exception import DatabaseNotExistException | |
| with self.assertRaises(DatabaseNotExistException): | |
| self.catalog.get_database('cascade_db') | |
| def test_cli_db_list_tables_basic(self): | |
| """Test basic list-tables via CLI.""" | |
| # Create database with tables | |
| self.catalog.create_database('list_db', True) | |
| pa_schema = pa.schema([ | |
| ('id', pa.int32()), | |
| ('value', pa.string()) | |
| ]) | |
| schema = Schema.from_pyarrow_schema(pa_schema) | |
| self.catalog.create_table('list_db.table_a', schema, True) | |
| self.catalog.create_table('list_db.table_b', schema, True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'list-tables', 'list_db']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('table_a', output) | |
| self.assertIn('table_b', output) | |
| def test_cli_db_list_tables_empty(self): | |
| """Test list-tables for empty database.""" | |
| self.catalog.create_database('empty_list_db', True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'list-tables', 'empty_list_db']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('No tables found', output) | |
| def test_cli_db_list_tables_nonexistent_db(self): | |
| """Test list-tables for nonexistent database.""" | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'list-tables', 'nonexistent_list_db']): | |
| with patch('sys.stderr', new_callable=StringIO) as mock_stderr: | |
| with self.assertRaises(SystemExit) as context: | |
| main() | |
| self.assertEqual(context.exception.code, 1) | |
| error_output = mock_stderr.getvalue() | |
| self.assertIn('Error', error_output) | |
| def test_cli_db_alter_not_supported(self): | |
| """Test db alter on filesystem catalog raises not supported error.""" | |
| self.catalog.create_database('alter_db', True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'alter', 'alter_db', | |
| '--set', '{"key1": "value1"}']): | |
| with patch('sys.stderr', new_callable=StringIO) as mock_stderr: | |
| with self.assertRaises(SystemExit) as context: | |
| main() | |
| self.assertEqual(context.exception.code, 1) | |
| error_output = mock_stderr.getvalue() | |
| self.assertIn('not supported', error_output) | |
| def test_cli_db_alter_no_changes(self): | |
| """Test db alter with no changes specified.""" | |
| self.catalog.create_database('alter_no_change_db', True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'alter', 'alter_no_change_db']): | |
| with patch('sys.stderr', new_callable=StringIO) as mock_stderr: | |
| with self.assertRaises(SystemExit) as context: | |
| main() | |
| self.assertEqual(context.exception.code, 1) | |
| error_output = mock_stderr.getvalue() | |
| self.assertIn('No changes specified', error_output) | |
| def test_cli_db_alter_invalid_json(self): | |
| """Test db alter with invalid JSON for --set.""" | |
| self.catalog.create_database('alter_invalid_db', True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'db', 'alter', 'alter_invalid_db', | |
| '--set', 'not_valid_json']): | |
| with patch('sys.stderr', new_callable=StringIO) as mock_stderr: | |
| with self.assertRaises(SystemExit) as context: | |
| main() | |
| self.assertEqual(context.exception.code, 1) | |
| error_output = mock_stderr.getvalue() | |
| self.assertIn('Invalid JSON', error_output) | |
| def test_cli_catalog_list_dbs_basic(self): | |
| """Test basic list databases via catalog CLI.""" | |
| self.catalog.create_database('list_db_a', True) | |
| self.catalog.create_database('list_db_b', True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'catalog', 'list-dbs']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('list_db_a', output) | |
| self.assertIn('list_db_b', output) | |
| def test_cli_catalog_list_dbs_excludes_nonexistent(self): | |
| """Test list databases does not include non-existent databases.""" | |
| self.catalog.create_database('list_exists_db', True) | |
| with patch('sys.argv', | |
| ['paimon', '-c', self.config_file, 'catalog', 'list-dbs']): | |
| with patch('sys.stdout', new_callable=StringIO) as mock_stdout: | |
| try: | |
| main() | |
| except SystemExit: | |
| pass | |
| output = mock_stdout.getvalue() | |
| self.assertIn('list_exists_db', output) | |
| self.assertNotIn('never_created_db', output) | |
| if __name__ == '__main__': | |
| unittest.main() |