diff --git a/fastlite/kw.py b/fastlite/kw.py index 3fc7d4e..57385aa 100644 --- a/fastlite/kw.py +++ b/fastlite/kw.py @@ -127,6 +127,7 @@ def update(self:Table, updates: dict|None=None, pk_values: list|tuple|str|int|fl updates = _process_row(updates) if not xtra: xtra = getattr(self, 'xtra_id', {}) updates = {**updates, **kwargs, **xtra} + if not updates: return {} if pk_values is None: pk_values = [updates[o] for o in self.pks] self._orig_update(pk_values, updates=updates, alter=alter, conversions=conversions) return self.get_last() diff --git a/nbs/test_update.ipynb b/nbs/test_update.ipynb new file mode 100644 index 0000000..f1cd230 --- /dev/null +++ b/nbs/test_update.ipynb @@ -0,0 +1,412 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fd325418", + "metadata": {}, + "source": [ + "# Test Update Operations" + ] + }, + { + "cell_type": "markdown", + "id": "417f2c4e", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad470f25", + "metadata": {}, + "outputs": [], + "source": [ + "from fastlite import *\n", + "from dataclasses import is_dataclass" + ] + }, + { + "cell_type": "markdown", + "id": "e4788661", + "metadata": {}, + "source": [ + "Note: Make sure to use fastlite's `database()` here" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97dd1b48", + "metadata": {}, + "outputs": [], + "source": [ + "db = database(':memory:')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5102a3ac", + "metadata": {}, + "outputs": [], + "source": [ + "class People: id: int; name: str" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9188c149", + "metadata": {}, + "outputs": [], + "source": [ + "people = db.create(People, pk='id')" + ] + }, + { + "cell_type": "markdown", + "id": "6c99cbae", + "metadata": {}, + "source": [ + "## Test Single Updates" + ] + }, + { + "cell_type": "markdown", + "id": "dbc67ac6", + "metadata": {}, + "source": [ + "Here we test `update()`" + ] + }, + { + "cell_type": "markdown", + "id": "a0673d88", + "metadata": {}, + "source": [ + "### Test Cases for `update()` Where Nothing Is Updated" + ] + }, + { + "cell_type": "markdown", + "id": "eb45e038", + "metadata": {}, + "source": [ + "Test that calling `insert()` without any parameters doesn't change anything, and returns nothing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fba0c4f7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "people.update()" + ] + }, + { + "cell_type": "markdown", + "id": "0355fe0a", + "metadata": {}, + "source": [ + "Test None doesn't change anything." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ace59c88", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "assert people.update(None) == {}\n", + "assert people.count == count" + ] + }, + { + "cell_type": "markdown", + "id": "2ab1795b", + "metadata": {}, + "source": [ + "Test empty dict doesn't change anything " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a93ec70a", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "assert people.update({}) == {}\n", + "assert people.count == count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79cd5186", + "metadata": {}, + "outputs": [], + "source": [ + "# Test empty dataclass doesn't change anything\n", + "PersonDC = people.dataclass()\n", + "count = people.count\n", + "assert people.update(PersonDC()) == {}\n", + "assert people.count == count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa988175", + "metadata": {}, + "outputs": [], + "source": [ + "# Test empty class instance doesn't change anything\n", + "class EmptyPerson: pass\n", + "count = people.count\n", + "assert people.update(EmptyPerson()) == {}\n", + "assert people.count == count" + ] + }, + { + "cell_type": "markdown", + "id": "811bc666", + "metadata": {}, + "source": [ + "### Single Update Types" + ] + }, + { + "cell_type": "markdown", + "id": "157baebb", + "metadata": {}, + "source": [ + "Test update with `dict`. Result should include the Updated value" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fdd0aaf", + "metadata": {}, + "outputs": [], + "source": [ + "person = people.insert(name='Alice')\n", + "adict = dict(id=person.id, name='Bob')\n", + "assert people.update(adict).name == 'Bob'\n", + "assert people[person.id].name == 'Bob'" + ] + }, + { + "cell_type": "markdown", + "id": "d58b023f", + "metadata": {}, + "source": [ + "Fetch record from database to confirm it has changed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5753017", + "metadata": {}, + "outputs": [], + "source": [ + "assert people[person.id].name == 'Bob'" + ] + }, + { + "cell_type": "markdown", + "id": "447e13c9", + "metadata": {}, + "source": [ + "Test update with dataclass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c736aa0f", + "metadata": {}, + "outputs": [], + "source": [ + "dc = People(id=person.id, name='Bobby')\n", + "assert is_dataclass(dc) is True\n", + "assert people.update(dc).name == 'Bobby'\n", + "assert people[person.id].name == 'Bobby'" + ] + }, + { + "cell_type": "markdown", + "id": "0b4eb6df", + "metadata": {}, + "source": [ + "Test with regular class" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cfd90ab0", + "metadata": {}, + "outputs": [], + "source": [ + "class Student: pass\n", + "student = Student()\n", + "student.name = 'Charlo'\n", + "student.id = person.id\n", + "\n", + "assert people.update(student).name == 'Charlo'\n", + "assert people[student.id].name == 'Charlo'" + ] + }, + { + "cell_type": "markdown", + "id": "26a9c38a", + "metadata": {}, + "source": [ + "### None and Empty String Handling" + ] + }, + { + "cell_type": "markdown", + "id": "9abadc7e", + "metadata": {}, + "source": [ + "SQLite makes a clear distinction between NULL (represented as None in Python) and an empty string (''). Unlike some popular Python ORMs, fastlite preserves this distinction because:\n", + "\n", + "1. NULL represents \"unknown\" or \"missing\" data\n", + "2. Empty string represents \"known to be empty\"\n", + "\n", + "These are semantically different concepts, and maintaining this distinction allows users to make appropriate queries (e.g. `WHERE name IS NULL` vs `WHERE name = ''`). The fact that fastlite preserves this distinction in both directions (Python->SQLite and SQLite->Python) is good database design." + ] + }, + { + "cell_type": "markdown", + "id": "37ad998d", + "metadata": {}, + "source": [ + "Test updating a record with name set to None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a968d13", + "metadata": {}, + "outputs": [], + "source": [ + "result = people.update(dict(id=person.id, name=None))\n", + "assert result.name is None\n", + "assert people[person.id].name == None" + ] + }, + { + "cell_type": "markdown", + "id": "dd0c180d", + "metadata": {}, + "source": [ + "Test with empty string" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92d53608", + "metadata": {}, + "outputs": [], + "source": [ + "result = people.update(dict(id=person.id, name=''))\n", + "assert result.name == ''\n", + "assert people[person.id].name == ''" + ] + }, + { + "cell_type": "markdown", + "id": "d855c6a8", + "metadata": {}, + "source": [ + "### Other Cases" + ] + }, + { + "cell_type": "markdown", + "id": "1ee61d32", + "metadata": {}, + "source": [ + "Test with special characters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "972bab86", + "metadata": {}, + "outputs": [], + "source": [ + "assert people.update(dict(id=person.id, name='O\\'Connor')).name == \"O'Connor\"\n", + "assert people[person.id].name == \"O'Connor\"\n", + "assert people.update(dict(id=person.id, name='José')).name == \"José\"\n", + "assert people[person.id].name == \"José\"" + ] + }, + { + "cell_type": "markdown", + "id": "f1209e4b", + "metadata": {}, + "source": [ + "Test that extra fields raise `sqlite3.OperationalError`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07c034e9", + "metadata": {}, + "outputs": [], + "source": [ + "from sqlite3 import OperationalError" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "963008b6", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " p = people.update(dict(id=person.id, name='Extra', age=25, title='Dr'))\n", + "except OperationalError as e:\n", + " assert e.args[0] == 'no such column: age'" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}