diff --git a/pyproject.toml b/pyproject.toml index 2c86f85..4ad1bd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,8 @@ CI = "https://github.com/simonw/sqlite-utils/actions" sqlite-utils = "sqlite_utils.cli:cli" [build-system] -requires = ["setuptools"] +# setuptools 77+ is needed for the PEP 639 license = "Apache-2.0" expression +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [tool.flake8] diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 1c94806..f2e1ba6 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1155,6 +1155,8 @@ def insert_upsert_implementation( db.table(table).insert_all( docs, pk=pk, batch_size=batch_size, alter=alter, **extra_kwargs ) + except NoTable as e: + raise click.ClickException(str(e)) except Exception as e: if ( isinstance(e, OperationalError) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 97e8920..b53df51 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -499,10 +499,12 @@ class Database: def __getitem__(self, table_name: str) -> Union["Table", "View"]: """ - ``db[table_name]`` returns a :class:`.Table` object for the table with the specified name. - If the table does not exist yet it will be created the first time data is inserted into it. + ``db[name]`` returns a :class:`.Table` object for the table with the specified name, + or a :class:`.View` object if the name matches an existing SQL view. + If neither exists yet, a table is assumed - it will be created the first + time data is inserted into it. - :param table_name: The name of the table + :param table_name: The name of the table or view """ if table_name in self.view_names(): return self.view(table_name) @@ -672,6 +674,12 @@ class Database: :param view_name: Name of the view """ if view_name not in self.view_names(): + if view_name in self.table_names(): + raise NoView( + "View {name} does not exist - {name} is a table".format( + name=view_name + ) + ) raise NoView("View {} does not exist".format(view_name)) return View(self, view_name) @@ -1618,7 +1626,8 @@ class Table(Queryable): :param not_null: List of columns that cannot be null :param defaults: Dictionary of column names and default values :param batch_size: Integer number of rows to insert at a time - :param hash_id: If True, use a hash of the row values as the primary key + :param hash_id: Name of a column to create and use as a primary key, where the + value of that primary key is derived from a hash of the row values :param hash_id_columns: List of columns to use for the hash_id :param alter: If True, automatically alter the table if it doesn't match the schema :param ignore: If True, ignore rows that already exist when inserting diff --git a/sqlite_utils/migrations.py b/sqlite_utils/migrations.py index 7ae72e6..36e053d 100644 --- a/sqlite_utils/migrations.py +++ b/sqlite_utils/migrations.py @@ -44,8 +44,15 @@ class Migrations: """ def inner(func: Callable) -> Callable: + migration_name = name or getattr(func, "__name__") + if any(m.name == migration_name for m in self._migrations): + raise ValueError( + "Migration '{}' is already registered in set '{}'".format( + migration_name, self.name + ) + ) self._migrations.append( - self._Migration(name or getattr(func, "__name__"), func, transactional) + self._Migration(migration_name, func, transactional) ) return func diff --git a/tests/test_cli_insert.py b/tests/test_cli_insert.py index 5812851..2df1e0c 100644 --- a/tests/test_cli_insert.py +++ b/tests/test_cli_insert.py @@ -615,3 +615,16 @@ def test_insert_csv_headers_only(tmpdir): # Table should not exist since there were no data rows db = Database(db_path) assert not db["data"].exists() + + +def test_insert_into_view_errors(tmpdir): + db_path = str(tmpdir / "test.db") + db = Database(db_path) + db["t"].insert({"id": 1}) + db.create_view("v", "select * from t") + db.close() + result = CliRunner().invoke( + cli.cli, ["insert", db_path, "v", "-"], input='{"id": 2}' + ) + assert result.exit_code == 1 + assert result.output.strip() == "Error: Table v is actually a view" diff --git a/tests/test_create.py b/tests/test_create.py index b1a6ad1..afa7a65 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -1382,7 +1382,10 @@ def test_bad_table_and_view_exceptions(fresh_db): assert ex.value.args[0] == "Table v is actually a view" with pytest.raises(NoView) as ex2: fresh_db.view("t") - assert ex2.value.args[0] == "View t does not exist" + assert ex2.value.args[0] == "View t does not exist - t is a table" + with pytest.raises(NoView) as ex3: + fresh_db.view("missing") + assert ex3.value.args[0] == "View missing does not exist" # Tests for issue #655: Table configuration should be stored in _defaults diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 1733aa4..5634d79 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -198,3 +198,19 @@ def test_pending_and_applied_are_read_only(migrations): assert migrations.applied(db) == [] # Neither call should have created the tracking table assert db.table_names() == [] + + +def test_duplicate_migration_name_errors(): + migrations = Migrations("test") + + @migrations() + def m001(db): + pass + + with pytest.raises(ValueError) as ex: + + @migrations(name="m001") + def m001_again(db): + pass + + assert "m001" in str(ex.value)