Skip to content

[CrateDB] Add support for data acquisition and data export - #148

Draft
amotl wants to merge 7 commits into
mainfrom
amo/cratedb
Draft

[CrateDB] Add support for data acquisition and data export#148
amotl wants to merge 7 commits into
mainfrom
amo/cratedb

Conversation

@amotl

@amotl amotl commented Jun 9, 2023

Copy link
Copy Markdown
Member

About

Migrating to InfluxDB version 2 would mean to leave SQL behind 1. While using the Flux query language is intriguing, and I will not reject bringing in support for InfluxDB2 and its successor IOx, supporting an SQL-based timeseries database makes sense for me, and this time maybe even a more capable one than InfluxDB in terms of broader support for data types and SQL operations.

So, I think viable alternatives are both CrateDB and TimescaleDB 2, which may even share parts of their corresponding adapter implementations, because both are building upon PostgreSQL standards. This patch makes a start by adding support for CrateDB, let's have a look at TimescaleDB later.

Documentation

https://kotori--148.org.readthedocs.build/database/cratedb.html

Backlog

  • Make Grafana instant dashboard provisioning work.
  • Make the data export feature work.
  • Update documentation across the board.
  • Demonstrate LTTB downsampling on behalf of a secondary Grafana panel.
  • Investigate whether some of the pandas routines on the data export subsystem could be replaced/optimized by query statements using LOCF and NOCB.
  • Investigate how and where max_by and min_by could also be utilized in a sensible way.

Footnotes

  1. https://docs.influxdata.com/influxdb/v2.7/query-data/flux/

  2. With the drawback that TimescaleDB also changed the license for parts of their code to non-FOSS, see https://github.com/timescale/timescaledb/blob/main/tsl/LICENSE-TIMESCALE.

@codecov

codecov Bot commented Jun 9, 2023

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.59218% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.88%. Comparing base (d5170ea) to head (424226b).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
kotori/daq/storage/cratedb.py 84.16% 19 Missing ⚠️
kotori/daq/graphing/grafana/manager.py 83.33% 2 Missing ⚠️
kotori/daq/graphing/grafana/dashboard.py 92.30% 1 Missing ⚠️
kotori/daq/services/mig.py 90.00% 1 Missing ⚠️
kotori/io/export/database.py 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #148      +/-   ##
==========================================
+ Coverage   78.45%   78.88%   +0.43%     
==========================================
  Files          56       59       +3     
  Lines        3021     3192     +171     
==========================================
+ Hits         2370     2518     +148     
- Misses        651      674      +23     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@amotl
amotl force-pushed the amo/cratedb branch 9 times, most recently from f395199 to f774a90 Compare June 9, 2023 20:36
@amotl amotl changed the title [CrateDB] Add basic data acquisition support for CrateDB [CrateDB] Add data acquisition support for CrateDB Jun 10, 2023
@amotl

amotl commented Jun 17, 2023

Copy link
Copy Markdown
Member Author

Grafana instant dashboards

About

8abe55d added baseline support for producing Grafana instant dashboards, and 9c663b2 now improves it by using proper time bucketing within the standard SQL statement template, based on emulating GROUP BY DATE_BIN() by using Grafana's $__timeGroupAlias macro for casting $__interval values, until CrateDB's DATE_BIN() function understands Grafana's native interval values.

Reference documentation

$__timeGroup(dateColumn, $__interval) will be replaced by an expression usable in a GROUP BY clause.
$__timeGroupAlias(dateColumn, $__interval) will be replaced identical to $__timeGroup but with an added column alias.

-- https://grafana.com/docs/grafana/latest/datasources/postgres/#macros

Thanks

Thank you for the guidance, @seut and @hammerhead.

@amotl
amotl force-pushed the amo/cratedb branch 2 times, most recently from a609bbb to 9c663b2 Compare June 17, 2023 22:34
@amotl amotl changed the title [CrateDB] Add data acquisition support for CrateDB [CrateDB] Add support for data acquisition and data export Jun 18, 2023
@amotl
amotl force-pushed the amo/cratedb branch 2 times, most recently from fcd4379 to 2a2ec79 Compare June 21, 2023 19:45
Comment on lines +134 to +139
def record_from_dict(item):
record = OrderedDict()
record.update({"time": item["time"]})
record.update(item["tags"])
record.update(item["fields"])
return record

@amotl amotl Jun 21, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking for a function to merge those two objects, tags and fields, into a single record, but have not been able to discover it. Only now, I luckily discovered the concat(object, object) function in @proddata's tutorial 1, and that it can also operate on objects, effectively merging those.

I think the two reasons why I have not been able to discover this function were:

a) That both sets of functions operating on container data types 23 have been on the "Scalar functions" page, and I did not expect to find them there.
b) That the search term "merge" did not occur in the corresponding documentation section of the concat(object, object) function, contrary to the documentation of the array_unique() function.

The concat(object, object) function combines two objects into a new object.
The array_unique(array, array, ...) function merges two arrays into one array with unique elements.

Please let me know if you think this could be improved on the CrateDB documentation.

Footnotes

  1. https://crate.io/resources/videos/json-object-data-in-cratedb

  2. https://crate.io/docs/crate/reference/en/latest/general/builtins/scalar-functions.html#array-functions

  3. https://crate.io/docs/crate/reference/en/latest/general/builtins/scalar-functions.html#object-functions

@amotl amotl Jun 22, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. concat() does not completely do what I am aiming at here.

cr> select time, concat(tags, fields) from mqttkit_2_itest.foo_bar_sensors;
+---------------+------------------------------------------+
|          time | concat(tags, fields)                     |
+---------------+------------------------------------------+
| 1687469154383 | {"humidity": 83.1, "temperature": 42.84} |
+---------------+------------------------------------------+

It is nice that it will merge two objects, but now, I would like to destructure the top-level attributes of that single object into individual fields again.

Is there any chance to do this, or, if not, would submitting a corresponding feature request make sense?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we can break down the object into fields at the moment, but maybe using object_keys and then referencing the fields one by one could be sufficient for what you are trying to do?

Comment thread doc/source/about/technologies.rst Outdated
Comment thread doc/source/handbook/usage/cratedb.rst Outdated
Comment thread doc/source/setup/linux-debian.rst Outdated
Comment thread doc/source/setup/macos.rst Outdated
Comment on lines +166 to +172
def write(self, meta, data):
"""
Format ingress data chunk and store it into database table.

TODO: This dearly needs efficiency improvements. Currently, there is no
batching, just single records/inserts. That yields bad performance.
"""

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should not be forgotten. When wrapping up all review comments, put this TODO item into the backlog for a subsequent iteration.

@amotl amotl Jun 22, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most probably, we should look at using the improvement from crate/crate-python#553 here, if that would be applicable.

Comment on lines +249 to +270
class TimezoneAwareCrateJsonEncoder(json.JSONEncoder):
epoch_aware = datetime(1970, 1, 1, tzinfo=pytz.UTC)
epoch_naive = datetime(1970, 1, 1)

def default(self, o):
if isinstance(o, Decimal):
return str(o)
if isinstance(o, datetime):
if o.tzinfo:
delta = o - self.epoch_aware
else:
delta = o - self.epoch_naive
return int(delta.microseconds / 1000.0 +
(delta.seconds + delta.days * 24 * 3600) * 1000.0)
if isinstance(o, date):
return calendar.timegm(o.timetuple()) * 1000
return json.JSONEncoder.default(self, o)


# Monkey patch.
# TODO: Submit upstream.
crate.client.http.CrateJsonEncoder = TimezoneAwareCrateJsonEncoder

@amotl amotl Jun 21, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This monkeypatch should be submitted upstream to the Python driver on behalf of the crate Python package. It may resolve https://github.com/crate/crate-python/issues/361.

Effectively, it is only this change:

if o.tzinfo:
    delta = o - self.epoch_aware
else:
    delta = o - self.epoch_naive

Comment on lines +27 to +29
# TODO: Add querying by tags.
tags = {}
# tags = CrateDBAdapter.get_tags(data)

@amotl amotl Jun 21, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not forget about implementing this, querying by tags. It hasn't been implemented for InfluxDB, but that does not mean it should stay like this.

Comment on lines +1 to +10
{
"alias": "{{ alias }}",
"format": "table",
"resultFormat": "time_series",
"tags": {{ tags }},
"groupByTags": [],
"measurement": "{{ measurement }}",
"rawQuery": true,
"rawSql": "SELECT $__timeGroupAlias(time, $__interval), MEAN(fields['{{ name }}']) AS {{ alias }} FROM {{ table }} WHERE $__timeFilter(time) GROUP BY time ORDER BY time"
}

@amotl amotl Jun 21, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hammerhead helped me to discover the right solution for the SQL query here, and he also told me that the DATE_BIN() function, which @seut recommended to use, does not yet understand Grafana's interval values.

This issue is already being tracked at crate/crate#14211. After it has been resolved, adjust the SQL statement template here, to use DATE_BIN() instead of $__timeGroupAlias(time, $__interval).

@amotl amotl Feb 22, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DATE_BIN() function, which @seut recommended to use, does not yet understand Grafana's interval values.

It looks like this may change with CrateDB 5.7. Thank you, @matriv!

@amotl amotl Jul 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using the new cratedb-grafana-plugin, this will provide a much better usability. Let's check if that needs any adjustments to the data source and dashboard management code.

Thank you @florinutz. 🚀

"groupByTags": [],
"measurement": "{{ measurement }}",
"rawQuery": true,
"rawSql": "SELECT $__timeGroupAlias(time, $__interval), MEAN(fields['{{ name }}']) AS {{ alias }} FROM {{ table }} WHERE $__timeFilter(time) GROUP BY time ORDER BY time"

@amotl amotl Jun 21, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On behalf of a subsequent iteration, we may also want to demonstrate advanced downsampling on a secondary panel, using the largest triangle three buckets (LTTB) algorithm, as presented by @hlcianfagna 1, when possible.

Footnotes

  1. https://community.crate.io/t/advanced-downsampling-with-the-lttb-algorithm/1287

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- Extremely fast distributed query execution.
- Auto-partitioning, auto-sharding, and auto-replication.
- Self-healing and auto-rebalancing.
- User-defined functions (UDFs) can be used to extend the functionality of CrateDB.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-defined functions are not mentioned on the canonical CrateDB README at all. It should be added.

-- https://github.com/crate/crate/blob/master/README.rst

@hlcianfagna hlcianfagna Jun 23, 2023

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +245 to +257
.. code-block:: sql

-- An SQL DDL statement defining a custom schema for holding sensor data.
CREATE TABLE iot_data (
timestamp TIMESTAMP WITH TIME ZONE,
sensor_data OBJECT (DYNAMIC) AS (
temperature FLOAT,
humidity FLOAT,
location OBJECT (DYNAMIC) AS (
latitude DOUBLE PRECISION, longitude DOUBLE PRECISION
)
)
);

@amotl amotl Jun 22, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the only SQL DDL statement within the "query examples" section. Adding a few more, including use of other CrateDB special data types, may be sensible. Do you have any suggestions in your toolboxes?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance of using GEO_POINT maybe?

Comment thread kotori/daq/storage/cratedb.py Outdated
Comment on lines +83 to +87
CREATE TABLE IF NOT EXISTS {tablename} (
time TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
tags OBJECT(DYNAMIC),
fields OBJECT(DYNAMIC)
);

@amotl amotl Jun 22, 2023

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to use such a DDL from the very beginning, as partitioning by year seems to be a general useful approach to be used as a reasonable default, @hlcianfagna?

Suggested change
CREATE TABLE IF NOT EXISTS {tablename} (
time TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
tags OBJECT(DYNAMIC),
fields OBJECT(DYNAMIC)
);
CREATE TABLE IF NOT EXISTS {tablename} (
time TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
tags OBJECT(DYNAMIC),
fields OBJECT(DYNAMIC),
year TIMESTAMP GENERATED ALWAYS AS DATE_TRUNC('year', time)
) PARTITIONED BY (year);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you went for this, I think it is a good idea, yes

Comment on lines +68 to +70
self.db_client = client.connect(
self.host_uri, username=self.username, password=self.password, pool_size=20,
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify and demonstrate connecting also to CrateDB Cloud, maybe on behalf of a subsequent iteration.

Comment on lines +29 to +30
into tables. Tables are grouped into schemas, which is equivalent to the concept of hosting
multiple databases on the same server instance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@proddata mentioned that, at least from a PostgreSQL perspective, databases are more like catalogs. Thanks.

amotl

This comment was marked as resolved.

@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • coderabbit-review

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74e93f70-bb98-479e-ba58-dc0cbedc2640

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch amo/cratedb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

amotl added 4 commits July 29, 2026 12:50
Apply time bucketing over interval obtained from Grafana's date range
picker.

Emulate `GROUP BY DATE_BIN()` by using Grafana's `$__timeGroupAlias`
macro for casting `$__interval` values, until CrateDB's `DATE_BIN()`
function understands Grafana's native interval values.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants