1

I want to return custom value for SQLite3 CURRENT_TIMESTAMP in my Python tests by mocking the return value (without interfering system clock).

I discovered this answer but it doesn't work for CURRENT_TIMESTAMP (apparently because it is a keyword and not a function). Any ideas how to get this working?

UPD. Tried to mock the DATETIME() function according to suggestion by @forpas, but looks like it is not working for CURRENT_TIMESTAMP (unlike calling DATETIME() directly):

def mock_date(*_):
    return '1975-02-14'

def mock_datetime(*_):
    return '1975-02-14 12:34:56'

connection = sqlite3.connect(':memory:')
print('Before DATE() mock, DATE(\'now\'): ' + connection.execute('SELECT DATE(\'now\')').fetchone()[0])
connection.create_function('DATE', -1, mock_date)
print('After DATE() mock, DATE(\'now\'): ' + connection.execute('SELECT DATE(\'now\')').fetchone()[0])
print('Before DATETIME() mock, CURRENT_TIMESTAMP: ' + connection.execute('SELECT CURRENT_TIMESTAMP').fetchone()[0])
print('Before DATETIME() mock, DATETIME(\'now\'): ' + connection.execute('SELECT DATETIME(\'now\')').fetchone()[0])
connection.create_function('DATETIME', -1, mock_datetime)
print('After DATETIME() mock, CURRENT_TIMESTAMP: ' + connection.execute('SELECT CURRENT_TIMESTAMP').fetchone()[0])
print('After DATETIME() mock, DATETIME(\'now\'): ' + connection.execute('SELECT DATETIME(\'now\')').fetchone()[0])
connection.create_function('CURRENT_TIMESTAMP', -1, mock_datetime)
print('After CURRENT_TIMESTAMP mock, CURRENT_TIMESTAMP: ' + connection.execute('SELECT CURRENT_TIMESTAMP').fetchone()[0])

Here are the test results:

Before DATE() mock, DATE('now'): 2023-01-11
After DATE() mock, DATE('now'): 1975-02-14
Before DATETIME() mock, CURRENT_TIMESTAMP: 2023-01-11 21:03:40
Before DATETIME() mock, DATETIME('now'): 2023-01-11 21:03:40
After DATETIME() mock, CURRENT_TIMESTAMP: 2023-01-11 21:03:40
After DATETIME() mock, DATETIME('now'): 1975-02-14 12:34:56
After CURRENT_TIMESTAMP mock, CURRENT_TIMESTAMP: 2023-01-11 21:03:40

So after DATETIME() is mocked, DATETIME('now') result has changed but CURRENT_TIMESTAMP has not.

UPD2. Added test case with mocking CURRENT_TIMESTAMP itself.

The python version is 3.9.13 and sqlite3 version is 3.37.2. Test is performed in Windows environment.

dimnnv
  • 678
  • 3
  • 8
  • 21
  • `CURRENT_TIMESTAMP` is an alias of the function `datetime()` which you can redefine like the answer in your link (I don't know if it works). – forpas Jan 08 '23 at 15:35
  • Unfortunately it is not working this way, added details into the question. – dimnnv Jan 08 '23 at 20:43
  • If you can mock the functions `date()` and `time()` separately then you can get the current timestamp with: `select date() || ' ' || time()`. – forpas Jan 09 '23 at 18:16
  • @forpas the thing is that I need to mock the existing SQL scripts that use CURRENT_TIMESTAMP rather than modify them only for mocking purposes. – dimnnv Jan 09 '23 at 18:34
  • Current code shows datetime mocking and date mocking - can you confirm what the behaviour was when trying current_timestamp? Looking briefly at the sqllite code it seems like current_timestamp is a [peer to datetime()](https://github.com/sqlite/sqlite/blob/26e817f69b4a153ca39ecb2c50fd99fd5287cf0a/src/date.c#L1348) in some ways; so wondering if there is some naming convention that would get to the right override. – Cadmium Jan 11 '23 at 02:19
  • More work, and definitely a little janky - but you could also mock the execute statement to regex replace CURRENT_TIMESTAMP keyword with datetime('now') and then pass along to the original function. Avoids changing the sql and seems moderately safe to match on CURRENT_TIMESTAMP across the string. – Cadmium Jan 11 '23 at 02:21

2 Answers2

2

I've found three ways to solve this issue. I can only explain the first one, so I would recommend using that over the other two:

  1. Explicitly accept the correct number of arguments (none)
  2. Create the functions twice [less recommended]
  3. Avoid querying the functions before setting them [least recommended]

I used the following code to show how each method works:

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()

Recommended method: explicit arg count

When calling connection.create_function CPython calls sqlite's sqlite3_create_function_v2. From sqlite's documentation:

It is permitted to register multiple implementations of the same functions with the same name but with either differing numbers of arguments or differing preferred text encodings. SQLite will use the implementation that most closely matches the way in which the SQL function is used. A function implementation with a non-negative nArg parameter is a better match than a function implementation with a negative nArg. A function where the preferred text encoding matches the database encoding is a better match than a function where the encoding is different. A function where the encoding difference is between UTF16le and UTF16be is a closer match than a function where the encoding difference is between UTF8 and UTF16.

A non-negative (incl. zero) nArg is a better match than a negative one, so setting nArg to zero resolves the issue:

$ 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')

Following are the less-recommended (or.. highly discouraged) methods, but both are worth mentioning in case someone else encounters such symptoms:


Unrecommended method #2 - create_function twice

I might be wrong (which is why I don't recommend this method), but as overloading functions is possible, it looks like defining the same function twice will make it take precedence over a zero-nArg option:

$ 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')

This might be a result of specific implementation details, and as such (as well as being undocumented) it might change without notice. Still, I figured it might be worth noting in case setting nArg to zero is not an option, for some reason.

Unrecommended method #3 - avoid queries before create_function

This is quite a strange behavior, which not only is not documented, but also cannot be sanely attributed to an overloading mechanism. For these reasons I strongly discourage its use, but I still think it's worth mentioning, for posterity.
When the CURRENT_%s are not queried before setting the overriding function, it appears as if create_function works as expected, even if the provided nArg is negative. This should not be the case, but it is (at least on my setup), and as it might explain why in some code-flows it might "work" but not in others, I think this too is worth mentioning:

$ 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')

As specific versions came to question, it is worth noting that the problem and suggested solutions were all successfully reproduced on MacOS Monterey, Python 3.9.6, sqlite3.version 2.6.0, and sqlite3.sqlite_version 3.37.0.
The provided code results in the same output, and by simply changing -1 to 0 like so:

connection.create_function('CURRENT_TIMESTAMP', 0, mock_datetime)

The provided example will print what appears to be the desired result (regardless of 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
micromoses
  • 6,747
  • 2
  • 20
  • 29
  • Thanks a lot for your effort! All 3 methods actually work in my environment and lead to successfully mocking CURRENT_TIMESTAMP. I agree that the 1st way is more preferable. It looks like a bug in sqlite3 library but looking at its code I could not quickly identify the cause. – dimnnv Jan 14 '23 at 08:35
0

Don't ask me why, but when you do it that order (wether you comment the lines I've commented or not), it seems to work:

import sqlite3

# def mock_date(*_):
#     return '1975-01-01'

# def mock_time(*_):
#     return '00:00:00'

def mock_datetime(*_):
    return '1975-01-01 00:00:00'


with sqlite3.connect(':memory:') as con:
    
    # con.create_function('CURRENT_TIME', -1, mock_time)
    # print(con.execute('SELECT CURRENT_TIME').fetchone())
    
    # con.create_function('CURRENT_DATE', -1, mock_date)
    # print(con.execute('SELECT CURRENT_DATE').fetchone())
    
    con.create_function('CURRENT_TIMESTAMP', -1, mock_datetime)
    print(con.execute('SELECT CURRENT_TIMESTAMP').fetchone())

I get this result :

('1975-01-01 00:00:00',)

I honestly can't see what I did that you didn't. (Note that using your code, I get the exact same results as you . Must be something with the order of mocking functions?)

python 3.9.2 and sqlite3.__version__ = 3.34.0

EDIT :

If you don't get the same result as me, I'd advise to update sqlite3. There's a question about it here (which I haven't tested)

tgrandje
  • 2,332
  • 11
  • 33
  • @tgrandje It did not work for current_timestamp, I have updated my question with test results and added details about environment. What OS are you on? – dimnnv Jan 11 '23 at 21:16
  • It does work on both windows 10 & ubuntu 18.04.6 – tgrandje Jan 12 '23 at 08:16
  • You should try to update your sqlite3 version (if I'm not mistaken it's a version dating [from 2002](https://www.sqlite.org/draft/changes.html)). Check [that question](https://stackoverflow.com/questions/61091438/how-to-upgrade-sqlite3-version-in-windows-10) to update it. – tgrandje Jan 12 '23 at 08:29
  • @tgrandje I told you Python module version 2.6.0, which is given by print(sqlite3.version). The actual library version is 3.37.2 (given by print(sqlite3.sqlite_version)). – dimnnv Jan 12 '23 at 18:45
  • Ok, so that's strange. I've reproduced my code's results on another machine (also windows10, also python 3.9.2 and sqlite3 3.34.0). Maybe you could try downgrading sqlite3 to the 3.34.0 ? – tgrandje Jan 12 '23 at 20:30