Skip to content
Open
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 conf/db/upgrade/V5.5.22__schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- ZSTAC-84025: Add pipelineTag to ModelVO for inference template auto-matching
CALL ADD_COLUMN('ModelVO', 'pipelineTag', 'VARCHAR(64)', 1, NULL);

-- ZSTAC-84025: Add isDefault to ModelServiceRefVO to mark the default inference template per model
ALTER TABLE `zstack`.`ModelServiceRefVO` ADD COLUMN `isDefault` TINYINT(1) NOT NULL DEFAULT 0;

-- ZSTAC-84025-F2: Add manifestJson to ModelVO so Step 1 (file format) of the auto-match Matcher can
-- parse file_types/file_extensions from the manifest returned by the aios agent.
CALL ADD_COLUMN('ModelVO', 'manifestJson', 'TEXT', 1, NULL);

-- ZSTAC-84025: Add createDate/lastOpDate to ModelServiceRefVO so the auto-match Matcher can
-- pick the earliest isDefault=true row when DB has the rare 2+ defaults anomaly (Q5).
ALTER TABLE `zstack`.`ModelServiceRefVO` ADD COLUMN `lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
ALTER TABLE `zstack`.`ModelServiceRefVO` ADD COLUMN `createDate` TIMESTAMP NULL DEFAULT NULL;

DROP PROCEDURE IF EXISTS backfill_model_service_ref_create_date;
DELIMITER $$
CREATE PROCEDURE backfill_model_service_ref_create_date()
BEGIN
UPDATE `zstack`.`ModelServiceRefVO`
SET `createDate` = CURRENT_TIMESTAMP
WHERE `createDate` IS NULL OR `createDate` = '0000-00-00 00:00:00';
END $$
DELIMITER ;
CALL backfill_model_service_ref_create_date();
DROP PROCEDURE IF EXISTS backfill_model_service_ref_create_date;

-- Older MySQL/MariaDB versions allow only one TIMESTAMP column with CURRENT_TIMESTAMP
-- in DEFAULT or ON UPDATE. lastOpDate already uses it, so keep createDate non-zero
-- and let ModelServiceRefVO.@PrePersist populate the real creation time for new rows.
ALTER TABLE `zstack`.`ModelServiceRefVO` MODIFY COLUMN `createDate` TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:00';
Comment on lines +28 to +31
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 在 ZStack 代码库中搜索 MySQL 最低版本要求的声明
rg -rn --type=xml --type=properties --type=txt -i "mysql.*version|minimum.*mysql|mariadb.*version" \
  -g '!**/target/**' -g '!**/node_modules/**' | head -40

# 也检查 pom.xml 或 build 配置中对 mysql-connector 的版本约束
rg -rn "mysql-connector|mysql.version" --type=xml | head -20

Repository: MatheMatrix/zstack

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Search for MySQL version requirements in documentation and config files
echo "=== Searching for MySQL version mentions in README and docs ==="
fd -e md -e txt . | xargs rg -l "mysql\|mariadb" -i | head -10 | while read f; do
  echo "File: $f"
  rg -C 3 "version|5\.[0-9]\.[0-9]" -i "$f" | head -20
done

echo "=== Searching in pom.xml for mysql-connector version ==="
fd pom.xml | xargs cat | rg -A 5 -B 5 "mysql-connector|mysql.*version" -i

Repository: MatheMatrix/zstack

Length of output: 1304


🌐 Web query:

MySQL 5.6.5 TIMESTAMP CURRENT_TIMESTAMP multiple columns restriction history

💡 Result:

In MySQL 5.6.5 and later, you can have multiple TIMESTAMP columns in the same table that use the special automatic clauses DEFAULT CURRENT_TIMESTAMP and/or ON UPDATE CURRENT_TIMESTAMP; this restriction (only one TIMESTAMP column per table could have those automatic properties) was lifted in MySQL 5.6.5 [1][2]. What the “restriction history” looks like - Before 5.6.5, at most one TIMESTAMP column per table could be automatically initialized or updated to the current date and time; attempting to use CURRENT_TIMESTAMP defaults/updates on more than one TIMESTAMP column would fail (or be disallowed by the server/UI), depending on context [1][3]. - In MySQL 5.6.5 (released 2012-04-10 per 5.6 release notes), “This restriction has been lifted. Any TIMESTAMP column definition can have any combination of DEFAULT CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP clauses.” The same changelog text also notes that these clauses can now be used with DATETIME definitions [1]. - MySQL’s current documentation (the referenced “Automatic Initialization and Updating for TIMESTAMP and DATETIME” section) describes the modern rules: TIMESTAMP/DATETIME columns can be specified to have current-timestamp behavior for default and/or auto-update, and different columns can have different combinations [2]. Practical notes for your specific query - If you’re on MySQL 5.6.5+: you can define multiple columns like: - col1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP - col2 TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP and MySQL will allow it [2][1]. - The server behavior is also affected by explicit_defaults_for_timestamp (documented): when it’s disabled, MySQL may implicitly apply DEFAULT CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP to the first TIMESTAMP column if neither is specified explicitly [2]. That’s not a “5.6.5 multiple columns restriction” anymore, but it can affect which columns end up with automatic defaults.

Citations:


🏁 Script executed:

#!/bin/bash
# Read the actual SQL file to verify the exact code and context
cat -n conf/db/upgrade/V5.5.22__schema.sql

Repository: MatheMatrix/zstack

Length of output: 2085


createDate 的哨兵默认值 '2000-01-01 00:00:00' 基于已过时的 MySQL 限制,应改为 CURRENT_TIMESTAMP

第 28–30 行的注释解释了不能使用 DEFAULT CURRENT_TIMESTAMP 的原因——"旧版 MySQL/MariaDB 每张表只允许一列 TIMESTAMP 使用 CURRENT_TIMESTAMP"。然而,MySQL 5.6.5(2012 年 4 月发布)已经取消了该限制,允许任意数量的 TIMESTAMP 列使用 DEFAULT CURRENT_TIMESTAMPON UPDATE CURRENT_TIMESTAMP 的任意组合。

ZStack 的 pom.xml 依赖 mysql-connector-j 8.2.0,明确表示项目针对 MySQL 8.x 开发和测试。这说明没有对 MySQL 5.6.5 之前版本的支持需求。因此,当前的 DEFAULT '2000-01-01 00:00:00' 绕过方案依据的前提已不成立。

该哨兵值带来的风险是:如果 ModelServiceRefVO.@PrePersist 因故障、异常或被绕过而无法执行,任何未显式设置 createDate 的新 INSERT 都会记录一个在 2000 年的错误时间戳,导致该行数据的时间信息彻底失真,难以在后续业务逻辑(如"挑选最早的 isDefault=true 行")中正确处理。

🛠️ 建议修复
-ALTER TABLE `zstack`.`ModelServiceRefVO` MODIFY COLUMN `createDate` TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:00';
+ALTER TABLE `zstack`.`ModelServiceRefVO` MODIFY COLUMN `createDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- Older MySQL/MariaDB versions allow only one TIMESTAMP column with CURRENT_TIMESTAMP
-- in DEFAULT or ON UPDATE. lastOpDate already uses it, so keep createDate non-zero
-- and let ModelServiceRefVO.@PrePersist populate the real creation time for new rows.
ALTER TABLE `zstack`.`ModelServiceRefVO` MODIFY COLUMN `createDate` TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:00';
-- Older MySQL/MariaDB versions allow only one TIMESTAMP column with CURRENT_TIMESTAMP
-- in DEFAULT or ON UPDATE. lastOpDate already uses it, so keep createDate non-zero
-- and let ModelServiceRefVO.@PrePersist populate the real creation time for new rows.
ALTER TABLE `zstack`.`ModelServiceRefVO` MODIFY COLUMN `createDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@conf/db/upgrade/V5.5.22__schema.sql` around lines 28 - 31, The schema uses a
sentinel default '2000-01-01 00:00:00' for ModelServiceRefVO.createDate based on
an outdated MySQL limitation; change the column definition to use DEFAULT
CURRENT_TIMESTAMP (keeping NOT NULL) so new rows get the actual DB timestamp,
and update any logic that assumes the sentinel (including
ModelServiceRefVO.@PrePersist) to tolerate/avoid relying on the sentinel
value—ensure the ALTER TABLE for ModelServiceRefVO modifies createDate to
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP and verify `@PrePersist` does not
overwrite or depend on a pre-set sentinel.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment from ye.zou:

这条建议我不采纳,当前改成非 CURRENT_TIMESTAMP 是为了兼容本分支实际验证环境里的 MySQL 5.7 行为:explicit_defaults_for_timestamp=OFF 时,lastOpDate 已带 ON UPDATE CURRENT_TIMESTAMP,再把 createDate 改为 DEFAULT CURRENT_TIMESTAMP 会直接触发本次失败日志里的错误:

Error Code : 1293 Incorrect table definition; there can be only one TIMESTAMP column with CURRENT_TIMESTAMP in DEFAULT or ON UPDATE clause

另外 Java 侧 ModelServiceRefVO.@PrePersist 已在新行写入真实创建时间,2000-01-01 00:00:00 只是为了让历史/绕过 JPA 的插入不违反 NOT NULL,并不作为业务排序依赖。这个 schema 改动已经在本地 PR docker 跑过 AutoMatchModelServiceCase,可以通过该迁移阶段。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zstack-robot-2,感谢 ye.zou 的详细解释,完全理解了。在 MySQL 5.7 且 explicit_defaults_for_timestamp=OFF 的环境下,Error 1293 确实会重新生效——即便 MySQL 5.6.5 在标准模式下解除了该限制,该模式下的行为仍然是每表只允许一列 TIMESTAMP 使用 CURRENT_TIMESTAMP。lastOpDate 已占用该位,createDate 使用 '2000-01-01 00:00:00' 作为 NOT NULL 占位是正确的处理方式,撤回该建议。


✏️ Learnings added
Learnt from: zstack-robot-2
Repo: MatheMatrix/zstack PR: 3891
File: conf/db/upgrade/V5.5.22__schema.sql:28-31
Timestamp: 2026-05-07T09:44:01.429Z
Learning: In ZStack upgrade scripts (e.g., conf/db/upgrade/V5.5.22__schema.sql), when a table already has a TIMESTAMP column with `ON UPDATE CURRENT_TIMESTAMP` (e.g., `lastOpDate`), the second TIMESTAMP column (e.g., `createDate`) must NOT use `DEFAULT CURRENT_TIMESTAMP`. The validated environment runs MySQL 5.7 with `explicit_defaults_for_timestamp=OFF`, which reintroduces the Error 1293 restriction ("there can be only one TIMESTAMP column with CURRENT_TIMESTAMP in DEFAULT or ON UPDATE clause") even though MySQL 5.6.5+ lifted it in standard mode. The intentional workaround is to use a sentinel default `'2000-01-01 00:00:00'` (NOT NULL) and rely on `ModelServiceRefVO.PrePersist` in Java to populate the real creation timestamp for new JPA-managed rows.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: zstack-robot-2
Repo: MatheMatrix/zstack PR: 3416
File: conf/db/upgrade/V5.5.12__schema.sql:24-35
Timestamp: 2026-03-05T02:13:23.339Z
Learning: In ZStack, the database schema name is fixed as 'zstack'. For SQL upgrade scripts under conf/db/upgrade, continue using TABLE_SCHEMA = 'zstack' and reference tables as zstack.`UsedIpVO` (i.e., qualify with the schema). Do not replace with DATABASE() or remove the schema qualifier for portability. This pattern can be assumed across upgrade scripts in this directory.

4 changes: 4 additions & 0 deletions sdk/src/main/java/SourceClassMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class SourceClassMap {
put("org.zstack.ai.entity.VmModelMountStatus", "org.zstack.sdk.VmModelMountStatus");
put("org.zstack.ai.message.ArchitectureImageMapping", "org.zstack.sdk.ArchitectureImageMapping");
put("org.zstack.ai.message.MaaSUsage", "org.zstack.sdk.MaaSUsage");
put("org.zstack.ai.message.MatchEvidence", "org.zstack.sdk.MatchEvidence");
put("org.zstack.ai.message.MatchedStep", "org.zstack.sdk.MatchedStep");
put("org.zstack.ai.message.ModelCenterServiceInventory", "org.zstack.sdk.ModelCenterServiceInventory");
put("org.zstack.ai.message.ModelCenterServiceInventory$MetaServerService", "org.zstack.sdk.MetaServerService");
put("org.zstack.ai.message.ModelCenterServiceInventory$ServiceStatus", "org.zstack.sdk.ServiceStatus");
Expand Down Expand Up @@ -1241,6 +1243,8 @@ public class SourceClassMap {
put("org.zstack.sdk.LunInventory", "org.zstack.header.storageDevice.LunInventory");
put("org.zstack.sdk.MaaSUsage", "org.zstack.ai.message.MaaSUsage");
put("org.zstack.sdk.ManagementNodeInventory", "org.zstack.header.managementnode.ManagementNodeInventory");
put("org.zstack.sdk.MatchEvidence", "org.zstack.ai.message.MatchEvidence");
put("org.zstack.sdk.MatchedStep", "org.zstack.ai.message.MatchedStep");
put("org.zstack.sdk.MdevDeviceChooser", "org.zstack.pciDevice.virtual.vfio_mdev.MdevDeviceChooser");
put("org.zstack.sdk.MdevDeviceInventory", "org.zstack.pciDevice.virtual.vfio_mdev.MdevDeviceInventory");
put("org.zstack.sdk.MdevDeviceSpecInventory", "org.zstack.pciDevice.specification.mdev.MdevDeviceSpecInventory");
Expand Down
3 changes: 3 additions & 0 deletions sdk/src/main/java/org/zstack/sdk/AddModelAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ public Result throwExceptionIfError() {
@Param(required = false, validValues = {"Public"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
public java.lang.String shareMode;

@Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
public java.lang.String defaultModelServiceUuid;

@Param(required = false)
public java.lang.String resourceUuid;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.zstack.sdk;

import java.util.HashMap;
import java.util.Map;
import org.zstack.sdk.*;

public class AutoMatchModelServiceByModelAction extends AbstractAction {

private static final HashMap<String, Parameter> parameterMap = new HashMap<>();

private static final HashMap<String, Parameter> nonAPIParameterMap = new HashMap<>();

public static class Result {
public ErrorCode error;
public org.zstack.sdk.AutoMatchModelServiceByModelResult value;

public Result throwExceptionIfError() {
if (error != null) {
throw new ApiException(
String.format("error[code: %s, description: %s, details: %s, globalErrorCode: %s]", error.code, error.description, error.details, error.globalErrorCode)
);
}

return this;
}
}

@Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
public java.lang.String modelUuid;

@Param(required = false)
public java.util.List systemTags;

@Param(required = false)
public java.util.List userTags;

@Param(required = false)
public String sessionId;

@Param(required = false)
public String accessKeyId;

@Param(required = false)
public String accessKeySecret;

@Param(required = false)
public String requestIp;


private Result makeResult(ApiResult res) {
Result ret = new Result();
if (res.error != null) {
ret.error = res.error;
return ret;
}

org.zstack.sdk.AutoMatchModelServiceByModelResult value = res.getResult(org.zstack.sdk.AutoMatchModelServiceByModelResult.class);
ret.value = value == null ? new org.zstack.sdk.AutoMatchModelServiceByModelResult() : value;

return ret;
}

public Result call() {
ApiResult res = ZSClient.call(this);
return makeResult(res);
}

public void call(final Completion<Result> completion) {
ZSClient.call(this, new InternalCompletion() {
@Override
public void complete(ApiResult res) {
completion.complete(makeResult(res));
}
});
}

protected Map<String, Parameter> getParameterMap() {
return parameterMap;
}

protected Map<String, Parameter> getNonAPIParameterMap() {
return nonAPIParameterMap;
}

protected RestInfo getRestInfo() {
RestInfo info = new RestInfo();
info.httpMethod = "GET";
info.path = "/ai/models/{modelUuid}/auto-match-service";
info.needSession = true;
info.needPoll = false;
info.parameterName = "";
return info;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.zstack.sdk;

import org.zstack.sdk.MatchedStep;
import org.zstack.sdk.MatchEvidence;

public class AutoMatchModelServiceByModelResult {
public java.lang.String recommendedServiceUuid;
public void setRecommendedServiceUuid(java.lang.String recommendedServiceUuid) {
this.recommendedServiceUuid = recommendedServiceUuid;
}
public java.lang.String getRecommendedServiceUuid() {
return this.recommendedServiceUuid;
}

public MatchedStep matchedByStep;
public void setMatchedByStep(MatchedStep matchedByStep) {
this.matchedByStep = matchedByStep;
}
public MatchedStep getMatchedByStep() {
return this.matchedByStep;
}

public MatchEvidence evidence;
public void setEvidence(MatchEvidence evidence) {
this.evidence = evidence;
}
public MatchEvidence getEvidence() {
return this.evidence;
}

}
47 changes: 47 additions & 0 deletions sdk/src/main/java/org/zstack/sdk/MatchEvidence.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.zstack.sdk;



public class MatchEvidence {

public java.lang.String matchedRefUuid;
public void setMatchedRefUuid(java.lang.String matchedRefUuid) {
this.matchedRefUuid = matchedRefUuid;
}
public java.lang.String getMatchedRefUuid() {
return this.matchedRefUuid;
}

public java.lang.String matchedFileExtension;
public void setMatchedFileExtension(java.lang.String matchedFileExtension) {
this.matchedFileExtension = matchedFileExtension;
}
public java.lang.String getMatchedFileExtension() {
return this.matchedFileExtension;
}

public java.lang.String matchedPipelineTag;
public void setMatchedPipelineTag(java.lang.String matchedPipelineTag) {
this.matchedPipelineTag = matchedPipelineTag;
}
public java.lang.String getMatchedPipelineTag() {
return this.matchedPipelineTag;
}

public java.util.List detectedPipelineTags;
public void setDetectedPipelineTags(java.util.List detectedPipelineTags) {
this.detectedPipelineTags = detectedPipelineTags;
}
public java.util.List getDetectedPipelineTags() {
return this.detectedPipelineTags;
}

public java.lang.String warning;
public void setWarning(java.lang.String warning) {
this.warning = warning;
}
public java.lang.String getWarning() {
return this.warning;
}

}
8 changes: 8 additions & 0 deletions sdk/src/main/java/org/zstack/sdk/MatchedStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.zstack.sdk;

public enum MatchedStep {
USER_PRESET,
FILE_FORMAT,
PIPELINE_TAG,
FALLBACK,
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,12 @@ public java.lang.String getModelServiceUuid() {
return this.modelServiceUuid;
}

public java.lang.Boolean isDefault;
public void setIsDefault(java.lang.Boolean isDefault) {
this.isDefault = isDefault;
}
public java.lang.Boolean getIsDefault() {
return this.isDefault;
}

}
3 changes: 3 additions & 0 deletions sdk/src/main/java/org/zstack/sdk/UpdateModelAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public Result throwExceptionIfError() {
@Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
public java.lang.String modelId;

@Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
public java.lang.String defaultModelServiceUuid;

@Param(required = false)
public java.util.List systemTags;

Expand Down
27 changes: 27 additions & 0 deletions testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4769,6 +4769,33 @@ abstract class ApiHelper {
}


def autoMatchModelServiceByModel(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.AutoMatchModelServiceByModelAction.class) Closure c) {
def a = new org.zstack.sdk.AutoMatchModelServiceByModelAction()
a.sessionId = Test.currentEnvSpec?.session?.uuid
c.resolveStrategy = Closure.OWNER_FIRST
c.delegate = a
c()


if (System.getProperty("apipath") != null) {
if (a.apiId == null) {
a.apiId = Platform.uuid
}

def tracker = new ApiPathTracker(a.apiId)
def out = errorOut(a.call())
def path = tracker.getApiPath()
if (!path.isEmpty()) {
Test.apiPaths[a.class.name] = path.join(" --->\n")
}

return out
} else {
return errorOut(a.call())
}
}


def backupDatabaseToPublicCloud(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.BackupDatabaseToPublicCloudAction.class) Closure c) {
def a = new org.zstack.sdk.BackupDatabaseToPublicCloudAction()
a.sessionId = Test.currentEnvSpec?.session?.uuid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14944,6 +14944,7 @@ public class CloudOperationsErrorCode {
public static final String ORG_ZSTACK_AI_10163 = "ORG_ZSTACK_AI_10163";
public static final String ORG_ZSTACK_AI_10164 = "ORG_ZSTACK_AI_10164";
public static final String ORG_ZSTACK_AI_10165 = "ORG_ZSTACK_AI_10165";
public static final String ORG_ZSTACK_AI_10166 = "ORG_ZSTACK_AI_10166";

public static final String ORG_ZSTACK_CORE_CLOUDBUS_10000 = "ORG_ZSTACK_CORE_CLOUDBUS_10000";

Expand Down