Skip to content

Commit 37919ce

Browse files
committed
fix: PRECISION missing support in timeseries grammar
Fixed issue #3820
1 parent 598496b commit 37919ce

7 files changed

Lines changed: 187 additions & 55 deletions

File tree

engine/src/main/antlr4/com/arcadedb/query/sql/grammar/SQLLexer.g4

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ SHARDS: S H A R D S;
243243
DAYS: D A Y S;
244244
HOURS: H O U R S;
245245
MINUTES: M I N U T E S;
246+
SECONDS: S E C O N D S;
247+
PRECISION: P R E C I S I O N;
248+
NANOSECOND: N A N O S E C O N D;
249+
MICROSECOND: M I C R O S E C O N D;
250+
MILLISECOND: M I L L I S E C O N D;
251+
INTERVAL: I N T E R V A L;
246252
DOWNSAMPLING: D O W N S A M P L I N G;
247253
POLICY: P O L I C Y;
248254
GRANULARITY: G R A N U L A R I T Y;

engine/src/main/antlr4/com/arcadedb/query/sql/grammar/SQLParser.g4

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,17 +442,31 @@ createTypeBody
442442

443443
/**
444444
* CREATE TIMESERIES TYPE body
445-
* Example: CREATE TIMESERIES TYPE SensorData TIMESTAMP ts TAGS (sensor_id STRING) FIELDS (temperature DOUBLE, humidity DOUBLE) SHARDS 4 RETENTION 90 DAYS COMPACTION_INTERVAL 1 HOURS
445+
* Example: CREATE TIMESERIES TYPE SensorData TIMESTAMP ts PRECISION NANOSECOND TAGS (sensor_id STRING) FIELDS (temperature DOUBLE, humidity DOUBLE) SHARDS 4 RETENTION 90 DAYS COMPACTION_INTERVAL 1 HOURS
446446
*/
447447
createTimeSeriesTypeBody
448448
: identifier
449449
(IF NOT EXISTS)?
450-
(TIMESTAMP identifier)?
450+
(TIMESTAMP identifier (PRECISION tsPrecision)?)?
451451
(TAGS LPAREN tsTagColumnDef (COMMA tsTagColumnDef)* RPAREN)?
452452
(FIELDS LPAREN tsFieldColumnDef (COMMA tsFieldColumnDef)* RPAREN)?
453453
(SHARDS INTEGER_LITERAL)?
454-
(RETENTION INTEGER_LITERAL (DAYS | HOURS | MINUTES)?)?
455-
(COMPACTION_INTERVAL INTEGER_LITERAL (DAYS | HOURS | MINUTES)?)?
454+
(RETENTION INTEGER_LITERAL tsRetentionUnit?)?
455+
((COMPACTION_INTERVAL | COMPACTION INTERVAL) INTEGER_LITERAL tsRetentionUnit?)?
456+
;
457+
458+
tsPrecision
459+
: NANOSECOND
460+
| MICROSECOND
461+
| MILLISECOND
462+
| SECOND
463+
;
464+
465+
tsRetentionUnit
466+
: DAYS
467+
| HOURS
468+
| MINUTES
469+
| SECONDS
456470
;
457471

458472
tsTagColumnDef
@@ -481,8 +495,10 @@ tsTimeUnit
481495
: DAYS
482496
| HOURS
483497
| MINUTES
498+
| SECONDS
484499
| HOUR
485500
| MINUTE
501+
| SECOND
486502
;
487503

488504
/**
@@ -1475,6 +1491,12 @@ identifier
14751491
| DAYS
14761492
| HOURS
14771493
| MINUTES
1494+
| SECONDS
1495+
| PRECISION
1496+
| NANOSECOND
1497+
| MICROSECOND
1498+
| MILLISECOND
1499+
| INTERVAL
14781500
| DOWNSAMPLING
14791501
| POLICY
14801502
| GRANULARITY

engine/src/main/java/com/arcadedb/query/sql/antlr/SQLASTBuilder.java

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6045,9 +6045,12 @@ public CreateTimeSeriesTypeStatement visitCreateTimeSeriesTypeStmt(
60456045
stmt.name = (Identifier) visit(bodyCtx.identifier(0));
60466046
stmt.ifNotExists = bodyCtx.IF() != null && bodyCtx.NOT() != null && bodyCtx.EXISTS() != null;
60476047

6048-
// TIMESTAMP column
6049-
if (bodyCtx.TIMESTAMP() != null && bodyCtx.identifier().size() > 1)
6048+
// TIMESTAMP column and optional PRECISION
6049+
if (bodyCtx.TIMESTAMP() != null && bodyCtx.identifier().size() > 1) {
60506050
stmt.timestampColumn = (Identifier) visit(bodyCtx.identifier(1));
6051+
if (bodyCtx.PRECISION() != null && bodyCtx.tsPrecision() != null)
6052+
stmt.precision = bodyCtx.tsPrecision().getText().toUpperCase();
6053+
}
60516054

60526055
// TAGS (name type, ...)
60536056
if (bodyCtx.TAGS() != null) {
@@ -6103,35 +6106,20 @@ public CreateTimeSeriesTypeStatement visitCreateTimeSeriesTypeStmt(
61036106
}
61046107
}
61056108

6106-
// Determine time unit by looking at tokens after RETENTION + INTEGER_LITERAL
6107-
long multiplier = 86400000L; // default: DAYS
6108-
boolean foundRetention = false;
6109-
boolean foundValue = false;
6110-
for (int i = 0; i < bodyCtx.children.size(); i++) {
6111-
if (bodyCtx.children.get(i) instanceof TerminalNode tn) {
6112-
if (tn.getSymbol().getType() == SQLParser.RETENTION)
6113-
foundRetention = true;
6114-
else if (foundRetention && tn.getSymbol().getType() == SQLParser.INTEGER_LITERAL)
6115-
foundValue = true;
6116-
else if (foundRetention && foundValue) {
6117-
if (tn.getSymbol().getType() == SQLParser.HOURS)
6118-
multiplier = 3600000L;
6119-
else if (tn.getSymbol().getType() == SQLParser.MINUTES)
6120-
multiplier = 60000L;
6121-
break;
6122-
}
6123-
}
6124-
}
6125-
6109+
// The first tsRetentionUnit after RETENTION belongs to RETENTION
6110+
final long multiplier = resolveTimeUnitAfterKeyword(bodyCtx, SQLParser.RETENTION, 86400000L);
61266111
stmt.retentionMs = retentionValue * multiplier;
61276112
}
61286113

6129-
// COMPACTION_INTERVAL value with optional time unit
6130-
if (bodyCtx.COMPACTION_INTERVAL() != null) {
6114+
// COMPACTION_INTERVAL or COMPACTION INTERVAL - value with optional time unit
6115+
final boolean hasCompaction = bodyCtx.COMPACTION_INTERVAL() != null || bodyCtx.COMPACTION() != null;
6116+
if (hasCompaction) {
61316117
long compactionValue = 0;
6118+
final int compactionTokenType = bodyCtx.COMPACTION_INTERVAL() != null
6119+
? SQLParser.COMPACTION_INTERVAL : SQLParser.COMPACTION;
61326120
for (int i = 0; i < bodyCtx.children.size(); i++) {
61336121
if (bodyCtx.children.get(i) instanceof TerminalNode tn
6134-
&& tn.getSymbol().getType() == SQLParser.COMPACTION_INTERVAL) {
6122+
&& tn.getSymbol().getType() == compactionTokenType) {
61356123
for (int j = i + 1; j < bodyCtx.children.size(); j++) {
61366124
if (bodyCtx.children.get(j) instanceof TerminalNode tn2
61376125
&& tn2.getSymbol().getType() == SQLParser.INTEGER_LITERAL) {
@@ -6143,29 +6131,7 @@ else if (tn.getSymbol().getType() == SQLParser.MINUTES)
61436131
}
61446132
}
61456133

6146-
// Determine time unit (default: HOURS for compaction interval)
6147-
long multiplier = 3600000L; // HOURS
6148-
// Check for unit keywords AFTER the COMPACTION_INTERVAL token
6149-
// We need to look at the remaining children after the integer literal
6150-
boolean foundCompaction = false;
6151-
for (int i = 0; i < bodyCtx.children.size(); i++) {
6152-
if (bodyCtx.children.get(i) instanceof TerminalNode tn
6153-
&& tn.getSymbol().getType() == SQLParser.COMPACTION_INTERVAL)
6154-
foundCompaction = true;
6155-
else if (foundCompaction && bodyCtx.children.get(i) instanceof TerminalNode tn) {
6156-
if (tn.getSymbol().getType() == SQLParser.DAYS) {
6157-
multiplier = 86400000L;
6158-
break;
6159-
} else if (tn.getSymbol().getType() == SQLParser.HOURS) {
6160-
multiplier = 3600000L;
6161-
break;
6162-
} else if (tn.getSymbol().getType() == SQLParser.MINUTES) {
6163-
multiplier = 60000L;
6164-
break;
6165-
}
6166-
}
6167-
}
6168-
6134+
final long multiplier = resolveTimeUnitAfterKeyword(bodyCtx, compactionTokenType, 3600000L);
61696135
stmt.compactionIntervalMs = compactionValue * multiplier;
61706136
}
61716137

@@ -6207,9 +6173,42 @@ private static long parseTimeUnitMs(final SQLParser.TsTimeUnitContext unitCtx) {
62076173
return 3600000L;
62086174
if (unitCtx.MINUTES() != null || unitCtx.MINUTE() != null)
62096175
return 60000L;
6176+
if (unitCtx.SECONDS() != null || unitCtx.SECOND() != null)
6177+
return 1000L;
62106178
return 86400000L; // default to days
62116179
}
62126180

6181+
/**
6182+
* Finds the tsRetentionUnit context that follows the given keyword token type
6183+
* in a createTimeSeriesTypeBody and returns the corresponding millisecond multiplier.
6184+
*/
6185+
private static long resolveTimeUnitAfterKeyword(
6186+
final SQLParser.CreateTimeSeriesTypeBodyContext bodyCtx,
6187+
final int keywordTokenType, final long defaultMs) {
6188+
boolean foundKeyword = false;
6189+
boolean foundValue = false;
6190+
for (int i = 0; i < bodyCtx.children.size(); i++) {
6191+
final var child = bodyCtx.children.get(i);
6192+
if (child instanceof TerminalNode tn && tn.getSymbol().getType() == keywordTokenType)
6193+
foundKeyword = true;
6194+
else if (foundKeyword && child instanceof TerminalNode tn
6195+
&& tn.getSymbol().getType() == SQLParser.INTEGER_LITERAL)
6196+
foundValue = true;
6197+
else if (foundKeyword && foundValue && child instanceof SQLParser.TsRetentionUnitContext unitCtx) {
6198+
if (unitCtx.DAYS() != null)
6199+
return 86400000L;
6200+
if (unitCtx.HOURS() != null)
6201+
return 3600000L;
6202+
if (unitCtx.MINUTES() != null)
6203+
return 60000L;
6204+
if (unitCtx.SECONDS() != null)
6205+
return 1000L;
6206+
return defaultMs;
6207+
}
6208+
}
6209+
return defaultMs;
6210+
}
6211+
62136212
@Override
62146213
public DropMaterializedViewStatement visitDropMaterializedViewStmt(
62156214
final SQLParser.DropMaterializedViewStmtContext ctx) {

engine/src/main/java/com/arcadedb/query/sql/parser/CreateTimeSeriesTypeStatement.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public class CreateTimeSeriesTypeStatement extends DDLStatement {
4242
public Identifier name;
4343
public boolean ifNotExists;
4444
public Identifier timestampColumn;
45+
public String precision;
4546
public PInteger shards;
4647
public long retentionMs;
4748
public long compactionIntervalMs;
@@ -69,6 +70,9 @@ public ResultSet executeDDL(final CommandContext context) {
6970
if (timestampColumn != null)
7071
builder = builder.withTimestamp(timestampColumn.getStringValue());
7172

73+
if (precision != null)
74+
builder = builder.withPrecision(precision);
75+
7276
for (final ColumnDef tag : tags)
7377
builder = builder.withTag(tag.name.getStringValue(), Type.getTypeByName(tag.type.getStringValue()));
7478

@@ -103,6 +107,8 @@ public void toString(final Map<String, Object> params, final StringBuilder build
103107
if (timestampColumn != null) {
104108
builder.append(" TIMESTAMP ");
105109
timestampColumn.toString(params, builder);
110+
if (precision != null)
111+
builder.append(" PRECISION ").append(precision);
106112
}
107113

108114
if (!tags.isEmpty()) {
@@ -151,6 +157,7 @@ public CreateTimeSeriesTypeStatement copy() {
151157
result.name = name == null ? null : name.copy();
152158
result.ifNotExists = ifNotExists;
153159
result.timestampColumn = timestampColumn == null ? null : timestampColumn.copy();
160+
result.precision = precision;
154161
result.shards = shards == null ? null : shards.copy();
155162
result.retentionMs = retentionMs;
156163
result.compactionIntervalMs = compactionIntervalMs;
@@ -172,13 +179,13 @@ public boolean equals(final Object o) {
172179
final CreateTimeSeriesTypeStatement that = (CreateTimeSeriesTypeStatement) o;
173180
return ifNotExists == that.ifNotExists && retentionMs == that.retentionMs
174181
&& compactionIntervalMs == that.compactionIntervalMs && Objects.equals(name, that.name)
175-
&& Objects.equals(timestampColumn, that.timestampColumn) && Objects.equals(shards, that.shards)
176-
&& Objects.equals(tags, that.tags) && Objects.equals(fields, that.fields);
182+
&& Objects.equals(timestampColumn, that.timestampColumn) && Objects.equals(precision, that.precision)
183+
&& Objects.equals(shards, that.shards) && Objects.equals(tags, that.tags) && Objects.equals(fields, that.fields);
177184
}
178185

179186
@Override
180187
public int hashCode() {
181-
return Objects.hash(name, ifNotExists, timestampColumn, shards, retentionMs, compactionIntervalMs, tags, fields);
188+
return Objects.hash(name, ifNotExists, timestampColumn, precision, shards, retentionMs, compactionIntervalMs, tags, fields);
182189
}
183190

184191
public static class ColumnDef {

engine/src/main/java/com/arcadedb/schema/LocalTimeSeriesType.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class LocalTimeSeriesType extends LocalDocumentType {
4444
public static final String KIND_CODE = "t";
4545

4646
private String timestampColumn;
47+
private String precision;
4748
private int shardCount;
4849
private long retentionMs;
4950
private long compactionBucketIntervalMs;
@@ -91,6 +92,14 @@ public void setTimestampColumn(final String timestampColumn) {
9192
this.timestampColumn = timestampColumn;
9293
}
9394

95+
public String getPrecision() {
96+
return precision;
97+
}
98+
99+
public void setPrecision(final String precision) {
100+
this.precision = precision;
101+
}
102+
94103
public int getShardCount() {
95104
return shardCount;
96105
}
@@ -139,6 +148,8 @@ public JSONObject toJSON() {
139148

140149
// TimeSeries-specific fields
141150
json.put("timestampColumn", timestampColumn);
151+
if (precision != null)
152+
json.put("precision", precision);
142153
json.put("shardCount", shardCount);
143154
json.put("retentionMs", retentionMs);
144155
if (compactionBucketIntervalMs > 0)
@@ -175,6 +186,7 @@ public JSONObject toJSON() {
175186
*/
176187
public void fromJSON(final JSONObject json) {
177188
timestampColumn = json.getString("timestampColumn", null);
189+
precision = json.getString("precision", null);
178190
shardCount = json.getInt("shardCount", 1);
179191
retentionMs = json.getLong("retentionMs", 0L);
180192
compactionBucketIntervalMs = json.getLong("compactionBucketIntervalMs", 0L);

engine/src/main/java/com/arcadedb/schema/TimeSeriesTypeBuilder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class TimeSeriesTypeBuilder {
3737
private final DatabaseInternal database;
3838
private String typeName;
3939
private String timestampColumn;
40+
private String precision;
4041
private int shards = 0; // 0 = default (async worker threads)
4142
private long retentionMs = 0;
4243
private long compactionBucketIntervalMs = 0;
@@ -58,6 +59,11 @@ public TimeSeriesTypeBuilder withTimestamp(final String name) {
5859
return this;
5960
}
6061

62+
public TimeSeriesTypeBuilder withPrecision(final String precision) {
63+
this.precision = precision;
64+
return this;
65+
}
66+
6167
public TimeSeriesTypeBuilder withTag(final String name, final Type type) {
6268
this.columns.add(new ColumnDefinition(name, type, ColumnDefinition.ColumnRole.TAG));
6369
return this;
@@ -100,6 +106,7 @@ public LocalTimeSeriesType create() {
100106

101107
final LocalTimeSeriesType type = new LocalTimeSeriesType(schema, typeName);
102108
type.setTimestampColumn(timestampColumn);
109+
type.setPrecision(precision);
103110
type.setShardCount(shards > 0 ? shards : database.getConfiguration().getValueAsInteger(GlobalConfiguration.ASYNC_WORKER_THREADS));
104111
type.setRetentionMs(retentionMs);
105112
type.setCompactionBucketIntervalMs(compactionBucketIntervalMs);

0 commit comments

Comments
 (0)