You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by "clintropolis (via GitHub)" <gi...@apache.org> on 2025/06/04 23:09:24 UTC
[PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
clintropolis opened a new pull request, #18078:
URL: https://github.com/apache/druid/pull/18078
### Description
changes:
* the cast expression will now be more lenient when casting arrays of primitive values. Previously single element arrays were allowed to be cast to scalar values, and more than 1 element would result in a type cast exception. Casting multi-element arrays will now create a null value in the target type instead of failing to be more consistent with other primitive value behavior, which return null if the cast cannot be completed. Complex value casting will still throw type cast exceptions.
* NestedFieldVirtualColumn has been simplified, removing many of its own selector implementations with built in casting in favor of delegating casts to expression selectors when casts are required
* fix several places in NestedFieldVirtualColumn which would allow creating selectors that were not consistent with the expected output type of the virtual column, leading to inconsistent query results depending on what kind of selector was created for the underlying field.
* NestedFieldVirtualColumn now uses JSON_VALUE and JSON_QUERY expressions when no underlying json columns are available to optimize (e.g. when using realtime queries), these selectors were very similar
* Adjusted test data to cover more of the inconsistencies and fixed tests which were encoding incorrect behavior
#### Release note
_tbd_
<hr>
This PR has:
- [ ] been self-reviewed.
- [ ] using the [concurrency checklist](https://github.com/apache/druid/blob/master/dev/code-review/concurrency.md) (Remove this item if the PR doesn't have any relation to concurrency.)
- [ ] added documentation for new or modified features or behaviors.
- [ ] a release note entry in the PR description.
- [ ] added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
- [ ] added or updated version, license, or notice information in [licenses.yaml](https://github.com/apache/druid/blob/master/dev/license.md)
- [ ] added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
- [ ] added unit tests or modified existing tests to cover new code paths, ensuring the threshold for [code coverage](https://github.com/apache/druid/blob/master/dev/code-review/code-coverage.md) is met.
- [ ] added integration tests.
- [ ] been tested in a test Druid cluster.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "clintropolis (via GitHub)" <gi...@apache.org>.
clintropolis commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2130412385
##########
sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java:
##########
@@ -6804,44 +6804,93 @@ public void testUnnestJsonQueryArraysJsonValueSum()
@Test
public void testJsonValueNestedEmptyArray()
Review Comment:
ah the all empty arrays column was a test for the type system during array ingestion, since there are no elements we cannot infer an array type so we just handle this column as the most restrictive array type, which is ARRAY<LONG>.
I think there should be some other objects/fields with arrays with values, or could be separately added to the input data file if there is some shape that isn't already covered rather than changing this one
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "clintropolis (via GitHub)" <gi...@apache.org>.
clintropolis commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2130397028
##########
processing/src/main/java/org/apache/druid/segment/virtual/ExpressionSelectors.java:
##########
@@ -132,35 +83,8 @@ public static ColumnValueSelector makeStringColumnValueSelector(
{
final ColumnValueSelector<ExprEval> baseSelector = makeExprEvalSelector(columnSelectorFactory, expression);
- return new ColumnValueSelector()
+ return new EvalUnwrappingColumnValueSelector(baseSelector)
Review Comment:
getObject is called per row, so i don't think we want to have a flag that must be checked every row by all implementations. This coerceEvalToObjectOrList is specific to multi-value strings, expression handling doesn't know about multi-value strings and so processes them as ARRAY<STRING>, but then must coerce them back into the List used by multi-value strings in the native layer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "clintropolis (via GitHub)" <gi...@apache.org>.
clintropolis commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2131069185
##########
processing/src/main/java/org/apache/druid/segment/nested/NestedFieldDictionaryEncodedColumn.java:
##########
@@ -163,11 +159,11 @@ public IndexedInts getMultiValueRow(int rowNum)
public String lookupName(int id)
{
final int globalId = dictionary.get(id);
- if (globalId < globalDictionary.size()) {
Review Comment:
ah just cleanup, i noticed while in debugger.. there are other similar places in code that re-use these precomputed values since these dictionaries don't ever change size and we already computed them for other reasons too, so was just cleaning up to re-use here too.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "cecemei (via GitHub)" <gi...@apache.org>.
cecemei commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2129903598
##########
processing/src/main/java/org/apache/druid/segment/virtual/ExpressionSelectors.java:
##########
@@ -132,35 +83,8 @@ public static ColumnValueSelector makeStringColumnValueSelector(
{
final ColumnValueSelector<ExprEval> baseSelector = makeExprEvalSelector(columnSelectorFactory, expression);
- return new ColumnValueSelector()
+ return new EvalUnwrappingColumnValueSelector(baseSelector)
Review Comment:
Maybe we can add a `coerceEvalToObjectOrList` boolean to EvalUnwrappingColumnValueSelector, so it encapsulates the `coerceEvalToObjectOrList` logic?
I also wonder maybe the flatten is not needed since we already cast single element array?
##########
sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java:
##########
@@ -7386,4 +7477,173 @@ public void testCountPathWithArraysReturning()
.build()
);
}
+
+ @Test
+ public void testSumPathWithArraysRealtime()
+ {
+ /*
+ "obj":{... "c": [100], ...}
+ "obj":{... "c": ["a", "b"], ...}
+ "obj":{...}
+ "obj":{... "c": {"a": 1}, ...},
+ "obj":{... "c": "hello", ...},
+ "obj":{... "c": 12.3, ...},
+ "obj":{... "c": null, ...},
+ */
+ skipVectorize();
+ testQuery(
+ "SELECT "
+ + "SUM(JSON_VALUE(obj, '$.c')) "
+ + "FROM druid.all_auto_realtime",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(DATA_SOURCE_ALL_REALTIME)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .virtualColumns(new NestedFieldVirtualColumn("obj", "$.c", "v0", ColumnType.DOUBLE))
+ .aggregators(aggregators(new DoubleSumAggregatorFactory("a0", "v0")))
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{112.3d}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.DOUBLE)
+ .build()
+ );
+ }
+
+ @Test
+ public void testCountPathWithArraysRealtime()
+ {
+ /*
+ "obj":{... "c": [100], ...}
+ "obj":{... "c": ["a", "b"], ...}
+ "obj":{...}
+ "obj":{... "c": {"a": 1}, ...},
+ "obj":{... "c": "hello", ...},
+ "obj":{... "c": 12.3, ...},
+ "obj":{... "c": null, ...},
+ */
+ // capturing existing behavior... the count should be 4 if it was counting all non-null primitive values, but that
+ // would mean that the virtual column would need to plan as ARRAY<STRING> expected type instead of STRING
+ // ... you might notice there are actually 5 non-null obj.c values, however json_value only returns primitive
+ // values, so the object row is rightfully skipped
+ skipVectorize();
+ testQuery(
Review Comment:
I guess we're saying single element array can be auto casted to string, while multiple elements array would return null, this sounds a bit unintuitive. Maybe we should also put down comments like, this is suboptimal and the purpose of this test case is just to reflect current behavior but could be improved.
##########
sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java:
##########
@@ -6804,44 +6804,93 @@ public void testUnnestJsonQueryArraysJsonValueSum()
@Test
public void testJsonValueNestedEmptyArray()
Review Comment:
nit: consider add a few values when '$.z.d' array is not empty, the test should be nested ~empty~ arrayright?
##########
sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java:
##########
@@ -7386,4 +7477,173 @@ public void testCountPathWithArraysReturning()
.build()
);
}
+
+ @Test
+ public void testSumPathWithArraysRealtime()
+ {
+ /*
+ "obj":{... "c": [100], ...}
+ "obj":{... "c": ["a", "b"], ...}
+ "obj":{...}
+ "obj":{... "c": {"a": 1}, ...},
+ "obj":{... "c": "hello", ...},
+ "obj":{... "c": 12.3, ...},
+ "obj":{... "c": null, ...},
+ */
+ skipVectorize();
+ testQuery(
+ "SELECT "
+ + "SUM(JSON_VALUE(obj, '$.c')) "
+ + "FROM druid.all_auto_realtime",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(DATA_SOURCE_ALL_REALTIME)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .virtualColumns(new NestedFieldVirtualColumn("obj", "$.c", "v0", ColumnType.DOUBLE))
+ .aggregators(aggregators(new DoubleSumAggregatorFactory("a0", "v0")))
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
Review Comment:
I was a bit suprised by this auto casting single element array to scalar, I wonder maybe array should be moved to JSON_QUERY, and the casting should be more an explicit requirement.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "clintropolis (via GitHub)" <gi...@apache.org>.
clintropolis commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2130496388
##########
sql/src/test/java/org/apache/druid/sql/calcite/CalciteNestedDataQueryTest.java:
##########
@@ -7386,4 +7477,173 @@ public void testCountPathWithArraysReturning()
.build()
);
}
+
+ @Test
+ public void testSumPathWithArraysRealtime()
+ {
+ /*
+ "obj":{... "c": [100], ...}
+ "obj":{... "c": ["a", "b"], ...}
+ "obj":{...}
+ "obj":{... "c": {"a": 1}, ...},
+ "obj":{... "c": "hello", ...},
+ "obj":{... "c": 12.3, ...},
+ "obj":{... "c": null, ...},
+ */
+ skipVectorize();
+ testQuery(
+ "SELECT "
+ + "SUM(JSON_VALUE(obj, '$.c')) "
+ + "FROM druid.all_auto_realtime",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(DATA_SOURCE_ALL_REALTIME)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .virtualColumns(new NestedFieldVirtualColumn("obj", "$.c", "v0", ColumnType.DOUBLE))
+ .aggregators(aggregators(new DoubleSumAggregatorFactory("a0", "v0")))
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
Review Comment:
With regards to the single element to scalar casting, this is just being consistent with Druids array handling in the expression layer, which is kind of swamped in multi-value string history as well (since arrays were first introduced as a way to perform array-like operations on multi-value string columns).
I certainly agree that it is weird that we can cast single element arrays to scalars, but i think changing that is a separate issue - and maybe a discussion worth having, since mvds are weird.
re: `json_query` vs `json_value`, arrays of primitives are treated themselves as a type of primitive, so it makes sense to me to still handle them with `json_value` since `json_value` is the only way to extract things out of json into other druid native types to use with non-json functions. `json_query` is a bit different in that it always returns a `COMPLEX<JSON>` type, which means it cannot directly be used with most other druid expressions without further extracting primitive values from it. For example you cannot use something like `array_contains(json_query(...))` because the json type is sort of opaque.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "clintropolis (via GitHub)" <gi...@apache.org>.
clintropolis commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2131106856
##########
processing/src/main/java/org/apache/druid/segment/virtual/ExpressionSelectors.java:
##########
@@ -169,19 +93,28 @@ public Object getObject()
ExprEval eval = baseSelector.getObject();
return coerceEvalToObjectOrList(eval);
}
+ };
+ }
+ public static ColumnValueSelector castColumnValueSelector(
Review Comment:
ah, right now it has to be non-null and would explode in `fromColumnTypeStrict`, however it doesn't really need to be... `ExprEval.ofType` falls back to `ExprEval.bestEffortOf` if type is null, so we could handle all casts with this. Will update PR and add some tests
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "gianm (via GitHub)" <gi...@apache.org>.
gianm commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2130579537
##########
processing/src/main/java/org/apache/druid/segment/AutoTypeColumnIndexer.java:
##########
@@ -444,10 +444,7 @@ public ColumnType getLogicalType()
}
if (fieldIndexers.size() == 1 && fieldIndexers.containsKey(NestedPathFinder.JSON_PATH_ROOT)) {
FieldIndexer rootField = fieldIndexers.get(NestedPathFinder.JSON_PATH_ROOT);
- ColumnType logicalType = null;
- for (ColumnType type : FieldTypeInfo.convertToSet(rootField.getTypes().getByteValue())) {
- logicalType = ColumnType.leastRestrictiveType(logicalType, type);
- }
+ ColumnType logicalType = ColumnType.leastRestrictiveType(FieldTypeInfo.convertToSet(rootField.getTypes().getByteValue()));
Review Comment:
Are all the changes like this one related to the patch / fixing anything, or just general code cleanup?
##########
processing/src/main/java/org/apache/druid/segment/nested/NestedFieldDictionaryEncodedColumn.java:
##########
@@ -163,11 +159,11 @@ public IndexedInts getMultiValueRow(int rowNum)
public String lookupName(int id)
{
final int globalId = dictionary.get(id);
- if (globalId < globalDictionary.size()) {
Review Comment:
are these related / fixing anything? Or just some code cleanup? Same question for the similar changes in `lookupGlobalScalarObject`.
##########
processing/src/main/java/org/apache/druid/segment/virtual/ExpressionSelectors.java:
##########
@@ -534,4 +467,63 @@ public static Object coerceEvalToObjectOrList(ExprEval eval)
}
return eval.valueOrDefault();
}
+
+ /**
+ * Wraps a {@link ColumnValueSelector<ExprEval>} and calls {@link ExprEval#valueOrDefault()} on the output of
+ * {@link #baseSelector#getObject()} in {@link #getObject()}.
+ */
+ private static class EvalUnwrappingColumnValueSelector implements ColumnValueSelector
+ {
+ private final ColumnValueSelector<ExprEval> baseSelector;
+
+ public EvalUnwrappingColumnValueSelector(ColumnValueSelector<ExprEval> baseSelector)
+ {
+ this.baseSelector = baseSelector;
+ }
+
+ @Override
+ public double getDouble()
+ {
+ return baseSelector.getDouble();
+ }
+
+ @Override
+ public float getFloat()
+ {
+ return baseSelector.getFloat();
+ }
+
+ @Override
+ public long getLong()
+ {
+ return baseSelector.getLong();
+ }
+
+ @Override
+ public boolean isNull()
+ {
+ return baseSelector.isNull();
+ }
+
+ @Nullable
+ @Override
+ public Object getObject()
+ {
+ // No need for null check on getObject() since baseSelector impls will never return null.
+ ExprEval eval = baseSelector.getObject();
+ return eval.valueOrDefault();
Review Comment:
`valueOrDefault` is pointless now, right? I think it should be the same as `eval.value()` since the removal of legacy null handling.
##########
processing/src/main/java/org/apache/druid/segment/virtual/ExpressionSelectors.java:
##########
@@ -169,19 +93,28 @@ public Object getObject()
ExprEval eval = baseSelector.getObject();
return coerceEvalToObjectOrList(eval);
}
+ };
+ }
+ public static ColumnValueSelector castColumnValueSelector(
Review Comment:
Some javadoc would be useful here. Also I'm wondering which parameters are nullable, if any? (Can the `delegateType` be null or must it be known?)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "clintropolis (via GitHub)" <gi...@apache.org>.
clintropolis merged PR #18078:
URL: https://github.com/apache/druid/pull/18078
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org
Re: [PR] more lenient array expression casting, fix inconsistencies and simplify json_value (druid)
Posted by "clintropolis (via GitHub)" <gi...@apache.org>.
clintropolis commented on code in PR #18078:
URL: https://github.com/apache/druid/pull/18078#discussion_r2131072699
##########
processing/src/main/java/org/apache/druid/segment/AutoTypeColumnIndexer.java:
##########
@@ -444,10 +444,7 @@ public ColumnType getLogicalType()
}
if (fieldIndexers.size() == 1 && fieldIndexers.containsKey(NestedPathFinder.JSON_PATH_ROOT)) {
FieldIndexer rootField = fieldIndexers.get(NestedPathFinder.JSON_PATH_ROOT);
- ColumnType logicalType = null;
- for (ColumnType type : FieldTypeInfo.convertToSet(rootField.getTypes().getByteValue())) {
- logicalType = ColumnType.leastRestrictiveType(logicalType, type);
- }
+ ColumnType logicalType = ColumnType.leastRestrictiveType(FieldTypeInfo.convertToSet(rootField.getTypes().getByteValue()));
Review Comment:
just cleanup, was writing one of these loops and was certain i had done it a number of times before so searched and sure enough many usages were loops like this, so add the new static method and replaced the existing loops
##########
processing/src/main/java/org/apache/druid/segment/virtual/ExpressionSelectors.java:
##########
@@ -534,4 +467,63 @@ public static Object coerceEvalToObjectOrList(ExprEval eval)
}
return eval.valueOrDefault();
}
+
+ /**
+ * Wraps a {@link ColumnValueSelector<ExprEval>} and calls {@link ExprEval#valueOrDefault()} on the output of
+ * {@link #baseSelector#getObject()} in {@link #getObject()}.
+ */
+ private static class EvalUnwrappingColumnValueSelector implements ColumnValueSelector
+ {
+ private final ColumnValueSelector<ExprEval> baseSelector;
+
+ public EvalUnwrappingColumnValueSelector(ColumnValueSelector<ExprEval> baseSelector)
+ {
+ this.baseSelector = baseSelector;
+ }
+
+ @Override
+ public double getDouble()
+ {
+ return baseSelector.getDouble();
+ }
+
+ @Override
+ public float getFloat()
+ {
+ return baseSelector.getFloat();
+ }
+
+ @Override
+ public long getLong()
+ {
+ return baseSelector.getLong();
+ }
+
+ @Override
+ public boolean isNull()
+ {
+ return baseSelector.isNull();
+ }
+
+ @Nullable
+ @Override
+ public Object getObject()
+ {
+ // No need for null check on getObject() since baseSelector impls will never return null.
+ ExprEval eval = baseSelector.getObject();
+ return eval.valueOrDefault();
Review Comment:
yes, i was thinking about that while working on this branch, was thinking removing in a follow-up PR
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org