Thanks to visit codestin.com
Credit goes to github.com

Skip to content

gh-79097: Add support for aggregate window functions in sqlite3 #20903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 57 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
bbef5a4
Add support for sqlite3 aggregate window functions
May 24, 2020
93bfb30
Add NEWS entry
May 24, 2020
7dfeb13
Add What's New
Mar 10, 2021
aaccca2
Merge branch 'main' into fix-issue-34916
Jun 15, 2021
718cc8c
Merge branch 'main' into fix-issue-34916
Jun 20, 2021
fb968ab
Merge branch 'main' into fix-issue-34916
Jul 14, 2021
00fb71c
Move What's New from 3.10 to 3.11
Jul 14, 2021
394c877
Merge branch 'main' into fix-issue-34916
Jul 14, 2021
8779a1b
Merge branch 'main' into fix-issue-34916
Jul 29, 2021
8ad68e2
Add traceback test
Jul 29, 2021
def7cbd
Merge branch 'main' into fix-issue-34916
Jul 30, 2021
60ac350
Improve coverage
Jul 30, 2021
a7eac02
Fix segfault with missing step method
Jul 31, 2021
0ba3ea1
Improve coverage
Jul 31, 2021
7d4f71c
Improve test namespace
Jul 31, 2021
99f0c84
Adjust fixme comment
Jul 31, 2021
f4fea56
Adjust docs
Jul 31, 2021
6f9c8c2
Merge branch 'main' into fix-issue-34916
Aug 8, 2021
44999be
Test unable to set return value in value callback
Aug 8, 2021
25ecc84
Make sure test db is committed
Aug 13, 2021
cd1cd66
Merge branch 'main' into fix-issue-34916
Aug 24, 2021
d73adc9
Convert to use the new callback_context struct
Aug 24, 2021
ff9c559
Merge branch 'main' into fix-issue-34916
Aug 26, 2021
397e05a
Use set_sqlite_error in. step callback
Aug 26, 2021
be2b54f
WIP
Aug 26, 2021
97d26c4
Merge branch 'main' into fix-issue-34916
Aug 30, 2021
388e5a3
Revert "WIP"
Aug 30, 2021
aeb7a9f
Merge branch 'main' into fix-issue-34916
Aug 31, 2021
1a49676
Merge branch 'main' into fix-issue-34916
Sep 8, 2021
6764984
Fix merge
Sep 8, 2021
f1331f2
Raise more accurate error messages for methods that are not defined
Sep 12, 2021
869559d
Merge branch 'main' into fix-issue-34916
Sep 12, 2021
6f5ed2b
Merge branch 'main' into fix-issue-34916
Sep 14, 2021
be8a4b5
Merge branch 'main' into fix-issue-34916
Jan 22, 2022
f173178
Fixup merge
Jan 22, 2022
0a3c0d5
Merge branch 'main' into fix-issue-34916
Mar 3, 2022
f1fe332
Improve docstring wording in example
Mar 3, 2022
1babe16
Use interned string for method lookup
Mar 3, 2022
0f06428
Test adjustments
Mar 3, 2022
61cdd90
Add tests for finalize errors, and missing finalize methods
Mar 3, 2022
6606760
Don't use stdbool
Mar 3, 2022
bbf0bab
Add keyword test
Mar 3, 2022
a4e0eb6
Use static inline iso. macro
Mar 3, 2022
d31f51f
No need to check if SQLITE_DETERMINISTIC is supported
Mar 3, 2022
2bb1ec2
Test that flags are keyword only
Mar 4, 2022
99b752f
Reduce PR: simplify API by excluding flags for now
Mar 4, 2022
81287f2
Remove keywords from docs
Mar 4, 2022
c66c992
Remove useless include
Mar 4, 2022
f442f77
Raise ProgrammingError if unable to create fn
Mar 4, 2022
cfc4d6f
Reword What's New and NEWS
Mar 4, 2022
9355614
Reword docs
Mar 4, 2022
d5b816d
Reword docstring
Mar 4, 2022
22fdb3d
Squeeze error handlers
Mar 4, 2022
217da20
Expand explanatory comment
Mar 4, 2022
6a789ac
Clean up tests
Mar 4, 2022
340cea9
Merge branch 'main' into fix-issue-34916
Apr 10, 2022
431ace8
Address review
Apr 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions Doc/includes/sqlite3/sumintwindow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Example taken from https://www.sqlite.org/windowfunctions.html#udfwinfunc
import sqlite3


class WindowSumInt:
def __init__(self):
self.count = 0

def step(self, value):
"""Adds a row to the current window."""
self.count += value

def value(self):
"""Returns the current value of the aggregate."""
return self.count

def inverse(self, value):
"""Removes a row from the current window."""
self.count -= value

def finalize(self):
"""Returns the final value of the aggregate.

Any clean-up actions should be placed here.
"""
return self.count


con = sqlite3.connect(":memory:")
cur = con.execute("create table test(x, y)")
values = [
("a", 4),
("b", 5),
("c", 3),
("d", 8),
("e", 1),
]
cur.executemany("insert into test values(?, ?)", values)
con.create_window_function("sumint", 1, WindowSumInt)
cur.execute("""
select x, sumint(y) over (
order by x rows between 1 preceding and 1 following
) as sum_y
from test order by x
""")
print(cur.fetchall())
29 changes: 29 additions & 0 deletions Doc/library/sqlite3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,35 @@ Connection Objects
.. literalinclude:: ../includes/sqlite3/mysumaggr.py


.. method:: create_window_function(name, num_params, aggregate_class, /)

Creates user-defined aggregate window function *name*.

*aggregate_class* must implement the following methods:

* ``step``: adds a row to the current window
* ``value``: returns the current value of the aggregate
* ``inverse``: removes a row from the current window
* ``finalize``: returns the final value of the aggregate

``step`` and ``value`` accept *num_params* number of parameters,
unless *num_params* is ``-1``, in which case they may take any number of
arguments. ``finalize`` and ``value`` can return any of the types
supported by SQLite:
:class:`bytes`, :class:`str`, :class:`int`, :class:`float`, and
:const:`None`. Call :meth:`create_window_function` with
*aggregate_class* set to :const:`None` to clear window function *name*.

Aggregate window functions are supported by SQLite 3.25.0 and higher.
:exc:`NotSupportedError` will be raised if used with older versions.

.. versionadded:: 3.11

Example:

.. literalinclude:: ../includes/sqlite3/sumintwindow.py


.. method:: create_collation(name, callable)

Creates a collation with the specified *name* and *callable*. The callable will
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ sqlite3
serializing and deserializing databases.
(Contributed by Erlend E. Aasland in :issue:`41930`.)

* Add :meth:`~sqlite3.Connection.create_window_function` to
:class:`sqlite3.Connection` for creating aggregate window functions.
(Contributed by Erlend E. Aasland in :issue:`34916`.)


sys
---
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,8 @@ def test_check_connection_thread(self):
if hasattr(sqlite.Connection, "serialize"):
fns.append(lambda: self.con.serialize())
fns.append(lambda: self.con.deserialize(b""))
if sqlite.sqlite_version_info >= (3, 25, 0):
fns.append(lambda: self.con.create_window_function("foo", 0, None))

for fn in fns:
with self.subTest(fn=fn):
Expand Down
168 changes: 163 additions & 5 deletions Lib/test/test_sqlite3/test_userfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
import re
import sys
import unittest
import unittest.mock
import sqlite3 as sqlite

from unittest.mock import Mock, patch
from test.support import bigmemtest, catch_unraisable_exception, gc_collect

from test.test_sqlite3.test_dbapi import cx_limit
Expand Down Expand Up @@ -393,7 +393,7 @@ def append_result(arg):
# indices, which allows testing based on syntax, iso. the query optimizer.
@unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "Requires SQLite 3.8.3 or higher")
def test_func_non_deterministic(self):
mock = unittest.mock.Mock(return_value=None)
mock = Mock(return_value=None)
self.con.create_function("nondeterministic", 0, mock, deterministic=False)
if sqlite.sqlite_version_info < (3, 15, 0):
self.con.execute("select nondeterministic() = nondeterministic()")
Expand All @@ -404,7 +404,7 @@ def test_func_non_deterministic(self):

@unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "Requires SQLite 3.8.3 or higher")
def test_func_deterministic(self):
mock = unittest.mock.Mock(return_value=None)
mock = Mock(return_value=None)
self.con.create_function("deterministic", 0, mock, deterministic=True)
if sqlite.sqlite_version_info < (3, 15, 0):
self.con.execute("select deterministic() = deterministic()")
Expand Down Expand Up @@ -482,6 +482,164 @@ def test_func_return_illegal_value(self):
self.con.execute, "select badreturn()")


class WindowSumInt:
def __init__(self):
self.count = 0

def step(self, value):
self.count += value

def value(self):
return self.count

def inverse(self, value):
self.count -= value

def finalize(self):
return self.count

class BadWindow(Exception):
pass


@unittest.skipIf(sqlite.sqlite_version_info < (3, 25, 0),
"Requires SQLite 3.25.0 or newer")
class WindowFunctionTests(unittest.TestCase):
def setUp(self):
self.con = sqlite.connect(":memory:")
self.cur = self.con.cursor()

# Test case taken from https://www.sqlite.org/windowfunctions.html#udfwinfunc
values = [
("a", 4),
("b", 5),
("c", 3),
("d", 8),
("e", 1),
]
with self.con:
self.con.execute("create table test(x, y)")
self.con.executemany("insert into test values(?, ?)", values)
self.expected = [
("a", 9),
("b", 12),
("c", 16),
("d", 12),
("e", 9),
]
self.query = """
select x, %s(y) over (
order by x rows between 1 preceding and 1 following
) as sum_y
from test order by x
"""
self.con.create_window_function("sumint", 1, WindowSumInt)

def test_win_sum_int(self):
self.cur.execute(self.query % "sumint")
self.assertEqual(self.cur.fetchall(), self.expected)

def test_win_error_on_create(self):
self.assertRaises(sqlite.ProgrammingError,
self.con.create_window_function,
"shouldfail", -100, WindowSumInt)

@with_tracebacks(BadWindow)
def test_win_exception_in_method(self):
for meth in "__init__", "step", "value", "inverse":
with self.subTest(meth=meth):
with patch.object(WindowSumInt, meth, side_effect=BadWindow):
name = f"exc_{meth}"
self.con.create_window_function(name, 1, WindowSumInt)
msg = f"'{meth}' method raised error"
with self.assertRaisesRegex(sqlite.OperationalError, msg):
self.cur.execute(self.query % name)
self.cur.fetchall()

@with_tracebacks(BadWindow)
def test_win_exception_in_finalize(self):
# Note: SQLite does not (as of version 3.38.0) propagate finalize
# callback errors to sqlite3_step(); this implies that OperationalError
# is _not_ raised.
with patch.object(WindowSumInt, "finalize", side_effect=BadWindow):
name = f"exception_in_finalize"
self.con.create_window_function(name, 1, WindowSumInt)
self.cur.execute(self.query % name)
self.cur.fetchall()

@with_tracebacks(AttributeError)
def test_win_missing_method(self):
class MissingValue:
def step(self, x): pass
def inverse(self, x): pass
def finalize(self): return 42

class MissingInverse:
def step(self, x): pass
def value(self): return 42
def finalize(self): return 42

class MissingStep:
def value(self): return 42
def inverse(self, x): pass
def finalize(self): return 42

dataset = (
("step", MissingStep),
("value", MissingValue),
("inverse", MissingInverse),
)
for meth, cls in dataset:
with self.subTest(meth=meth, cls=cls):
name = f"exc_{meth}"
self.con.create_window_function(name, 1, cls)
with self.assertRaisesRegex(sqlite.OperationalError,
f"'{meth}' method not defined"):
self.cur.execute(self.query % name)
self.cur.fetchall()

@with_tracebacks(AttributeError)
def test_win_missing_finalize(self):
# Note: SQLite does not (as of version 3.38.0) propagate finalize
# callback errors to sqlite3_step(); this implies that OperationalError
# is _not_ raised.
class MissingFinalize:
def step(self, x): pass
def value(self): return 42
def inverse(self, x): pass

name = "missing_finalize"
self.con.create_window_function(name, 1, MissingFinalize)
self.cur.execute(self.query % name)
self.cur.fetchall()

def test_win_clear_function(self):
self.con.create_window_function("sumint", 1, None)
self.assertRaises(sqlite.OperationalError, self.cur.execute,
self.query % "sumint")

def test_win_redefine_function(self):
# Redefine WindowSumInt; adjust the expected results accordingly.
class Redefined(WindowSumInt):
def step(self, value): self.count += value * 2
def inverse(self, value): self.count -= value * 2
expected = [(v[0], v[1]*2) for v in self.expected]

self.con.create_window_function("sumint", 1, Redefined)
self.cur.execute(self.query % "sumint")
self.assertEqual(self.cur.fetchall(), expected)

def test_win_error_value_return(self):
class ErrorValueReturn:
def __init__(self): pass
def step(self, x): pass
def value(self): return 1 << 65

self.con.create_window_function("err_val_ret", 1, ErrorValueReturn)
self.assertRaisesRegex(sqlite.DataError, "string or blob too big",
self.cur.execute, self.query % "err_val_ret")


class AggregateTests(unittest.TestCase):
def setUp(self):
self.con = sqlite.connect(":memory:")
Expand Down Expand Up @@ -527,10 +685,10 @@ def test_aggr_no_step(self):

def test_aggr_no_finalize(self):
cur = self.con.cursor()
with self.assertRaises(sqlite.OperationalError) as cm:
msg = "user-defined aggregate's 'finalize' method not defined"
with self.assertRaisesRegex(sqlite.OperationalError, msg):
cur.execute("select nofinalize(t) from test")
val = cur.fetchone()[0]
self.assertEqual(str(cm.exception), "user-defined aggregate's 'finalize' method raised error")

@with_tracebacks(ZeroDivisionError, name="AggrExceptionInInit")
def test_aggr_exception_in_init(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :meth:`~sqlite3.Connection.create_window_function` to
:class:`sqlite3.Connection` for creating aggregate window functions.
Patch by Erlend E. Aasland.
53 changes: 52 additions & 1 deletion Modules/_sqlite/clinic/connection.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading