From a73afc0c6e97c4a76b0cc305f2aeb2047fee3e4a Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Wed, 20 Nov 2024 12:22:34 +0000 Subject: [PATCH 1/3] Create test_updates --- fastlite/kw.py | 1 + nbs/test_update.ipynb | 782 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 783 insertions(+) create mode 100644 nbs/test_update.ipynb diff --git a/fastlite/kw.py b/fastlite/kw.py index 49a8be3..ffcd38f 100644 --- a/fastlite/kw.py +++ b/fastlite/kw.py @@ -123,6 +123,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..5fb9f21 --- /dev/null +++ b/nbs/test_update.ipynb @@ -0,0 +1,782 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fd325418", + "metadata": {}, + "source": [ + "# Test Update Operations" + ] + }, + { + "cell_type": "markdown", + "id": "417f2c4e", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ad470f25", + "metadata": {}, + "outputs": [], + "source": [ + "from fastlite import *" + ] + }, + { + "cell_type": "markdown", + "id": "e4788661", + "metadata": {}, + "source": [ + "Note: Make sure to use fastlite's `database()` here" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "97dd1b48", + "metadata": {}, + "outputs": [], + "source": [ + "db = database(':memory:')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5102a3ac", + "metadata": {}, + "outputs": [], + "source": [ + "class People: id: int; name: str" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "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": 5, + "id": "fba0c4f7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 5, + "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": 6, + "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": 7, + "id": "a93ec70a", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "assert people.update({}) == {}\n", + "assert people.count == count" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "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": 9, + "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 insert with keyword argument. Result should be the inserted item." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "1fdd0aaf", + "metadata": {}, + "outputs": [ + { + "ename": "KeyError", + "evalue": "'id'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[10], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m result \u001b[38;5;241m=\u001b[39m people\u001b[38;5;241m.\u001b[39minsert(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mAlice\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m----> 2\u001b[0m \u001b[43mpeople\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mupdate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mBob\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m people\u001b[38;5;241m.\u001b[39mrows \u001b[38;5;241m==\u001b[39m []\n", + "File \u001b[0;32m~/answer/fastlite/fastlite/kw.py:129\u001b[0m, in \u001b[0;36mupdate\u001b[0;34m(self, updates, pk_values, alter, conversions, xtra, **kwargs)\u001b[0m\n\u001b[1;32m 127\u001b[0m updates \u001b[38;5;241m=\u001b[39m {\u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mupdates, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mxtra}\n\u001b[1;32m 128\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m updates: \u001b[38;5;28;01mreturn\u001b[39;00m {}\n\u001b[0;32m--> 129\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m pk_values \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m: pk_values \u001b[38;5;241m=\u001b[39m [\u001b[43mupdates\u001b[49m\u001b[43m[\u001b[49m\u001b[43mo\u001b[49m\u001b[43m]\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m o \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpks]\n\u001b[1;32m 130\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_orig_update(pk_values, updates\u001b[38;5;241m=\u001b[39mupdates, alter\u001b[38;5;241m=\u001b[39malter, conversions\u001b[38;5;241m=\u001b[39mconversions)\n\u001b[1;32m 131\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_last()\n", + "\u001b[0;31mKeyError\u001b[0m: 'id'" + ] + } + ], + "source": [ + "result = people.insert(name='Alice')\n", + "people.update(name='Bob')\n", + "assert people.rows == []" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c85fe7a9", + "metadata": {}, + "outputs": [], + "source": [ + "raise Exception" + ] + }, + { + "cell_type": "markdown", + "id": "447e13c9", + "metadata": {}, + "source": [ + "Test insert with dataclass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c736aa0f", + "metadata": {}, + "outputs": [], + "source": [ + "assert people.insert(People(name='Bobba')).name == 'Bobba'" + ] + }, + { + "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", + "\n", + "assert people.insert(student).name == 'Charlo'" + ] + }, + { + "cell_type": "markdown", + "id": "38ff8b74", + "metadata": {}, + "source": [ + "Verify count is 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72a25f8d", + "metadata": {}, + "outputs": [], + "source": [ + "assert people.count == 3" + ] + }, + { + "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 inserting a record with name set to None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a968d13", + "metadata": {}, + "outputs": [], + "source": [ + "result = people.insert(name=None)\n", + "assert result.name is 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.insert(name='')\n", + "assert result.name == ''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51cb29b1", + "metadata": {}, + "outputs": [], + "source": [ + "assert people.get(pk_values=4).name == None" + ] + }, + { + "cell_type": "markdown", + "id": "46d8230c", + "metadata": {}, + "source": [ + "Remember, `get()` is for getting single items. The following would not work here. `pk_values` can be a list only for tables with compound primary keys.\n", + "\n", + "```python\n", + "# people.get(pk_values=[4,5])\n", + "```" + ] + }, + { + "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.insert(name='O\\'Connor').name == \"O'Connor\"\n", + "assert people.insert(name='José').name == 'José'" + ] + }, + { + "cell_type": "markdown", + "id": "f3261fa3", + "metadata": {}, + "source": [ + "Test id auto-increment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55364dd6", + "metadata": {}, + "outputs": [], + "source": [ + "p1 = people.insert(name='Test1')\n", + "p2 = people.insert(name='Test2') \n", + "assert p2.id == p1.id + 1" + ] + }, + { + "cell_type": "markdown", + "id": "f27e986a", + "metadata": {}, + "source": [ + "Test dict insert" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45a4c2aa", + "metadata": {}, + "outputs": [], + "source": [ + "assert people.insert({'name': 'Dict Test'}).name == 'Dict Test'" + ] + }, + { + "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.insert(name='Extra', age=25, title='Dr')\n", + "except OperationalError as e:\n", + " assert e.args[0] == 'table people has no column named age'" + ] + }, + { + "cell_type": "markdown", + "id": "7d7d252b", + "metadata": {}, + "source": [ + "## Test Multiple Inserts" + ] + }, + { + "cell_type": "markdown", + "id": "04de34fb", + "metadata": {}, + "source": [ + "Here we test `insert_all()`" + ] + }, + { + "cell_type": "markdown", + "id": "46bc9961", + "metadata": {}, + "source": [ + "### Test cases for `insert_all()` where nothing is changed" + ] + }, + { + "cell_type": "markdown", + "id": "eeb1fdda", + "metadata": {}, + "source": [ + "Test empty list doesn't change anything" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8a95079", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "people.insert_all([])\n", + "assert people.count == count" + ] + }, + { + "cell_type": "markdown", + "id": "f46e99a2", + "metadata": {}, + "source": [ + "Test other empty iterables don't change anything" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cee37620", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "people.insert_all(iter([])) # empty iterator\n", + "people.insert_all(set()) # empty set\n", + "people.insert_all(tuple()) # empty tuple\n", + "assert people.count == count" + ] + }, + { + "cell_type": "markdown", + "id": "3dcde075", + "metadata": {}, + "source": [ + "Test that lists of `None` don't change anything." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98118662", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "assert people.insert_all([None, None]) == people\n", + "assert people.result == []\n", + "assert people.count == count" + ] + }, + { + "cell_type": "markdown", + "id": "43a78fc8", + "metadata": {}, + "source": [ + "### Test cases for `insert_all()` where records are inserted" + ] + }, + { + "cell_type": "markdown", + "id": "8677a8a4", + "metadata": {}, + "source": [ + "Test that a list containing both None and a valid records only inserts the valid record." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96632dfb", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "people.insert_all([None, None, None, None, None, dict(name='Dermot')])\n", + "assert people.count == count + 1" + ] + }, + { + "cell_type": "markdown", + "id": "7d7ea003", + "metadata": {}, + "source": [ + "Test list of dicts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b110b0a7", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "data = [{'name': 'Bulk1'}, {'name': 'Bulk2'}, {'name': 'Bulk3'}]\n", + "people.insert_all(data)\n", + "assert people.count == len(data) + count" + ] + }, + { + "cell_type": "markdown", + "id": "d1255a3b", + "metadata": {}, + "source": [ + "Test `insert_all` with a list of dataclass instances to insert" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "803e6bc9", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "Person = people.dataclass()\n", + "data = [Person(name=f'DC{i}') for i in range(3)]\n", + "people.insert_all(data)\n", + "assert people.count == count + 3" + ] + }, + { + "cell_type": "markdown", + "id": "8be30bff", + "metadata": {}, + "source": [ + "Test list of regular class instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "570d5dce", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "class Student:\n", + " def __init__(self, name): self.name = name\n", + "students = [Student(f'Student{i}') for i in range(3)]\n", + "people.insert_all(students)\n", + "assert people.count == count + 3" + ] + }, + { + "cell_type": "markdown", + "id": "bd68bde0", + "metadata": {}, + "source": [ + "### Edge Cases" + ] + }, + { + "cell_type": "markdown", + "id": "e9ff33c6", + "metadata": {}, + "source": [ + "Test mixed types in list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca76eb12", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "Person = people.dataclass()\n", + "mixed_data = [\n", + " {'name': 'Dict1'},\n", + " Person(name='DC1'),\n", + " Student('Student1')\n", + "]\n", + "people.insert_all(mixed_data)\n", + "assert people.count == count + 3" + ] + }, + { + "cell_type": "markdown", + "id": "76f0e0b4", + "metadata": {}, + "source": [ + "Test None/empty strings in bulk insert" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a37e482", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "null_data = [\n", + " {'name': None},\n", + " {'name': ''},\n", + " {'name': 'Regular'}\n", + "]\n", + "people.insert_all(null_data)\n", + "assert people.count == count + 3" + ] + }, + { + "cell_type": "markdown", + "id": "c92ada52", + "metadata": {}, + "source": [ + "Test with special characters in bulk" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da81a215", + "metadata": {}, + "outputs": [], + "source": [ + "count = people.count\n", + "special_data = [\n", + " {'name': \"O'Brien\"},\n", + " {'name': 'José'},\n", + " {'name': '张伟'}\n", + "]\n", + "res = people.insert_all(special_data)\n", + "assert people.count == count + 3" + ] + }, + { + "cell_type": "markdown", + "id": "63b76213", + "metadata": {}, + "source": [ + "Test error on invalid column" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d7d7991", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " people.insert_all([{'name': 'Valid'}, {'invalid_col': 'Bad'}])\n", + "except OperationalError as e:\n", + " assert 'no column named invalid_col' in str(e)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 1ab9da6ea98c55ba31e264d57be2897c94034cf9 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Wed, 20 Nov 2024 14:40:17 +0000 Subject: [PATCH 2/3] Working updates that do record checks Co-authored-by: Audrey Roy Greenfeld --- nbs/test_update.ipynb | 81 ++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/nbs/test_update.ipynb b/nbs/test_update.ipynb index 5fb9f21..df1f95e 100644 --- a/nbs/test_update.ipynb +++ b/nbs/test_update.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "ad470f25", "metadata": {}, "outputs": [], @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "97dd1b48", "metadata": {}, "outputs": [], @@ -46,7 +46,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "5102a3ac", "metadata": {}, "outputs": [], @@ -56,7 +56,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "9188c149", "metadata": {}, "outputs": [], @@ -98,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "fba0c4f7", "metadata": {}, "outputs": [ @@ -108,7 +108,7 @@ "{}" ] }, - "execution_count": 5, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -127,7 +127,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "ace59c88", "metadata": {}, "outputs": [], @@ -147,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "a93ec70a", "metadata": {}, "outputs": [], @@ -159,7 +159,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "79cd5186", "metadata": {}, "outputs": [], @@ -173,7 +173,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "aa988175", "metadata": {}, "outputs": [], @@ -198,42 +198,37 @@ "id": "157baebb", "metadata": {}, "source": [ - "Test insert with keyword argument. Result should be the inserted item." + "Test update with keyword argument. Result should include the Updated value" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "1fdd0aaf", "metadata": {}, - "outputs": [ - { - "ename": "KeyError", - "evalue": "'id'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[10], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m result \u001b[38;5;241m=\u001b[39m people\u001b[38;5;241m.\u001b[39minsert(name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mAlice\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m----> 2\u001b[0m \u001b[43mpeople\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mupdate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mBob\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m people\u001b[38;5;241m.\u001b[39mrows \u001b[38;5;241m==\u001b[39m []\n", - "File \u001b[0;32m~/answer/fastlite/fastlite/kw.py:129\u001b[0m, in \u001b[0;36mupdate\u001b[0;34m(self, updates, pk_values, alter, conversions, xtra, **kwargs)\u001b[0m\n\u001b[1;32m 127\u001b[0m updates \u001b[38;5;241m=\u001b[39m {\u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mupdates, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mxtra}\n\u001b[1;32m 128\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m updates: \u001b[38;5;28;01mreturn\u001b[39;00m {}\n\u001b[0;32m--> 129\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m pk_values \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m: pk_values \u001b[38;5;241m=\u001b[39m [\u001b[43mupdates\u001b[49m\u001b[43m[\u001b[49m\u001b[43mo\u001b[49m\u001b[43m]\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m o \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mpks]\n\u001b[1;32m 130\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_orig_update(pk_values, updates\u001b[38;5;241m=\u001b[39mupdates, alter\u001b[38;5;241m=\u001b[39malter, conversions\u001b[38;5;241m=\u001b[39mconversions)\n\u001b[1;32m 131\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_last()\n", - "\u001b[0;31mKeyError\u001b[0m: 'id'" - ] - } - ], + "outputs": [], "source": [ - "result = people.insert(name='Alice')\n", - "people.update(name='Bob')\n", - "assert people.rows == []" + "person = people.insert(name='Alice')\n", + "person.name = 'Bob'\n", + "assert people.update(person).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": "c85fe7a9", + "id": "e5753017", "metadata": {}, "outputs": [], "source": [ - "raise Exception" + "assert people[person.id].name == 'Bob'" ] }, { @@ -241,7 +236,7 @@ "id": "447e13c9", "metadata": {}, "source": [ - "Test insert with dataclass" + "Test update with dataclass" ] }, { @@ -251,7 +246,9 @@ "metadata": {}, "outputs": [], "source": [ - "assert people.insert(People(name='Bobba')).name == 'Bobba'" + "dc = People(id=person.id, name='Bobby')\n", + "assert people.update(dc).name == 'Bobby'\n", + "assert people[person.id].name == 'Bobby'" ] }, { @@ -272,8 +269,10 @@ "class Student: pass\n", "student = Student()\n", "student.name = 'Charlo'\n", + "student.id = person.id\n", "\n", - "assert people.insert(student).name == 'Charlo'" + "assert people.update(student).name == 'Charlo'\n", + "assert people[stu.id].name == 'Bobby'" ] }, { @@ -760,21 +759,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.6" } }, "nbformat": 4, From 657579b697dbb472c7be46eb20eae72929287723 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Wed, 20 Nov 2024 15:49:02 +0000 Subject: [PATCH 3/3] Comprehensive update tests Co-authored-by: Audrey Roy Greenfeld --- nbs/test_update.ipynb | 399 +++--------------------------------------- 1 file changed, 21 insertions(+), 378 deletions(-) diff --git a/nbs/test_update.ipynb b/nbs/test_update.ipynb index df1f95e..f1cd230 100644 --- a/nbs/test_update.ipynb +++ b/nbs/test_update.ipynb @@ -23,7 +23,8 @@ "metadata": {}, "outputs": [], "source": [ - "from fastlite import *" + "from fastlite import *\n", + "from dataclasses import is_dataclass" ] }, { @@ -198,7 +199,7 @@ "id": "157baebb", "metadata": {}, "source": [ - "Test update with keyword argument. Result should include the Updated value" + "Test update with `dict`. Result should include the Updated value" ] }, { @@ -209,8 +210,9 @@ "outputs": [], "source": [ "person = people.insert(name='Alice')\n", - "person.name = 'Bob'\n", - "assert people.update(person).name == 'Bob'" + "adict = dict(id=person.id, name='Bob')\n", + "assert people.update(adict).name == 'Bob'\n", + "assert people[person.id].name == 'Bob'" ] }, { @@ -247,6 +249,7 @@ "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'" ] @@ -272,25 +275,7 @@ "student.id = person.id\n", "\n", "assert people.update(student).name == 'Charlo'\n", - "assert people[stu.id].name == 'Bobby'" - ] - }, - { - "cell_type": "markdown", - "id": "38ff8b74", - "metadata": {}, - "source": [ - "Verify count is 3" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "72a25f8d", - "metadata": {}, - "outputs": [], - "source": [ - "assert people.count == 3" + "assert people[student.id].name == 'Charlo'" ] }, { @@ -319,7 +304,7 @@ "id": "37ad998d", "metadata": {}, "source": [ - "Test inserting a record with name set to None" + "Test updating a record with name set to None" ] }, { @@ -329,8 +314,9 @@ "metadata": {}, "outputs": [], "source": [ - "result = people.insert(name=None)\n", - "assert result.name is None" + "result = people.update(dict(id=person.id, name=None))\n", + "assert result.name is None\n", + "assert people[person.id].name == None" ] }, { @@ -348,30 +334,9 @@ "metadata": {}, "outputs": [], "source": [ - "result = people.insert(name='')\n", - "assert result.name == ''" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "51cb29b1", - "metadata": {}, - "outputs": [], - "source": [ - "assert people.get(pk_values=4).name == None" - ] - }, - { - "cell_type": "markdown", - "id": "46d8230c", - "metadata": {}, - "source": [ - "Remember, `get()` is for getting single items. The following would not work here. `pk_values` can be a list only for tables with compound primary keys.\n", - "\n", - "```python\n", - "# people.get(pk_values=[4,5])\n", - "```" + "result = people.update(dict(id=person.id, name=''))\n", + "assert result.name == ''\n", + "assert people[person.id].name == ''" ] }, { @@ -397,46 +362,10 @@ "metadata": {}, "outputs": [], "source": [ - "assert people.insert(name='O\\'Connor').name == \"O'Connor\"\n", - "assert people.insert(name='José').name == 'José'" - ] - }, - { - "cell_type": "markdown", - "id": "f3261fa3", - "metadata": {}, - "source": [ - "Test id auto-increment" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "55364dd6", - "metadata": {}, - "outputs": [], - "source": [ - "p1 = people.insert(name='Test1')\n", - "p2 = people.insert(name='Test2') \n", - "assert p2.id == p1.id + 1" - ] - }, - { - "cell_type": "markdown", - "id": "f27e986a", - "metadata": {}, - "source": [ - "Test dict insert" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "45a4c2aa", - "metadata": {}, - "outputs": [], - "source": [ - "assert people.insert({'name': 'Dict Test'}).name == 'Dict Test'" + "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é\"" ] }, { @@ -465,295 +394,9 @@ "outputs": [], "source": [ "try:\n", - " p = people.insert(name='Extra', age=25, title='Dr')\n", - "except OperationalError as e:\n", - " assert e.args[0] == 'table people has no column named age'" - ] - }, - { - "cell_type": "markdown", - "id": "7d7d252b", - "metadata": {}, - "source": [ - "## Test Multiple Inserts" - ] - }, - { - "cell_type": "markdown", - "id": "04de34fb", - "metadata": {}, - "source": [ - "Here we test `insert_all()`" - ] - }, - { - "cell_type": "markdown", - "id": "46bc9961", - "metadata": {}, - "source": [ - "### Test cases for `insert_all()` where nothing is changed" - ] - }, - { - "cell_type": "markdown", - "id": "eeb1fdda", - "metadata": {}, - "source": [ - "Test empty list doesn't change anything" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c8a95079", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "people.insert_all([])\n", - "assert people.count == count" - ] - }, - { - "cell_type": "markdown", - "id": "f46e99a2", - "metadata": {}, - "source": [ - "Test other empty iterables don't change anything" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cee37620", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "people.insert_all(iter([])) # empty iterator\n", - "people.insert_all(set()) # empty set\n", - "people.insert_all(tuple()) # empty tuple\n", - "assert people.count == count" - ] - }, - { - "cell_type": "markdown", - "id": "3dcde075", - "metadata": {}, - "source": [ - "Test that lists of `None` don't change anything." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "98118662", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "assert people.insert_all([None, None]) == people\n", - "assert people.result == []\n", - "assert people.count == count" - ] - }, - { - "cell_type": "markdown", - "id": "43a78fc8", - "metadata": {}, - "source": [ - "### Test cases for `insert_all()` where records are inserted" - ] - }, - { - "cell_type": "markdown", - "id": "8677a8a4", - "metadata": {}, - "source": [ - "Test that a list containing both None and a valid records only inserts the valid record." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "96632dfb", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "people.insert_all([None, None, None, None, None, dict(name='Dermot')])\n", - "assert people.count == count + 1" - ] - }, - { - "cell_type": "markdown", - "id": "7d7ea003", - "metadata": {}, - "source": [ - "Test list of dicts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b110b0a7", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "data = [{'name': 'Bulk1'}, {'name': 'Bulk2'}, {'name': 'Bulk3'}]\n", - "people.insert_all(data)\n", - "assert people.count == len(data) + count" - ] - }, - { - "cell_type": "markdown", - "id": "d1255a3b", - "metadata": {}, - "source": [ - "Test `insert_all` with a list of dataclass instances to insert" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "803e6bc9", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "Person = people.dataclass()\n", - "data = [Person(name=f'DC{i}') for i in range(3)]\n", - "people.insert_all(data)\n", - "assert people.count == count + 3" - ] - }, - { - "cell_type": "markdown", - "id": "8be30bff", - "metadata": {}, - "source": [ - "Test list of regular class instances" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "570d5dce", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "class Student:\n", - " def __init__(self, name): self.name = name\n", - "students = [Student(f'Student{i}') for i in range(3)]\n", - "people.insert_all(students)\n", - "assert people.count == count + 3" - ] - }, - { - "cell_type": "markdown", - "id": "bd68bde0", - "metadata": {}, - "source": [ - "### Edge Cases" - ] - }, - { - "cell_type": "markdown", - "id": "e9ff33c6", - "metadata": {}, - "source": [ - "Test mixed types in list" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ca76eb12", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "Person = people.dataclass()\n", - "mixed_data = [\n", - " {'name': 'Dict1'},\n", - " Person(name='DC1'),\n", - " Student('Student1')\n", - "]\n", - "people.insert_all(mixed_data)\n", - "assert people.count == count + 3" - ] - }, - { - "cell_type": "markdown", - "id": "76f0e0b4", - "metadata": {}, - "source": [ - "Test None/empty strings in bulk insert" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5a37e482", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "null_data = [\n", - " {'name': None},\n", - " {'name': ''},\n", - " {'name': 'Regular'}\n", - "]\n", - "people.insert_all(null_data)\n", - "assert people.count == count + 3" - ] - }, - { - "cell_type": "markdown", - "id": "c92ada52", - "metadata": {}, - "source": [ - "Test with special characters in bulk" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "da81a215", - "metadata": {}, - "outputs": [], - "source": [ - "count = people.count\n", - "special_data = [\n", - " {'name': \"O'Brien\"},\n", - " {'name': 'José'},\n", - " {'name': '张伟'}\n", - "]\n", - "res = people.insert_all(special_data)\n", - "assert people.count == count + 3" - ] - }, - { - "cell_type": "markdown", - "id": "63b76213", - "metadata": {}, - "source": [ - "Test error on invalid column" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9d7d7991", - "metadata": {}, - "outputs": [], - "source": [ - "try:\n", - " people.insert_all([{'name': 'Valid'}, {'invalid_col': 'Bad'}])\n", + " p = people.update(dict(id=person.id, name='Extra', age=25, title='Dr'))\n", "except OperationalError as e:\n", - " assert 'no column named invalid_col' in str(e)" + " assert e.args[0] == 'no such column: age'" ] } ],