mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-25 10:24:32 +02:00
Compare commits
3 commits
main
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d584f0c60 |
||
|
|
147fd7e48b |
||
|
|
c791a31047 |
9 changed files with 157 additions and 17 deletions
|
|
@ -41,3 +41,4 @@ Contents
|
||||||
cli-reference
|
cli-reference
|
||||||
contributing
|
contributing
|
||||||
changelog
|
changelog
|
||||||
|
upgrading
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
.. 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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
134
docs/upgrading.rst
Normal file
134
docs/upgrading.rst
Normal file
|
|
@ -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.
|
||||||
|
|
@ -12,6 +12,7 @@ from sqlite_utils.db import (
|
||||||
BadMultiValues,
|
BadMultiValues,
|
||||||
DescIndex,
|
DescIndex,
|
||||||
NoTable,
|
NoTable,
|
||||||
|
NoView,
|
||||||
quote_identifier,
|
quote_identifier,
|
||||||
)
|
)
|
||||||
from sqlite_utils.plugins import pm, get_plugins
|
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)
|
_register_db_for_cleanup(db)
|
||||||
_load_extensions(db, load_extension)
|
_load_extensions(db, load_extension)
|
||||||
try:
|
try:
|
||||||
db[view].drop(ignore=ignore)
|
db.view(view).drop(ignore=ignore)
|
||||||
except OperationalError:
|
except NoView:
|
||||||
raise click.ClickException('View "{}" does not exist'.format(view))
|
if not ignore:
|
||||||
|
raise click.ClickException('View "{}" does not exist'.format(view))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
|
|
||||||
|
|
@ -445,15 +445,15 @@ class Database:
|
||||||
finally:
|
finally:
|
||||||
self._tracer = prev_tracer
|
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.
|
``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.
|
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
|
: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)
|
return self.table(table_name)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|
@ -1206,9 +1206,9 @@ class Database:
|
||||||
return self
|
return self
|
||||||
elif replace:
|
elif replace:
|
||||||
# If SQL is the same, do nothing
|
# If SQL is the same, do nothing
|
||||||
if create_sql == self[name].schema:
|
if create_sql == self.view(name).schema:
|
||||||
return self
|
return self
|
||||||
self[name].drop()
|
self.view(name).drop()
|
||||||
self.execute(create_sql)
|
self.execute(create_sql)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1367,7 +1367,8 @@ def test_create_view():
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert (
|
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 result.exit_code == 0
|
||||||
assert (
|
assert (
|
||||||
'CREATE VIEW "version" AS select sqlite_version() + 1'
|
'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 result.exit_code == 0
|
||||||
assert (
|
assert (
|
||||||
'CREATE VIEW "version" AS select sqlite_version()' == db["version"].schema
|
'CREATE VIEW "version" AS select sqlite_version()'
|
||||||
|
== db.view("version").schema
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1082,7 +1082,7 @@ def test_drop(fresh_db):
|
||||||
def test_drop_view(fresh_db):
|
def test_drop_view(fresh_db):
|
||||||
fresh_db.create_view("foo_view", "select 1")
|
fresh_db.create_view("foo_view", "select 1")
|
||||||
assert ["foo_view"] == fresh_db.view_names()
|
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()
|
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
|
# Testing view is harder, we need to create it in order
|
||||||
# to get a View object, then drop it twice
|
# to get a View object, then drop it twice
|
||||||
fresh_db.create_view("foo_view", "select 1")
|
fresh_db.create_view("foo_view", "select 1")
|
||||||
view = fresh_db["foo_view"]
|
view = fresh_db.view("foo_view")
|
||||||
assert isinstance(view, View)
|
assert isinstance(view, View)
|
||||||
view.drop()
|
view.drop()
|
||||||
with pytest.raises(sqlite3.OperationalError):
|
with pytest.raises(sqlite3.OperationalError):
|
||||||
|
|
|
||||||
|
|
@ -424,7 +424,7 @@ def test_enable_fts_error_message_on_views():
|
||||||
db = Database(memory=True)
|
db = Database(memory=True)
|
||||||
db.create_view("hello", "select 1 + 1")
|
db.create_view("hello", "select 1 + 1")
|
||||||
with pytest.raises(NotImplementedError) as e:
|
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"
|
assert e.value.args[0] == "enable_fts() is supported on tables but not on views"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ def test_tracer():
|
||||||
("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 = '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 = 'view'", None),
|
|
||||||
("select name from sqlite_master where type = 'table'", 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),
|
||||||
('CREATE TABLE "dogs" (\n "name" TEXT\n);\n ', None),
|
('CREATE TABLE "dogs" (\n "name" TEXT\n);\n ', None),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue