Skip to content

Commit d09b2e6

Browse files
committed
keystrokes to insert Unix timestamp literals
by analogy to existing keystrokes for inserting date/datetime literals. * C-o u: inserts the Unix timestamp in seconds * C-o C-u: inserts the Unix timestamp in microseconds The timestamp is calculated clientside, rather than invoking UNIX_TIMESTAMP() on the server, under the theory that clocks are correctly synchronized. This could be revisited. It allows a nicer microsecond value. Motivation: queries using UNIX_TIMESTAMP() are not repeatable. Incidentally adds a noop keystroke test.
1 parent 7f45c5a commit d09b2e6

7 files changed

Lines changed: 132 additions & 0 deletions

File tree

‎changelog.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Features
55
--------
66
* Remove support for Python 3.10.
77
* Exit faster when using a Boundary tunnel.
8+
* Keystrokes to insert literal Unix timestamps.
89

910

1011
Documentation

‎doc/key_bindings.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,11 @@ Insert the current datetime at cursor.
6565
## <kbd>C-o</kbd> <kbd>C-t</kbd> _(Emacs-mode)_
6666

6767
Insert the quoted current datetime at cursor.
68+
69+
## <kbd>C-o</kbd> <kbd>u</kbd> _(Emacs-mode)_
70+
71+
Insert the current literal `UNIX_TIMESTAMP()` at cursor, in seconds.
72+
73+
## <kbd>C-o</kbd> <kbd>C-u</kbd> _(Emacs-mode)_
74+
75+
Insert the current literal `UNIX_TIMESTAMP()` at cursor, in microseconds.

‎mycli/TIPS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ insert the current datetime using keystrokes control-o + t!
194194

195195
insert the quoted current date using keystrokes control-o + control-t!
196196

197+
insert the literal UNIX_TIMESTAMP() with keystroke control-o + u!
198+
197199
search query history using keystroke control-r!
198200

199201
use keystroke control-g to cancel completion popups!

‎mycli/key_bindings.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,24 @@ def _(event: KeyPressEvent) -> None:
269269

270270
event.app.current_buffer.insert_text(key_binding_utils.server_datetime(mycli.sqlexecute, quoted=True))
271271

272+
@kb.add('c-o', 'u', filter=emacs_mode)
273+
def _(event: KeyPressEvent) -> None:
274+
"""
275+
Insert the current unix timestamp in seconds.
276+
"""
277+
_logger.debug('Detected <C-o u> key.')
278+
279+
event.app.current_buffer.insert_text(key_binding_utils.unix_timestamp())
280+
281+
@kb.add('c-o', 'c-u', filter=emacs_mode)
282+
def _(event: KeyPressEvent) -> None:
283+
"""
284+
Insert the current unix timestamp in microseconds.
285+
"""
286+
_logger.debug('Detected <C-o C-u> key.')
287+
288+
event.app.current_buffer.insert_text(key_binding_utils.unix_timestamp(microseconds=True))
289+
272290
@kb.add("c-r", filter=control_is_searchable)
273291
def _(event: KeyPressEvent) -> None:
274292
"""Search history using fzf or reverse incremental search."""

‎mycli/packages/key_binding_utils.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import math
4+
import time
35
from typing import TYPE_CHECKING, Callable
46

57
from prompt_toolkit.shortcuts import PromptSession
@@ -28,6 +30,13 @@ def server_datetime(sqlexecute: SQLExecute, quoted: bool = False) -> str:
2830
return server_datetime_str
2931

3032

33+
def unix_timestamp(microseconds: bool = False) -> str:
34+
if microseconds:
35+
return str(math.floor(time.time() * 1e6))
36+
else:
37+
return str(math.floor(time.time()))
38+
39+
3140
# todo: maybe these handlers belong in a repl_handlers.py (which does not exist yet)
3241
# \clip doesn't even have a keybinding
3342
def handle_clip_command(mycli: 'MyCli', text: str) -> bool:

‎test/pytests/test_key_binding_utils.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,39 @@ def test_server_datetime_returns_quoted_and_unquoted_values() -> None:
6060
assert key_binding_utils.server_datetime(cast(Any, sqlexecute), quoted=True) == "'2026-04-03 14:05:06'"
6161

6262

63+
@pytest.mark.parametrize(
64+
('timestamp', 'expected'),
65+
[(0.0, '0'), (1700000000.875, '1700000000'), (-0.125, '-1')],
66+
)
67+
def test_unix_timestamp_returns_seconds(
68+
monkeypatch: pytest.MonkeyPatch,
69+
timestamp: float,
70+
expected: str,
71+
) -> None:
72+
monkeypatch.setattr(key_binding_utils.time, 'time', lambda: timestamp)
73+
74+
assert key_binding_utils.unix_timestamp() == expected
75+
76+
77+
@pytest.mark.parametrize(
78+
('timestamp', 'expected'),
79+
[
80+
(0.0, '0'),
81+
(1700000000.875, '1700000000875000'),
82+
(0.00000175, '1'),
83+
(-0.00000175, '-2'),
84+
],
85+
)
86+
def test_unix_timestamp_returns_microseconds(
87+
monkeypatch: pytest.MonkeyPatch,
88+
timestamp: float,
89+
expected: str,
90+
) -> None:
91+
monkeypatch.setattr(key_binding_utils.time, 'time', lambda: timestamp)
92+
93+
assert key_binding_utils.unix_timestamp(microseconds=True) == expected
94+
95+
6396
def test_prettify_statement():
6497
statement = 'SELECT 1'
6598
mycli = FakeMyCli()

‎test/pytests/test_key_bindings.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,29 @@ def test_escape_binding_cancels_completion_menu(
365365
assert event.app.current_buffer.complete_state is None
366366

367367

368+
@pytest.mark.parametrize('key, config_name', [(Keys.ControlI, 'tab'), (Keys.ControlAt, 'control_space')])
369+
@pytest.mark.parametrize('completion_active', [False, True])
370+
def test_completion_binding_without_configured_behaviors_is_noop(
371+
key: Keys,
372+
config_name: str,
373+
completion_active: bool,
374+
) -> None:
375+
mycli = DummyMyCli(DummyKeysConfig(behaviors={config_name: []}))
376+
kb = key_bindings.mycli_bindings(mycli)
377+
complete_state = object() if completion_active else None
378+
buffer = DummyBuffer(text='sel', complete_state=complete_state)
379+
event = make_event(buffer)
380+
381+
binding_handler(kb, key)(event)
382+
383+
assert buffer.complete_state is complete_state
384+
assert buffer.start_completion_calls == []
385+
assert buffer.complete_next_calls == 0
386+
assert buffer.cancel_completion_calls == 0
387+
assert buffer.start_selection_calls == []
388+
assert buffer.inserted_text == []
389+
390+
368391
def test_control_space_toolkit_default_starts_selection_for_non_empty_text() -> None:
369392
mycli = DummyMyCli(DummyKeysConfig(behaviors={'control_space': ['toolkit_default']}))
370393
kb = key_bindings.mycli_bindings(mycli)
@@ -501,6 +524,44 @@ def test_date_and_datetime_bindings_insert_shortcuts(
501524
assert event.app.current_buffer.inserted_text == [expected_text]
502525

503526

527+
@pytest.mark.parametrize(
528+
('keys', 'expected_text'),
529+
[
530+
((Keys.ControlO, 'u'), '1700000000'),
531+
((Keys.ControlO, Keys.ControlU), '1700000000875000'),
532+
],
533+
)
534+
def test_unix_timestamp_bindings_insert_numeric_literals(
535+
monkeypatch: pytest.MonkeyPatch,
536+
keys: tuple[str | Keys, ...],
537+
expected_text: str,
538+
) -> None:
539+
mycli = DummyMyCli(DummyKeysConfig(), key_bindings_mode='emacs')
540+
kb = key_bindings.mycli_bindings(mycli)
541+
event = make_event()
542+
monkeypatch.setattr(key_bindings.key_binding_utils.time, 'time', lambda: 1700000000.875)
543+
544+
binding_handler(kb, *keys)(event)
545+
546+
assert event.app.current_buffer.inserted_text == [expected_text]
547+
548+
549+
@pytest.mark.parametrize('keys', [(Keys.ControlO, 'u'), (Keys.ControlO, Keys.ControlU)])
550+
@pytest.mark.parametrize('editing_mode, enabled', [(EditingMode.EMACS, True), (EditingMode.VI, False)])
551+
def test_unix_timestamp_bindings_are_emacs_only(
552+
monkeypatch: pytest.MonkeyPatch,
553+
keys: tuple[str | Keys, ...],
554+
editing_mode: EditingMode,
555+
enabled: bool,
556+
) -> None:
557+
mycli = DummyMyCli(DummyKeysConfig())
558+
kb = key_bindings.mycli_bindings(mycli)
559+
app = DummyApp(current_buffer=DummyBuffer(), editing_mode=editing_mode)
560+
patch_filter_app(monkeypatch, app)
561+
562+
assert binding_filter(kb, *keys)() is enabled
563+
564+
504565
def test_control_r_uses_reverse_isearch_mode_when_configured(monkeypatch) -> None:
505566
mycli = DummyMyCli(DummyKeysConfig(options={'control_r': 'reverse_isearch'}), key_bindings_mode='emacs')
506567
kb = key_bindings.mycli_bindings(mycli)

0 commit comments

Comments
 (0)