Skip to main content

· 3 min read
Geyu Chen

Apache DevLake is a dev data platform that can collect and integrate data from different dev tools including Jira, Github, Gitlab and Jenkins.

This blog will not aim at a comprehensive summary of the compatibility of database but a record of issues for future reference.

1.Different Data Types

PostgreSQL does not have a uint type

type JenkinsBuild struct {
common.NoPKModel
JobName string `gorm:"primaryKey;type:varchar(255)"`
Duration float64 // build time
DisplayName string // "#7"
EstimatedDuration float64
Number int64 `gorm:"primaryKey;type:INT(10) UNSIGNED NOT NULL"`
Result string
Timestamp int64 // start time
StartTime time.Time // convered by timestamp
CommitSha string
}

In JenkinsBuild.Number, thegormstruct tag used UNSIGNED, which will lead to the failure to create table and should be removed.

MySQL does not have a bool data type

For a field defined as bool type in model, gorm will map it to MySQL's TINYINT data type, which can be queried directly with 0 or 1 in SQL, but PostgreSQL has a bool type, so gorm will map it to the BOOL type. If 0 or 1 is still used in SQL to query, there will be a report of error.

Here is an example(only relevant fields are shown in the example). The lookup statement works in MySQL, but will lead to an error in PostgreSQL.

type GitlabMergeRequestNote struct {
MergeRequestId int `gorm:"index"`
System bool
}

db.Where("merge_request_id = ? AND `system` = 0", gitlabMr.GitlabId).

After changing the sentence as it follows, an error will still be reported. The reason will be shown in the part about backticks.

db.Where("merge_request_id = ? AND `system` = ?", gitlabMr.GitlabId, false)

2.Different Behaviors

Bulk insertion

When ON CONFLIT UPDATE ALL was used to achieve bulk insertion, and if there are multiple records with the same primary key, it will report errors in PostgreSQL but not in MySQL.

Inconsistent definition of model with schema

For example, in the model definition, GithubPullRequest.AuthorId is of the int type, but this field in the database is of VARCHAR type. When inserting data, MySQL will accept it, but ProstgresSQL will report an error.

type GithubPullRequest struct {
GithubId int `gorm:"primaryKey"`
RepoId int `gorm:"index"`
Number int `gorm:"index"`
State string `gorm:"type:varchar(255)"`
Title string `gorm:"type:varchar(255)"`
GithubCreatedAt time.Time
GithubUpdatedAt time.Time `gorm:"index"`
ClosedAt *time.Time
// In order to get the following fields, we need to collect PRs individually from GitHub
Additions int
Deletions int
Comments int
Commits int
ReviewComments int
Merged bool
MergedAt *time.Time
Body string
Type string `gorm:"type:varchar(255)"`
Component string `gorm:"type:varchar(255)"`
MergeCommitSha string `gorm:"type:varchar(40)"`
HeadRef string `gorm:"type:varchar(255)"`
BaseRef string `gorm:"type:varchar(255)"`
BaseCommitSha string `gorm:"type:varchar(255)"`
HeadCommitSha string `gorm:"type:varchar(255)"`
Url string `gorm:"type:varchar(255)"`
AuthorName string `gorm:"type:varchar(100)"`
AuthorId int
common.NoPKModel
}

3.MySQL-Specific Functions

We used the GROUP_CONCATfunction in a complex query. Although there are similar functions in PostgreSQL, the function names are different and the usage is slightly different.

cursor2, err := db.Table("pull_requests pr1").
Joins("left join pull_requests pr2 on pr1.parent_pr_id = pr2.id").Group("pr1.parent_pr_id, pr2.created_date").Where("pr1.parent_pr_id != ''").
Joins("left join repos on pr2.base_repo_id = repos.id").
Order("pr2.created_date ASC").
Select(`pr2.key as parent_pr_key, pr1.parent_pr_id as parent_pr_id, GROUP_CONCAT(pr1.base_ref order by pr1.base_ref ASC) as cherrypick_base_branches,
GROUP_CONCAT(pr1.key order by pr1.base_ref ASC) as cherrypick_pr_keys, repos.name as repo_name,
concat(repos.url, '/pull/', pr2.key) as parent_pr_url`).Rows()

Solution: We finally decided to use two steps to achieve the GROUP_CONCAT function. First we used the simplest SQL query to get multiple pieces of the sorted data, and then used the code to group them.

After modification:

    cursor2, err := db.Raw(
`
SELECT pr2.pull_request_key AS parent_pr_key,
pr1.parent_pr_id AS parent_pr_id,
pr1.base_ref AS cherrypick_base_branch,
pr1.pull_request_key AS cherrypick_pr_key,
repos.NAME AS repo_name,
Concat(repos.url, '/pull/', pr2.pull_request_key) AS parent_pr_url,
pr2.created_date
FROM pull_requests pr1
LEFT JOIN pull_requests pr2
ON pr1.parent_pr_id = pr2.id
LEFT JOIN repos
ON pr2.base_repo_id = repos.id
WHERE pr1.parent_pr_id != ''
ORDER BY pr1.parent_pr_id,
pr2.created_date,
pr1.base_ref ASC
`).Rows()

4.Different Grammar

Backticks

We used backticks in some SQL statements to protect field names from conflicting with MySQL reserved words, which can lead to errors in PostgreSQL. To solve this problem we revisited our code, modified all field names that conflict with reserved words, and removed the backticks in the SQL statement. In the example just mentioned:

db.Where("merge_request_id = ? AND `system` = ?", gitlabMr.GitlabId, false)

Solution: We changed system to is_system to avoid the usage of backticks.

db.Where("merge_request_id = ? AND is_system = ?", gitlabMr.GitlabId, false)

Non-standard delete statement

There were delete statements as followed in our code, which are legal in MySQL but will report an error in PostgreSQL.

err := db.Exec(`
DELETE ic
FROM jira_issue_commits ic
LEFT JOIN jira_board_issues bi ON (bi.source_id = ic.source_id AND bi.issue_id = ic.issue_id)
WHERE ic.source_id = ? AND bi.board_id = ?
`, sourceId, boardId).Error

· 19 min read
Nddtfjiang

本文作者:Nddtfjiang 个人主页:https://nddtf.com/github

什么是 计算提交版本差异(CalculateCommitsDiff)?

我们常常需要计算两个提交版本之间的差异。具体的说,就是需要知道两个不同的分支/标签之间相差了哪些提交版本

对于一般用户来说,通过计算提交版本差异,用户能迅速的判断两个不同的分支/标签之间在功能、BUG 修复等等方面的区别。以帮助用户选择不同的分支/标签来使用。

而如果只是使用 diff 命令来查看这两个不同的分支/标签的话,大量庞杂冗余的代码修改信息就会毫无组织的混杂在其中,要从中提取出具体的功能变更之类的信息,等同于大海捞针。

对于一款致力于提升研发效能的产品来说,通过计算提交版本差异,就能查看一组组不同的分支/标签的变迁状况,这一数据的获取,有助于我们做进一步的效能分析。

例如,当一个项目的管理者,想要看看为什么最近的几个版本发版越来越慢了的时候。就可以对最近的几组分支/标签来计算计算提交版本差异。此时有些分支/标签组之间有着大量的提交版本,而有些分支/标签组之间有着较少的提交版本。项目管理者可以更进一步的计算这些提交版本各自的代码当量,把这些数据以图表的形式展示出来,最终得到一组很直观的分支/标签的图像。此时他或许就能发现,原来是因为最近的几次发版涉及到的变更越来越复杂了。通过这样的直观的信息,开发者和管理者们都能做出相应的调整,以便提升研发效能。

已有的解决方案

当我们在 GitLab 上打开一个代码仓库的时候,我们可以通过在 url 末尾追加 compare 的方式来进入到仓库的比对页面。

在该页面,我们可以通过设置源分支/标签目标分支/标签GitLab 向我们展示 目标分支落后于源分支哪些版本,以及落后了多少个版本。

设置完毕后,GitLab 会展示如下:

在这里,我们能看到我们选择的目标分支/标签源分支/标签少了如图所示的提交版本(Commits)

然而遗憾的是,像 GitLab 这类解决方案,都没有做批量化,自动化的处理。也更没有对后续的计算出来的结果进行相应的数据汇总处理。用户面对海量的分支提交的时候,既不可能手动的一个一个去比较,也不可能手动的去把数据结果自己复制粘贴后再分析。

因此 DevLake 就必须要解决这个问题。

所谓的计算提交版本差异具体是在计算什么?

GitLab 的计算过程为例来说的话,所谓的计算提交版本差异也就是当一个提交版本源分支/标签存在,但是在目标分支/标签不存在的时候,这个提交版本就会被 GitLab 给逮出来。

那么,或许有人会问,假如一个提交版本源分支/标签不存在,相反的,在目标分支/标签存在,那是不是也会被抓起来呢?

答案是,不会

也就是说,当我们计算提交版本的差异的时候,我们只关心目标分支/标签缺少了什么,而并不关心目标分支/标签多出来了什么东西。

这就好像以前有一位算法竞赛的学生,在 NOI 比赛结束后被相关学校面试的时候,一个劲的自我介绍自己担任过什么广播站青协学生会,什么会长副会长之类的经历。结果很快就惹得面试官老师们忍无可忍的告诫道:

我们只想知道你信息学方面的自我介绍,其余的我都不感兴趣!!!

在计算提交版本差异时,GitLab 是这样。 GitHub 也是这样。事实上,在使用 git 命令 git log branch1...branch2 的时候,git 也是这样的。

它们都只关心目标分支/标签相对于源分支/标签缺少的部分。

计算提交版本差异实际上就是:

  • 计算待计算的目标分支/标签相对于源分支/标签缺少了哪些提交版本

提交版本进行数学建模

想要做计算,那么首先,我们需要把一个抽象的现实问题,转换成一个数学问题。

这里我们就需要进行数学建模了。

我们需要把像目标分支/标签源分支/标签提交版本 这样一系列的概念变成数学模型中的对象。

如此我们才能为其设计算法。

想当然的,我们就想到了使用图的方式来进行数学建模。

我们将每一个提交版本都看作是图上的一个节点,把提交版本合并之前的一组提交版本与当前提交版本之间的父子关系,看作成是一条有向边

由于目标分支源分支事实上也各自与一个特定的提交版本相绑定,因此也能将它们看作是图上的特别节点。

  • 目标分支/标签所对应的节点,命名为旧节点
  • 源分支/标签所对应的节点,命名为新节点

当然,这里我们还有一个需要特别关注的节点,就是初始的提交版本所代表的节点

  • 将初始提交版本所对应的节点,命名为根节点

上述的描述或许显得有点儿抽象。

我们现在来实际举一个例子。来看看如何对一个仓库进行上述数学建模。

假设现在有基于如下描述而产生的一个仓库:

  1. 创建空仓库
  2. main 分支上创建提交版本 1 作为初始提交
  3. main 分支上创建提交版本 2
  4. main 分支上创建新分支 nd
  5. nd 分支上创建提交版本 3
  6. main 分支上创建提交版本 4
  7. main 分支上创建新分支 dtf
  8. main 分支上创建提交版本 5
  9. dtf 分支上创建提交版本 6
  10. main 分支上创建新分支 nddtf
  11. nddtf 分支上创建提交版本 7
  12. nd 分支合并到 nddtf分支
  13. dtf 分支合并到 nddtf分支
  14. main 分支上创建提交版本 8
  15. nddtf 分支上创建提交版本 9

我们对上述的仓库进行构图之后,最终会得到如下图所示的一个有向图:

  • 此时彩色节点 1根节点
  • main 分支为 1 2 4 5 8
  • nd 分支为 1 2 3 随后合并入 nddtf 分支
  • dtf 分支为 1 2 4 6 随后合并入 nddtf 分支
  • nddtf 分支为 1 2 3 4 5 6 7 9

可以看到,每一个提交版本在图中都相对应的有一个节点

此时我们把提交版本 1 所代表的节点,作为根节点

当然这里可能会有同学提问了:

  • 假如我这个仓库有一万个根节点怎么破?

相信一些经常做图的建模的同学应该都知道破法。

  • 创建一个名叫为一万个根节点的虚拟节点,把它设为这些个虚假的根节点的父节点,来当作真正的根节点即可。

在这个有向图中,我们并没有实际的去指定具体的目标分支/标签或者源分支/标签

在实际使用中,我们可以把任意的两个提交版本作为一对目标分支/标签源分支/标签 当然,有的同学在这里可能又会产生一个问题:

  • 目标分支/标签源分支/标签 虽然都能映射到其最后的提交版本上,但是实际上来说提交版本分支/标签本质上就是两种不同的概念。

分支/标签的实质,是包含一系列的提交版本的集合。而特定的提交版本仅仅是这个集合中的最后一个元素罢了。

当我们把一个仓库通过上述数学建模抽象成一个有向图之后,这个集合的信息,会因此而丢失掉吗?

对于一个合法的仓库来说,答案显然是,不会

实际上,这也就是为什么我们一定要在该有向图中强调根节点的原因。

我们这里这里,先给出结论:

分支/标签所对应的节点,到根节点的全部路径中途径的所有节点的集合,即为该分支/标签所包含的提交版本集合。

简单证明 上述结论

  • 根节点为节点 A
  • 设要求的分支/标签所代表的节点为节点 B

  • 当 节点 C 是属于要求的分支/标签
  • 因为 节点 C 是属于要求的分支/标签
  • 所以 必然存在一组提交或者合并 使得 节点 C 可以一直提交到节点 B
  • 又因为 每一个新增的提交 或者 合并操作,均会切实的建立一条从新增的提交/合并到当前提交的边
  • 所以,反过来说,每一个提交或者合并后的节点,均可以抵达节点 C
  • 所以 节点 B 存在至少一条路径 可以 抵达节点 C
  • 同理可证,节点 C 存在至少一条路径抵达根节点 也就是节点 A
  • 综上,存在一条从节点 B 到节点 A 的路径,经过节点 C

  • 当 节点 C 不属于要求的分支/标签
  • 假设 存在一条从节点 B 到节点 A 的路径,经过节点 C
  • 因为 每一条边都是由新增的提交或者合并操作建立的
  • 所以 必然存在一系列的新增提交或者合并操作,使得节点 C 成为节点 B
  • 又因为 每一个提交在抽象逻辑上都是独一无二的
  • 因此,如果缺失了节点 C 则必然导致在构建节点 B 所代表的分支/标签的过程中,至少存在一个提交或者合并操作无法执行。
  • 这将导致分支非法
  • 因此 假设不成立
  • 因此 其逆否命题 对任意一条从节点 B 到节点 A 的路径,都不会经过节点 C 成立

  • 根据
  • 当 节点 C 是属于要求的分支/标签,存在一条从节点 B 到节点 A 的路径,经过节点 C (必要性)
  • 当 节点 C 不属于要求的分支/标签,对任意一条从节点 B 到节点 A 的路径,都不会经过节点 C (充分性)
  • 可得 分支/标签所对应的节点,到根节点的全部路径中途径的所有节点的集合,即为该分支/标签所包含的提交版本集合。

算法选择

我们现在已经完成了数学建模,并且已经为数学建模做了基本的证明。现在,我们终于可以开始在这个数学模型的基础上来设计并实现我们的算法了。

如果没有做上述基本论证的同学,这里可能会犯一个小错误:那就是它们会误以为,只要计算两个节点之间的最短路径即可。若真是如此的话,SPFA迪杰斯特拉(Dijkstra),甚至头铁一点儿,来个弗洛伊德(Floyd)都是很容易想到的。当然由于该有向图的所有边长都是 1,所以更简单的方法是直接使用广/宽度优先搜索算法(BFS)来计算最短路。

上述的一系列耳熟能详的算法,或多或少都有成熟的库可以直接使用。但是遗憾的是,如果真的是去算最短路的话,那最终结果恐怕会不尽如人意。

DevLake 的早期不成熟的版本中,曾经使用过最短路的算法来计算。尽管对于比较简单线性的仓库来说,可以歪打正着的算出结果。但是当仓库的分支和合并变得复杂的时候,最短路所计算的结果往往都会遗漏大量的提交版本

因为在刚才我们已经论证过了,这个分支/标签所包含的提交版本集合,是必须要全部路径才行的。只有全部路径,才能满足充分且必要条件。

也就是说,中间只要漏了一条路径,那就会漏掉一系列的提交版本

要计算这个有向图上的旧节点所代表的分支/标签新节点所代表的分支/标签缺少了哪些提交版本

实质上就是在计算旧节点根节点的全部路径所经节点,对比新节点根节点的全部路径所经节点,缺少了哪些节点。

如果我们数学建模的时候,把这个有向图建立成一棵树的话。

那么熟悉算法的同学,就可以很自然的使用最近公共祖先(LCA)算法,求出其并集,然后再来计算其对应的缺失的部分。

但是作为一个有向图来说,树结构的算法是没法直接使用的。所幸的是,我们的这个图,在由合法仓库生成的情况下,必然是一个有向无环图。

一个有向无环图,也是有自己的最近公共祖先(LCA)算法的。

只是,这里有两个问题:

  • 我们真的对 最近公共祖先 这个特定的节点感兴趣吗?
  • 在有多个不同路径的公共祖先的情况下,只求一个最近公共祖先有什么意义呢?

首先,我们需要明确我们的需求。

我们只是为了计算 。

  • 旧节点根节点的全部路径所经节点,对比新节点根节点的全部路径所经节点,缺少了哪些节点。

除此之外的,我们不感兴趣。

换句话说,我们想知道其公共祖先,但是,不关心它是不是最近的。

它是近的也好,它是远的也罢,只要是公共祖先,都得抓起来。去求最近公共祖先,在树结构下,可以近似等价于求其全部祖先。因此可以使用该算法。

但是在有向无环图下,最近公共祖先就是个废物。求出来了又能如何?

根本上,还是应该去求全部的公共祖先。

所以我们别无选择,只能用最直接的算法。

  • 计算出旧节点根节点的全部路径所经节点
  • 计算出新节点根节点的全部路径所经节点
  • 检查新节点的全部路径所经节点缺少了哪些节点

如何计算任意节点到根节点的全部路径所经节点?

在 OI 上熟练于骗分导论的同学们,应该很自然的就意识到了

深度优先搜索(DFS)

当然,这里补充一下,由于根节点的性质,事实上,无论是从哪个方向出发,无论走那条边,只要是能走的边,最终都会抵达根节点

因此,在上述条件成立的基础上,没有记录路径状态的广/宽度优先搜索(BFS)也是可以使用的。因为在必然能抵达根节点的前提下,可以忽略路径状态,不做路径的可行性判断。

当然,这一前提,也有利于我们深度优先搜索(DFS)进行优化。

在我们执行深度优先搜索(DFS)的时候,我们可以将所有访问到的节点,均添加到集合中,而无需等待确认该节点能确实抵达根节点后再进行添加。

实际上这里在一个问题上我们又会出现了两种分歧。 问题是,如何将一个节点添加到集合中。方案有如下两种。

染色法:添加到集合中的节点进行染色,未添加到集合中的节点不进行染色。 集合法:使用平衡树算法建立一个集合,将节点添加到该集合中。

这两种算法各有优劣。

  • 染色法的优势在于,染色法添加一个元素的时间复杂度是 O(1) 的,快准狠。相比较而言,集合法添加一个元素的时间复杂度是 O(log(n))。
  • 集合法的优势在于,集合法遍历所有元素的时间复杂度是 O(n) 的,而染色法下,要遍历所有元素时间复杂度会是 O(m),同时集合法可以通过设计一个优秀的 hash 算法代替平衡树,来将时间复杂度优化到接近 O(1).(这里 n 表示集合大小,m 表示整个图的大小)

我们这里选择使用集合法。实际上这两种算法都差不多。

算法实现

  • 根据提交建图
  • 我们对旧节点使用深度优先搜索(DFS)计算出其到根节点的全部路径所经节点,添加到集合 A
  • 接着,我们对新节点使用深度优先搜索(DFS)计算出其到根节点的全部路径所经节点,添加到集合 B
  • 注意,这里有一个优化,这个优化是建立在我们的需求上
  • 重复一遍我们的需求
  • 我们只关心目标分支/标签缺少了什么,而并不关心目标分支/标签多出来了什么东西。
  • 因此当对新节点使用深度优先搜索(DFS)搜索到已经在集合 A 中的节点时,可以认为该节点已搜索过,无需再次搜索。
  • 此时的集合 B,可以恰好的避开集合 A 中已有的所有节点,因此,恰好就是我们所需的结果。

核心的计算代码如下所示:

oldGroup := make(map[string]*CommitNode)
var dfs func(*CommitNode)
// put all commit sha which can be depth-first-searched by old commit
dfs = func(now *CommitNode) {
if _, ok = oldGroup[now.Sha]; ok {
return
}
oldGroup[now.Sha] = now
for _, node := range now.Parent {
dfs(node)
}
}
dfs(oldCommitNode)

var newGroup = make(map[string]*CommitNode)
// put all commit sha which can be depth-first-searched by new commit, will stop when find any in either group
dfs = func(now *CommitNode) {
if _, ok = oldGroup[now.Sha]; ok {
return
}
if _, ok = newGroup[now.Sha]; ok {
return
}
newGroup[now.Sha] = now
lostSha = append(lostSha, now.Sha)
for _, node := range now.Parent {
dfs(node)
}
}
dfs(newCommitNode)

这里的 lostSha 即为我们最终求得的缺失的部分

算法执行的演示动画

我们用一个简陋的动画来简单的演示一下,上述算法在逻辑上执行的情况。

  • 旧节点为节点 8
  • 新节点为节点 9

如上述动画所演示的一般 从节点 8 开始执行深度优先搜索(DFS)根节点中止 从节点 9 开始执行深度优先搜索(DFS)到已经在节点 8 的集合中的节点为止 此时,在节点 9 执行深度优先搜索(DFS)过程中被访问到的所有非节点 8 的节点

  • 节点 3
  • 节点 6
  • 节点 7
  • 节点 9

它们所对应的提交版本就是我们要求的差集

此时最短路为 9 -> 7 -> 5 -> 8 此时最近公共父节点为 5,到该节点的路径为 9 -> 7 -> 5 从上图中也可以直观的看到如果使用最短路算法,或者最近公共父节点算法的情况下,我们是无法得到正确答案的。

时空复杂度

提交版本的总大小为 m,每一组源分支/标签目标分支/标签的平均大小为 n,一共有 k 组数据

DFS 每访问一个节点,需要执行一次加入集合操作。我们按照我们实际实现中使用的 平衡树算法来计算 时间复杂度为 O(log(n))

此时我们可以计算得出

  • 建图的时间复杂度:O(m)
  • 计算一组源分支/标签目标分支/标签时间复杂度:O(n*log(n))
  • 计算所有源分支/标签目标分支/标签时间复杂度:O(k*n*log(n))
  • 读取、统计结果时间复杂度:O(k*n)
  • 总体时间复杂度:O(m + k*n*log(n))

  • 图的空间复杂度:O(m)
  • 每组源分支/标签目标分支/标签集合的空间复杂度:O(n) (非并发情况下,k组数据可共用)
  • 总体空间复杂度:O(m+n)

关键词

  • DevLake
  • CalculateCommitsDiff
  • 算法
  • 数学建模
  • 证明逻辑
  • 充分条件
  • 必要条件
  • 图论
  • 深度优先搜索(DFS)
  • 广/宽度优先搜索(BFS)
  • 时间复杂度
  • 空间复杂度
  • 时空复杂度

了解更多最新动态

官网:https://devlake.incubator.apache.org/

GitHub:https://github.com/apache/incubator-devlake/

Slack:通过 Slack 联系我们

· 4 min read
ZhangLiang

本文作者:ZhangLiang
个人主页:https://github.com/mindlesscloud

Apache DevLake 是一个研发数据平台,可以收集和整合各类研发工具的数据,比如 Jira、Github、Gitlab、Jenkins。

本文并不打算对数据库兼容这个问题做全面的总结,只是对我们实际遇到的问题做一个记录,希望能对有相似需求的人提供一个参考。

1、数据类型差异

PostgreSQL 不支持 uint 类型的数据类型

type JenkinsBuild struct {
common.NoPKModel
JobName string `gorm:"primaryKey;type:varchar(255)"`
Duration float64 // build time
DisplayName string // "#7"
EstimatedDuration float64
Number int64 `gorm:"primaryKey;type:INT(10) UNSIGNED NOT NULL"`
Result string
Timestamp int64 // start time
StartTime time.Time // convered by timestamp
CommitSha string
}

其中的JenkinsBuild.Number字段的gorm struct tag 使用了UNSIGNED会导致建表失败,需要去掉。

MySQL 没有 bool 型

对于 model 里定义为 bool 型的字段,gorm 会把它映射成 MySQL 的 TINYINT 类型,在 SQL 里可以直接用 0 或者 1 查询,但是 PostgreSQL 里是有 bool 类型的,所以 gorm 会把它映射成 BOOL 类型,如果 SQL 里还是用的 0 或者 1 去查询就会报错。

以下是一个具体的例子(为了清晰起见我们删掉了无关的字段),下面的查询语句在 MySQL 里是没有问题的,但是在 PostgreSQL 上就会报错。

type GitlabMergeRequestNote struct {
MergeRequestId int `gorm:"index"`
System bool
}

db.Where("merge_request_id = ? AND `system` = 0", gitlabMr.GitlabId).

语句改成这样后仍然会有错误,具体请见下面关于反引号的问题。

db.Where("merge_request_id = ? AND `system` = ?", gitlabMr.GitlabId, false)  

2、行为差异

批量插入

如果使用了ON CONFLIT UPDATE ALL从句批量插入的时候,本批次如果有多条主键相同的记录会导致 PostgreSQL 报错,MySQL 则不会。

字段类型 model 定义与 schema 不一致

例如在 model 定义中GithubPullRequest.AuthorId是 int 类型,但是数据库里这个字段是 VARCHAR 类型,插入数据的时候 MySQL 是允许的,PostgreSQL 则会报错。

type GithubPullRequest struct {
GithubId int `gorm:"primaryKey"`
RepoId int `gorm:"index"`
Number int `gorm:"index"`
State string `gorm:"type:varchar(255)"`
Title string `gorm:"type:varchar(255)"`
GithubCreatedAt time.Time
GithubUpdatedAt time.Time `gorm:"index"`
ClosedAt *time.Time
// In order to get the following fields, we need to collect PRs individually from GitHub
Additions int
Deletions int
Comments int
Commits int
ReviewComments int
Merged bool
MergedAt *time.Time
Body string
Type string `gorm:"type:varchar(255)"`
Component string `gorm:"type:varchar(255)"`
MergeCommitSha string `gorm:"type:varchar(40)"`
HeadRef string `gorm:"type:varchar(255)"`
BaseRef string `gorm:"type:varchar(255)"`
BaseCommitSha string `gorm:"type:varchar(255)"`
HeadCommitSha string `gorm:"type:varchar(255)"`
Url string `gorm:"type:varchar(255)"`
AuthorName string `gorm:"type:varchar(100)"`
AuthorId int
common.NoPKModel
}

3、MySQL 特有的函数

在一个复杂查询中我们曾经使用了 GROUP_CONCAT 函数,虽然 PostgreSQL 中有功能类似的函数但是函数名不同,使用方式也有细微差别。

cursor2, err := db.Table("pull_requests pr1").
Joins("left join pull_requests pr2 on pr1.parent_pr_id = pr2.id").Group("pr1.parent_pr_id, pr2.created_date").Where("pr1.parent_pr_id != ''").
Joins("left join repos on pr2.base_repo_id = repos.id").
Order("pr2.created_date ASC").
Select(`pr2.key as parent_pr_key, pr1.parent_pr_id as parent_pr_id, GROUP_CONCAT(pr1.base_ref order by pr1.base_ref ASC) as cherrypick_base_branches,
GROUP_CONCAT(pr1.key order by pr1.base_ref ASC) as cherrypick_pr_keys, repos.name as repo_name,
concat(repos.url, '/pull/', pr2.key) as parent_pr_url`).Rows()

解决方案: 我们最终决定把GROUP_CONCAT函数的功能拆分成两步,先用最简单的 SQL 查询得到排序好的多条数据,然后用代码做聚合。

修改后:

cursor2, err := db.Raw(
`
SELECT pr2.pull_request_key AS parent_pr_key,
pr1.parent_pr_id AS parent_pr_id,
pr1.base_ref AS cherrypick_base_branch,
pr1.pull_request_key AS cherrypick_pr_key,
repos.NAME AS repo_name,
Concat(repos.url, '/pull/', pr2.pull_request_key) AS parent_pr_url,
pr2.created_date
FROM pull_requests pr1
LEFT JOIN pull_requests pr2
ON pr1.parent_pr_id = pr2.id
LEFT JOIN repos
ON pr2.base_repo_id = repos.id
WHERE pr1.parent_pr_id != ''
ORDER BY pr1.parent_pr_id,
pr2.created_date,
pr1.base_ref ASC
`).Rows()

4、语法差异

反引号

某些 SQL 语句中我们使用了反引号,用来保护字段名,以免跟 MySQL 保留字有冲突,这种做法在 PostgreSQL 会导致语法错误。为了解决这个问题我们重新审视了我们的代码,把所有跟保留字冲突的字段名做了修改,同时去掉了 SQL 语句中的反引号。例如刚才提到的这个例子:

db.Where("merge_request_id = ? AND `system` = ?", gitlabMr.GitlabId, false)

解决方案:我们把system改个名字is_system,这样就可以把反引号去掉。

db.Where("merge_request_id = ? AND is_system = ?", gitlabMr.GitlabId, false)

不规范的删除语句

我们的代码中曾经出现过这种删除语句,这在 MySQL 中是合法的,但是在 PostgreSQL 中会报语法错误。

err := db.Exec(`
DELETE ic
FROM jira_issue_commits ic
LEFT JOIN jira_board_issues bi ON (bi.source_id = ic.source_id AND bi.issue_id = ic.issue_id)
WHERE ic.source_id = ? AND bi.board_id = ?
`, sourceId, boardId).Error

解决方案:我们把DELETE后面的表别名去掉就可以了。

了解更多最新动态

官网:https://devlake.incubator.apache.org/

GitHub:https://github.com/apache/incubator-devlake/

Slack:通过 Slack 联系我们

· 4 min read
Danna Wang

Apache DevLake is an integration tool with the DevOps data collection functionality, which presents a different stage of data to development teams via Grafana. which also can leverage teams to improve the development process with a data-driven model.

Apache DevLack Architecture Overview

  • The left side of the following screenshot is an integrative DevOps data plugin, the existing plugins include Github, GitLab, JIRA, Jenkins, Tapd, Feishu, and the most featured analysis engine in the Simayi platform.
  • The main framework in the middle of the following screenshot, completes data collection, expansion, and conversion to the domain layer by running subtasks in the plugins. The user can trigger the tasks by config-UI or all API.
  • RMDBS currently supports Mysql and PostgresSQL, more databases will be supported in the future.
  • Grafana can generate different types of needed data by using SQL.

Generated

Then let’s move on to how to start running DevLake.

· 9 min read
abeizn

Apache DevLake是什么?

研发数据散落在软件研发生命周期的不同阶段、不同工作流、不同DevOps工具中,且标准化程度低,导致效能数据难以留存、汇集并转化为有效洞见。为了解决这一痛点,Apache DevLake 应运而生。Apache DevLake是一款开源的研发数据平台,它通过提供自动化、一站式的数据收集、分析以及可视化能力,帮助研发团队更好地理解开发过程,挖掘关键瓶颈与提效机会。

Apache DevLake架构概述

img

Apache DevLake 架构图

  • Config UI: 人如其名,配置的可视化,其主要承载Apache DevLake的配置工作。通过Config UI,用户可以建立数据源连接,并实现数据的收集范围,部分数据的转换规则,以及收集频率等任务。
  • Api Sever:Apache DevLake的Api接口,是前端调用后端数据的通道。
  • Runner:Apache DevLake运行的底层支撑机制。
  • Plugins:具体执行的插件业务,主要承载Apache DevLake的后端数据收集、扩展和转换的工作。除dbt插件外的插件产出Apache DevLake的预置数据,预置数据主要包括三层;
    • raw layer:负责储存最原始的api response json。
    • tool layer:根据raw layer提取出此插件所需的数据。
    • domain layer:根据tool layer层抽象出共性的数据,这些数据会被使用在Grafana图表中,用于多种研发指标的展示。
  • RDBS: 关系型数据库。目前Apache DavLake支持MySQL和PostgreSQL,后期还会继续支持更多的数据库。
  • Grafana Dashboards: 其主要承载Apache DevLake的前端展示工作。根据Apache DevLake收集的数据,通过sql语句来生成团队需要的交付效率、质量、成本、能力等各种研发效能指标。

目录结构Tree

├── api
│   ├── blueprints
│   ├── docs
│   ├── domainlayer
│   ├── ping
│   ├── pipelines
│   ├── push
│   ├── shared
│   ├── task
│   └── version
├── config
├── config-ui
├── devops
│   └── lake-builder
├── e2e
├── errors
├── grafana
│   ├── _archive
│   ├── dashboards
│   ├── img
│   └── provisioning
│   ├── dashboards
│   └── datasources
├── img
├── logger
├── logs
├── migration
├── models
│   ├── common
│   ├── domainlayer
│   │   ├── code
│   │   ├── crossdomain
│   │   ├── devops
│   │   ├── didgen
│   │   ├── ticket
│   │   └── user
│   └── migrationscripts
│   └── archived
├── plugins
│   ├── ae
│   │   ├── api
│   │   ├── models
│   │   │   └── migrationscripts
│   │   │   └── archived
│   │   └── tasks
│   ├── core
│   ├── dbt
│   │   └── tasks
│   ├── feishu
│   │   ├── apimodels
│   │   ├── models
│   │   │   └── migrationscripts
│   │   │   └── archived
│   │   └── tasks
│   ├── gitextractor
│   │   ├── models
│   │   ├── parser
│   │   ├── store
│   │   └── tasks
│   ├── github
│   │   ├── api
│   │   ├── models
│   │   │   └── migrationscripts
│   │   │   └── archived
│   │   ├── tasks
│   │   └── utils
│   ├── gitlab
│   │   ├── api
│   │   ├── e2e
│   │   │   └── tables
│   │   ├── impl
│   │   ├── models
│   │   │   └── migrationscripts
│   │   │   └── archived
│   │   └── tasks
│   ├── helper
│   ├── jenkins
│   │   ├── api
│   │   ├── models
│   │   │   └── migrationscripts
│   │   │   └── archived
│   │   └── tasks
│   ├── jira
│   │   ├── api
│   │   ├── models
│   │   │   └── migrationscripts
│   │   │   └── archived
│   │   └── tasks
│   │   └── apiv2models
│   ├── refdiff
│   │   ├── tasks
│   │   └── utils
│   └── tapd
│   ├── api
│   ├── models
│   │   └── migrationscripts
│   │   └── archived
│   └── tasks
├── releases
│   ├── lake-v0.10.0
│   ├── lake-v0.10.0-beta1
│   ├── lake-v0.10.1
│   ├── lake-v0.7.0
│   ├── lake-v0.8.0
│   └── lake-v0.9.0
├── runner
├── scripts
├── services
├── test
│   ├── api
│   │   └── task
│   └── example
├── testhelper
├── utils
├── version
├── worker
├── Dockerfile
├── docker-compose.yml
├── docker-compose-temporal.yml
├── k8s-deploy.yaml
├── Makefile
└── .env.exemple


目录导览

  • 后端部分:
    • config:对.env配置文件的读、写以及修改的操作。
    • logger:log日志的level、format等设置。
    • errors:Error的定义。
    • utils:工具包,它包含一些基础通用的函数。
    • runner:提供基础执行服务,包括数据库,cmd,pipelines,tasks以及加载编译后的插件等基础服务。
    • models:定义框架级别的实体。
      • common:基础struct定义。
      • domainlayer:领域层是来自不同工具数据的通用抽象。
        • ticket:Issue Tracking,即问题跟踪领域。
        • code:包括Source Code源代码关联领域。以及Code Review代码审查领域。
        • devops:CI/CD,即持续集成、持续交付和持续部署领域。
        • crossdomain:跨域实体,这些实体用于关联不同领域之间的实体,这是建立全方面分析的基础。
        • user:对用户的抽象领域,user也属于crossdomain范畴。
      • migrationscripts:初始化并更新数据库。
    • plugins:
      • core:插件通用接口的定义以及管理。
      • helper:插件通用工具的集合,提供插件所需要的辅助类,如api收集,数据ETL,时间处理等。
        • 网络请求Api Client工具。
        • 收集数据Collector辅助类,我们基于api相同的处理模式,统一了并发,限速以及重试等功能,最终实现了一套通用的框架,极大地减少了开发和维护成本。
        • 提取数据Extractor辅助类,同时也内建了批量处理机制。
        • 转换数据Convertor辅助类。
        • 数据库处理工具。
        • 时间处理工具。
        • 函数工具。
      • ae:分析引擎,用于导入merico ae分析引擎的数据。
      • feishu:收集飞书数据,目前主要是获取一段时间内组织内会议使用的top用户列表的数据。
      • github:收集Github数据并计算相关指标。(其他的大部分插件的目录结构和实现功能和github大同小异,这里以github为例来介绍)。
        • github.go:github启动入口。
        • tasks:具体执行的4类任务。
          • *_collector.go:收集数据到raw layer层。
          • *_extractor.go:提取所需的数据到tool layer层。
          • *_convertor.go:转换所需的数据到domain layer层。
          • *_enricher.go:domain layer层更进一步的数据计算转换。
        • models:定义github对应实体entity。
        • api:api接口。
        • utils:github提取的一些基本通用函数。
      • gitextractor:git数据提取工具,该插件可以从远端或本地git仓库提取commit和reference信息,并保存到数据库或csv文件。用来代替github插件收集commit信息以减少api请求的数量,提高收集速度。
      • refdiff:在分析开发工作产生代码量时,经常需要知道两个版本之间的diff。本插件基于数据库中存储的commits父子关系信息,提供了计算ref(branch/tag)之间相差commits列表的能力。
      • gitlab:收集Gitlab数据并计算相关指标。
      • jenkins:收集jenkins的build和job相关指标。
      • jira:收集jira数据并计算相关指标。
      • tapd:收集tapd数据并计算相关指标。
      • dbt:(data build tool)是一款流行的开源数据转换工具,能够通过SQL实现数据转化,将命令转化为表或者视图,提升数据分析师的工作效率。Apache DevLake增加了dbt插件,用于数据定制的需要。
    • services:创建、管理Apache DevLake各种服务,包含notifications、blueprints、pipelines、tasks、plugins等。
    • api:使用Gin框架搭建的一个通用Apache DevLake API服务。
  • 前端部分:
    • congfig-ui:主要是Apache DevLake的插件配置信息的可视化。一些术语的解释
      • 常规模式
        • blueprints的配置。
        • data connections的配置。
        • transformation rules的配置。
      • 高级模式:主要是通过json的方式来请求api,可选择对应的插件,对应的subtasks,以及插件所需要的其他信息。
    • Grafana:其主要承载Apache DevLake的前端展示工作。根据收集的数据,通过sql语句来生成团队需要的各种数据。目前sql主要用domain layer层的表来实现通用数据展示需求。
  • migration:数据库迁移工具。
    • migration:数据库迁移工具migration的具体实现。
    • models/migrationscripts:domian layer层的数据库迁移脚本。
    • plugins/xxx/models/migrationscripts:插件的数据库迁移脚本。主要是rawtool开头的数据库的迁移。
  • 测试部分:
    • testhelper和plugins下的*_test.go文件:即单元测试,属于白盒测试范畴。针对目标对象自身的逻辑,执行路径的正确性进行测试,如果目标对象有依赖其它资源或对够用,采用注入或者 mock 等方式进行模拟,可以比较方便地制造一些难以复现的极端情况。
    • test:集成测试,灰盒测试范畴。在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。
    • e2e: 端到端测试,属于黑盒测试范畴。相对于单元测试更注重于目标自身,e2e更重视目标与系统其它部分互动的整体正确性,相对于单元测试着重逻辑测试,e2e侧重于输出结果的正确性。
  • 编译,发布部分:
    • devops/lake-builder: mericodev/lake-builder的docker构建。
    • Dockerfile:主要用于构建devlake镜像。
    • docker-compose.yml:是用于定义和运行多容器Docker应用程序的工具,用户可以使用YML文件来配置Apache DevLake所需要的服务组件。
    • docker-compose-temporal.yml:Temporal是一个微服务编排平台,以分布式的模式来部署Apache DevLake,目前处于试验阶段,仅供参考。
    • worker:Temporal分布式部署形式中的worker实现,目前处于试验阶段,仅供参考。
    • k8s-deploy.yaml:Kubernetes是一个可移植、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。目前Apache DevLake已支持在k8s集群上部署。
    • Makefile:是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。
    • releases:Apache DevLake历史release版本的配置文件,包括docker-compose.yml和env.example。
    • scripts:shell脚本,包括编译plugins脚本。
  • 其他:
    • img:logo、社区微信二维码等图像信息。
    • version:实现版本显示的支持,在正式的镜像中会显示对应release的版本。
    • .env.exemple:配置文件实例,包括DB URL, LOG以及各插件的配置示例信息。

如何联系我们

· 2 min read
Klesh Wong

上周(2022-05-12),我们以先到先得的方式为大家列出了两个"good first issue"。 这很有趣,它们几乎立刻就被拿走了...... 但对于那些有兴趣但没有得到的人来说可能就不那么有趣了。

所以...

我们决定,不再有竞争,你可以从我们的github issue pages中挑选你喜欢的issue。如果没有了,甚至可以创建你自己的。 我们毕竟是社区!

怎么做呢?这很简单!

进入我们的问题页面,然后点击这里。我们所有的Good First Issue都列在这里! good first issue

  • 首先,寻找现有的issues,找到一个你喜欢的。 你可以通过评论"I'll take it!"来预订它。 接下来你可以写一份“攻略”,以展示你对问题的理解和你将采取什么样的步骤来解决这个issue,然后开始Coding。

  • 如果没有GFI了怎么办?创造你自己的issue! 现在,通过查看我们的代码库。 你肯定能发现很多问题,比如文档、单元测试,甚至是错字。 把你觉得不对的地方提出来,我们会验证它是否必要, 然后你就可以开始Coding了。

  • 最后,你可能会问,我为什么要费尽心思为你写代码? 不不不,你不是为我们写代码,你是为社区里的每个人写代码,你是为自己写代码。 为了提高你的技能,为了学习如何与他人合作。而对于那些做出重大贡献的人, 我们为您提供一个Apache Committer的席位,甚至是PPMC!

就这些了,有任何问题请随时提出。编码快乐!

· 2 min read
Maxim Wheatley

We are excited to share today that the Apache Software Foundation (ASF) voted to make DevLake an officially supported project of the Apache Incubator.

What is DevLake?

Launched in December of 2021, Apache DevLake is an open-source dev data platform that ingests, analyzes, and visualizes the fragmented data in developer tools.

Software development is complex, requiring many tools and processes, and as a result creates a storm of data scattered across tools in many formats. This makes it difficult to organize, query, and make sense of. We built Apache DevLake, to make it easy to make sense of this rich data and to translate it into actionable insights.

· 5 min read
Warren Chen

Apache DevLake 是一个DevOps数据收集和整合工具,通过 Grafana 为开发团队呈现出不同阶段的数据,让团队能够以数据为驱动改进开发流程。

Apache DevLake 架构概述

  • 左边是可集成的DevOps数据插件,目前已有的插件包括 Github,Gitlab,JIRA,Jenkins,Tapd,Feishu 以及思码逸主打的代码分析引擎
  • 中间是主体框架,通过主体框架运行插件中的子任务,完成数据的收集,扩展,并转换到领域层,用户可以通过 config-ui 或者 api 调用的形式来触发任务
  • RMDBS 目前支持 Mysql 和 PostgreSQL,后期还会继续支持更多的数据库
  • Grafana 可以通过sql语句生成团队需要的各种数据

Generated

接下来我们就详细聊一聊系统是怎么跑起来的。

· 6 min read
Warren Chen

1. 背景

我们的项目有大量的api请求由goroutine完成,所以我们需要引入一个pool来节省频繁创建goroutine所造成的的开销,同时也可以更简易的调度goroutine,在对github上多个协程池的对比后,我们最终选定了ants作为我们的调度管理pool。

  1. 最近在测试中偶然发现系统出现了“死锁”的情况,进而采取断网的方式发现“死锁”在极端情况下是稳定出现,经过满篇的log,break,最终把问题定位到了ants的submit方法。这个问题来自于在使用ants pool的过程中,为了实现重试,我们在方法中又递归调用了方法本身,也就是submit task内部又submit一个task,下面是简化后的代码: