Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ Contents
cli-reference
contributing
changelog
upgrading
6 changes: 4 additions & 2 deletions docs/python-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
134 changes: 134 additions & 0 deletions docs/upgrading.rst
Original file line number Diff line number Diff line change
@@ -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 <reference_db_table>` or :ref:`View <reference_db_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.
8 changes: 5 additions & 3 deletions sqlite_utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BadMultiValues,
DescIndex,
NoTable,
NoView,
quote_identifier,
)
from sqlite_utils.plugins import pm, get_plugins
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 5 additions & 5 deletions sqlite_utils/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down Expand Up @@ -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
)


Expand All @@ -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
)


Expand Down
4 changes: 2 additions & 2 deletions tests/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_fts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down
1 change: 0 additions & 1 deletion tests/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading