Launcher 0.1.25 中,下方带 distinct: true 与 orderBy: ["artist$id"] 的合法查询校验、执行均成功,normalized orderBy 仍是升序,但实际执行 SQL 和 explain SQL 都缺少 ORDER BY,返回结果没有按请求排序。
归属:Java Runtime / 查询编译与执行。当前只确认本文 SQLite/Chinook 请求,不概括为全部 DISTINCT 查询都失败,也尚未定位具体代码分支。
db5db39aa36dc30b51d9db7d941e333c8d39e0d2;内部 Java 项目版本 9.3.0-SNAPSHOT。b3fe4b6a3189132363b59ba16c143ef6ddab2fd7886ef0d7e7c0838fef1c350a。运行进程已核对实际加载此 JAR。foggy-runtime 0.1.24,公开安装包,无源码构建、无产品代码修改。http://127.0.0.1:18067,namespace chinook,命名数据源 chinook-sqlite;Console/FAP 关闭,匿名本地开发查询。bdf635be69850bd3be09c9a2dbeef7ddfb80036bd3ef3381383cd03b61e4a61a。capabilities 摘要:
{
"engine": "java",
"runtimeApiVersion": "foggy-runtime-api/v1",
"schemaVersion": "2026-06-06",
"securityMode": "none-dev-test-only",
"capabilities": {
"models.describe": "supported",
"query.validate": "supported",
"query.execute": "supported",
"query.explain": "supported"
}
}
start-foggy-runtime.ps1 -Port 18067 -WorkDir ./work 启动。运行 wait-ready、capabilities。Chinook.sqlite。配置命名数据源并绑定 namespace:$dbPath = (Resolve-Path ./Chinook.sqlite).Path -replace '\\', '/'
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook datasources add --name chinook-sqlite --type sqlite --jdbc-url "jdbc:sqlite:$dbPath"
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook datasources bind --namespace chinook --data-source chinook-sqlite
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook datasources test chinook-sqlite
./models/model/ChinookInvoiceLineModel.tm 和 ./models/query/ChinookInvoiceLineQueryModel.qm。在尚未登记该目录的新实例中先校验,再登记、刷新和 describe:foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook models validate --models-dir ./models
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook bundles add --name chinook-distinct-repro --path ./models
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook models refresh
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook models describe ChinookInvoiceLineQueryModel
本次实际教学实例为六文件校验全部通过;新复现实例只需本文这一对 TM/QM。未另外执行干净进程复现,不宣称已排除全部状态因素。
distinct-artists.json,对同一个文件先校验、再执行、再 explain:{
"columns": [
"artist$id",
"artist$caption"
],
"slice": [
{
"field": "invoice$invoiceDate",
"op": "[)",
"value": [
"2012-01-01",
"2013-01-01"
]
}
],
"distinct": true,
"orderBy": [
"artist$id"
],
"limit": 500,
"returnTotal": true
}
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook query validate ChinookInvoiceLineQueryModel --payload ./distinct-artists.json
foggy-runtime --base-url http://127.0.0.1:18067 --namespace chinook query execute ChinookInvoiceLineQueryModel --payload ./distinct-artists.json
foggy-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
[1, 2, 3, 4, 5, 6, 7, 8]。success: true,没有排序未生效的明确报错。[90, 91, 92, 93, 94, 95, 46, 96],不是升序。[
{
"dir": "asc",
"field": "artist$id",
"nullFirst": false,
"nullLast": false
}
]
data.debug.extra.sql 实际执行 SQL 如下,完全没有 ORDER BY;query explain 的 sqlTrace.finalPhysicalSql 也缺少 ORDER BY:select distinct d5.ArtistId "artist$id", d6.Name "artist$caption" from InvoiceLine t1 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 d1.InvoiceDate >=? and d1.InvoiceDate <?
返回结果节选:
{
"success": true,
"total": 119,
"items": [
{
"artist$caption": "Iron Maiden",
"artist$id": 90
},
{
"artist$caption": "James Brown",
"artist$id": 91
},
{
"artist$caption": "Jamiroquai",
"artist$id": 92
},
{
"artist$caption": "JET",
"artist$id": 93
},
{
"artist$caption": "Jimi Hendrix",
"artist$id": 94
},
{
"artist$caption": "Joe Satriani",
"artist$id": 95
},
{
"artist$caption": "Jorge Ben",
"artist$id": 46
},
{
"artist$caption": "Jota Quest",
"artist$id": 96
}
]
}
同一模型、同一日期范围,按 artist$id、artist$caption 分组,并带 count(invoiceLineId) as lineCount,再按 artist$id 排序,得到同一批 119 位成员且 ID 升序正确。该对照说明模型字段可以参与有效排序,但不能据此推断 DISTINCT 全部分支的根因。
未排序的 DISTINCT 结果不能提供调用方预期的排序保证;后续依赖 LIMIT/OFFSET 展示成员时会受到影响。本次 DISTINCT 请求 limit=500,119 项全部返回,因此不是分页截断或客户端只展示前一页造成的错觉。尚未对这个异常 DISTINCT 请求单独验证重复页或缺失页。
建议回归覆盖:本文升序请求、降序、带相同展示名称的不同 ID、明确次序下的 LIMIT/OFFSET;同时检查执行结果和最终 SQL,不只检查 normalized DSL 或 success。
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,也不能将数量定义成明细条数。' }
]
};
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: []
};
本 issue 仅跟踪 DISTINCT 查询丢失已接受的排序,不混入另一次专用成员接口的错误对象或 slot/name mismatch。未修改数据库、权限或模型来掩盖问题,也未执行 Text-to-SQL 正确性对比。