diff --git a/Cargo.lock b/Cargo.lock index 779024042..a68d8e6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1156,6 +1156,17 @@ dependencies = [ "cc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.8" @@ -2102,6 +2113,7 @@ dependencies = [ "itertools", "lexical-parse-float", "libc", + "libsqlite3-sys", "libz-sys", "mac_address", "md-5", diff --git a/Lib/sqlite3/__init__.py b/Lib/sqlite3/__init__.py new file mode 100644 index 000000000..927267cf0 --- /dev/null +++ b/Lib/sqlite3/__init__.py @@ -0,0 +1,70 @@ +# pysqlite2/__init__.py: the pysqlite2 package. +# +# Copyright (C) 2005 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +""" +The sqlite3 extension module provides a DB-API 2.0 (PEP 249) compliant +interface to the SQLite library, and requires SQLite 3.7.15 or newer. + +To use the module, start by creating a database Connection object: + + import sqlite3 + cx = sqlite3.connect("test.db") # test.db will be created or opened + +The special path name ":memory:" can be provided to connect to a transient +in-memory database: + + cx = sqlite3.connect(":memory:") # connect to a database in RAM + +Once a connection has been established, create a Cursor object and call +its execute() method to perform SQL queries: + + cu = cx.cursor() + + # create a table + cu.execute("create table lang(name, first_appeared)") + + # insert values into a table + cu.execute("insert into lang values (?, ?)", ("C", 1972)) + + # execute a query and iterate over the result + for row in cu.execute("select * from lang"): + print(row) + + cx.close() + +The sqlite3 module is written by Gerhard Häring . +""" + +from sqlite3.dbapi2 import * +from sqlite3.dbapi2 import (_deprecated_names, + _deprecated_version_info, + _deprecated_version) + + +def __getattr__(name): + if name in _deprecated_names: + from warnings import warn + + warn(f"{name} is deprecated and will be removed in Python 3.14", + DeprecationWarning, stacklevel=2) + return globals()[f"_deprecated_{name}"] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/sqlite3/__main__.py b/Lib/sqlite3/__main__.py new file mode 100644 index 000000000..1832fc130 --- /dev/null +++ b/Lib/sqlite3/__main__.py @@ -0,0 +1,132 @@ +"""A simple SQLite CLI for the sqlite3 module. + +Apart from using 'argparse' for the command-line interface, +this module implements the REPL as a thin wrapper around +the InteractiveConsole class from the 'code' stdlib module. +""" +import sqlite3 +import sys + +from argparse import ArgumentParser +from code import InteractiveConsole +from textwrap import dedent + + +def execute(c, sql, suppress_errors=True): + """Helper that wraps execution of SQL code. + + This is used both by the REPL and by direct execution from the CLI. + + 'c' may be a cursor or a connection. + 'sql' is the SQL string to execute. + """ + + try: + for row in c.execute(sql): + print(row) + except sqlite3.Error as e: + tp = type(e).__name__ + try: + print(f"{tp} ({e.sqlite_errorname}): {e}", file=sys.stderr) + except AttributeError: + print(f"{tp}: {e}", file=sys.stderr) + if not suppress_errors: + sys.exit(1) + + +class SqliteInteractiveConsole(InteractiveConsole): + """A simple SQLite REPL.""" + + def __init__(self, connection): + super().__init__() + self._con = connection + self._cur = connection.cursor() + + def runsource(self, source, filename="", symbol="single"): + """Override runsource, the core of the InteractiveConsole REPL. + + Return True if more input is needed; buffering is done automatically. + Return False is input is a complete statement ready for execution. + """ + if source == ".version": + print(f"{sqlite3.sqlite_version}") + elif source == ".help": + print("Enter SQL code and press enter.") + elif source == ".quit": + sys.exit(0) + elif not sqlite3.complete_statement(source): + return True + else: + execute(self._cur, source) + return False + # TODO: RUSTPYTHON match statement supporting + # match source: + # case ".version": + # print(f"{sqlite3.sqlite_version}") + # case ".help": + # print("Enter SQL code and press enter.") + # case ".quit": + # sys.exit(0) + # case _: + # if not sqlite3.complete_statement(source): + # return True + # execute(self._cur, source) + # return False + + +def main(): + parser = ArgumentParser( + description="Python sqlite3 CLI", + prog="python -m sqlite3", + ) + parser.add_argument( + "filename", type=str, default=":memory:", nargs="?", + help=( + "SQLite database to open (defaults to ':memory:'). " + "A new database is created if the file does not previously exist." + ), + ) + parser.add_argument( + "sql", type=str, nargs="?", + help=( + "An SQL query to execute. " + "Any returned rows are printed to stdout." + ), + ) + parser.add_argument( + "-v", "--version", action="version", + version=f"SQLite version {sqlite3.sqlite_version}", + help="Print underlying SQLite library version", + ) + args = parser.parse_args() + + if args.filename == ":memory:": + db_name = "a transient in-memory database" + else: + db_name = repr(args.filename) + + # Prepare REPL banner and prompts. + banner = dedent(f""" + sqlite3 shell, running on SQLite version {sqlite3.sqlite_version} + Connected to {db_name} + + Each command will be run using execute() on the cursor. + Type ".help" for more information; type ".quit" or CTRL-D to quit. + """).strip() + sys.ps1 = "sqlite> " + sys.ps2 = " ... " + + con = sqlite3.connect(args.filename, isolation_level=None) + try: + if args.sql: + # SQL statement provided on the command-line; execute it directly. + execute(con, args.sql, suppress_errors=False) + else: + # No SQL provided; start the REPL. + console = SqliteInteractiveConsole(con) + console.interact(banner, exitmsg="") + finally: + con.close() + + +main() diff --git a/Lib/sqlite3/dbapi2.py b/Lib/sqlite3/dbapi2.py new file mode 100644 index 000000000..56fc0461e --- /dev/null +++ b/Lib/sqlite3/dbapi2.py @@ -0,0 +1,108 @@ +# pysqlite2/dbapi2.py: the DB-API 2.0 interface +# +# Copyright (C) 2004-2005 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import datetime +import time +import collections.abc + +from _sqlite3 import * +from _sqlite3 import _deprecated_version + +_deprecated_names = frozenset({"version", "version_info"}) + +paramstyle = "qmark" + +apilevel = "2.0" + +Date = datetime.date + +Time = datetime.time + +Timestamp = datetime.datetime + +def DateFromTicks(ticks): + return Date(*time.localtime(ticks)[:3]) + +def TimeFromTicks(ticks): + return Time(*time.localtime(ticks)[3:6]) + +def TimestampFromTicks(ticks): + return Timestamp(*time.localtime(ticks)[:6]) + +_deprecated_version_info = tuple(map(int, _deprecated_version.split("."))) +sqlite_version_info = tuple([int(x) for x in sqlite_version.split(".")]) + +Binary = memoryview +collections.abc.Sequence.register(Row) + +def register_adapters_and_converters(): + from warnings import warn + + msg = ("The default {what} is deprecated as of Python 3.12; " + "see the sqlite3 documentation for suggested replacement recipes") + + def adapt_date(val): + warn(msg.format(what="date adapter"), DeprecationWarning, stacklevel=2) + return val.isoformat() + + def adapt_datetime(val): + warn(msg.format(what="datetime adapter"), DeprecationWarning, stacklevel=2) + return val.isoformat(" ") + + def convert_date(val): + warn(msg.format(what="date converter"), DeprecationWarning, stacklevel=2) + return datetime.date(*map(int, val.split(b"-"))) + + def convert_timestamp(val): + warn(msg.format(what="timestamp converter"), DeprecationWarning, stacklevel=2) + datepart, timepart = val.split(b" ") + year, month, day = map(int, datepart.split(b"-")) + timepart_full = timepart.split(b".") + hours, minutes, seconds = map(int, timepart_full[0].split(b":")) + if len(timepart_full) == 2: + microseconds = int('{:0<6.6}'.format(timepart_full[1].decode())) + else: + microseconds = 0 + + val = datetime.datetime(year, month, day, hours, minutes, seconds, microseconds) + return val + + + register_adapter(datetime.date, adapt_date) + register_adapter(datetime.datetime, adapt_datetime) + register_converter("date", convert_date) + register_converter("timestamp", convert_timestamp) + +register_adapters_and_converters() + +# Clean up namespace + +del(register_adapters_and_converters) + +def __getattr__(name): + if name in _deprecated_names: + from warnings import warn + + warn(f"{name} is deprecated and will be removed in Python 3.14", + DeprecationWarning, stacklevel=2) + return globals()[f"_deprecated_{name}"] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/sqlite3/dump.py b/Lib/sqlite3/dump.py new file mode 100644 index 000000000..07b9da10b --- /dev/null +++ b/Lib/sqlite3/dump.py @@ -0,0 +1,82 @@ +# Mimic the sqlite3 console shell's .dump command +# Author: Paul Kippes + +# Every identifier in sql is quoted based on a comment in sqlite +# documentation "SQLite adds new keywords from time to time when it +# takes on new features. So to prevent your code from being broken by +# future enhancements, you should normally quote any identifier that +# is an English language word, even if you do not have to." + +def _iterdump(connection): + """ + Returns an iterator to the dump of the database in an SQL text format. + + Used to produce an SQL dump of the database. Useful to save an in-memory + database for later restoration. This function should not be called + directly but instead called from the Connection method, iterdump(). + """ + + cu = connection.cursor() + yield('BEGIN TRANSACTION;') + + # sqlite_master table contains the SQL CREATE statements for the database. + q = """ + SELECT "name", "type", "sql" + FROM "sqlite_master" + WHERE "sql" NOT NULL AND + "type" == 'table' + ORDER BY "name" + """ + schema_res = cu.execute(q) + sqlite_sequence = [] + for table_name, type, sql in schema_res.fetchall(): + if table_name == 'sqlite_sequence': + rows = cu.execute('SELECT * FROM "sqlite_sequence";').fetchall() + sqlite_sequence = ['DELETE FROM "sqlite_sequence"'] + sqlite_sequence += [ + f'INSERT INTO "sqlite_sequence" VALUES(\'{row[0]}\',{row[1]})' + for row in rows + ] + continue + elif table_name == 'sqlite_stat1': + yield('ANALYZE "sqlite_master";') + elif table_name.startswith('sqlite_'): + continue + # NOTE: Virtual table support not implemented + #elif sql.startswith('CREATE VIRTUAL TABLE'): + # qtable = table_name.replace("'", "''") + # yield("INSERT INTO sqlite_master(type,name,tbl_name,rootpage,sql)"\ + # "VALUES('table','{0}','{0}',0,'{1}');".format( + # qtable, + # sql.replace("''"))) + else: + yield('{0};'.format(sql)) + + # Build the insert statement for each row of the current table + table_name_ident = table_name.replace('"', '""') + res = cu.execute('PRAGMA table_info("{0}")'.format(table_name_ident)) + column_names = [str(table_info[1]) for table_info in res.fetchall()] + q = """SELECT 'INSERT INTO "{0}" VALUES({1})' FROM "{0}";""".format( + table_name_ident, + ",".join("""'||quote("{0}")||'""".format(col.replace('"', '""')) for col in column_names)) + query_res = cu.execute(q) + for row in query_res: + yield("{0};".format(row[0])) + + # Now when the type is 'index', 'trigger', or 'view' + q = """ + SELECT "name", "type", "sql" + FROM "sqlite_master" + WHERE "sql" NOT NULL AND + "type" IN ('index', 'trigger', 'view') + """ + schema_res = cu.execute(q) + for name, type, sql in schema_res.fetchall(): + yield('{0};'.format(sql)) + + # gh-79009: Yield statements concerning the sqlite_sequence table at the + # end of the transaction. + for row in sqlite_sequence: + yield('{0};'.format(row)) + + yield('COMMIT;') diff --git a/Lib/test/test_sqlite3/__init__.py b/Lib/test/test_sqlite3/__init__.py new file mode 100644 index 000000000..d777fca82 --- /dev/null +++ b/Lib/test/test_sqlite3/__init__.py @@ -0,0 +1,15 @@ +from test.support import import_helper, load_package_tests, verbose + +# Skip test if _sqlite3 module not installed. +import_helper.import_module('_sqlite3') + +import os +import sqlite3 + +# Implement the unittest "load tests" protocol. +def load_tests(*args): + pkg_dir = os.path.dirname(__file__) + return load_package_tests(pkg_dir, *args) + +if verbose: + print(f"test_sqlite3: testing with SQLite version {sqlite3.sqlite_version}") diff --git a/Lib/test/test_sqlite3/__main__.py b/Lib/test/test_sqlite3/__main__.py new file mode 100644 index 000000000..ca6a8347f --- /dev/null +++ b/Lib/test/test_sqlite3/__main__.py @@ -0,0 +1,4 @@ +from test.test_sqlite3 import load_tests # Needed for the "load tests" protocol. +import unittest + +unittest.main() diff --git a/Lib/test/test_sqlite3/test_backup.py b/Lib/test/test_sqlite3/test_backup.py new file mode 100644 index 000000000..fb3a83e3b --- /dev/null +++ b/Lib/test/test_sqlite3/test_backup.py @@ -0,0 +1,166 @@ +import sqlite3 as sqlite +import unittest + + +class BackupTests(unittest.TestCase): + def setUp(self): + cx = self.cx = sqlite.connect(":memory:") + cx.execute('CREATE TABLE foo (key INTEGER)') + cx.executemany('INSERT INTO foo (key) VALUES (?)', [(3,), (4,)]) + cx.commit() + + def tearDown(self): + self.cx.close() + + def verify_backup(self, bckcx): + result = bckcx.execute("SELECT key FROM foo ORDER BY key").fetchall() + self.assertEqual(result[0][0], 3) + self.assertEqual(result[1][0], 4) + + def test_bad_target(self): + with self.assertRaises(TypeError): + self.cx.backup(None) + with self.assertRaises(TypeError): + self.cx.backup() + + def test_bad_target_filename(self): + with self.assertRaises(TypeError): + self.cx.backup('some_file_name.db') + + def test_bad_target_same_connection(self): + with self.assertRaises(ValueError): + self.cx.backup(self.cx) + + def test_bad_target_closed_connection(self): + bck = sqlite.connect(':memory:') + bck.close() + with self.assertRaises(sqlite.ProgrammingError): + self.cx.backup(bck) + + def test_bad_source_closed_connection(self): + bck = sqlite.connect(':memory:') + source = sqlite.connect(":memory:") + source.close() + with self.assertRaises(sqlite.ProgrammingError): + source.backup(bck) + + def test_bad_target_in_transaction(self): + bck = sqlite.connect(':memory:') + bck.execute('CREATE TABLE bar (key INTEGER)') + bck.executemany('INSERT INTO bar (key) VALUES (?)', [(3,), (4,)]) + with self.assertRaises(sqlite.OperationalError) as cm: + self.cx.backup(bck) + if sqlite.sqlite_version_info < (3, 8, 8): + self.assertEqual(str(cm.exception), 'target is in transaction') + + def test_keyword_only_args(self): + with self.assertRaises(TypeError): + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, 1) + + def test_simple(self): + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck) + self.verify_backup(bck) + + def test_progress(self): + journal = [] + + def progress(status, remaining, total): + journal.append(status) + + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, pages=1, progress=progress) + self.verify_backup(bck) + + self.assertEqual(len(journal), 2) + self.assertEqual(journal[0], sqlite.SQLITE_OK) + self.assertEqual(journal[1], sqlite.SQLITE_DONE) + + def test_progress_all_pages_at_once_1(self): + journal = [] + + def progress(status, remaining, total): + journal.append(remaining) + + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, progress=progress) + self.verify_backup(bck) + + self.assertEqual(len(journal), 1) + self.assertEqual(journal[0], 0) + + def test_progress_all_pages_at_once_2(self): + journal = [] + + def progress(status, remaining, total): + journal.append(remaining) + + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, pages=-1, progress=progress) + self.verify_backup(bck) + + self.assertEqual(len(journal), 1) + self.assertEqual(journal[0], 0) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_non_callable_progress(self): + with self.assertRaises(TypeError) as cm: + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, pages=1, progress='bar') + self.assertEqual(str(cm.exception), 'progress argument must be a callable') + + def test_modifying_progress(self): + journal = [] + + def progress(status, remaining, total): + if not journal: + self.cx.execute('INSERT INTO foo (key) VALUES (?)', (remaining+1000,)) + self.cx.commit() + journal.append(remaining) + + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, pages=1, progress=progress) + self.verify_backup(bck) + + result = bck.execute("SELECT key FROM foo" + " WHERE key >= 1000" + " ORDER BY key").fetchall() + self.assertEqual(result[0][0], 1001) + + self.assertEqual(len(journal), 3) + self.assertEqual(journal[0], 1) + self.assertEqual(journal[1], 1) + self.assertEqual(journal[2], 0) + + def test_failing_progress(self): + def progress(status, remaining, total): + raise SystemError('nearly out of space') + + with self.assertRaises(SystemError) as err: + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, progress=progress) + self.assertEqual(str(err.exception), 'nearly out of space') + + def test_database_source_name(self): + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, name='main') + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, name='temp') + with self.assertRaises(sqlite.OperationalError) as cm: + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, name='non-existing') + self.assertIn("unknown database", str(cm.exception)) + + self.cx.execute("ATTACH DATABASE ':memory:' AS attached_db") + self.cx.execute('CREATE TABLE attached_db.foo (key INTEGER)') + self.cx.executemany('INSERT INTO attached_db.foo (key) VALUES (?)', [(3,), (4,)]) + self.cx.commit() + with sqlite.connect(':memory:') as bck: + self.cx.backup(bck, name='attached_db') + self.verify_backup(bck) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_cli.py b/Lib/test/test_sqlite3/test_cli.py new file mode 100644 index 000000000..560cd9efc --- /dev/null +++ b/Lib/test/test_sqlite3/test_cli.py @@ -0,0 +1,157 @@ +"""sqlite3 CLI tests.""" + +import sqlite3 as sqlite +import subprocess +import sys +import unittest + +from test.support import SHORT_TIMEOUT#, requires_subprocess +from test.support.os_helper import TESTFN, unlink + + +# TODO: RUSTPYTHON +#@requires_subprocess() +class CommandLineInterface(unittest.TestCase): + + def _do_test(self, *args, expect_success=True): + with subprocess.Popen( + [sys.executable, "-Xutf8", "-m", "sqlite3", *args], + encoding="utf-8", + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as proc: + proc.wait() + if expect_success == bool(proc.returncode): + self.fail("".join(proc.stderr)) + stdout = proc.stdout.read() + stderr = proc.stderr.read() + if expect_success: + self.assertEqual(stderr, "") + else: + self.assertEqual(stdout, "") + return stdout, stderr + + def expect_success(self, *args): + out, _ = self._do_test(*args) + return out + + def expect_failure(self, *args): + _, err = self._do_test(*args, expect_success=False) + return err + + def test_cli_help(self): + out = self.expect_success("-h") + self.assertIn("usage: python -m sqlite3", out) + + def test_cli_version(self): + out = self.expect_success("-v") + self.assertIn(sqlite.sqlite_version, out) + + def test_cli_execute_sql(self): + out = self.expect_success(":memory:", "select 1") + self.assertIn("(1,)", out) + + def test_cli_execute_too_much_sql(self): + stderr = self.expect_failure(":memory:", "select 1; select 2") + err = "ProgrammingError: You can only execute one statement at a time" + self.assertIn(err, stderr) + + def test_cli_execute_incomplete_sql(self): + stderr = self.expect_failure(":memory:", "sel") + self.assertIn("OperationalError (SQLITE_ERROR)", stderr) + + def test_cli_on_disk_db(self): + self.addCleanup(unlink, TESTFN) + out = self.expect_success(TESTFN, "create table t(t)") + self.assertEqual(out, "") + out = self.expect_success(TESTFN, "select count(t) from t") + self.assertIn("(0,)", out) + + +# TODO: RUSTPYTHON +#@requires_subprocess() +class InteractiveSession(unittest.TestCase): + TIMEOUT = SHORT_TIMEOUT / 10. + MEMORY_DB_MSG = "Connected to a transient in-memory database" + PS1 = "sqlite> " + PS2 = "... " + + def start_cli(self, *args): + return subprocess.Popen( + [sys.executable, "-Xutf8", "-m", "sqlite3", *args], + encoding="utf-8", + bufsize=0, + stdin=subprocess.PIPE, + # Note: the banner is printed to stderr, the prompt to stdout. + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def expect_success(self, proc): + proc.wait() + if proc.returncode: + self.fail("".join(proc.stderr)) + + def test_interact(self): + with self.start_cli() as proc: + out, err = proc.communicate(timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(self.PS1, out) + self.expect_success(proc) + + def test_interact_quit(self): + with self.start_cli() as proc: + out, err = proc.communicate(input=".quit", timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(self.PS1, out) + self.expect_success(proc) + + def test_interact_version(self): + with self.start_cli() as proc: + out, err = proc.communicate(input=".version", timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(sqlite.sqlite_version, out) + self.expect_success(proc) + + def test_interact_valid_sql(self): + with self.start_cli() as proc: + out, err = proc.communicate(input="select 1;", + timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn("(1,)", out) + self.expect_success(proc) + + def test_interact_valid_multiline_sql(self): + with self.start_cli() as proc: + out, err = proc.communicate(input="select 1\n;", + timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn(self.PS2, out) + self.assertIn("(1,)", out) + self.expect_success(proc) + + def test_interact_invalid_sql(self): + with self.start_cli() as proc: + out, err = proc.communicate(input="sel;", timeout=self.TIMEOUT) + self.assertIn(self.MEMORY_DB_MSG, err) + self.assertIn("OperationalError (SQLITE_ERROR)", err) + self.expect_success(proc) + + def test_interact_on_disk_file(self): + self.addCleanup(unlink, TESTFN) + with self.start_cli(TESTFN) as proc: + out, err = proc.communicate(input="create table t(t);", + timeout=self.TIMEOUT) + self.assertIn(TESTFN, err) + self.assertIn(self.PS1, out) + self.expect_success(proc) + with self.start_cli(TESTFN, "select count(t) from t") as proc: + out = proc.stdout.read() + err = proc.stderr.read() + self.assertIn("(0,)", out) + self.expect_success(proc) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py new file mode 100644 index 000000000..bbc151fcb --- /dev/null +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -0,0 +1,1965 @@ +# pysqlite2/test/dbapi.py: tests for DB-API compliance +# +# Copyright (C) 2004-2010 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import contextlib +import os +import sqlite3 as sqlite +import subprocess +import sys +import threading +import unittest +import urllib.parse + +from test.support import ( + SHORT_TIMEOUT, check_disallow_instantiation,# requires_subprocess, + #is_emscripten, is_wasi +# TODO: RUSTPYTHON +) +from test.support import threading_helper +# TODO: RUSTPYTHON +#from _testcapi import INT_MAX, ULLONG_MAX +from os import SEEK_SET, SEEK_CUR, SEEK_END +from test.support.os_helper import TESTFN, TESTFN_UNDECODABLE, unlink, temp_dir, FakePath + + +# Helper for temporary memory databases +def memory_database(*args, **kwargs): + cx = sqlite.connect(":memory:", *args, **kwargs) + return contextlib.closing(cx) + + +# Temporarily limit a database connection parameter +@contextlib.contextmanager +def cx_limit(cx, category=sqlite.SQLITE_LIMIT_SQL_LENGTH, limit=128): + try: + _prev = cx.setlimit(category, limit) + yield limit + finally: + cx.setlimit(category, _prev) + + +class ModuleTests(unittest.TestCase): + def test_api_level(self): + self.assertEqual(sqlite.apilevel, "2.0", + "apilevel is %s, should be 2.0" % sqlite.apilevel) + + def test_deprecated_version(self): + msg = "deprecated and will be removed in Python 3.14" + for attr in "version", "version_info": + with self.subTest(attr=attr): + with self.assertWarnsRegex(DeprecationWarning, msg) as cm: + getattr(sqlite, attr) + self.assertEqual(cm.filename, __file__) + with self.assertWarnsRegex(DeprecationWarning, msg) as cm: + getattr(sqlite.dbapi2, attr) + self.assertEqual(cm.filename, __file__) + + def test_thread_safety(self): + self.assertIn(sqlite.threadsafety, {0, 1, 3}, + "threadsafety is %d, should be 0, 1 or 3" % + sqlite.threadsafety) + + def test_param_style(self): + self.assertEqual(sqlite.paramstyle, "qmark", + "paramstyle is '%s', should be 'qmark'" % + sqlite.paramstyle) + + def test_warning(self): + self.assertTrue(issubclass(sqlite.Warning, Exception), + "Warning is not a subclass of Exception") + + def test_error(self): + self.assertTrue(issubclass(sqlite.Error, Exception), + "Error is not a subclass of Exception") + + def test_interface_error(self): + self.assertTrue(issubclass(sqlite.InterfaceError, sqlite.Error), + "InterfaceError is not a subclass of Error") + + def test_database_error(self): + self.assertTrue(issubclass(sqlite.DatabaseError, sqlite.Error), + "DatabaseError is not a subclass of Error") + + def test_data_error(self): + self.assertTrue(issubclass(sqlite.DataError, sqlite.DatabaseError), + "DataError is not a subclass of DatabaseError") + + def test_operational_error(self): + self.assertTrue(issubclass(sqlite.OperationalError, sqlite.DatabaseError), + "OperationalError is not a subclass of DatabaseError") + + def test_integrity_error(self): + self.assertTrue(issubclass(sqlite.IntegrityError, sqlite.DatabaseError), + "IntegrityError is not a subclass of DatabaseError") + + def test_internal_error(self): + self.assertTrue(issubclass(sqlite.InternalError, sqlite.DatabaseError), + "InternalError is not a subclass of DatabaseError") + + def test_programming_error(self): + self.assertTrue(issubclass(sqlite.ProgrammingError, sqlite.DatabaseError), + "ProgrammingError is not a subclass of DatabaseError") + + def test_not_supported_error(self): + self.assertTrue(issubclass(sqlite.NotSupportedError, + sqlite.DatabaseError), + "NotSupportedError is not a subclass of DatabaseError") + + def test_module_constants(self): + consts = [ + "SQLITE_ABORT", + "SQLITE_ALTER_TABLE", + "SQLITE_ANALYZE", + "SQLITE_ATTACH", + "SQLITE_AUTH", + "SQLITE_BUSY", + "SQLITE_CANTOPEN", + "SQLITE_CONSTRAINT", + "SQLITE_CORRUPT", + "SQLITE_CREATE_INDEX", + "SQLITE_CREATE_TABLE", + "SQLITE_CREATE_TEMP_INDEX", + "SQLITE_CREATE_TEMP_TABLE", + "SQLITE_CREATE_TEMP_TRIGGER", + "SQLITE_CREATE_TEMP_VIEW", + "SQLITE_CREATE_TRIGGER", + "SQLITE_CREATE_VIEW", + "SQLITE_CREATE_VTABLE", + "SQLITE_DELETE", + "SQLITE_DENY", + "SQLITE_DETACH", + "SQLITE_DONE", + "SQLITE_DROP_INDEX", + "SQLITE_DROP_TABLE", + "SQLITE_DROP_TEMP_INDEX", + "SQLITE_DROP_TEMP_TABLE", + "SQLITE_DROP_TEMP_TRIGGER", + "SQLITE_DROP_TEMP_VIEW", + "SQLITE_DROP_TRIGGER", + "SQLITE_DROP_VIEW", + "SQLITE_DROP_VTABLE", + "SQLITE_EMPTY", + "SQLITE_ERROR", + "SQLITE_FORMAT", + "SQLITE_FULL", + "SQLITE_FUNCTION", + "SQLITE_IGNORE", + "SQLITE_INSERT", + "SQLITE_INTERNAL", + "SQLITE_INTERRUPT", + "SQLITE_IOERR", + "SQLITE_LOCKED", + "SQLITE_MISMATCH", + "SQLITE_MISUSE", + "SQLITE_NOLFS", + "SQLITE_NOMEM", + "SQLITE_NOTADB", + "SQLITE_NOTFOUND", + "SQLITE_OK", + "SQLITE_PERM", + "SQLITE_PRAGMA", + "SQLITE_PROTOCOL", + "SQLITE_RANGE", + "SQLITE_READ", + "SQLITE_READONLY", + "SQLITE_REINDEX", + "SQLITE_ROW", + "SQLITE_SAVEPOINT", + "SQLITE_SCHEMA", + "SQLITE_SELECT", + "SQLITE_TOOBIG", + "SQLITE_TRANSACTION", + "SQLITE_UPDATE", + # Run-time limit categories + "SQLITE_LIMIT_LENGTH", + "SQLITE_LIMIT_SQL_LENGTH", + "SQLITE_LIMIT_COLUMN", + "SQLITE_LIMIT_EXPR_DEPTH", + "SQLITE_LIMIT_COMPOUND_SELECT", + "SQLITE_LIMIT_VDBE_OP", + "SQLITE_LIMIT_FUNCTION_ARG", + "SQLITE_LIMIT_ATTACHED", + "SQLITE_LIMIT_LIKE_PATTERN_LENGTH", + "SQLITE_LIMIT_VARIABLE_NUMBER", + "SQLITE_LIMIT_TRIGGER_DEPTH", + ] + if sqlite.sqlite_version_info >= (3, 7, 17): + consts += ["SQLITE_NOTICE", "SQLITE_WARNING"] + if sqlite.sqlite_version_info >= (3, 8, 3): + consts.append("SQLITE_RECURSIVE") + if sqlite.sqlite_version_info >= (3, 8, 7): + consts.append("SQLITE_LIMIT_WORKER_THREADS") + consts += ["PARSE_DECLTYPES", "PARSE_COLNAMES"] + # Extended result codes + consts += [ + "SQLITE_ABORT_ROLLBACK", + "SQLITE_BUSY_RECOVERY", + "SQLITE_CANTOPEN_FULLPATH", + "SQLITE_CANTOPEN_ISDIR", + "SQLITE_CANTOPEN_NOTEMPDIR", + "SQLITE_CORRUPT_VTAB", + "SQLITE_IOERR_ACCESS", + "SQLITE_IOERR_BLOCKED", + "SQLITE_IOERR_CHECKRESERVEDLOCK", + "SQLITE_IOERR_CLOSE", + "SQLITE_IOERR_DELETE", + "SQLITE_IOERR_DELETE_NOENT", + "SQLITE_IOERR_DIR_CLOSE", + "SQLITE_IOERR_DIR_FSYNC", + "SQLITE_IOERR_FSTAT", + "SQLITE_IOERR_FSYNC", + "SQLITE_IOERR_LOCK", + "SQLITE_IOERR_NOMEM", + "SQLITE_IOERR_RDLOCK", + "SQLITE_IOERR_READ", + "SQLITE_IOERR_SEEK", + "SQLITE_IOERR_SHMLOCK", + "SQLITE_IOERR_SHMMAP", + "SQLITE_IOERR_SHMOPEN", + "SQLITE_IOERR_SHMSIZE", + "SQLITE_IOERR_SHORT_READ", + "SQLITE_IOERR_TRUNCATE", + "SQLITE_IOERR_UNLOCK", + "SQLITE_IOERR_WRITE", + "SQLITE_LOCKED_SHAREDCACHE", + "SQLITE_READONLY_CANTLOCK", + "SQLITE_READONLY_RECOVERY", + ] + if sqlite.sqlite_version_info >= (3, 7, 16): + consts += [ + "SQLITE_CONSTRAINT_CHECK", + "SQLITE_CONSTRAINT_COMMITHOOK", + "SQLITE_CONSTRAINT_FOREIGNKEY", + "SQLITE_CONSTRAINT_FUNCTION", + "SQLITE_CONSTRAINT_NOTNULL", + "SQLITE_CONSTRAINT_PRIMARYKEY", + "SQLITE_CONSTRAINT_TRIGGER", + "SQLITE_CONSTRAINT_UNIQUE", + "SQLITE_CONSTRAINT_VTAB", + "SQLITE_READONLY_ROLLBACK", + ] + if sqlite.sqlite_version_info >= (3, 7, 17): + consts += [ + "SQLITE_IOERR_MMAP", + "SQLITE_NOTICE_RECOVER_ROLLBACK", + "SQLITE_NOTICE_RECOVER_WAL", + ] + if sqlite.sqlite_version_info >= (3, 8, 0): + consts += [ + "SQLITE_BUSY_SNAPSHOT", + "SQLITE_IOERR_GETTEMPPATH", + "SQLITE_WARNING_AUTOINDEX", + ] + if sqlite.sqlite_version_info >= (3, 8, 1): + consts += ["SQLITE_CANTOPEN_CONVPATH", "SQLITE_IOERR_CONVPATH"] + if sqlite.sqlite_version_info >= (3, 8, 2): + consts.append("SQLITE_CONSTRAINT_ROWID") + if sqlite.sqlite_version_info >= (3, 8, 3): + consts.append("SQLITE_READONLY_DBMOVED") + if sqlite.sqlite_version_info >= (3, 8, 7): + consts.append("SQLITE_AUTH_USER") + if sqlite.sqlite_version_info >= (3, 9, 0): + consts.append("SQLITE_IOERR_VNODE") + if sqlite.sqlite_version_info >= (3, 10, 0): + consts.append("SQLITE_IOERR_AUTH") + if sqlite.sqlite_version_info >= (3, 14, 1): + consts.append("SQLITE_OK_LOAD_PERMANENTLY") + if sqlite.sqlite_version_info >= (3, 21, 0): + consts += [ + "SQLITE_IOERR_BEGIN_ATOMIC", + "SQLITE_IOERR_COMMIT_ATOMIC", + "SQLITE_IOERR_ROLLBACK_ATOMIC", + ] + if sqlite.sqlite_version_info >= (3, 22, 0): + consts += [ + "SQLITE_ERROR_MISSING_COLLSEQ", + "SQLITE_ERROR_RETRY", + "SQLITE_READONLY_CANTINIT", + "SQLITE_READONLY_DIRECTORY", + ] + if sqlite.sqlite_version_info >= (3, 24, 0): + consts += ["SQLITE_CORRUPT_SEQUENCE", "SQLITE_LOCKED_VTAB"] + if sqlite.sqlite_version_info >= (3, 25, 0): + consts += ["SQLITE_CANTOPEN_DIRTYWAL", "SQLITE_ERROR_SNAPSHOT"] + if sqlite.sqlite_version_info >= (3, 31, 0): + consts += [ + "SQLITE_CANTOPEN_SYMLINK", + "SQLITE_CONSTRAINT_PINNED", + "SQLITE_OK_SYMLINK", + ] + if sqlite.sqlite_version_info >= (3, 32, 0): + consts += [ + "SQLITE_BUSY_TIMEOUT", + "SQLITE_CORRUPT_INDEX", + "SQLITE_IOERR_DATA", + ] + if sqlite.sqlite_version_info >= (3, 34, 0): + consts.append("SQLITE_IOERR_CORRUPTFS") + for const in consts: + with self.subTest(const=const): + self.assertTrue(hasattr(sqlite, const)) + + def test_error_code_on_exception(self): + err_msg = "unable to open database file" + if sys.platform.startswith("win"): + err_code = sqlite.SQLITE_CANTOPEN_ISDIR + else: + err_code = sqlite.SQLITE_CANTOPEN + + with temp_dir() as db: + with self.assertRaisesRegex(sqlite.Error, err_msg) as cm: + sqlite.connect(db) + e = cm.exception + self.assertEqual(e.sqlite_errorcode, err_code) + self.assertTrue(e.sqlite_errorname.startswith("SQLITE_CANTOPEN")) + + @unittest.skipIf(sqlite.sqlite_version_info <= (3, 7, 16), + "Requires SQLite 3.7.16 or newer") + def test_extended_error_code_on_exception(self): + with memory_database() as con: + with con: + con.execute("create table t(t integer check(t > 0))") + errmsg = "constraint failed" + with self.assertRaisesRegex(sqlite.IntegrityError, errmsg) as cm: + con.execute("insert into t values(-1)") + exc = cm.exception + self.assertEqual(exc.sqlite_errorcode, + sqlite.SQLITE_CONSTRAINT_CHECK) + self.assertEqual(exc.sqlite_errorname, "SQLITE_CONSTRAINT_CHECK") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_disallow_instantiation(self): + cx = sqlite.connect(":memory:") + check_disallow_instantiation(self, type(cx("select 1"))) + check_disallow_instantiation(self, sqlite.Blob) + + def test_complete_statement(self): + self.assertFalse(sqlite.complete_statement("select t")) + self.assertTrue(sqlite.complete_statement("create table t(t);")) + + +class ConnectionTests(unittest.TestCase): + + def setUp(self): + self.cx = sqlite.connect(":memory:") + cu = self.cx.cursor() + cu.execute("create table test(id integer primary key, name text)") + cu.execute("insert into test(name) values (?)", ("foo",)) + + def tearDown(self): + self.cx.close() + + def test_commit(self): + self.cx.commit() + + def test_commit_after_no_changes(self): + """ + A commit should also work when no changes were made to the database. + """ + self.cx.commit() + self.cx.commit() + + def test_rollback(self): + self.cx.rollback() + + def test_rollback_after_no_changes(self): + """ + A rollback should also work when no changes were made to the database. + """ + self.cx.rollback() + self.cx.rollback() + + def test_cursor(self): + cu = self.cx.cursor() + + def test_failed_open(self): + YOU_CANNOT_OPEN_THIS = "/foo/bar/bla/23534/mydb.db" + with self.assertRaises(sqlite.OperationalError): + sqlite.connect(YOU_CANNOT_OPEN_THIS) + + def test_close(self): + self.cx.close() + + def test_use_after_close(self): + sql = "select 1" + cu = self.cx.cursor() + res = cu.execute(sql) + self.cx.close() + self.assertRaises(sqlite.ProgrammingError, res.fetchall) + self.assertRaises(sqlite.ProgrammingError, cu.execute, sql) + self.assertRaises(sqlite.ProgrammingError, cu.executemany, sql, []) + self.assertRaises(sqlite.ProgrammingError, cu.executescript, sql) + self.assertRaises(sqlite.ProgrammingError, self.cx.execute, sql) + self.assertRaises(sqlite.ProgrammingError, + self.cx.executemany, sql, []) + self.assertRaises(sqlite.ProgrammingError, self.cx.executescript, sql) + self.assertRaises(sqlite.ProgrammingError, + self.cx.create_function, "t", 1, lambda x: x) + self.assertRaises(sqlite.ProgrammingError, self.cx.cursor) + with self.assertRaises(sqlite.ProgrammingError): + with self.cx: + pass + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_exceptions(self): + # Optional DB-API extension. + self.assertEqual(self.cx.Warning, sqlite.Warning) + self.assertEqual(self.cx.Error, sqlite.Error) + self.assertEqual(self.cx.InterfaceError, sqlite.InterfaceError) + self.assertEqual(self.cx.DatabaseError, sqlite.DatabaseError) + self.assertEqual(self.cx.DataError, sqlite.DataError) + self.assertEqual(self.cx.OperationalError, sqlite.OperationalError) + self.assertEqual(self.cx.IntegrityError, sqlite.IntegrityError) + self.assertEqual(self.cx.InternalError, sqlite.InternalError) + self.assertEqual(self.cx.ProgrammingError, sqlite.ProgrammingError) + self.assertEqual(self.cx.NotSupportedError, sqlite.NotSupportedError) + + def test_in_transaction(self): + # Can't use db from setUp because we want to test initial state. + cx = sqlite.connect(":memory:") + cu = cx.cursor() + self.assertEqual(cx.in_transaction, False) + cu.execute("create table transactiontest(id integer primary key, name text)") + self.assertEqual(cx.in_transaction, False) + cu.execute("insert into transactiontest(name) values (?)", ("foo",)) + self.assertEqual(cx.in_transaction, True) + cu.execute("select name from transactiontest where name=?", ["foo"]) + row = cu.fetchone() + self.assertEqual(cx.in_transaction, True) + cx.commit() + self.assertEqual(cx.in_transaction, False) + cu.execute("select name from transactiontest where name=?", ["foo"]) + row = cu.fetchone() + self.assertEqual(cx.in_transaction, False) + + def test_in_transaction_ro(self): + with self.assertRaises(AttributeError): + self.cx.in_transaction = True + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_connection_exceptions(self): + exceptions = [ + "DataError", + "DatabaseError", + "Error", + "IntegrityError", + "InterfaceError", + "NotSupportedError", + "OperationalError", + "ProgrammingError", + "Warning", + ] + for exc in exceptions: + with self.subTest(exc=exc): + self.assertTrue(hasattr(self.cx, exc)) + self.assertIs(getattr(sqlite, exc), getattr(self.cx, exc)) + + def test_interrupt_on_closed_db(self): + cx = sqlite.connect(":memory:") + cx.close() + with self.assertRaises(sqlite.ProgrammingError): + cx.interrupt() + + def test_interrupt(self): + self.assertIsNone(self.cx.interrupt()) + + def test_drop_unused_refs(self): + for n in range(500): + cu = self.cx.execute(f"select {n}") + self.assertEqual(cu.fetchone()[0], n) + + def test_connection_limits(self): + category = sqlite.SQLITE_LIMIT_SQL_LENGTH + saved_limit = self.cx.getlimit(category) + try: + new_limit = 10 + prev_limit = self.cx.setlimit(category, new_limit) + self.assertEqual(saved_limit, prev_limit) + self.assertEqual(self.cx.getlimit(category), new_limit) + msg = "query string is too large" + self.assertRaisesRegex(sqlite.DataError, msg, + self.cx.execute, "select 1 as '16'") + finally: # restore saved limit + self.cx.setlimit(category, saved_limit) + + def test_connection_bad_limit_category(self): + msg = "'category' is out of bounds" + cat = 1111 + self.assertRaisesRegex(sqlite.ProgrammingError, msg, + self.cx.getlimit, cat) + self.assertRaisesRegex(sqlite.ProgrammingError, msg, + self.cx.setlimit, cat, 0) + + def test_connection_init_bad_isolation_level(self): + msg = ( + "isolation_level string must be '', 'DEFERRED', 'IMMEDIATE', or " + "'EXCLUSIVE'" + ) + levels = ( + "BOGUS", + " ", + "DEFERRE", + "IMMEDIAT", + "EXCLUSIV", + "DEFERREDS", + "IMMEDIATES", + "EXCLUSIVES", + ) + for level in levels: + with self.subTest(level=level): + with self.assertRaisesRegex(ValueError, msg): + memory_database(isolation_level=level) + with memory_database() as cx: + with self.assertRaisesRegex(ValueError, msg): + cx.isolation_level = level + # Check that the default level is not changed + self.assertEqual(cx.isolation_level, "") + + def test_connection_init_good_isolation_levels(self): + for level in ("", "DEFERRED", "IMMEDIATE", "EXCLUSIVE", None): + with self.subTest(level=level): + with memory_database(isolation_level=level) as cx: + self.assertEqual(cx.isolation_level, level) + with memory_database() as cx: + self.assertEqual(cx.isolation_level, "") + cx.isolation_level = level + self.assertEqual(cx.isolation_level, level) + + # TODO: RUSTPYTHON + # @unittest.expectedFailure + @unittest.skip("TODO: RUSTPYTHON deadlock") + def test_connection_reinit(self): + db = ":memory:" + cx = sqlite.connect(db) + cx.text_factory = bytes + cx.row_factory = sqlite.Row + cu = cx.cursor() + cu.execute("create table foo (bar)") + cu.executemany("insert into foo (bar) values (?)", + ((str(v),) for v in range(4))) + cu.execute("select bar from foo") + + rows = [r for r in cu.fetchmany(2)] + self.assertTrue(all(isinstance(r, sqlite.Row) for r in rows)) + self.assertEqual([r[0] for r in rows], [b"0", b"1"]) + + cx.__init__(db) + cx.execute("create table foo (bar)") + cx.executemany("insert into foo (bar) values (?)", + ((v,) for v in ("a", "b", "c", "d"))) + + # This uses the old database, old row factory, but new text factory + rows = [r for r in cu.fetchall()] + self.assertTrue(all(isinstance(r, sqlite.Row) for r in rows)) + self.assertEqual([r[0] for r in rows], ["2", "3"]) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_connection_bad_reinit(self): + cx = sqlite.connect(":memory:") + with cx: + cx.execute("create table t(t)") + with temp_dir() as db: + self.assertRaisesRegex(sqlite.OperationalError, + "unable to open database file", + cx.__init__, db) + self.assertRaisesRegex(sqlite.ProgrammingError, + "Base Connection.__init__ not called", + cx.executemany, "insert into t values(?)", + ((v,) for v in range(3))) + + +@unittest.skip("TODO: RUSTPYHON") +class UninitialisedConnectionTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.Connection.__new__(sqlite.Connection) + + def test_uninit_operations(self): + funcs = ( + lambda: self.cx.isolation_level, + lambda: self.cx.total_changes, + lambda: self.cx.in_transaction, + lambda: self.cx.iterdump(), + lambda: self.cx.cursor(), + lambda: self.cx.close(), + ) + for func in funcs: + with self.subTest(func=func): + self.assertRaisesRegex(sqlite.ProgrammingError, + "Base Connection.__init__ not called", + func) + + +@unittest.skipUnless(hasattr(sqlite.Connection, "serialize"), + "Needs SQLite serialize API") +class SerializeTests(unittest.TestCase): + def test_serialize_deserialize(self): + with memory_database() as cx: + with cx: + cx.execute("create table t(t)") + data = cx.serialize() + self.assertEqual(len(data), 8192) + + # Remove test table, verify that it was removed. + with cx: + cx.execute("drop table t") + regex = "no such table" + with self.assertRaisesRegex(sqlite.OperationalError, regex): + cx.execute("select t from t") + + # Deserialize and verify that test table is restored. + cx.deserialize(data) + cx.execute("select t from t") + + def test_deserialize_wrong_args(self): + dataset = ( + (BufferError, memoryview(b"blob")[::2]), + (TypeError, []), + (TypeError, 1), + (TypeError, None), + ) + for exc, arg in dataset: + with self.subTest(exc=exc, arg=arg): + with memory_database() as cx: + self.assertRaises(exc, cx.deserialize, arg) + + def test_deserialize_corrupt_database(self): + with memory_database() as cx: + regex = "file is not a database" + with self.assertRaisesRegex(sqlite.DatabaseError, regex): + cx.deserialize(b"\0\1\3") + # SQLite does not generate an error until you try to query the + # deserialized database. + cx.execute("create table fail(f)") + + +class OpenTests(unittest.TestCase): + _sql = "create table test(id integer)" + + def test_open_with_path_like_object(self): + """ Checks that we can successfully connect to a database using an object that + is PathLike, i.e. has __fspath__(). """ + path = FakePath(TESTFN) + self.addCleanup(unlink, path) + self.assertFalse(os.path.exists(path)) + with contextlib.closing(sqlite.connect(path)) as cx: + self.assertTrue(os.path.exists(path)) + cx.execute(self._sql) + + @unittest.skipIf(sys.platform == "win32", "skipped on Windows") + @unittest.skipIf(sys.platform == "darwin", "skipped on macOS") + # TODO: RUSTPYTHON + # @unittest.skipIf(is_emscripten or is_wasi, "not supported on Emscripten/WASI") + @unittest.skipUnless(TESTFN_UNDECODABLE, "only works if there are undecodable paths") + def test_open_with_undecodable_path(self): + path = TESTFN_UNDECODABLE + self.addCleanup(unlink, path) + self.assertFalse(os.path.exists(path)) + with contextlib.closing(sqlite.connect(path)) as cx: + self.assertTrue(os.path.exists(path)) + cx.execute(self._sql) + + def test_open_uri(self): + path = TESTFN + self.addCleanup(unlink, path) + uri = "file:" + urllib.parse.quote(os.fsencode(path)) + self.assertFalse(os.path.exists(path)) + with contextlib.closing(sqlite.connect(uri, uri=True)) as cx: + self.assertTrue(os.path.exists(path)) + cx.execute(self._sql) + + def test_open_unquoted_uri(self): + path = TESTFN + self.addCleanup(unlink, path) + uri = "file:" + path + self.assertFalse(os.path.exists(path)) + with contextlib.closing(sqlite.connect(uri, uri=True)) as cx: + self.assertTrue(os.path.exists(path)) + cx.execute(self._sql) + + def test_open_uri_readonly(self): + path = TESTFN + self.addCleanup(unlink, path) + uri = "file:" + urllib.parse.quote(os.fsencode(path)) + "?mode=ro" + self.assertFalse(os.path.exists(path)) + # Cannot create new DB + with self.assertRaises(sqlite.OperationalError): + sqlite.connect(uri, uri=True) + self.assertFalse(os.path.exists(path)) + sqlite.connect(path).close() + self.assertTrue(os.path.exists(path)) + # Cannot modify new DB + with contextlib.closing(sqlite.connect(uri, uri=True)) as cx: + with self.assertRaises(sqlite.OperationalError): + cx.execute(self._sql) + + @unittest.skipIf(sys.platform == "win32", "skipped on Windows") + @unittest.skipIf(sys.platform == "darwin", "skipped on macOS") + # TODO: RUSTPYTHON + # @unittest.skipIf(is_emscripten or is_wasi, "not supported on Emscripten/WASI") + @unittest.skipUnless(TESTFN_UNDECODABLE, "only works if there are undecodable paths") + def test_open_undecodable_uri(self): + path = TESTFN_UNDECODABLE + self.addCleanup(unlink, path) + uri = "file:" + urllib.parse.quote(path) + self.assertFalse(os.path.exists(path)) + with contextlib.closing(sqlite.connect(uri, uri=True)) as cx: + self.assertTrue(os.path.exists(path)) + cx.execute(self._sql) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_factory_database_arg(self): + def factory(database, *args, **kwargs): + nonlocal database_arg + database_arg = database + return sqlite.Connection(":memory:", *args, **kwargs) + + for database in (TESTFN, os.fsencode(TESTFN), + FakePath(TESTFN), FakePath(os.fsencode(TESTFN))): + database_arg = None + sqlite.connect(database, factory=factory).close() + self.assertEqual(database_arg, database) + + def test_database_keyword(self): + with contextlib.closing(sqlite.connect(database=":memory:")) as cx: + self.assertEqual(type(cx), sqlite.Connection) + + +class CursorTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cu = self.cx.cursor() + self.cu.execute( + "create table test(id integer primary key, name text, " + "income number, unique_test text unique)" + ) + self.cu.execute("insert into test(name) values (?)", ("foo",)) + + def tearDown(self): + self.cu.close() + self.cx.close() + + def test_execute_no_args(self): + self.cu.execute("delete from test") + + def test_execute_illegal_sql(self): + with self.assertRaises(sqlite.OperationalError): + self.cu.execute("select asdf") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_execute_multiple_statements(self): + msg = "You can only execute one statement at a time" + dataset = ( + "select 1; select 2", + "select 1; // c++ comments are not allowed", + "select 1; *not a comment", + "select 1; -*not a comment", + "select 1; /* */ a", + "select 1; /**/a", + "select 1; -", + "select 1; /", + "select 1; -\n- select 2", + """select 1; + -- comment + select 2 + """, + ) + for query in dataset: + with self.subTest(query=query): + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + self.cu.execute(query) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_execute_with_appended_comments(self): + dataset = ( + "select 1; -- foo bar", + "select 1; --", + "select 1; /*", # Unclosed comments ending in \0 are skipped. + """ + select 5+4; + + /* + foo + */ + """, + ) + for query in dataset: + with self.subTest(query=query): + self.cu.execute(query) + + def test_execute_wrong_sql_arg(self): + with self.assertRaises(TypeError): + self.cu.execute(42) + + def test_execute_arg_int(self): + self.cu.execute("insert into test(id) values (?)", (42,)) + + def test_execute_arg_float(self): + self.cu.execute("insert into test(income) values (?)", (2500.32,)) + + def test_execute_arg_string(self): + self.cu.execute("insert into test(name) values (?)", ("Hugo",)) + + def test_execute_arg_string_with_zero_byte(self): + self.cu.execute("insert into test(name) values (?)", ("Hu\x00go",)) + + self.cu.execute("select name from test where id=?", (self.cu.lastrowid,)) + row = self.cu.fetchone() + self.assertEqual(row[0], "Hu\x00go") + + def test_execute_non_iterable(self): + with self.assertRaises(sqlite.ProgrammingError) as cm: + self.cu.execute("insert into test(id) values (?)", 42) + self.assertEqual(str(cm.exception), 'parameters are of unsupported type') + + def test_execute_wrong_no_of_args1(self): + # too many parameters + with self.assertRaises(sqlite.ProgrammingError): + self.cu.execute("insert into test(id) values (?)", (17, "Egon")) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_execute_wrong_no_of_args2(self): + # too little parameters + with self.assertRaises(sqlite.ProgrammingError): + self.cu.execute("insert into test(id) values (?)") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_execute_wrong_no_of_args3(self): + # no parameters, parameters are needed + with self.assertRaises(sqlite.ProgrammingError): + self.cu.execute("insert into test(id) values (?)") + + def test_execute_param_list(self): + self.cu.execute("insert into test(name) values ('foo')") + self.cu.execute("select name from test where name=?", ["foo"]) + row = self.cu.fetchone() + self.assertEqual(row[0], "foo") + + def test_execute_param_sequence(self): + class L: + def __len__(self): + return 1 + def __getitem__(self, x): + assert x == 0 + return "foo" + + self.cu.execute("insert into test(name) values ('foo')") + self.cu.execute("select name from test where name=?", L()) + row = self.cu.fetchone() + self.assertEqual(row[0], "foo") + + def test_execute_param_sequence_bad_len(self): + # Issue41662: Error in __len__() was overridden with ProgrammingError. + class L: + def __len__(self): + 1/0 + def __getitem__(slf, x): + raise AssertionError + + self.cu.execute("insert into test(name) values ('foo')") + with self.assertRaises(ZeroDivisionError): + self.cu.execute("select name from test where name=?", L()) + + def test_execute_too_many_params(self): + category = sqlite.SQLITE_LIMIT_VARIABLE_NUMBER + msg = "too many SQL variables" + with cx_limit(self.cx, category=category, limit=1): + self.cu.execute("select * from test where id=?", (1,)) + with self.assertRaisesRegex(sqlite.OperationalError, msg): + self.cu.execute("select * from test where id!=? and id!=?", + (1, 2)) + + def test_execute_dict_mapping(self): + self.cu.execute("insert into test(name) values ('foo')") + self.cu.execute("select name from test where name=:name", {"name": "foo"}) + row = self.cu.fetchone() + self.assertEqual(row[0], "foo") + + def test_execute_dict_mapping_mapping(self): + class D(dict): + def __missing__(self, key): + return "foo" + + self.cu.execute("insert into test(name) values ('foo')") + self.cu.execute("select name from test where name=:name", D()) + row = self.cu.fetchone() + self.assertEqual(row[0], "foo") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_execute_dict_mapping_too_little_args(self): + self.cu.execute("insert into test(name) values ('foo')") + with self.assertRaises(sqlite.ProgrammingError): + self.cu.execute("select name from test where name=:name and id=:id", {"name": "foo"}) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_execute_dict_mapping_no_args(self): + self.cu.execute("insert into test(name) values ('foo')") + with self.assertRaises(sqlite.ProgrammingError): + self.cu.execute("select name from test where name=:name") + + def test_execute_dict_mapping_unnamed(self): + self.cu.execute("insert into test(name) values ('foo')") + with self.assertRaises(sqlite.ProgrammingError): + self.cu.execute("select name from test where name=?", {"name": "foo"}) + + def test_close(self): + self.cu.close() + + def test_rowcount_execute(self): + self.cu.execute("delete from test") + self.cu.execute("insert into test(name) values ('foo')") + self.cu.execute("insert into test(name) values ('foo')") + self.cu.execute("update test set name='bar'") + self.assertEqual(self.cu.rowcount, 2) + + def test_rowcount_select(self): + """ + pysqlite does not know the rowcount of SELECT statements, because we + don't fetch all rows after executing the select statement. The rowcount + has thus to be -1. + """ + self.cu.execute("select 5 union select 6") + self.assertEqual(self.cu.rowcount, -1) + + def test_rowcount_executemany(self): + self.cu.execute("delete from test") + self.cu.executemany("insert into test(name) values (?)", [(1,), (2,), (3,)]) + self.assertEqual(self.cu.rowcount, 3) + + @unittest.skipIf(sqlite.sqlite_version_info < (3, 35, 0), + "Requires SQLite 3.35.0 or newer") + def test_rowcount_update_returning(self): + # gh-93421: rowcount is updated correctly for UPDATE...RETURNING queries + self.cu.execute("update test set name='bar' where name='foo' returning 1") + self.assertEqual(self.cu.fetchone()[0], 1) + self.assertEqual(self.cu.rowcount, 1) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_rowcount_prefixed_with_comment(self): + # gh-79579: rowcount is updated even if query is prefixed with comments + self.cu.execute(""" + -- foo + insert into test(name) values ('foo'), ('foo') + """) + self.assertEqual(self.cu.rowcount, 2) + self.cu.execute(""" + /* -- messy *r /* /* ** *- *-- + */ + /* one more */ insert into test(name) values ('messy') + """) + self.assertEqual(self.cu.rowcount, 1) + self.cu.execute("/* bar */ update test set name='bar' where name='foo'") + self.assertEqual(self.cu.rowcount, 3) + + def test_rowcount_vaccuum(self): + data = ((1,), (2,), (3,)) + self.cu.executemany("insert into test(income) values(?)", data) + self.assertEqual(self.cu.rowcount, 3) + self.cx.commit() + self.cu.execute("vacuum") + self.assertEqual(self.cu.rowcount, -1) + + def test_total_changes(self): + self.cu.execute("insert into test(name) values ('foo')") + self.cu.execute("insert into test(name) values ('foo')") + self.assertLess(2, self.cx.total_changes, msg='total changes reported wrong value') + + # Checks for executemany: + # Sequences are required by the DB-API, iterators + # enhancements in pysqlite. + + def test_execute_many_sequence(self): + self.cu.executemany("insert into test(income) values (?)", [(x,) for x in range(100, 110)]) + + def test_execute_many_iterator(self): + class MyIter: + def __init__(self): + self.value = 5 + + def __iter__(self): + return self + + def __next__(self): + if self.value == 10: + raise StopIteration + else: + self.value += 1 + return (self.value,) + + self.cu.executemany("insert into test(income) values (?)", MyIter()) + + def test_execute_many_generator(self): + def mygen(): + for i in range(5): + yield (i,) + + self.cu.executemany("insert into test(income) values (?)", mygen()) + + def test_execute_many_wrong_sql_arg(self): + with self.assertRaises(TypeError): + self.cu.executemany(42, [(3,)]) + + def test_execute_many_select(self): + with self.assertRaises(sqlite.ProgrammingError): + self.cu.executemany("select ?", [(3,)]) + + def test_execute_many_not_iterable(self): + with self.assertRaises(TypeError): + self.cu.executemany("insert into test(income) values (?)", 42) + + def test_fetch_iter(self): + # Optional DB-API extension. + self.cu.execute("delete from test") + self.cu.execute("insert into test(id) values (?)", (5,)) + self.cu.execute("insert into test(id) values (?)", (6,)) + self.cu.execute("select id from test order by id") + lst = [] + for row in self.cu: + lst.append(row[0]) + self.assertEqual(lst[0], 5) + self.assertEqual(lst[1], 6) + + def test_fetchone(self): + self.cu.execute("select name from test") + row = self.cu.fetchone() + self.assertEqual(row[0], "foo") + row = self.cu.fetchone() + self.assertEqual(row, None) + + def test_fetchone_no_statement(self): + cur = self.cx.cursor() + row = cur.fetchone() + self.assertEqual(row, None) + + def test_array_size(self): + # must default to 1 + self.assertEqual(self.cu.arraysize, 1) + + # now set to 2 + self.cu.arraysize = 2 + + # now make the query return 3 rows + self.cu.execute("delete from test") + self.cu.execute("insert into test(name) values ('A')") + self.cu.execute("insert into test(name) values ('B')") + self.cu.execute("insert into test(name) values ('C')") + self.cu.execute("select name from test") + res = self.cu.fetchmany() + + self.assertEqual(len(res), 2) + + def test_fetchmany(self): + self.cu.execute("select name from test") + res = self.cu.fetchmany(100) + self.assertEqual(len(res), 1) + res = self.cu.fetchmany(100) + self.assertEqual(res, []) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_fetchmany_kw_arg(self): + """Checks if fetchmany works with keyword arguments""" + self.cu.execute("select name from test") + res = self.cu.fetchmany(size=100) + self.assertEqual(len(res), 1) + + def test_fetchall(self): + self.cu.execute("select name from test") + res = self.cu.fetchall() + self.assertEqual(len(res), 1) + res = self.cu.fetchall() + self.assertEqual(res, []) + + def test_setinputsizes(self): + self.cu.setinputsizes([3, 4, 5]) + + def test_setoutputsize(self): + self.cu.setoutputsize(5, 0) + + def test_setoutputsize_no_column(self): + self.cu.setoutputsize(42) + + def test_cursor_connection(self): + # Optional DB-API extension. + self.assertEqual(self.cu.connection, self.cx) + + def test_wrong_cursor_callable(self): + with self.assertRaises(TypeError): + def f(): pass + cur = self.cx.cursor(f) + + def test_cursor_wrong_class(self): + class Foo: pass + foo = Foo() + with self.assertRaises(TypeError): + cur = sqlite.Cursor(foo) + + def test_last_row_id_on_replace(self): + """ + INSERT OR REPLACE and REPLACE INTO should produce the same behavior. + """ + sql = '{} INTO test(id, unique_test) VALUES (?, ?)' + for statement in ('INSERT OR REPLACE', 'REPLACE'): + with self.subTest(statement=statement): + self.cu.execute(sql.format(statement), (1, 'foo')) + self.assertEqual(self.cu.lastrowid, 1) + + def test_last_row_id_on_ignore(self): + self.cu.execute( + "insert or ignore into test(unique_test) values (?)", + ('test',)) + self.assertEqual(self.cu.lastrowid, 2) + self.cu.execute( + "insert or ignore into test(unique_test) values (?)", + ('test',)) + self.assertEqual(self.cu.lastrowid, 2) + + def test_last_row_id_insert_o_r(self): + results = [] + for statement in ('FAIL', 'ABORT', 'ROLLBACK'): + sql = 'INSERT OR {} INTO test(unique_test) VALUES (?)' + with self.subTest(statement='INSERT OR {}'.format(statement)): + self.cu.execute(sql.format(statement), (statement,)) + results.append((statement, self.cu.lastrowid)) + with self.assertRaises(sqlite.IntegrityError): + self.cu.execute(sql.format(statement), (statement,)) + results.append((statement, self.cu.lastrowid)) + expected = [ + ('FAIL', 2), ('FAIL', 2), + ('ABORT', 3), ('ABORT', 3), + ('ROLLBACK', 4), ('ROLLBACK', 4), + ] + self.assertEqual(results, expected) + + def test_column_count(self): + # Check that column count is updated correctly for cached statements + select = "select * from test" + res = self.cu.execute(select) + old_count = len(res.description) + # Add a new column and execute the cached select query again + self.cu.execute("alter table test add newcol") + res = self.cu.execute(select) + new_count = len(res.description) + self.assertEqual(new_count - old_count, 1) + + def test_same_query_in_multiple_cursors(self): + cursors = [self.cx.execute("select 1") for _ in range(3)] + for cu in cursors: + self.assertEqual(cu.fetchall(), [(1,)]) + + +class BlobTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cx.execute("create table test(b blob)") + self.data = b"this blob data string is exactly fifty bytes long!" + self.cx.execute("insert into test(b) values (?)", (self.data,)) + self.blob = self.cx.blobopen("test", "b", 1) + + def tearDown(self): + self.blob.close() + self.cx.close() + + def test_blob_is_a_blob(self): + self.assertIsInstance(self.blob, sqlite.Blob) + + def test_blob_seek_and_tell(self): + self.blob.seek(10) + self.assertEqual(self.blob.tell(), 10) + + self.blob.seek(10, SEEK_SET) + self.assertEqual(self.blob.tell(), 10) + + self.blob.seek(10, SEEK_CUR) + self.assertEqual(self.blob.tell(), 20) + + self.blob.seek(-10, SEEK_END) + self.assertEqual(self.blob.tell(), 40) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_seek_error(self): + msg_oor = "offset out of blob range" + msg_orig = "'origin' should be os.SEEK_SET, os.SEEK_CUR, or os.SEEK_END" + msg_of = "seek offset results in overflow" + + dataset = ( + (ValueError, msg_oor, lambda: self.blob.seek(1000)), + (ValueError, msg_oor, lambda: self.blob.seek(-10)), + (ValueError, msg_orig, lambda: self.blob.seek(10, -1)), + (ValueError, msg_orig, lambda: self.blob.seek(10, 3)), + ) + for exc, msg, fn in dataset: + with self.subTest(exc=exc, msg=msg, fn=fn): + self.assertRaisesRegex(exc, msg, fn) + + # Force overflow errors + self.blob.seek(1, SEEK_SET) + with self.assertRaisesRegex(OverflowError, msg_of): + self.blob.seek(INT_MAX, SEEK_CUR) + with self.assertRaisesRegex(OverflowError, msg_of): + self.blob.seek(INT_MAX, SEEK_END) + + def test_blob_read(self): + buf = self.blob.read() + self.assertEqual(buf, self.data) + + def test_blob_read_oversized(self): + buf = self.blob.read(len(self.data) * 2) + self.assertEqual(buf, self.data) + + def test_blob_read_advance_offset(self): + n = 10 + buf = self.blob.read(n) + self.assertEqual(buf, self.data[:n]) + self.assertEqual(self.blob.tell(), n) + + def test_blob_read_at_offset(self): + self.blob.seek(10) + self.assertEqual(self.blob.read(10), self.data[10:20]) + + def test_blob_read_error_row_changed(self): + self.cx.execute("update test set b='aaaa' where rowid=1") + with self.assertRaises(sqlite.OperationalError): + self.blob.read() + + def test_blob_write(self): + new_data = b"new data".ljust(50) + self.blob.write(new_data) + row = self.cx.execute("select b from test").fetchone() + self.assertEqual(row[0], new_data) + + def test_blob_write_at_offset(self): + new_data = b"c" * 25 + self.blob.seek(25) + self.blob.write(new_data) + row = self.cx.execute("select b from test").fetchone() + self.assertEqual(row[0], self.data[:25] + new_data) + + def test_blob_write_advance_offset(self): + self.blob.write(b"d"*10) + self.assertEqual(self.blob.tell(), 10) + + def test_blob_write_error_length(self): + with self.assertRaisesRegex(ValueError, "data longer than blob"): + self.blob.write(b"a" * 1000) + + self.blob.seek(0, SEEK_SET) + n = len(self.blob) + self.blob.write(b"a" * (n-1)) + self.blob.write(b"a") + with self.assertRaisesRegex(ValueError, "data longer than blob"): + self.blob.write(b"a") + + def test_blob_write_error_row_changed(self): + self.cx.execute("update test set b='aaaa' where rowid=1") + with self.assertRaises(sqlite.OperationalError): + self.blob.write(b"aaa") + + def test_blob_write_error_readonly(self): + ro_blob = self.cx.blobopen("test", "b", 1, readonly=True) + with self.assertRaisesRegex(sqlite.OperationalError, "readonly"): + ro_blob.write(b"aaa") + ro_blob.close() + + def test_blob_open_error(self): + dataset = ( + (("test", "b", 1), {"name": "notexisting"}), + (("notexisting", "b", 1), {}), + (("test", "notexisting", 1), {}), + (("test", "b", 2), {}), + ) + regex = "no such" + for args, kwds in dataset: + with self.subTest(args=args, kwds=kwds): + with self.assertRaisesRegex(sqlite.OperationalError, regex): + self.cx.blobopen(*args, **kwds) + + def test_blob_length(self): + self.assertEqual(len(self.blob), 50) + + def test_blob_get_item(self): + self.assertEqual(self.blob[5], ord("b")) + self.assertEqual(self.blob[6], ord("l")) + self.assertEqual(self.blob[7], ord("o")) + self.assertEqual(self.blob[8], ord("b")) + self.assertEqual(self.blob[-1], ord("!")) + + def test_blob_set_item(self): + self.blob[0] = ord("b") + expected = b"b" + self.data[1:] + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, expected) + + def test_blob_set_item_with_offset(self): + self.blob.seek(0, SEEK_END) + self.assertEqual(self.blob.read(), b"") # verify that we're at EOB + self.blob[0] = ord("T") + self.blob[-1] = ord(".") + self.blob.seek(0, SEEK_SET) + expected = b"This blob data string is exactly fifty bytes long." + self.assertEqual(self.blob.read(), expected) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_set_slice_buffer_object(self): + from array import array + self.blob[0:5] = memoryview(b"12345") + self.assertEqual(self.blob[0:5], b"12345") + + self.blob[0:5] = bytearray(b"23456") + self.assertEqual(self.blob[0:5], b"23456") + + self.blob[0:5] = array("b", [1, 2, 3, 4, 5]) + self.assertEqual(self.blob[0:5], b"\x01\x02\x03\x04\x05") + + def test_blob_set_item_negative_index(self): + self.blob[-1] = 255 + self.assertEqual(self.blob[-1], 255) + + def test_blob_get_slice(self): + self.assertEqual(self.blob[5:14], b"blob data") + + def test_blob_get_empty_slice(self): + self.assertEqual(self.blob[5:5], b"") + + def test_blob_get_slice_negative_index(self): + self.assertEqual(self.blob[5:-5], self.data[5:-5]) + + def test_blob_get_slice_with_skip(self): + self.assertEqual(self.blob[0:10:2], b"ti lb") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_set_slice(self): + self.blob[0:5] = b"12345" + expected = b"12345" + self.data[5:] + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, expected) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_set_empty_slice(self): + self.blob[0:0] = b"" + self.assertEqual(self.blob[:], self.data) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_set_slice_with_skip(self): + self.blob[0:10:2] = b"12345" + actual = self.cx.execute("select b from test").fetchone()[0] + expected = b"1h2s3b4o5 " + self.data[10:] + self.assertEqual(actual, expected) + + def test_blob_mapping_invalid_index_type(self): + msg = "indices must be integers" + with self.assertRaisesRegex(TypeError, msg): + self.blob[5:5.5] + with self.assertRaisesRegex(TypeError, msg): + self.blob[1.5] + with self.assertRaisesRegex(TypeError, msg): + self.blob["a"] = b"b" + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_get_item_error(self): + dataset = [len(self.blob), 105, -105] + for idx in dataset: + with self.subTest(idx=idx): + with self.assertRaisesRegex(IndexError, "index out of range"): + self.blob[idx] + with self.assertRaisesRegex(IndexError, "cannot fit 'int'"): + self.blob[ULLONG_MAX] + + # Provoke read error + self.cx.execute("update test set b='aaaa' where rowid=1") + with self.assertRaises(sqlite.OperationalError): + self.blob[0] + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_set_item_error(self): + with self.assertRaisesRegex(TypeError, "cannot be interpreted"): + self.blob[0] = b"multiple" + with self.assertRaisesRegex(TypeError, "cannot be interpreted"): + self.blob[0] = b"1" + with self.assertRaisesRegex(TypeError, "cannot be interpreted"): + self.blob[0] = bytearray(b"1") + with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"): + del self.blob[0] + with self.assertRaisesRegex(IndexError, "Blob index out of range"): + self.blob[1000] = 0 + with self.assertRaisesRegex(ValueError, "must be in range"): + self.blob[0] = -1 + with self.assertRaisesRegex(ValueError, "must be in range"): + self.blob[0] = 256 + # Overflow errors are overridden with ValueError + with self.assertRaisesRegex(ValueError, "must be in range"): + self.blob[0] = 2**65 + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_set_slice_error(self): + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[5:10] = b"a" + with self.assertRaisesRegex(IndexError, "wrong size"): + self.blob[5:10] = b"a" * 1000 + with self.assertRaisesRegex(TypeError, "doesn't support.*deletion"): + del self.blob[5:10] + with self.assertRaisesRegex(ValueError, "step cannot be zero"): + self.blob[5:10:0] = b"12345" + with self.assertRaises(BufferError): + self.blob[5:10] = memoryview(b"abcde")[::2] + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_sequence_not_supported(self): + with self.assertRaisesRegex(TypeError, "unsupported operand"): + self.blob + self.blob + with self.assertRaisesRegex(TypeError, "unsupported operand"): + self.blob * 5 + with self.assertRaisesRegex(TypeError, "is not iterable"): + b"a" in self.blob + + def test_blob_context_manager(self): + data = b"a" * 50 + with self.cx.blobopen("test", "b", 1) as blob: + blob.write(data) + actual = self.cx.execute("select b from test").fetchone()[0] + self.assertEqual(actual, data) + + # Check that __exit__ closed the blob + with self.assertRaisesRegex(sqlite.ProgrammingError, "closed blob"): + blob.read() + + def test_blob_context_manager_reraise_exceptions(self): + class DummyException(Exception): + pass + with self.assertRaisesRegex(DummyException, "reraised"): + with self.cx.blobopen("test", "b", 1) as blob: + raise DummyException("reraised") + + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_closed(self): + with memory_database() as cx: + cx.execute("create table test(b blob)") + cx.execute("insert into test values (zeroblob(100))") + blob = cx.blobopen("test", "b", 1) + blob.close() + + msg = "Cannot operate on a closed blob" + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.read() + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.write(b"") + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.seek(0) + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.tell() + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.__enter__() + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob.__exit__(None, None, None) + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + len(blob) + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob[0] + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob[0:1] + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + blob[0] = b"" + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_blob_closed_db_read(self): + with memory_database() as cx: + cx.execute("create table test(b blob)") + cx.execute("insert into test(b) values (zeroblob(100))") + blob = cx.blobopen("test", "b", 1) + cx.close() + self.assertRaisesRegex(sqlite.ProgrammingError, + "Cannot operate on a closed database", + blob.read) + + +# TODO: RUSTPYTHON +# @threading_helper.requires_working_threading() +class ThreadTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + self.cur = self.con.cursor() + self.cur.execute("create table test(name text, b blob)") + self.cur.execute("insert into test values('blob', zeroblob(1))") + + def tearDown(self): + self.cur.close() + self.con.close() + + @threading_helper.reap_threads + def _run_test(self, fn, *args, **kwds): + def run(err): + try: + fn(*args, **kwds) + err.append("did not raise ProgrammingError") + except sqlite.ProgrammingError: + pass + except: + err.append("raised wrong exception") + + err = [] + t = threading.Thread(target=run, kwargs={"err": err}) + t.start() + t.join() + if err: + self.fail("\n".join(err)) + + def test_check_connection_thread(self): + fns = [ + lambda: self.con.cursor(), + lambda: self.con.commit(), + lambda: self.con.rollback(), + lambda: self.con.close(), + lambda: self.con.set_trace_callback(None), + lambda: self.con.set_authorizer(None), + lambda: self.con.create_collation("foo", None), + lambda: self.con.setlimit(sqlite.SQLITE_LIMIT_LENGTH, -1), + lambda: self.con.getlimit(sqlite.SQLITE_LIMIT_LENGTH), + lambda: self.con.blobopen("test", "b", 1), + ] + 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): + self._run_test(fn) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_check_cursor_thread(self): + fns = [ + lambda: self.cur.execute("insert into test(name) values('a')"), + lambda: self.cur.close(), + lambda: self.cur.execute("select name from test"), + lambda: self.cur.fetchone(), + ] + for fn in fns: + with self.subTest(fn=fn): + self._run_test(fn) + + + @threading_helper.reap_threads + def test_dont_check_same_thread(self): + def run(con, err): + try: + con.execute("select 1") + except sqlite.Error: + err.append("multi-threading not allowed") + + con = sqlite.connect(":memory:", check_same_thread=False) + err = [] + t = threading.Thread(target=run, kwargs={"con": con, "err": err}) + t.start() + t.join() + self.assertEqual(len(err), 0, "\n".join(err)) + + +class ConstructorTests(unittest.TestCase): + def test_date(self): + d = sqlite.Date(2004, 10, 28) + + def test_time(self): + t = sqlite.Time(12, 39, 35) + + def test_timestamp(self): + ts = sqlite.Timestamp(2004, 10, 28, 12, 39, 35) + + def test_date_from_ticks(self): + d = sqlite.DateFromTicks(42) + + def test_time_from_ticks(self): + t = sqlite.TimeFromTicks(42) + + def test_timestamp_from_ticks(self): + ts = sqlite.TimestampFromTicks(42) + + def test_binary(self): + b = sqlite.Binary(b"\0'") + +class ExtensionTests(unittest.TestCase): + def test_script_string_sql(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + cur.executescript(""" + -- bla bla + /* a stupid comment */ + create table a(i); + insert into a(i) values (5); + """) + cur.execute("select i from a") + res = cur.fetchone()[0] + self.assertEqual(res, 5) + + def test_script_syntax_error(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + with self.assertRaises(sqlite.OperationalError): + cur.executescript("create table test(x); asdf; create table test2(x)") + + def test_script_error_normal(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + with self.assertRaises(sqlite.OperationalError): + cur.executescript("create table test(sadfsadfdsa); select foo from hurz;") + + def test_cursor_executescript_as_bytes(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + with self.assertRaises(TypeError): + cur.executescript(b"create table test(foo); insert into test(foo) values (5);") + + def test_cursor_executescript_with_null_characters(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + with self.assertRaises(ValueError): + cur.executescript(""" + create table a(i);\0 + insert into a(i) values (5); + """) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_cursor_executescript_with_surrogates(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + with self.assertRaises(UnicodeEncodeError): + cur.executescript(""" + create table a(s); + insert into a(s) values ('\ud8ff'); + """) + + def test_cursor_executescript_too_large_script(self): + msg = "query string is too large" + with memory_database() as cx, cx_limit(cx) as lim: + cx.executescript("select 'almost too large'".ljust(lim)) + with self.assertRaisesRegex(sqlite.DataError, msg): + cx.executescript("select 'too large'".ljust(lim+1)) + + def test_cursor_executescript_tx_control(self): + con = sqlite.connect(":memory:") + con.execute("begin") + self.assertTrue(con.in_transaction) + con.executescript("select 1") + self.assertFalse(con.in_transaction) + + def test_connection_execute(self): + con = sqlite.connect(":memory:") + result = con.execute("select 5").fetchone()[0] + self.assertEqual(result, 5, "Basic test of Connection.execute") + + def test_connection_executemany(self): + con = sqlite.connect(":memory:") + con.execute("create table test(foo)") + con.executemany("insert into test(foo) values (?)", [(3,), (4,)]) + result = con.execute("select foo from test order by foo").fetchall() + self.assertEqual(result[0][0], 3, "Basic test of Connection.executemany") + self.assertEqual(result[1][0], 4, "Basic test of Connection.executemany") + + def test_connection_executescript(self): + con = sqlite.connect(":memory:") + con.executescript("create table test(foo); insert into test(foo) values (5);") + result = con.execute("select foo from test").fetchone()[0] + self.assertEqual(result, 5, "Basic test of Connection.executescript") + +class ClosedConTests(unittest.TestCase): + def test_closed_con_cursor(self): + con = sqlite.connect(":memory:") + con.close() + with self.assertRaises(sqlite.ProgrammingError): + cur = con.cursor() + + def test_closed_con_commit(self): + con = sqlite.connect(":memory:") + con.close() + with self.assertRaises(sqlite.ProgrammingError): + con.commit() + + def test_closed_con_rollback(self): + con = sqlite.connect(":memory:") + con.close() + with self.assertRaises(sqlite.ProgrammingError): + con.rollback() + + def test_closed_cur_execute(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + con.close() + with self.assertRaises(sqlite.ProgrammingError): + cur.execute("select 4") + + def test_closed_create_function(self): + con = sqlite.connect(":memory:") + con.close() + def f(x): return 17 + with self.assertRaises(sqlite.ProgrammingError): + con.create_function("foo", 1, f) + + def test_closed_create_aggregate(self): + con = sqlite.connect(":memory:") + con.close() + class Agg: + def __init__(self): + pass + def step(self, x): + pass + def finalize(self): + return 17 + with self.assertRaises(sqlite.ProgrammingError): + con.create_aggregate("foo", 1, Agg) + + def test_closed_set_authorizer(self): + con = sqlite.connect(":memory:") + con.close() + def authorizer(*args): + return sqlite.DENY + with self.assertRaises(sqlite.ProgrammingError): + con.set_authorizer(authorizer) + + def test_closed_set_progress_callback(self): + con = sqlite.connect(":memory:") + con.close() + def progress(): pass + with self.assertRaises(sqlite.ProgrammingError): + con.set_progress_handler(progress, 100) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_closed_call(self): + con = sqlite.connect(":memory:") + con.close() + with self.assertRaises(sqlite.ProgrammingError): + con() + +class ClosedCurTests(unittest.TestCase): + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_closed(self): + con = sqlite.connect(":memory:") + cur = con.cursor() + cur.close() + + for method_name in ("execute", "executemany", "executescript", "fetchall", "fetchmany", "fetchone"): + if method_name in ("execute", "executescript"): + params = ("select 4 union select 5",) + elif method_name == "executemany": + params = ("insert into foo(bar) values (?)", [(3,), (4,)]) + else: + params = [] + + with self.assertRaises(sqlite.ProgrammingError): + method = getattr(cur, method_name) + method(*params) + + +class SqliteOnConflictTests(unittest.TestCase): + """ + Tests for SQLite's "insert on conflict" feature. + + See https://www.sqlite.org/lang_conflict.html for details. + """ + + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cu = self.cx.cursor() + self.cu.execute(""" + CREATE TABLE test( + id INTEGER PRIMARY KEY, name TEXT, unique_name TEXT UNIQUE + ); + """) + + def tearDown(self): + self.cu.close() + self.cx.close() + + def test_on_conflict_rollback_with_explicit_transaction(self): + self.cx.isolation_level = None # autocommit mode + self.cu = self.cx.cursor() + # Start an explicit transaction. + self.cu.execute("BEGIN") + self.cu.execute("INSERT INTO test(name) VALUES ('abort_test')") + self.cu.execute("INSERT OR ROLLBACK INTO test(unique_name) VALUES ('foo')") + with self.assertRaises(sqlite.IntegrityError): + self.cu.execute("INSERT OR ROLLBACK INTO test(unique_name) VALUES ('foo')") + # Use connection to commit. + self.cx.commit() + self.cu.execute("SELECT name, unique_name from test") + # Transaction should have rolled back and nothing should be in table. + self.assertEqual(self.cu.fetchall(), []) + + def test_on_conflict_abort_raises_with_explicit_transactions(self): + # Abort cancels the current sql statement but doesn't change anything + # about the current transaction. + self.cx.isolation_level = None # autocommit mode + self.cu = self.cx.cursor() + # Start an explicit transaction. + self.cu.execute("BEGIN") + self.cu.execute("INSERT INTO test(name) VALUES ('abort_test')") + self.cu.execute("INSERT OR ABORT INTO test(unique_name) VALUES ('foo')") + with self.assertRaises(sqlite.IntegrityError): + self.cu.execute("INSERT OR ABORT INTO test(unique_name) VALUES ('foo')") + self.cx.commit() + self.cu.execute("SELECT name, unique_name FROM test") + # Expect the first two inserts to work, third to do nothing. + self.assertEqual(self.cu.fetchall(), [('abort_test', None), (None, 'foo',)]) + + def test_on_conflict_rollback_without_transaction(self): + # Start of implicit transaction + self.cu.execute("INSERT INTO test(name) VALUES ('abort_test')") + self.cu.execute("INSERT OR ROLLBACK INTO test(unique_name) VALUES ('foo')") + with self.assertRaises(sqlite.IntegrityError): + self.cu.execute("INSERT OR ROLLBACK INTO test(unique_name) VALUES ('foo')") + self.cu.execute("SELECT name, unique_name FROM test") + # Implicit transaction is rolled back on error. + self.assertEqual(self.cu.fetchall(), []) + + def test_on_conflict_abort_raises_without_transactions(self): + # Abort cancels the current sql statement but doesn't change anything + # about the current transaction. + self.cu.execute("INSERT INTO test(name) VALUES ('abort_test')") + self.cu.execute("INSERT OR ABORT INTO test(unique_name) VALUES ('foo')") + with self.assertRaises(sqlite.IntegrityError): + self.cu.execute("INSERT OR ABORT INTO test(unique_name) VALUES ('foo')") + # Make sure all other values were inserted. + self.cu.execute("SELECT name, unique_name FROM test") + self.assertEqual(self.cu.fetchall(), [('abort_test', None), (None, 'foo',)]) + + def test_on_conflict_fail(self): + self.cu.execute("INSERT OR FAIL INTO test(unique_name) VALUES ('foo')") + with self.assertRaises(sqlite.IntegrityError): + self.cu.execute("INSERT OR FAIL INTO test(unique_name) VALUES ('foo')") + self.assertEqual(self.cu.fetchall(), []) + + def test_on_conflict_ignore(self): + self.cu.execute("INSERT OR IGNORE INTO test(unique_name) VALUES ('foo')") + # Nothing should happen. + self.cu.execute("INSERT OR IGNORE INTO test(unique_name) VALUES ('foo')") + self.cu.execute("SELECT unique_name FROM test") + self.assertEqual(self.cu.fetchall(), [('foo',)]) + + def test_on_conflict_replace(self): + self.cu.execute("INSERT OR REPLACE INTO test(name, unique_name) VALUES ('Data!', 'foo')") + # There shouldn't be an IntegrityError exception. + self.cu.execute("INSERT OR REPLACE INTO test(name, unique_name) VALUES ('Very different data!', 'foo')") + self.cu.execute("SELECT name, unique_name FROM test") + self.assertEqual(self.cu.fetchall(), [('Very different data!', 'foo')]) + + +# TODO: RUSTPYTHON +# @requires_subprocess() +class MultiprocessTests(unittest.TestCase): + CONNECTION_TIMEOUT = SHORT_TIMEOUT / 1000. # Defaults to 30 ms + + def tearDown(self): + unlink(TESTFN) + + def test_ctx_mgr_rollback_if_commit_failed(self): + # bpo-27334: ctx manager does not rollback if commit fails + SCRIPT = f"""if 1: + import sqlite3 + def wait(): + print("started") + assert "database is locked" in input() + + cx = sqlite3.connect("{TESTFN}", timeout={self.CONNECTION_TIMEOUT}) + cx.create_function("wait", 0, wait) + with cx: + cx.execute("create table t(t)") + try: + # execute two transactions; both will try to lock the db + cx.executescript(''' + -- start a transaction and wait for parent + begin transaction; + select * from t; + select wait(); + rollback; + + -- start a new transaction; would fail if parent holds lock + begin transaction; + select * from t; + rollback; + ''') + finally: + cx.close() + """ + + # spawn child process + proc = subprocess.Popen( + [sys.executable, "-c", SCRIPT], + encoding="utf-8", + bufsize=0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + self.addCleanup(proc.communicate) + + # wait for child process to start + self.assertEqual("started", proc.stdout.readline().strip()) + + cx = sqlite.connect(TESTFN, timeout=self.CONNECTION_TIMEOUT) + try: # context manager should correctly release the db lock + with cx: + cx.execute("insert into t values('test')") + except sqlite.OperationalError as exc: + proc.stdin.write(str(exc)) + else: + proc.stdin.write("no error") + finally: + cx.close() + + # terminate child process + self.assertIsNone(proc.returncode) + try: + proc.communicate(input="end", timeout=SHORT_TIMEOUT) + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + raise + self.assertEqual(proc.returncode, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_dump.py b/Lib/test/test_sqlite3/test_dump.py new file mode 100644 index 000000000..ec4a11da8 --- /dev/null +++ b/Lib/test/test_sqlite3/test_dump.py @@ -0,0 +1,124 @@ +# Author: Paul Kippes + +import unittest +import sqlite3 as sqlite +from .test_dbapi import memory_database + + +# TODO: RUSTPYTHON +@unittest.expectedFailure +class DumpTests(unittest.TestCase): + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cu = self.cx.cursor() + + def tearDown(self): + self.cx.close() + + def test_table_dump(self): + expected_sqls = [ + """CREATE TABLE "index"("index" blob);""" + , + """INSERT INTO "index" VALUES(X'01');""" + , + """CREATE TABLE "quoted""table"("quoted""field" text);""" + , + """INSERT INTO "quoted""table" VALUES('quoted''value');""" + , + "CREATE TABLE t1(id integer primary key, s1 text, " \ + "t1_i1 integer not null, i2 integer, unique (s1), " \ + "constraint t1_idx1 unique (i2));" + , + "INSERT INTO \"t1\" VALUES(1,'foo',10,20);" + , + "INSERT INTO \"t1\" VALUES(2,'foo2',30,30);" + , + "CREATE TABLE t2(id integer, t2_i1 integer, " \ + "t2_i2 integer, primary key (id)," \ + "foreign key(t2_i1) references t1(t1_i1));" + , + "CREATE TRIGGER trigger_1 update of t1_i1 on t1 " \ + "begin " \ + "update t2 set t2_i1 = new.t1_i1 where t2_i1 = old.t1_i1; " \ + "end;" + , + "CREATE VIEW v1 as select * from t1 left join t2 " \ + "using (id);" + ] + [self.cu.execute(s) for s in expected_sqls] + i = self.cx.iterdump() + actual_sqls = [s for s in i] + expected_sqls = ['BEGIN TRANSACTION;'] + expected_sqls + \ + ['COMMIT;'] + [self.assertEqual(expected_sqls[i], actual_sqls[i]) + for i in range(len(expected_sqls))] + + def test_dump_autoincrement(self): + expected = [ + 'CREATE TABLE "t1" (id integer primary key autoincrement);', + 'INSERT INTO "t1" VALUES(NULL);', + 'CREATE TABLE "t2" (id integer primary key autoincrement);', + ] + self.cu.executescript("".join(expected)) + + # the NULL value should now be automatically be set to 1 + expected[1] = expected[1].replace("NULL", "1") + expected.insert(0, "BEGIN TRANSACTION;") + expected.extend([ + 'DELETE FROM "sqlite_sequence";', + 'INSERT INTO "sqlite_sequence" VALUES(\'t1\',1);', + 'COMMIT;', + ]) + + actual = [stmt for stmt in self.cx.iterdump()] + self.assertEqual(expected, actual) + + def test_dump_autoincrement_create_new_db(self): + self.cu.execute("BEGIN TRANSACTION") + self.cu.execute("CREATE TABLE t1 (id integer primary key autoincrement)") + self.cu.execute("CREATE TABLE t2 (id integer primary key autoincrement)") + self.cu.executemany("INSERT INTO t1 VALUES(?)", ((None,) for _ in range(9))) + self.cu.executemany("INSERT INTO t2 VALUES(?)", ((None,) for _ in range(4))) + self.cx.commit() + + with memory_database() as cx2: + query = "".join(self.cx.iterdump()) + cx2.executescript(query) + cu2 = cx2.cursor() + + dataset = ( + ("t1", 9), + ("t2", 4), + ) + for table, seq in dataset: + with self.subTest(table=table, seq=seq): + res = cu2.execute(""" + SELECT "seq" FROM "sqlite_sequence" WHERE "name" == ? + """, (table,)) + rows = res.fetchall() + self.assertEqual(rows[0][0], seq) + + def test_unorderable_row(self): + # iterdump() should be able to cope with unorderable row types (issue #15545) + class UnorderableRow: + def __init__(self, cursor, row): + self.row = row + def __getitem__(self, index): + return self.row[index] + self.cx.row_factory = UnorderableRow + CREATE_ALPHA = """CREATE TABLE "alpha" ("one");""" + CREATE_BETA = """CREATE TABLE "beta" ("two");""" + expected = [ + "BEGIN TRANSACTION;", + CREATE_ALPHA, + CREATE_BETA, + "COMMIT;" + ] + self.cu.execute(CREATE_BETA) + self.cu.execute(CREATE_ALPHA) + got = list(self.cx.iterdump()) + self.assertEqual(expected, got) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_factory.py b/Lib/test/test_sqlite3/test_factory.py new file mode 100644 index 000000000..e52c10fe9 --- /dev/null +++ b/Lib/test/test_sqlite3/test_factory.py @@ -0,0 +1,330 @@ +# pysqlite2/test/factory.py: tests for the various factories in pysqlite +# +# Copyright (C) 2005-2007 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import unittest +import sqlite3 as sqlite +from collections.abc import Sequence + + +def dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + +class MyCursor(sqlite.Cursor): + def __init__(self, *args, **kwargs): + sqlite.Cursor.__init__(self, *args, **kwargs) + self.row_factory = dict_factory + +class ConnectionFactoryTests(unittest.TestCase): + def test_connection_factories(self): + class DefectFactory(sqlite.Connection): + def __init__(self, *args, **kwargs): + return None + class OkFactory(sqlite.Connection): + def __init__(self, *args, **kwargs): + sqlite.Connection.__init__(self, *args, **kwargs) + + for factory in DefectFactory, OkFactory: + with self.subTest(factory=factory): + con = sqlite.connect(":memory:", factory=factory) + self.assertIsInstance(con, factory) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_connection_factory_relayed_call(self): + # gh-95132: keyword args must not be passed as positional args + class Factory(sqlite.Connection): + def __init__(self, *args, **kwargs): + kwargs["isolation_level"] = None + super(Factory, self).__init__(*args, **kwargs) + + con = sqlite.connect(":memory:", factory=Factory) + self.assertIsNone(con.isolation_level) + self.assertIsInstance(con, Factory) + + def test_connection_factory_as_positional_arg(self): + class Factory(sqlite.Connection): + def __init__(self, *args, **kwargs): + super(Factory, self).__init__(*args, **kwargs) + + con = sqlite.connect(":memory:", 5.0, 0, None, True, Factory) + self.assertIsNone(con.isolation_level) + self.assertIsInstance(con, Factory) + + +class CursorFactoryTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + def tearDown(self): + self.con.close() + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_is_instance(self): + cur = self.con.cursor() + self.assertIsInstance(cur, sqlite.Cursor) + cur = self.con.cursor(MyCursor) + self.assertIsInstance(cur, MyCursor) + cur = self.con.cursor(factory=lambda con: MyCursor(con)) + self.assertIsInstance(cur, MyCursor) + + def test_invalid_factory(self): + # not a callable at all + self.assertRaises(TypeError, self.con.cursor, None) + # invalid callable with not exact one argument + self.assertRaises(TypeError, self.con.cursor, lambda: None) + # invalid callable returning non-cursor + self.assertRaises(TypeError, self.con.cursor, lambda con: None) + +class RowFactoryTestsBackwardsCompat(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_is_produced_by_factory(self): + cur = self.con.cursor(factory=MyCursor) + cur.execute("select 4+5 as foo") + row = cur.fetchone() + self.assertIsInstance(row, dict) + cur.close() + + def tearDown(self): + self.con.close() + +class RowFactoryTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + def test_custom_factory(self): + self.con.row_factory = lambda cur, row: list(row) + row = self.con.execute("select 1, 2").fetchone() + self.assertIsInstance(row, list) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_sqlite_row_index(self): + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1 as a_1, 2 as b").fetchone() + self.assertIsInstance(row, sqlite.Row) + + self.assertEqual(row["a_1"], 1, "by name: wrong result for column 'a_1'") + self.assertEqual(row["b"], 2, "by name: wrong result for column 'b'") + + self.assertEqual(row["A_1"], 1, "by name: wrong result for column 'A_1'") + self.assertEqual(row["B"], 2, "by name: wrong result for column 'B'") + + self.assertEqual(row[0], 1, "by index: wrong result for column 0") + self.assertEqual(row[1], 2, "by index: wrong result for column 1") + self.assertEqual(row[-1], 2, "by index: wrong result for column -1") + self.assertEqual(row[-2], 1, "by index: wrong result for column -2") + + with self.assertRaises(IndexError): + row['c'] + with self.assertRaises(IndexError): + row['a_\x11'] + with self.assertRaises(IndexError): + row['a\x7f1'] + with self.assertRaises(IndexError): + row[2] + with self.assertRaises(IndexError): + row[-3] + with self.assertRaises(IndexError): + row[2**1000] + with self.assertRaises(IndexError): + row[complex()] # index must be int or string + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_sqlite_row_index_unicode(self): + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1 as \xff").fetchone() + self.assertEqual(row["\xff"], 1) + with self.assertRaises(IndexError): + row['\u0178'] + with self.assertRaises(IndexError): + row['\xdf'] + + def test_sqlite_row_slice(self): + # A sqlite.Row can be sliced like a list. + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1, 2, 3, 4").fetchone() + self.assertEqual(row[0:0], ()) + self.assertEqual(row[0:1], (1,)) + self.assertEqual(row[1:3], (2, 3)) + self.assertEqual(row[3:1], ()) + # Explicit bounds are optional. + self.assertEqual(row[1:], (2, 3, 4)) + self.assertEqual(row[:3], (1, 2, 3)) + # Slices can use negative indices. + self.assertEqual(row[-2:-1], (3,)) + self.assertEqual(row[-2:], (3, 4)) + # Slicing supports steps. + self.assertEqual(row[0:4:2], (1, 3)) + self.assertEqual(row[3:0:-2], (4, 2)) + + def test_sqlite_row_iter(self): + """Checks if the row object is iterable""" + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1 as a, 2 as b").fetchone() + for col in row: + pass + + def test_sqlite_row_as_tuple(self): + """Checks if the row object can be converted to a tuple""" + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1 as a, 2 as b").fetchone() + t = tuple(row) + self.assertEqual(t, (row['a'], row['b'])) + + def test_sqlite_row_as_dict(self): + """Checks if the row object can be correctly converted to a dictionary""" + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1 as a, 2 as b").fetchone() + d = dict(row) + self.assertEqual(d["a"], row["a"]) + self.assertEqual(d["b"], row["b"]) + + def test_sqlite_row_hash_cmp(self): + """Checks if the row object compares and hashes correctly""" + self.con.row_factory = sqlite.Row + row_1 = self.con.execute("select 1 as a, 2 as b").fetchone() + row_2 = self.con.execute("select 1 as a, 2 as b").fetchone() + row_3 = self.con.execute("select 1 as a, 3 as b").fetchone() + row_4 = self.con.execute("select 1 as b, 2 as a").fetchone() + row_5 = self.con.execute("select 2 as b, 1 as a").fetchone() + + self.assertTrue(row_1 == row_1) + self.assertTrue(row_1 == row_2) + self.assertFalse(row_1 == row_3) + self.assertFalse(row_1 == row_4) + self.assertFalse(row_1 == row_5) + self.assertFalse(row_1 == object()) + + self.assertFalse(row_1 != row_1) + self.assertFalse(row_1 != row_2) + self.assertTrue(row_1 != row_3) + self.assertTrue(row_1 != row_4) + self.assertTrue(row_1 != row_5) + self.assertTrue(row_1 != object()) + + with self.assertRaises(TypeError): + row_1 > row_2 + with self.assertRaises(TypeError): + row_1 < row_2 + with self.assertRaises(TypeError): + row_1 >= row_2 + with self.assertRaises(TypeError): + row_1 <= row_2 + + self.assertEqual(hash(row_1), hash(row_2)) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_sqlite_row_as_sequence(self): + """ Checks if the row object can act like a sequence """ + self.con.row_factory = sqlite.Row + row = self.con.execute("select 1 as a, 2 as b").fetchone() + + as_tuple = tuple(row) + self.assertEqual(list(reversed(row)), list(reversed(as_tuple))) + self.assertIsInstance(row, Sequence) + + def test_fake_cursor_class(self): + # Issue #24257: Incorrect use of PyObject_IsInstance() caused + # segmentation fault. + # Issue #27861: Also applies for cursor factory. + class FakeCursor(str): + __class__ = sqlite.Cursor + self.con.row_factory = sqlite.Row + self.assertRaises(TypeError, self.con.cursor, FakeCursor) + self.assertRaises(TypeError, sqlite.Row, FakeCursor(), ()) + + def tearDown(self): + self.con.close() + +class TextFactoryTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + def test_unicode(self): + austria = "Österreich" + row = self.con.execute("select ?", (austria,)).fetchone() + self.assertEqual(type(row[0]), str, "type of row[0] must be unicode") + + def test_string(self): + self.con.text_factory = bytes + austria = "Österreich" + row = self.con.execute("select ?", (austria,)).fetchone() + self.assertEqual(type(row[0]), bytes, "type of row[0] must be bytes") + self.assertEqual(row[0], austria.encode("utf-8"), "column must equal original data in UTF-8") + + def test_custom(self): + self.con.text_factory = lambda x: str(x, "utf-8", "ignore") + austria = "Österreich" + row = self.con.execute("select ?", (austria,)).fetchone() + self.assertEqual(type(row[0]), str, "type of row[0] must be unicode") + self.assertTrue(row[0].endswith("reich"), "column must contain original data") + + def tearDown(self): + self.con.close() + +class TextFactoryTestsWithEmbeddedZeroBytes(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + self.con.execute("create table test (value text)") + self.con.execute("insert into test (value) values (?)", ("a\x00b",)) + + def test_string(self): + # text_factory defaults to str + row = self.con.execute("select value from test").fetchone() + self.assertIs(type(row[0]), str) + self.assertEqual(row[0], "a\x00b") + + def test_bytes(self): + self.con.text_factory = bytes + row = self.con.execute("select value from test").fetchone() + self.assertIs(type(row[0]), bytes) + self.assertEqual(row[0], b"a\x00b") + + def test_bytearray(self): + self.con.text_factory = bytearray + row = self.con.execute("select value from test").fetchone() + self.assertIs(type(row[0]), bytearray) + self.assertEqual(row[0], b"a\x00b") + + def test_custom(self): + # A custom factory should receive a bytes argument + self.con.text_factory = lambda x: x + row = self.con.execute("select value from test").fetchone() + self.assertIs(type(row[0]), bytes) + self.assertEqual(row[0], b"a\x00b") + + def tearDown(self): + self.con.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_hooks.py b/Lib/test/test_sqlite3/test_hooks.py new file mode 100644 index 000000000..1ab62688c --- /dev/null +++ b/Lib/test/test_sqlite3/test_hooks.py @@ -0,0 +1,356 @@ +# pysqlite2/test/hooks.py: tests for various SQLite-specific hooks +# +# Copyright (C) 2006-2007 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import contextlib +import sqlite3 as sqlite +import unittest + +from test.support.os_helper import TESTFN, unlink + +from test.test_sqlite3.test_dbapi import memory_database, cx_limit +from test.test_sqlite3.test_userfunctions import with_tracebacks + + +class CollationTests(unittest.TestCase): + def test_create_collation_not_string(self): + con = sqlite.connect(":memory:") + with self.assertRaises(TypeError): + con.create_collation(None, lambda x, y: (x > y) - (x < y)) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_create_collation_not_callable(self): + con = sqlite.connect(":memory:") + with self.assertRaises(TypeError) as cm: + con.create_collation("X", 42) + self.assertEqual(str(cm.exception), 'parameter must be callable') + + def test_create_collation_not_ascii(self): + con = sqlite.connect(":memory:") + con.create_collation("collä", lambda x, y: (x > y) - (x < y)) + + def test_create_collation_bad_upper(self): + class BadUpperStr(str): + def upper(self): + return None + con = sqlite.connect(":memory:") + mycoll = lambda x, y: -((x > y) - (x < y)) + con.create_collation(BadUpperStr("mycoll"), mycoll) + result = con.execute(""" + select x from ( + select 'a' as x + union + select 'b' as x + ) order by x collate mycoll + """).fetchall() + self.assertEqual(result[0][0], 'b') + self.assertEqual(result[1][0], 'a') + + def test_collation_is_used(self): + def mycoll(x, y): + # reverse order + return -((x > y) - (x < y)) + + con = sqlite.connect(":memory:") + con.create_collation("mycoll", mycoll) + sql = """ + select x from ( + select 'a' as x + union + select 'b' as x + union + select 'c' as x + ) order by x collate mycoll + """ + result = con.execute(sql).fetchall() + self.assertEqual(result, [('c',), ('b',), ('a',)], + msg='the expected order was not returned') + + con.create_collation("mycoll", None) + with self.assertRaises(sqlite.OperationalError) as cm: + result = con.execute(sql).fetchall() + self.assertEqual(str(cm.exception), 'no such collation sequence: mycoll') + + def test_collation_returns_large_integer(self): + def mycoll(x, y): + # reverse order + return -((x > y) - (x < y)) * 2**32 + con = sqlite.connect(":memory:") + con.create_collation("mycoll", mycoll) + sql = """ + select x from ( + select 'a' as x + union + select 'b' as x + union + select 'c' as x + ) order by x collate mycoll + """ + result = con.execute(sql).fetchall() + self.assertEqual(result, [('c',), ('b',), ('a',)], + msg="the expected order was not returned") + + def test_collation_register_twice(self): + """ + Register two different collation functions under the same name. + Verify that the last one is actually used. + """ + con = sqlite.connect(":memory:") + con.create_collation("mycoll", lambda x, y: (x > y) - (x < y)) + con.create_collation("mycoll", lambda x, y: -((x > y) - (x < y))) + result = con.execute(""" + select x from (select 'a' as x union select 'b' as x) order by x collate mycoll + """).fetchall() + self.assertEqual(result[0][0], 'b') + self.assertEqual(result[1][0], 'a') + + def test_deregister_collation(self): + """ + Register a collation, then deregister it. Make sure an error is raised if we try + to use it. + """ + con = sqlite.connect(":memory:") + con.create_collation("mycoll", lambda x, y: (x > y) - (x < y)) + con.create_collation("mycoll", None) + with self.assertRaises(sqlite.OperationalError) as cm: + con.execute("select 'a' as x union select 'b' as x order by x collate mycoll") + self.assertEqual(str(cm.exception), 'no such collation sequence: mycoll') + +class ProgressTests(unittest.TestCase): + def test_progress_handler_used(self): + """ + Test that the progress handler is invoked once it is set. + """ + con = sqlite.connect(":memory:") + progress_calls = [] + def progress(): + progress_calls.append(None) + return 0 + con.set_progress_handler(progress, 1) + con.execute(""" + create table foo(a, b) + """) + self.assertTrue(progress_calls) + + def test_opcode_count(self): + """ + Test that the opcode argument is respected. + """ + con = sqlite.connect(":memory:") + progress_calls = [] + def progress(): + progress_calls.append(None) + return 0 + con.set_progress_handler(progress, 1) + curs = con.cursor() + curs.execute(""" + create table foo (a, b) + """) + first_count = len(progress_calls) + progress_calls = [] + con.set_progress_handler(progress, 2) + curs.execute(""" + create table bar (a, b) + """) + second_count = len(progress_calls) + self.assertGreaterEqual(first_count, second_count) + + def test_cancel_operation(self): + """ + Test that returning a non-zero value stops the operation in progress. + """ + con = sqlite.connect(":memory:") + def progress(): + return 1 + con.set_progress_handler(progress, 1) + curs = con.cursor() + self.assertRaises( + sqlite.OperationalError, + curs.execute, + "create table bar (a, b)") + + def test_clear_handler(self): + """ + Test that setting the progress handler to None clears the previously set handler. + """ + con = sqlite.connect(":memory:") + action = 0 + def progress(): + nonlocal action + action = 1 + return 0 + con.set_progress_handler(progress, 1) + con.set_progress_handler(None, 1) + con.execute("select 1 union select 2 union select 3").fetchall() + self.assertEqual(action, 0, "progress handler was not cleared") + + @with_tracebacks(ZeroDivisionError, name="bad_progress") + def test_error_in_progress_handler(self): + con = sqlite.connect(":memory:") + def bad_progress(): + 1 / 0 + con.set_progress_handler(bad_progress, 1) + with self.assertRaises(sqlite.OperationalError): + con.execute(""" + create table foo(a, b) + """) + + @with_tracebacks(ZeroDivisionError, name="bad_progress") + def test_error_in_progress_handler_result(self): + con = sqlite.connect(":memory:") + class BadBool: + def __bool__(self): + 1 / 0 + def bad_progress(): + return BadBool() + con.set_progress_handler(bad_progress, 1) + with self.assertRaises(sqlite.OperationalError): + con.execute(""" + create table foo(a, b) + """) + + +class TraceCallbackTests(unittest.TestCase): + @contextlib.contextmanager + def check_stmt_trace(self, cx, expected): + try: + traced = [] + cx.set_trace_callback(lambda stmt: traced.append(stmt)) + yield + finally: + self.assertEqual(traced, expected) + cx.set_trace_callback(None) + + def test_trace_callback_used(self): + """ + Test that the trace callback is invoked once it is set. + """ + con = sqlite.connect(":memory:") + traced_statements = [] + def trace(statement): + traced_statements.append(statement) + con.set_trace_callback(trace) + con.execute("create table foo(a, b)") + self.assertTrue(traced_statements) + self.assertTrue(any("create table foo" in stmt for stmt in traced_statements)) + + def test_clear_trace_callback(self): + """ + Test that setting the trace callback to None clears the previously set callback. + """ + con = sqlite.connect(":memory:") + traced_statements = [] + def trace(statement): + traced_statements.append(statement) + con.set_trace_callback(trace) + con.set_trace_callback(None) + con.execute("create table foo(a, b)") + self.assertFalse(traced_statements, "trace callback was not cleared") + + def test_unicode_content(self): + """ + Test that the statement can contain unicode literals. + """ + unicode_value = '\xf6\xe4\xfc\xd6\xc4\xdc\xdf\u20ac' + con = sqlite.connect(":memory:") + traced_statements = [] + def trace(statement): + traced_statements.append(statement) + con.set_trace_callback(trace) + con.execute("create table foo(x)") + con.execute("insert into foo(x) values ('%s')" % unicode_value) + con.commit() + self.assertTrue(any(unicode_value in stmt for stmt in traced_statements), + "Unicode data %s garbled in trace callback: %s" + % (ascii(unicode_value), ', '.join(map(ascii, traced_statements)))) + + def test_trace_callback_content(self): + # set_trace_callback() shouldn't produce duplicate content (bpo-26187) + traced_statements = [] + def trace(statement): + traced_statements.append(statement) + + queries = ["create table foo(x)", + "insert into foo(x) values(1)"] + self.addCleanup(unlink, TESTFN) + con1 = sqlite.connect(TESTFN, isolation_level=None) + con2 = sqlite.connect(TESTFN) + try: + con1.set_trace_callback(trace) + cur = con1.cursor() + cur.execute(queries[0]) + con2.execute("create table bar(x)") + cur.execute(queries[1]) + finally: + con1.close() + con2.close() + self.assertEqual(traced_statements, queries) + + def test_trace_expanded_sql(self): + expected = [ + "create table t(t)", + "BEGIN ", + "insert into t values(0)", + "insert into t values(1)", + "insert into t values(2)", + "COMMIT", + ] + with memory_database() as cx, self.check_stmt_trace(cx, expected): + with cx: + cx.execute("create table t(t)") + cx.executemany("insert into t values(?)", ((v,) for v in range(3))) + + @with_tracebacks( + sqlite.DataError, + regex="Expanded SQL string exceeds the maximum string length" + ) + def test_trace_too_much_expanded_sql(self): + # If the expanded string is too large, we'll fall back to the + # unexpanded SQL statement (for SQLite 3.14.0 and newer). + # The resulting string length is limited by the runtime limit + # SQLITE_LIMIT_LENGTH. + template = "select 1 as a where a=" + category = sqlite.SQLITE_LIMIT_LENGTH + with memory_database() as cx, cx_limit(cx, category=category) as lim: + ok_param = "a" + bad_param = "a" * lim + + unexpanded_query = template + "?" + expected = [unexpanded_query] + if sqlite.sqlite_version_info < (3, 14, 0): + expected = [] + with self.check_stmt_trace(cx, expected): + cx.execute(unexpanded_query, (bad_param,)) + + expanded_query = f"{template}'{ok_param}'" + with self.check_stmt_trace(cx, [expanded_query]): + cx.execute(unexpanded_query, (ok_param,)) + + @with_tracebacks(ZeroDivisionError, regex="division by zero") + def test_trace_bad_handler(self): + with memory_database() as cx: + cx.set_trace_callback(lambda stmt: 5/0) + cx.execute("select 1") + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_regression.py b/Lib/test/test_sqlite3/test_regression.py new file mode 100644 index 000000000..dfcf3b11f --- /dev/null +++ b/Lib/test/test_sqlite3/test_regression.py @@ -0,0 +1,535 @@ +# pysqlite2/test/regression.py: pysqlite regression tests +# +# Copyright (C) 2006-2010 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import datetime +import unittest +import sqlite3 as sqlite +import weakref +import functools + +from test import support +from unittest.mock import patch +from test.test_sqlite3.test_dbapi import memory_database, cx_limit + + +class RegressionTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + def tearDown(self): + self.con.close() + + def test_pragma_user_version(self): + # This used to crash pysqlite because this pragma command returns NULL for the column name + cur = self.con.cursor() + cur.execute("pragma user_version") + + def test_pragma_schema_version(self): + # This still crashed pysqlite <= 2.2.1 + con = sqlite.connect(":memory:", detect_types=sqlite.PARSE_COLNAMES) + try: + cur = self.con.cursor() + cur.execute("pragma schema_version") + finally: + cur.close() + con.close() + + def test_statement_reset(self): + # pysqlite 2.1.0 to 2.2.0 have the problem that not all statements are + # reset before a rollback, but only those that are still in the + # statement cache. The others are not accessible from the connection object. + con = sqlite.connect(":memory:", cached_statements=5) + cursors = [con.cursor() for x in range(5)] + cursors[0].execute("create table test(x)") + for i in range(10): + cursors[0].executemany("insert into test(x) values (?)", [(x,) for x in range(10)]) + + for i in range(5): + cursors[i].execute(" " * i + "select x from test") + + con.rollback() + + def test_column_name_with_spaces(self): + cur = self.con.cursor() + cur.execute('select 1 as "foo bar [datetime]"') + self.assertEqual(cur.description[0][0], "foo bar [datetime]") + + cur.execute('select 1 as "foo baz"') + self.assertEqual(cur.description[0][0], "foo baz") + + def test_statement_finalization_on_close_db(self): + # pysqlite versions <= 2.3.3 only finalized statements in the statement + # cache when closing the database. statements that were still + # referenced in cursors weren't closed and could provoke " + # "OperationalError: Unable to close due to unfinalised statements". + con = sqlite.connect(":memory:") + cursors = [] + # default statement cache size is 100 + for i in range(105): + cur = con.cursor() + cursors.append(cur) + cur.execute("select 1 x union select " + str(i)) + con.close() + + def test_on_conflict_rollback(self): + con = sqlite.connect(":memory:") + con.execute("create table foo(x, unique(x) on conflict rollback)") + con.execute("insert into foo(x) values (1)") + try: + con.execute("insert into foo(x) values (1)") + except sqlite.DatabaseError: + pass + con.execute("insert into foo(x) values (2)") + try: + con.commit() + except sqlite.OperationalError: + self.fail("pysqlite knew nothing about the implicit ROLLBACK") + + def test_workaround_for_buggy_sqlite_transfer_bindings(self): + """ + pysqlite would crash with older SQLite versions unless + a workaround is implemented. + """ + self.con.execute("create table foo(bar)") + self.con.execute("drop table foo") + self.con.execute("create table foo(bar)") + + def test_empty_statement(self): + """ + pysqlite used to segfault with SQLite versions 3.5.x. These return NULL + for "no-operation" statements + """ + self.con.execute("") + + def test_type_map_usage(self): + """ + pysqlite until 2.4.1 did not rebuild the row_cast_map when recompiling + a statement. This test exhibits the problem. + """ + SELECT = "select * from foo" + con = sqlite.connect(":memory:",detect_types=sqlite.PARSE_DECLTYPES) + cur = con.cursor() + cur.execute("create table foo(bar timestamp)") + with self.assertWarnsRegex(DeprecationWarning, "adapter"): + cur.execute("insert into foo(bar) values (?)", (datetime.datetime.now(),)) + cur.execute(SELECT) + cur.execute("drop table foo") + cur.execute("create table foo(bar integer)") + cur.execute("insert into foo(bar) values (5)") + cur.execute(SELECT) + + def test_bind_mutating_list(self): + # Issue41662: Crash when mutate a list of parameters during iteration. + class X: + def __conform__(self, protocol): + parameters.clear() + return "..." + parameters = [X(), 0] + con = sqlite.connect(":memory:",detect_types=sqlite.PARSE_DECLTYPES) + con.execute("create table foo(bar X, baz integer)") + # Should not crash + with self.assertRaises(IndexError): + con.execute("insert into foo(bar, baz) values (?, ?)", parameters) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_error_msg_decode_error(self): + # When porting the module to Python 3.0, the error message about + # decoding errors disappeared. This verifies they're back again. + with self.assertRaises(sqlite.OperationalError) as cm: + self.con.execute("select 'xxx' || ? || 'yyy' colname", + (bytes(bytearray([250])),)).fetchone() + msg = "Could not decode to UTF-8 column 'colname' with text 'xxx" + self.assertIn(msg, str(cm.exception)) + + def test_register_adapter(self): + """ + See issue 3312. + """ + self.assertRaises(TypeError, sqlite.register_adapter, {}, None) + + def test_set_isolation_level(self): + # See issue 27881. + class CustomStr(str): + def upper(self): + return None + def __del__(self): + con.isolation_level = "" + + con = sqlite.connect(":memory:") + con.isolation_level = None + for level in "", "DEFERRED", "IMMEDIATE", "EXCLUSIVE": + with self.subTest(level=level): + con.isolation_level = level + con.isolation_level = level.lower() + con.isolation_level = level.capitalize() + con.isolation_level = CustomStr(level) + + # setting isolation_level failure should not alter previous state + con.isolation_level = None + con.isolation_level = "DEFERRED" + pairs = [ + (1, TypeError), (b'', TypeError), ("abc", ValueError), + ("IMMEDIATE\0EXCLUSIVE", ValueError), ("\xe9", ValueError), + ] + for value, exc in pairs: + with self.subTest(level=value): + with self.assertRaises(exc): + con.isolation_level = value + self.assertEqual(con.isolation_level, "DEFERRED") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_cursor_constructor_call_check(self): + """ + Verifies that cursor methods check whether base class __init__ was + called. + """ + class Cursor(sqlite.Cursor): + def __init__(self, con): + pass + + con = sqlite.connect(":memory:") + cur = Cursor(con) + with self.assertRaises(sqlite.ProgrammingError): + cur.execute("select 4+5").fetchall() + with self.assertRaisesRegex(sqlite.ProgrammingError, + r'^Base Cursor\.__init__ not called\.$'): + cur.close() + + def test_str_subclass(self): + """ + The Python 3.0 port of the module didn't cope with values of subclasses of str. + """ + class MyStr(str): pass + self.con.execute("select ?", (MyStr("abc"),)) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_connection_constructor_call_check(self): + """ + Verifies that connection methods check whether base class __init__ was + called. + """ + class Connection(sqlite.Connection): + def __init__(self, name): + pass + + con = Connection(":memory:") + with self.assertRaises(sqlite.ProgrammingError): + cur = con.cursor() + + def test_auto_commit(self): + """ + Verifies that creating a connection in autocommit mode works. + 2.5.3 introduced a regression so that these could no longer + be created. + """ + con = sqlite.connect(":memory:", isolation_level=None) + + def test_pragma_autocommit(self): + """ + Verifies that running a PRAGMA statement that does an autocommit does + work. This did not work in 2.5.3/2.5.4. + """ + cur = self.con.cursor() + cur.execute("create table foo(bar)") + cur.execute("insert into foo(bar) values (5)") + + cur.execute("pragma page_size") + row = cur.fetchone() + + def test_connection_call(self): + """ + Call a connection with a non-string SQL request: check error handling + of the statement constructor. + """ + self.assertRaises(TypeError, self.con, b"select 1") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_collation(self): + def collation_cb(a, b): + return 1 + self.assertRaises(UnicodeEncodeError, self.con.create_collation, + # Lone surrogate cannot be encoded to the default encoding (utf8) + "\uDC80", collation_cb) + + @unittest.skip("TODO: RUSTPYTHON deadlock") + def test_recursive_cursor_use(self): + """ + http://bugs.python.org/issue10811 + + Recursively using a cursor, such as when reusing it from a generator led to segfaults. + Now we catch recursive cursor usage and raise a ProgrammingError. + """ + con = sqlite.connect(":memory:") + + cur = con.cursor() + cur.execute("create table a (bar)") + cur.execute("create table b (baz)") + + def foo(): + cur.execute("insert into a (bar) values (?)", (1,)) + yield 1 + + with self.assertRaises(sqlite.ProgrammingError): + cur.executemany("insert into b (baz) values (?)", + ((i,) for i in foo())) + + def test_convert_timestamp_microsecond_padding(self): + """ + http://bugs.python.org/issue14720 + + The microsecond parsing of convert_timestamp() should pad with zeros, + since the microsecond string "456" actually represents "456000". + """ + + con = sqlite.connect(":memory:", detect_types=sqlite.PARSE_DECLTYPES) + cur = con.cursor() + cur.execute("CREATE TABLE t (x TIMESTAMP)") + + # Microseconds should be 456000 + cur.execute("INSERT INTO t (x) VALUES ('2012-04-04 15:06:00.456')") + + # Microseconds should be truncated to 123456 + cur.execute("INSERT INTO t (x) VALUES ('2012-04-04 15:06:00.123456789')") + + cur.execute("SELECT * FROM t") + with self.assertWarnsRegex(DeprecationWarning, "converter"): + values = [x[0] for x in cur.fetchall()] + + self.assertEqual(values, [ + datetime.datetime(2012, 4, 4, 15, 6, 0, 456000), + datetime.datetime(2012, 4, 4, 15, 6, 0, 123456), + ]) + + def test_invalid_isolation_level_type(self): + # isolation level is a string, not an integer + self.assertRaises(TypeError, + sqlite.connect, ":memory:", isolation_level=123) + + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_null_character(self): + # Issue #21147 + cur = self.con.cursor() + queries = ["\0select 1", "select 1\0"] + for query in queries: + with self.subTest(query=query): + self.assertRaisesRegex(sqlite.ProgrammingError, "null char", + self.con.execute, query) + with self.subTest(query=query): + self.assertRaisesRegex(sqlite.ProgrammingError, "null char", + cur.execute, query) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_surrogates(self): + con = sqlite.connect(":memory:") + self.assertRaises(UnicodeEncodeError, con, "select '\ud8ff'") + self.assertRaises(UnicodeEncodeError, con, "select '\udcff'") + cur = con.cursor() + self.assertRaises(UnicodeEncodeError, cur.execute, "select '\ud8ff'") + self.assertRaises(UnicodeEncodeError, cur.execute, "select '\udcff'") + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_large_sql(self): + msg = "query string is too large" + with memory_database() as cx, cx_limit(cx) as lim: + cu = cx.cursor() + + cx("select 1".ljust(lim)) + # use a different SQL statement; don't reuse from the LRU cache + cu.execute("select 2".ljust(lim)) + + sql = "select 3".ljust(lim+1) + self.assertRaisesRegex(sqlite.DataError, msg, cx, sql) + self.assertRaisesRegex(sqlite.DataError, msg, cu.execute, sql) + + def test_commit_cursor_reset(self): + """ + Connection.commit() did reset cursors, which made sqlite3 + to return rows multiple times when fetched from cursors + after commit. See issues 10513 and 23129 for details. + """ + con = sqlite.connect(":memory:") + con.executescript(""" + create table t(c); + create table t2(c); + insert into t values(0); + insert into t values(1); + insert into t values(2); + """) + + self.assertEqual(con.isolation_level, "") + + counter = 0 + for i, row in enumerate(con.execute("select c from t")): + with self.subTest(i=i, row=row): + con.execute("insert into t2(c) values (?)", (i,)) + con.commit() + if counter == 0: + self.assertEqual(row[0], 0) + elif counter == 1: + self.assertEqual(row[0], 1) + elif counter == 2: + self.assertEqual(row[0], 2) + counter += 1 + self.assertEqual(counter, 3, "should have returned exactly three rows") + + def test_bpo31770(self): + """ + The interpreter shouldn't crash in case Cursor.__init__() is called + more than once. + """ + def callback(*args): + pass + con = sqlite.connect(":memory:") + cur = sqlite.Cursor(con) + ref = weakref.ref(cur, callback) + cur.__init__(con) + del cur + # The interpreter shouldn't crash when ref is collected. + del ref + support.gc_collect() + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_del_isolation_level_segfault(self): + with self.assertRaises(AttributeError): + del self.con.isolation_level + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_bpo37347(self): + class Printer: + def log(self, *args): + return sqlite.SQLITE_OK + + for method in [self.con.set_trace_callback, + functools.partial(self.con.set_progress_handler, n=1), + self.con.set_authorizer]: + printer_instance = Printer() + method(printer_instance.log) + method(printer_instance.log) + self.con.execute("select 1") # trigger seg fault + method(None) + + def test_return_empty_bytestring(self): + cur = self.con.execute("select X''") + val = cur.fetchone()[0] + self.assertEqual(val, b'') + + def test_table_lock_cursor_replace_stmt(self): + with memory_database() as con: + cur = con.cursor() + cur.execute("create table t(t)") + cur.executemany("insert into t values(?)", + ((v,) for v in range(5))) + con.commit() + cur.execute("select t from t") + cur.execute("drop table t") + con.commit() + + def test_table_lock_cursor_dealloc(self): + with memory_database() as con: + con.execute("create table t(t)") + con.executemany("insert into t values(?)", + ((v,) for v in range(5))) + con.commit() + cur = con.execute("select t from t") + del cur + con.execute("drop table t") + con.commit() + + @unittest.skip("TODO: RUSTPYTHON deadlock") + def test_table_lock_cursor_non_readonly_select(self): + with memory_database() as con: + con.execute("create table t(t)") + con.executemany("insert into t values(?)", + ((v,) for v in range(5))) + con.commit() + def dup(v): + con.execute("insert into t values(?)", (v,)) + return + con.create_function("dup", 1, dup) + cur = con.execute("select dup(t) from t") + del cur + con.execute("drop table t") + con.commit() + + def test_executescript_step_through_select(self): + with memory_database() as con: + values = [(v,) for v in range(5)] + with con: + con.execute("create table t(t)") + con.executemany("insert into t values(?)", values) + steps = [] + con.create_function("step", 1, lambda x: steps.append((x,))) + con.executescript("select step(t) from t") + self.assertEqual(steps, values) + + +@unittest.skip("TODO: RUSTPYTHON deadlock") +class RecursiveUseOfCursors(unittest.TestCase): + # GH-80254: sqlite3 should not segfault for recursive use of cursors. + msg = "Recursive use of cursors not allowed" + + def setUp(self): + self.con = sqlite.connect(":memory:", + detect_types=sqlite.PARSE_COLNAMES) + self.cur = self.con.cursor() + self.cur.execute("create table test(x foo)") + self.cur.executemany("insert into test(x) values (?)", + [("foo",), ("bar",)]) + + def tearDown(self): + self.cur.close() + self.con.close() + + def test_recursive_cursor_init(self): + conv = lambda x: self.cur.__init__(self.con) + with patch.dict(sqlite.converters, {"INIT": conv}): + self.cur.execute(f'select x as "x [INIT]", x from test') + self.assertRaisesRegex(sqlite.ProgrammingError, self.msg, + self.cur.fetchall) + + def test_recursive_cursor_close(self): + conv = lambda x: self.cur.close() + with patch.dict(sqlite.converters, {"CLOSE": conv}): + self.cur.execute(f'select x as "x [CLOSE]", x from test') + self.assertRaisesRegex(sqlite.ProgrammingError, self.msg, + self.cur.fetchall) + + def test_recursive_cursor_iter(self): + conv = lambda x, l=[]: self.cur.fetchone() if l else l.append(None) + with patch.dict(sqlite.converters, {"ITER": conv}): + self.cur.execute(f'select x as "x [ITER]", x from test') + self.assertRaisesRegex(sqlite.ProgrammingError, self.msg, + self.cur.fetchall) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_transactions.py b/Lib/test/test_sqlite3/test_transactions.py new file mode 100644 index 000000000..8835ef76b --- /dev/null +++ b/Lib/test/test_sqlite3/test_transactions.py @@ -0,0 +1,372 @@ +# pysqlite2/test/transactions.py: tests transactions +# +# Copyright (C) 2005-2007 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import unittest +import sqlite3 as sqlite + +from test.support import LOOPBACK_TIMEOUT +from test.support.os_helper import TESTFN, unlink + +from test.test_sqlite3.test_dbapi import memory_database + + +TIMEOUT = LOOPBACK_TIMEOUT / 10 + + +class TransactionTests(unittest.TestCase): + def setUp(self): + self.con1 = sqlite.connect(TESTFN, timeout=TIMEOUT) + self.cur1 = self.con1.cursor() + + self.con2 = sqlite.connect(TESTFN, timeout=TIMEOUT) + self.cur2 = self.con2.cursor() + + def tearDown(self): + try: + self.cur1.close() + self.con1.close() + + self.cur2.close() + self.con2.close() + + finally: + unlink(TESTFN) + + def test_dml_does_not_auto_commit_before(self): + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + self.cur1.execute("create table test2(j)") + self.cur2.execute("select i from test") + res = self.cur2.fetchall() + self.assertEqual(len(res), 0) + + def test_insert_starts_transaction(self): + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + self.cur2.execute("select i from test") + res = self.cur2.fetchall() + self.assertEqual(len(res), 0) + + def test_update_starts_transaction(self): + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + self.con1.commit() + self.cur1.execute("update test set i=6") + self.cur2.execute("select i from test") + res = self.cur2.fetchone()[0] + self.assertEqual(res, 5) + + def test_delete_starts_transaction(self): + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + self.con1.commit() + self.cur1.execute("delete from test") + self.cur2.execute("select i from test") + res = self.cur2.fetchall() + self.assertEqual(len(res), 1) + + def test_replace_starts_transaction(self): + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + self.con1.commit() + self.cur1.execute("replace into test(i) values (6)") + self.cur2.execute("select i from test") + res = self.cur2.fetchall() + self.assertEqual(len(res), 1) + self.assertEqual(res[0][0], 5) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_toggle_auto_commit(self): + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + self.con1.isolation_level = None + self.assertEqual(self.con1.isolation_level, None) + self.cur2.execute("select i from test") + res = self.cur2.fetchall() + self.assertEqual(len(res), 1) + + self.con1.isolation_level = "DEFERRED" + self.assertEqual(self.con1.isolation_level , "DEFERRED") + self.cur1.execute("insert into test(i) values (5)") + self.cur2.execute("select i from test") + res = self.cur2.fetchall() + self.assertEqual(len(res), 1) + + def test_raise_timeout(self): + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + with self.assertRaises(sqlite.OperationalError): + self.cur2.execute("insert into test(i) values (5)") + + def test_locking(self): + """ + This tests the improved concurrency with pysqlite 2.3.4. You needed + to roll back con2 before you could commit con1. + """ + self.cur1.execute("create table test(i)") + self.cur1.execute("insert into test(i) values (5)") + with self.assertRaises(sqlite.OperationalError): + self.cur2.execute("insert into test(i) values (5)") + # NO self.con2.rollback() HERE!!! + self.con1.commit() + + def test_rollback_cursor_consistency(self): + """Check that cursors behave correctly after rollback.""" + con = sqlite.connect(":memory:") + cur = con.cursor() + cur.execute("create table test(x)") + cur.execute("insert into test(x) values (5)") + cur.execute("select 1 union select 2 union select 3") + + con.rollback() + self.assertEqual(cur.fetchall(), [(1,), (2,), (3,)]) + + def test_multiple_cursors_and_iternext(self): + # gh-94028: statements are cleared and reset in cursor iternext. + + # Provoke the gh-94028 by using a cursor cache. + CURSORS = {} + def sql(cx, sql, *args): + cu = cx.cursor() + cu.execute(sql, args) + CURSORS[id(sql)] = cu + return cu + + self.con1.execute("create table t(t)") + sql(self.con1, "insert into t values (?), (?), (?)", "u1", "u2", "u3") + self.con1.commit() + + # On second connection, verify rows are visible, then delete them. + count = sql(self.con2, "select count(*) from t").fetchone()[0] + self.assertEqual(count, 3) + changes = sql(self.con2, "delete from t").rowcount + self.assertEqual(changes, 3) + self.con2.commit() + + # Back in original connection, create 2 new users. + sql(self.con1, "insert into t values (?)", "u4") + sql(self.con1, "insert into t values (?)", "u5") + + # The second connection cannot see uncommitted changes. + count = sql(self.con2, "select count(*) from t").fetchone()[0] + self.assertEqual(count, 0) + + # First connection can see its own changes. + count = sql(self.con1, "select count(*) from t").fetchone()[0] + self.assertEqual(count, 2) + + # The second connection can now see the changes. + self.con1.commit() + count = sql(self.con2, "select count(*) from t").fetchone()[0] + self.assertEqual(count, 2) + + +class RollbackTests(unittest.TestCase): + """bpo-44092: sqlite3 now leaves it to SQLite to resolve rollback issues""" + + def setUp(self): + self.con = sqlite.connect(":memory:") + self.cur1 = self.con.cursor() + self.cur2 = self.con.cursor() + with self.con: + self.con.execute("create table t(c)"); + self.con.executemany("insert into t values(?)", [(0,), (1,), (2,)]) + self.cur1.execute("begin transaction") + select = "select c from t" + self.cur1.execute(select) + self.con.rollback() + self.res = self.cur2.execute(select) # Reusing stmt from cache + + def tearDown(self): + self.con.close() + + def _check_rows(self): + for i, row in enumerate(self.res): + self.assertEqual(row[0], i) + + def test_no_duplicate_rows_after_rollback_del_cursor(self): + del self.cur1 + self._check_rows() + + def test_no_duplicate_rows_after_rollback_close_cursor(self): + self.cur1.close() + self._check_rows() + + def test_no_duplicate_rows_after_rollback_new_query(self): + self.cur1.execute("select c from t where c = 1") + self._check_rows() + + + +class SpecialCommandTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + self.cur = self.con.cursor() + + def test_drop_table(self): + self.cur.execute("create table test(i)") + self.cur.execute("insert into test(i) values (5)") + self.cur.execute("drop table test") + + def test_pragma(self): + self.cur.execute("create table test(i)") + self.cur.execute("insert into test(i) values (5)") + self.cur.execute("pragma count_changes=1") + + def tearDown(self): + self.cur.close() + self.con.close() + + +class TransactionalDDL(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + def test_ddl_does_not_autostart_transaction(self): + # For backwards compatibility reasons, DDL statements should not + # implicitly start a transaction. + self.con.execute("create table test(i)") + self.con.rollback() + result = self.con.execute("select * from test").fetchall() + self.assertEqual(result, []) + + def test_immediate_transactional_ddl(self): + # You can achieve transactional DDL by issuing a BEGIN + # statement manually. + self.con.execute("begin immediate") + self.con.execute("create table test(i)") + self.con.rollback() + with self.assertRaises(sqlite.OperationalError): + self.con.execute("select * from test") + + def test_transactional_ddl(self): + # You can achieve transactional DDL by issuing a BEGIN + # statement manually. + self.con.execute("begin") + self.con.execute("create table test(i)") + self.con.rollback() + with self.assertRaises(sqlite.OperationalError): + self.con.execute("select * from test") + + def tearDown(self): + self.con.close() + + +class IsolationLevelFromInit(unittest.TestCase): + CREATE = "create table t(t)" + INSERT = "insert into t values(1)" + + def setUp(self): + self.traced = [] + + def _run_test(self, cx): + cx.execute(self.CREATE) + cx.set_trace_callback(lambda stmt: self.traced.append(stmt)) + with cx: + cx.execute(self.INSERT) + + def test_isolation_level_default(self): + with memory_database() as cx: + self._run_test(cx) + self.assertEqual(self.traced, ["BEGIN ", self.INSERT, "COMMIT"]) + + def test_isolation_level_begin(self): + with memory_database(isolation_level="") as cx: + self._run_test(cx) + self.assertEqual(self.traced, ["BEGIN ", self.INSERT, "COMMIT"]) + + def test_isolation_level_deferred(self): + with memory_database(isolation_level="DEFERRED") as cx: + self._run_test(cx) + self.assertEqual(self.traced, ["BEGIN DEFERRED", self.INSERT, "COMMIT"]) + + def test_isolation_level_immediate(self): + with memory_database(isolation_level="IMMEDIATE") as cx: + self._run_test(cx) + self.assertEqual(self.traced, + ["BEGIN IMMEDIATE", self.INSERT, "COMMIT"]) + + def test_isolation_level_exclusive(self): + with memory_database(isolation_level="EXCLUSIVE") as cx: + self._run_test(cx) + self.assertEqual(self.traced, + ["BEGIN EXCLUSIVE", self.INSERT, "COMMIT"]) + + def test_isolation_level_none(self): + with memory_database(isolation_level=None) as cx: + self._run_test(cx) + self.assertEqual(self.traced, [self.INSERT]) + + +class IsolationLevelPostInit(unittest.TestCase): + QUERY = "insert into t values(1)" + + def setUp(self): + self.cx = sqlite.connect(":memory:") + self.cx.execute("create table t(t)") + self.traced = [] + self.cx.set_trace_callback(lambda stmt: self.traced.append(stmt)) + + def tearDown(self): + self.cx.close() + + def test_isolation_level_default(self): + with self.cx: + self.cx.execute(self.QUERY) + self.assertEqual(self.traced, ["BEGIN ", self.QUERY, "COMMIT"]) + + def test_isolation_level_begin(self): + self.cx.isolation_level = "" + with self.cx: + self.cx.execute(self.QUERY) + self.assertEqual(self.traced, ["BEGIN ", self.QUERY, "COMMIT"]) + + def test_isolation_level_deferrred(self): + self.cx.isolation_level = "DEFERRED" + with self.cx: + self.cx.execute(self.QUERY) + self.assertEqual(self.traced, ["BEGIN DEFERRED", self.QUERY, "COMMIT"]) + + def test_isolation_level_immediate(self): + self.cx.isolation_level = "IMMEDIATE" + with self.cx: + self.cx.execute(self.QUERY) + self.assertEqual(self.traced, + ["BEGIN IMMEDIATE", self.QUERY, "COMMIT"]) + + def test_isolation_level_exclusive(self): + self.cx.isolation_level = "EXCLUSIVE" + with self.cx: + self.cx.execute(self.QUERY) + self.assertEqual(self.traced, + ["BEGIN EXCLUSIVE", self.QUERY, "COMMIT"]) + + def test_isolation_level_none(self): + self.cx.isolation_level = None + with self.cx: + self.cx.execute(self.QUERY) + self.assertEqual(self.traced, [self.QUERY]) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_types.py b/Lib/test/test_sqlite3/test_types.py new file mode 100644 index 000000000..d7631ec93 --- /dev/null +++ b/Lib/test/test_sqlite3/test_types.py @@ -0,0 +1,558 @@ +# pysqlite2/test/types.py: tests for type conversion and detection +# +# Copyright (C) 2005 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import datetime +import unittest +import sqlite3 as sqlite +import sys +try: + import zlib +except ImportError: + zlib = None + +from test import support + + +class SqliteTypeTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + self.cur = self.con.cursor() + self.cur.execute("create table test(i integer, s varchar, f number, b blob)") + + def tearDown(self): + self.cur.close() + self.con.close() + + def test_string(self): + self.cur.execute("insert into test(s) values (?)", ("Österreich",)) + self.cur.execute("select s from test") + row = self.cur.fetchone() + self.assertEqual(row[0], "Österreich") + + def test_string_with_null_character(self): + self.cur.execute("insert into test(s) values (?)", ("a\0b",)) + self.cur.execute("select s from test") + row = self.cur.fetchone() + self.assertEqual(row[0], "a\0b") + + def test_small_int(self): + self.cur.execute("insert into test(i) values (?)", (42,)) + self.cur.execute("select i from test") + row = self.cur.fetchone() + self.assertEqual(row[0], 42) + + def test_large_int(self): + num = 123456789123456789 + self.cur.execute("insert into test(i) values (?)", (num,)) + self.cur.execute("select i from test") + row = self.cur.fetchone() + self.assertEqual(row[0], num) + + def test_float(self): + val = 3.14 + self.cur.execute("insert into test(f) values (?)", (val,)) + self.cur.execute("select f from test") + row = self.cur.fetchone() + self.assertEqual(row[0], val) + + def test_blob(self): + sample = b"Guglhupf" + val = memoryview(sample) + self.cur.execute("insert into test(b) values (?)", (val,)) + self.cur.execute("select b from test") + row = self.cur.fetchone() + self.assertEqual(row[0], sample) + + def test_unicode_execute(self): + self.cur.execute("select 'Österreich'") + row = self.cur.fetchone() + self.assertEqual(row[0], "Österreich") + + def test_too_large_int(self): + for value in 2**63, -2**63-1, 2**64: + with self.assertRaises(OverflowError): + self.cur.execute("insert into test(i) values (?)", (value,)) + self.cur.execute("select i from test") + row = self.cur.fetchone() + self.assertIsNone(row) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_string_with_surrogates(self): + for value in 0xd8ff, 0xdcff: + with self.assertRaises(UnicodeEncodeError): + self.cur.execute("insert into test(s) values (?)", (chr(value),)) + self.cur.execute("select s from test") + row = self.cur.fetchone() + self.assertIsNone(row) + + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @support.bigmemtest(size=2**31, memuse=4, dry_run=False) + def test_too_large_string(self, maxsize): + with self.assertRaises(sqlite.InterfaceError): + self.cur.execute("insert into test(s) values (?)", ('x'*(2**31-1),)) + with self.assertRaises(OverflowError): + self.cur.execute("insert into test(s) values (?)", ('x'*(2**31),)) + self.cur.execute("select 1 from test") + row = self.cur.fetchone() + self.assertIsNone(row) + + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @support.bigmemtest(size=2**31, memuse=3, dry_run=False) + def test_too_large_blob(self, maxsize): + with self.assertRaises(sqlite.InterfaceError): + self.cur.execute("insert into test(s) values (?)", (b'x'*(2**31-1),)) + with self.assertRaises(OverflowError): + self.cur.execute("insert into test(s) values (?)", (b'x'*(2**31),)) + self.cur.execute("select 1 from test") + row = self.cur.fetchone() + self.assertIsNone(row) + + +class DeclTypesTests(unittest.TestCase): + class Foo: + def __init__(self, _val): + if isinstance(_val, bytes): + # sqlite3 always calls __init__ with a bytes created from a + # UTF-8 string when __conform__ was used to store the object. + _val = _val.decode('utf-8') + self.val = _val + + def __eq__(self, other): + if not isinstance(other, DeclTypesTests.Foo): + return NotImplemented + return self.val == other.val + + def __conform__(self, protocol): + if protocol is sqlite.PrepareProtocol: + return self.val + else: + return None + + def __str__(self): + return "<%s>" % self.val + + class BadConform: + def __init__(self, exc): + self.exc = exc + def __conform__(self, protocol): + raise self.exc + + def setUp(self): + self.con = sqlite.connect(":memory:", detect_types=sqlite.PARSE_DECLTYPES) + self.cur = self.con.cursor() + self.cur.execute(""" + create table test( + i int, + s str, + f float, + b bool, + u unicode, + foo foo, + bin blob, + n1 number, + n2 number(5), + bad bad, + cbin cblob) + """) + + # override float, make them always return the same number + sqlite.converters["FLOAT"] = lambda x: 47.2 + + # and implement two custom ones + sqlite.converters["BOOL"] = lambda x: bool(int(x)) + sqlite.converters["FOO"] = DeclTypesTests.Foo + sqlite.converters["BAD"] = DeclTypesTests.BadConform + sqlite.converters["WRONG"] = lambda x: "WRONG" + sqlite.converters["NUMBER"] = float + sqlite.converters["CBLOB"] = lambda x: b"blobish" + + def tearDown(self): + del sqlite.converters["FLOAT"] + del sqlite.converters["BOOL"] + del sqlite.converters["FOO"] + del sqlite.converters["BAD"] + del sqlite.converters["WRONG"] + del sqlite.converters["NUMBER"] + del sqlite.converters["CBLOB"] + self.cur.close() + self.con.close() + + def test_string(self): + # default + self.cur.execute("insert into test(s) values (?)", ("foo",)) + self.cur.execute('select s as "s [WRONG]" from test') + row = self.cur.fetchone() + self.assertEqual(row[0], "foo") + + def test_small_int(self): + # default + self.cur.execute("insert into test(i) values (?)", (42,)) + self.cur.execute("select i from test") + row = self.cur.fetchone() + self.assertEqual(row[0], 42) + + def test_large_int(self): + # default + num = 123456789123456789 + self.cur.execute("insert into test(i) values (?)", (num,)) + self.cur.execute("select i from test") + row = self.cur.fetchone() + self.assertEqual(row[0], num) + + def test_float(self): + # custom + val = 3.14 + self.cur.execute("insert into test(f) values (?)", (val,)) + self.cur.execute("select f from test") + row = self.cur.fetchone() + self.assertEqual(row[0], 47.2) + + def test_bool(self): + # custom + self.cur.execute("insert into test(b) values (?)", (False,)) + self.cur.execute("select b from test") + row = self.cur.fetchone() + self.assertIs(row[0], False) + + self.cur.execute("delete from test") + self.cur.execute("insert into test(b) values (?)", (True,)) + self.cur.execute("select b from test") + row = self.cur.fetchone() + self.assertIs(row[0], True) + + def test_unicode(self): + # default + val = "\xd6sterreich" + self.cur.execute("insert into test(u) values (?)", (val,)) + self.cur.execute("select u from test") + row = self.cur.fetchone() + self.assertEqual(row[0], val) + + def test_foo(self): + val = DeclTypesTests.Foo("bla") + self.cur.execute("insert into test(foo) values (?)", (val,)) + self.cur.execute("select foo from test") + row = self.cur.fetchone() + self.assertEqual(row[0], val) + + def test_error_in_conform(self): + val = DeclTypesTests.BadConform(TypeError) + with self.assertRaises(sqlite.ProgrammingError): + self.cur.execute("insert into test(bad) values (?)", (val,)) + with self.assertRaises(sqlite.ProgrammingError): + self.cur.execute("insert into test(bad) values (:val)", {"val": val}) + + val = DeclTypesTests.BadConform(KeyboardInterrupt) + with self.assertRaises(KeyboardInterrupt): + self.cur.execute("insert into test(bad) values (?)", (val,)) + with self.assertRaises(KeyboardInterrupt): + self.cur.execute("insert into test(bad) values (:val)", {"val": val}) + + def test_unsupported_seq(self): + class Bar: pass + val = Bar() + with self.assertRaises(sqlite.ProgrammingError): + self.cur.execute("insert into test(f) values (?)", (val,)) + + def test_unsupported_dict(self): + class Bar: pass + val = Bar() + with self.assertRaises(sqlite.ProgrammingError): + self.cur.execute("insert into test(f) values (:val)", {"val": val}) + + def test_blob(self): + # default + sample = b"Guglhupf" + val = memoryview(sample) + self.cur.execute("insert into test(bin) values (?)", (val,)) + self.cur.execute("select bin from test") + row = self.cur.fetchone() + self.assertEqual(row[0], sample) + + def test_number1(self): + self.cur.execute("insert into test(n1) values (5)") + value = self.cur.execute("select n1 from test").fetchone()[0] + # if the converter is not used, it's an int instead of a float + self.assertEqual(type(value), float) + + def test_number2(self): + """Checks whether converter names are cut off at '(' characters""" + self.cur.execute("insert into test(n2) values (5)") + value = self.cur.execute("select n2 from test").fetchone()[0] + # if the converter is not used, it's an int instead of a float + self.assertEqual(type(value), float) + + def test_convert_zero_sized_blob(self): + self.con.execute("insert into test(cbin) values (?)", (b"",)) + cur = self.con.execute("select cbin from test") + # Zero-sized blobs with converters returns None. This differs from + # blobs without a converter, where b"" is returned. + self.assertIsNone(cur.fetchone()[0]) + + +class ColNamesTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:", detect_types=sqlite.PARSE_COLNAMES) + self.cur = self.con.cursor() + self.cur.execute("create table test(x foo)") + + sqlite.converters["FOO"] = lambda x: "[%s]" % x.decode("ascii") + sqlite.converters["BAR"] = lambda x: "<%s>" % x.decode("ascii") + sqlite.converters["EXC"] = lambda x: 5/0 + sqlite.converters["B1B1"] = lambda x: "MARKER" + + def tearDown(self): + del sqlite.converters["FOO"] + del sqlite.converters["BAR"] + del sqlite.converters["EXC"] + del sqlite.converters["B1B1"] + self.cur.close() + self.con.close() + + def test_decl_type_not_used(self): + """ + Assures that the declared type is not used when PARSE_DECLTYPES + is not set. + """ + self.cur.execute("insert into test(x) values (?)", ("xxx",)) + self.cur.execute("select x from test") + val = self.cur.fetchone()[0] + self.assertEqual(val, "xxx") + + def test_none(self): + self.cur.execute("insert into test(x) values (?)", (None,)) + self.cur.execute("select x from test") + val = self.cur.fetchone()[0] + self.assertEqual(val, None) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_col_name(self): + self.cur.execute("insert into test(x) values (?)", ("xxx",)) + self.cur.execute('select x as "x y [bar]" from test') + val = self.cur.fetchone()[0] + self.assertEqual(val, "") + + # Check if the stripping of colnames works. Everything after the first + # '[' (and the preceding space) should be stripped. + self.assertEqual(self.cur.description[0][0], "x y") + + def test_case_in_converter_name(self): + self.cur.execute("select 'other' as \"x [b1b1]\"") + val = self.cur.fetchone()[0] + self.assertEqual(val, "MARKER") + + def test_cursor_description_no_row(self): + """ + cursor.description should at least provide the column name(s), even if + no row returned. + """ + self.cur.execute("select * from test where 0 = 1") + self.assertEqual(self.cur.description[0][0], "x") + + def test_cursor_description_insert(self): + self.cur.execute("insert into test values (1)") + self.assertIsNone(self.cur.description) + + +@unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "CTEs not supported") +class CommonTableExpressionTests(unittest.TestCase): + + def setUp(self): + self.con = sqlite.connect(":memory:") + self.cur = self.con.cursor() + self.cur.execute("create table test(x foo)") + + def tearDown(self): + self.cur.close() + self.con.close() + + def test_cursor_description_cte_simple(self): + self.cur.execute("with one as (select 1) select * from one") + self.assertIsNotNone(self.cur.description) + self.assertEqual(self.cur.description[0][0], "1") + + def test_cursor_description_cte_multiple_columns(self): + self.cur.execute("insert into test values(1)") + self.cur.execute("insert into test values(2)") + self.cur.execute("with testCTE as (select * from test) select * from testCTE") + self.assertIsNotNone(self.cur.description) + self.assertEqual(self.cur.description[0][0], "x") + + def test_cursor_description_cte(self): + self.cur.execute("insert into test values (1)") + self.cur.execute("with bar as (select * from test) select * from test where x = 1") + self.assertIsNotNone(self.cur.description) + self.assertEqual(self.cur.description[0][0], "x") + self.cur.execute("with bar as (select * from test) select * from test where x = 2") + self.assertIsNotNone(self.cur.description) + self.assertEqual(self.cur.description[0][0], "x") + + +class ObjectAdaptationTests(unittest.TestCase): + def cast(obj): + return float(obj) + cast = staticmethod(cast) + + def setUp(self): + self.con = sqlite.connect(":memory:") + try: + del sqlite.adapters[int] + except: + pass + sqlite.register_adapter(int, ObjectAdaptationTests.cast) + self.cur = self.con.cursor() + + def tearDown(self): + del sqlite.adapters[(int, sqlite.PrepareProtocol)] + self.cur.close() + self.con.close() + + def test_caster_is_used(self): + self.cur.execute("select ?", (4,)) + val = self.cur.fetchone()[0] + self.assertEqual(type(val), float) + + def test_missing_adapter(self): + with self.assertRaises(sqlite.ProgrammingError): + sqlite.adapt(1.) # No float adapter registered + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_missing_protocol(self): + with self.assertRaises(sqlite.ProgrammingError): + sqlite.adapt(1, None) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_defect_proto(self): + class DefectProto(): + def __adapt__(self): + return None + with self.assertRaises(sqlite.ProgrammingError): + sqlite.adapt(1., DefectProto) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_defect_self_adapt(self): + class DefectSelfAdapt(float): + def __conform__(self, _): + return None + with self.assertRaises(sqlite.ProgrammingError): + sqlite.adapt(DefectSelfAdapt(1.)) + + def test_custom_proto(self): + class CustomProto(): + def __adapt__(self): + return "adapted" + self.assertEqual(sqlite.adapt(1., CustomProto), "adapted") + + def test_adapt(self): + val = 42 + self.assertEqual(float(val), sqlite.adapt(val)) + + def test_adapt_alt(self): + alt = "other" + self.assertEqual(alt, sqlite.adapt(1., None, alt)) + + +@unittest.skipUnless(zlib, "requires zlib") +class BinaryConverterTests(unittest.TestCase): + def convert(s): + return zlib.decompress(s) + convert = staticmethod(convert) + + def setUp(self): + self.con = sqlite.connect(":memory:", detect_types=sqlite.PARSE_COLNAMES) + sqlite.register_converter("bin", BinaryConverterTests.convert) + + def tearDown(self): + self.con.close() + + def test_binary_input_for_converter(self): + testdata = b"abcdefg" * 10 + result = self.con.execute('select ? as "x [bin]"', (memoryview(zlib.compress(testdata)),)).fetchone()[0] + self.assertEqual(testdata, result) + +class DateTimeTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:", detect_types=sqlite.PARSE_DECLTYPES) + self.cur = self.con.cursor() + self.cur.execute("create table test(d date, ts timestamp)") + + def tearDown(self): + self.cur.close() + self.con.close() + + def test_sqlite_date(self): + d = sqlite.Date(2004, 2, 14) + with self.assertWarnsRegex(DeprecationWarning, "adapter") as cm: + self.cur.execute("insert into test(d) values (?)", (d,)) + self.assertEqual(cm.filename, __file__) + self.cur.execute("select d from test") + with self.assertWarnsRegex(DeprecationWarning, "converter") as cm: + d2 = self.cur.fetchone()[0] + self.assertEqual(cm.filename, __file__) + self.assertEqual(d, d2) + + def test_sqlite_timestamp(self): + ts = sqlite.Timestamp(2004, 2, 14, 7, 15, 0) + with self.assertWarnsRegex(DeprecationWarning, "adapter") as cm: + self.cur.execute("insert into test(ts) values (?)", (ts,)) + self.assertEqual(cm.filename, __file__) + self.cur.execute("select ts from test") + with self.assertWarnsRegex(DeprecationWarning, "converter") as cm: + ts2 = self.cur.fetchone()[0] + self.assertEqual(cm.filename, __file__) + self.assertEqual(ts, ts2) + + def test_sql_timestamp(self): + now = datetime.datetime.utcnow() + self.cur.execute("insert into test(ts) values (current_timestamp)") + self.cur.execute("select ts from test") + with self.assertWarnsRegex(DeprecationWarning, "converter"): + ts = self.cur.fetchone()[0] + self.assertEqual(type(ts), datetime.datetime) + self.assertEqual(ts.year, now.year) + + def test_date_time_sub_seconds(self): + ts = sqlite.Timestamp(2004, 2, 14, 7, 15, 0, 500000) + with self.assertWarnsRegex(DeprecationWarning, "adapter"): + self.cur.execute("insert into test(ts) values (?)", (ts,)) + self.cur.execute("select ts from test") + with self.assertWarnsRegex(DeprecationWarning, "converter"): + ts2 = self.cur.fetchone()[0] + self.assertEqual(ts, ts2) + + def test_date_time_sub_seconds_floating_point(self): + ts = sqlite.Timestamp(2004, 2, 14, 7, 15, 0, 510241) + with self.assertWarnsRegex(DeprecationWarning, "adapter"): + self.cur.execute("insert into test(ts) values (?)", (ts,)) + self.cur.execute("select ts from test") + with self.assertWarnsRegex(DeprecationWarning, "converter"): + ts2 = self.cur.fetchone()[0] + self.assertEqual(ts, ts2) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py new file mode 100644 index 000000000..d2925feb0 --- /dev/null +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -0,0 +1,861 @@ +# pysqlite2/test/userfunctions.py: tests for user-defined functions and +# aggregates. +# +# Copyright (C) 2005-2007 Gerhard Häring +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import contextlib +import functools +import io +import re +import sys +import unittest +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 + + +def with_tracebacks(exc, regex="", name=""): + """Convenience decorator for testing callback tracebacks.""" + def decorator(func): + _regex = re.compile(regex) if regex else None + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + with catch_unraisable_exception() as cm: + # First, run the test with traceback enabled. + with check_tracebacks(self, cm, exc, _regex, name): + func(self, *args, **kwargs) + + # Then run the test with traceback disabled. + func(self, *args, **kwargs) + return wrapper + return decorator + + +@contextlib.contextmanager +def check_tracebacks(self, cm, exc, regex, obj_name): + """Convenience context manager for testing callback tracebacks.""" + sqlite.enable_callback_tracebacks(True) + try: + buf = io.StringIO() + with contextlib.redirect_stderr(buf): + yield + + # TODO: RUSTPYTHON need unraisable exception + # self.assertEqual(cm.unraisable.exc_type, exc) + # if regex: + # msg = str(cm.unraisable.exc_value) + # self.assertIsNotNone(regex.search(msg)) + # if obj_name: + # self.assertEqual(cm.unraisable.object.__name__, obj_name) + finally: + sqlite.enable_callback_tracebacks(False) + + +def func_returntext(): + return "foo" +def func_returntextwithnull(): + return "1\x002" +def func_returnunicode(): + return "bar" +def func_returnint(): + return 42 +def func_returnfloat(): + return 3.14 +def func_returnnull(): + return None +def func_returnblob(): + return b"blob" +def func_returnlonglong(): + return 1<<31 +def func_raiseexception(): + 5/0 +def func_memoryerror(): + raise MemoryError +def func_overflowerror(): + raise OverflowError + +class AggrNoStep: + def __init__(self): + pass + + def finalize(self): + return 1 + +class AggrNoFinalize: + def __init__(self): + pass + + def step(self, x): + pass + +class AggrExceptionInInit: + def __init__(self): + 5/0 + + def step(self, x): + pass + + def finalize(self): + pass + +class AggrExceptionInStep: + def __init__(self): + pass + + def step(self, x): + 5/0 + + def finalize(self): + return 42 + +class AggrExceptionInFinalize: + def __init__(self): + pass + + def step(self, x): + pass + + def finalize(self): + 5/0 + +class AggrCheckType: + def __init__(self): + self.val = None + + def step(self, whichType, val): + theType = {"str": str, "int": int, "float": float, "None": type(None), + "blob": bytes} + self.val = int(theType[whichType] is type(val)) + + def finalize(self): + return self.val + +class AggrCheckTypes: + def __init__(self): + self.val = 0 + + def step(self, whichType, *vals): + theType = {"str": str, "int": int, "float": float, "None": type(None), + "blob": bytes} + for val in vals: + self.val += int(theType[whichType] is type(val)) + + def finalize(self): + return self.val + +class AggrSum: + def __init__(self): + self.val = 0.0 + + def step(self, val): + self.val += val + + def finalize(self): + return self.val + +class AggrText: + def __init__(self): + self.txt = "" + def step(self, txt): + self.txt = self.txt + txt + def finalize(self): + return self.txt + + +class FunctionTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + self.con.create_function("returntext", 0, func_returntext) + self.con.create_function("returntextwithnull", 0, func_returntextwithnull) + self.con.create_function("returnunicode", 0, func_returnunicode) + self.con.create_function("returnint", 0, func_returnint) + self.con.create_function("returnfloat", 0, func_returnfloat) + self.con.create_function("returnnull", 0, func_returnnull) + self.con.create_function("returnblob", 0, func_returnblob) + self.con.create_function("returnlonglong", 0, func_returnlonglong) + self.con.create_function("returnnan", 0, lambda: float("nan")) + self.con.create_function("returntoolargeint", 0, lambda: 1 << 65) + self.con.create_function("return_noncont_blob", 0, + lambda: memoryview(b"blob")[::2]) + self.con.create_function("raiseexception", 0, func_raiseexception) + self.con.create_function("memoryerror", 0, func_memoryerror) + self.con.create_function("overflowerror", 0, func_overflowerror) + + self.con.create_function("isblob", 1, lambda x: isinstance(x, bytes)) + self.con.create_function("isnone", 1, lambda x: x is None) + self.con.create_function("spam", -1, lambda *x: len(x)) + self.con.execute("create table test(t text)") + + def tearDown(self): + self.con.close() + + def test_func_error_on_create(self): + with self.assertRaises(sqlite.OperationalError): + self.con.create_function("bla", -100, lambda x: 2*x) + + def test_func_too_many_args(self): + category = sqlite.SQLITE_LIMIT_FUNCTION_ARG + msg = "too many arguments on function" + with cx_limit(self.con, category=category, limit=1): + self.con.execute("select abs(-1)"); + with self.assertRaisesRegex(sqlite.OperationalError, msg): + self.con.execute("select max(1, 2)"); + + def test_func_ref_count(self): + def getfunc(): + def f(): + return 1 + return f + f = getfunc() + globals()["foo"] = f + # self.con.create_function("reftest", 0, getfunc()) + self.con.create_function("reftest", 0, f) + cur = self.con.cursor() + cur.execute("select reftest()") + + def test_func_return_text(self): + cur = self.con.cursor() + cur.execute("select returntext()") + val = cur.fetchone()[0] + self.assertEqual(type(val), str) + self.assertEqual(val, "foo") + + def test_func_return_text_with_null_char(self): + cur = self.con.cursor() + res = cur.execute("select returntextwithnull()").fetchone()[0] + self.assertEqual(type(res), str) + self.assertEqual(res, "1\x002") + + def test_func_return_unicode(self): + cur = self.con.cursor() + cur.execute("select returnunicode()") + val = cur.fetchone()[0] + self.assertEqual(type(val), str) + self.assertEqual(val, "bar") + + def test_func_return_int(self): + cur = self.con.cursor() + cur.execute("select returnint()") + val = cur.fetchone()[0] + self.assertEqual(type(val), int) + self.assertEqual(val, 42) + + def test_func_return_float(self): + cur = self.con.cursor() + cur.execute("select returnfloat()") + val = cur.fetchone()[0] + self.assertEqual(type(val), float) + if val < 3.139 or val > 3.141: + self.fail("wrong value") + + def test_func_return_null(self): + cur = self.con.cursor() + cur.execute("select returnnull()") + val = cur.fetchone()[0] + self.assertEqual(type(val), type(None)) + self.assertEqual(val, None) + + def test_func_return_blob(self): + cur = self.con.cursor() + cur.execute("select returnblob()") + val = cur.fetchone()[0] + self.assertEqual(type(val), bytes) + self.assertEqual(val, b"blob") + + def test_func_return_long_long(self): + cur = self.con.cursor() + cur.execute("select returnlonglong()") + val = cur.fetchone()[0] + self.assertEqual(val, 1<<31) + + def test_func_return_nan(self): + cur = self.con.cursor() + cur.execute("select returnnan()") + self.assertIsNone(cur.fetchone()[0]) + + def test_func_return_too_large_int(self): + cur = self.con.cursor() + self.assertRaisesRegex(sqlite.DataError, "string or blob too big", + self.con.execute, "select returntoolargeint()") + + @with_tracebacks(ZeroDivisionError, name="func_raiseexception") + def test_func_exception(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select raiseexception()") + cur.fetchone() + self.assertEqual(str(cm.exception), 'user-defined function raised exception') + + @with_tracebacks(MemoryError, name="func_memoryerror") + def test_func_memory_error(self): + cur = self.con.cursor() + with self.assertRaises(MemoryError): + cur.execute("select memoryerror()") + cur.fetchone() + + @with_tracebacks(OverflowError, name="func_overflowerror") + def test_func_overflow_error(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.DataError): + cur.execute("select overflowerror()") + cur.fetchone() + + def test_any_arguments(self): + cur = self.con.cursor() + cur.execute("select spam(?, ?)", (1, 2)) + val = cur.fetchone()[0] + self.assertEqual(val, 2) + + def test_empty_blob(self): + cur = self.con.execute("select isblob(x'')") + self.assertTrue(cur.fetchone()[0]) + + def test_nan_float(self): + cur = self.con.execute("select isnone(?)", (float("nan"),)) + # SQLite has no concept of nan; it is converted to NULL + self.assertTrue(cur.fetchone()[0]) + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_too_large_int(self): + err = "Python int too large to convert to SQLite INTEGER" + self.assertRaisesRegex(OverflowError, err, self.con.execute, + "select spam(?)", (1 << 65,)) + + def test_non_contiguous_blob(self): + self.assertRaisesRegex(BufferError, + "underlying buffer is not C-contiguous", + self.con.execute, "select spam(?)", + (memoryview(b"blob")[::2],)) + + @with_tracebacks(BufferError, regex="buffer.*contiguous") + def test_return_non_contiguous_blob(self): + with self.assertRaises(sqlite.OperationalError): + cur = self.con.execute("select return_noncont_blob()") + cur.fetchone() + + # TODO: RUSTPYTHON + @unittest.expectedFailure + def test_param_surrogates(self): + self.assertRaisesRegex(UnicodeEncodeError, "surrogates not allowed", + self.con.execute, "select spam(?)", + ("\ud803\ude6d",)) + + def test_func_params(self): + results = [] + def append_result(arg): + results.append((arg, type(arg))) + self.con.create_function("test_params", 1, append_result) + + dataset = [ + (42, int), + (-1, int), + (1234567890123456789, int), + (4611686018427387905, int), # 63-bit int with non-zero low bits + (3.14, float), + (float('inf'), float), + ("text", str), + ("1\x002", str), + ("\u02e2q\u02e1\u2071\u1d57\u1d49", str), + (b"blob", bytes), + (bytearray(range(2)), bytes), + (memoryview(b"blob"), bytes), + (None, type(None)), + ] + for val, _ in dataset: + cur = self.con.execute("select test_params(?)", (val,)) + cur.fetchone() + self.assertEqual(dataset, results) + + # Regarding deterministic functions: + # + # Between 3.8.3 and 3.15.0, deterministic functions were only used to + # optimize inner loops, so for those versions we can only test if the + # sqlite machinery has factored out a call or not. From 3.15.0 and onward, + # deterministic functions were permitted in WHERE clauses of partial + # 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 = 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()") + self.assertEqual(mock.call_count, 2) + else: + with self.assertRaises(sqlite.OperationalError): + self.con.execute("create index t on test(t) where nondeterministic() is not null") + + @unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "Requires SQLite 3.8.3 or higher") + def test_func_deterministic(self): + 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()") + self.assertEqual(mock.call_count, 1) + else: + try: + self.con.execute("create index t on test(t) where deterministic() is not null") + except sqlite.OperationalError: + self.fail("Unexpected failure while creating partial index") + + @unittest.skipIf(sqlite.sqlite_version_info >= (3, 8, 3), "SQLite < 3.8.3 needed") + def test_func_deterministic_not_supported(self): + with self.assertRaises(sqlite.NotSupportedError): + self.con.create_function("deterministic", 0, int, deterministic=True) + + def test_func_deterministic_keyword_only(self): + with self.assertRaises(TypeError): + self.con.create_function("deterministic", 0, int, True) + + def test_function_destructor_via_gc(self): + # See bpo-44304: The destructor of the user function can + # crash if is called without the GIL from the gc functions + dest = sqlite.connect(':memory:') + def md5sum(t): + return + + dest.create_function("md5", 1, md5sum) + x = dest("create table lang (name, first_appeared)") + del md5sum, dest + + y = [x] + y.append(y) + + del x,y + gc_collect() + + @with_tracebacks(OverflowError) + def test_func_return_too_large_int(self): + cur = self.con.cursor() + for value in 2**63, -2**63-1, 2**64: + self.con.create_function("largeint", 0, lambda value=value: value) + with self.assertRaises(sqlite.DataError): + cur.execute("select largeint()") + + @with_tracebacks(UnicodeEncodeError, "surrogates not allowed", "chr") + def test_func_return_text_with_surrogates(self): + cur = self.con.cursor() + self.con.create_function("pychr", 1, chr) + for value in 0xd8ff, 0xdcff: + with self.assertRaises(sqlite.OperationalError): + cur.execute("select pychr(?)", (value,)) + + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @bigmemtest(size=2**31, memuse=3, dry_run=False) + def test_func_return_too_large_text(self, size): + cur = self.con.cursor() + for size in 2**31-1, 2**31: + self.con.create_function("largetext", 0, lambda size=size: "b" * size) + with self.assertRaises(sqlite.DataError): + cur.execute("select largetext()") + + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @bigmemtest(size=2**31, memuse=2, dry_run=False) + def test_func_return_too_large_blob(self, size): + cur = self.con.cursor() + for size in 2**31-1, 2**31: + self.con.create_function("largeblob", 0, lambda size=size: b"b" * size) + with self.assertRaises(sqlite.DataError): + cur.execute("select largeblob()") + + def test_func_return_illegal_value(self): + self.con.create_function("badreturn", 0, lambda: self) + msg = "user-defined function raised exception" + self.assertRaisesRegex(sqlite.OperationalError, msg, + 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:") + cur = self.con.cursor() + cur.execute(""" + create table test( + t text, + i integer, + f float, + n, + b blob + ) + """) + cur.execute("insert into test(t, i, f, n, b) values (?, ?, ?, ?, ?)", + ("foo", 5, 3.14, None, memoryview(b"blob"),)) + + self.con.create_aggregate("nostep", 1, AggrNoStep) + self.con.create_aggregate("nofinalize", 1, AggrNoFinalize) + self.con.create_aggregate("excInit", 1, AggrExceptionInInit) + self.con.create_aggregate("excStep", 1, AggrExceptionInStep) + self.con.create_aggregate("excFinalize", 1, AggrExceptionInFinalize) + self.con.create_aggregate("checkType", 2, AggrCheckType) + self.con.create_aggregate("checkTypes", -1, AggrCheckTypes) + self.con.create_aggregate("mysum", 1, AggrSum) + self.con.create_aggregate("aggtxt", 1, AggrText) + + def tearDown(self): + #self.cur.close() + #self.con.close() + pass + + def test_aggr_error_on_create(self): + with self.assertRaises(sqlite.OperationalError): + self.con.create_function("bla", -100, AggrSum) + + @with_tracebacks(AttributeError, name="AggrNoStep") + def test_aggr_no_step(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select nostep(t) from test") + self.assertEqual(str(cm.exception), + "user-defined aggregate's 'step' method not defined") + + def test_aggr_no_finalize(self): + cur = self.con.cursor() + 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] + + @with_tracebacks(ZeroDivisionError, name="AggrExceptionInInit") + def test_aggr_exception_in_init(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select excInit(t) from test") + val = cur.fetchone()[0] + self.assertEqual(str(cm.exception), "user-defined aggregate's '__init__' method raised error") + + @with_tracebacks(ZeroDivisionError, name="AggrExceptionInStep") + def test_aggr_exception_in_step(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select excStep(t) from test") + val = cur.fetchone()[0] + self.assertEqual(str(cm.exception), "user-defined aggregate's 'step' method raised error") + + @with_tracebacks(ZeroDivisionError, name="AggrExceptionInFinalize") + def test_aggr_exception_in_finalize(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select excFinalize(t) from test") + val = cur.fetchone()[0] + self.assertEqual(str(cm.exception), "user-defined aggregate's 'finalize' method raised error") + + def test_aggr_check_param_str(self): + cur = self.con.cursor() + cur.execute("select checkTypes('str', ?, ?)", ("foo", str())) + val = cur.fetchone()[0] + self.assertEqual(val, 2) + + def test_aggr_check_param_int(self): + cur = self.con.cursor() + cur.execute("select checkType('int', ?)", (42,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_params_int(self): + cur = self.con.cursor() + cur.execute("select checkTypes('int', ?, ?)", (42, 24)) + val = cur.fetchone()[0] + self.assertEqual(val, 2) + + def test_aggr_check_param_float(self): + cur = self.con.cursor() + cur.execute("select checkType('float', ?)", (3.14,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_param_none(self): + cur = self.con.cursor() + cur.execute("select checkType('None', ?)", (None,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_param_blob(self): + cur = self.con.cursor() + cur.execute("select checkType('blob', ?)", (memoryview(b"blob"),)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_aggr_sum(self): + cur = self.con.cursor() + cur.execute("delete from test") + cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)]) + cur.execute("select mysum(i) from test") + val = cur.fetchone()[0] + self.assertEqual(val, 60) + + def test_aggr_no_match(self): + cur = self.con.execute("select mysum(i) from (select 1 as i) where i == 0") + val = cur.fetchone()[0] + self.assertIsNone(val) + + def test_aggr_text(self): + cur = self.con.cursor() + for txt in ["foo", "1\x002"]: + with self.subTest(txt=txt): + cur.execute("select aggtxt(?) from test", (txt,)) + val = cur.fetchone()[0] + self.assertEqual(val, txt) + + +class AuthorizerTests(unittest.TestCase): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + return sqlite.SQLITE_DENY + if arg2 == 'c2' or arg1 == 't2': + return sqlite.SQLITE_DENY + return sqlite.SQLITE_OK + + def setUp(self): + # TODO: RUSTPYTHON difference 'prohibited' + self.prohibited = 'not authorized' + self.con = sqlite.connect(":memory:") + self.con.executescript(""" + create table t1 (c1, c2); + create table t2 (c1, c2); + insert into t1 (c1, c2) values (1, 2); + insert into t2 (c1, c2) values (4, 5); + """) + + # For our security test: + self.con.execute("select c2 from t2") + + self.con.set_authorizer(self.authorizer_cb) + + def tearDown(self): + pass + + def test_table_access(self): + with self.assertRaises(sqlite.DatabaseError) as cm: + self.con.execute("select * from t2") + self.assertIn(self.prohibited, str(cm.exception)) + + def test_column_access(self): + with self.assertRaises(sqlite.DatabaseError) as cm: + self.con.execute("select c2 from t1") + self.assertIn(self.prohibited, str(cm.exception)) + + def test_clear_authorizer(self): + self.con.set_authorizer(None) + self.con.execute("select * from t2") + self.con.execute("select c2 from t1") + + +class AuthorizerRaiseExceptionTests(AuthorizerTests): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + raise ValueError + if arg2 == 'c2' or arg1 == 't2': + raise ValueError + return sqlite.SQLITE_OK + + @with_tracebacks(ValueError, name="authorizer_cb") + def test_table_access(self): + super().test_table_access() + + @with_tracebacks(ValueError, name="authorizer_cb") + def test_column_access(self): + super().test_table_access() + +class AuthorizerIllegalTypeTests(AuthorizerTests): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + return 0.0 + if arg2 == 'c2' or arg1 == 't2': + return 0.0 + return sqlite.SQLITE_OK + +class AuthorizerLargeIntegerTests(AuthorizerTests): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + return 2**32 + if arg2 == 'c2' or arg1 == 't2': + return 2**32 + return sqlite.SQLITE_OK + + +if __name__ == "__main__": + unittest.main() diff --git a/extra_tests/snippets/stdlib_sqlite.py b/extra_tests/snippets/stdlib_sqlite.py new file mode 100644 index 000000000..8ec5416fe --- /dev/null +++ b/extra_tests/snippets/stdlib_sqlite.py @@ -0,0 +1,49 @@ +import sqlite3 as sqlite +import unittest + +rows = [(3,), (4,)] +cx = sqlite.connect(":memory:") +cx.execute(";") +cx.executescript(";") +cx.execute("CREATE TABLE foo(key INTEGER)") +cx.executemany("INSERT INTO foo(key) VALUES (?)", rows) + +cur = cx.cursor() +fetchall = cur.execute("SELECT * FROM foo").fetchall() +assert fetchall == rows + +cx.executescript(""" + /* CREATE TABLE foo(key INTEGER); */ + INSERT INTO foo(key) VALUES (10); + INSERT INTO foo(key) VALUES (11); +""") + +class AggrSum: + def __init__(self): + self.val = 0.0 + + def step(self, val): + self.val += val + + def finalize(self): + return self.val + +cx.create_aggregate("mysum", 1, AggrSum) +cur.execute("select mysum(key) from foo") +assert cur.fetchone()[0] == 28.0 + +# toobig = 2**64 +# cur.execute("insert into foo(key) values (?)", (toobig,)) + +class AggrText: + def __init__(self): + self.txt = "" + def step(self, txt): + txt = str(txt) + self.txt = self.txt + txt + def finalize(self): + return self.txt + +cx.create_aggregate("aggtxt", 1, AggrText) +cur.execute("select aggtxt(key) from foo") +assert cur.fetchone()[0] == '341011' \ No newline at end of file diff --git a/stdlib/Cargo.toml b/stdlib/Cargo.toml index 533f4f9cf..eef401de7 100644 --- a/stdlib/Cargo.toml +++ b/stdlib/Cargo.toml @@ -97,6 +97,9 @@ openssl-sys = { version = "0.9.72", optional = true } openssl-probe = { version = "0.1.5", optional = true } foreign-types-shared = { version = "0.1.1", optional = true } +[target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies] +libsqlite3-sys = { version = "0.25", features = ["min_sqlite_version_3_7_16", "bundled"] } + [target.'cfg(windows)'.dependencies] schannel = "0.1.19" widestring = "0.5.1" diff --git a/stdlib/src/lib.rs b/stdlib/src/lib.rs index b8b6ee359..a4bbabf5e 100644 --- a/stdlib/src/lib.rs +++ b/stdlib/src/lib.rs @@ -50,6 +50,8 @@ mod resource; mod scproxy; #[cfg(not(target_arch = "wasm32"))] mod select; +#[cfg(not(any(target_os = "android", target_arch = "wasm32")))] +mod sqlite; #[cfg(all(not(target_arch = "wasm32"), feature = "ssl"))] mod ssl; #[cfg(all(unix, not(target_os = "redox"), not(target_os = "ios")))] @@ -118,6 +120,10 @@ pub fn get_module_inits() -> impl Iterator, StdlibInit "_socket" => socket::make_module, "faulthandler" => faulthandler::make_module, } + #[cfg(not(any(target_os = "android", target_arch = "wasm32")))] + { + "_sqlite3" => sqlite::make_module, + } #[cfg(feature = "ssl")] { "_ssl" => ssl::make_module, diff --git a/stdlib/src/sqlite.rs b/stdlib/src/sqlite.rs new file mode 100644 index 000000000..ed3108ce1 --- /dev/null +++ b/stdlib/src/sqlite.rs @@ -0,0 +1,2959 @@ +use rustpython_vm::{PyObjectRef, VirtualMachine}; + +// pub(crate) use _sqlite::make_module; +pub(crate) fn make_module(vm: &VirtualMachine) -> PyObjectRef { + // TODO: sqlite version check + let module = _sqlite::make_module(vm); + _sqlite::setup_module(&module, vm); + module +} + +#[pymodule] +mod _sqlite { + use libsqlite3_sys::{ + sqlite3, sqlite3_aggregate_context, sqlite3_backup_finish, sqlite3_backup_init, + sqlite3_backup_pagecount, sqlite3_backup_remaining, sqlite3_backup_step, sqlite3_bind_blob, + sqlite3_bind_double, sqlite3_bind_int64, sqlite3_bind_null, sqlite3_bind_parameter_count, + sqlite3_bind_parameter_name, sqlite3_bind_text, sqlite3_blob, sqlite3_blob_bytes, + sqlite3_blob_close, sqlite3_blob_open, sqlite3_blob_read, sqlite3_blob_write, + sqlite3_busy_timeout, sqlite3_changes, sqlite3_close_v2, sqlite3_column_blob, + sqlite3_column_bytes, sqlite3_column_count, sqlite3_column_decltype, sqlite3_column_double, + sqlite3_column_int64, sqlite3_column_name, sqlite3_column_text, sqlite3_column_type, + sqlite3_complete, sqlite3_context, sqlite3_context_db_handle, sqlite3_create_collation_v2, + sqlite3_create_function_v2, sqlite3_create_window_function, sqlite3_data_count, + sqlite3_db_handle, sqlite3_errcode, sqlite3_errmsg, sqlite3_exec, sqlite3_expanded_sql, + sqlite3_extended_errcode, sqlite3_finalize, sqlite3_get_autocommit, sqlite3_interrupt, + sqlite3_last_insert_rowid, sqlite3_libversion, sqlite3_limit, sqlite3_open_v2, + sqlite3_prepare_v2, sqlite3_progress_handler, sqlite3_reset, sqlite3_result_blob, + sqlite3_result_double, sqlite3_result_error, sqlite3_result_error_nomem, + sqlite3_result_error_toobig, sqlite3_result_int64, sqlite3_result_null, + sqlite3_result_text, sqlite3_set_authorizer, sqlite3_sleep, sqlite3_step, sqlite3_stmt, + sqlite3_stmt_busy, sqlite3_stmt_readonly, sqlite3_threadsafe, sqlite3_total_changes, + sqlite3_trace_v2, sqlite3_user_data, sqlite3_value, sqlite3_value_blob, + sqlite3_value_bytes, sqlite3_value_double, sqlite3_value_int64, sqlite3_value_text, + sqlite3_value_type, SQLITE_BLOB, SQLITE_DETERMINISTIC, SQLITE_FLOAT, SQLITE_INTEGER, + SQLITE_NULL, SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE, SQLITE_OPEN_URI, SQLITE_TEXT, + SQLITE_TRACE_STMT, SQLITE_TRANSIENT, SQLITE_UTF8, + }; + use rustpython_common::{ + atomic::{Ordering, PyAtomic, Radium}, + hash::PyHash, + lock::{PyMappedMutexGuard, PyMutex, PyMutexGuard}, + static_cell, + }; + use rustpython_vm::{ + atomic_func, + builtins::{ + PyBaseException, PyBaseExceptionRef, PyByteArray, PyBytes, PyDict, PyDictRef, PyFloat, + PyInt, PyIntRef, PySlice, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef, + }, + convert::IntoObject, + function::{ArgCallable, ArgIterable, FuncArgs, OptionalArg, PyComparisonValue}, + protocol::{PyBuffer, PyIterReturn, PyMappingMethods, PySequence, PySequenceMethods}, + sliceable::{SaturatedSliceIter, SliceableSequenceOp}, + stdlib::os::PyPathLike, + types::{ + AsMapping, AsSequence, Callable, Comparable, Constructor, Hashable, IterNext, + IterNextIterable, Iterable, PyComparisonOp, + }, + utils::ToCString, + AsObject, Py, PyAtomicRef, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, + TryFromBorrowedObject, VirtualMachine, + __exports::paste, + }; + use std::{ + ffi::{c_int, c_longlong, c_uint, c_void, CStr}, + fmt::Debug, + ops::Deref, + ptr::{addr_of_mut, null, null_mut}, + thread::ThreadId, + }; + + macro_rules! exceptions { + ($(($x:ident, $base:expr)),*) => { + paste::paste! { + static_cell! { + $( + static [<$x:snake:upper>]: PyTypeRef; + )* + } + $( + #[allow(dead_code)] + fn [](vm: &VirtualMachine, msg: String) -> PyBaseExceptionRef { + vm.new_exception_msg([<$x:snake _type>]().to_owned(), msg) + } + fn [<$x:snake _type>]() -> &'static Py { + [<$x:snake:upper>].get().expect("exception type not initialize") + } + )* + fn setup_module_exceptions(module: &PyObject, vm: &VirtualMachine) { + $( + let exception = [<$x:snake:upper>].get_or_init( + || vm.ctx.new_exception_type("_sqlite3", stringify!($x), Some(vec![$base(vm).to_owned()]))); + module.set_attr(stringify!($x), exception.clone().into_object(), vm).unwrap(); + )* + } + } + }; + } + + exceptions!( + (Warning, |vm: &VirtualMachine| vm + .ctx + .exceptions + .exception_type), + (Error, |vm: &VirtualMachine| vm + .ctx + .exceptions + .exception_type), + (InterfaceError, |_| error_type()), + (DatabaseError, |_| error_type()), + (DataError, |_| database_error_type()), + (OperationalError, |_| database_error_type()), + (IntegrityError, |_| database_error_type()), + (InternalError, |_| database_error_type()), + (ProgrammingError, |_| database_error_type()), + (NotSupportedError, |_| database_error_type()) + ); + + #[pyattr] + fn sqlite_version(vm: &VirtualMachine) -> String { + let s = unsafe { sqlite3_libversion() }; + ptr_to_str(s, vm).unwrap().to_owned() + } + + #[pyattr] + fn threadsafety(_: &VirtualMachine) -> c_int { + let mode = unsafe { sqlite3_threadsafe() }; + match mode { + 0 => 0, + 1 => 3, + 2 => 1, + _ => panic!("Unable to interpret SQLite threadsafety mode"), + } + } + + #[pyattr(name = "_deprecated_version")] + const PYSQLITE_VERSION: &str = "2.6.0"; + + #[pyattr] + const PARSE_DECLTYPES: c_int = 1; + #[pyattr] + const PARSE_COLNAMES: c_int = 2; + + #[pyattr] + use libsqlite3_sys::{ + SQLITE_ALTER_TABLE, SQLITE_ANALYZE, SQLITE_ATTACH, SQLITE_CREATE_INDEX, + SQLITE_CREATE_TABLE, SQLITE_CREATE_TEMP_INDEX, SQLITE_CREATE_TEMP_TABLE, + SQLITE_CREATE_TEMP_TRIGGER, SQLITE_CREATE_TEMP_VIEW, SQLITE_CREATE_TRIGGER, + SQLITE_CREATE_VIEW, SQLITE_CREATE_VTABLE, SQLITE_DELETE, SQLITE_DENY, SQLITE_DETACH, + SQLITE_DROP_INDEX, SQLITE_DROP_TABLE, SQLITE_DROP_TEMP_INDEX, SQLITE_DROP_TEMP_TABLE, + SQLITE_DROP_TEMP_TRIGGER, SQLITE_DROP_TEMP_VIEW, SQLITE_DROP_TRIGGER, SQLITE_DROP_VIEW, + SQLITE_DROP_VTABLE, SQLITE_FUNCTION, SQLITE_IGNORE, SQLITE_INSERT, SQLITE_LIMIT_ATTACHED, + SQLITE_LIMIT_COLUMN, SQLITE_LIMIT_COMPOUND_SELECT, SQLITE_LIMIT_EXPR_DEPTH, + SQLITE_LIMIT_FUNCTION_ARG, SQLITE_LIMIT_LENGTH, SQLITE_LIMIT_LIKE_PATTERN_LENGTH, + SQLITE_LIMIT_SQL_LENGTH, SQLITE_LIMIT_TRIGGER_DEPTH, SQLITE_LIMIT_VARIABLE_NUMBER, + SQLITE_LIMIT_VDBE_OP, SQLITE_LIMIT_WORKER_THREADS, SQLITE_PRAGMA, SQLITE_READ, + SQLITE_RECURSIVE, SQLITE_REINDEX, SQLITE_SAVEPOINT, SQLITE_SELECT, SQLITE_TRANSACTION, + SQLITE_UPDATE, + }; + + macro_rules! error_codes { + ($($x:ident),*) => { + $( + #[allow(unused_imports)] + use libsqlite3_sys::$x; + )* + static ERROR_CODES: &[(&str, c_int)] = &[ + $( + (stringify!($x), libsqlite3_sys::$x), + )* + ]; + }; + } + + error_codes!( + SQLITE_ABORT, + SQLITE_AUTH, + SQLITE_BUSY, + SQLITE_CANTOPEN, + SQLITE_CONSTRAINT, + SQLITE_CORRUPT, + SQLITE_DONE, + SQLITE_EMPTY, + SQLITE_ERROR, + SQLITE_FORMAT, + SQLITE_FULL, + SQLITE_INTERNAL, + SQLITE_INTERRUPT, + SQLITE_IOERR, + SQLITE_LOCKED, + SQLITE_MISMATCH, + SQLITE_MISUSE, + SQLITE_NOLFS, + SQLITE_NOMEM, + SQLITE_NOTADB, + SQLITE_NOTFOUND, + SQLITE_OK, + SQLITE_PERM, + SQLITE_PROTOCOL, + SQLITE_RANGE, + SQLITE_READONLY, + SQLITE_ROW, + SQLITE_SCHEMA, + SQLITE_TOOBIG, + SQLITE_NOTICE, + SQLITE_WARNING, + SQLITE_ABORT_ROLLBACK, + SQLITE_BUSY_RECOVERY, + SQLITE_CANTOPEN_FULLPATH, + SQLITE_CANTOPEN_ISDIR, + SQLITE_CANTOPEN_NOTEMPDIR, + SQLITE_CORRUPT_VTAB, + SQLITE_IOERR_ACCESS, + SQLITE_IOERR_BLOCKED, + SQLITE_IOERR_CHECKRESERVEDLOCK, + SQLITE_IOERR_CLOSE, + SQLITE_IOERR_DELETE, + SQLITE_IOERR_DELETE_NOENT, + SQLITE_IOERR_DIR_CLOSE, + SQLITE_IOERR_DIR_FSYNC, + SQLITE_IOERR_FSTAT, + SQLITE_IOERR_FSYNC, + SQLITE_IOERR_LOCK, + SQLITE_IOERR_NOMEM, + SQLITE_IOERR_RDLOCK, + SQLITE_IOERR_READ, + SQLITE_IOERR_SEEK, + SQLITE_IOERR_SHMLOCK, + SQLITE_IOERR_SHMMAP, + SQLITE_IOERR_SHMOPEN, + SQLITE_IOERR_SHMSIZE, + SQLITE_IOERR_SHORT_READ, + SQLITE_IOERR_TRUNCATE, + SQLITE_IOERR_UNLOCK, + SQLITE_IOERR_WRITE, + SQLITE_LOCKED_SHAREDCACHE, + SQLITE_READONLY_CANTLOCK, + SQLITE_READONLY_RECOVERY, + SQLITE_CONSTRAINT_CHECK, + SQLITE_CONSTRAINT_COMMITHOOK, + SQLITE_CONSTRAINT_FOREIGNKEY, + SQLITE_CONSTRAINT_FUNCTION, + SQLITE_CONSTRAINT_NOTNULL, + SQLITE_CONSTRAINT_PRIMARYKEY, + SQLITE_CONSTRAINT_TRIGGER, + SQLITE_CONSTRAINT_UNIQUE, + SQLITE_CONSTRAINT_VTAB, + SQLITE_READONLY_ROLLBACK, + SQLITE_IOERR_MMAP, + SQLITE_NOTICE_RECOVER_ROLLBACK, + SQLITE_NOTICE_RECOVER_WAL, + SQLITE_BUSY_SNAPSHOT, + SQLITE_IOERR_GETTEMPPATH, + SQLITE_WARNING_AUTOINDEX, + SQLITE_CANTOPEN_CONVPATH, + SQLITE_IOERR_CONVPATH, + SQLITE_CONSTRAINT_ROWID, + SQLITE_READONLY_DBMOVED, + SQLITE_AUTH_USER, + SQLITE_OK_LOAD_PERMANENTLY, + SQLITE_IOERR_VNODE, + SQLITE_IOERR_AUTH, + SQLITE_IOERR_BEGIN_ATOMIC, + SQLITE_IOERR_COMMIT_ATOMIC, + SQLITE_IOERR_ROLLBACK_ATOMIC, + SQLITE_ERROR_MISSING_COLLSEQ, + SQLITE_ERROR_RETRY, + SQLITE_READONLY_CANTINIT, + SQLITE_READONLY_DIRECTORY, + SQLITE_CORRUPT_SEQUENCE, + SQLITE_LOCKED_VTAB, + SQLITE_CANTOPEN_DIRTYWAL, + SQLITE_ERROR_SNAPSHOT, + SQLITE_CANTOPEN_SYMLINK, + SQLITE_CONSTRAINT_PINNED, + SQLITE_OK_SYMLINK, + SQLITE_BUSY_TIMEOUT, + SQLITE_CORRUPT_INDEX, + SQLITE_IOERR_DATA, + SQLITE_IOERR_CORRUPTFS + ); + + #[derive(FromArgs)] + struct ConnectArgs { + #[pyarg(any)] + database: PyPathLike, + #[pyarg(any, default = "5.0")] + timeout: f64, + #[pyarg(any, default = "0")] + detect_types: c_int, + #[pyarg(any, default = "Some(vm.ctx.empty_str.clone())")] + isolation_level: Option, + #[pyarg(any, default = "true")] + check_same_thread: bool, + #[pyarg(any, default = "Connection::class(vm).to_owned()")] + factory: PyTypeRef, + // TODO: cache statements + #[allow(dead_code)] + #[pyarg(any, default = "0")] + cached_statements: c_int, + #[pyarg(any, default = "false")] + uri: bool, + } + + #[derive(FromArgs)] + struct BackupArgs { + #[pyarg(any)] + target: PyRef, + #[pyarg(named, default = "-1")] + pages: c_int, + #[pyarg(named, optional)] + progress: Option, + #[pyarg(named, optional)] + name: Option, + #[pyarg(named, default = "0.250")] + sleep: f64, + } + + #[derive(FromArgs)] + struct CreateFunctionArgs { + #[pyarg(any)] + name: PyStrRef, + #[pyarg(any)] + narg: c_int, + #[pyarg(any)] + func: PyObjectRef, + #[pyarg(named, default)] + deterministic: bool, + } + + #[derive(FromArgs)] + struct CreateAggregateArgs { + #[pyarg(any)] + name: PyStrRef, + #[pyarg(positional)] + narg: c_int, + #[pyarg(positional)] + aggregate_class: PyObjectRef, + } + + #[derive(FromArgs)] + struct BlobOpenArgs { + #[pyarg(positional)] + table: PyStrRef, + #[pyarg(positional)] + column: PyStrRef, + #[pyarg(positional)] + row: i64, + #[pyarg(named, default)] + readonly: bool, + #[pyarg(named, default = "vm.ctx.new_str(stringify!(main))")] + name: PyStrRef, + } + + struct CallbackData { + obj: *const PyObject, + vm: *const VirtualMachine, + } + + impl CallbackData { + fn new(obj: PyObjectRef, vm: &VirtualMachine) -> Option { + (!vm.is_none(&obj)).then_some(Self { + obj: obj.into_raw(), + vm, + }) + } + + fn retrive(&self) -> (&PyObject, &VirtualMachine) { + unsafe { (&*self.obj, &*self.vm) } + } + + unsafe extern "C" fn destructor(data: *mut c_void) { + drop(Box::from_raw(data.cast::())); + } + + unsafe extern "C" fn func_callback( + context: *mut sqlite3_context, + argc: c_int, + argv: *mut *mut sqlite3_value, + ) { + let context = SqliteContext::from(context); + let (func, vm) = (*context.user_data::()).retrive(); + let args = std::slice::from_raw_parts(argv, argc as usize); + + let f = || -> PyResult<()> { + let db = context.db_handle(); + let args = args + .iter() + .cloned() + .map(|val| value_to_object(val, db, vm)) + .collect::>>()?; + + let val = vm.invoke(func, args)?; + + context.result_from_object(&val, vm) + }; + + if let Err(exc) = f() { + context.result_exception(vm, exc, "user-defined function raised exception\0") + } + } + + unsafe extern "C" fn step_callback( + context: *mut sqlite3_context, + argc: c_int, + argv: *mut *mut sqlite3_value, + ) { + let context = SqliteContext::from(context); + let (cls, vm) = (*context.user_data::()).retrive(); + let args = std::slice::from_raw_parts(argv, argc as usize); + let instance = context.aggregate_context::<*const PyObject>(); + if (*instance).is_null() { + match vm.invoke(cls, ()) { + Ok(obj) => *instance = obj.into_raw(), + Err(exc) => { + return context.result_exception( + vm, + exc, + "user-defined aggregate's '__init__' method raised error\0", + ) + } + } + } + let instance = &**instance; + + Self::call_method_with_args(context, instance, "step", args, vm); + } + + unsafe extern "C" fn finalize_callback(context: *mut sqlite3_context) { + let context = SqliteContext::from(context); + let (_, vm) = (*context.user_data::()).retrive(); + let instance = context.aggregate_context::<*const PyObject>(); + let Some(instance) = (*instance).as_ref() else { return; }; + + Self::callback_result_from_method(context, instance, "finalize", vm); + } + + unsafe extern "C" fn collation_callback( + data: *mut c_void, + a_len: c_int, + a_ptr: *const c_void, + b_len: c_int, + b_ptr: *const c_void, + ) -> c_int { + let (callable, vm) = (*data.cast::()).retrive(); + + let f = || -> PyResult { + let text1 = ptr_to_string(a_ptr.cast(), a_len, null_mut(), vm)?; + let text1 = vm.ctx.new_str(text1); + let text2 = ptr_to_string(b_ptr.cast(), b_len, null_mut(), vm)?; + let text2 = vm.ctx.new_str(text2); + + let val = vm.invoke(callable, (text1, text2))?; + let Some(val) = val.to_number().index(vm) else { + return Ok(0); + }; + + let val = match val?.as_bigint().sign() { + num_bigint::Sign::Plus => 1, + num_bigint::Sign::Minus => -1, + num_bigint::Sign::NoSign => 0, + }; + + Ok(val) + }; + + f().unwrap_or(0) + } + + unsafe extern "C" fn value_callback(context: *mut sqlite3_context) { + let context = SqliteContext::from(context); + let (_, vm) = (*context.user_data::()).retrive(); + let instance = context.aggregate_context::<*const PyObject>(); + let instance = &**instance; + + Self::callback_result_from_method(context, instance, "value", vm); + } + + unsafe extern "C" fn inverse_callback( + context: *mut sqlite3_context, + argc: c_int, + argv: *mut *mut sqlite3_value, + ) { + let context = SqliteContext::from(context); + let (_, vm) = (*context.user_data::()).retrive(); + let args = std::slice::from_raw_parts(argv, argc as usize); + let instance = context.aggregate_context::<*const PyObject>(); + let instance = &**instance; + + Self::call_method_with_args(context, instance, "inverse", args, vm); + } + + unsafe extern "C" fn authorizer_callback( + data: *mut c_void, + action: c_int, + arg1: *const i8, + arg2: *const i8, + db_name: *const i8, + access: *const i8, + ) -> c_int { + let (callable, vm) = (*data.cast::()).retrive(); + let f = || -> PyResult { + let arg1 = ptr_to_str(arg1, vm)?; + let arg2 = ptr_to_str(arg2, vm)?; + let db_name = ptr_to_str(db_name, vm)?; + let access = ptr_to_str(access, vm)?; + + let val = vm.invoke(callable, (action, arg1, arg2, db_name, access))?; + let Some(val) = val.payload::() else { + return Ok(SQLITE_DENY); + }; + val.try_to_primitive::(vm) + }; + + f().unwrap_or(SQLITE_DENY) + } + + unsafe extern "C" fn trace_callback( + _typ: c_uint, + data: *mut c_void, + stmt: *mut c_void, + sql: *mut c_void, + ) -> c_int { + let (callable, vm) = (*data.cast::()).retrive(); + let expanded = sqlite3_expanded_sql(stmt.cast()); + let f = || -> PyResult<()> { + let stmt = ptr_to_str(expanded, vm).or_else(|_| ptr_to_str(sql.cast(), vm))?; + vm.invoke(callable, (stmt,)).map(drop) + }; + let _ = f(); + 0 + } + + unsafe extern "C" fn progress_callback(data: *mut c_void) -> c_int { + let (callable, vm) = (*data.cast::()).retrive(); + if let Ok(val) = vm.invoke(callable, ()) { + if let Ok(val) = val.is_true(vm) { + return val as c_int; + } + } + -1 + } + + fn callback_result_from_method( + context: SqliteContext, + instance: &PyObject, + name: &str, + vm: &VirtualMachine, + ) { + let f = || -> PyResult<()> { + let val = vm.call_method(instance, name, ())?; + context.result_from_object(&val, vm) + }; + + if let Err(exc) = f() { + if exc.fast_isinstance(vm.ctx.exceptions.attribute_error) { + context.result_exception( + vm, + exc, + &format!("user-defined aggregate's '{}' method not defined\0", name), + ) + } else { + context.result_exception( + vm, + exc, + &format!("user-defined aggregate's '{}' method raised error\0", name), + ) + } + } + } + + fn call_method_with_args( + context: SqliteContext, + instance: &PyObject, + name: &str, + args: &[*mut sqlite3_value], + vm: &VirtualMachine, + ) { + let f = || -> PyResult<()> { + let db = context.db_handle(); + let args = args + .iter() + .cloned() + .map(|val| value_to_object(val, db, vm)) + .collect::>>()?; + vm.call_method(instance, name, args).map(drop) + }; + + if let Err(exc) = f() { + if exc.fast_isinstance(vm.ctx.exceptions.attribute_error) { + context.result_exception( + vm, + exc, + &format!("user-defined aggregate's '{}' method not defined\0", name), + ) + } else { + context.result_exception( + vm, + exc, + &format!("user-defined aggregate's '{}' method raised error\0", name), + ) + } + } + } + } + + impl Drop for CallbackData { + fn drop(&mut self) { + unsafe { PyObjectRef::from_raw(self.obj) }; + } + } + + #[pyfunction] + fn connect(args: ConnectArgs, vm: &VirtualMachine) -> PyResult { + Connection::py_new(args.factory.clone(), args, vm) + } + + #[pyfunction] + fn complete_statement(statement: PyStrRef, vm: &VirtualMachine) -> PyResult { + let s = statement.to_cstring(vm)?; + let ret = unsafe { sqlite3_complete(s.as_ptr()) }; + Ok(ret == 1) + } + + #[pyfunction] + fn enable_callback_tracebacks(flag: bool) { + enable_traceback().store(flag, Ordering::Relaxed); + } + + #[pyfunction] + fn register_adapter(typ: PyTypeRef, adapter: ArgCallable, vm: &VirtualMachine) -> PyResult<()> { + if typ.is(PyInt::class(vm)) + || typ.is(PyFloat::class(vm)) + || typ.is(PyStr::class(vm)) + || typ.is(PyByteArray::class(vm)) + { + let _ = BASE_TYPE_ADAPTED.set(()); + } + let protocol = PrepareProtocol::class(vm).to_owned(); + let key = vm.ctx.new_tuple(vec![typ.into(), protocol.into()]); + adapters().set_item(key.as_object(), adapter.into(), vm) + } + + #[pyfunction] + fn register_converter( + typename: PyStrRef, + converter: ArgCallable, + vm: &VirtualMachine, + ) -> PyResult<()> { + let name = typename.as_str().to_uppercase(); + converters().set_item(&name, converter.into(), vm) + } + + fn _adapt(obj: &PyObject, proto: PyTypeRef, alt: F, vm: &VirtualMachine) -> PyResult + where + F: FnOnce(&PyObject) -> PyResult, + { + let proto = proto.into_object(); + let key = vm + .ctx + .new_tuple(vec![obj.class().to_owned().into(), proto.clone()]); + + if let Some(adapter) = adapters().get_item_opt(key.as_object(), vm)? { + return vm.invoke(&adapter, (obj,)); + } + if let Ok(adapter) = proto.get_attr("__adapt__", vm) { + match vm.invoke(&adapter, (obj,)) { + Ok(val) => return Ok(val), + Err(exc) => { + if !exc.fast_isinstance(vm.ctx.exceptions.type_error) { + return Err(exc); + } + } + } + } + if let Ok(adapter) = obj.get_attr("__conform__", vm) { + match vm.invoke(&adapter, (proto,)) { + Ok(val) => return Ok(val), + Err(exc) => { + if !exc.fast_isinstance(vm.ctx.exceptions.type_error) { + return Err(exc); + } + } + } + } + + alt(obj) + } + + #[pyfunction] + fn adapt( + obj: PyObjectRef, + proto: OptionalArg>, + alt: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + // TODO: None proto + let proto = proto + .flatten() + .unwrap_or_else(|| PrepareProtocol::class(vm).to_owned()); + + _adapt( + &obj, + proto, + |_| { + if let OptionalArg::Present(alt) = alt { + Ok(alt) + } else { + Err(new_programming_error(vm, "can't adapt".to_owned())) + } + }, + vm, + ) + } + + fn need_adapt(obj: &PyObject, vm: &VirtualMachine) -> bool { + if BASE_TYPE_ADAPTED.get().is_some() { + true + } else { + let cls = obj.class(); + !(cls.is(vm.ctx.types.int_type) + || cls.is(vm.ctx.types.float_type) + || cls.is(vm.ctx.types.str_type) + || cls.is(vm.ctx.types.bytearray_type)) + } + } + + static_cell! { + static CONVERTERS: PyDictRef; + static ADAPTERS: PyDictRef; + static BASE_TYPE_ADAPTED: (); + static USER_FUNCTION_EXCEPTION: PyAtomicRef>; + static ENABLE_TRACEBACK: PyAtomic; + } + + fn converters() -> &'static Py { + CONVERTERS.get().expect("converters not initialize") + } + + fn adapters() -> &'static Py { + ADAPTERS.get().expect("adapters not initialize") + } + + fn user_function_exception() -> &'static PyAtomicRef> { + USER_FUNCTION_EXCEPTION + .get() + .expect("user function exception not initialize") + } + + fn enable_traceback() -> &'static PyAtomic { + ENABLE_TRACEBACK + .get() + .expect("enable traceback not initialize") + } + + pub(super) fn setup_module(module: &PyObject, vm: &VirtualMachine) { + for (name, code) in ERROR_CODES { + let name = vm.ctx.new_str(*name); + let code = vm.new_pyobj(*code); + module.set_attr(name, code, vm).unwrap(); + } + + setup_module_exceptions(module, vm); + + let _ = CONVERTERS.set(vm.ctx.new_dict()); + let _ = ADAPTERS.set(vm.ctx.new_dict()); + let _ = USER_FUNCTION_EXCEPTION.set(PyAtomicRef::from(None)); + let _ = ENABLE_TRACEBACK.set(Radium::new(false)); + + module + .set_attr("converters", converters().to_owned(), vm) + .unwrap(); + module + .set_attr("adapters", adapters().to_owned(), vm) + .unwrap(); + } + + #[pyattr] + #[pyclass(name)] + #[derive(PyPayload)] + struct Connection { + db: PyMutex>, + detect_types: c_int, + isolation_level: PyAtomicRef>, + check_same_thread: bool, + thread_ident: ThreadId, + row_factory: PyAtomicRef>, + text_factory: PyAtomicRef, + } + + impl Debug for Connection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "Sqlite3 Connection") + } + } + + impl Constructor for Connection { + type Args = ConnectArgs; + + fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult { + Self::new(args, vm).and_then(|x| x.into_ref_with_type(vm, cls).map(Into::into)) + } + } + + impl Callable for Connection { + type Args = (PyStrRef,); + + fn call(zelf: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { + if let Some(stmt) = Statement::new(zelf, &args.0, vm)? { + Ok(stmt.into_ref(vm).into()) + } else { + Ok(vm.ctx.none()) + } + } + } + + #[pyclass(with(Constructor, Callable), flags(BASETYPE))] + impl Connection { + fn new(args: ConnectArgs, vm: &VirtualMachine) -> PyResult { + let path = args.database.into_cstring(vm)?; + let db = Sqlite::from(SqliteRaw::open(path.as_ptr(), args.uri, vm)?); + let timeout = (args.timeout * 1000.0) as c_int; + db.busy_timeout(timeout); + if let Some(isolation_level) = &args.isolation_level { + begin_statement_ptr_from_isolation_level(isolation_level, vm)?; + } + let text_factory = PyStr::class(vm).to_owned().into_object(); + + Ok(Self { + db: PyMutex::new(Some(db)), + detect_types: args.detect_types, + isolation_level: PyAtomicRef::from(args.isolation_level), + check_same_thread: args.check_same_thread, + thread_ident: std::thread::current().id(), + row_factory: PyAtomicRef::from(None), + text_factory: PyAtomicRef::from(text_factory), + }) + } + + fn db_lock(&self, vm: &VirtualMachine) -> PyResult> { + self.check_thread(vm)?; + self._db_lock(vm) + } + + fn _db_lock(&self, vm: &VirtualMachine) -> PyResult> { + let guard = self.db.lock(); + if guard.is_some() { + Ok(PyMutexGuard::map(guard, |x| unsafe { + x.as_mut().unwrap_unchecked() + })) + } else { + Err(new_programming_error( + vm, + "Cannot operate on a closed database.".to_owned(), + )) + } + } + + #[pymethod] + fn cursor( + zelf: PyRef, + factory: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult> { + zelf.db_lock(vm).map(drop)?; + + let cursor = if let OptionalArg::Present(factory) = factory { + let cursor = factory.invoke((zelf.clone(),), vm)?; + let cursor = cursor.downcast::().map_err(|x| { + vm.new_type_error(format!("factory must return a cursor, not {}", x.class())) + })?; + unsafe { cursor.row_factory.swap(zelf.row_factory.to_owned()) }; + cursor + } else { + Cursor::new(zelf.clone(), zelf.row_factory.to_owned(), vm).into_ref(vm) + }; + Ok(cursor) + } + + #[pymethod] + fn blobopen( + zelf: PyRef, + args: BlobOpenArgs, + vm: &VirtualMachine, + ) -> PyResult> { + let table = args.table.to_cstring(vm)?; + let column = args.column.to_cstring(vm)?; + let name = args.name.to_cstring(vm)?; + + let db = zelf.db_lock(vm)?; + + let mut blob = null_mut(); + let ret = unsafe { + sqlite3_blob_open( + db.db, + name.as_ptr(), + table.as_ptr(), + column.as_ptr(), + args.row, + (!args.readonly) as c_int, + &mut blob, + ) + }; + db.check(ret, vm)?; + drop(db); + + let blob = SqliteBlob { blob }; + let blob = Blob { + connection: zelf, + inner: PyMutex::new(Some(BlobInner { blob, offset: 0 })), + }; + Ok(blob.into_ref(vm)) + } + + #[pymethod] + fn close(&self, vm: &VirtualMachine) -> PyResult<()> { + self.check_thread(vm)?; + self.db.lock().take(); + Ok(()) + } + + #[pymethod] + fn commit(&self, vm: &VirtualMachine) -> PyResult<()> { + self.db_lock(vm)?.implicity_commit(vm) + } + + #[pymethod] + fn rollback(&self, vm: &VirtualMachine) -> PyResult<()> { + let db = self.db_lock(vm)?; + if !db.is_autocommit() { + db._exec(b"ROLLBACK\0", vm) + } else { + Ok(()) + } + } + + #[pymethod] + fn execute( + zelf: PyRef, + sql: PyStrRef, + parameters: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult> { + let cursor = Cursor::new(zelf.clone(), zelf.row_factory.to_owned(), vm).into_ref(vm); + Cursor::execute(cursor, sql, parameters, vm) + } + + #[pymethod] + fn executemany( + zelf: PyRef, + sql: PyStrRef, + seq_of_params: ArgIterable, + vm: &VirtualMachine, + ) -> PyResult> { + let cursor = Cursor::new(zelf.clone(), zelf.row_factory.to_owned(), vm).into_ref(vm); + Cursor::executemany(cursor, sql, seq_of_params, vm) + } + + #[pymethod] + fn executescript( + zelf: PyRef, + script: PyStrRef, + vm: &VirtualMachine, + ) -> PyResult> { + Cursor::executescript( + Cursor::new(zelf.clone(), zelf.row_factory.to_owned(), vm).into_ref(vm), + script, + vm, + ) + } + + #[pymethod] + fn backup(zelf: PyRef, args: BackupArgs, vm: &VirtualMachine) -> PyResult<()> { + let BackupArgs { + target, + pages, + progress, + name, + sleep, + } = args; + if zelf.is(&target) { + return Err( + vm.new_value_error("target cannot be the same connection instance".to_owned()) + ); + } + + let pages = if pages == 0 { -1 } else { pages }; + + let name_cstring; + let name_ptr = if let Some(name) = &name { + name_cstring = name.to_cstring(vm)?; + name_cstring.as_ptr() + } else { + b"main\0".as_ptr().cast() + }; + + let sleep_ms = (sleep * 1000.0) as c_int; + + let db = zelf.db_lock(vm)?; + let target_db = target.db_lock(vm)?; + + let handle = unsafe { + sqlite3_backup_init(target_db.db, b"main\0".as_ptr().cast(), db.db, name_ptr) + }; + + if handle.is_null() { + return Err(target_db.error_extended(vm)); + } + + drop(db); + drop(target_db); + + loop { + let ret = unsafe { sqlite3_backup_step(handle, pages) }; + + if let Some(progress) = &progress { + let remaining = unsafe { sqlite3_backup_remaining(handle) }; + let pagecount = unsafe { sqlite3_backup_pagecount(handle) }; + if let Err(err) = progress.invoke((ret, remaining, pagecount), vm) { + unsafe { sqlite3_backup_finish(handle) }; + return Err(err); + } + } + + if ret == SQLITE_BUSY || ret == SQLITE_LOCKED { + unsafe { sqlite3_sleep(sleep_ms) }; + } else if ret != SQLITE_OK { + break; + } + } + + let ret = unsafe { sqlite3_backup_finish(handle) }; + if ret == SQLITE_OK { + Ok(()) + } else { + Err(target.db_lock(vm)?.error_extended(vm)) + } + } + + #[pymethod] + fn create_function(&self, args: CreateFunctionArgs, vm: &VirtualMachine) -> PyResult<()> { + let name = args.name.to_cstring(vm)?; + let flags = if args.deterministic { + SQLITE_UTF8 | SQLITE_DETERMINISTIC + } else { + SQLITE_UTF8 + }; + let db = self.db_lock(vm)?; + let Some(data) = CallbackData::new(args.func, vm) else { + return db.create_function(name.as_ptr(), args.narg, flags, null_mut(), None, None, None, None, vm); + }; + + db.create_function( + name.as_ptr(), + args.narg, + flags, + Box::into_raw(Box::new(data)).cast(), + Some(CallbackData::func_callback), + None, + None, + Some(CallbackData::destructor), + vm, + ) + } + + #[pymethod] + fn create_aggregate(&self, args: CreateAggregateArgs, vm: &VirtualMachine) -> PyResult<()> { + let name = args.name.to_cstring(vm)?; + let db = self.db_lock(vm)?; + let Some(data) = CallbackData::new(args.aggregate_class, vm) else { + return db.create_function(name.as_ptr(), args.narg, SQLITE_UTF8, null_mut(), None, None, None, None, vm); + }; + + db.create_function( + name.as_ptr(), + args.narg, + SQLITE_UTF8, + Box::into_raw(Box::new(data)).cast(), + None, + Some(CallbackData::step_callback), + Some(CallbackData::finalize_callback), + Some(CallbackData::destructor), + vm, + ) + } + + #[pymethod] + fn create_collation( + &self, + name: PyStrRef, + callable: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let name = name.to_cstring(vm)?; + let db = self.db_lock(vm)?; + let Some(data )= CallbackData::new(callable, vm) else { + unsafe { + sqlite3_create_collation_v2(db.db, name.as_ptr(), SQLITE_UTF8, null_mut(), None, None); + } + return Ok(()); + }; + let data = Box::into_raw(Box::new(data)); + + let ret = unsafe { + sqlite3_create_collation_v2( + db.db, + name.as_ptr(), + SQLITE_UTF8, + data.cast(), + Some(CallbackData::collation_callback), + Some(CallbackData::destructor), + ) + }; + + // TODO: replace with Result.inspect_err when stable + if let Err(exc) = db.check(ret, vm) { + // create_colation do not call destructor if error occur + unsafe { Box::from_raw(data) }; + Err(exc) + } else { + Ok(()) + } + } + + #[pymethod] + fn create_window_function( + &self, + name: PyStrRef, + narg: c_int, + aggregate_class: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + let name = name.to_cstring(vm)?; + let db = self.db_lock(vm)?; + let Some(data) = CallbackData::new(aggregate_class, vm) else { + unsafe { + sqlite3_create_window_function(db.db, name.as_ptr(), narg, SQLITE_UTF8, null_mut(), None, None, None, None, None) + }; + return Ok(()); + }; + + let ret = unsafe { + sqlite3_create_window_function( + db.db, + name.as_ptr(), + narg, + SQLITE_UTF8, + Box::into_raw(Box::new(data)).cast(), + Some(CallbackData::step_callback), + Some(CallbackData::finalize_callback), + Some(CallbackData::value_callback), + Some(CallbackData::inverse_callback), + Some(CallbackData::destructor), + ) + }; + db.check(ret, vm) + .map_err(|_| new_programming_error(vm, "Error creating window function".to_owned())) + } + + #[pymethod] + fn set_authorizer(&self, callable: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let db = self.db_lock(vm)?; + let Some(data) = CallbackData::new(callable, vm) else { + unsafe { sqlite3_set_authorizer(db.db, None, null_mut()) }; + return Ok(()); + }; + + let ret = unsafe { + sqlite3_set_authorizer( + db.db, + Some(CallbackData::authorizer_callback), + Box::into_raw(Box::new(data)).cast(), + ) + }; + db.check(ret, vm).map_err(|_| { + new_operational_error(vm, "Error setting authorizer callback".to_owned()) + }) + } + + #[pymethod] + fn set_trace_callback(&self, callable: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let db = self.db_lock(vm)?; + let Some(data )= CallbackData::new(callable, vm) else { + unsafe { + sqlite3_trace_v2(db.db, SQLITE_TRACE_STMT as u32, None, null_mut()) + }; + return Ok(()); + }; + + let ret = unsafe { + sqlite3_trace_v2( + db.db, + SQLITE_TRACE_STMT as u32, + Some(CallbackData::trace_callback), + Box::into_raw(Box::new(data)).cast(), + ) + }; + + db.check(ret, vm) + } + + #[pymethod] + fn set_progress_handler( + &self, + callable: PyObjectRef, + n: c_int, + vm: &VirtualMachine, + ) -> PyResult<()> { + let db = self.db_lock(vm)?; + let Some(data )= CallbackData::new(callable, vm) else { + unsafe { sqlite3_progress_handler(db.db, n, None, null_mut()) }; + return Ok(()); + }; + + unsafe { + sqlite3_progress_handler( + db.db, + n, + Some(CallbackData::progress_callback), + Box::into_raw(Box::new(data)).cast(), + ) + }; + + Ok(()) + } + + #[pymethod] + fn iterdump(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + let module = vm.import("sqlite3.dump", None, 0)?; + let func = module.get_attr("_iterdump", vm)?; + vm.invoke(&func, (zelf,)) + } + + #[pymethod] + fn interrupt(&self, vm: &VirtualMachine) -> PyResult<()> { + // DO NOT check thread safety + self._db_lock(vm).map(|x| x.interrupt()) + } + + #[pymethod] + fn getlimit(&self, category: c_int, vm: &VirtualMachine) -> PyResult { + self.db_lock(vm)?.limit(category, -1, vm) + } + + #[pymethod] + fn setlimit(&self, category: c_int, limit: c_int, vm: &VirtualMachine) -> PyResult { + self.db_lock(vm)?.limit(category, limit, vm) + } + + #[pymethod(magic)] + fn enter(zelf: PyRef) -> PyRef { + zelf + } + + #[pymethod(magic)] + fn exit( + &self, + cls: PyObjectRef, + exc: PyObjectRef, + tb: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult<()> { + if vm.is_none(&cls) && vm.is_none(&exc) && vm.is_none(&tb) { + self.commit(vm) + } else { + self.rollback(vm) + } + } + + #[pygetset] + fn isolation_level(&self) -> Option { + self.isolation_level.deref().map(|x| x.to_owned()) + } + #[pygetset(setter)] + fn set_isolation_level(&self, val: Option, vm: &VirtualMachine) -> PyResult<()> { + if let Some(val) = &val { + begin_statement_ptr_from_isolation_level(val, vm)?; + } + unsafe { self.isolation_level.swap(val) }; + Ok(()) + } + + #[pygetset] + fn text_factory(&self) -> PyObjectRef { + self.text_factory.to_owned() + } + #[pygetset(setter)] + fn set_text_factory(&self, val: PyObjectRef) { + unsafe { self.text_factory.swap(val) }; + } + + #[pygetset] + fn row_factory(&self) -> Option { + self.row_factory.to_owned() + } + #[pygetset(setter)] + fn set_row_factory(&self, val: Option) { + unsafe { self.row_factory.swap(val) }; + } + + fn check_thread(&self, vm: &VirtualMachine) -> PyResult<()> { + if self.check_same_thread && (std::thread::current().id() != self.thread_ident) { + Err(new_programming_error( + vm, + "SQLite objects created in a thread can only be used in that same thread." + .to_owned(), + )) + } else { + Ok(()) + } + } + + #[pygetset] + fn in_transaction(&self, vm: &VirtualMachine) -> PyResult { + self._db_lock(vm).map(|x| !x.is_autocommit()) + } + + #[pygetset] + fn total_changes(&self, vm: &VirtualMachine) -> PyResult { + self._db_lock(vm).map(|x| x.total_changes()) + } + } + + #[pyattr] + #[pyclass(name)] + #[derive(Debug, PyPayload)] + struct Cursor { + connection: PyRef, + arraysize: PyAtomic, + row_factory: PyAtomicRef>, + inner: PyMutex>, + } + + #[derive(Debug)] + struct CursorInner { + description: Option, + row_cast_map: Vec>, + lastrowid: i64, + rowcount: i64, + statement: Option>, + } + + #[pyclass(with(Constructor, IterNext), flags(BASETYPE))] + impl Cursor { + fn new( + connection: PyRef, + row_factory: Option, + _vm: &VirtualMachine, + ) -> Self { + Self { + connection, + arraysize: Radium::new(1), + row_factory: PyAtomicRef::from(row_factory), + inner: PyMutex::from(Some(CursorInner { + description: None, + row_cast_map: vec![], + lastrowid: -1, + rowcount: -1, + statement: None, + })), + } + } + + fn inner(&self, vm: &VirtualMachine) -> PyResult> { + let guard = self.inner.lock(); + if guard.is_some() { + Ok(PyMutexGuard::map(guard, |x| unsafe { + x.as_mut().unwrap_unchecked() + })) + } else { + Err(new_programming_error( + vm, + "Cannot operate on a closed cursor.".to_owned(), + )) + } + } + + #[pymethod] + fn execute( + zelf: PyRef, + sql: PyStrRef, + parameters: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult> { + let mut inner = zelf.inner(vm)?; + + if let Some(stmt) = inner.statement.take() { + stmt.lock().reset(); + } + + let Some(stmt) = Statement::new(&zelf.connection, &sql, vm)? else { + drop(inner); + return Ok(zelf); + }; + let stmt = stmt.into_ref(vm); + + inner.rowcount = if stmt.is_dml { 0 } else { -1 }; + + let db = zelf.connection.db_lock(vm)?; + + if stmt.is_dml && db.is_autocommit() { + db.begin_transaction( + zelf.connection + .isolation_level + .deref() + .map(|x| x.to_owned()), + vm, + )?; + } + + let st = stmt.lock(); + if let OptionalArg::Present(parameters) = parameters { + st.bind_parameters(¶meters, vm)?; + } + + let ret = st.step(); + + if ret != SQLITE_DONE && ret != SQLITE_ROW { + if let Some(exc) = unsafe { user_function_exception().swap(None) } { + return Err(exc); + } + return Err(db.error_extended(vm)); + } + + inner.row_cast_map = zelf.build_row_cast_map(&st, vm)?; + + inner.description = st.columns_description(vm)?; + + if ret == SQLITE_ROW { + drop(st); + inner.statement = Some(stmt); + } else { + st.reset(); + drop(st); + if stmt.is_dml { + inner.rowcount += db.changes() as i64; + } + } + + inner.lastrowid = db.lastrowid(); + + drop(inner); + drop(db); + Ok(zelf) + } + + #[pymethod] + fn executemany( + zelf: PyRef, + sql: PyStrRef, + seq_of_params: ArgIterable, + vm: &VirtualMachine, + ) -> PyResult> { + let mut inner = zelf.inner(vm)?; + + if let Some(stmt) = inner.statement.take() { + stmt.lock().reset(); + } + + let Some(stmt) = Statement::new(&zelf.connection, &sql, vm)? else { + drop(inner); + return Ok(zelf); + }; + let stmt = stmt.into_ref(vm); + + let st = stmt.lock(); + + if st.readonly() { + return Err(new_programming_error( + vm, + "executemany() can only execute DML statements.".to_owned(), + )); + } + + inner.description = st.columns_description(vm)?; + + inner.rowcount = if stmt.is_dml { 0 } else { -1 }; + + let db = zelf.connection.db_lock(vm)?; + + if stmt.is_dml && db.is_autocommit() { + db.begin_transaction( + zelf.connection + .isolation_level + .deref() + .map(|x| x.to_owned()), + vm, + )?; + } + + let iter = seq_of_params.iter(vm)?; + for params in iter { + let params = params?; + st.bind_parameters(¶ms, vm)?; + + if !st.step_row_else_done(vm)? { + if stmt.is_dml { + inner.rowcount += db.changes() as i64; + } + st.reset(); + } + + // if let Some(exc) = unsafe { user_function_exception().swap(None) } { + // return Err(exc); + // } + } + + if st.busy() { + drop(st); + inner.statement = Some(stmt); + } + + drop(inner); + drop(db); + Ok(zelf) + } + + #[pymethod] + fn executescript( + zelf: PyRef, + script: PyStrRef, + vm: &VirtualMachine, + ) -> PyResult> { + let db = zelf.connection.db_lock(vm)?; + + db.sql_limit(script.byte_len(), vm)?; + + db.implicity_commit(vm)?; + + let script = script.to_cstring(vm)?; + let mut ptr = script.as_ptr(); + + while let Some(st) = db.prepare(ptr, &mut ptr, vm)? { + while st.step_row_else_done(vm)? {} + } + + drop(db); + Ok(zelf) + } + + #[pymethod] + fn fetchone(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Self::next(&zelf, vm).map(|x| match x { + PyIterReturn::Return(row) => row, + PyIterReturn::StopIteration(_) => vm.ctx.none(), + }) + } + + #[pymethod] + fn fetchmany( + zelf: PyRef, + max_rows: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult> { + let max_rows = max_rows.unwrap_or_else(|| zelf.arraysize.load(Ordering::Relaxed)); + let mut list = vec![]; + while let PyIterReturn::Return(row) = Self::next(&zelf, vm)? { + list.push(row); + if list.len() as c_int >= max_rows { + break; + } + } + Ok(list) + } + + #[pymethod] + fn fetchall(zelf: PyRef, vm: &VirtualMachine) -> PyResult> { + let mut list = vec![]; + while let PyIterReturn::Return(row) = Self::next(&zelf, vm)? { + list.push(row); + } + Ok(list) + } + + #[pymethod] + fn close(&self) { + if let Some(inner) = self.inner.lock().take() { + if let Some(stmt) = inner.statement { + stmt.lock().reset(); + } + } + } + + #[pymethod] + fn setinputsizes(&self, _sizes: PyObjectRef) {} + #[pymethod] + fn setoutputsize(&self, _size: PyObjectRef, _column: OptionalArg) {} + + #[pygetset] + fn connection(&self) -> PyRef { + self.connection.clone() + } + + #[pygetset] + fn lastrowid(&self, vm: &VirtualMachine) -> PyResult { + self.inner(vm).map(|x| x.lastrowid) + } + + #[pygetset] + fn rowcount(&self, vm: &VirtualMachine) -> PyResult { + self.inner(vm).map(|x| x.rowcount) + } + + #[pygetset] + fn description(&self, vm: &VirtualMachine) -> PyResult> { + self.inner(vm).map(|x| x.description.clone()) + } + + #[pygetset] + fn arraysize(&self) -> c_int { + self.arraysize.load(Ordering::Relaxed) + } + #[pygetset(setter)] + fn set_arraysize(&self, val: c_int) { + self.arraysize.store(val, Ordering::Relaxed); + } + + fn build_row_cast_map( + &self, + st: &SqliteStatementRaw, + vm: &VirtualMachine, + ) -> PyResult>> { + if self.connection.detect_types == 0 { + return Ok(vec![]); + } + + let mut cast_map = vec![]; + let num_cols = st.column_count(); + + for i in 0..num_cols { + if self.connection.detect_types & PARSE_COLNAMES != 0 { + let col_name = st.column_name(i); + let col_name = ptr_to_str(col_name, vm)?; + let col_name = col_name + .chars() + .skip_while(|&x| x != '[') + .skip(1) + .take_while(|&x| x != ']') + .flat_map(|x| x.to_uppercase()) + .collect::(); + if let Some(converter) = converters().get_item_opt(&col_name, vm)? { + cast_map.push(Some(converter.clone())); + continue; + } + } + if self.connection.detect_types & PARSE_DECLTYPES != 0 { + let decltype = st.column_decltype(i); + let decltype = ptr_to_str(decltype, vm)?; + if let Some(decltype) = decltype.split_terminator(&[' ', '(']).next() { + let decltype = decltype.to_uppercase(); + if let Some(converter) = converters().get_item_opt(&decltype, vm)? { + cast_map.push(Some(converter.clone())); + continue; + } + } + } + cast_map.push(None); + } + + Ok(cast_map) + } + } + + impl Constructor for Cursor { + type Args = (PyRef,); + + fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult { + Self::new(args.0, None, vm) + .into_ref_with_type(vm, cls) + .map(Into::into) + } + } + + impl IterNextIterable for Cursor {} + impl IterNext for Cursor { + fn next(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let mut inner = zelf.inner(vm)?; + let Some(stmt) = &inner.statement else { + return Ok(PyIterReturn::StopIteration(None)); + }; + let st = stmt.lock(); + let db = zelf.connection.db_lock(vm)?; + // fetch_one_row + + let num_cols = st.data_count(); + + let mut row = Vec::with_capacity(num_cols as usize); + + for i in 0..num_cols { + let val = if let Some(converter) = + inner.row_cast_map.get(i as usize).cloned().flatten() + { + let blob = st.column_blob(i); + if blob.is_null() { + vm.ctx.none() + } else { + let nbytes = st.column_bytes(i); + let blob = unsafe { + std::slice::from_raw_parts(blob.cast::(), nbytes as usize) + }; + let blob = vm.ctx.new_bytes(blob.to_vec()); + vm.invoke(&converter, (blob,))? + } + } else { + let col_type = st.column_type(i); + match col_type { + SQLITE_NULL => vm.ctx.none(), + SQLITE_INTEGER => vm.ctx.new_int(st.column_int(i)).into(), + SQLITE_FLOAT => vm.ctx.new_float(st.column_double(i)).into(), + SQLITE_TEXT => { + let text = + ptr_to_vec(st.column_text(i), st.column_bytes(i), db.db, vm)?; + + let text_factory = zelf.connection.text_factory.to_owned(); + + if text_factory.is(PyStr::class(vm)) { + let text = String::from_utf8(text).map_err(|_| { + new_operational_error(vm, "not valid UTF-8".to_owned()) + })?; + vm.ctx.new_str(text).into() + } else if text_factory.is(PyBytes::class(vm)) { + vm.ctx.new_bytes(text).into() + } else if text_factory.is(PyByteArray::class(vm)) { + PyByteArray::from(text).into_ref(vm).into() + } else { + let bytes = vm.ctx.new_bytes(text); + vm.invoke(&text_factory, (bytes,))? + } + } + SQLITE_BLOB => { + let blob = ptr_to_vec( + st.column_blob(i).cast(), + st.column_bytes(i), + db.db, + vm, + )?; + + vm.ctx.new_bytes(blob).into() + } + _ => { + return Err(vm.new_not_implemented_error(format!( + "unknown column type: {}", + col_type + ))); + } + } + }; + + row.push(val); + } + + if !st.step_row_else_done(vm)? { + st.reset(); + drop(st); + if stmt.is_dml { + inner.rowcount = db.changes() as i64; + } + inner.statement = None; + } else { + drop(st); + } + + drop(db); + drop(inner); + + let row = vm.ctx.new_tuple(row); + + if let Some(row_factory) = zelf.row_factory.to_owned() { + vm.invoke(&row_factory, (zelf.to_owned(), row)) + .map(PyIterReturn::Return) + } else { + Ok(PyIterReturn::Return(row.into())) + } + } + } + + #[pyattr] + #[pyclass(name)] + #[derive(Debug, PyPayload)] + struct Row { + data: PyTupleRef, + description: PyTupleRef, + } + + #[pyclass( + with(Constructor, Hashable, Comparable, Iterable, AsMapping, AsSequence), + flags(BASETYPE) + )] + impl Row { + #[pymethod] + fn keys(&self, _vm: &VirtualMachine) -> PyResult> { + Ok(self + .description + .iter() + .map(|x| x.payload::().unwrap().as_slice()[0].clone()) + .collect()) + } + + fn subscript(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { + if let Some(i) = needle.payload::() { + let i = i.try_to_primitive::(vm)?; + self.data.getitem_by_index(vm, i) + } else if let Some(name) = needle.payload::() { + for (obj, i) in self.description.iter().zip(0..) { + let obj = &obj.payload::().unwrap().as_slice()[0]; + let Some(obj) = obj.payload::() else { break; }; + let a_iter = name.as_str().chars().flat_map(|x| x.to_uppercase()); + let b_iter = obj.as_str().chars().flat_map(|x| x.to_uppercase()); + + if a_iter.eq(b_iter) { + return self.data.getitem_by_index(vm, i); + } + } + Err(vm.new_index_error("No item with that key".to_owned())) + } else if let Some(slice) = needle.payload::() { + let list = self.data.getitem_by_slice(vm, slice.to_saturated(vm)?)?; + Ok(vm.ctx.new_tuple(list).into()) + } else { + Err(vm.new_index_error("Index must be int or string".to_owned())) + } + } + } + + impl Constructor for Row { + type Args = (PyRef, PyTupleRef); + + fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult { + let description = args + .0 + .inner(vm)? + .description + .clone() + .ok_or_else(|| vm.new_value_error("no description in Cursor".to_owned()))?; + + Self { + data: args.1, + description, + } + .into_ref_with_type(vm, cls) + .map(Into::into) + } + } + + impl Hashable for Row { + fn hash(zelf: &Py, vm: &VirtualMachine) -> PyResult { + Ok(zelf.description.as_object().hash(vm)? | zelf.data.as_object().hash(vm)?) + } + } + + impl Comparable for Row { + fn cmp( + zelf: &Py, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult { + op.eq_only(|| { + if let Some(other) = other.payload::() { + let eq = vm + .bool_eq(zelf.description.as_object(), other.description.as_object())? + && vm.bool_eq(zelf.data.as_object(), other.data.as_object())?; + Ok(eq.into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } + }) + } + } + + impl Iterable for Row { + fn iter(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Iterable::iter(zelf.data.clone(), vm) + } + } + + impl AsMapping for Row { + fn as_mapping() -> &'static PyMappingMethods { + static AS_MAPPING: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| PyMappingMethods { + length: atomic_func!(|mapping, _vm| Ok(Row::mapping_downcast(mapping) + .data + .len())), + subscript: atomic_func!(|mapping, needle, vm| { + Row::mapping_downcast(mapping).subscript(needle, vm) + }), + ..PyMappingMethods::NOT_IMPLEMENTED + }); + &AS_MAPPING + } + } + + impl AsSequence for Row { + fn as_sequence() -> &'static PySequenceMethods { + static AS_SEQUENCE: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(|| PySequenceMethods { + length: atomic_func!(|seq, _vm| Ok(Row::sequence_downcast(seq).data.len())), + item: atomic_func!(|seq, i, vm| Row::sequence_downcast(seq) + .data + .getitem_by_index(vm, i)), + ..PySequenceMethods::NOT_IMPLEMENTED + }); + &AS_SEQUENCE + } + } + + #[pyattr] + #[pyclass(name)] + #[derive(Debug, PyPayload)] + struct Blob { + connection: PyRef, + inner: PyMutex>, + } + + #[derive(Debug)] + struct BlobInner { + blob: SqliteBlob, + offset: c_int, + } + + impl Drop for BlobInner { + fn drop(&mut self) { + unsafe { sqlite3_blob_close(self.blob.blob) }; + } + } + + #[pyclass(with(AsMapping))] + impl Blob { + #[pymethod] + fn close(&self) { + self.inner.lock().take(); + } + + #[pymethod] + fn read( + &self, + length: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult> { + let mut length = length.unwrap_or(-1); + let mut inner = self.inner(vm)?; + let blob_len = inner.blob.bytes(); + let max_read = blob_len - inner.offset; + + if length < 0 || length > max_read { + length = max_read; + } + + if length == 0 { + Ok(vm.ctx.empty_bytes.clone()) + } else { + let mut buf = Vec::::with_capacity(length as usize); + let ret = inner + .blob + .read(buf.as_mut_ptr().cast(), length, inner.offset); + self.check(ret, vm)?; + unsafe { buf.set_len(length as usize) }; + inner.offset += length; + Ok(vm.ctx.new_bytes(buf)) + } + } + + #[pymethod] + fn write(&self, data: PyBuffer, vm: &VirtualMachine) -> PyResult<()> { + let mut inner = self.inner(vm)?; + let blob_len = inner.blob.bytes(); + let length = Self::expect_write(blob_len, data.desc.len, inner.offset, vm)?; + + let ret = data.contiguous_or_collect(|buf| { + inner.blob.write(buf.as_ptr().cast(), length, inner.offset) + }); + + self.check(ret, vm)?; + inner.offset += length; + Ok(()) + } + + #[pymethod] + fn tell(&self, vm: &VirtualMachine) -> PyResult { + self.inner(vm).map(|x| x.offset) + } + + #[pymethod] + fn seek( + &self, + mut offset: c_int, + origin: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult<()> { + let origin = origin.unwrap_or(libc::SEEK_SET); + let mut inner = self.inner(vm)?; + let blob_len = inner.blob.bytes(); + + let overflow_err = + || vm.new_overflow_error("seek offset results in overflow".to_owned()); + + match origin { + libc::SEEK_SET => {} + libc::SEEK_CUR => { + offset = offset.checked_add(inner.offset).ok_or_else(overflow_err)? + } + libc::SEEK_END => offset = offset.checked_add(blob_len).ok_or_else(overflow_err)?, + _ => { + return Err(vm.new_value_error( + "'origin' should be os.SEEK_SET, os.SEEK_CUR, or os.SEEK_END".to_owned(), + )) + } + } + + if offset < 0 || offset > blob_len { + Err(vm.new_value_error("offset out of blob range".to_owned())) + } else { + inner.offset = offset; + Ok(()) + } + } + + #[pymethod(magic)] + fn enter(zelf: PyRef) -> PyRef { + zelf + } + + #[pymethod(magic)] + fn exit(&self, _args: FuncArgs) { + self.close() + } + + fn inner(&self, vm: &VirtualMachine) -> PyResult> { + let guard = self.inner.lock(); + if guard.is_some() { + Ok(PyMutexGuard::map(guard, |x| unsafe { + x.as_mut().unwrap_unchecked() + })) + } else { + Err(new_programming_error( + vm, + "Cannot operate on a closed blob.".to_owned(), + )) + } + } + + fn wrapped_index(index: PyIntRef, length: c_int, vm: &VirtualMachine) -> PyResult { + let mut index = index.try_to_primitive::(vm)?; + if index < 0 { + index += length; + } + if index < 0 || index >= length { + Err(vm.new_index_error("Blob index out of range".to_owned())) + } else { + Ok(index) + } + } + + fn expect_write( + blob_len: c_int, + length: usize, + offset: c_int, + vm: &VirtualMachine, + ) -> PyResult { + let max_write = blob_len - offset; + if length <= max_write as usize { + Ok(length as c_int) + } else { + Err(vm.new_value_error("data longer than blob length".to_owned())) + } + } + + fn subscript(&self, needle: &PyObject, vm: &VirtualMachine) -> PyResult { + let inner = self.inner(vm)?; + if let Some(index) = needle.try_index_opt(vm) { + let blob_len = inner.blob.bytes(); + let index = Self::wrapped_index(index?, blob_len, vm)?; + let mut byte: u8 = 0; + let ret = inner.blob.read_single(&mut byte, index); + self.check(ret, vm).map(|_| vm.ctx.new_int(byte).into()) + } else if let Some(slice) = needle.payload::() { + let blob_len = inner.blob.bytes(); + let slice = slice.to_saturated(vm)?; + let (range, step, length) = slice.adjust_indices(blob_len as usize); + let mut buf = Vec::::with_capacity(length); + + if step == 1 { + let ret = inner.blob.read( + buf.as_mut_ptr().cast(), + length as c_int, + range.start as c_int, + ); + self.check(ret, vm)?; + unsafe { buf.set_len(length) }; + } else { + let iter = SaturatedSliceIter::from_adjust_indices(range, step, length); + let mut byte: u8 = 0; + for index in iter { + let ret = inner.blob.read_single(&mut byte, index as c_int); + self.check(ret, vm)?; + buf.push(byte); + } + } + Ok(vm.ctx.new_bytes(buf).into()) + } else { + Err(vm.new_type_error("Blob indices must be integers".to_owned())) + } + } + + fn ass_subscript( + &self, + needle: &PyObject, + value: Option, + vm: &VirtualMachine, + ) -> PyResult<()> { + let Some(value) = value else { + return Err(vm.new_type_error("Blob doesn't support deletion".to_owned())); + }; + let inner = self.inner(vm)?; + + if let Some(index) = needle.try_index_opt(vm) { + let Some(value) = value.payload::() else { + return Err(vm.new_type_error(format!("'{}' object cannot be interpreted as an integer", value.class()))); + }; + let value = value.try_to_primitive::(vm)?; + let blob_len = inner.blob.bytes(); + let index = Self::wrapped_index(index?, blob_len, vm)?; + Self::expect_write(blob_len, 1, index, vm)?; + let ret = inner.blob.write_single(value, index); + self.check(ret, vm) + } else if let Some(_slice) = needle.payload::() { + Err(vm + .new_not_implemented_error("Blob slice assigment is not implmented".to_owned())) + // let blob_len = inner.blob.bytes(); + // let slice = slice.to_saturated(vm)?; + // let (range, step, length) = slice.adjust_indices(blob_len as usize); + } else { + Err(vm.new_type_error("Blob indices must be integers".to_owned())) + } + } + + fn check(&self, ret: c_int, vm: &VirtualMachine) -> PyResult<()> { + if ret == SQLITE_OK { + Ok(()) + } else { + Err(self.connection.db_lock(vm)?.error_extended(vm)) + } + } + } + + impl AsMapping for Blob { + fn as_mapping() -> &'static PyMappingMethods { + static AS_MAPPING: PyMappingMethods = PyMappingMethods { + length: atomic_func!(|mapping, vm| Blob::mapping_downcast(mapping) + .inner(vm) + .map(|x| x.blob.bytes() as usize)), + subscript: atomic_func!(|mapping, needle, vm| { + Blob::mapping_downcast(mapping).subscript(needle, vm) + }), + ass_subscript: atomic_func!(|mapping, needle, value, vm| { + Blob::mapping_downcast(mapping).ass_subscript(needle, value, vm) + }), + }; + &AS_MAPPING + } + } + + #[pyattr] + #[pyclass(name)] + #[derive(Debug, PyPayload)] + struct PrepareProtocol {} + + #[pyclass()] + impl PrepareProtocol {} + + #[pyattr] + #[pyclass(name)] + #[derive(PyPayload)] + struct Statement { + st: PyMutex, + pub is_dml: bool, + } + + impl Debug for Statement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{} Statement", + if self.is_dml { "DML" } else { "Non-DML" } + ) + } + } + + #[pyclass()] + impl Statement { + fn new( + connection: &Connection, + sql: &PyStr, + vm: &VirtualMachine, + ) -> PyResult> { + let sql_cstr = sql.to_cstring(vm)?; + let sql_len = sql.byte_len() + 1; + + let db = connection.db_lock(vm)?; + + db.sql_limit(sql_len, vm)?; + + let mut tail = null(); + let st = db.prepare(sql_cstr.as_ptr(), &mut tail, vm)?; + + let Some(st) = st else { + return Ok(None); + }; + + let tail = unsafe { CStr::from_ptr(tail) }; + let tail = tail.to_bytes(); + if lstrip_sql(tail).is_some() { + return Err(new_programming_error( + vm, + "You can only execute one statement at a time.".to_owned(), + )); + } + + let is_dml = if let Some(head) = lstrip_sql(sql_cstr.as_bytes()) { + head.len() >= 6 + && (head[..6].eq_ignore_ascii_case(b"insert") + || head[..6].eq_ignore_ascii_case(b"update") + || head[..6].eq_ignore_ascii_case(b"delete") + || (head.len() >= 7 && head[..7].eq_ignore_ascii_case(b"replace"))) + } else { + false + }; + + Ok(Some(Self { + st: PyMutex::from(st), + is_dml, + })) + } + + fn lock(&self) -> PyMutexGuard { + self.st.lock() + } + } + + struct Sqlite { + raw: SqliteRaw, + } + + impl From for Sqlite { + fn from(raw: SqliteRaw) -> Self { + Self { raw } + } + } + + impl Drop for Sqlite { + fn drop(&mut self) { + unsafe { sqlite3_close_v2(self.raw.db) }; + } + } + + impl Deref for Sqlite { + type Target = SqliteRaw; + + fn deref(&self) -> &Self::Target { + &self.raw + } + } + + #[derive(Copy, Clone)] + struct SqliteRaw { + db: *mut sqlite3, + } + + cfg_if::cfg_if! { + if #[cfg(feature = "threading")] { + unsafe impl Send for SqliteStatement {} + // unsafe impl Sync for SqliteStatement {} + unsafe impl Send for Sqlite {} + // unsafe impl Sync for Sqlite {} + unsafe impl Send for SqliteBlob {} + } + } + + impl From for SqliteRaw { + fn from(stmt: SqliteStatementRaw) -> Self { + unsafe { + Self { + db: sqlite3_db_handle(stmt.st), + } + } + } + } + + impl SqliteRaw { + fn check(self, ret: c_int, vm: &VirtualMachine) -> PyResult<()> { + if ret == SQLITE_OK { + Ok(()) + } else { + Err(self.error_extended(vm)) + } + } + + fn error_extended(self, vm: &VirtualMachine) -> PyBaseExceptionRef { + let errcode = unsafe { sqlite3_errcode(self.db) }; + let typ = exception_type_from_errcode(errcode, vm); + let extended_errcode = unsafe { sqlite3_extended_errcode(self.db) }; + let errmsg = unsafe { sqlite3_errmsg(self.db) }; + let errmsg = unsafe { CStr::from_ptr(errmsg) }; + let errmsg = errmsg.to_str().unwrap().to_owned(); + + raise_exception(typ.to_owned(), extended_errcode, errmsg, vm) + } + + fn open(path: *const i8, uri: bool, vm: &VirtualMachine) -> PyResult { + let mut db = null_mut(); + let ret = unsafe { + sqlite3_open_v2( + path, + addr_of_mut!(db), + SQLITE_OPEN_READWRITE + | SQLITE_OPEN_CREATE + | if uri { SQLITE_OPEN_URI } else { 0 }, + null(), + ) + }; + let zelf = Self { db }; + zelf.check(ret, vm).map(|_| zelf) + } + + fn _exec(self, sql: &[u8], vm: &VirtualMachine) -> PyResult<()> { + let ret = + unsafe { sqlite3_exec(self.db, sql.as_ptr().cast(), None, null_mut(), null_mut()) }; + self.check(ret, vm) + } + + fn prepare( + self, + sql: *const i8, + tail: *mut *const i8, + vm: &VirtualMachine, + ) -> PyResult> { + let mut st = null_mut(); + let ret = unsafe { sqlite3_prepare_v2(self.db, sql, -1, &mut st, tail) }; + self.check(ret, vm)?; + if st.is_null() { + Ok(None) + } else { + Ok(Some(SqliteStatement::from(SqliteStatementRaw::from(st)))) + } + } + + fn limit(self, category: c_int, limit: c_int, vm: &VirtualMachine) -> PyResult { + let old_limit = unsafe { sqlite3_limit(self.db, category, limit) }; + if old_limit >= 0 { + Ok(old_limit) + } else { + Err(new_programming_error( + vm, + "'category' is out of bounds".to_owned(), + )) + } + } + + fn sql_limit(self, len: usize, vm: &VirtualMachine) -> PyResult<()> { + if len <= unsafe { sqlite3_limit(self.db, SQLITE_LIMIT_SQL_LENGTH, -1) } as usize { + Ok(()) + } else { + Err(new_data_error(vm, "query string is too large".to_owned())) + } + } + + fn is_autocommit(self) -> bool { + unsafe { sqlite3_get_autocommit(self.db) != 0 } + } + + fn changes(self) -> c_int { + unsafe { sqlite3_changes(self.db) } + } + + fn total_changes(self) -> c_int { + unsafe { sqlite3_total_changes(self.db) } + } + + fn lastrowid(self) -> c_longlong { + unsafe { sqlite3_last_insert_rowid(self.db) } + } + + fn implicity_commit(self, vm: &VirtualMachine) -> PyResult<()> { + if self.is_autocommit() { + Ok(()) + } else { + self._exec(b"COMMIT\0", vm) + } + } + + fn begin_transaction( + self, + isolation_level: Option, + vm: &VirtualMachine, + ) -> PyResult<()> { + let Some(isolation_level) = isolation_level else { + return Ok(()); + }; + let mut s = Vec::with_capacity(16); + s.extend(b"BEGIN "); + s.extend(isolation_level.as_str().bytes()); + s.push(b'\0'); + self._exec(&s, vm) + } + + fn interrupt(self) { + unsafe { sqlite3_interrupt(self.db) } + } + + fn busy_timeout(self, timeout: i32) { + unsafe { sqlite3_busy_timeout(self.db, timeout) }; + } + + #[allow(clippy::too_many_arguments)] + fn create_function( + self, + name: *const i8, + narg: c_int, + flags: c_int, + data: *mut c_void, + func: Option< + unsafe extern "C" fn( + arg1: *mut sqlite3_context, + arg2: c_int, + arg3: *mut *mut sqlite3_value, + ), + >, + step: Option< + unsafe extern "C" fn( + arg1: *mut sqlite3_context, + arg2: c_int, + arg3: *mut *mut sqlite3_value, + ), + >, + finalize: Option, + destroy: Option, + vm: &VirtualMachine, + ) -> PyResult<()> { + let ret = unsafe { + sqlite3_create_function_v2( + self.db, name, narg, flags, data, func, step, finalize, destroy, + ) + }; + self.check(ret, vm) + .map_err(|_| new_operational_error(vm, "Error creating function".to_owned())) + } + } + + struct SqliteStatement { + raw: SqliteStatementRaw, + } + + impl From for SqliteStatement { + fn from(raw: SqliteStatementRaw) -> Self { + Self { raw } + } + } + + impl Drop for SqliteStatement { + fn drop(&mut self) { + unsafe { + sqlite3_finalize(self.raw.st); + } + } + } + + impl Deref for SqliteStatement { + type Target = SqliteStatementRaw; + + fn deref(&self) -> &Self::Target { + &self.raw + } + } + + #[derive(Copy, Clone)] + struct SqliteStatementRaw { + st: *mut sqlite3_stmt, + } + + impl From<*mut sqlite3_stmt> for SqliteStatementRaw { + fn from(st: *mut sqlite3_stmt) -> Self { + SqliteStatementRaw { st } + } + } + + impl SqliteStatementRaw { + fn step(self) -> c_int { + unsafe { sqlite3_step(self.st) } + } + + fn step_row_else_done(self, vm: &VirtualMachine) -> PyResult { + let ret = self.step(); + + if let Some(exc) = unsafe { user_function_exception().swap(None) } { + Err(exc) + } else if ret == SQLITE_ROW { + Ok(true) + } else if ret == SQLITE_DONE { + Ok(false) + } else { + Err(SqliteRaw::from(self).error_extended(vm)) + } + } + + fn reset(self) { + unsafe { sqlite3_reset(self.st) }; + } + + fn data_count(self) -> c_int { + unsafe { sqlite3_data_count(self.st) } + } + + fn bind_parameter( + self, + pos: c_int, + parameter: &PyObject, + vm: &VirtualMachine, + ) -> PyResult<()> { + let adapted; + let obj = if need_adapt(parameter, vm) { + adapted = _adapt( + parameter, + PrepareProtocol::class(vm).to_owned(), + |x| Ok(x.to_owned()), + vm, + )?; + &adapted + } else { + parameter + }; + + let ret = if vm.is_none(obj) { + unsafe { sqlite3_bind_null(self.st, pos) } + } else if let Some(val) = obj.payload::() { + let val = val.try_to_primitive::(vm)?; + unsafe { sqlite3_bind_int64(self.st, pos, val) } + } else if let Some(val) = obj.payload::() { + let val = val.to_f64(); + unsafe { sqlite3_bind_double(self.st, pos, val) } + } else if let Some(val) = obj.payload::() { + let (ptr, len) = str_to_ptr_len(val, vm)?; + unsafe { sqlite3_bind_text(self.st, pos, ptr, len, SQLITE_TRANSIENT()) } + } else if let Ok(buffer) = PyBuffer::try_from_borrowed_object(vm, obj) { + let (ptr, len) = buffer_to_ptr_len(&buffer, vm)?; + unsafe { sqlite3_bind_blob(self.st, pos, ptr, len, SQLITE_TRANSIENT()) } + } else { + return Err(new_programming_error( + vm, + format!( + "Error binding parameter {}: type '{}' is not supported", + pos, + obj.class() + ), + )); + }; + + if ret == SQLITE_OK { + Ok(()) + } else { + let db = SqliteRaw::from(self); + db.check(ret, vm) + } + } + + fn bind_parameters(self, parameters: &PyObject, vm: &VirtualMachine) -> PyResult<()> { + if let Some(dict) = parameters.downcast_ref::() { + self.bind_parameters_name(dict, vm) + } else if let Ok(seq) = PySequence::try_protocol(parameters, vm) { + self.bind_parameters_sequence(seq, vm) + } else { + Err(new_programming_error( + vm, + "parameters are of unsupported type".to_owned(), + )) + } + } + + fn bind_parameters_name(self, dict: &Py, vm: &VirtualMachine) -> PyResult<()> { + let num_needed = unsafe { sqlite3_bind_parameter_count(self.st) }; + + for i in 1..=num_needed { + let name = unsafe { sqlite3_bind_parameter_name(self.st, i) }; + if name.is_null() { + return Err(new_programming_error(vm, "Binding {} has no name, but you supplied a dictionary (which has only names).".to_owned())); + } + let name = unsafe { name.add(1) }; + let name = ptr_to_str(name, vm)?; + + let val = dict.get_item(name, vm)?; + + self.bind_parameter(i, &val, vm)?; + } + Ok(()) + } + + fn bind_parameters_sequence(self, seq: PySequence, vm: &VirtualMachine) -> PyResult<()> { + let num_needed = unsafe { sqlite3_bind_parameter_count(self.st) }; + if seq.length(vm)? != num_needed as usize { + return Err(new_programming_error( + vm, + "Incorrect number of binding supplied".to_owned(), + )); + } + + for i in 1..=num_needed { + let val = seq.get_item(i as isize - 1, vm)?; + self.bind_parameter(i, &val, vm)?; + } + Ok(()) + } + + fn column_count(self) -> c_int { + unsafe { sqlite3_column_count(self.st) } + } + + fn column_type(self, pos: c_int) -> c_int { + unsafe { sqlite3_column_type(self.st, pos) } + } + + fn column_int(self, pos: c_int) -> i64 { + unsafe { sqlite3_column_int64(self.st, pos) } + } + + fn column_double(self, pos: c_int) -> f64 { + unsafe { sqlite3_column_double(self.st, pos) } + } + + fn column_blob(self, pos: c_int) -> *const c_void { + unsafe { sqlite3_column_blob(self.st, pos) } + } + + fn column_text(self, pos: c_int) -> *const u8 { + unsafe { sqlite3_column_text(self.st, pos) } + } + + fn column_decltype(self, pos: c_int) -> *const i8 { + unsafe { sqlite3_column_decltype(self.st, pos) } + } + + fn column_bytes(self, pos: c_int) -> c_int { + unsafe { sqlite3_column_bytes(self.st, pos) } + } + + fn column_name(self, pos: c_int) -> *const i8 { + unsafe { sqlite3_column_name(self.st, pos) } + } + + fn columns_name(self, vm: &VirtualMachine) -> PyResult> { + let count = self.column_count(); + (0..count) + .map(|i| { + let name = self.column_name(i); + ptr_to_str(name, vm).map(|x| vm.ctx.new_str(x)) + }) + .collect() + } + + fn columns_description(self, vm: &VirtualMachine) -> PyResult> { + if self.column_count() == 0 { + return Ok(None); + } + let columns = self + .columns_name(vm)? + .into_iter() + .map(|s| { + vm.ctx + .new_tuple(vec![ + s.into(), + vm.ctx.none(), + vm.ctx.none(), + vm.ctx.none(), + vm.ctx.none(), + vm.ctx.none(), + vm.ctx.none(), + ]) + .into() + }) + .collect(); + Ok(Some(vm.ctx.new_tuple(columns))) + } + + fn busy(self) -> bool { + unsafe { sqlite3_stmt_busy(self.st) != 0 } + } + + fn readonly(self) -> bool { + unsafe { sqlite3_stmt_readonly(self.st) != 0 } + } + } + + #[derive(Debug, Copy, Clone)] + struct SqliteBlob { + blob: *mut sqlite3_blob, + } + + impl SqliteBlob { + fn bytes(self) -> c_int { + unsafe { sqlite3_blob_bytes(self.blob) } + } + + fn write(self, buf: *const c_void, length: c_int, offset: c_int) -> c_int { + unsafe { sqlite3_blob_write(self.blob, buf, length, offset) } + } + + fn read(self, buf: *mut c_void, length: c_int, offset: c_int) -> c_int { + unsafe { sqlite3_blob_read(self.blob, buf, length, offset) } + } + + fn read_single(self, byte: &mut u8, offset: c_int) -> c_int { + self.read(byte as *mut u8 as *mut _, 1, offset) + } + + fn write_single(self, byte: u8, offset: c_int) -> c_int { + self.write(&byte as *const u8 as *const _, 1, offset) + } + } + + #[derive(Copy, Clone)] + struct SqliteContext { + ctx: *mut sqlite3_context, + } + + impl From<*mut sqlite3_context> for SqliteContext { + fn from(ctx: *mut sqlite3_context) -> Self { + Self { ctx } + } + } + + impl SqliteContext { + fn user_data(self) -> *mut T { + unsafe { sqlite3_user_data(self.ctx).cast() } + } + + fn aggregate_context(self) -> *mut T { + unsafe { sqlite3_aggregate_context(self.ctx, std::mem::size_of::() as c_int).cast() } + } + + fn result_exception(self, vm: &VirtualMachine, exc: PyBaseExceptionRef, msg: &str) { + if exc.fast_isinstance(vm.ctx.exceptions.memory_error) { + unsafe { sqlite3_result_error_nomem(self.ctx) } + } else if exc.fast_isinstance(vm.ctx.exceptions.overflow_error) { + unsafe { sqlite3_result_error_toobig(self.ctx) } + } else { + unsafe { sqlite3_result_error(self.ctx, msg.as_ptr().cast(), -1) } + } + if enable_traceback().load(Ordering::Relaxed) { + vm.print_exception(exc); + } + } + + fn db_handle(self) -> *mut sqlite3 { + unsafe { sqlite3_context_db_handle(self.ctx) } + } + + fn result_from_object(self, val: &PyObject, vm: &VirtualMachine) -> PyResult<()> { + unsafe { + if vm.is_none(val) { + sqlite3_result_null(self.ctx) + } else if let Some(val) = val.payload::() { + sqlite3_result_int64(self.ctx, val.try_to_primitive(vm)?) + } else if let Some(val) = val.payload::() { + sqlite3_result_double(self.ctx, val.to_f64()) + } else if let Some(val) = val.payload::() { + let (ptr, len) = str_to_ptr_len(val, vm)?; + sqlite3_result_text(self.ctx, ptr, len, SQLITE_TRANSIENT()) + } else if let Ok(buffer) = PyBuffer::try_from_borrowed_object(vm, val) { + let (ptr, len) = buffer_to_ptr_len(&buffer, vm)?; + sqlite3_result_blob(self.ctx, ptr, len, SQLITE_TRANSIENT()) + } else { + return Err(new_programming_error( + vm, + "result type not support".to_owned(), + )); + } + } + Ok(()) + } + } + + fn value_to_object(val: *mut sqlite3_value, db: *mut sqlite3, vm: &VirtualMachine) -> PyResult { + let obj = unsafe { + match sqlite3_value_type(val) { + SQLITE_INTEGER => vm.ctx.new_int(sqlite3_value_int64(val)).into(), + SQLITE_FLOAT => vm.ctx.new_float(sqlite3_value_double(val)).into(), + SQLITE_TEXT => { + let text = + ptr_to_vec(sqlite3_value_text(val), sqlite3_value_bytes(val), db, vm)?; + let text = String::from_utf8(text).map_err(|_| { + vm.new_value_error("invalid utf-8 with SQLITE_TEXT".to_owned()) + })?; + vm.ctx.new_str(text).into() + } + SQLITE_BLOB => { + let blob = ptr_to_vec( + sqlite3_value_blob(val).cast(), + sqlite3_value_bytes(val), + db, + vm, + )?; + vm.ctx.new_bytes(blob).into() + } + _ => vm.ctx.none(), + } + }; + Ok(obj) + } + + fn ptr_to_str<'a>(p: *const i8, vm: &VirtualMachine) -> PyResult<&'a str> { + if p.is_null() { + return Err(vm.new_memory_error("string pointer is null".to_owned())); + } + unsafe { CStr::from_ptr(p).to_str() } + .map_err(|_| vm.new_value_error("Invalid UIF-8 codepoint".to_owned())) + } + + fn ptr_to_string( + p: *const u8, + nbytes: c_int, + db: *mut sqlite3, + vm: &VirtualMachine, + ) -> PyResult { + let s = ptr_to_vec(p, nbytes, db, vm)?; + String::from_utf8(s).map_err(|_| vm.new_value_error("invalid utf-8".to_owned())) + } + + fn ptr_to_vec( + p: *const u8, + nbytes: c_int, + db: *mut sqlite3, + vm: &VirtualMachine, + ) -> PyResult> { + if p.is_null() { + if !db.is_null() && unsafe { sqlite3_errcode(db) } == SQLITE_NOMEM { + Err(vm.new_memory_error("sqlite out of memory".to_owned())) + } else { + Ok(vec![]) + } + } else if nbytes < 0 { + Err(vm.new_system_error("negative size with ptr".to_owned())) + } else { + Ok(unsafe { std::slice::from_raw_parts(p.cast(), nbytes as usize) }.to_vec()) + } + } + + fn str_to_ptr_len(s: &PyStr, vm: &VirtualMachine) -> PyResult<(*const i8, i32)> { + let len = c_int::try_from(s.byte_len()) + .map_err(|_| vm.new_overflow_error("TEXT longer than INT_MAX bytes".to_owned()))?; + let ptr = s.as_str().as_ptr().cast(); + Ok((ptr, len)) + } + + fn buffer_to_ptr_len(buffer: &PyBuffer, vm: &VirtualMachine) -> PyResult<(*const c_void, i32)> { + let bytes = buffer.as_contiguous().ok_or_else(|| { + vm.new_buffer_error("underlying buffer is not C-contiguous".to_owned()) + })?; + let len = c_int::try_from(bytes.len()) + .map_err(|_| vm.new_overflow_error("BLOB longer than INT_MAX bytes".to_owned()))?; + let ptr = bytes.as_ptr().cast(); + Ok((ptr, len)) + } + + fn exception_type_from_errcode(errcode: c_int, vm: &VirtualMachine) -> &'static Py { + match errcode { + SQLITE_INTERNAL | SQLITE_NOTFOUND => internal_error_type(), + SQLITE_NOMEM => vm.ctx.exceptions.memory_error, + SQLITE_ERROR | SQLITE_PERM | SQLITE_ABORT | SQLITE_BUSY | SQLITE_LOCKED + | SQLITE_READONLY | SQLITE_INTERRUPT | SQLITE_IOERR | SQLITE_FULL | SQLITE_CANTOPEN + | SQLITE_PROTOCOL | SQLITE_EMPTY | SQLITE_SCHEMA => operational_error_type(), + SQLITE_CORRUPT => database_error_type(), + SQLITE_TOOBIG => data_error_type(), + SQLITE_CONSTRAINT | SQLITE_MISMATCH => integrity_error_type(), + SQLITE_MISUSE | SQLITE_RANGE => interface_error_type(), + _ => database_error_type(), + } + } + + fn name_from_errcode(errcode: c_int) -> &'static str { + for (name, code) in ERROR_CODES { + if *code == errcode { + return name; + } + } + "unknown error code" + } + + fn raise_exception( + typ: PyTypeRef, + errcode: c_int, + msg: String, + vm: &VirtualMachine, + ) -> PyBaseExceptionRef { + let dict = vm.ctx.new_dict(); + if let Err(e) = dict.set_item("sqlite_errorcode", vm.ctx.new_int(errcode).into(), vm) { + return e; + } + let errname = name_from_errcode(errcode); + if let Err(e) = dict.set_item("sqlite_errorname", vm.ctx.new_str(errname).into(), vm) { + return e; + } + + vm.new_exception_msg_dict(typ, msg, dict) + } + + static BEGIN_STATEMENTS: &[&[u8]] = &[ + b"BEGIN ", + b"BEGIN DEFERRED", + b"BEGIN IMMEDIATE", + b"BEGIN EXCLUSIVE", + ]; + + fn begin_statement_ptr_from_isolation_level( + s: &PyStr, + vm: &VirtualMachine, + ) -> PyResult<*const i8> { + BEGIN_STATEMENTS + .iter() + .find(|&&x| x[6..].eq_ignore_ascii_case(s.as_str().as_bytes())) + .map(|&x| x.as_ptr().cast()) + .ok_or_else(|| { + vm.new_value_error( + "isolation_level string must be '', 'DEFERRED', 'IMMEDIATE', or 'EXCLUSIVE'" + .to_owned(), + ) + }) + } + + fn lstrip_sql(sql: &[u8]) -> Option<&[u8]> { + let mut pos = sql; + loop { + match pos.first()? { + b' ' | b'\t' | b'\x0c' | b'\n' | b'\r' => { + pos = &pos[1..]; + } + b'-' => { + if *pos.get(1)? == b'-' { + // line comments + pos = &pos[2..]; + while *pos.first()? != b'\n' { + pos = &pos[1..]; + } + } else { + return Some(pos); + } + } + b'/' => { + if *pos.get(1)? == b'*' { + // c style comments + pos = &pos[2..]; + while *pos.first()? != b'*' || *pos.get(1)? != b'/' { + pos = &pos[1..]; + } + } else { + return Some(pos); + } + } + _ => return Some(pos), + } + } + } +} diff --git a/vm/src/lib.rs b/vm/src/lib.rs index ce2fc5fda..0fb964b94 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -81,7 +81,8 @@ pub mod warn; pub use self::convert::{TryFromBorrowedObject, TryFromObject}; pub use self::object::{ - AsObject, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult, PyWeakRef, + AsObject, Py, PyAtomicRef, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, + PyResult, PyWeakRef, }; pub use self::vm::{Context, Interpreter, Settings, VirtualMachine}; diff --git a/vm/src/object/ext.rs b/vm/src/object/ext.rs index d9683b35d..5bd0779ee 100644 --- a/vm/src/object/ext.rs +++ b/vm/src/object/ext.rs @@ -227,6 +227,22 @@ cfg_if::cfg_if! { unsafe impl Sync for PyAtomicRef> {} unsafe impl Send for PyAtomicRef {} unsafe impl Sync for PyAtomicRef {} + unsafe impl Send for PyAtomicRef> {} + unsafe impl Sync for PyAtomicRef> {} + } +} + +impl fmt::Debug for PyAtomicRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PyAtomicRef(")?; + unsafe { + self.inner + .load(Ordering::Relaxed) + .cast::() + .as_ref() + .fmt(f) + }?; + write!(f, ")") } } @@ -290,6 +306,10 @@ impl PyAtomicRef> { unsafe { self.inner.load(Ordering::Relaxed).cast::>().as_ref() } } + pub fn to_owned(&self) -> Option> { + self.deref().map(|x| x.to_owned()) + } + /// # Safety /// The caller is responsible to keep the returned PyRef alive /// until no more reference can be used via PyAtomicRef::deref() @@ -342,9 +362,9 @@ impl PyAtomicRef { /// until no more reference can be used via PyAtomicRef::deref() #[must_use] pub unsafe fn swap(&self, obj: PyObjectRef) -> PyObjectRef { - let obj = obj.into_raw() as *mut _; - let old = Radium::swap(&self.inner, obj, Ordering::AcqRel); - PyObjectRef::from_raw(old.cast()) + let obj = obj.into_raw(); + let old = Radium::swap(&self.inner, obj as *mut _, Ordering::AcqRel); + PyObjectRef::from_raw(old as _) } pub fn swap_to_temporary_refs(&self, obj: PyObjectRef, vm: &VirtualMachine) { @@ -355,6 +375,52 @@ impl PyAtomicRef { } } +impl From> for PyAtomicRef> { + fn from(obj: Option) -> Self { + let val = obj.map(|x| x.into_raw() as *mut _).unwrap_or(null_mut()); + Self { + inner: Radium::new(val), + _phantom: Default::default(), + } + } +} + +impl PyAtomicRef> { + pub fn deref(&self) -> Option<&PyObject> { + unsafe { + self.inner + .load(Ordering::Relaxed) + .cast::() + .as_ref() + } + } + + pub fn to_owned(&self) -> Option { + self.deref().map(|x| x.to_owned()) + } + + /// # Safety + /// The caller is responsible to keep the returned PyRef alive + /// until no more reference can be used via PyAtomicRef::deref() + #[must_use] + pub unsafe fn swap(&self, obj: Option) -> Option { + let val = obj.map(|x| x.into_raw() as *mut _).unwrap_or(null_mut()); + let old = Radium::swap(&self.inner, val, Ordering::AcqRel); + old.cast::() + .as_ref() + .map(|x| PyObjectRef::from_raw(x)) + } + + pub fn swap_to_temporary_refs(&self, obj: Option, vm: &VirtualMachine) { + let Some(old) = (unsafe { self.swap(obj) }) else { + return; + }; + if let Some(frame) = vm.current_frame() { + frame.temporary_refs.lock().push(old); + } + } +} + pub trait AsObject where Self: Borrow, diff --git a/vm/src/stdlib/mod.rs b/vm/src/stdlib/mod.rs index 3aa5efa18..16936f1ab 100644 --- a/vm/src/stdlib/mod.rs +++ b/vm/src/stdlib/mod.rs @@ -19,7 +19,7 @@ mod string; mod symtable; mod sysconfigdata; #[cfg(feature = "threading")] -mod thread; +pub mod thread; pub mod time; pub mod warnings; mod weakref; diff --git a/vm/src/stdlib/os.rs b/vm/src/stdlib/os.rs index 9fccfd7e5..392bf05c8 100644 --- a/vm/src/stdlib/os.rs +++ b/vm/src/stdlib/os.rs @@ -73,7 +73,12 @@ impl PyPathLike { self.path.into_os_string().into_vec() } - #[cfg(any(unix, target_os = "wasi"))] + #[cfg(windows)] + pub fn into_bytes(self) -> Vec { + self.path.to_string_lossy().to_string().into_bytes() + } + + // #[cfg(any(unix, target_os = "wasi"))] pub fn into_cstring(self, vm: &VirtualMachine) -> PyResult { ffi::CString::new(self.into_bytes()).map_err(|err| err.into_pyexception(vm)) } diff --git a/vm/src/vm/vm_new.rs b/vm/src/vm/vm_new.rs index 0b7433d9c..0932ecf27 100644 --- a/vm/src/vm/vm_new.rs +++ b/vm/src/vm/vm_new.rs @@ -84,6 +84,27 @@ impl VirtualMachine { self.new_exception(exc_type, vec![self.ctx.new_str(msg).into()]) } + /// Instantiate an exception with `msg` as the only argument and `dict` for object + /// This function should only be used with builtin exception types; if a user-defined exception + /// type is passed in, it may not be fully initialized; try using + /// [`vm.invoke_exception()`][Self::invoke_exception] or + /// [`exceptions::ExceptionCtor`][crate::exceptions::ExceptionCtor] instead. + pub fn new_exception_msg_dict( + &self, + exc_type: PyTypeRef, + msg: String, + dict: PyDictRef, + ) -> PyBaseExceptionRef { + PyRef::new_ref( + // TODO: this costructor might be invalid, because multiple + // exception (even builtin ones) are using custom constructors, + // see `OSError` as an example: + PyBaseException::new(vec![self.ctx.new_str(msg).into()], self), + exc_type, + Some(dict), + ) + } + pub fn new_lookup_error(&self, msg: String) -> PyBaseExceptionRef { let lookup_error = self.ctx.exceptions.lookup_error.to_owned(); self.new_exception_msg(lookup_error, msg)