549d2d68b3
QuestDB: {p}sample.tag is now SYMBOL INDEX. The processed-read data-range
lookups (EarliestSample / LatestSample) were partition scans of the unindexed
symbol; on 53.6M rows / 120 tags / 61 daily partitions the indexed seeks are
1-1.5 ms warm / 2-4 ms cold whatever the tag's history (unindexed: up to 11 ms
warm, 14 ms cold; cost grew with the table). The bootstrapper adds the index to a
pre-#683 table (ALTER ... ADD INDEX, a WAL txn, idempotent, ~3 s for 68M rows);
AutoCreateSchema=false only logs the statement. Disk +~12%; write apply time
within noise; main per-sample/raw/time-weighted queries unchanged or faster.
Rejected by measurement/analysis: bounded existence probes (still full scans
when a side is absent), LATEST ON (slowest unindexed), per-tag first/last side
table (cannot track TTL drops or deletes INSERT-only).
Both backends now read the data range before the bucket statement: an append
racing the read reaches the buckets but not the range, so it can no longer
produce a Good Count of 0 for an interval that has samples.
Tests: TagIndexTests (new-table index, bound-param seek uses the index,
tombstones excluded / outage marker counted, pre-#683 migration with identical
processed results across 10 aggregates x 4 windows x both directions).
88 lines
4.3 KiB
C#
88 lines
4.3 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.Tests;
|
|
|
|
public sealed class QuestDbTablesTests
|
|
{
|
|
[Fact]
|
|
public void Sample_ddl_without_ttl_is_pinned() =>
|
|
QuestDbTables.CreateSample("hist_", null).ShouldBe(
|
|
"CREATE TABLE IF NOT EXISTS hist_sample (ts TIMESTAMP, tag SYMBOL INDEX, kind BYTE, num_value DOUBLE, " +
|
|
"int_value LONG, str_value VARCHAR, status LONG, deleted BOOLEAN, server_ts TIMESTAMP) " +
|
|
"TIMESTAMP(ts) PARTITION BY DAY WAL DEDUP UPSERT KEYS(ts, tag)");
|
|
|
|
[Fact]
|
|
public void Sample_ddl_with_ttl_puts_ttl_between_partition_and_wal() =>
|
|
QuestDbTables.CreateSample("hist_", 7).ShouldBe(
|
|
"CREATE TABLE IF NOT EXISTS hist_sample (ts TIMESTAMP, tag SYMBOL INDEX, kind BYTE, num_value DOUBLE, " +
|
|
"int_value LONG, str_value VARCHAR, status LONG, deleted BOOLEAN, server_ts TIMESTAMP) " +
|
|
"TIMESTAMP(ts) PARTITION BY DAY TTL 7 DAYS WAL DEDUP UPSERT KEYS(ts, tag)");
|
|
|
|
[Fact]
|
|
public void Tag_index_migration_statements_are_pinned()
|
|
{
|
|
// #683: the migration for a sample table created before the tag index existed, and its probe.
|
|
QuestDbTables.AddTagIndex("hist_sample").ShouldBe("ALTER TABLE hist_sample ALTER COLUMN tag ADD INDEX");
|
|
QuestDbTables.TagIndexedQuery("hist_sample").ShouldBe(
|
|
"SELECT indexed FROM table_columns('hist_sample') WHERE \"column\" = 'tag'");
|
|
}
|
|
|
|
[Fact]
|
|
public void Event_ddl_is_pinned()
|
|
{
|
|
QuestDbTables.CreateEvent("hist_", null).ShouldBe(
|
|
"CREATE TABLE IF NOT EXISTS hist_event (ts TIMESTAMP, event_id VARCHAR, source SYMBOL, alarm_id VARCHAR, " +
|
|
"alarm_name VARCHAR, alarm_type_name VARCHAR, severity INT, event_kind VARCHAR, message VARCHAR, " +
|
|
"user_name VARCHAR, comment VARCHAR, received_utc TIMESTAMP) " +
|
|
"TIMESTAMP(ts) PARTITION BY DAY WAL DEDUP UPSERT KEYS(ts, event_id)");
|
|
QuestDbTables.CreateEvent("hist_", 30).ShouldContain("PARTITION BY DAY TTL 30 DAYS WAL DEDUP");
|
|
}
|
|
|
|
[Fact]
|
|
public void Tag_ddl_is_pinned_and_never_carries_a_ttl() =>
|
|
QuestDbTables.CreateTag("hist_").ShouldBe(
|
|
"CREATE TABLE IF NOT EXISTS hist_tag (ts TIMESTAMP, name SYMBOL, data_type BYTE, eng_unit VARCHAR, " +
|
|
"description VARCHAR, updated_utc TIMESTAMP) TIMESTAMP(ts) PARTITION BY YEAR WAL DEDUP UPSERT KEYS(ts, name)");
|
|
|
|
[Fact]
|
|
public void The_prefix_is_applied_to_every_table_name()
|
|
{
|
|
QuestDbTables.All("t1_").ShouldBe(["t1_sample", "t1_event", "t1_tag", "t1_sample_modified"]);
|
|
QuestDbTables.Expiring("t1_").ShouldBe(["t1_sample", "t1_event", "t1_sample_modified"]);
|
|
QuestDbTables.CreateAll("t1_", 3).ShouldAllBe(s => s.Contains("t1_"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Sample_modified_ddl_is_pinned_append_only_and_carries_the_ttl()
|
|
{
|
|
QuestDbTables.CreateSampleModified("hist_", null).ShouldBe(
|
|
"CREATE TABLE IF NOT EXISTS hist_sample_modified (ts TIMESTAMP, tag SYMBOL, modified_id LONG, " +
|
|
"modification_ts TIMESTAMP, update_type BYTE, principal VARCHAR, api_key_id VARCHAR, kind BYTE, num_value DOUBLE, " +
|
|
"int_value LONG, str_value VARCHAR, status LONG, server_ts TIMESTAMP) TIMESTAMP(ts) PARTITION BY DAY WAL");
|
|
QuestDbTables.CreateSampleModified("hist_", 30).ShouldEndWith("PARTITION BY DAY TTL 30 DAYS WAL");
|
|
QuestDbTables.CreateSampleModified("hist_", null).ShouldNotContain("DEDUP");
|
|
}
|
|
|
|
[Fact]
|
|
public void An_invalid_prefix_is_refused() =>
|
|
Should.Throw<ArgumentException>(() => QuestDbTables.CreateSample("a b", null));
|
|
|
|
[Fact]
|
|
public void Alter_ttl_is_pinned_and_null_removes_it()
|
|
{
|
|
QuestDbTables.AlterTtl("hist_sample", 30).ShouldBe("ALTER TABLE hist_sample SET TTL 30 DAYS");
|
|
QuestDbTables.AlterTtl("hist_sample", null).ShouldBe("ALTER TABLE hist_sample SET TTL 0 DAYS");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, "WEEK", 168)] // 7 days reads back as 1 WEEK
|
|
[InlineData(2, "WEEK", 336)]
|
|
[InlineData(30, "DAY", 720)]
|
|
[InlineData(0, "HOUR", 0)] // no TTL
|
|
[InlineData(5, "hour", 5)]
|
|
[InlineData(1, "MONTH", -1)] // unknown unit never equals a wanted value
|
|
public void Ttl_is_compared_in_hours(long value, string unit, long hours) =>
|
|
QuestDbTables.TtlHours(value, unit).ShouldBe(hours);
|
|
}
|