{"body":"## 概要\r\n\r\nLauncher 0.1.25 中，下方带 `distinct: true` 与 `orderBy: [\"artist$id\"]` 的合法查询校验、执行均成功，normalized orderBy 仍是升序，但实际执行 SQL 和 explain SQL 都缺少 `ORDER BY`，返回结果没有按请求排序。\r\n\r\n归属：Java Runtime / 查询编译与执行。当前只确认本文 SQLite/Chinook 请求，不概括为全部 DISTINCT 查询都失败，也尚未定位具体代码分支。\r\n\r\n## 环境\r\n\r\n- Windows 11 Pro x64（build 26200），Java 17.0.12，Python 3.11.5，PowerShell 7.6.5。\r\n- [Launcher 0.1.25 Release](https://github.com/foggy-projects/foggy-data-mcp-bridge/releases/tag/foggy-runtime-launcher-v0.1.25)，使用发行 README 和原 PowerShell 启动脚本，lite profile。\r\n- manifest source commit：`db5db39aa36dc30b51d9db7d941e333c8d39e0d2`；内部 Java 项目版本 `9.3.0-SNAPSHOT`。\r\n- JAR SHA256：`b3fe4b6a3189132363b59ba16c143ef6ddab2fd7886ef0d7e7c0838fef1c350a`。运行进程已核对实际加载此 JAR。\r\n- CLI `foggy-runtime 0.1.24`，公开安装包，无源码构建、无产品代码修改。\r\n- 当前本地教学入口 `http://127.0.0.1:18067`，namespace `chinook`，命名数据源 `chinook-sqlite`；Console/FAP 关闭，匿名本地开发查询。\r\n- 数据：[Chinook v1.4.5 官方 SQLite](https://github.com/lerocha/chinook-database/releases/download/v1.4.5/Chinook_Sqlite.sqlite)，使用副本。SHA256 `bdf635be69850bd3be09c9a2dbeef7ddfb80036bd3ef3381383cd03b61e4a61a`。\r\n- 使用本机已有建模/查询 Skill，未将定制安装当成某个原版 Skill Release；复现只依赖以下模型、payload 和公开 CLI。\r\n\r\ncapabilities 摘要：\r\n\r\n```json\r\n{\r\n  \"engine\": \"java\",\r\n  \"runtimeApiVersion\": \"foggy-runtime-api/v1\",\r\n  \"schemaVersion\": \"2026-06-06\",\r\n  \"securityMode\": \"none-dev-test-only\",\r\n  \"capabilities\": {\r\n    \"models.describe\": \"supported\",\r\n    \"query.validate\": \"supported\",\r\n    \"query.execute\": \"supported\",\r\n    \"query.explain\": \"supported\"\r\n  }\r\n}\r\n```\r\n\r\n## 复现步骤\r\n\r\n1. 按 Release README 下载并校验发行资产，使用 `start-foggy-runtime.ps1 -Port 18067 -WorkDir ./work` 启动。运行 `wait-ready`、`capabilities`。\r\n2. 下载上面的 Chinook SQLite，使用副本，例如保存为当前目录的 `Chinook.sqlite`。配置命名数据源并绑定 namespace：\r\n\r\n```powershell\r\n$dbPath = (Resolve-Path ./Chinook.sqlite).Path -replace '\\\\', '/'\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook datasources add --name chinook-sqlite --type sqlite --jdbc-url \"jdbc:sqlite:$dbPath\"\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook datasources bind --namespace chinook --data-source chinook-sqlite\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook datasources test chinook-sqlite\r\n```\r\n\r\n3. 将下文两个模型文件分别保存到 `./models/model/ChinookInvoiceLineModel.tm` 和 `./models/query/ChinookInvoiceLineQueryModel.qm`。在尚未登记该目录的新实例中先校验，再登记、刷新和 describe：\r\n\r\n```powershell\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook models validate --models-dir ./models\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook bundles add --name chinook-distinct-repro --path ./models\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook models refresh\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook models describe ChinookInvoiceLineQueryModel\r\n```\r\n\r\n本次实际教学实例为六文件校验全部通过；新复现实例只需本文这一对 TM/QM。未另外执行干净进程复现，不宣称已排除全部状态因素。\r\n\r\n4. 将以下 payload 保存为 `distinct-artists.json`，对同一个文件先校验、再执行、再 explain：\r\n\r\n```json\r\n{\r\n  \"columns\": [\r\n    \"artist$id\",\r\n    \"artist$caption\"\r\n  ],\r\n  \"slice\": [\r\n    {\r\n      \"field\": \"invoice$invoiceDate\",\r\n      \"op\": \"[)\",\r\n      \"value\": [\r\n        \"2012-01-01\",\r\n        \"2013-01-01\"\r\n      ]\r\n    }\r\n  ],\r\n  \"distinct\": true,\r\n  \"orderBy\": [\r\n    \"artist$id\"\r\n  ],\r\n  \"limit\": 500,\r\n  \"returnTotal\": true\r\n}\r\n```\r\n\r\n```powershell\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook query validate ChinookInvoiceLineQueryModel --payload ./distinct-artists.json\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook query execute ChinookInvoiceLineQueryModel --payload ./distinct-artists.json\r\nfoggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook query explain ChinookInvoiceLineQueryModel --payload ./distinct-artists.json --include-sql --include-physical-names --depth detailed\r\n```\r\n\r\n## 预期\r\n\r\n- 对 ID、名称去重，得到 2012 年有成交记录的 119 位艺术家。\r\n- 按公开 ID 字段升序排列；本数据前八个 ID 应为 `[1, 2, 3, 4, 5, 6, 7, 8]`。\r\n- 排序应落实在 DISTINCT 查询的物理 SQL 中，并在 LIMIT/OFFSET 前生效。\r\n- 如果确有不支持的组合，应在校验阶段明确拒绝，不能接受排序参数后悄悄丢弃。\r\n\r\n## 实际\r\n\r\n- validate 和 execute 的 CLI 退出码均为 **0**，响应均 `success: true`，没有排序未生效的明确报错。\r\n- 返回 total=119，ID/名称去重集合与原始数据独立核对一致；**集合正确，顺序错误**。\r\n- 实际前八个 ID：`[90, 91, 92, 93, 94, 95, 46, 96]`，不是升序。\r\n- 响应中的 normalized orderBy 仍保留用户要求：\r\n\r\n```json\r\n[\r\n  {\r\n    \"dir\": \"asc\",\r\n    \"field\": \"artist$id\",\r\n    \"nullFirst\": false,\r\n    \"nullLast\": false\r\n  }\r\n]\r\n```\r\n\r\n- `data.debug.extra.sql` 实际执行 SQL 如下，完全没有 ORDER BY；`query explain` 的 `sqlTrace.finalPhysicalSql` 也缺少 ORDER BY：\r\n\r\n```sql\r\nselect\t distinct \td5.ArtistId \"artist$id\",\td6.Name \"artist$caption\"\tfrom InvoiceLine t1\t left join Track d3  on t1.TrackId=d3.TrackId left join Album d5  on d3.AlbumId=d5.AlbumId left join Artist d6  on d5.ArtistId=d6.ArtistId left join Invoice d1  on t1.InvoiceId=d1.InvoiceId where  1=1  and \td1.InvoiceDate >=?  and \td1.InvoiceDate <?\r\n```\r\n\r\n返回结果节选：\r\n\r\n```json\r\n{\r\n  \"success\": true,\r\n  \"total\": 119,\r\n  \"items\": [\r\n    {\r\n      \"artist$caption\": \"Iron Maiden\",\r\n      \"artist$id\": 90\r\n    },\r\n    {\r\n      \"artist$caption\": \"James Brown\",\r\n      \"artist$id\": 91\r\n    },\r\n    {\r\n      \"artist$caption\": \"Jamiroquai\",\r\n      \"artist$id\": 92\r\n    },\r\n    {\r\n      \"artist$caption\": \"JET\",\r\n      \"artist$id\": 93\r\n    },\r\n    {\r\n      \"artist$caption\": \"Jimi Hendrix\",\r\n      \"artist$id\": 94\r\n    },\r\n    {\r\n      \"artist$caption\": \"Joe Satriani\",\r\n      \"artist$id\": 95\r\n    },\r\n    {\r\n      \"artist$caption\": \"Jorge Ben\",\r\n      \"artist$id\": 46\r\n    },\r\n    {\r\n      \"artist$caption\": \"Jota Quest\",\r\n      \"artist$id\": 96\r\n    }\r\n  ]\r\n}\r\n```\r\n\r\n## 对照与影响\r\n\r\n同一模型、同一日期范围，按 `artist$id`、`artist$caption` 分组，并带 `count(invoiceLineId) as lineCount`，再按 `artist$id` 排序，得到同一批 119 位成员且 ID 升序正确。该对照说明模型字段可以参与有效排序，但不能据此推断 DISTINCT 全部分支的根因。\r\n\r\n未排序的 DISTINCT 结果不能提供调用方预期的排序保证；后续依赖 LIMIT/OFFSET 展示成员时会受到影响。本次 DISTINCT 请求 limit=500，119 项全部返回，因此不是分页截断或客户端只展示前一页造成的错觉。尚未对这个异常 DISTINCT 请求单独验证重复页或缺失页。\r\n\r\n建议回归覆盖：本文升序请求、降序、带相同展示名称的不同 ID、明确次序下的 LIMIT/OFFSET；同时检查执行结果和最终 SQL，不只检查 normalized DSL 或 success。\r\n\r\n## 原始模型（公开样例，无业务凭据）\r\n\r\n<details>\r\n<summary>ChinookInvoiceLineModel.tm（完整）</summary>\r\n\r\n```javascript\r\nexport const model = {\r\n    name: 'ChinookInvoiceLineModel',\r\n    caption: 'Chinook 账单明细',\r\n    description: '一行是一条账单明细。分类分析累计交易明细金额，不累计账单总金额，不改用目录单价。',\r\n    tableName: 'InvoiceLine',\r\n    idColumn: 'InvoiceLineId',\r\n    properties: [\r\n        { column: 'InvoiceLineId', name: 'invoiceLineId', caption: '明细 ID', type: 'INTEGER' },\r\n        { column: 'InvoiceId', name: 'invoiceId', caption: '账单 ID', type: 'INTEGER' },\r\n        { column: 'TrackId', name: 'trackId', caption: '曲目或媒体 ID', type: 'INTEGER' },\r\n        { column: 'UnitPrice', name: 'unitPrice', caption: '交易单价', type: 'MONEY',\r\n          description: '账单明细上的交易单价，不是 Track 目录单价。样例原始单位，币种未指定。' }\r\n    ],\r\n    dimensions: [\r\n        {\r\n            name: 'invoice', caption: '账单', tableName: 'Invoice',\r\n            foreignKey: 'InvoiceId', primaryKey: 'InvoiceId', captionColumn: 'InvoiceId',\r\n            properties: [\r\n                { column: 'InvoiceDate', name: 'invoiceDate', caption: '账单日期', type: 'DATETIME',\r\n                  description: '按账单记录日期统计，不自行做时区转换。' },\r\n                { column: 'BillingCountry', name: 'billingCountry', caption: '账单国家', type: 'STRING' },\r\n                { column: 'CustomerId', name: 'customerId', caption: '客户 ID', type: 'INTEGER' }\r\n            ],\r\n            dimensions: [\r\n                {\r\n                    name: 'customer', caption: '客户', tableName: 'Customer',\r\n                    foreignKey: 'CustomerId', primaryKey: 'CustomerId', captionColumn: 'LastName',\r\n                    properties: [\r\n                        { column: 'FirstName', name: 'fullName', caption: '客户姓名', type: 'STRING',\r\n                          description: 'FirstName 与 LastName 拼接；与客户 ID 一起使用。',\r\n                          formulaDef: { builder: (alias) => `${alias}.FirstName || ' ' || ${alias}.LastName` } }\r\n                    ]\r\n                }\r\n            ]\r\n        },\r\n        {\r\n            name: 'track', caption: '曲目或媒体', tableName: 'Track',\r\n            foreignKey: 'TrackId', primaryKey: 'TrackId', captionColumn: 'Name',\r\n            properties: [\r\n                { column: 'Name', name: 'title', caption: '曲目或媒体名称', type: 'STRING' }\r\n            ],\r\n            dimensions: [\r\n                {\r\n                    name: 'genre', caption: '内容分类', tableName: 'Genre',\r\n                    foreignKey: 'GenreId', primaryKey: 'GenreId', captionColumn: 'Name',\r\n                    description: '包含音乐和影视内容；分类缺失不应丢弃交易金额。',\r\n                    properties: [\r\n                        { column: 'GenreId', name: 'genreId', caption: '分类 ID', type: 'INTEGER' },\r\n                        { column: 'Name', name: 'genreName', caption: '内容分类名称', type: 'STRING' }\r\n                    ]\r\n                },\r\n                {\r\n                    name: 'album', caption: '专辑', tableName: 'Album',\r\n                    foreignKey: 'AlbumId', primaryKey: 'AlbumId', captionColumn: 'Title',\r\n                    description: '曲目所属专辑，包含音乐及影视专辑；用 ID 区分同名专辑。',\r\n                    dimensions: [\r\n                        {\r\n                            name: 'artist', caption: '艺术家', tableName: 'Artist',\r\n                            foreignKey: 'ArtistId', primaryKey: 'ArtistId', captionColumn: 'Name',\r\n                            description: '以专辑所属艺术家归属交易金额，不等同于 Track.Composer 作曲者；用 ID 区分同名。'\r\n                        }\r\n                    ]\r\n                }\r\n            ]\r\n        }\r\n    ],\r\n    measures: [\r\n        { column: 'UnitPrice', name: 'lineAmount', caption: '明细金额', type: 'MONEY', aggregation: 'sum',\r\n          description: '交易单价乘数量，沿用样例原始单位，不除以 100，不指定币种。',\r\n          formulaDef: { builder: (alias) => `${alias}.UnitPrice * ${alias}.Quantity` } },\r\n        { column: 'Quantity', name: 'salesQuantity', caption: '销售数量', type: 'INTEGER', aggregation: 'sum',\r\n          description: '累计 Quantity；本样例全部为 1，也不能将数量定义成明细条数。' }\r\n    ]\r\n};\r\n```\r\n\r\n</details>\r\n\r\n<details>\r\n<summary>ChinookInvoiceLineQueryModel.qm（完整）</summary>\r\n\r\n```javascript\r\nconst line = loadTableModel('ChinookInvoiceLineModel');\r\n\r\nexport const queryModel = {\r\n    name: 'ChinookInvoiceLineQueryModel',\r\n    caption: 'Chinook 明细分析',\r\n    description: '按账单日期、客户、曲目或媒体、内容分类、专辑及其所属艺术家分析明细金额和销售数量。每行是一条明细，不提供可累计的账单总金额。艺术家按 Album.ArtistId 归属，不使用 Track.Composer。金额沿用样例原始单位，不指定币种、不做分元换算，不代表利润、回款或退款。',\r\n    loader: 'v2',\r\n    model: line,\r\n    columnGroups: [\r\n        { caption: '交易明细', items: [\r\n            { ref: line.invoiceLineId },\r\n            { ref: line.invoiceId },\r\n            { ref: line.trackId },\r\n            { ref: line.unitPrice },\r\n            { ref: line.invoice$invoiceDate },\r\n            { ref: line.invoice$billingCountry },\r\n            { ref: line.invoice$customerId },\r\n            { ref: line.invoice.customer$fullName },\r\n            { ref: line.track$title },\r\n            { ref: line.track.genre$genreId },\r\n            { ref: line.track.genre$genreName }\r\n        ] },\r\n        { caption: '专辑与艺术家', items: [\r\n            { ref: line.track.album },\r\n            { ref: line.track.album.artist }\r\n        ] },\r\n        { caption: '明细指标', items: [\r\n            { ref: line.lineAmount },\r\n            { ref: line.salesQuantity }\r\n        ] }\r\n    ],\r\n    accesses: []\r\n};\r\n```\r\n\r\n</details>\r\n\r\n## 范围说明\r\n\r\n本 issue 仅跟踪 DISTINCT 查询丢失已接受的排序，不混入另一次专用成员接口的错误对象或 slot/name mismatch。未修改数据库、权限或模型来掩盖问题，也未执行 Text-to-SQL 正确性对比。\r\n","number":145,"state":"OPEN","title":"[Bug] DISTINCT 查询接受 orderBy 但执行 SQL 丢失 ORDER BY（Launcher 0.1.25）","url":"https://github.com/foggy-projects/foggy-data-mcp-bridge/issues/145"}
