返回正文目录

概要

Launcher 0.1.25 中,下方带 distinct: true 与 orderBy: ["artist$id"] 的合法查询校验、执行均成功,normalized orderBy 仍是升序,但实际执行 SQL 和 explain SQL 都缺少 ORDER BY,返回结果没有按请求排序。

归属:Java Runtime / 查询编译与执行。当前只确认本文 SQLite/Chinook 请求,不概括为全部 DISTINCT 查询都失败,也尚未定位具体代码分支。

环境

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"
  }
}

复现步骤

  1. 按 Release README 下载并校验发行资产,使用 start-foggy-runtime.ps1 -Port 18067 -WorkDir ./work 启动。运行 wait-ready、capabilities。
  2. 下载上面的 Chinook SQLite,使用副本,例如保存为当前目录的 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
  1. 将下文两个模型文件分别保存到 ./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。未另外执行干净进程复现,不宣称已排除全部状态因素。

  1. 将以下 payload 保存为 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

预期

实际

[
  {
    "dir": "asc",
    "field": "artist$id",
    "nullFirst": false,
    "nullLast": false
  }
]
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。

原始模型(公开样例,无业务凭据)

ChinookInvoiceLineModel.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(完整)
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 正确性对比。