Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "3.0.0"
current_version = "4.0.0"
commit = false
tag = false

Expand Down
42 changes: 42 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,48 @@ Changelog

=========

4.0.0 (2025-10-17)
------------------

New Breaking Change Feature
~~~~~~~~~~~~~~~~~~~~~~~~~~~

From QuestDB 9.1.0 onwards you can use ``CREATE TABLE`` SQL statements with
``TIMESTAMP_NS`` column types, or rely on column auto-creation.

This client release adds support for sending nanoseconds timestamps to the
server without loss of precision.

This release does not introduce new APIs, instead enhancing the sender/buffer's
``.row()`` API to additionally accept nanosecond precision.

.. code-block:: python

conf = 'http::addr=localhost:9000;'
# or `conf = 'tcp::addr=localhost:9009;protocol_version=2;'`
with Sender.from_conf(conf) as sender:
sender.row(
'trade_executions',
symbols={
'product': 'VOD.L',
'parent_order': '65d1ba36-390e-49a2-93e3-a05ef004b5ff'
'side': 'buy'},
columns={
'order_sent': TimestampNanos(1759246702031355012)},
at=TimestampNanos(1759246702909423071))

If you're using dataframes, nanosecond timestamps are now also transferred with
full precision.

The change is backwards compatible with older QuestDB releases which will simply
continue using the ``TIMESTAMP`` column, even when nanoseconds are specified in
the client.

This is a breaking change because it introduces new breaking timestamp
`column auto-creation <https://questdb.com/docs/reference/api/ilp/overview/#table-and-column-auto-creation>`
behaviour. For full details and upgrade advice, see the
`nanosecond PR on GitHub <https://github.com/questdb/py-questdb-client/pull/113>`_.

3.0.0 (2025-07-07)
------------------

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ and full-connection encryption with
Install
=======

The latest version of the library is **3.0.0** (`changelog <https://py-questdb-client.readthedocs.io/en/latest/changelog.html>`_).
The latest version of the library is 4.0.0 (`changelog <https://py-questdb-client.readthedocs.io/en/latest/changelog.html>`_).

::

Expand Down
10 changes: 5 additions & 5 deletions ci/run_tests_pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ stages:
pool:
name: $(poolName)
vmImage: $(imageName)
timeoutInMinutes: 45
timeoutInMinutes: 90
steps:
- checkout: self
fetchDepth: 1
Expand Down Expand Up @@ -63,18 +63,18 @@ stages:
- script: python3 proj.py test 1
displayName: "Test vs released"
env:
JAVA_HOME: $(JAVA_HOME_11_X64)
JAVA_HOME: $(JAVA_HOME_17_X64)
- script: python3 proj.py test 1
displayName: "Test vs master"
env:
JAVA_HOME: $(JAVA_HOME_11_X64)
JAVA_HOME: $(JAVA_HOME_17_X64)
QDB_REPO_PATH: './questdb'
condition: eq(variables.vsQuestDbMaster, true)
- job: TestsAgainstVariousNumpyVersion1x
pool:
name: "Azure Pipelines"
vmImage: "ubuntu-latest"
timeoutInMinutes: 45
timeoutInMinutes: 90
steps:
- checkout: self
fetchDepth: 1
Expand All @@ -98,7 +98,7 @@ stages:
pool:
name: "Azure Pipelines"
vmImage: "ubuntu-latest"
timeoutInMinutes: 45
timeoutInMinutes: 90
steps:
- checkout: self
fetchDepth: 1
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
year = '2024'
author = 'QuestDB'
copyright = '{0}, {1}'.format(year, author)
version = release = '3.0.0'
version = release = '4.0.0'

github_repo_url = 'https://github.com/questdb/py-questdb-client'

Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See: https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
name = "questdb"
requires-python = ">=3.9"
version = "3.0.0"
version = "4.0.0"
description = "QuestDB client library for Python"
readme = "README.rst"
classifiers = [
Expand Down Expand Up @@ -65,6 +65,10 @@ skip = [
# Skip all 32-bit builds, except for Windows.
# Those builds are named `*win32*` in cibuildwheel.
"*i686*",

# Skip Python 3.14 builds until the dependencies catch up
"cp314-*",
"cp314t-*"
]

# [tool.cibuildwheel.windows]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def readme():

setup(
name='questdb',
version='3.0.0',
version='4.0.0',
platforms=['any'],
python_requires='>=3.8',
install_requires=[],
Expand Down
2 changes: 1 addition & 1 deletion src/questdb/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '3.0.0'
__version__ = '4.0.0'
35 changes: 26 additions & 9 deletions src/questdb/ingress.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ cnp.import_array()
# This value is automatically updated by the `bump2version` tool.
# If you need to update it, also update the search definition in
# .bumpversion.cfg.
VERSION = '3.0.0'
VERSION = '4.0.0'

WARN_HIGH_RECONNECTS = True

Expand Down Expand Up @@ -648,7 +648,7 @@ cdef class SenderTransaction:
symbols: Optional[Dict[str, Optional[str]]]=None,
columns: Optional[Dict[
str,
Union[None, bool, int, float, str, TimestampMicros, datetime.datetime, numpy.ndarray]]
Union[None, bool, int, float, str, TimestampMicros, TimestampNanos, datetime.datetime, numpy.ndarray]]
]=None,
at: Union[ServerTimestampType, TimestampNanos, datetime.datetime]):
"""
Expand Down Expand Up @@ -962,12 +962,18 @@ cdef class Buffer:
if not line_sender_buffer_column_str(self._impl, c_name, c_value, &err):
raise c_err_to_py(err)

cdef inline void_int _column_ts(
cdef inline void_int _column_ts_micros(
self, line_sender_column_name c_name, TimestampMicros ts) except -1:
cdef line_sender_error* err = NULL
if not line_sender_buffer_column_ts_micros(self._impl, c_name, ts._value, &err):
raise c_err_to_py(err)

cdef inline void_int _column_ts_nanos(
self, line_sender_column_name c_name, TimestampNanos ts) except -1:
cdef line_sender_error* err = NULL
if not line_sender_buffer_column_ts_nanos(self._impl, c_name, ts._value, &err):
raise c_err_to_py(err)

cdef inline void_int _column_numpy(
self, line_sender_column_name c_name, cnp.ndarray arr) except -1:
if cnp.PyArray_TYPE(arr) != cnp.NPY_FLOAT64:
Expand Down Expand Up @@ -1004,6 +1010,8 @@ cdef class Buffer:
cdef inline void_int _column_dt(
self, line_sender_column_name c_name, cp_datetime dt) except -1:
cdef line_sender_error* err = NULL
# We limit ourselves to micros, since this is the maxium precision
# exposed by the datetime library in Python.
if not line_sender_buffer_column_ts_micros(
self._impl, c_name, datetime_to_micros(dt), &err):
raise c_err_to_py(err)
Expand All @@ -1020,7 +1028,9 @@ cdef class Buffer:
elif PyUnicode_CheckExact(<PyObject*>value):
self._column_str(c_name, value)
elif isinstance(value, TimestampMicros):
self._column_ts(c_name, value)
self._column_ts_micros(c_name, value)
elif isinstance(value, TimestampNanos):
self._column_ts_nanos(c_name, value)
elif PyArray_CheckExact(<PyObject *> value):
self._column_numpy(c_name, value)
elif isinstance(value, cp_datetime):
Expand All @@ -1045,15 +1055,20 @@ cdef class Buffer:
if sender != NULL:
may_flush_on_row_complete(self, <Sender><object>sender)

cdef inline void_int _at_ts(self, TimestampNanos ts) except -1:
cdef inline void_int _at_ts_us(self, TimestampMicros ts) except -1:
cdef line_sender_error* err = NULL
if not line_sender_buffer_at_micros(self._impl, ts._value, &err):
raise c_err_to_py(err)

cdef inline void_int _at_ts_ns(self, TimestampNanos ts) except -1:
cdef line_sender_error* err = NULL
if not line_sender_buffer_at_nanos(self._impl, ts._value, &err):
raise c_err_to_py(err)

cdef inline void_int _at_dt(self, cp_datetime dt) except -1:
cdef int64_t value = datetime_to_nanos(dt)
cdef int64_t value = datetime_to_micros(dt)
cdef line_sender_error* err = NULL
if not line_sender_buffer_at_nanos(self._impl, value, &err):
if not line_sender_buffer_at_micros(self._impl, value, &err):
raise c_err_to_py(err)

cdef inline void_int _at_now(self) except -1:
Expand All @@ -1064,8 +1079,10 @@ cdef class Buffer:
cdef inline void_int _at(self, object ts) except -1:
if ts is None:
self._at_now()
elif isinstance(ts, TimestampMicros):
self._at_ts_us(ts)
elif isinstance(ts, TimestampNanos):
self._at_ts(ts)
self._at_ts_ns(ts)
elif isinstance(ts, cp_datetime):
self._at_dt(ts)
else:
Expand Down Expand Up @@ -1115,7 +1132,7 @@ cdef class Buffer:
symbols: Optional[Dict[str, Optional[str]]]=None,
columns: Optional[Dict[
str,
Union[None, bool, int, float, str, TimestampMicros, datetime.datetime, numpy.ndarray]]
Union[None, bool, int, float, str, TimestampMicros, TimestampNanos, datetime.datetime, numpy.ndarray]]
]=None,
at: Union[ServerTimestampType, TimestampNanos, datetime.datetime]):
"""
Expand Down
9 changes: 7 additions & 2 deletions test/system_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import questdb.ingress as qi


QUESTDB_VERSION = '8.3.2'
QUESTDB_VERSION = '9.1.0'
QUESTDB_PLAIN_INSTALL_PATH = None
QUESTDB_AUTH_INSTALL_PATH = None
FIRST_ARRAY_RELEASE = (8, 4, 0)
Expand Down Expand Up @@ -211,13 +211,18 @@ def test_http(self):
return

resp = self.qdb_plain.retry_check_table(table_name, min_rows=3)

# Re-enable the line below once https://github.com/questdb/questdb/pull/6220 is merged
# exp_ts_type = 'TIMESTAMP' if self.qdb_plain.version <= (9, 1, 0) else 'TIMESTAMP_NS'
exp_ts_type = 'TIMESTAMP'

exp_columns = [
{'name': 'name_a', 'type': 'SYMBOL'},
{'name': 'name_b', 'type': 'BOOLEAN'},
{'name': 'name_c', 'type': 'LONG'},
{'name': 'name_d', 'type': 'DOUBLE'},
{'name': 'name_e', 'type': 'VARCHAR'},
{'name': 'timestamp', 'type': 'TIMESTAMP'}]
{'name': 'timestamp', 'type': exp_ts_type}]
self.assertEqual(resp['columns'], exp_columns)

exp_dataset = [ # Comparison excludes timestamp column.
Expand Down
Loading