我可以在Python测试中模拟sqlite3的CURRENT_TIMESTAMP吗?
我可以在Python测试中模拟sqlite3的CURRENT_TIMESTAMP吗?
我想在我的Python测试中通过模拟返回值(而不干扰系统时钟)来返回SQLite3 CURRENT_TIMESTAMP的自定义值。我发现了这个答案,但对于CURRENT_TIMESTAMP却不起作用(显然因为它是一个关键字而不是函数)。有什么办法可以让它起作用吗?
更新。 根据@forpas的建议,尝试模拟DATETIME()函数,但看起来对于CURRENT_TIMESTAMP不起作用(与直接调用DATETIME()不同):
def mock_date(*_): return '1975-02-14' def mock_datetime(*_): return '1975-02-14 12:34:56' connection = sqlite3.connect(':memory:') print('在DATE()模拟之前,DATE(\'now\'):' + connection.execute('SELECT DATE(\'now\')').fetchone()[0]) connection.create_function('DATE', -1, mock_date) print('在DATE()模拟之后,DATE(\'now\'):' + connection.execute('SELECT DATE(\'now\')').fetchone()[0]) print('在DATETIME()模拟之前,CURRENT_TIMESTAMP:' + connection.execute('SELECT CURRENT_TIMESTAMP').fetchone()[0]) print('在DATETIME()模拟之前,DATETIME(\'now\'):' + connection.execute('SELECT DATETIME(\'now\')').fetchone()[0]) connection.create_function('DATETIME', -1, mock_datetime) print('在DATETIME()模拟之后,CURRENT_TIMESTAMP:' + connection.execute('SELECT CURRENT_TIMESTAMP').fetchone()[0]) print('在DATETIME()模拟之后,DATETIME(\'now\'):' + connection.execute('SELECT DATETIME(\'now\')').fetchone()[0]) connection.create_function('CURRENT_TIMESTAMP', -1, mock_datetime) print('在CURRENT_TIMESTAMP模拟之后,CURRENT_TIMESTAMP:' + connection.execute('SELECT CURRENT_TIMESTAMP').fetchone()[0])
以下是测试结果:
在DATE()模拟之前,DATE('now'):2023-01-11 在DATE()模拟之后,DATE('now'):1975-02-14 在DATETIME()模拟之前,CURRENT_TIMESTAMP:2023-01-11 21:03:40 在DATETIME()模拟之前,DATETIME('now'):2023-01-11 21:03:40 在DATETIME()模拟之后,CURRENT_TIMESTAMP:2023-01-11 21:03:40 在DATETIME()模拟之后,DATETIME('now'):1975-02-14 12:34:56 在CURRENT_TIMESTAMP模拟之后,CURRENT_TIMESTAMP:2023-01-11 21:03:40
因此,在模拟DATETIME()
之后,DATETIME('now')
的结果发生了变化,但CURRENT_TIMESTAMP
没有变化。
更新2。 添加了模拟CURRENT_TIMESTAMP本身的测试用例。
Python版本为3.9.13,sqlite3版本为3.37.2。测试在Windows环境下进行。
问题的出现原因:
- 通过代码模拟在Python测试中的sqlite3 CURRENT_TIMESTAMP函数时,一些用户无法获得期望的结果。
- 有人注意到可能与模拟函数的顺序有关。
解决方法:
- 引入sqlite3库。
- 定义模拟日期时间函数mock_datetime,返回固定的日期时间值'1975-01-01 00:00:00'。
- 使用内存数据库连接。
- 创建模拟函数con.create_function('CURRENT_TIMESTAMP', -1, mock_datetime)。
- 执行查询con.execute('SELECT CURRENT_TIMESTAMP')并打印结果。
以下是整理的文章:
不要问我为什么,但是当你按照下面的顺序编写代码时(无论是否注释掉我注释的那些行),它似乎可以工作:
import sqlite3 def mock_datetime(*_): return '1975-01-01 00:00:00' with sqlite3.connect(':memory:') as con: con.create_function('CURRENT_TIMESTAMP', -1, mock_datetime) print(con.execute('SELECT CURRENT_TIMESTAMP').fetchone())
我得到了这个结果:
('1975-01-01 00:00:00',)
我真的看不出我做了什么你没有做的。(请注意,使用你的代码,我得到了与你完全相同的结果。可能与模拟函数的顺序有关吗?)
python 3.9.2和sqlite3.__version__ = 3.34.0
编辑:
如果你没有得到和我相同的结果,我建议你更新sqlite3版本。这里有一个相关问题(我没有测试):[点击查看问题](https://stackoverflow.com/questions/61091438)
它对CURRENT_TIMESTAMP没有起作用,我已经更新了我的问题并附上了测试结果,并添加了关于环境的详细信息。你使用的是什么操作系统?
在Windows 10和Ubuntu 18.04.6上都可以工作。
你应该尝试更新你的sqlite3版本(如果我没记错的话,它是一个2002年的版本)。查看这个问题以进行更新:[点击查看问题](https://stackoverflow.com/questions/61091438)
我告诉你Python模块的版本是2.6.0,通过print(sqlite3.version)得到的。实际的库版本是3.37.2(通过print(sqlite3.sqlite_version)得到)。
那么这很奇怪。我在另一台机器上复现了我的代码结果(也是Windows 10,Python 3.9.2和sqlite3 3.34.0)。也许你可以尝试将sqlite3降级到3.34.0版本?
问题的出现的原因是在Python测试中,无法模拟sqlite3的CURRENT_TIMESTAMP函数。解决方法有三种,推荐使用第一种方法:
- 显式地接受正确数量的参数(没有参数)
- 创建两次函数(不推荐)
- 在设置函数之前避免查询函数(最不推荐)
作者使用了以下代码来展示每种方法的工作原理:
import sqlite3 import argparse CURRENT_KEYWORDS = ( 'CURRENT_TIME', 'CURRENT_DATE', 'CURRENT_TIMESTAMP', ) def mocked(*_): return 'MOCKED' def check(no_pre_query, narg): connection = sqlite3.connect(':memory:') select_stmt = "SELECT {}".format(",".join(CURRENT_KEYWORDS)) print(f"Select statement: '{select_stmt}'; {no_pre_query=}, {narg=}") if no_pre_query: print('Skipping initial query') else: print('Before mock: {}'.format(connection.execute(select_stmt).fetchone())) for sql_kw in CURRENT_KEYWORDS: connection.create_function(sql_kw, narg, mocked) print('After mock: {}'.format(connection.execute(select_stmt).fetchone())) for sql_kw in CURRENT_KEYWORDS: connection.create_function(sql_kw, narg, mocked) print('Second attempt after mock: {}'.format(connection.execute(select_stmt).fetchone())) def main(): parser = argparse.ArgumentParser() parser.add_argument("--no-pre-query", action="store_true", default=False) parser.add_argument("--narg", type=int, default=-1) args = parser.parse_args() check(args.no_pre_query, args.narg) if __name__ == "__main__": main()
推荐方法:显式参数数量
当调用connection.create_function
时,CPython调用sqlite的sqlite3_create_function_v2
。根据sqlite的文档:
允许使用相同名称但参数数量或首选文本编码不同的多个函数实现进行注册。SQLite将使用最符合SQL函数使用方式的实现。具有非负nArg参数的函数实现比具有负nArg参数的函数实现更匹配。首选文本编码与数据库编码匹配的函数更匹配,而编码不同的函数更匹配。UTF16le和UTF16be之间的编码差异比UTF8和UTF16之间的编码差异更接近。
非负(包括零)的nArg比负数更匹配,因此将nArg设置为零可以解决该问题:
$ python /tmp/sql.py --narg=0 Select statement: 'SELECT CURRENT_TIME,CURRENT_DATE,CURRENT_TIMESTAMP'; no_pre_query=False, narg=0 Before mock: ('19:22:53', '2023-01-13', '2023-01-13 19:22:53') After mock: ('MOCKED', 'MOCKED', 'MOCKED') Second attempt after mock: ('MOCKED', 'MOCKED', 'MOCKED')
以下是不推荐的方法,但两种方法都值得一提,以防其他人遇到类似的问题:
不推荐的方法2 - 创建两次函数
我可能是错误的(这就是为什么我不推荐这种方法的原因),但是由于函数重载是可能的,定义两次相同的函数似乎会使它优先于零nArg选项:
$ python /tmp/sql.py Select statement: 'SELECT CURRENT_TIME,CURRENT_DATE,CURRENT_TIMESTAMP'; no_pre_query=False, narg=-1 Before mock: ('19:30:18', '2023-01-13', '2023-01-13 19:30:18') After mock: ('19:30:18', '2023-01-13', '2023-01-13 19:30:18') Second attempt after mock: ('MOCKED', 'MOCKED', 'MOCKED')
这可能是特定实现细节的结果,因此可能会在未经通知的情况下发生更改。尽管如此,我认为在某些原因下无法将nArg设置为零时,这可能值得注意。
不推荐的方法3 - 在设置函数之前避免查询函数
这是一种非常奇怪的行为,既没有记录,也无法合理地归因于重载机制。出于这些原因,我强烈不建议使用它,但我仍然认为值得一提,以备后人参考。
当在设置覆盖函数之前没有查询CURRENT_%s
时,似乎create_function
按预期工作,即使提供的nArg为负数。尽管这不应该是这样的,但它确实是(至少在我的设置中),由于它可能解释了为什么在某些代码流中它可能“工作”,但在其他代码流中却不起作用,我认为这也值得一提:
$ python /tmp/sql.py --no-pre-query Select statement: 'SELECT CURRENT_TIME,CURRENT_DATE,CURRENT_TIMESTAMP'; no_pre_query=True, narg=-1 Skipping initial query After mock: ('MOCKED', 'MOCKED', 'MOCKED') Second attempt after mock: ('MOCKED', 'MOCKED', 'MOCKED')
由于特定版本的问题,值得注意的是,这个问题和建议的解决方法都在MacOS Monterey、Python 3.9.6、sqlite3.version
2.6.0和sqlite3.sqlite_version
3.37.0上成功重现。
通过简单地将-1更改为0,如下所示:
connection.create_function('CURRENT_TIMESTAMP', 0, mock_datetime)
提供的示例将打印出似乎是期望的结果(与DATETIME()
无关):
Before DATE() mock, DATE('now'): 2023-01-13 After DATE() mock, DATE('now'): 1975-02-14 Before DATETIME() mock, CURRENT_TIMESTAMP: 2023-01-13 20:05:54 Before DATETIME() mock, DATETIME('now'): 2023-01-13 20:05:54 After DATETIME() mock, CURRENT_TIMESTAMP: 2023-01-13 20:05:54 After DATETIME() mock, DATETIME('now'): 1975-02-14 12:34:56 After CURRENT_TIMESTAMP mock, CURRENT_TIMESTAMP: 1975-02-14 12:34:56
非常感谢您的努力!在我的环境中,这三种方法都可以成功地模拟CURRENT_TIMESTAMP。我同意第一种方法更可取。看起来这是sqlite3库中的一个bug,但是在查看它的代码时,我无法快速地找到原因。