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
31 changes: 31 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/dsl/AstDSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,37 @@ public static Span span(UnresolvedExpression field, UnresolvedExpression value,
return new Span(field, value, unit);
}

/**
* Creates a Span expression from a field and a span length literal. Parses string literals to
* extract numeric value and time unit (e.g., "1h" -> value=1, unit=h).
*
* @param field The field expression to apply the span to
* @param spanLengthLiteral The literal value containing either a string with embedded unit (e.g.,
* "1h", "30m") or a plain number
* @return A Span expression with parsed value and unit
*/
public static Span spanFromSpanLengthLiteral(
UnresolvedExpression field, Literal spanLengthLiteral) {
if (spanLengthLiteral.getType() == DataType.STRING) {
String spanText = spanLengthLiteral.getValue().toString();
String valueStr = spanText.replaceAll("[^0-9]", "");
String unitStr = spanText.replaceAll("[0-9]", "");

if (valueStr.isEmpty()) {
// No numeric value found, use the literal as-is
return new Span(field, spanLengthLiteral, SpanUnit.NONE);
} else {
// Parse numeric value and unit
Integer value = Integer.parseInt(valueStr);
SpanUnit unit = unitStr.isEmpty() ? SpanUnit.NONE : SpanUnit.of(unitStr);
return span(field, intLiteral(value), unit);
}
} else {
// Non-string literal (e.g., integer)
return span(field, spanLengthLiteral, SpanUnit.NONE);
}
}

public static Sort sort(UnresolvedPlan input, Field... sorts) {
return new Sort(Arrays.asList(sorts)).attach(input);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,37 @@ public enum SpanUnit {
NONE(""),
MILLISECOND("ms"),
MS("ms"),
SECONDS("s"),
SECOND("s"),
SECS("s"),
SEC("s"),
S("s"),
MINUTES("m"),
MINUTE("m"),
MINS("m"),
MIN("m"),
m("m"),
HOURS("h"),
HOUR("h"),
HRS("h"),
HR("h"),
H("h"),
DAYS("d"),
DAY("d"),
D("d"),
WEEKS("w"),
WEEK("w"),
W("w"),
MONTH("M"),
MONTHS("M"),
MON("M"),
M("M"),
QUARTERS("q"),
QUARTER("q"),
QTRS("q"),
QTR("q"),
Q("q"),
YEARS("y"),
YEAR("y"),
Y("y");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,33 +506,6 @@ public void testBinSpanWithStartEndNeverShrinkRange() throws IOException {
rows("39-40", 39));
}

@Test
public void testBinFloatingPointSpanBasicFunctionality() throws IOException {
JSONObject result =
executeQuery(
String.format(
"source=%s | bin age span=2.5 | fields age | head 3", TEST_INDEX_ACCOUNT));
verifySchema(result, schema("age", null, "string"));

// Test that floating point spans work with proper range formatting
verifyDataRows(result, rows("27.5-30.0"), rows("30.0-32.5"), rows("35.0-37.5"));
}

@Test
public void testBinFloatingPointSpanWithStats() throws IOException {
JSONObject result =
executeQuery(
String.format(
"source=%s | bin balance span=15000.5 | fields balance | sort balance |"
+ " head 2",
TEST_INDEX_ACCOUNT));

verifySchema(result, schema("balance", null, "string"));

// Test floating point spans without aggregation - verify proper decimal formatting
verifyDataRows(result, rows("0.0-15000.5"), rows("0.0-15000.5"));
}

@Test
@Ignore
public void testBinWithNumericSpanStatsCount() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ public void testAvgByTimeSpanAndFields() throws IOException {
JSONObject actual =
executeQuery(
String.format(
"source=%s | stats avg(balance) by span(birthdate, 1 month) as age_balance",
"source=%s | stats avg(balance) by span(birthdate, 1month) as age_balance",
TEST_INDEX_BANK));
verifySchema(actual, schema("age_balance", "timestamp"), schema("avg(balance)", "double"));
verifyDataRows(
Expand All @@ -473,7 +473,7 @@ public void testCountByCustomTimeSpanWithDifferentUnits() throws IOException {
JSONObject actual =
executeQuery(
String.format(
"source=%s | head 5 | stats count(datetime0) by span(datetime0, 15 minute) as"
"source=%s | head 5 | stats count(datetime0) by span(datetime0, 15minute) as"
+ " datetime_span",
TEST_INDEX_CALCS));
verifySchema(
Expand All @@ -489,7 +489,7 @@ public void testCountByCustomTimeSpanWithDifferentUnits() throws IOException {
actual =
executeQuery(
String.format(
"source=%s | head 5 | stats count(datetime0) by span(datetime0, 5 second) as"
"source=%s | head 5 | stats count(datetime0) by span(datetime0, 5second) as"
+ " datetime_span",
TEST_INDEX_CALCS));
verifySchema(
Expand All @@ -505,7 +505,7 @@ public void testCountByCustomTimeSpanWithDifferentUnits() throws IOException {
actual =
executeQuery(
String.format(
"source=%s | head 5 | stats count(datetime0) by span(datetime0, 3 month) as"
"source=%s | head 5 | stats count(datetime0) by span(datetime0, 3month) as"
+ " datetime_span",
TEST_INDEX_CALCS));
verifySchema(
Expand All @@ -519,7 +519,7 @@ public void testCountByNullableTimeSpan() throws IOException {
executeQuery(
String.format(
"source=%s | head 5 | stats count(datetime0), count(datetime1) by span(time1,"
+ " 15 minute) as time_span",
+ " 15minute) as time_span",
TEST_INDEX_CALCS));
verifySchema(
actual,
Expand All @@ -539,24 +539,23 @@ public void testCountByDateTypeSpanWithDifferentUnits() throws IOException {
JSONObject actual =
executeQuery(
String.format(
"source=%s | stats count(strict_date) by span(strict_date, 1 day) as"
+ " date_span",
"source=%s | stats count(strict_date) by span(strict_date, 1day) as" + " date_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(actual, schema("date_span", "date"), schema("count(strict_date)", "bigint"));
verifyDataRows(actual, rows(2, "1984-04-12"));

actual =
executeQuery(
String.format(
"source=%s | stats count(basic_date) by span(basic_date, 1 year) as" + " date_span",
"source=%s | stats count(basic_date) by span(basic_date, 1year) as" + " date_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(actual, schema("date_span", "date"), schema("count(basic_date)", "bigint"));
verifyDataRows(actual, rows(2, "1984-01-01"));

actual =
executeQuery(
String.format(
"source=%s | stats count(year_month_day) by span(year_month_day, 1 month)"
"source=%s | stats count(year_month_day) by span(year_month_day, 1month)"
+ " as date_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(actual, schema("date_span", "date"), schema("count(year_month_day)", "bigint"));
Expand All @@ -569,7 +568,7 @@ public void testCountByTimeTypeSpanWithDifferentUnits() throws IOException {
executeQuery(
String.format(
"source=%s | stats count(hour_minute_second) by span(hour_minute_second, 1"
+ " minute) as time_span",
+ "minute) as time_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(
actual, schema("time_span", "time"), schema("count(hour_minute_second)", "bigint"));
Expand All @@ -578,7 +577,7 @@ public void testCountByTimeTypeSpanWithDifferentUnits() throws IOException {
actual =
executeQuery(
String.format(
"source=%s | stats count(custom_time) by span(custom_time, 1 second) as"
"source=%s | stats count(custom_time) by span(custom_time, 1second) as"
+ " time_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(actual, schema("time_span", "time"), schema("count(custom_time)", "bigint"));
Expand All @@ -587,7 +586,7 @@ public void testCountByTimeTypeSpanWithDifferentUnits() throws IOException {
actual =
executeQuery(
String.format(
"source=%s | stats count(hour) by span(hour, 6 hour) as time_span",
"source=%s | stats count(hour) by span(hour, 6hour) as time_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(actual, schema("time_span", "time"), schema("count(hour)", "bigint"));
verifyDataRows(actual, rows(2, "06:00:00"));
Expand Down Expand Up @@ -687,7 +686,7 @@ public void testCountBySpanForCustomFormats() throws IOException {
executeQuery(
String.format(
"source=%s | stats count(custom_date_or_date) by span(custom_date_or_date, 1"
+ " month) as date_span",
+ "month) as date_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(
actual, schema("date_span", "date"), schema("count(custom_date_or_date)", "bigint"));
Expand All @@ -697,7 +696,7 @@ public void testCountBySpanForCustomFormats() throws IOException {
executeQuery(
String.format(
"source=%s | stats count(custom_date_or_custom_time) by"
+ " span(custom_date_or_custom_time, 1 hour) as timestamp_span",
+ " span(custom_date_or_custom_time, 1hour) as timestamp_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(
actual,
Expand All @@ -709,7 +708,7 @@ public void testCountBySpanForCustomFormats() throws IOException {
executeQuery(
String.format(
"source=%s | stats count(custom_no_delimiter_ts) by span(custom_no_delimiter_ts, 1"
+ " hour) as timestamp_span",
+ "hour) as timestamp_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(
actual,
Expand All @@ -721,7 +720,7 @@ public void testCountBySpanForCustomFormats() throws IOException {
executeQuery(
String.format(
"source=%s | stats count(incomplete_custom_time) by span(incomplete_custom_time, 12"
+ " hour) as time_span",
+ "hour) as time_span",
TEST_INDEX_DATE_FORMATS));
verifySchema(
actual, schema("time_span", "time"), schema("count(incomplete_custom_time)", "bigint"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -917,4 +917,77 @@ public void testSearchWithDateINOperator() throws IOException {
rows("2024-01-15 10:30:00.123456789", "INFO"),
rows("2024-01-15 10:30:01.23456789", "ERROR"));
}

@Test
public void testSearchWithTraceId() throws IOException {
// Test 1: Search for specific traceId
JSONObject specificTraceId =
executeQuery(
String.format(
"search source=%s b3cb01a03c846973fd496b973f49be85 | fields" + " traceId, body",
TEST_INDEX_OTEL_LOGS));
verifyDataRows(
specificTraceId,
rows(
"b3cb01a03c846973fd496b973f49be85",
"User e1ce63e6-8501-11f0-930d-c2fcbdc05f14 adding 4 of product HQTGWGPNH4 to cart"));
}

@Test
public void testSearchWithSpanLength() throws IOException {
// Test searching for SPANLENGTH keyword in free text search
// This tests that SPANLENGTH tokens like "3month" are searchable
JSONObject result =
executeQuery(
String.format(
"search source=%s 3month | fields body, `attributes.span.duration`",
TEST_INDEX_OTEL_LOGS));
verifyDataRows(result, rows("Processing data for 3month period", "3month"));
}

@Test
public void testSearchWithSpanLengthInField() throws IOException {
// Test searching for SPANLENGTH value in a specific field
JSONObject result =
executeQuery(
String.format(
"search source=%s `attributes.span.duration`=\\\"3month\\\" | fields body,"
+ " `attributes.span.duration`",
TEST_INDEX_OTEL_LOGS));
verifyDataRows(result, rows("Processing data for 3month period", "3month"));
}

@Test
public void testSearchWithNumericIdVsSpanLength() throws IOException {
// Test that NUMERIC_ID tokens like "1s4f7" (which start with what could be a SPANLENGTH like
// "1s")
// are properly searchable as complete tokens
// This verifies that NUMERIC_ID takes precedence over SPANLENGTH when applicable

// Test 1: Search for the NUMERIC_ID token in free text
JSONObject numericIdResult =
executeQuery(
String.format(
"search source=%s 1s4f7 | fields body, `attributes.transaction.id`",
TEST_INDEX_OTEL_LOGS));
verifyDataRows(numericIdResult, rows("Transaction ID 1s4f7 processed successfully", "1s4f7"));

// Test 2: Search for NUMERIC_ID in specific field
JSONObject fieldSearchResult =
executeQuery(
String.format(
"search source=%s `attributes.transaction.id`=1s4f7 | fields body,"
+ " `attributes.transaction.id`",
TEST_INDEX_OTEL_LOGS));
verifyDataRows(fieldSearchResult, rows("Transaction ID 1s4f7 processed successfully", "1s4f7"));

// Test 3: Verify that searching for just "1s" (which would be a SPANLENGTH)
// does NOT match the "1s4f7" token
JSONObject spanLengthSearchResult =
executeQuery(
String.format(
"search source=%s `attributes.transaction.id`=1s | fields body",
TEST_INDEX_OTEL_LOGS));
verifyDataRows(spanLengthSearchResult); // Should return no results
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"calcite": {
"logical": "LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(spanId=[$0], traceId=[$1], @timestamp=[$2], instrumentationScope=[$3], severityText=[$7], resource=[$8], flags=[$23], attributes=[$24], droppedAttributesCount=[$153], severityNumber=[$154], time=[$155], body=[$156])\n LogicalFilter(condition=[query_string(MAP('query', 'ERROR':VARCHAR))])\n CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_otel_logs]])\n",
"logical": "LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(spanId=[$0], traceId=[$1], @timestamp=[$2], instrumentationScope=[$3], severityText=[$7], resource=[$8], flags=[$23], attributes=[$24], droppedAttributesCount=[$162], severityNumber=[$163], time=[$164], body=[$165])\n LogicalFilter(condition=[query_string(MAP('query', 'ERROR':VARCHAR))])\n CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_otel_logs]])\n",
"physical": "CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_otel_logs]], PushDownContext=[[PROJECT->[spanId, traceId, @timestamp, instrumentationScope, severityText, resource, flags, attributes, droppedAttributesCount, severityNumber, time, body], FILTER->query_string(MAP('query', 'ERROR':VARCHAR)), LIMIT->10000], OpenSearchRequestBuilder(sourceBuilder={\"from\":0,\"size\":10000,\"timeout\":\"1m\",\"query\":{\"query_string\":{\"query\":\"ERROR\",\"fields\":[],\"type\":\"best_fields\",\"default_operator\":\"or\",\"max_determinized_states\":10000,\"enable_position_increments\":true,\"fuzziness\":\"AUTO\",\"fuzzy_prefix_length\":0,\"fuzzy_max_expansions\":50,\"phrase_slop\":0,\"escape\":false,\"auto_generate_synonyms_phrase_query\":true,\"fuzzy_transpositions\":true,\"boost\":1.0}},\"_source\":{\"includes\":[\"spanId\",\"traceId\",\"@timestamp\",\"instrumentationScope\",\"severityText\",\"resource\",\"flags\",\"attributes\",\"droppedAttributesCount\",\"severityNumber\",\"time\",\"body\"],\"excludes\":[]},\"sort\":[{\"_doc\":{\"order\":\"asc\"}}]}, requestedTotalSize=10000, pageSize=null, startFrom=0)])\n"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"calcite": {
"logical": "LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(spanId=[$0], traceId=[$1], @timestamp=[$2], instrumentationScope=[$3], severityText=[$7], resource=[$8], flags=[$23], attributes=[$24], droppedAttributesCount=[$153], severityNumber=[$154], time=[$155], body=[$156])\n LogicalFilter(condition=[query_string(MAP('query', 'severityNumber:>15':VARCHAR))])\n CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_otel_logs]])\n",
"logical": "LogicalSystemLimit(fetch=[10000], type=[QUERY_SIZE_LIMIT])\n LogicalProject(spanId=[$0], traceId=[$1], @timestamp=[$2], instrumentationScope=[$3], severityText=[$7], resource=[$8], flags=[$23], attributes=[$24], droppedAttributesCount=[$162], severityNumber=[$163], time=[$164], body=[$165])\n LogicalFilter(condition=[query_string(MAP('query', 'severityNumber:>15':VARCHAR))])\n CalciteLogicalIndexScan(table=[[OpenSearch, opensearch-sql_test_index_otel_logs]])\n",
"physical": "CalciteEnumerableIndexScan(table=[[OpenSearch, opensearch-sql_test_index_otel_logs]], PushDownContext=[[PROJECT->[spanId, traceId, @timestamp, instrumentationScope, severityText, resource, flags, attributes, droppedAttributesCount, severityNumber, time, body], FILTER->query_string(MAP('query', 'severityNumber:>15':VARCHAR)), LIMIT->10000], OpenSearchRequestBuilder(sourceBuilder={\"from\":0,\"size\":10000,\"timeout\":\"1m\",\"query\":{\"query_string\":{\"query\":\"severityNumber:>15\",\"fields\":[],\"type\":\"best_fields\",\"default_operator\":\"or\",\"max_determinized_states\":10000,\"enable_position_increments\":true,\"fuzziness\":\"AUTO\",\"fuzzy_prefix_length\":0,\"fuzzy_max_expansions\":50,\"phrase_slop\":0,\"escape\":false,\"auto_generate_synonyms_phrase_query\":true,\"fuzzy_transpositions\":true,\"boost\":1.0}},\"_source\":{\"includes\":[\"spanId\",\"traceId\",\"@timestamp\",\"instrumentationScope\",\"severityText\",\"resource\",\"flags\",\"attributes\",\"droppedAttributesCount\",\"severityNumber\",\"time\",\"body\"],\"excludes\":[]},\"sort\":[{\"_doc\":{\"order\":\"asc\"}}]}, requestedTotalSize=10000, pageSize=null, startFrom=0)])\n"
}
}
Loading
Loading