Separate View and Table classes

Both of these subclass a common Queryable class.

Also updated documentation to cover the new View class.

And added view.drop() method.
This commit is contained in:
Simon Willison 2019-08-21 13:12:12 +03:00
commit 4441d6d838
4 changed files with 101 additions and 68 deletions

View file

@ -72,12 +72,20 @@ Listing views
>>> db.table_views()
['good_dogs']
You can also iterate through view objects using the ``.views`` property::
You can iterate through view objects using the ``.views`` property::
>>> db.views
[<View good_dogs>]
View objects work the same as table objects, except any attempts to insert or update data will throw an error.
View objects are similar to Table objects, except that any attempts to insert or update data will throw an error. The full list of methods and properties available on a view object is as follows:
* ``columns``
* ``columns_dict``
* ``count``
* ``schema``
* ``rows``
* ``rows_where(where, where_args)``
* ``drop()``
.. _python_api_rows:
@ -701,10 +709,10 @@ If you want to ensure that every foreign key column in your database has a corre
.. _python_api_drop:
Dropping a table
================
Dropping a table or view
========================
You can drop a table by using the ``.drop()`` method:
You can drop a table or view using the ``.drop()`` method:
.. code-block:: python
@ -783,7 +791,7 @@ For example:
Introspection
=============
If you have loaded an existing table, you can use introspection to find out more about it::
If you have loaded an existing table or view, you can use introspection to find out more about it::
>>> db["PlantType"]
<Table PlantType (id, value)>
@ -795,7 +803,7 @@ The ``.count`` property shows the current number of rows (``select count(*) from
>>> db["Street_Tree_List"].count
189144
The ``.columns`` property shows the columns in the table::
The ``.columns`` property shows the columns in the table or view::
>>> db["PlantType"].columns
[Column(cid=0, name='id', type='INTEGER', notnull=0, default_value=None, is_pk=1),
@ -806,7 +814,9 @@ The ``.columns_dict`` property returns a dictionary version of this with just th
>>> db["PlantType"].columns_dict
{'id': <class 'int'>, 'value': <class 'str'>}
The ``.foreign_keys`` property shows if the table has any foreign key relationships::
The ``.foreign_keys`` property shows if the table has any foreign key relationships. It is not available on views.
::
>>> db["Street_Tree_List"].foreign_keys
[ForeignKey(table='Street_Tree_List', column='qLegalStatus', other_table='qLegalStatus', other_column='id'),
@ -846,7 +856,9 @@ The ``.schema`` property outputs the table's schema as a SQL string::
FOREIGN KEY ("qCareAssistant") REFERENCES [qCareAssistant](id),
FOREIGN KEY ("qLegalStatus") REFERENCES [qLegalStatus](id))
The ``.indexes`` property shows you all indexes created for a table::
The ``.indexes`` property shows you all indexes created for a table. It is not available on views.
::
>>> db["Street_Tree_List"].indexes
[Index(seq=0, name='"Street_Tree_List_qLegalStatus"', unique=0, origin='c', partial=0, columns=['qLegalStatus']),

View file

@ -63,6 +63,7 @@ if np:
REVERSE_COLUMN_TYPE_MAPPING = {
"": str, # Columns in views sometimes have type = ''
"TEXT": str,
"BLOB": bytes,
"INTEGER": int,
@ -107,7 +108,8 @@ class Database:
return "<Database {}>".format(self.conn)
def table(self, table_name, **kwargs):
return Table(self, table_name, **kwargs)
klass = View if table_name in self.view_names() else Table
return klass(self, table_name, **kwargs)
def escape(self, value):
# Normally we would use .execute(sql, [params]) for escaping, but
@ -397,54 +399,12 @@ class Database:
self.conn.execute("VACUUM;")
class Table:
def __init__(
self,
db,
name,
pk=None,
foreign_keys=None,
column_order=None,
not_null=None,
defaults=None,
upsert=False,
batch_size=100,
hash_id=None,
alter=False,
ignore=False,
extracts=None,
):
class Queryable:
exists = False
def __init__(self, db, name):
self.db = db
self.name = name
self.is_view = False
self.exists = self.name in self.db.table_names()
if not self.exists:
# It might be a view
self.is_view = self.name in self.db.view_names()
if self.is_view:
self.exists = True
self._defaults = dict(
pk=pk,
foreign_keys=foreign_keys,
column_order=column_order,
not_null=not_null,
defaults=defaults,
upsert=upsert,
batch_size=batch_size,
hash_id=hash_id,
alter=alter,
ignore=ignore,
extracts=extracts,
)
def __repr__(self):
return "<{} {}{}>".format(
"View" if self.is_view else "Table",
self.name,
" (does not exist yet)"
if not self.exists
else " ({})".format(", ".join(c.name for c in self.columns)),
)
@property
def count(self):
@ -452,6 +412,21 @@ class Table:
"select count(*) from [{}]".format(self.name)
).fetchone()[0]
@property
def rows(self):
return self.rows_where()
def rows_where(self, where=None, where_args=None):
if not self.exists:
return []
sql = "select * from [{}]".format(self.name)
if where is not None:
sql += " where " + where
cursor = self.db.conn.execute(sql, where_args or [])
columns = [c[0] for c in cursor.description]
for row in cursor:
yield dict(zip(columns, row))
@property
def columns(self):
if not self.exists:
@ -470,19 +445,52 @@ class Table:
}
@property
def rows(self):
return self.rows_where()
def schema(self):
return self.db.conn.execute(
"select sql from sqlite_master where name = ?", (self.name,)
).fetchone()[0]
def rows_where(self, where=None, where_args=None):
if not self.exists:
return []
sql = "select * from [{}]".format(self.name)
if where is not None:
sql += " where " + where
cursor = self.db.conn.execute(sql, where_args or [])
columns = [c[0] for c in cursor.description]
for row in cursor:
yield dict(zip(columns, row))
class Table(Queryable):
def __init__(
self,
db,
name,
pk=None,
foreign_keys=None,
column_order=None,
not_null=None,
defaults=None,
upsert=False,
batch_size=100,
hash_id=None,
alter=False,
ignore=False,
extracts=None,
):
super().__init__(db, name)
self.exists = self.name in self.db.table_names()
self._defaults = dict(
pk=pk,
foreign_keys=foreign_keys,
column_order=column_order,
not_null=not_null,
defaults=defaults,
upsert=upsert,
batch_size=batch_size,
hash_id=hash_id,
alter=alter,
ignore=ignore,
extracts=extracts,
)
def __repr__(self):
return "<Table {}{}>".format(
self.name,
" (does not exist yet)"
if not self.exists
else " ({})".format(", ".join(c.name for c in self.columns)),
)
@property
def pks(self):
@ -530,12 +538,6 @@ class Table:
)
return fks
@property
def schema(self):
return self.db.conn.execute(
"select sql from sqlite_master where name = ?", (self.name,)
).fetchone()[0]
@property
def indexes(self):
sql = 'PRAGMA index_list("{}")'.format(self.name)
@ -1133,6 +1135,18 @@ class Table:
return self
class View(Queryable):
exists = True
def __repr__(self):
return "<View {} ({})>".format(
self.name, ", ".join(c.name for c in self.columns)
)
def drop(self):
self.db.conn.execute("DROP VIEW {}".format(self.name))
def chunks(sequence, size):
iterator = iter(sequence)
for item in iterator:

View file

@ -782,3 +782,10 @@ def test_drop(fresh_db):
assert ["t"] == fresh_db.table_names()
assert None is fresh_db["t"].drop()
assert [] == fresh_db.table_names()
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 [] == fresh_db.view_names()

View file

@ -1,4 +1,4 @@
from sqlite_utils.db import Index
from sqlite_utils.db import Index, View
import pytest
@ -45,10 +45,10 @@ def test_views(fresh_db):
fresh_db.create_view("foo_view", "select 1")
assert 1 == len(fresh_db.views)
view = fresh_db.views[0]
assert view.is_view
assert view.exists
assert isinstance(view, View)
assert "foo_view" == view.name
assert "<View foo_view (1)>" == repr(view)
assert {"1": str} == view.columns_dict
def test_count(existing_db):