Files
workspace/code/fms/fms-vue/tests/unit/query-ast.spec.js
T
2026-09-17 21:29:08 +08:00

238 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import {
advancedQueryFromAst,
astToSql,
buildAdvancedAst,
buildQuickAst,
defaultOperator,
emptyConditionValue,
hasConditionValue,
operatorsForField,
} from '@/components/fms-module-common/queryUtils'
const fields = new Map([
['name', { b_field: 'name', b_type: 'input' }],
['amount', { b_field: 'amount', b_type: 'money' }],
['created', { b_field: 'created', b_type: 'datetime' }],
['status', { b_field: 'status', b_type: 'select' }],
])
describe('查询条件 AST 构造', () => {
it('按字段类型给出默认操作符:文本 like、日期 between、其余 eq', () => {
expect(defaultOperator({ b_type: 'input' })).toBe('like')
expect(defaultOperator({ b_type: 'datetime' })).toBe('between')
expect(defaultOperator({ b_type: 'select' })).toBe('eq')
expect(operatorsForField({ b_type: 'money' })).toContain('between')
})
it('生成转义后的 like / 不等 / 空值条件', () => {
expect(
astToSql({ type: 'condition', field: 'name', operator: 'like', value: "O'Reilly" }, fields),
).toBe("[name] LIKE N'%O''Reilly%'")
expect(
astToSql({ type: 'condition', field: 'name', operator: 'not_like', value: 'a' }, fields),
).toBe("[name] NOT LIKE N'%a%'")
expect(
astToSql({ type: 'condition', field: 'status', operator: 'ne', value: 'done' }, fields),
).toBe("[status] <> N'done'")
expect(
astToSql({ type: 'condition', field: 'name', operator: 'is_null', value: '' }, fields),
).toBe('[name] IS NULL')
})
it('区间条件可退化为单边比较,日期单边取对应端点', () => {
expect(
astToSql(
{ type: 'condition', field: 'amount', operator: 'between', value: ['10', ''] },
fields,
),
).toBe('[amount] >= 10')
expect(
astToSql(
{ type: 'condition', field: 'created', operator: 'ge', value: ['2026-01-01', ''] },
fields,
),
).toBe("[created] >= N'2026-01-01 00:00:00'")
expect(
astToSql(
{ type: 'condition', field: 'created', operator: 'le', value: ['', '2026-01-31'] },
fields,
),
).toBe("[created] <= N'2026-01-31 23:59:59'")
})
it('嵌套条件组按各自逻辑加括号,空组不产生条件', () => {
const ast = {
type: 'group',
logic: 'and',
children: [
{ type: 'condition', field: 'status', operator: 'eq', value: 'new' },
{
type: 'group',
logic: 'or',
children: [
{ type: 'condition', field: 'name', operator: 'like', value: 'a' },
{ type: 'condition', field: 'name', operator: 'like', value: '' },
],
},
],
}
expect(astToSql(ast, fields)).toBe("([status] = N'new' AND [name] LIKE N'%a%')")
expect(astToSql({ type: 'group', logic: 'and', children: [] }, fields)).toBe('')
})
it('常用条件跳过空值,操作符取模块配置', () => {
const items = [
{ field: fields.get('name'), config: { b_operator: 'like' } },
{ field: fields.get('status'), config: { b_operator: 'eq' } },
]
const ast = buildQuickAst(items, { name: 'abc', status: '' })
expect(ast.children).toHaveLength(1)
expect(astToSql(ast, fields)).toBe("[name] LIKE N'%abc%'")
})
it('高级条件按每项 join 生成 AST,单条件的组不再套一层', () => {
const ast = buildAdvancedAst({
nodes: [
{ join: 'or', field: 'name', operator: 'like', value: 'a' },
{ join: 'or', field: 'name', operator: 'like', value: 'b' },
],
groups: [
{
id: 'g1',
join: 'or',
nodes: [{ join: 'and', field: 'status', operator: 'eq', value: 'new' }],
},
],
})
// join 决定每项与上一相邻项的连接;首项 join 忽略;单条件的组等价于一个普通条件
expect(astToSql(ast, fields)).toBe(
"([name] LIKE N'%a%' OR [name] LIKE N'%b%' OR [status] = N'new')",
)
})
it('支持 (A 且 B) 或 C:组行内且、组与前一行或', () => {
const ast = buildAdvancedAst({
nodes: [{ join: 'or', field: 'name', operator: 'eq', value: '张三' }],
groups: [
{
id: 'g1',
join: 'or',
nodes: [
{ join: 'and', field: 'amount', operator: 'gt', value: 100 },
{ join: 'and', field: 'amount', operator: 'gt', value: 1000 },
],
},
],
})
expect(astToSql(ast, fields)).toBe("([name] = N'张三' OR ([amount] > 100 AND [amount] > 1000))")
})
it('同一层可混用 且/或:(a 且 b) 或 c 从左到右加括号', () => {
const ast = buildAdvancedAst({
nodes: [
{ join: 'and', field: 'name', operator: 'like', value: 'a' },
{ join: 'and', field: 'name', operator: 'like', value: 'b' },
{ join: 'or', field: 'name', operator: 'like', value: 'c' },
],
})
expect(astToSql(ast, fields)).toBe(
"(([name] LIKE N'%a%' AND [name] LIKE N'%b%') OR [name] LIKE N'%c%')",
)
})
it('区间 / 多选只要有一项即算已填写,空值按操作符重置', () => {
expect(hasConditionValue(['', '20'])).toBe(true)
expect(hasConditionValue(['', ''])).toBe(false)
expect(hasConditionValue(undefined)).toBe(false)
expect(emptyConditionValue({ b_type: 'money' }, 'between')).toEqual(['', ''])
expect(emptyConditionValue({ b_type: 'select' }, 'eq')).toBeUndefined()
})
})
describe('高级条件 AST → 界面状态(advancedQueryFromAst)', () => {
let seq = 0
const idFactory = () => `n${(seq += 1)}`
it('空 AST / 空组都给空结构且不降级', () => {
expect(advancedQueryFromAst(null, { idFactory })).toEqual({
nodes: [],
groups: [],
degraded: false,
})
expect(advancedQueryFromAst({ type: 'group', children: [] }, { idFactory })).toEqual({
nodes: [],
groups: [],
degraded: false,
})
})
it('单层「行 + 组」往返:还原后再构造语义等价', () => {
const ast = buildAdvancedAst({
nodes: [
{ join: 'and', field: 'name', operator: 'like', value: 'a' },
{ join: 'or', field: 'name', operator: 'like', value: 'b' },
],
groups: [
{
join: 'and',
nodes: [
{ join: 'and', field: 'amount', operator: 'gt', value: 100 },
{ join: 'or', field: 'status', operator: 'eq', value: 'done' },
],
},
],
})
const state = advancedQueryFromAst(ast, { idFactory })
expect(state.degraded).toBe(false)
expect(state.nodes.map((node) => [node.field, node.join])).toEqual([
['name', 'and'],
['name', 'or'],
])
expect(state.groups).toHaveLength(1)
expect(state.groups[0].nodes.map((node) => [node.field, node.join])).toEqual([
['amount', 'and'],
['status', 'or'],
])
expect(astToSql(buildAdvancedAst(state), fields)).toBe(astToSql(ast, fields))
})
it('组内再嵌套组:拍平成单层并置 degraded(根层的组不算降级)', () => {
const ast = {
type: 'group',
children: [
{ type: 'condition', field: 'name', operator: 'like', value: 'a', join: 'and' },
{
type: 'group',
join: 'or',
children: [
{ type: 'condition', field: 'amount', operator: 'gt', value: 1, join: 'and' },
{
type: 'group',
join: 'and',
children: [{ type: 'condition', field: 'amount', operator: 'lt', value: 9 }],
},
],
},
],
}
const state = advancedQueryFromAst(ast, { idFactory })
expect(state.degraded).toBe(true)
expect(state.nodes.map((node) => node.field)).toEqual(['name'])
expect(state.groups).toHaveLength(1)
expect(state.groups[0].nodes.map((node) => node.field)).toEqual(['amount', 'amount'])
expect(state.groups[0].join).toBe('or')
})
it('单条件 AST(非 group)也能还原成一行', () => {
const state = advancedQueryFromAst(
{ type: 'condition', field: 'status', operator: 'eq', value: 'draft' },
{ idFactory },
)
expect(state.nodes).toHaveLength(1)
expect(state.nodes[0]).toMatchObject({ field: 'status', operator: 'eq', value: 'draft' })
})
})