diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 9b9ee20e..1add5aa4 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1176,7 +1176,7 @@ def insert_upsert_implementation( ) else: raise - if tracker is not None: + if tracker is not None and db.table(table).exists(): db.table(table).transform(types=tracker.types) # Clean up open file-like objects @@ -2033,7 +2033,7 @@ def memory( rows = (_flatten(row) for row in rows) db.table(file_table).insert_all(rows, alter=True) - if tracker is not None: + if tracker is not None and db.table(file_table).exists(): db.table(file_table).transform(types=tracker.types) # Add convenient t / t1 / t2 views view_names = ["t{}".format(i + 1)] diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index aacdc893..ee2613fc 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -40,6 +40,7 @@ Tuple, ) import uuid +import warnings from sqlite_utils.plugins import pm try: @@ -401,13 +402,16 @@ def close(self) -> None: self.conn.close() @contextlib.contextmanager - def ensure_autocommit_off(self) -> Generator[None, None, None]: + def ensure_autocommit_on(self) -> Generator[None, None, None]: """ - Ensure autocommit is off for this database connection. + Ensure autocommit is on for this database connection. + + In SQLite's Python module, ``isolation_level = None`` enables autocommit mode, + where each statement is committed immediately. Example usage:: - with db.ensure_autocommit_off(): + with db.ensure_autocommit_on(): # do stuff here This will reset to the previous autocommit state at the end of the block. @@ -419,6 +423,21 @@ def ensure_autocommit_off(self) -> Generator[None, None, None]: finally: self.conn.isolation_level = old_isolation_level + @contextlib.contextmanager + def ensure_autocommit_off(self) -> Generator[None, None, None]: + """ + Deprecated alias for :meth:`ensure_autocommit_on`. + + This method name is confusing - ``isolation_level = None`` actually enables + autocommit mode in SQLite, not disables it. Use :meth:`ensure_autocommit_on` instead. + """ + warnings.warn( + "ensure_autocommit_off() is deprecated, use ensure_autocommit_on() instead", + DeprecationWarning, + ) + with self.ensure_autocommit_on(): + yield + @contextlib.contextmanager def tracer( self, tracer: Optional[Callable[[str, Optional[Sequence]], None]] = None @@ -781,13 +800,13 @@ def enable_wal(self) -> None: Sets ``journal_mode`` to ``'wal'`` to enable Write-Ahead Log mode. """ if self.journal_mode != "wal": - with self.ensure_autocommit_off(): + with self.ensure_autocommit_on(): self.execute("PRAGMA journal_mode=wal;") def disable_wal(self) -> None: "Sets ``journal_mode`` back to ``'delete'`` to disable Write-Ahead Log mode." if self.journal_mode != "delete": - with self.ensure_autocommit_off(): + with self.ensure_autocommit_on(): self.execute("PRAGMA journal_mode=delete;") def _ensure_counts_table(self) -> None: diff --git a/tests/test_cli_insert.py b/tests/test_cli_insert.py index 9f21b001..58128516 100644 --- a/tests/test_cli_insert.py +++ b/tests/test_cli_insert.py @@ -597,3 +597,21 @@ def try_until(expected): proc.stdin.close() proc.wait() assert proc.returncode == 0 + + +def test_insert_csv_headers_only(tmpdir): + """Test that CSV with only header row (no data) works with --detect-types (issue #702)""" + db_path = str(tmpdir / "test.db") + csv_path = str(tmpdir / "headers_only.csv") + with open(csv_path, "w") as fp: + fp.write("id,name,age\n") + # Should not crash with --detect-types (which is now the default) + result = CliRunner().invoke( + cli.cli, + ["insert", db_path, "data", csv_path, "--csv"], + catch_exceptions=False, + ) + assert result.exit_code == 0 + # Table should not exist since there were no data rows + db = Database(db_path) + assert not db["data"].exists()