Actual Budget 的 ActualQL 实战示例从按月查询到 CLI 聚合统计【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actualActualQL 是 Actual Budget本地优先的个人财务管理应用内置的声明式查询语言本文基于官方示例文档packages/docs/docs/api/actual-ql/examples.md展开完整覆盖按月/按年搜索、按商户payee与按类别分组汇总、#interest备注模糊匹配等高频场景并结合 ActualQL 底层编译器compiler.ts、查询构造器query.ts与 CLI 工具query.ts说明每个示例背后的实现原理。读完本文你将能熟练编写从取某月全部交易到按类别统计某财年支出的完整 ActualQL 查询并在终端中用actual query run完成同样任务。前置知识ActualQL 查询的基本结构ActualQL 于 Actual 0.0.129 引入见 actual-ql/index.md它替代了此前行为被硬编码在后端的filterTransactions方法让你可以自由控制排序、字段筛选与金额汇总。一次查询由q()构造、链式调用修饰符、再由runQuery/$query执行let { q, runQuery } require(actual-app/api); let { data } await runQuery( q(transactions) .filter({ category.name: Food, date: 2021-02-20 }) .select([id, date, amount]), );q(table)构造查询从 query.ts 的q函数返回一个不可变的Query实例每次链式调用都会产生携带新状态的新实例。.filter(expr)追加过滤条件内部实现为向filterExpressions数组追加表达式query.ts。.select(exprs)指定返回字段可以传*、字段名字符串数组或混合聚合表达式的对象数组query.ts。.calculate(expr)单一聚合快捷方式等价于把表达式放入select的result字段并把calculation置为truequery.ts。执行结果为{ data }对象普通查询时data是记录数组使用calculate时data直接是聚合值。金额约定Actual 中所有金额以整数分存储例如-5000表示 -$50.00。因此下面示例中在展示前统一执行了amount / 100的换算。按月或按年搜索$transform与日期函数日期字段在数据库中通常是完整日期如2021-01-01。要按月份搜索仅用$eq: 2021-01是不够的——需要先把date字段转换为月份粒度再比较。ActualQL 的解决方案是把转换函数放在$transform键中q(transactions) .filter({ date: { $transform: $month, $eq: 2021-01 } }) .select(*);这条查询返回2021-01月份的全部交易。将$month换成$year即可按年搜索。编译器中的实现原理compileOp在处理过滤器对象时会先把$transform从操作符对象中解构出来其余键作为比较操作compiler.ts。因此{ $transform: $month, $eq: 2021-01 }等价于对字段应用$month函数后与2021-01比较。$transform的值可以是字符串如$month也可以是一个完整函数表达式对象如{ $month: $ }$代表当前字段——编译时两者都会被改写为对当前字段的调用compiler.ts。$month函数内部调用castInput(state, args[0], date-month)将输入强制转换为date-month类型$year则转换为date-year类型compiler.ts。castInput对date类型执行安全的自动降粒度转换date-month接受datedate-year接受date与date-month并据此生成对应的 SQL 表达式。最终生成的是形如CAST(date AS month) 2021-01的 SQL 片段由 exec.ts 在 SQLite 上执行。注意编译器仅接受字面量日期字符串的自动转换运行时对非字面量字符串字段的日期转换会直接抛出CompileErrorcompiler.ts。按日期范围过滤$gte/$lte与$andActualQL 支持$eq、$lt、$lte、$gt、$gte、$ne、$oneof、$regex、$like、$notlike等比较操作符。多个条件可用$and组合q(transactions).filter({ $and: [ { date: { $gte: 2020-04-06 } }, { date: { $lte: 2021-04-05 } }, ], });其编译结果就是两条 SQL 比较表达式的AND连接compileConditions以AND拼接见 compiler.ts。$gte与$lte分别编译为与compiler.ts。两种等效的简写也受支持字段值传入数组时自动按$and组合例如date: [{ $gte: 2021-01-01 }, { $lte: 2021-12-31 }]详见 actual-ql/index.md多选日期用$or。每个商户的总金额指定时间段分组求和以下示例统计 2020-04-06 至 2021-04-05英国财年内每个商户的交易总额按商户名排序输出( await $query( $q(transactions) .filter({ $and: [ { date: { $gte: 2020-04-06 } }, { date: { $lte: 2021-04-05 } }, ], }) .groupBy(payee.name) .orderBy(payee.name) .select([payee.name, { amount: { $sum: $amount } }]), ) ).data.map(row { console.log(${row[payee.name]}: ${row.amount / 100}); });关键点拆解点号字段与表连接payee.name中的.会让编译器穿透到被引用表。transactions.payee是payees表的外键 idpayee.name通过表连接makePath/resolvePath见 compiler.ts直接取得商户名称从而按名称而非 id 过滤/分组。聚合必须命名在select中使用聚合表达式时必须给结果命名这里命名为amount不命名会直接报错。$sum: $amount中的$amount是对字段的引用。排序orderBy(payee.name)默认升序orderBy也接受对象形式{ category.name: desc }或数组做多字段排序。备注含#interest (P)的全部交易总额当需要筛选备注中带有特定文本的交易时使用$like做 SQL 风格的模糊匹配%为通配符。文档给出了两种等价写法结果一致写法一calculate直接返回聚合值( await $query( $q(transactions) .filter({ $and: [ { date: { $gte: 2020-04-06 } }, { date: { $lte: 2021-04-05 } }, { notes: { $like: %#interest (P)% } }, ], }) .calculate({ $sum: $amount }), ) ).data / 100;写法二select命名聚合后取元素( await $query( $q(transactions) .filter({ $and: [ { date: { $gte: 2020-04-06 } }, { date: { $lte: 2021-04-05 } }, { notes: { $like: %#interest (P)% } }, ], }) .select({ total: { $sum: $amount } }), ) ).data[0].total / 100;区别在于calculate把聚合表达式包进{ result: expr }并设置calculation: truequery.ts执行结果data直接就是数值而select的data是单元素数组需要data[0].total取值。calculate的便利性在于无需为聚合命名。每个类别的总金额按分组层级排序按类别分组时category.name通过categories表连接取得类别名称同时还能访问其所属的类别组category.group.name并按照类别组顺序 类别顺序排序从而得到与预算页面一致的展示层级( await $query( $q(transactions) .filter({ $and: [ { date: { $gte: 2020-04-06 } }, { date: { $lte: 2021-04-05 } }, ], }) .groupBy(category.name) .orderBy([category.group.sort_order, category.sort_order]) .select([ category.group.name, category.name, { amount: { $sum: $amount } }, ]), ) ).data.map(row { console.log( ${row[category.group.name]}/${row[category.name]}: ${ row.amount / 100 }, ); });输出形如Essentials/Groceries: 245.32。这里体现了多级点号字段的解析能力category.group.name需要先从transactions连接到categories再从categories的group_id连接到category_groups编译器会在路径不存在时抛出Path does not exist的CompileErrorcompiler.ts。在 CLI 中运行相同查询以上示例均为 JavaScript 写法。若使用actual-app/cli提供的 CLI 工具可以用命令行参数表达同样查询。CLI 文档中的映射对照如下# 选择特定字段JS: .select([date, amount, payee.name]) actual query run --table transactions --select date,amount,payee.name # 条件过滤JS: .filter({ amount: { $lt: 0 } }) actual query run --table transactions --filter {amount:{$lt:0}} # 字段降序JS: .orderBy([{ date: desc }]) actual query run --table transactions --order-by date:desc # 按月搜索JS: .filter({ date: { $transform: $month, $eq: 2021-01 } }) actual query run --table transactions --filter {date:{$transform:$month,$eq:2021-01}} # 按 payee 分组求和 —— 聚合表达式需使用 --file 传入 echo {table:transactions,groupBy:[payee.name],select:[payee.name,{amount:{$sum:$amount}}]} | actual query run --file - # 统计交易数JS: .calculate({ $count: * }) actual query run --table transactions --count # 快捷方式最近 10 笔交易 actual query run --last 10CLI 参数背后的实现--select、--filter、--order-by、--group-by、--limit、--offset、--count、--file是query run的主要选项见 cli.md 中的 Options 表。--where是--filter的别名二者不能同时使用。--order-by date:desc,amount:asc会被解析为 AQL 的[{ date: desc }, { amount: asc }]形式不写方向默认升序方向只接受asc/desc非法方向会直接报错query.ts。--last n是一个快捷参数隐含--table transactions与--order-by date:desc默认输出列为date, account.name, payee.name, category.name, amount, notesquery.ts。由于--filter/--select等参数以字符串传递聚合表达式这类复杂结构统一通过--fileJSON 文件或-从 stdin 读取构造完整查询CLI 内部以 API 包 为桥梁把解析后的查询对象交给 ActualQL 执行器。CLI 内置了各表字段元数据query.tsquery fields transactions可以直接列出date、amount、payee.name、category.group.name等可用字段及类型方便构造上述查询。实战技巧与注意点拆分交易split transactions的处理默认splits: inline只返回子交易不返回父交易求和不会重复计数grouped则总是返回完整拆分交易并附带subtransactions属性另有高级选项all以扁平列表同时返回父与子。需要全量精确求和时也可显式过滤is_parent: false详见 actual-ql/index.md 与 cli.md 的 Tips。未分类交易没有类别的交易其category.name为null按类别过滤或分组时要考虑到这一点cli.md。不要用 AQL 字段做日期子字段date.month、date.year这类字段在 AQL 中并不存在按月分组应使用$transform方案或按日期范围取数后在脚本中自行聚合。金额换算所有金额单位为整数分CLI 的--format table/csv会自动转为小数显示而 JSON 输出始终为原始分值脚本消费时记得除以 100。进一步阅读ActualQL 概念与过滤操作符全集actual-ql/index.md字段引用.穿透连接、排序与聚合函数说明actual-ql/functions.mdActualQL 函数与类型参考api/reference.mdCLI 完整命令与配置说明api/cli.md查询构造器实现loot-core/src/shared/query.ts查询编译器实现loot-core/src/server/aql/compiler.ts【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考