Skip to content

Commit 290ad27

Browse files
committed
db.table() only returns tables, added db.view(), refs #657
1 parent d892d2a commit 290ad27

File tree

5 files changed

+100
-74
lines changed

5 files changed

+100
-74
lines changed

docs/python-api.rst

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,21 +251,36 @@ In this example ``next()`` is used to retrieve the first result in the iterator
251251
Accessing tables
252252
================
253253

254-
Tables are accessed using the indexing operator, like so:
254+
Tables are accessed using the ``db.table()`` method, like so:
255+
256+
.. code-block:: python
257+
258+
table = db.table("my_table")
259+
260+
Using this factory function allows you to set :ref:`python_api_table_configuration`. Additional keyword arguments to ``db.table()`` will be used if a further method call causes the table to be created.
261+
262+
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.
263+
264+
You can also access tables or views using dictionary-style syntax, like this:
255265

256266
.. code-block:: python
257267
258268
table = db["my_table"]
259269
260-
If the table does not yet exist, it will be created the first time you attempt to insert or upsert data into it.
270+
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.
271+
272+
.. _python_api_view:
261273

262-
You can also access tables using the ``.table()`` method like so:
274+
Accessing views
275+
===============
276+
277+
SQL views can be accessed using the ``db.view()`` method, like so:
263278

264279
.. code-block:: python
265280
266-
table = db.table("my_table")
281+
view = db.view("my_view")
267282
268-
Using this factory function allows you to set :ref:`python_api_table_configuration`.
283+
This will return a :ref:`reference_db_view` instance, or raise a ``sqlite_utils.db.NoView`` exception if the view does not exist.
269284

270285
.. _python_api_tables:
271286

sqlite_utils/cli.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ def tables(
186186
if schema:
187187
headers.append("schema")
188188

189+
method = db.view if views else db.table
190+
189191
def _iter():
190192
if views:
191193
items = db.view_names()
@@ -194,15 +196,15 @@ def _iter():
194196
for name in items:
195197
row = [name]
196198
if counts:
197-
row.append(db[name].count)
199+
row.append(method(name).count)
198200
if columns:
199-
cols = [c.name for c in db[name].columns]
201+
cols = [c.name for c in method(name).columns]
200202
if csv:
201203
row.append("\n".join(cols))
202204
else:
203205
row.append(cols)
204206
if schema:
205-
row.append(db[name].schema)
207+
row.append(method(name).schema)
206208
yield row
207209

208210
if table or fmt:
@@ -1693,7 +1695,7 @@ def create_view(path, view, select, ignore, replace, load_extension):
16931695
if ignore:
16941696
return
16951697
elif replace:
1696-
db[view].drop()
1698+
db.view(view).drop()
16971699
else:
16981700
raise click.ClickException(
16991701
'View "{}" already exists. Use --replace to delete and replace it.'.format(

sqlite_utils/db.py

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ class NoTable(Exception):
247247
"Specified table does not exist"
248248

249249

250+
class NoView(Exception):
251+
"Specified view does not exist"
252+
253+
250254
class BadPrimaryKey(Exception):
251255
"Table does not have a single obvious primary key"
252256

@@ -419,6 +423,8 @@ def __getitem__(self, table_name: str) -> Union["Table", "View"]:
419423
420424
:param table_name: The name of the table
421425
"""
426+
if table_name in self.view_names():
427+
return self.view(table_name)
422428
return self.table(table_name)
423429

424430
def __repr__(self) -> str:
@@ -541,7 +547,7 @@ def executescript(self, sql: str) -> sqlite3.Cursor:
541547
self._tracer(sql, None)
542548
return self.conn.executescript(sql)
543549

544-
def table(self, table_name: str, **kwargs) -> Union["Table", "View"]:
550+
def table(self, table_name: str, **kwargs) -> "Table":
545551
"""
546552
Return a table object, optionally configured with default options.
547553
@@ -550,10 +556,19 @@ def table(self, table_name: str, **kwargs) -> Union["Table", "View"]:
550556
:param table_name: Name of the table
551557
"""
552558
if table_name in self.view_names():
553-
return View(self, table_name, **kwargs)
554-
else:
555-
kwargs.setdefault("strict", self.strict)
556-
return Table(self, table_name, **kwargs)
559+
raise NoTable("Table {} is actually a view".format(table_name))
560+
kwargs.setdefault("strict", self.strict)
561+
return Table(self, table_name, **kwargs)
562+
563+
def view(self, view_name: str) -> "View":
564+
"""
565+
Return a view object.
566+
567+
:param view_name: Name of the view
568+
"""
569+
if view_name not in self.view_names():
570+
raise NoView("View {} does not exist".format(view_name))
571+
return View(self, view_name)
557572

558573
def quote(self, value: str) -> str:
559574
"""
@@ -637,12 +652,12 @@ def view_names(self) -> List[str]:
637652
@property
638653
def tables(self) -> List["Table"]:
639654
"List of Table objects in this database."
640-
return cast(List["Table"], [self[name] for name in self.table_names()])
655+
return [self.table(name) for name in self.table_names()]
641656

642657
@property
643658
def views(self) -> List["View"]:
644659
"List of View objects in this database."
645-
return cast(List["View"], [self[name] for name in self.view_names()])
660+
return [self.view(name) for name in self.view_names()]
646661

647662
@property
648663
def triggers(self) -> List[Trigger]:
@@ -808,7 +823,7 @@ def resolve_foreign_keys(
808823
or a tuple of (column, other_table, other_column), or a tuple of
809824
(table, column, other_table, other_column)
810825
"""
811-
table = cast(Table, self[name])
826+
table = self.table(name)
812827
if all(isinstance(fk, ForeignKey) for fk in foreign_keys):
813828
return cast(List[ForeignKey], foreign_keys)
814829
if all(isinstance(fk, str) for fk in foreign_keys):
@@ -1039,11 +1054,11 @@ def create_table(
10391054
# Transform table to match the new definition if table already exists:
10401055
if self[name].exists():
10411056
if ignore:
1042-
return cast(Table, self[name])
1057+
return self.table(name)
10431058
elif replace:
10441059
self[name].drop()
10451060
if transform and self[name].exists():
1046-
table = cast(Table, self[name])
1061+
table = self.table(name)
10471062
should_transform = False
10481063
# First add missing columns and figure out columns to drop
10491064
existing_columns = table.columns_dict
@@ -1109,7 +1124,7 @@ def create_table(
11091124
strict=strict,
11101125
)
11111126
self.execute(sql)
1112-
created_table = self.table(
1127+
return self.table(
11131128
name,
11141129
pk=pk,
11151130
foreign_keys=foreign_keys,
@@ -1119,7 +1134,6 @@ def create_table(
11191134
hash_id=hash_id,
11201135
hash_id_columns=hash_id_columns,
11211136
)
1122-
return cast(Table, created_table)
11231137

11241138
def rename_table(self, name: str, new_name: str):
11251139
"""
@@ -1196,12 +1210,9 @@ def add_foreign_keys(self, foreign_keys: Iterable[Tuple[str, str, str, str]]):
11961210

11971211
# Verify that all tables and columns exist
11981212
for table, column, other_table, other_column in foreign_keys:
1199-
if not self[table].exists():
1213+
if not self.table(table).exists():
12001214
raise AlterError("No such table: {}".format(table))
1201-
table_obj = self[table]
1202-
if not isinstance(table_obj, Table):
1203-
raise AlterError("Must be a table, not a view: {}".format(table))
1204-
table_obj = cast(Table, table_obj)
1215+
table_obj = self.table(table)
12051216
if column not in table_obj.columns_dict:
12061217
raise AlterError("No such column: {} in {}".format(column, table))
12071218
if not self[other_table].exists():
@@ -1231,7 +1242,7 @@ def add_foreign_keys(self, foreign_keys: Iterable[Tuple[str, str, str, str]]):
12311242
by_table.setdefault(fk[0], []).append(fk)
12321243

12331244
for table, fks in by_table.items():
1234-
cast(Table, self[table]).transform(add_foreign_keys=fks)
1245+
self.table(table).transform(add_foreign_keys=fks)
12351246

12361247
self.vacuum()
12371248

@@ -3655,7 +3666,7 @@ def m2m(
36553666
already exists.
36563667
"""
36573668
if isinstance(other_table, str):
3658-
other_table = cast(Table, self.db.table(other_table, pk=pk))
3669+
other_table = self.db.table(other_table, pk=pk)
36593670
our_id = self.last_pk
36603671
if lookup is not None:
36613672
assert record_or_iterable is None, "Provide lookup= or record, not both"

tests/test_enable_counts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def test_uses_counts_after_enable_counts(counts_db_path):
129129
db = Database(counts_db_path)
130130
logged = []
131131
with db.tracer(lambda sql, parameters: logged.append((sql, parameters))):
132-
assert db["foo"].count == 1
132+
assert db.table("foo").count == 1
133133
assert logged == [
134134
("select name from sqlite_master where type = 'view'", None),
135135
("select count(*) from [foo]", []),
@@ -138,7 +138,7 @@ def test_uses_counts_after_enable_counts(counts_db_path):
138138
assert not db.use_counts_table
139139
db.enable_counts()
140140
assert db.use_counts_table
141-
assert db["foo"].count == 1
141+
assert db.table("foo").count == 1
142142
assert logged == [
143143
(
144144
"CREATE TABLE IF NOT EXISTS [_counts](\n [table] TEXT PRIMARY KEY,\n count INTEGER DEFAULT 0\n);",

tests/test_tracer.py

Lines changed: 42 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@ def test_tracer():
66
db = Database(
77
memory=True, tracer=lambda sql, params: collected.append((sql, params))
88
)
9-
db["dogs"].insert({"name": "Cleopaws"})
10-
db["dogs"].enable_fts(["name"])
11-
db["dogs"].search("Cleopaws")
9+
dogs = db.table("dogs")
10+
dogs.insert({"name": "Cleopaws"})
11+
dogs.enable_fts(["name"])
12+
dogs.search("Cleopaws")
1213
assert collected == [
1314
("PRAGMA recursive_triggers=on;", None),
1415
("select name from sqlite_master where type = 'view'", None),
1516
("select name from sqlite_master where type = 'table'", None),
1617
("select name from sqlite_master where type = 'view'", None),
18+
("select name from sqlite_master where type = 'view'", None),
1719
("select name from sqlite_master where type = 'table'", None),
1820
("select name from sqlite_master where type = 'view'", None),
1921
("CREATE TABLE [dogs] (\n [name] TEXT\n);\n ", None),
2022
("select name from sqlite_master where type = 'view'", None),
2123
("INSERT INTO [dogs] ([name]) VALUES (?)", ["Cleopaws"]),
22-
("select name from sqlite_master where type = 'view'", None),
2324
(
2425
"CREATE VIRTUAL TABLE [dogs_fts] USING FTS5 (\n [name],\n content=[dogs]\n)",
2526
None,
@@ -28,7 +29,6 @@ def test_tracer():
2829
"INSERT INTO [dogs_fts] (rowid, [name])\n SELECT rowid, [name] FROM [dogs];",
2930
None,
3031
),
31-
("select name from sqlite_master where type = 'view'", None),
3232
]
3333

3434

@@ -40,60 +40,58 @@ def tracer(sql, params):
4040

4141
db = Database(memory=True)
4242

43-
db["dogs"].insert({"name": "Cleopaws"})
44-
db["dogs"].enable_fts(["name"])
43+
dogs = db.table("dogs")
44+
45+
dogs.insert({"name": "Cleopaws"})
46+
dogs.enable_fts(["name"])
4547

4648
assert len(collected) == 0
4749

4850
with db.tracer(tracer):
49-
list(db["dogs"].search("Cleopaws"))
51+
list(dogs.search("Cleopaws"))
5052

5153
assert len(collected) == 5
5254
assert collected == [
53-
("select name from sqlite_master where type = 'view'", None),
5455
(
55-
(
56-
"SELECT name FROM sqlite_master\n"
57-
" WHERE rootpage = 0\n"
58-
" AND (\n"
59-
" sql LIKE :like\n"
60-
" OR sql LIKE :like2\n"
61-
" OR (\n"
62-
" tbl_name = :table\n"
63-
" AND sql LIKE '%VIRTUAL TABLE%USING FTS%'\n"
64-
" )\n"
65-
" )",
66-
{
67-
"like": "%VIRTUAL TABLE%USING FTS%content=[dogs]%",
68-
"like2": '%VIRTUAL TABLE%USING FTS%content="dogs"%',
69-
"table": "dogs",
70-
},
71-
)
56+
"SELECT name FROM sqlite_master\n"
57+
" WHERE rootpage = 0\n"
58+
" AND (\n"
59+
" sql LIKE :like\n"
60+
" OR sql LIKE :like2\n"
61+
" OR (\n"
62+
" tbl_name = :table\n"
63+
" AND sql LIKE '%VIRTUAL TABLE%USING FTS%'\n"
64+
" )\n"
65+
" )",
66+
{
67+
"like": "%VIRTUAL TABLE%USING FTS%content=[dogs]%",
68+
"like2": '%VIRTUAL TABLE%USING FTS%content="dogs"%',
69+
"table": "dogs",
70+
},
7271
),
7372
("select name from sqlite_master where type = 'view'", None),
73+
("select name from sqlite_master where type = 'view'", None),
7474
("select sql from sqlite_master where name = ?", ("dogs_fts",)),
7575
(
76-
(
77-
"with original as (\n"
78-
" select\n"
79-
" rowid,\n"
80-
" *\n"
81-
" from [dogs]\n"
82-
")\n"
83-
"select\n"
84-
" [original].*\n"
85-
"from\n"
86-
" [original]\n"
87-
" join [dogs_fts] on [original].rowid = [dogs_fts].rowid\n"
88-
"where\n"
89-
" [dogs_fts] match :query\n"
90-
"order by\n"
91-
" [dogs_fts].rank"
92-
),
76+
"with original as (\n"
77+
" select\n"
78+
" rowid,\n"
79+
" *\n"
80+
" from [dogs]\n"
81+
")\n"
82+
"select\n"
83+
" [original].*\n"
84+
"from\n"
85+
" [original]\n"
86+
" join [dogs_fts] on [original].rowid = [dogs_fts].rowid\n"
87+
"where\n"
88+
" [dogs_fts] match :query\n"
89+
"order by\n"
90+
" [dogs_fts].rank",
9391
{"query": "Cleopaws"},
9492
),
9593
]
9694

9795
# Outside the with block collected should not be appended to
98-
db["dogs"].insert({"name": "Cleopaws"})
96+
dogs.insert({"name": "Cleopaws"})
9997
assert len(collected) == 5

0 commit comments

Comments
 (0)