diff --git a/docs/python-api.rst b/docs/python-api.rst index f877b10..28ae96f 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -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 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"] @@ -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': , 'value': } -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']), diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 2127b63..6247428 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -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 "".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 "
".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 "".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: diff --git a/tests/test_create.py b/tests/test_create.py index b4778f3..05dfe4a 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -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() diff --git a/tests/test_introspect.py b/tests/test_introspect.py index 5e5b9ec..b04af6d 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -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 "" == repr(view) + assert {"1": str} == view.columns_dict def test_count(existing_db):