Skip to content

Commit 725a8cc

Browse files
gh-84943: Add support for 'directonly' and 'innocuous' flags for user-defined functions
Co-authored-by: Erlend E. Aasland <erlend@python.org>
1 parent 5f8d9d3 commit 725a8cc

5 files changed

Lines changed: 409 additions & 36 deletions

File tree

Doc/library/sqlite3.rst

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ Connection objects
699699
:meth:`~Cursor.executescript` on it with the given *sql_script*.
700700
Return the new cursor object.
701701

702-
.. method:: create_function(name, narg, func, /, *, deterministic=False)
702+
.. method:: create_function(name, narg, func, /, *, deterministic=False, innocuous=False, directonly=False)
703703

704704
Create or remove a user-defined SQL function.
705705

@@ -722,12 +722,31 @@ Connection objects
722722
`deterministic <https://sqlite.org/deterministic.html>`_,
723723
which allows SQLite to perform additional optimizations.
724724

725+
:param bool innocuous:
726+
If ``True``, the created SQL function is marked as
727+
`innocuous <https://sqlite.org/c3ref/c_deterministic.html#sqliteinnocuous>`__,
728+
making it usable in views, triggers and schema structures even when
729+
the ``trusted_schema`` pragma is disabled.
730+
731+
:param bool directonly:
732+
If ``True``, the created SQL function is marked as
733+
`directonly <https://sqlite.org/c3ref/c_deterministic.html#sqlitedirectonly>`__,
734+
restricting its use to top-level SQL statements regardless of the
735+
value of the ``trusted_schema`` pragma.
736+
737+
:raises NotSupportedError:
738+
If called with *innocuous* or *directonly* equal to True on a version
739+
of SQLite older than 3.31.0.
740+
725741
.. versionchanged:: 3.8
726742
Added the *deterministic* parameter.
727743

728744
.. versionchanged:: 3.15
729745
The first three parameters are now positional-only.
730746

747+
.. versionchanged:: next
748+
Added the *innocuous* and *directonly* parameters.
749+
731750
Example:
732751

733752
.. doctest::
@@ -743,7 +762,7 @@ Connection objects
743762
>>> con.close()
744763

745764

746-
.. method:: create_aggregate(name, n_arg, aggregate_class, /)
765+
.. method:: create_aggregate(name, n_arg, aggregate_class, /, *, deterministic=False, innocuous=False, directonly=False)
747766

748767
Create or remove a user-defined SQL aggregate function.
749768

@@ -767,9 +786,33 @@ Connection objects
767786
Set to ``None`` to remove an existing SQL aggregate function.
768787
:type aggregate_class: :term:`class` | None
769788

789+
:param bool deterministic:
790+
If ``True``, the created SQL function is marked as
791+
`deterministic <https://sqlite.org/deterministic.html>`__,
792+
which allows SQLite to perform additional optimizations.
793+
794+
:param bool innocuous:
795+
If ``True``, the created SQL function is marked as
796+
`innocuous <https://sqlite.org/c3ref/c_deterministic.html#sqliteinnocuous>`__,
797+
making it usable in views, triggers and schema structures even when
798+
the ``trusted_schema`` pragma is disabled.
799+
800+
:param bool directonly:
801+
If ``True``, the created SQL function is marked as
802+
`directonly <https://sqlite.org/c3ref/c_deterministic.html#sqlitedirectonly>`__,
803+
restricting its use to top-level SQL statements regardless of the
804+
value of the ``trusted_schema`` pragma.
805+
806+
:raises NotSupportedError:
807+
If called with *innocuous* or *directonly* equal to True on a version
808+
of SQLite older than 3.31.0.
809+
770810
.. versionchanged:: 3.15
771811
All three parameters are now positional-only.
772812

813+
.. versionchanged:: next
814+
Added the *deterministic*, *innocuous* and *directonly* parameters.
815+
773816
Example:
774817

775818
.. testcode::
@@ -800,7 +843,7 @@ Connection objects
800843
3
801844

802845

803-
.. method:: create_window_function(name, num_params, aggregate_class, /)
846+
.. method:: create_window_function(name, num_params, aggregate_class, /, *, deterministic=False, innocuous=False, directonly=False)
804847

805848
Create or remove a user-defined aggregate window function.
806849

@@ -825,14 +868,38 @@ Connection objects
825868

826869
Set to ``None`` to remove an existing SQL aggregate window function.
827870

871+
:param bool deterministic:
872+
If ``True``, the created SQL function is marked as
873+
`deterministic <https://sqlite.org/deterministic.html>`__,
874+
which allows SQLite to perform additional optimizations.
875+
876+
:param bool innocuous:
877+
If ``True``, the created SQL function is marked as
878+
`innocuous <https://sqlite.org/c3ref/c_deterministic.html#sqliteinnocuous>`__,
879+
making it usable in views, triggers and schema structures even when
880+
the ``trusted_schema`` pragma is disabled.
881+
882+
:param bool directonly:
883+
If ``True``, the created SQL function is marked as
884+
`directonly <https://sqlite.org/c3ref/c_deterministic.html#sqlitedirectonly>`__,
885+
restricting its use to top-level SQL statements regardless of the
886+
value of the ``trusted_schema`` pragma.
887+
828888
:raises NotSupportedError:
829889
If used with a version of SQLite older than 3.25.0,
830890
which does not support aggregate window functions.
831891

892+
:raises NotSupportedError:
893+
If called with *innocuous* or *directonly* equal to True on a version
894+
of SQLite older than 3.31.0.
895+
832896
:type aggregate_class: :term:`class` | None
833897

834898
.. versionadded:: 3.11
835899

900+
.. versionchanged:: next
901+
Added the *deterministic*, *innocuous* and *directonly* parameters.
902+
836903
Example:
837904

838905
.. testcode::

Lib/test/test_sqlite3/test_userfunctions.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,40 @@ def test_func_deterministic_keyword_only(self):
363363
with self.assertRaises(TypeError):
364364
self.con.create_function("deterministic", 0, int, True)
365365

366+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
367+
"Requires SQLite 3.31.0 or higher")
368+
def test_func_non_innocuous_in_trusted_env(self):
369+
mock = Mock(return_value=None)
370+
self.con.create_function("noninnocuous", 0, mock, innocuous=False)
371+
self.con.execute("pragma trusted_schema = 0")
372+
self.con.execute("create view notallowed as select noninnocuous() = noninnocuous()")
373+
with self.assertRaises(sqlite.OperationalError) as cm:
374+
self.con.execute("select * from notallowed")
375+
self.assertEqual(str(cm.exception), 'unsafe use of noninnocuous()')
376+
377+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
378+
"Requires SQLite 3.31.0 or higher")
379+
def test_func_innocuous_in_trusted_env(self):
380+
mock = Mock(return_value=None)
381+
self.con.create_function("innocuous", 0, mock, innocuous=True)
382+
self.con.execute("pragma trusted_schema = 0")
383+
self.con.execute("create view allowed as select innocuous() = innocuous()")
384+
self.con.execute("select * from allowed")
385+
self.assertEqual(mock.call_count, 2)
386+
387+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
388+
"Requires SQLite 3.31.0 or higher")
389+
def test_func_direct_only(self):
390+
mock = Mock(return_value=None)
391+
self.con.create_function("directonly", 0, mock, directonly=True)
392+
self.con.execute("pragma trusted_schema = 1")
393+
self.con.execute("select directonly() = directonly()")
394+
self.assertEqual(mock.call_count, 2)
395+
self.con.execute("create view notallowed as select directonly() = directonly()")
396+
with self.assertRaises(sqlite.OperationalError) as cm:
397+
self.con.execute("select * from notallowed")
398+
self.assertEqual(str(cm.exception), 'unsafe use of directonly()')
399+
366400
def test_function_destructor_via_gc(self):
367401
# See bpo-44304: The destructor of the user function can
368402
# crash if is called without the GIL from the gc functions
@@ -479,6 +513,9 @@ def setUp(self):
479513
from test order by x
480514
"""
481515
self.con.create_window_function("sumint", 1, WindowSumInt)
516+
if sqlite.sqlite_version_info >= (3, 31, 0):
517+
self.con.create_window_function("sumintInnocuous", 1, WindowSumInt, innocuous=True)
518+
self.con.create_window_function("sumintDirectOnly", 1, WindowSumInt, directonly=True)
482519

483520
def tearDown(self):
484521
self.cur.close()
@@ -488,6 +525,34 @@ def test_win_sum_int(self):
488525
self.cur.execute(self.query % "sumint")
489526
self.assertEqual(self.cur.fetchall(), self.expected)
490527

528+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
529+
"Requires SQLite 3.31.0 or newer")
530+
def test_win_non_innocuous(self):
531+
self.cur.execute("pragma trusted_schema = 0")
532+
self.cur.execute("create view notallowed as " + self.query % "sumint")
533+
with self.assertRaises(sqlite.OperationalError) as cm:
534+
self.cur.execute("select * from notallowed")
535+
self.assertEqual(str(cm.exception), 'unsafe use of sumint()')
536+
537+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
538+
"Requires SQLite 3.31.0 or newer")
539+
def test_win_innocuous(self):
540+
self.cur.execute("pragma trusted_schema = 0")
541+
self.cur.execute("create view allowed as " + self.query % "sumintInnocuous")
542+
self.cur.execute("select * from allowed")
543+
self.assertEqual(self.cur.fetchall(), self.expected)
544+
545+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
546+
"Requires SQLite 3.31.0 or newer")
547+
def test_win_directonly(self):
548+
self.cur.execute("pragma trusted_schema = 1")
549+
self.cur.execute("create view notallowed as " + self.query % "sumintDirectOnly")
550+
with self.assertRaises(sqlite.OperationalError) as cm:
551+
self.cur.execute("select * from notallowed")
552+
self.assertEqual(str(cm.exception), 'unsafe use of sumintDirectOnly()')
553+
self.cur.execute(self.query % "sumintDirectOnly")
554+
self.assertEqual(self.cur.fetchall(), self.expected)
555+
491556
def test_win_error_on_create(self):
492557
with self.assertRaisesRegex(sqlite.ProgrammingError, "not -100"):
493558
self.con.create_window_function("shouldfail", -100, WindowSumInt)
@@ -614,6 +679,9 @@ def setUp(self):
614679
self.con.create_aggregate("checkTypes", -1, AggrCheckTypes)
615680
self.con.create_aggregate("mysum", 1, AggrSum)
616681
self.con.create_aggregate("aggtxt", 1, AggrText)
682+
if sqlite.sqlite_version_info >= (3, 31, 0):
683+
self.con.create_aggregate("mysumInnocuous", 1, AggrSum, innocuous=True)
684+
self.con.create_aggregate("mysumDirectOnly", 1, AggrSum, directonly=True)
617685

618686
def tearDown(self):
619687
self.con.close()
@@ -705,6 +773,45 @@ def test_aggr_check_aggr_sum(self):
705773
val = cur.fetchone()[0]
706774
self.assertEqual(val, 60)
707775

776+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
777+
"Requires SQLite 3.31.0 or newer")
778+
def test_aggr_non_innocuous(self):
779+
cur = self.con.cursor()
780+
cur.execute("pragma trusted_schema = 0")
781+
cur.execute("delete from test")
782+
cur.execute("insert into test(i) values (?)", (10,))
783+
cur.execute("create view notallowed as select mysum(i) from test")
784+
with self.assertRaises(sqlite.OperationalError) as cm:
785+
cur.execute("select * from notallowed")
786+
self.assertEqual(str(cm.exception), 'unsafe use of mysum()')
787+
788+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
789+
"Requires SQLite 3.31.0 or newer")
790+
def test_aggr_innocuous(self):
791+
cur = self.con.cursor()
792+
cur.execute("pragma trusted_schema = 0")
793+
cur.execute("delete from test")
794+
cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)])
795+
cur.execute("create view allowed as select mysumInnocuous(i) from test")
796+
cur.execute("select * from allowed")
797+
val = cur.fetchone()[0]
798+
self.assertEqual(val, 60)
799+
800+
@unittest.skipIf(sqlite.sqlite_version_info < (3, 31, 0),
801+
"Requires SQLite 3.31.0 or newer")
802+
def test_aggr_directonly(self):
803+
cur = self.con.cursor()
804+
cur.execute("pragma trusted_schema = 1")
805+
cur.execute("delete from test")
806+
cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)])
807+
cur.execute("create view notallowed as select mysumDirectOnly(i) from test")
808+
with self.assertRaises(sqlite.OperationalError) as cm:
809+
cur.execute("select * from notallowed")
810+
self.assertEqual(str(cm.exception), 'unsafe use of mysumDirectOnly()')
811+
cur.execute("select mysumDirectOnly(i) from test")
812+
val = cur.fetchone()[0]
813+
self.assertEqual(val, 60)
814+
708815
def test_aggr_no_match(self):
709816
cur = self.con.execute("select mysum(i) from (select 1 as i) where i == 0")
710817
val = cur.fetchone()[0]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support for ``SQLITE_INNOCUOUS`` and ``SQLITE_DIRECTONLY`` flags in
2+
:mod:`sqlite3`.

0 commit comments

Comments
 (0)