github-actions[bot] commented on code in PR #67673:
URL: https://github.com/apache/doris/pull/67673#discussion_r3978208049


##########
fe/fe-core/src/main/java/org/apache/doris/catalog/AIResource.java:
##########
@@ -127,7 +135,8 @@ public void modifyProperties(Map<String, String> 
properties) throws DdlException
         writeLock();
         for (Map.Entry<String, String> kv : properties.entrySet()) {
             replaceIfEffectiveValue(this.properties, kv.getKey(), 
kv.getValue());
-            if (kv.getKey().equals(AIProperties.API_KEY)) {
+            if (kv.getKey().equals(AIProperties.API_KEY)
+                    || kv.getKey().equals(AIProperties.EMBED_API_KEY)) {

Review Comment:
   [P2] Validate the merged resource under the write lock. Two concurrent, 
individually valid ALTERs can currently commit an invalid embed group: from 
OPENAI plus a key, one request can set LOCAL and clear the key while another 
sets GEMINI; both validate the old snapshot, then LOCAL/empty followed by 
GEMINI leaves a remote provider with no key. Both calls succeed and the invalid 
resource is journaled. Build, validate, normalize, and install the merged 
snapshot as one locked transition, and add an interleaving test.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/property/constants/AIProperties.java:
##########
@@ -53,29 +58,31 @@ public class AIProperties extends BaseProperties {
     public static final String VALIDITY_CHECK = "ai.validity_check";
 
     public static final List<String> REQUIRED_FIELDS = Arrays.asList(ENDPOINT, 
PROVIDER_TYPE, MODEL_NAME);
+    public static final List<String> EMBED_REQUIRED_FIELDS =
+            Arrays.asList(EMBED_ENDPOINT, EMBED_PROVIDER_TYPE, 
EMBED_MODEL_NAME);
     public static final List<String> PROVIDERS
             = Arrays.asList("OPENAI", "LOCAL", "GEMINI", "DEEPSEEK", 
"ANTHROPIC",
             "MOONSHOT", "QWEN", "MINIMAX", "ZHIPU", "BAICHUAN", "VOYAGEAI", 
"JINA");
+    public static final List<String> EFFORT_LEVELS =
+            Arrays.asList("none", "minimal", "low", "medium", "high", "xhigh", 
"max");
 
     public static void requiredAIProperties(Map<String, String> properties) 
throws DdlException {
-        // Check required field
-        for (String field : REQUIRED_FIELDS) {
-            if (Strings.isNullOrEmpty(properties.get(field))) {
-                throw new DdlException("Missing [" + field + "] in 
properties.");
-            }
+        boolean hasGeneralProperties = hasAnyProperty(properties, 
REQUIRED_FIELDS, API_KEY);
+        boolean hasEmbedProperties = hasAnyProperty(properties, 
EMBED_REQUIRED_FIELDS, EMBED_API_KEY);
+        if (!hasGeneralProperties && !hasEmbedProperties) {
+            throw new DdlException("At least one complete AI property group 
must be configured.");
         }
 
-        // Check the provider is valid
-        properties.put(PROVIDER_TYPE, 
properties.get(PROVIDER_TYPE).toUpperCase());
-        if (PROVIDERS.stream().noneMatch(s -> 
s.equals(properties.get(PROVIDER_TYPE).toUpperCase()))) {
-            throw new DdlException("Provider must be one of " + PROVIDERS);
+        if (hasGeneralProperties) {
+            validatePropertyGroup(properties, REQUIRED_FIELDS, PROVIDER_TYPE, 
API_KEY);
+        }
+        if (hasEmbedProperties) {
+            validatePropertyGroup(properties, EMBED_REQUIRED_FIELDS, 
EMBED_PROVIDER_TYPE, EMBED_API_KEY);

Review Comment:
   [P2] Validate dedicated groups against embedding-capable providers. This 
path accepts every generation provider, including DEEPSEEK and MOONSHOT, but 
their BE adapters explicitly return NotSupported from both embedding methods. 
An embed-only resource therefore passes CREATE and analysis yet can never 
execute. Use a capability-specific allowlist (or shared capability metadata) 
and cover rejected providers in FE tests.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ai/AIFunction.java:
##########
@@ -61,6 +61,10 @@ public void checkLegalityAfterRewrite() {
             if (!(resource instanceof AIResource)) {
                 throw new AnalysisException("AI resource '" + resourceName + 
"' does not exist");
             }
+            if (!((AIResource) resource).hasCompleteGeneralProperties()) {

Review Comment:
   [P1] Pin the resource validated for this statement. The check is against one 
object, but only its name is registered; a concurrent DROP followed by CREATE 
of a valid embed-only resource under the same name makes both transport 
builders serialize the replacement. Scalar AI then creates an adapter from an 
empty general provider and hits `DORIS_CHECK(adapter)` (`AI_AGG` dereferences 
it). Store the validated snapshot/identity in `StatementContext`, or revalidate 
and snapshot atomically at transport, and add a DROP/CREATE interleaving test.



##########
fe/fe-core/src/main/java/org/apache/doris/catalog/AIResource.java:
##########
@@ -159,10 +169,33 @@ protected void getProcNodeData(BaseProcResult result) {
 
     public TAIResource toThrift() throws NumberFormatException {
         TAIResource tAIResource = new TAIResource();
-        
tAIResource.setProviderType(properties.get(AIProperties.PROVIDER_TYPE));
-        tAIResource.setEndpoint(properties.get(AIProperties.ENDPOINT));
-        tAIResource.setApiKey(properties.get(AIProperties.API_KEY));
-        tAIResource.setModelName(properties.get(AIProperties.MODEL_NAME));
+        if (properties.containsKey(AIProperties.PROVIDER_TYPE)) {
+            
tAIResource.setProviderType(properties.get(AIProperties.PROVIDER_TYPE));
+        }
+        if (properties.containsKey(AIProperties.ENDPOINT)) {
+            tAIResource.setEndpoint(properties.get(AIProperties.ENDPOINT));
+        }
+        if (properties.containsKey(AIProperties.API_KEY)) {
+            tAIResource.setApiKey(properties.get(AIProperties.API_KEY));
+        }
+        if (properties.containsKey(AIProperties.MODEL_NAME)) {
+            tAIResource.setModelName(properties.get(AIProperties.MODEL_NAME));
+        }
+        if (properties.containsKey(AIProperties.EMBED_PROVIDER_TYPE)) {
+            
tAIResource.setEmbedProviderType(properties.get(AIProperties.EMBED_PROVIDER_TYPE));
+        }
+        if (properties.containsKey(AIProperties.EMBED_ENDPOINT)) {
+            
tAIResource.setEmbedEndpoint(properties.get(AIProperties.EMBED_ENDPOINT));
+        }
+        if (properties.containsKey(AIProperties.EMBED_API_KEY)) {
+            
tAIResource.setEmbedApiKey(properties.get(AIProperties.EMBED_API_KEY));
+        }
+        if (properties.containsKey(AIProperties.EMBED_MODEL_NAME)) {
+            
tAIResource.setEmbedModelName(properties.get(AIProperties.EMBED_MODEL_NAME));
+        }
+        if (!Strings.isNullOrEmpty(properties.get(AIProperties.EFFORT))) {

Review Comment:
   [P2] Preserve an empty effort as an actual clear on ALTER. The validator 
accepts `ai.effort=""` and this branch defines empty as omitted, but 
`modifyProperties` routes it through `replaceIfEffectiveValue`, which ignores 
empty strings; only the API-key fields get an explicit empty-value override. 
After setting `high`, an ALTER to `""` succeeds and increments the version 
while `high` remains stored and forwarded. Remove/store this optional field 
explicitly and test set -> empty through persistence and 
`toThrift().isSetEffort()`.



##########
be/test/ai/aggregate_function_ai_agg_test.cpp:
##########
@@ -214,6 +214,136 @@ TEST_F(AggregateFunctionAIAggTest, 
serialize_deserialize_test) {
     _agg_function->destroy(place2);
 }
 
+TEST_F(AggregateFunctionAIAggTest, 
serialize_effort_only_for_supported_exec_version) {
+    TAIResource ai_resource;
+    ai_resource.provider_type = "MOCK";
+    ai_resource.model_name = "mock_model";
+    ai_resource.endpoint = "http://localhost";;
+    ai_resource.api_key = "xxx";
+    ai_resource.temperature = 0.5;
+    ai_resource.max_tokens = 16;
+    ai_resource.max_retries = 1;
+    ai_resource.retry_delay_second = 1;
+    ai_resource.dimensions = 514;
+    ai_resource.effort = "high";
+    _query_ctx->set_ai_resources(
+            std::map<std::string, TAIResource> {{"effort_resource", 
ai_resource}});
+
+    auto resource_col = ColumnString::create();
+    auto text_col = ColumnString::create();
+    auto task_col = ColumnString::create();
+    resource_col->insert_data("effort_resource", 15);
+    text_col->insert_data("test", 4);
+    task_col->insert_data("summarize", 9);
+
+    std::unique_ptr<char[]> memory(new char[_agg_function->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    _agg_function->create(place);
+    const IColumn* columns[3] = {resource_col.get(), text_col.get(), 
task_col.get()};
+    _agg_function->add(place, columns, 0, _arena);
+
+    _agg_function->set_version(14);
+    auto legacy_column = _agg_function->create_serialize_column();
+    _agg_function->serialize_without_key_to_column(place, *legacy_column);
+
+    _agg_function->set_version(15);
+    auto effort_column = _agg_function->create_serialize_column();
+    _agg_function->serialize_without_key_to_column(place, *effort_column);
+
+    const StringRef legacy_state = legacy_column->get_data_at(0);
+    const StringRef effort_state = effort_column->get_data_at(0);
+
+    ColumnString serialized_effort;
+    VectorBufferWriter effort_writer(serialized_effort);
+    effort_writer.write_binary(std::string("high"));
+    effort_writer.commit();
+    const StringRef expected_suffix = serialized_effort.get_data_at(0);
+
+    ASSERT_EQ(effort_state.size, legacy_state.size + expected_suffix.size);
+    EXPECT_EQ(std::string_view(effort_state.data + legacy_state.size, 
expected_suffix.size),
+              std::string_view(expected_suffix.data, expected_suffix.size));
+
+    _agg_function->destroy(place);
+}
+
+TEST_F(AggregateFunctionAIAggTest, 
deserialize_legacy_multi_row_state_does_not_read_effort) {
+    TAIResource ai_resource;
+    ai_resource.provider_type = "MOCK";
+    ai_resource.model_name = "mock_model";
+    ai_resource.endpoint = "http://localhost";;
+    ai_resource.api_key = "xxx";
+    ai_resource.temperature = 0.5;
+    ai_resource.max_tokens = 16;
+    ai_resource.max_retries = 1;
+    ai_resource.retry_delay_second = 1;
+    ai_resource.dimensions = 514;
+    ai_resource.effort = "high";
+    _query_ctx->set_ai_resources(
+            std::map<std::string, TAIResource> {{"effort_resource", 
ai_resource}});
+
+    auto resource_col = ColumnString::create();
+    auto text_col = ColumnString::create();
+    auto task_col = ColumnString::create();
+    resource_col->insert_data("effort_resource", 15);
+    text_col->insert_data("test", 4);
+    task_col->insert_data("summarize", 9);
+
+    std::unique_ptr<char[]> source_memory(new 
char[_agg_function->size_of_data()]);
+    AggregateDataPtr source_place = source_memory.get();
+    _agg_function->create(source_place);
+    const IColumn* columns[3] = {resource_col.get(), text_col.get(), 
task_col.get()};
+    _agg_function->add(source_place, columns, 0, _arena);
+
+    _agg_function->set_version(15);
+    auto current_format_column = _agg_function->create_serialize_column();
+    _agg_function->serialize_without_key_to_column(source_place, 
*current_format_column);
+    const StringRef current_format_state = 
current_format_column->get_data_at(0);
+
+    ColumnString serialized_effort;
+    VectorBufferWriter effort_writer(serialized_effort);
+    effort_writer.write_binary(std::string("high"));
+    effort_writer.commit();
+    const size_t effort_size = serialized_effort.get_data_at(0).size;
+
+    auto legacy_column = ColumnString::create();
+    for (int i = 0; i < 3; ++i) {
+        legacy_column->insert_data(current_format_state.data,
+                                   current_format_state.size - effort_size);
+    }
+
+    std::unique_ptr<char[]> merged_memory(new 
char[_agg_function->size_of_data()]);
+    AggregateDataPtr merged_place = merged_memory.get();
+    _agg_function->create(merged_place);
+    _agg_function->set_version(14);
+    _agg_function->deserialize_and_merge_from_column_range(merged_place, 
*legacy_column, 0, 1,
+                                                           _arena);
+
+    const auto& merged_data =

Review Comment:
   [P2] Apply clang-format v16 here. The exact-head Clang Formatter check is 
failing, and a local non-mutating run over all changed BE files isolates this 
declaration; the repository formatter places the declaration and cast on one 
line.



-- 
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: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to