疑似回归,关联 #106。 Launcher 0.1.29 对同 Namespace、已登记的同一模型目录再次执行 models validate --models-dir,报 duplicate query model resource in namespace。重启及显式 clear-existing 仍失败;相同六文件在新 Namespace 首次校验全部通过。
#106 的已关闭合同明确同 namespace / 同 path 应复用已加载 Bundle 并给出 BUNDLE_ALREADY_REGISTERED warning。本 issue 记录新版本的复现条件,不改写旧修复记录,也尚未断言与旧问题是相同代码根因。
归属:Java Runtime / TM-QM detached validation、Bundle/catalog 候选合并。与 #146 的查询名/输出别名不同;后者在本轮已修复并通过。失败的模型校验没有被绕过后发布。
a3693d0a812129f51966ef9c18247b0094a4f7f7;内部 Java project version:9.3.0-SNAPSHOT。f02ee6fabced558543b7da11c7b97da0469aaab5b0a32ff23d03a42b1eaf8595。六项 GitHub asset digest 与 SHA256SUMS 五项全部匹配,实际 JAR/进程命令行/监听 PID 已核对。http://127.0.0.1:18068;Namespace chinook,named datasource chinook-sqlite,none-dev-test-only;匿名本地开发查询。foggy-runtime-api/v1,schema 2026-06-06,engine java。相关 capabilities 声明 supported。bdf635be69850bd3be09c9a2dbeef7ddfb80036bd3ef3381383cd03b61e4a61a。将发行启动脚本及 JAR 放在同一目录。下载公开 SQLite 为当前目录的 Chinook.sqlite,将下文源码按文件名保存到 ./models/model/ 与 ./models/query/。以下目录均为调用者自己的练习目录。
.\start-foggy-runtime.ps1 -Port 18068 -WorkDir "$PWD/work"
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook wait-ready --timeout-seconds 45
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook capabilities
$dbPath = (Resolve-Path ./Chinook.sqlite).Path -replace '\\', '/'
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook datasources add --name chinook-sqlite --type sqlite --jdbc-url "jdbc:sqlite:$dbPath"
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook datasources bind --namespace chinook --data-source chinook-sqlite
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook datasources test chinook-sqlite
后续每条 foggy-runtime 均在命令名前附加全局参数 --base-url http://127.0.0.1:18068 --namespace chinook。
{ caption: '专辑与艺术家', items: [
{ ref: line.track.album },
{ ref: line.track.album.artist }
] }
chinook 校验,实际六文件通过,然后登记一个 Bundle 并刷新:foggy-runtime models validate --models-dir ./models
foggy-runtime bundles add --name chinook-0129-control --path ./models
foggy-runtime models refresh
foggy-runtime models describe ChinookInvoiceLineQueryModel
foggy-runtime models validate --models-dir ./models --include-stack-trace
退出 2。实际 3 个 TM 通过、3 个 QM 失败,均指向相同 duplicate QM 资源。提供的是已编辑明细 QM 的实际场景,不把“完全未编辑目录也失败”冒称本轮单独验证项。
foggy-runtime models validate --models-dir ./models --clear-existing --include-stack-trace
仍退出 2,同样的重复资源失败。
foggy-runtime datasources bind --namespace chinook-alias-probe --data-source chinook-sqlite
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook-alias-probe models validate --models-dir ./models
实际 6/6 valid、0 invalid。随后在这个新 Namespace 首次登记/刷新,实时元数据与第十二章 11 个别名查询通过。这仅排除了明显模型配置错误,不是原 Namespace 正常更新流程已通过。
下面是重启后执行 --include-stack-trace 的实际 JSON(本机路径已脱敏);服务并未给出额外可用的 Java cause 栈,stackTrace 实际值就是 Lifecycle operation failed.,没有补造堆栈:
{
"diagnostics": {
"attributes": {
"invalidFiles": 3,
"validation": {
"cascadingErrors": 0,
"durationMs": 37,
"errors": [
{
"category": "MODEL",
"code": "IllegalStateException",
"file": "ChinookInvoiceLineQueryModel.qm",
"message": "duplicate query model resource in namespace: ChinookInvoiceLineQueryModel",
"severity": "ERROR",
"stackTrace": "Lifecycle operation failed.",
"type": "QM"
},
{
"category": "MODEL",
"code": "IllegalStateException",
"file": "ChinookInvoiceQueryModel.qm",
"message": "duplicate query model resource in namespace: ChinookInvoiceLineQueryModel",
"severity": "ERROR",
"stackTrace": "Lifecycle operation failed.",
"type": "QM"
},
{
"category": "MODEL",
"code": "IllegalStateException",
"file": "ChinookInvoiceTimeQueryModel.qm",
"message": "duplicate query model resource in namespace: ChinookInvoiceLineQueryModel",
"severity": "ERROR",
"stackTrace": "Lifecycle operation failed.",
"type": "QM"
}
],
"invalidFiles": 3,
"namespace": "chinook",
"path": "<redacted-path>",
"totalFiles": 6,
"valid": false,
"validFiles": 3,
"warnings": []
}
},
"warnings": [
"Detached model validation rejected the candidate."
]
},
"engine": "java",
"error": {
"code": "MODEL_VALIDATE_FAILED",
"lifecycle": {
"affectedBindingGenerations": [
{
"backendId": "runtime-registry:chinook-sqlite",
"bindingKey": "runtime:namespace-default:chinook",
"generation": "f04995a6-94b0-4c1b-85aa-5f15791e8628:2"
}
],
"beforeCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:2",
"catalogState": "ACTIVE",
"diagnostics": [
{
"message": "Detached model validation failed (IllegalStateException).",
"phase": "validate.qm",
"suggestedNextAction": "Fix the model definition and retry validation.",
"target": "ChinookInvoiceLineQueryModel"
},
{
"message": "Detached model validation failed (IllegalStateException).",
"phase": "validate.qm",
"suggestedNextAction": "Fix the model definition and retry validation.",
"target": "ChinookInvoiceQueryModel"
},
{
"message": "Detached model validation failed (IllegalStateException).",
"phase": "validate.qm",
"suggestedNextAction": "Fix the model definition and retry validation.",
"target": "ChinookInvoiceTimeQueryModel"
}
],
"failedTargets": [
"ChinookInvoiceLineQueryModel",
"ChinookInvoiceQueryModel",
"ChinookInvoiceTimeQueryModel"
],
"namespace": "chinook",
"sourceRevision": "source:103e6b0b-d9de-4d38-a8c4-d1bacc5ab9cc:g0:n2"
},
"lifecycleCode": "CATALOG_VALIDATION_FAILED",
"message": "Detached model validation rejected the candidate.",
"model": "ChinookInvoiceLineQueryModel",
"phase": "models.validate",
"safeToAutoRepair": false,
"suggestedNextAction": "Fix the model definition and retry validation."
},
"runtimeApiVersion": "foggy-runtime-api/v1",
"success": false
}
显式 --clear-existing 的错误摘要:
{
"success": false,
"error": {
"code": "MODEL_VALIDATE_FAILED",
"lifecycle": {
"affectedBindingGenerations": [
{
"backendId": "runtime-registry:chinook-sqlite",
"bindingKey": "runtime:namespace-default:chinook",
"generation": "f04995a6-94b0-4c1b-85aa-5f15791e8628:2"
}
],
"beforeCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:2",
"catalogState": "ACTIVE",
"diagnostics": [
{
"message": "Detached model validation failed (IllegalStateException).",
"phase": "validate.qm",
"suggestedNextAction": "Fix the model definition and retry validation.",
"target": "ChinookInvoiceLineQueryModel"
},
{
"message": "Detached model validation failed (IllegalStateException).",
"phase": "validate.qm",
"suggestedNextAction": "Fix the model definition and retry validation.",
"target": "ChinookInvoiceQueryModel"
},
{
"message": "Detached model validation failed (IllegalStateException).",
"phase": "validate.qm",
"suggestedNextAction": "Fix the model definition and retry validation.",
"target": "ChinookInvoiceTimeQueryModel"
}
],
"failedTargets": [
"ChinookInvoiceLineQueryModel",
"ChinookInvoiceQueryModel",
"ChinookInvoiceTimeQueryModel"
],
"namespace": "chinook",
"sourceRevision": "source:103e6b0b-d9de-4d38-a8c4-d1bacc5ab9cc:g0:n2"
},
"lifecycleCode": "CATALOG_VALIDATION_FAILED",
"message": "Detached model validation rejected the candidate.",
"model": "ChinookInvoiceLineQueryModel",
"phase": "models.validate",
"safeToAutoRepair": false,
"suggestedNextAction": "Fix the model definition and retry validation."
}
}
新 Namespace 首次校验摘要:
{
"success": true,
"data": {
"affectedBindingGenerations": [],
"afterCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:3",
"beforeCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:3",
"cascadingErrors": 0,
"catalogState": "ACTIVE",
"durationMs": 41,
"errors": [],
"invalidFiles": 0,
"namespace": "chinook-alias-probe",
"path": "[lab-root]\\releases\\launcher-0.1.29\\control-models",
"sourceRevision": "source:103e6b0b-d9de-4d38-a8c4-d1bacc5ab9cc:g0:n0",
"totalFiles": 6,
"valid": true,
"validFiles": 6,
"warnings": []
}
}
重启后 Runtime stdout 的加载记录(保留原行号,脱敏路径):
80: 2026-10-21 10:03:29.686 [main] INFO c.f.bundle.SystemBundlesContextImpl - 动态添加外部Bundle成功: chinook-0129-control -> [lab-root]\releases\launcher-0.1.29\control-models (namespace: chinook)
83: 2026-10-21 10:03:29.874 [main] INFO c.f.d.m.e.BundleLifecycleListener - Bundle catalog refresh published: bundleName=chinook-0129-control, namespace=chinook, generation=catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:1, pivotOuterCacheRemoved=0
收尾时 chinook 的实际注册条目仅一个;watch=false,避免把文件监视重复登记当作已知根因:
[
{
"name": "chinook-0129-control",
"namespace": "chinook",
"path": "[lab-root]\\releases\\launcher-0.1.29\\control-models",
"watch": false,
"enabled": true,
"createdAt": "2026-10-21T02:00:29.550752300Z",
"updatedAt": "2026-10-21T02:00:29.550752300Z",
"immutablePublication": false
}
]
独立 stdout/stderr、各次 CLI JSON 与命令日志已留档。Runtime 对重复校验失败没有另打印完整 cause 栈;上面的结构化诊断是可取得的关键错误日志。
下列六文件使用公开 Chinook;首次加载时只把明细 QM 两项换成步骤 1 的旧形式,再按步骤 3 恢复为这里的四个具体列。此处为实际六文件复现,尚未额外缩减成单 TM/QM 对。
export const model = {
name: 'ChinookInvoiceLineModel',
caption: 'Chinook 账单明细',
description: '一行是一条账单明细。分类分析累计交易明细金额,不累计账单总金额,不改用目录单价。',
tableName: 'InvoiceLine',
idColumn: 'InvoiceLineId',
properties: [
{ column: 'InvoiceLineId', name: 'invoiceLineId', caption: '明细 ID', type: 'INTEGER' },
{ column: 'InvoiceId', name: 'invoiceId', caption: '账单 ID', type: 'INTEGER' },
{ column: 'TrackId', name: 'trackId', caption: '曲目或媒体 ID', type: 'INTEGER' },
{ column: 'UnitPrice', name: 'unitPrice', caption: '交易单价', type: 'MONEY',
description: '账单明细上的交易单价,不是 Track 目录单价。样例原始单位,币种未指定。' }
],
dimensions: [
{
name: 'invoice', caption: '账单', tableName: 'Invoice',
foreignKey: 'InvoiceId', primaryKey: 'InvoiceId', captionColumn: 'InvoiceId',
properties: [
{ column: 'InvoiceDate', name: 'invoiceDate', caption: '账单日期', type: 'DATETIME',
description: '按账单记录日期统计,不自行做时区转换。' },
{ column: 'BillingCountry', name: 'billingCountry', caption: '账单国家', type: 'STRING' },
{ column: 'CustomerId', name: 'customerId', caption: '客户 ID', type: 'INTEGER' }
],
dimensions: [
{
name: 'customer', caption: '客户', tableName: 'Customer',
foreignKey: 'CustomerId', primaryKey: 'CustomerId', captionColumn: 'LastName',
properties: [
{ column: 'FirstName', name: 'fullName', caption: '客户姓名', type: 'STRING',
description: 'FirstName 与 LastName 拼接;与客户 ID 一起使用。',
formulaDef: { builder: (alias) => `${alias}.FirstName || ' ' || ${alias}.LastName` } }
]
}
]
},
{
name: 'track', caption: '曲目或媒体', tableName: 'Track',
foreignKey: 'TrackId', primaryKey: 'TrackId', captionColumn: 'Name',
properties: [
{ column: 'Name', name: 'title', caption: '曲目或媒体名称', type: 'STRING' }
],
dimensions: [
{
name: 'genre', caption: '内容分类', tableName: 'Genre',
foreignKey: 'GenreId', primaryKey: 'GenreId', captionColumn: 'Name',
description: '包含音乐和影视内容;分类缺失不应丢弃交易金额。',
properties: [
{ column: 'GenreId', name: 'genreId', caption: '分类 ID', type: 'INTEGER' },
{ column: 'Name', name: 'genreName', caption: '内容分类名称', type: 'STRING' }
]
},
{
name: 'album', caption: '专辑', tableName: 'Album',
foreignKey: 'AlbumId', primaryKey: 'AlbumId', captionColumn: 'Title',
description: '曲目所属专辑,包含音乐及影视专辑;用 ID 区分同名专辑。',
dimensions: [
{
name: 'artist', caption: '艺术家', tableName: 'Artist',
foreignKey: 'ArtistId', primaryKey: 'ArtistId', captionColumn: 'Name',
description: '以专辑所属艺术家归属交易金额,不等同于 Track.Composer 作曲者;用 ID 区分同名。'
}
]
}
]
}
],
measures: [
{ column: 'UnitPrice', name: 'lineAmount', caption: '明细金额', type: 'MONEY', aggregation: 'sum',
description: '交易单价乘数量,沿用样例原始单位,不除以 100,不指定币种。',
formulaDef: { builder: (alias) => `${alias}.UnitPrice * ${alias}.Quantity` } },
{ column: 'Quantity', name: 'salesQuantity', caption: '销售数量', type: 'INTEGER', aggregation: 'sum',
description: '累计 Quantity;本样例全部为 1,也不能将数量定义成明细条数。' }
]
};
export const model = {
name: 'ChinookInvoiceModel',
caption: 'Chinook 账单',
description: '一行是一张账单,不关联明细后累计账单金额。金额沿用样例原始单位,币种未指定。',
tableName: 'Invoice',
idColumn: 'InvoiceId',
properties: [
{ column: 'InvoiceId', name: 'invoiceId', caption: '账单 ID', type: 'INTEGER' },
{ column: 'CustomerId', name: 'customerId', caption: '客户 ID', type: 'INTEGER',
description: '客户识别键,排名时与姓名一起分组,避免同名客户合并。' },
{ column: 'InvoiceDate', name: 'invoiceDate', caption: '账单日期', type: 'DATETIME',
description: '按账单记录日期统计,不自行做时区转换。' },
{ column: 'BillingCountry', name: 'billingCountry', caption: '账单国家', type: 'STRING',
description: '账单地址中的国家,不是客户地址国家。' }
],
dimensions: [
{
name: 'customer', caption: '客户', tableName: 'Customer',
foreignKey: 'CustomerId', primaryKey: 'CustomerId',
captionColumn: 'LastName',
keyCaption: '客户 ID',
description: '用客户 ID 区分客户,姓名仅用于展示。',
properties: [
{ column: 'FirstName', name: 'fullName', caption: '客户姓名', type: 'STRING',
description: 'FirstName 与 LastName 拼接;展示时与客户 ID 一起使用。',
formulaDef: { builder: (alias) => `${alias}.FirstName || ' ' || ${alias}.LastName` } },
{ column: 'Country', name: 'country', caption: '客户国家', type: 'STRING' }
]
}
],
measures: [
{ column: 'Total', name: 'invoiceAmount', caption: '账单金额', type: 'MONEY', aggregation: 'sum',
description: '每张账单只累计一次。样例原始单位,不除以 100,不指定币种。' },
{ column: 'InvoiceId', name: 'invoiceCount', caption: '账单数', type: 'INTEGER', aggregation: 'count',
description: '按账单 ID 计数,只在账单分析中统计。' }
]
};
export const model = {
name: 'ChinookInvoiceTimeModel',
caption: 'Chinook 账单时间分析',
description: '一行是一张账单,按账单记录日期的自然年、自然月统计。金额沿用样例原始单位,币种未指定,不自行做时区转换。',
tableName: 'Invoice',
idColumn: 'InvoiceId',
properties: [
{ column: 'InvoiceId', name: 'invoiceId', caption: '账单 ID', type: 'INTEGER' }
],
dimensions: [
{
name: 'invoiceDate', caption: '账单日期',
foreignKey: 'InvoiceDate', primaryKey: 'InvoiceDate', captionColumn: 'InvoiceDate',
type: 'DATETIME', timeRole: 'business_date',
description: '直接使用账单表的 InvoiceDate,不关联额外日期表;按记录日期统计,不自行做时区转换。',
properties: [
{ column: 'InvoiceDate', name: 'year', caption: '账单年份', type: 'INTEGER',
dialectFormulaDef: { sqlite: { builder: (alias) => `CAST(strftime('%Y', ${alias}.InvoiceDate) AS INTEGER)` } } },
{ column: 'InvoiceDate', name: 'month', caption: '账单月份', type: 'INTEGER',
description: '月份序号 1—12,跨年分析时应与年份一起使用。',
dialectFormulaDef: { sqlite: { builder: (alias) => `CAST(strftime('%m', ${alias}.InvoiceDate) AS INTEGER)` } } },
{ column: 'InvoiceDate', name: 'yearMonth', caption: '账单年月', type: 'STRING',
description: 'YYYY-MM,自然月分组键,保留年份。',
dialectFormulaDef: { sqlite: { builder: (alias) => `strftime('%Y-%m', ${alias}.InvoiceDate)` } } }
]
}
],
measures: [
{ column: 'Total', name: 'invoiceAmount', caption: '账单金额', type: 'MONEY', aggregation: 'sum',
description: '每张账单只累计一次,样例原始单位,不指定币种,不代表利润或回款。' },
{ column: 'InvoiceId', name: 'invoiceCount', caption: '账单数', type: 'INTEGER', aggregation: 'count' }
]
};
const line = loadTableModel('ChinookInvoiceLineModel');
export const queryModel = {
name: 'ChinookInvoiceLineQueryModel',
caption: 'Chinook 明细分析',
description: '按账单日期、客户、曲目或媒体、内容分类、专辑及其所属艺术家分析明细金额和销售数量。每行是一条明细,不提供可累计的账单总金额。艺术家按 Album.ArtistId 归属,不使用 Track.Composer。金额沿用样例原始单位,不指定币种、不做分元换算,不代表利润、回款或退款。',
loader: 'v2',
model: line,
columnGroups: [
{ caption: '交易明细', items: [
{ ref: line.invoiceLineId },
{ ref: line.invoiceId },
{ ref: line.trackId },
{ ref: line.unitPrice },
{ ref: line.invoice$invoiceDate },
{ ref: line.invoice$billingCountry },
{ ref: line.invoice$customerId },
{ ref: line.invoice.customer$fullName },
{ ref: line.track$title },
{ ref: line.track.genre$genreId },
{ ref: line.track.genre$genreName }
] },
{ caption: '专辑与艺术家', items: [
{ ref: line.track.album$id, name: 'albumId', alias: 'albumId' },
{ ref: line.track.album$caption, name: 'albumName', alias: 'albumName' },
{ ref: line.track.album.artist$id, name: 'albumArtistId', alias: 'albumArtistId' },
{ ref: line.track.album.artist$caption, name: 'albumArtistName', alias: 'albumArtistName' }
] },
{ caption: '明细指标', items: [
{ ref: line.lineAmount },
{ ref: line.salesQuantity }
] }
],
accesses: []
};
const invoice = loadTableModel('ChinookInvoiceModel');
export const queryModel = {
name: 'ChinookInvoiceQueryModel',
caption: 'Chinook 账单分析',
description: '按账单日期、客户和账单国家分析账单金额与张数。金额沿用样例原始单位,不指定币种、不做分元换算,不代表利润、回款或退款。分类分析请用明细分析,不按分类分摊账单总额。',
loader: 'v2',
model: invoice,
columnGroups: [
{ caption: '账单与客户', items: [
{ ref: invoice.invoiceId },
{ ref: invoice.invoiceDate },
{ ref: invoice.billingCountry },
{ ref: invoice.customerId },
{ ref: invoice.customer$fullName },
{ ref: invoice.customer$country }
] },
{ caption: '账单指标', items: [
{ ref: invoice.invoiceAmount },
{ ref: invoice.invoiceCount },
{
name: 'averageAmount',
caption: '平均账单金额',
formula: 'sum(invoiceAmount) / nullif(count(invoiceId), 0)',
type: 'MONEY',
description: '当前筛选与分组范围的账单总金额除以账单数;无账单时为空,不累计或简单平均分组均额。样例原始单位,币种未指定。'
}
] }
],
accesses: []
};
const invoice = loadTableModel('ChinookInvoiceTimeModel');
export const queryModel = {
name: 'ChinookInvoiceTimeQueryModel',
caption: 'Chinook 账单时间分析',
description: '按账单日期、自然年和自然月分析账单金额与张数。月份序号需与年份结合,年月字段为 YYYY-MM;金额沿用样例原始单位,币种未指定。',
loader: 'v2',
model: invoice,
columnGroups: [
{ caption: '账单与时间', items: [
{ ref: invoice.invoiceId },
{ ref: invoice.invoiceDate },
{ ref: invoice.invoiceDate$year },
{ ref: invoice.invoiceDate$month },
{ ref: invoice.invoiceDate$yearMonth }
] },
{ caption: '账单指标', items: [
{ ref: invoice.invoiceAmount },
{ ref: invoice.invoiceCount }
] }
],
accesses: []
};
建议覆盖同 path 重复校验、真实 QM 修改后重复校验、clear-existing、重启恢复、Windows 路径规范化、旧 validation Bundle 与已有 Bundle 合并,以及失败前后 catalog 可用性;同时检查新 Namespace 首次校验这个对照。
目前只证明上述正式发行版、SQLite/Chinook 与明确命令顺序;具体代码根因、所有数据库/宿主的影响未验证。未删除已有 Bundle 作为修复,未改产品代码,无生产环境操作。