# 原生结构化查询

需要 `fsys.openXml` 0.15.0+。不依赖 Office、ACE/OLEDB；不解析 SQL、不调用 eval、不执行用户回调。只处理保存值及公式缓存，不重新计算。

## 筛选、投影、排序和汇总

```aardio
import fsys.openXml.xlsx;
var result,err = fsys.openXml.xlsx.querySheet("/input.xlsx",1,{
    range = "B3:M8";             // 第一行明确用作标题
    columns = ["Name","Number"];
    where = {column="Number";op="ge";value=0};
    orderBy = [{column="Number";descending=true}];
    offset = 0;
    limit = 20;                  // 结果上限，不是扫描上限
    maxScanRows = 10000;
    maxScanCells = 200000;
    aggregates = [{name="total";op="sum";column="Number"}];
});
if(!result) return null,err;
return result;
```

参数2的工作表定位规则同 readSheet。不指定 range 时使用实际已存储格边界，不信任 dimension。指定 range 时保留矩形的空边界。标题行可为任意内容，因此有封面/大标题时应显式选数据区域，不能依赖自动猜表头。

### 列与结果

- `header=true` 默认将选区首行作为标题；false 时首行也是数据，字段名为 ColumnA、ColumnB 等**绝对列字母**。
- 标题 trim，空标题补 Column+列字母，重名追加 `_2/_3`。只处理标题，不 trim 数据，不转换数字文本。
- `columns` 不指定则返回全部可选列；指定时按输入顺序投影，元素为标题字符串、绝对列号，或 `{column=...,name="别名"}`。空数组表示零列投影（仍返回原行号）。不接受表达式。
- 数值列定位永远是工作表绝对列号，区域 B3:H10 中列3仍是 C，而非 D。字符串 `"C"` 被当作标题，不是列字母。
- 结果 `columns` 项含 `name,column,letter,hidden`；`rows` 项为 `{row,values,refs}`，row 为原始工作表行号，refs 与投影逐列对应。
- `values` 是带普通 length 字段的稀疏数组，用 `table.len(values)` 或结果 columns 宽度遍历；null、空字符串、0、false 保持区别。
- 默认跳过没有值也没有公式的行。显式空串是有值；只有无缓存公式的行也不是空行。`includeEmptyRows=true` 可让矩形中完全缺失的坐标行参与条件。

## 条件语法

叶子为 `{column,op,value}`；支持以下 op：

| op | 规则 |
|---|---|
| `eq` / `ne` | 全等/不全等，不进行类型转换；value省略可与null比较 |
| `gt/ge/lt/le` | 同类型 number/string 才比较；数值与数字文本不互转 |
| `contains` | 大小写敏感的字面包含，只用于字符串；不是正则；空 value 匹配所有字符串（含空串） |
| `in` | value 必须为不超过10000项的标量数组，逐项全等 |
| `isNull` | 缺格、缺值或无缓存公式；不包括空串/0/false |
| `isEmpty` | 只匹配存在的空字符串 |

组合为 `{all=[条件...]}` 或 `{any=[条件...]}`，可嵌套；一个节点不能同时有 all/any。空 all 为真、空 any 为假。最多256节点/16层；不支持条件代码字符串或函数。数组须稠密且没有附加哈希字段。

```aardio
import fsys.openXml.xlsx;
var result,err = fsys.openXml.xlsx.querySheet("/input.xlsx",1,{
    range="B3:M8";
    where={all=[
        {column="Flag";op="eq";value=false},
        {any=[
            {column="Code";op="eq";value="00123"},
            {column="Empty";op="isEmpty"}
        ]}
    ]};
});
if(!result) return null,err;
return result;
```

错误单元格的查询值是原始错误字符串，如 `#N/A`；不会作为缺值处理。需要区分错误类型与同文本普通字符串时使用 findCells 的返回项 `valueType` 检查。共享公式正文不展开。

### 排序

`orderBy=[{column=...,descending=false,nulls="last"},...]`，最多64个键：

- 同类型按数值/字符串字节序/false→true 排序；字符串区分大小写，不做语言区域排序。
- 混合类型有固定顺序：number、string、boolean。
- null 默认最后，可设 first；位置不随 descending 翻转。
- 全部排序键相同时按原工作表行号升序，保证稳定、可定位。
- 先匹配并排序，再 offset/limit 分页；不支持公式表达式排序。

### 简单汇总

`aggregates=[{name="唯一名称",op="count/sum/min/max/average",column=...}]`，最多128项。

- count 不指定 column 计匹配行；指定 column 计非null值，包含空串、0、false及错误字符串。
- 其他运算只使用数值单元格；不转换数字文本，不把 false 当0，不使用错误值或无缓存公式。
- `aggregates[name]` 返回 `value,numericCount,skippedCount,op,column,name`。numericCount/skippedCount用于数值运算，count只返回计数值。
- 空数集 sum=0；min/max/average 的 value 为null。数值溢出报错。
- **汇总先于分页，始终仅针对已扫描的匹配行**，`aggregateScope="matched-scanned-rows"`。没有 GROUP BY/JOIN/SQL 子查询。

## 限额与完整性

| 选项 | 默认 | 上限/含义 |
|---|---|---|
| `maxScanRows` | 100000 | 最高1048576，按选区数据坐标行计，标题行除外 |
| `maxScanCells` | 1000000 | 最高10000000，每个扫描行按选区完整宽度计成本；不扫描半行 |
| `offset` | 0 | 跳过匹配结果数，最高1048576 |
| `limit` | 100 | 返回结果行上限，最高1000000；0仅统计，不返回行 |
| `maxResultCells` | 1000000 | 最高10000000，返回行数×投影宽度超限则失败；正整数 |

扫描限额0表示不扫描。即使跳过空行/隐藏行，也计算扫描成本。offset/limit不影响匹配总数或聚合；maxScanRows/maxScanCells会影响。为避免误解，查询**拒绝 maxRows/maxColumns 及起点 row/column**，请使用 range 和上述扫描/结果限额。

返回信息：

- `scannedRows,scannedCells,totalDataRows,matchedRows,returnedRows`；
- `actualRange,selectedRange,scannedRange`，分别为源实际边界、请求矩形和真正扫描的数据坐标范围；
- `scanTruncated/scanComplete` 只描述指定选区的扫描是否完整，不声称全工作簿完整；
- `resultTruncated` 表示 offset/limit 遗漏匹配结果；`truncated` 为两者之一；
- `warnings` 包含公式缓存和扫描边界信息。

扫描数量为0或矩形为空时 scannedRange=null。即使没有行匹配，scanTruncated仍必须检查。**不要把扫描前100行所得的sum/average称为整张表统计。**

### 可见行列

`visibleOnly=true`：仅使用保存的工作表/行/列隐藏标记，不重新执行自动筛选条件。隐藏行、隐藏页不产生匹配；默认投影排除隐藏列；显式在投影/条件/排序/汇总中访问隐藏列会报错。合并格仍不填充。

## 按单元格查找

```aardio
import fsys.openXml.xlsx;
var result,err = fsys.openXml.xlsx.findCells("/input.xlsx",1,{
    range="B3:M8";
    target="formula";
    op="contains";
    value="C4";
    maxScanCells=10000;
    limit=20;
});
if(!result) return null,err;
return result;
```

- target 默认为 value；formula 只匹配实际存在 `<f>` 的格，包括无缓存公式，匹配保存的正文，不重算/展开共享公式。
- op/value 与上述标量条件相同；不接受 where、列投影、排序、聚合或 maxScanRows。
- 只扫描选区中已存储 c 节点，**不枚举不存在的空格**。所以 findCells 的 isNull 与 querySheet 的矩形缺格判断范围不同。
- 返回 `cells,scannedCells,matchedCells,returnedCells,sourceSelectedCells,lastScannedRef`、实际/选择范围与扫描/结果截取状态；cells仍含原始坐标、类型、公式和缓存字段。
- maxScanCells/offset/limit 的默认与上限同查询；visibleOnly 可排除隐藏格；可选 includeStructure/includeStyles/parseDates。

所有新入口失败返回 null,err。通用 ZIP/XML 安全限额照常生效。此版本使用 DOM；扫描限额约束查询处理与结果，不避免解析整张工作表或保留选区的稀疏cells。查询不同时构造完整值矩阵，但不能据此承诺百万行流式能力。
