返回正文目录

Summary

Launcher 0.1.29 中,嵌套维度 InvoiceLine → Track → Album → Artist 的专用 members list 返回错误对象或 500:查询艺术家目录却返回 Track 数据;使用已公开的艺术家 ID 字段会触发 query model slot/name mismatch。CLI 还收到旧式 code/data 或 code/msg 信封,成功形状未符合 Runtime CLI success 合同。

归属:Java Runtime / synthetic member-QM、维度来源绑定与 Runtime API 响应。不能只修改 CLI 退出码,因为数据对象本身已错。与 #145 的 DISTINCT 排序、#146 的具体列命名问题分开;这两项在同一 0.1.29 已复测通过。#75 是前端 memberSelect 选择字段问题,当前证据是直接 CLI 调用,不依赖该前端。

Environment / 发行身份

基础准备

将发行启动脚本及 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。

Reproduction

本轮实际先使用旧字段 QM(源码见下文),六文件校验通过,再登记 Bundle、刷新并 describe。下方提供实际涉及的明细 TM/QM,供后续缩减复现;尚未额外执行仅此两文件的新进程缩减复现,实际证据来自六文件教学 Bundle。

foggy-runtime models validate --models-dir ./models
foggy-runtime bundles add --name chinook-members-repro --path ./models
foggy-runtime models refresh
foggy-runtime models describe ChinookInvoiceLineQueryModel
foggy-runtime members list ChinookInvoiceLineQueryModel artist
foggy-runtime members list ChinookInvoiceLineQueryModel artist$id

公开元数据包含 artist$id、artist$caption。先调用 artist,返回 code=200、total=3503、前十条曲目;随后 artist$id 报 slot/name mismatch。album$id 同样返回曲目,完整点路径/下划线路径也另有 synthetic member-QM 解析错误。

后续已将该隔离测试副本 QM 改为具体列 name/alias(见第二份 QM),使用同 WorkDir/registry 重启 0.1.29,再按 artist$id → albumArtistId → artist 的顺序调用。下面是重启后保留的响应;三个 CLI 退出码均为 2:

[
  {
    "field": "artist$id",
    "cliExitCode": 2,
    "response": {
      "code": 500,
      "exCode": "B500",
      "msg": "服务器内部错误: query model slot/name mismatch: ChinookInvoiceLineQueryModel#album"
    }
  },
  {
    "field": "albumArtistId",
    "cliExitCode": 2,
    "response": {
      "code": 500,
      "exCode": "B500",
      "msg": "服务器内部错误: cannot resolve synthetic member-QM from fieldName: albumArtistId"
    }
  },
  {
    "field": "artist",
    "cliExitCode": 2,
    "response": {
      "code": 200,
      "data": {
        "empty": false,
        "hasNext": false,
        "items": [
          {
            "caption": "For Those About To Rock (We Salute You)",
            "id": 1
          },
          {
            "caption": "Balls to the Wall",
            "id": 2
          },
          {
            "caption": "Fast As a Shark",
            "id": 3
          },
          {
            "caption": "Restless and Wild",
            "id": 4
          },
          {
            "caption": "Princess of the Dawn",
            "id": 5
          },
          {
            "caption": "Put The Finger On You",
            "id": 6
          },
          {
            "caption": "Let's Get It Up",
            "id": 7
          },
          {
            "caption": "Inject The Venom",
            "id": 8
          },
          {
            "caption": "Snowballed",
            "id": 9
          },
          {
            "caption": "Evil Walks",
            "id": 10
          }
        ],
        "limit": 10,
        "pageNumber": 1,
        "pageSize": 10,
        "start": 0,
        "total": 3503,
        "totalData": {
          "total": 3503
        },
        "totalPage": 350
      }
    }
  }
]

这说明 slot/name mismatch 不仅发生在先调用 artist 后。重启后的别名 QM 是补充条件,不能冒称是未改 QM 的完全同请求新进程复现。

Expected

Actual / Runtime log excerpts

实际错误对象首项是 Track ID=1 的 For Those About To Rock (We Salute You),不是 Artist ID=1 的 AC/DC;total=3503 也与 Track 数量吻合。本轮通过只读原表计数与 ID/name 核对,不以 success 或 HTTP 状态推断业务正确。

默认 lite 进程日志(脱敏路径,保留原行号):

167: 2026-10-21 10:01:12.843 [http-nio-127.0.0.1-18068-exec-3] ERROR c.f.d.m.c.FoggyDatasetExceptionHandler - 系统异常: 
168: java.lang.IllegalArgumentException: query model slot/name mismatch: ChinookInvoiceLineQueryModel#album
169: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.validateQueryModelSlot(CatalogSnapshot.java:216)
170: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.lambda$validateCatalogIntegrity$3(CatalogSnapshot.java:126)
171: 	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
172: 	at java.base/java.util.Collections$UnmodifiableMap.forEach(Collections.java:1553)
173: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.validateCatalogIntegrity(CatalogSnapshot.java:125)
174: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.<init>(CatalogSnapshot.java:44)
175: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogCandidate.freeze(CatalogCandidate.java:582)
176: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore.lambda$publishAdditive$2(CatalogSnapshotStore.java:456)
177: 	at com.foggyframework.fsscript.lifecycle.CommittedSourceRevisionRegistry.publishIfCurrent(CommittedSourceRevisionRegistry.java:106)
178: 	at com.foggyframework.dataset.model.DbModelAutoConfiguration$1.publishIfCurrent(DbModelAutoConfiguration.java:157)
179: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore.publishIfSourceCurrent(CatalogSnapshotStore.java:525)
180: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore.publishAdditive(CatalogSnapshotStore.java:433)
181: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore$CandidateScope.commitAdditive(CatalogSnapshotStore.java:625)
182: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.publishGuarded(RuntimeNamedDataSourceResolver.java:284)
183: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.lambda$publishGuarded$22(RuntimeNamedDataSourceResolver.java:288)
184: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.continueComposite(RuntimeNamedDataSourceResolver.java:330)
185: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.lambda$publishGuarded$23(RuntimeNamedDataSourceResolver.java:287)
186: 	at com.foggyframework.runtime.api.service.RuntimeDatasourceRegistryService.publishIfCurrent(RuntimeDatasourceRegistryService.java:497)
187: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.publishGuarded(RuntimeNamedDataSourceResolver.java:290)
188: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.publishIfCurrent(RuntimeNamedDataSourceResolver.java:125)
189: 	at com.foggyframework.dataset.model.engine.query_model.QueryModelLoaderImpl.commitIfBindingsCurrent(QueryModelLoaderImpl.java:542)
190: 	at com.foggyframework.dataset.model.engine.query_model.QueryModelLoaderImpl.buildAndPublish(QueryModelLoaderImpl.java:423)
191: 	at com.foggyframework.dataset.model.engine.query_model.QueryModelLoaderImpl.lambda$resolveWithSingleFlight$1(QueryModelLoaderImpl.java:214)
192: 	at com.foggyframework.dataset.model.lifecycle.concurrent.ModelBuildSingleFlight.execute(ModelBuildSingleFlight.java:58)
193: 	at com.foggyframework.dataset.model.engine.query_model.QueryModelLoaderImpl.resolveWithSingleFlight(QueryModelLoaderImpl.java:212)
194: 	at com.foggyframework.dataset.model.engine.query_model.QueryModelLoaderImpl.resolveJdbcQueryModel(QueryModelLoaderImpl.java:164)
195: 	at com.foggyframework.dataset.model.service.impl.QueryFacadeImpl.resolveAndPin(QueryFacadeImpl.java:314)
196: 	at com.foggyframework.dataset.model.service.impl.QueryFacadeImpl.doQuery(QueryFacadeImpl.java:153)

重启后日志节选:

91: 2026-10-21 10:05:39.936 [http-nio-127.0.0.1-18068-exec-9] ERROR c.f.d.m.c.FoggyDatasetExceptionHandler - 系统异常: 
92: java.lang.IllegalArgumentException: query model slot/name mismatch: ChinookInvoiceLineQueryModel#album
93: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.validateQueryModelSlot(CatalogSnapshot.java:216)
94: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.lambda$validateCatalogIntegrity$3(CatalogSnapshot.java:126)
95: 	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
96: 	at java.base/java.util.Collections$UnmodifiableMap.forEach(Collections.java:1553)
97: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.validateCatalogIntegrity(CatalogSnapshot.java:125)
98: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshot.<init>(CatalogSnapshot.java:44)
99: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogCandidate.freeze(CatalogCandidate.java:582)
100: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore.lambda$publishAdditive$2(CatalogSnapshotStore.java:456)
101: 	at com.foggyframework.fsscript.lifecycle.CommittedSourceRevisionRegistry.publishIfCurrent(CommittedSourceRevisionRegistry.java:106)
102: 	at com.foggyframework.dataset.model.DbModelAutoConfiguration$1.publishIfCurrent(DbModelAutoConfiguration.java:157)
103: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore.publishIfSourceCurrent(CatalogSnapshotStore.java:525)
104: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore.publishAdditive(CatalogSnapshotStore.java:433)
105: 	at com.foggyframework.dataset.model.lifecycle.catalog.CatalogSnapshotStore$CandidateScope.commitAdditive(CatalogSnapshotStore.java:625)
106: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.publishGuarded(RuntimeNamedDataSourceResolver.java:284)
107: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.lambda$publishGuarded$22(RuntimeNamedDataSourceResolver.java:288)
108: 	at com.foggyframework.runtime.api.service.RuntimeNamedDataSourceResolver.continueComposite(RuntimeNamedDataSourceResolver.java:330)

Governed control

同 QM 的语义查询校验和执行通过:按日期筛选、按艺术家 ID/名称分组,得到 119 位 2012 年有成交记录的艺术家。它只作为字段/关联查询可用的对照,不作为完整目录,也不算 members 接口修复:

{
  "columns": [
    "artist$id",
    "artist$caption",
    "count(invoiceLineId) as lineCount"
  ],
  "slice": [
    {
      "field": "invoice$invoiceDate",
      "op": "[)",
      "value": [
        "2012-01-01",
        "2013-01-01"
      ]
    }
  ],
  "groupBy": [
    "artist$id",
    "artist$caption"
  ],
  "orderBy": [
    "artist$id"
  ],
  "limit": 500,
  "returnTotal": true
}

Model sources

ChinookInvoiceLineModel.tm — 实际完整 TM
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,也不能将数量定义成明细条数。' }
    ]
};
ChinookInvoiceLineQueryModel.qm — 首轮实际旧字段 QM
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 },
            { ref: line.track.album.artist }
        ] },
        { caption: '明细指标', items: [
            { ref: line.lineAmount },
            { ref: line.salesQuantity }
        ] }
    ],
    accesses: []
};
ChinookInvoiceLineQueryModel.qm — 重启轮实际 name/alias QM
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: []
};

Scope / suggested regression

覆盖嵌套 Artist/Album 与直接 Track 的来源绑定、当前公开字段/维度参数、别名投影、调用次序、fresh process、信封与 CLI 退出码。修复还应对成员范围与主查询权限保持一致;本轮为匿名本地开发测试,未把生产权限标为已验收。

原始 CLI JSON、精确命令及完整 stdout/stderr 均在本地留档;上述是可公开的精确响应与必要日志,未上传整份本机日志。测试实例已停止,无数据库写入、无产品代码改动。