2727import re
2828import sys
2929import unittest
30- import unittest .mock
3130import sqlite3 as sqlite
3231
32+ from unittest .mock import Mock , patch
3333from test .support import bigmemtest , catch_unraisable_exception , gc_collect
3434
3535from test .test_sqlite3 .test_dbapi import cx_limit
@@ -393,7 +393,7 @@ def append_result(arg):
393393 # indices, which allows testing based on syntax, iso. the query optimizer.
394394 @unittest .skipIf (sqlite .sqlite_version_info < (3 , 8 , 3 ), "Requires SQLite 3.8.3 or higher" )
395395 def test_func_non_deterministic (self ):
396- mock = unittest . mock . Mock (return_value = None )
396+ mock = Mock (return_value = None )
397397 self .con .create_function ("nondeterministic" , 0 , mock , deterministic = False )
398398 if sqlite .sqlite_version_info < (3 , 15 , 0 ):
399399 self .con .execute ("select nondeterministic() = nondeterministic()" )
@@ -404,7 +404,7 @@ def test_func_non_deterministic(self):
404404
405405 @unittest .skipIf (sqlite .sqlite_version_info < (3 , 8 , 3 ), "Requires SQLite 3.8.3 or higher" )
406406 def test_func_deterministic (self ):
407- mock = unittest . mock . Mock (return_value = None )
407+ mock = Mock (return_value = None )
408408 self .con .create_function ("deterministic" , 0 , mock , deterministic = True )
409409 if sqlite .sqlite_version_info < (3 , 15 , 0 ):
410410 self .con .execute ("select deterministic() = deterministic()" )
@@ -482,6 +482,164 @@ def test_func_return_illegal_value(self):
482482 self .con .execute , "select badreturn()" )
483483
484484
485+ class WindowSumInt :
486+ def __init__ (self ):
487+ self .count = 0
488+
489+ def step (self , value ):
490+ self .count += value
491+
492+ def value (self ):
493+ return self .count
494+
495+ def inverse (self , value ):
496+ self .count -= value
497+
498+ def finalize (self ):
499+ return self .count
500+
501+ class BadWindow (Exception ):
502+ pass
503+
504+
505+ @unittest .skipIf (sqlite .sqlite_version_info < (3 , 25 , 0 ),
506+ "Requires SQLite 3.25.0 or newer" )
507+ class WindowFunctionTests (unittest .TestCase ):
508+ def setUp (self ):
509+ self .con = sqlite .connect (":memory:" )
510+ self .cur = self .con .cursor ()
511+
512+ # Test case taken from https://www.sqlite.org/windowfunctions.html#udfwinfunc
513+ values = [
514+ ("a" , 4 ),
515+ ("b" , 5 ),
516+ ("c" , 3 ),
517+ ("d" , 8 ),
518+ ("e" , 1 ),
519+ ]
520+ with self .con :
521+ self .con .execute ("create table test(x, y)" )
522+ self .con .executemany ("insert into test values(?, ?)" , values )
523+ self .expected = [
524+ ("a" , 9 ),
525+ ("b" , 12 ),
526+ ("c" , 16 ),
527+ ("d" , 12 ),
528+ ("e" , 9 ),
529+ ]
530+ self .query = """
531+ select x, %s(y) over (
532+ order by x rows between 1 preceding and 1 following
533+ ) as sum_y
534+ from test order by x
535+ """
536+ self .con .create_window_function ("sumint" , 1 , WindowSumInt )
537+
538+ def test_win_sum_int (self ):
539+ self .cur .execute (self .query % "sumint" )
540+ self .assertEqual (self .cur .fetchall (), self .expected )
541+
542+ def test_win_error_on_create (self ):
543+ self .assertRaises (sqlite .ProgrammingError ,
544+ self .con .create_window_function ,
545+ "shouldfail" , - 100 , WindowSumInt )
546+
547+ @with_tracebacks (BadWindow )
548+ def test_win_exception_in_method (self ):
549+ for meth in "__init__" , "step" , "value" , "inverse" :
550+ with self .subTest (meth = meth ):
551+ with patch .object (WindowSumInt , meth , side_effect = BadWindow ):
552+ name = f"exc_{ meth } "
553+ self .con .create_window_function (name , 1 , WindowSumInt )
554+ msg = f"'{ meth } ' method raised error"
555+ with self .assertRaisesRegex (sqlite .OperationalError , msg ):
556+ self .cur .execute (self .query % name )
557+ self .cur .fetchall ()
558+
559+ @with_tracebacks (BadWindow )
560+ def test_win_exception_in_finalize (self ):
561+ # Note: SQLite does not (as of version 3.38.0) propagate finalize
562+ # callback errors to sqlite3_step(); this implies that OperationalError
563+ # is _not_ raised.
564+ with patch .object (WindowSumInt , "finalize" , side_effect = BadWindow ):
565+ name = f"exception_in_finalize"
566+ self .con .create_window_function (name , 1 , WindowSumInt )
567+ self .cur .execute (self .query % name )
568+ self .cur .fetchall ()
569+
570+ @with_tracebacks (AttributeError )
571+ def test_win_missing_method (self ):
572+ class MissingValue :
573+ def step (self , x ): pass
574+ def inverse (self , x ): pass
575+ def finalize (self ): return 42
576+
577+ class MissingInverse :
578+ def step (self , x ): pass
579+ def value (self ): return 42
580+ def finalize (self ): return 42
581+
582+ class MissingStep :
583+ def value (self ): return 42
584+ def inverse (self , x ): pass
585+ def finalize (self ): return 42
586+
587+ dataset = (
588+ ("step" , MissingStep ),
589+ ("value" , MissingValue ),
590+ ("inverse" , MissingInverse ),
591+ )
592+ for meth , cls in dataset :
593+ with self .subTest (meth = meth , cls = cls ):
594+ name = f"exc_{ meth } "
595+ self .con .create_window_function (name , 1 , cls )
596+ with self .assertRaisesRegex (sqlite .OperationalError ,
597+ f"'{ meth } ' method not defined" ):
598+ self .cur .execute (self .query % name )
599+ self .cur .fetchall ()
600+
601+ @with_tracebacks (AttributeError )
602+ def test_win_missing_finalize (self ):
603+ # Note: SQLite does not (as of version 3.38.0) propagate finalize
604+ # callback errors to sqlite3_step(); this implies that OperationalError
605+ # is _not_ raised.
606+ class MissingFinalize :
607+ def step (self , x ): pass
608+ def value (self ): return 42
609+ def inverse (self , x ): pass
610+
611+ name = "missing_finalize"
612+ self .con .create_window_function (name , 1 , MissingFinalize )
613+ self .cur .execute (self .query % name )
614+ self .cur .fetchall ()
615+
616+ def test_win_clear_function (self ):
617+ self .con .create_window_function ("sumint" , 1 , None )
618+ self .assertRaises (sqlite .OperationalError , self .cur .execute ,
619+ self .query % "sumint" )
620+
621+ def test_win_redefine_function (self ):
622+ # Redefine WindowSumInt; adjust the expected results accordingly.
623+ class Redefined (WindowSumInt ):
624+ def step (self , value ): self .count += value * 2
625+ def inverse (self , value ): self .count -= value * 2
626+ expected = [(v [0 ], v [1 ]* 2 ) for v in self .expected ]
627+
628+ self .con .create_window_function ("sumint" , 1 , Redefined )
629+ self .cur .execute (self .query % "sumint" )
630+ self .assertEqual (self .cur .fetchall (), expected )
631+
632+ def test_win_error_value_return (self ):
633+ class ErrorValueReturn :
634+ def __init__ (self ): pass
635+ def step (self , x ): pass
636+ def value (self ): return 1 << 65
637+
638+ self .con .create_window_function ("err_val_ret" , 1 , ErrorValueReturn )
639+ self .assertRaisesRegex (sqlite .DataError , "string or blob too big" ,
640+ self .cur .execute , self .query % "err_val_ret" )
641+
642+
485643class AggregateTests (unittest .TestCase ):
486644 def setUp (self ):
487645 self .con = sqlite .connect (":memory:" )
@@ -527,10 +685,10 @@ def test_aggr_no_step(self):
527685
528686 def test_aggr_no_finalize (self ):
529687 cur = self .con .cursor ()
530- with self .assertRaises (sqlite .OperationalError ) as cm :
688+ msg = "user-defined aggregate's 'finalize' method not defined"
689+ with self .assertRaisesRegex (sqlite .OperationalError , msg ):
531690 cur .execute ("select nofinalize(t) from test" )
532691 val = cur .fetchone ()[0 ]
533- self .assertEqual (str (cm .exception ), "user-defined aggregate's 'finalize' method raised error" )
534692
535693 @with_tracebacks (ZeroDivisionError , name = "AggrExceptionInInit" )
536694 def test_aggr_exception_in_init (self ):
0 commit comments