LogiQL 入门
0x00 前言
最近看的工具中用到了 Doop,这是一个以 指针分析 算法为中心的 Java 静态分析框架,使用 Datalog 规则形式表达的各种分析的集合。维护两套规则:
- LogicBlox:使用 LogiQL,Datalog 程序的构建块以 谓词 的形式出现,例如
Person("John")
、Parent("John", "Johnny jr")
,利用 规则(IDB 逻辑)从已知的事实中推断出新的信息,例如Ancestor(x, y) <- Parent(x, y)
- souffle-lang/souffle (Systematic, Ontological, Undiscovered Fact Finding Logic Engine):由 Datalog 启发的一种编程语言,可用于静态分析、网络分析和数据分析。默认使用该引擎。
没看到默认用的是开源的 souffle,结果花了一下午入门 LogiQL 还有找 3.10.x 版本的 LogicBlox …
LogicBlox 一直是闭源软件,目前最新版本 4.x.x 都可以下载,Release Archive (LB 3.10.x) 不再提供下载,可以通过 LogicBlox Academic License Request 申请。找了好久没地方下载还要申请,回头仔细看了看 Doop 的说明文档才发现应该去看 souffle 😅😅😅
LogiQL 入门跟着教程 LogiQL in 30 minutes 走就好了,介绍的特别简单,能够让人刚踩到门槛上吧(大概)。
0x01 LogiQL 入门
实验环境
用在线的 LogicBlox REPL (Read-Eval-Print Loop) 进行实验,REPL 是一个基于命令的交互式环境,可以在其中执行 LogiQL 命令并立即检查结果。
术语
工作区(workspace)指代数据库(database),结合了模式(schema)、逻辑(logic)和数据(data)
实验过程
定义一个冰淇淋商店会计系统,记录以下信息:
- 成本(人工,原料):cost(labor, ingredients)
- 价格:price
- 销量/周:sales
- 每个冰淇淋的收入/周:revenue
- 所有冰淇淋的收入/周:week revenue
- 每个冰淇淋的收益/周:profit
- 所有冰淇淋的收益/周:week profit
定义功能谓词(functional predicates)
- 谓词类似 SQL 数据库中的表
- 除了最后一列之外的所有列都形成一个主键,最后一列表示值,因此一个主键最多可以关联一个值。
1. cost
谓词
定义名为 cost 的谓词,键为 icecream,值为 c;指向右侧的箭头表示约束(constraint),icecream 的值必须是字符串、c 的值必须是整数
- 约束表明如果箭头左侧为真,则右侧必须成立
1 | cost[icecream] = c -> |
在 REPL 中执行需要使用相应的命令,addblock
接受一个参数,在引号之间添加的定义的 LogiQL 代码。
1 | addblock 'cost[icecream] = c -> |
添加之后使用 list
命令列出谓词
1 | list |
2. 事实(facts)
谓词中的数据条目在 LogiQL 中称为事实(facts)。
LogiQL 支持 3 种增量更新操作
- 插入(insert):插入新事实,使用
+
前缀 - 删除(remove):删除事实,使用
-
前缀 - 更新(upsert):添加或更新,使用
^
前缀,只能应用在功能谓词上
使用 exec
执行一次性的命令,而 addblock
可以理解为“永久”地将谓词添加到工作区。
添加一个成本为 23 的冰淇淋:
1 | exec '+cost["Popsicle Lemon"] = 23.' |
print
命令用于打印谓词。
1 | print cost |
添加冰淇淋(成本):
1 | exec '+cost["Fruit Sundae"] = 120. |
更新冰淇淋的售价:
1 | exec '^cost["Popsicle Lemon"] = 25.' |
3. price
谓词、week_sales
谓词
定义谓词 price 表示冰淇淋的售价,定义谓词 week_sales 表示每周的销售数量(可以把 week_sales 看作二维映射)。
1 | addblock 'price[icecream] = p -> |
添加冰淇淋售价及其第 1、2 周的销量(往谓词中添加事实):
1 | exec '+price["Popsicle Lemon"] = 50. |
打印谓词:
1 | print price |
4. 规则(rules)
规则可用于推导新的事实,添加计算收入 revenue
的规则:
1 | addblock 'week_revenue[icecream, week] = |
打印规则,该规则会自动进行计算:
1 | print week_revenue |
修改一个价格,会自动进行重新计算:
1 | exec '^price["Popsicle Lemon"] = 70.' |
添加计算冰淇淋收益 profit
的规则、计算每周收益的规则 week_profit
:
1 | addblock 'profit[icecream] = |
然后打印规则,会自动进行计算:
1 | print profit |
5. 聚合函数(Aggregate)
和 SQL 类似,LogiQL 中也有聚合函数。
定义 agg_profit 规则,使用 total()
聚合函数求和,将 profit 中所有事实的值 p 求和绑定到 value,其中 week 是聚合的周,使用通配符 _
忽略冰淇淋类型。表示总收益(收入-成本)。
agg_revenue 规则也是类似的定义,只不过把 p 换成了 r,表示总收入。
1 | agg_profit[week] = value <- |
使用 addblock
添加规则:
1 | addblock 'agg_profit[week] = value <- |
打印规则,得到聚合的结果:
1 | print agg_profit |
定义一个没有键的规则计算 price 谓词中的事实(条目)数量,即冰淇淋类型的数量:
1 | addblock 'icecream_count[] = value <- |
打印规则,得到冰淇淋类型的数量:
1 | print icecream_count |
6. 查询(query)
之前使用 print 查看谓词和规则的值,现在可以用 query 执行更高级的查询。
定义一个谓词 _
用事实填充,通常使用规则实现。
下面的查询与 print week_sales
拥有等价的结果。这里不使用功能谓词语法 predicate[k1, k2] = v
,而是使用 predicate(k1, k2, k3)
,因为这里对键值不感兴趣,只对事实感兴趣。
1 | query '_(icecream, week, value) <- |
检索 week_sales 谓词中使用的所有冰淇淋,尽管 week_sales 谓词多次包含这些值中的每一个,但每个不同值只返回一次(有 1、2 两周的数据,所以冰淇淋重复出现)
- 在 LogiQL 中所有谓词都是集合:每一行只能出现一次
1 | query '_(icecream) <- week_sales[icecream, _] = _.' |
将冰淇淋和每周的销售额、收入、收益整合在一起,就像 SQL 中的 JOIN 操作:
1 | query '_(icecream, week, sales, revenue, profit) <- |
7. 约束(constraint)
降低冰淇淋的售价,再次查询发现冰淇淋的收益变成了负数:
1 | exec '^price["Popsicle Lemon"] = 20.' |
定义一个约束:对于 profit 谓词中的每个事实,value 应该大于等于 0。
1 | profit[_] = value -> value >= 0. |
使用 addblock
添加约束,因为 profit 谓词中有事实的 value 值小于 0,所以添加约束失败。
1 | addblock 'profit[_] = value -> value >= 0.' |
修改冰淇淋售价后再添加约束:
1 | exec '^price["Popsicle Lemon"] = 40.' |
再次修改冰淇淋售价,返回约束错误:
1 | exec '^price["Popsicle Lemon"] = 10.' |
0x02 结语
有一说一,这个入门完全不足以让人看懂 doop/lb-logic 里面的规则,做都做了就理上来了XD
补充:本地可以使用 PA-Datalog 引擎,是 LogicBlox v3 的改良版本,可用于程序分析。