simonw / sqlite-utils

Python CLI utility and library for manipulating SQLite databases
https://sqlite-utils.datasette.io
Apache License 2.0
1.58k stars 106 forks source link

`table.get(column=value)` option for retrieving things not by their primary key #588

Open simonw opened 11 months ago

simonw commented 11 months ago

This came up working on this feature:

I have a table with this schema:

CREATE TABLE [collections] (
   [id] INTEGER PRIMARY KEY,
   [name] TEXT,
   [model] TEXT
);
CREATE UNIQUE INDEX [idx_collections_name]
    ON [collections] ([name]);

So the primary key is an integer (because it's going to have a huge number of rows foreign key related to it, and I don't want to store a larger text value thousands of times), but there is a unique constraint on the name - that would be the primary key column if not for all of those foreign keys.

Problem is, fetching the collection by name is actually pretty inconvenient.

Fetch by numeric ID:

try:
    table["collections"].get(1)
except NotFoundError:
    # It doesn't exist

Fetching by name:

def get_collection(db, collection):
    rows = db["collections"].rows_where("name = ?", [collection])
    try:
        return next(rows)
    except StopIteration:
        raise NotFoundError("Collection not found: {}".format(collection))

It would be neat if, for columns where we know that we should always get 0 or one result, we could do this instead:

try:
    collection = table["collections"].get(name="entries")
except NotFoundError:
    # It doesn't exist

The existing .get() method doesn't have any non-positional arguments, so using **kwargs like that should work:

https://github.com/simonw/sqlite-utils/blob/1260bdc7bfe31c36c272572c6389125f8de6ef71/sqlite_utils/db.py#L1495

simonw commented 11 months ago

Tips on typing **kwargs: https://adamj.eu/tech/2021/05/11/python-type-hints-args-and-kwargs/

rsyring commented 6 months ago

Would be nice to have something like this. I've created a get_by() method previously to do the same thing but adding it to get() makes sense.

Tangentially, a first() method that worked similarly but returned None instead of raising when a record isn't found would also be helpful IMO.