diff --git a/docs/index.rst b/docs/index.rst index 27a22c33..530004ff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,3 +41,4 @@ Contents cli-reference contributing changelog + upgrading diff --git a/docs/python-api.rst b/docs/python-api.rst index 267591ac..acc2515e 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -282,11 +282,13 @@ Using this factory function allows you to set :ref:`python_api_table_configurati The ``db.table()`` method will always return a :ref:`reference_db_table` instance, or raise a ``sqlite_utils.db.NoTable`` exception if the table name is actually a SQL view. -You can also access tables or views using dictionary-style syntax, like this: +You can also access tables using dictionary-style syntax, like this: .. code-block:: python - table_or_view = db["my_table_or_view_name"] + table = db["my_table_name"] + +This is equivalent to calling ``db.table("my_table_name")``. It will raise a ``sqlite_utils.db.NoTable`` exception if the name refers to a view rather than a table. If a table accessed using either of these methods does not yet exist, it will be created the first time you attempt to insert or upsert data into it. diff --git a/docs/upgrading.rst b/docs/upgrading.rst new file mode 100644 index 00000000..dd9df51a --- /dev/null +++ b/docs/upgrading.rst @@ -0,0 +1,134 @@ +.. _upgrading: + +===================== + Upgrading to 4.0 +===================== + +sqlite-utils 4.0 includes several breaking changes. This page describes what has changed and how to update your code. + +Python library changes +====================== + +db["name"] only returns tables +------------------------------ + +In previous versions, ``db["table_or_view_name"]`` would return either a :ref:`Table ` or :ref:`View ` object depending on what existed in the database. + +In 4.0, this syntax **only returns Table objects**. Attempting to use it with a view name will raise a ``sqlite_utils.db.NoTable`` exception. + +**Before (3.x):** + +.. code-block:: python + + # This could return either a Table or View + obj = db["my_view"] + obj.drop() + +**After (4.0):** + +.. code-block:: python + + # Use db.view() explicitly for views + view = db.view("my_view") + view.drop() + + # db["name"] now only works with tables + table = db["my_table"] + +This change improves type safety since views lack methods like ``.insert()`` that are available on tables. + +db.table() raises NoTable for views +----------------------------------- + +The ``db.table(name)`` method now raises ``sqlite_utils.db.NoTable`` if the name refers to a view. Use ``db.view(name)`` instead. + +Default floating point type is REAL +----------------------------------- + +When inserting data with auto-detected column types, floating point values now create columns with type ``REAL`` instead of ``FLOAT``. ``REAL`` is the correct SQLite affinity for floating point values. + +This affects the schema of newly created tables but does not change how data is stored or queried. + +convert() no longer skips False values +-------------------------------------- + +The ``table.convert()`` method previously skipped rows where the column value evaluated to ``False`` (including ``0``, empty strings, and ``None``). This behavior has been removed. + +**Before (3.x):** + +.. code-block:: python + + # Rows with falsey values were skipped by default + # --skip-false was needed to process all rows + table.convert("column", lambda x: x.upper(), skip_false=False) + +**After (4.0):** + +.. code-block:: python + + # All rows are now processed, including those with falsey values + table.convert("column", lambda x: x.upper() if x else x) + +Table schemas use double quotes +------------------------------- + +Tables created by sqlite-utils now use ``"double-quotes"`` for table and column names in the schema instead of ``[square-braces]``. Both are valid SQL, but double quotes are the SQL standard. + +This only affects how the schema is written. Existing tables are not modified. + +Upsert uses modern SQLite syntax +-------------------------------- + +Upsert operations now use SQLite's ``INSERT ... ON CONFLICT SET`` syntax on SQLite versions 3.24.0 and later. The previous implementation used ``INSERT OR IGNORE`` followed by ``UPDATE``. + +To use the old behavior, pass ``use_old_upsert=True`` to the ``Database()`` constructor: + +.. code-block:: python + + db = Database("my.db", use_old_upsert=True) + +CLI changes +=========== + +Type detection is now the default +--------------------------------- + +When importing CSV or TSV data with the ``insert`` or ``upsert`` commands, sqlite-utils now automatically detects column types. Previously all columns were treated as ``TEXT`` unless ``--detect-types`` was passed. + +**Before (3.x):** + +.. code-block:: bash + + # Types were detected only with --detect-types + sqlite-utils insert data.db mytable data.csv --csv --detect-types + +**After (4.0):** + +.. code-block:: bash + + # Types are detected by default + sqlite-utils insert data.db mytable data.csv --csv + + # Use --no-detect-types to treat all columns as TEXT + sqlite-utils insert data.db mytable data.csv --csv --no-detect-types + +The ``SQLITE_UTILS_DETECT_TYPES`` environment variable has been removed. + +convert --skip-false removed +---------------------------- + +The ``--skip-false`` option for ``sqlite-utils convert`` has been removed. All rows are now processed regardless of whether the column value is falsey. + +sqlite-utils tui is now a plugin +-------------------------------- + +The ``sqlite-utils tui`` command has been moved to a separate plugin. Install it with: + +.. code-block:: bash + + sqlite-utils install sqlite-utils-tui + +Python version requirements +=========================== + +sqlite-utils 4.0 requires Python 3.10 or higher. Python 3.8 and 3.9 are no longer supported. diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index 9b9ee20e..65deb303 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -12,6 +12,7 @@ BadMultiValues, DescIndex, NoTable, + NoView, quote_identifier, ) from sqlite_utils.plugins import pm, get_plugins @@ -1796,9 +1797,10 @@ def drop_view(path, view, ignore, load_extension): _register_db_for_cleanup(db) _load_extensions(db, load_extension) try: - db[view].drop(ignore=ignore) - except OperationalError: - raise click.ClickException('View "{}" does not exist'.format(view)) + db.view(view).drop(ignore=ignore) + except NoView: + if not ignore: + raise click.ClickException('View "{}" does not exist'.format(view)) @cli.command() diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index aacdc893..b0ee014b 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -445,15 +445,15 @@ def tracer( finally: self._tracer = prev_tracer - def __getitem__(self, table_name: str) -> Union["Table", "View"]: + def __getitem__(self, table_name: str) -> "Table": """ ``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. + Use ``db.view(view_name)`` to access views. + :param table_name: The name of the table """ - if table_name in self.view_names(): - return self.view(table_name) return self.table(table_name) def __repr__(self) -> str: @@ -1206,9 +1206,9 @@ def create_view( return self elif replace: # If SQL is the same, do nothing - if create_sql == self[name].schema: + if create_sql == self.view(name).schema: return self - self[name].drop() + self.view(name).drop() self.execute(create_sql) return self diff --git a/tests/test_cli.py b/tests/test_cli.py index 40c3595c..eb7ee30f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1367,7 +1367,8 @@ def test_create_view(): ) assert result.exit_code == 0 assert ( - 'CREATE VIEW "version" AS select sqlite_version()' == db["version"].schema + 'CREATE VIEW "version" AS select sqlite_version()' + == db.view("version").schema ) @@ -1404,7 +1405,7 @@ def test_create_view_ignore(): assert result.exit_code == 0 assert ( 'CREATE VIEW "version" AS select sqlite_version() + 1' - == db["version"].schema + == db.view("version").schema ) @@ -1425,7 +1426,8 @@ def test_create_view_replace(): ) assert result.exit_code == 0 assert ( - 'CREATE VIEW "version" AS select sqlite_version()' == db["version"].schema + 'CREATE VIEW "version" AS select sqlite_version()' + == db.view("version").schema ) diff --git a/tests/test_create.py b/tests/test_create.py index ea09c459..18292164 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -1082,7 +1082,7 @@ def test_drop(fresh_db): def test_drop_view(fresh_db): fresh_db.create_view("foo_view", "select 1") assert ["foo_view"] == fresh_db.view_names() - assert None is fresh_db["foo_view"].drop() + assert None is fresh_db.view("foo_view").drop() assert [] == fresh_db.view_names() @@ -1093,7 +1093,7 @@ def test_drop_ignore(fresh_db): # Testing view is harder, we need to create it in order # to get a View object, then drop it twice fresh_db.create_view("foo_view", "select 1") - view = fresh_db["foo_view"] + view = fresh_db.view("foo_view") assert isinstance(view, View) view.drop() with pytest.raises(sqlite3.OperationalError): diff --git a/tests/test_fts.py b/tests/test_fts.py index 9c635ffa..b80af6ac 100644 --- a/tests/test_fts.py +++ b/tests/test_fts.py @@ -424,7 +424,7 @@ def test_enable_fts_error_message_on_views(): db = Database(memory=True) db.create_view("hello", "select 1 + 1") with pytest.raises(NotImplementedError) as e: - db["hello"].enable_fts() # type: ignore[call-arg] + db.view("hello").enable_fts() # type: ignore[call-arg] assert e.value.args[0] == "enable_fts() is supported on tables but not on views" diff --git a/tests/test_tracer.py b/tests/test_tracer.py index ac490c58..ab55e8c2 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -15,7 +15,6 @@ def test_tracer(): ("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'table'", None), ("select name from sqlite_master where type = 'view'", None), - ("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'table'", None), ("select name from sqlite_master where type = 'view'", None), ('CREATE TABLE "dogs" (\n "name" TEXT\n);\n ', None),