PowerShell 使用SqlScriptDOM对T-SQL做规则校验

news2025/1/12 6:13:21

        对于数据项目来说,编写Sql是一项基本任务同时也是数量最多的代码。为了统一项目代码规范同时降低Code Review的成本,因此需要通过自动化的方式来进行规则校验。由于本人所在的项目以SQL Server数据库为基础,于是本人决定通过使用SqlScriptDom类库来做T-SQL的规则校验。如果是其他数据库项目,则可采用ANTLR库做规则校验,其实现的方式大体一致。

        SqlScriptDom是针对SQL Server的.Net的类库,由微软公司开发并开源,源码地址 。有兴趣的朋友可以去研究一下。其次项目采用Powershell来开发,有以下原因

  1. 使用脚本开发比较灵活,不用编译,开发即可部署。
  2. Powershell可以直接使用.Net类库,并且具有高级语言的一些特点方便开发。

项目使用VS Code作为开发调试工具,需要安装Powershell相关的插件。由于要使用到PowerShell的自定义类来开发,所以需要提前将类库加载到Powershell中,因此需要配置Powershell的环境。如何配置环境可以参考这篇文章,介绍如何创建和使用 PowerShell 配置文件。并通过Add-Type这个命令来加载它。

以下是具体代码

  

using namespace Microsoft.SqlServer.TransactSql.ScriptDom
using namespace System
using namespace System.Collections.Generic
using namespace System.IO
using namespace Management.Automation
using namespace System.Reflection

enum Severity {
    Information = 1
    Warning = 2
    Exception = 3
    Fault = 4
}

enum ResponseCode {
    Success = 0
    Exception = 10001
    ParseError = 10002
}

     

using namespace Microsoft.SqlServer.TransactSql.ScriptDom

         这句是使用了命名空间,后面在使用相关对象时候无需采用完全限定名,从而简化代码。随后定义了两个枚举,Severity定义规则的严重程度,ResponseCode定义在程序处理过程中的各种状态。


        下面定义CustomerParser类,该类的功能是接收输入的Sql代码,通过语法和词法分析后生成相关语法树,再对语法树进行分析,从而判断代码中哪些片段是违反了项目的编码规则,从而达到Code Review的作用。

class CustomParser {

    hidden [TSqlParser] $TSqlParser
    hidden [TSqlFragment]$Tree
    hidden $AnalysisCodeSummary = [PSCustomObject]([ordered]@{
            ResponseCode      = [ResponseCode]::Success;
            ResponseMessage   = "Success";
            FileName          = $null;
            DocumentName      = $null;
            Code              = $null;
            IsDocument        = $true;
            ParseErrors       = [List[ParseError]]::new();
            ValidationResults = [List[psobject]]::new();
        })

    hidden [bool] $IsDocument
    hidden [string] $FileName
    hidden [string] $Code

    hidden CustomParser([SqlVersion]$version, [SqlEngineType]$engineType) {
        switch ($version) {
            [SqlVersion]::Sql120 { $this.TSqlParser = [TSql120Parser]::new($true) }
            [SqlVersion]::Sql130 { $this.TSqlParser = [TSql130Parser]::new($true, $engineType) }
            [SqlVersion]::Sql140 { $this.TSqlParser = [TSql140Parser]::new($true, $engineType) }
            [SqlVersion]::Sql150 { $this.TSqlParser = [TSql150Parser]::new($true, $engineType) }
            Default { $this.TSqlParser = [TSql160Parser]::new($true, $engineType) }
        }
    }

    hidden [void] Parse() {
        $this.AnalysisCodeSummary.FileName = $this.FileName
        $this.AnalysisCodeSummary.IsDocument = $this.IsDocument 
        $this.AnalysisCodeSummary.DocumentName = [Path]::GetFileName($this.FileName)

        [StringReader]$reader = $null
        [ParseError[]]$errors = @()      

        try {
            if ($this.IsDocument) { $this.Code = [File]::ReadAllText($this.FileName) }
            $this.AnalysisCodeSummary.Code = $this.Code
            $reader = [StringReader]::new($this.Code) 
            $this.Tree = $this.TSqlParser.Parse($reader, [ref] $errors)
        }
        catch {
            $this.AnalysisCodeSummary.ResponseCode = [ResponseCode]::Exception
            $this.AnalysisCodeSummary.ResponseMessage = $_.Exception.Message            
            return
        }
        finally {
            if ($null -ne $reader) { $reader.Close() }
        }

        if ($errors.Count -ne 0) {
            $this.AnalysisCodeSummary.ResponseCode = [ResponseCode]::ParseError
            $this.AnalysisCodeSummary.ResponseMessage = "An error occurred while parsing the code."
            $this.AnalysisCodeSummary.ParseErrors = $errors
        }
    }

    hidden [void]Validate([BaseRule] $rule, [bool]$lockRule) {
        [psobject]$validationResult = [PSCustomObject]([ordered]@{
                ResponseCode        = [ResponseCode]::Success;
                ResponseMessage     = "Success";
                RuleName            = $rule.RuleName;
                Descrtiption        = $rule.Descrtiption;
                Severity            = $rule.Severity;
                Validated           = $true;
                AnalysisCodeResults = @();
            })
        $lockTaken = $false
        try {
            if ($lockRule) { [Threading.Monitor]::Enter($rule.AnalysisCodeResults, [ref] $lockTaken) }
            $rule.AnalysisCodeResults = @()
            $this.Tree.Accept($rule)
            $validationResult.AnalysisCodeResults += $rule.AnalysisCodeResults
        }
        catch {
            $validationResult.ResponseCode = [ResponseCode]::Exception
            $validationResult.ResponseMessage = $_.Exception.Message
            return
        }
        finally {
            if ($lockTaken) { [Threading.Monitor]::Exit($rule.AnalysisCodeResults) }
            $validationResult.Validated = $validationResult.ResponseCode -eq [ResponseCode]::Success `
                -and (( $validationResult.AnalysisCodeResults | Where-Object { -not $_.Validated } ).Count -eq 0)
                
            if (-not $validationResult.Validated) {
                $this.AnalysisCodeSummary.ValidationResults += $validationResult
            }        
        }
    }

    static [psobject] Analysis([string]$codeOrFile, [bool]$isDocumnet, [BaseRule[]]$rules) {
        [CustomParser]$parser = [CustomParser]::new([SqlVersion]::Sql130, [SqlEngineType]::All)
        if (-not $isDocumnet) { $parser.Code = $codeOrFile }else { $parser.FileName = $codeOrFile }
        $parser.IsDocument = $isDocumnet
        $parser.Parse()
        if ($parser.AnalysisCodeSummary.ResponseCode -eq [ResponseCode]::Success) {
            foreach ($rule in $rules) {
                $parser.Validate($rule, $false)
            }
        }
        return $parser.AnalysisCodeSummary
    }

    static [psobject[]] Analysis([string[]]$files, [BaseRule[]]$rules) {
        $result = @()
        foreach ($file in $files) { $result += [CustomParser]::Analysis($file, $true, $rules) }
        return $result
    }
}
hidden [TSqlParser] $TSqlParser

该变量是T-SQL的分析器,通过该变量的Parse方法将SQL解析成语法树,hidden表示该变量仅在类内部使用。

 hidden [TSqlFragment]$Tree

该变量则存储分析后的语法树。

hidden $AnalysisCodeSummary = [PSCustomObject]([ordered]@{
            ResponseCode      = [ResponseCode]::Success;
            ResponseMessage   = "Success";
            FileName          = $null;
            DocumentName      = $null;
            Code              = $null;
            IsDocument        = $true;
            ParseErrors       = [List[ParseError]]::new();
            ValidationResults = [List[psobject]]::new();
        })

该变量是存储语法分析和规则分析的结果。

ParseErrors列表存储的是当语法分析出错时的错误结果。ValidationResults列表则存储的是每条规则校验后的结果。

hidden CustomParser([SqlVersion]$version, [SqlEngineType]$engineType) {
        switch ($version) {
            [SqlVersion]::Sql120 { $this.TSqlParser = [TSql120Parser]::new($true) }
            [SqlVersion]::Sql130 { $this.TSqlParser = [TSql130Parser]::new($true, $engineType) }
            [SqlVersion]::Sql140 { $this.TSqlParser = [TSql140Parser]::new($true, $engineType) }
            [SqlVersion]::Sql150 { $this.TSqlParser = [TSql150Parser]::new($true, $engineType) }
            Default { $this.TSqlParser = [TSql160Parser]::new($true, $engineType) }
        }
    }

CustomParser类的构造函数,$version定的时使用那个版本的分析器,比如Sql130就对应Sql Server2016,$engineType参数定义了使用哪种引擎,是Sql Server还是Azure亦或两者都采用。

CustomParser类中的Parse方法是做语法分析的。Validate方法则是做规则校验,该方法的$rule参数是传入的各种验证规则,均继承自BaseRule类。$lockRule是当采用多线程执行时是否加锁来保证结果完整。

下面则是BaseRule的代码。

class BaseRule:TSqlFragmentVisitor {

    [string]$Descrtiption
    [Severity]$Severity = [Severity]::Information
    $AnalysisCodeResults = @()
    [string]$RuleName = $this.GetType().Name
    hidden [string] $Additional

    hidden [void] Validate([TSqlFragment] $node, [bool] $validated , [string] $addtional) {
        $this.AnalysisCodeResults += [BaseRule]::GetAnalysisResult($node, $validated, $addtional)
    }

    static  [BaseRule[]] GetAllRules() {
        return [Assembly]::GetAssembly([BaseRule]).GetTypes() `
        | Where-Object { $_ -ne [BaseRule] -and $_.BaseType -eq [BaseRule] } `
        | ForEach-Object { New-Object $_ }
    }

    static [psobject] GetAnalysisResult([TSqlFragment] $node, [bool] $validated , [string] $addtional) {
        return [PSCustomObject]([ordered]@{
                StartLine   = $node.StartLine;
                EndLine     = if ($node.LastTokenIndex -gt 0) { $node.ScriptTokenStream[$node.LastTokenIndex].Line } else { $node.LastTokenIndex }
                StartColumn = $node.StartColumn;
                Validated   = $validated;
                Text        = if ($node.FragmentLength -gt 0) `
                { $node.ScriptTokenStream[$node.FirstTokenIndex..$node.LastTokenIndex].Text -join [string]::Empty } `
                    else { $null }
                Additional  = $addtional     
            })
    }
}

它继承自TSqlFragmentVisitor,Validate方法用来解析被规则命中的语法节点,并记录该节点在代码中的详情,如该节点在代码中的开始行,结束行,命中的文本等,方便修改相关的SQL代码。同时将这些记录添加到AnalysisCodeResults列表,并将该列表的数据添加到CustomParser类中的ValidationResults列表中。具体规则通过重写基类的Visit方法来实现代码分析。此外还定义了一个静态方法GetAllRules用以获取项目中所有的规则。以上便是整个项目的核心代码,下面将介绍一些具体样例。

我们先做一个简单的例子,比如我们规定在Select中不能包含星号(*)。代码如下:

class PDE001: BaseRule {
    PDE001() {
        $this.Descrtiption = "Asterisk in select list."
        $this.Severity = [Severity]::Warning
    }

    [void] Visit([SelectStarExpression] $node) {
        $this.Validate($node, $false, $null)
    }
}

够简单了吧,首先继承自BaseRule类,然后重写Visit方法。由于Visit被重载了很多,我们选择参数类型为SelectStarExpression的方法,当语法树中存在这个节点的时候,我们调用基类的$this.Validate($node, $false, $null)方法,并记录了该节点的详情,这样就代表Sql代码没能通过该条规则。

比如我们写下这样一条Sql,Select * from test;然后通过调用来看下执行结果。

接下来我再讲一条比较复杂的规则。比如我们在做数据操作的时,为了降低对资源的占用时间。我们不能直接插入,删除或者更新大批量数据,这是就需要将数据分成小批量,然后通过循环的方式来处理。为了防止这样的代码,我们需要制定该规则。当然该规则也会有一些特例,如被处理的对象是表变量或者临时表,则可以忽略该规则。以下是该规则的代码实现


class PDE003:BaseRule {
    PDE003() {
        $this.Descrtiption = "You should use batch operations in statements."
        $this.Severity = [Severity]::Exception
    }

    hidden [int]$start = 0
    hidden [int]$end = 0

    [void] Visit([UpdateDeleteSpecificationBase]$node) {
        $target = $node.Target

        if ($target -is [VariableTableReference]) { return }
        if ($this.CheckWhile($node)) { return }
        [NamedTableReference] $namedTableReference = $target -as [NamedTableReference]
        $targetTable = $namedTableReference.SchemaObject.BaseIdentifier.Value
        
        if ($targetTable -imatch "^#{1,2}") { return }

        $fromClause = $node.FromClause
        if ($null -ne $fromClause) {
            [TemporaryTableVisitor]$tempVisitor = [TemporaryTableVisitor]::new($fromClause, $targetTable)
            $fromClause.AcceptChildren($tempVisitor)
            if ($tempVisitor.Validated) { return }
        }
        $this.Validate($node, $false, $null)
    }

    [void] Visit([InsertSpecification]$node) {
        $target = $node.Target
        if ($target -is [VariableTableReference]) { return }
        if ($this.CheckWhile($node)) { return }
        $namedTableReference = $target -as [NamedTableReference]
        if ($namedTableReference.SchemaObject.BaseIdentifier.Value -imatch "^#{1,2}") { return }
        $valuesInsertSource = $node.InsertSource -as [ValuesInsertSource]
        if ($null -ne $valuesInsertSource) { return }

        $this.Validate($node, $false, $null)
    }

    [void] Visit([MergeSpecification]$node) {
        $target = $node.Target
        if ( $this.CheckWhile($node)) { return }
        if ($target -is [VariableTableReference]) { return }
        $namedTableReference = $target -as [NamedTableReference]
        if ($namedTableReference.SchemaObject.BaseIdentifier.Value -imatch "^#{1,2}") { return }
        $this.Validate($node, $false, $null)
        
    }

    [void] Visit([WhileStatement]$node) {
        $this.start = $node.StartLine
        $this.end = $node.ScriptTokenStream[$node.LastTokenIndex].Line
    }

    hidden [bool] CheckWhile([TSqlFragment] $node) {
        return $node.StartLine -ge $this.start -and $node.ScriptTokenStream[$node.LastTokenIndex].Line -le $this.end
    }
}

class TemporaryTableVisitor:TSqlFragmentVisitor {

    [bool]$Validated = $false
    hidden [string] $pattern = "^(@|#{1,2})"
    hidden [FromClause]$fromClause
    hidden [string]$target

    TemporaryTableVisitor([FromClause]$fromClause, [string]$target) {
        $this.fromClause = $fromClause
        $this.target = $target
        if ($null -eq $fromClause) { $this.Validated = $true }
    }

    [void] Visit([NamedTableReference]$node) {
        $tableName = $node.SchemaObject.BaseIdentifier.Value
        $alias = $node.Alias.Value
        if ($this.target -in $alias, $tableName) {
            $this.Validated = $this.Validated -or ($tableName -imatch $this.pattern)
        }  
    }

    [void] Visit([VariableTableReference]$node) {
        $tableName = $node.Variable.Name
        $alias = $node.Alias.Value
        if ($this.target -in $alias, $tableName) {
            $this.Validated = $this.Validated -or ($tableName -imatch $this.pattern)
        }  
    }
}

 该类还引用了另外一个辅助类,辅助类是处理当前节点为Insert、Update、Delete语句的时候,获取该语句中的FROM节点中的表对象,并判断该表是否属于临时表或者表变量,如果是则忽略该规则。

当直接输入 DELETE A FROM TEST1 A INNER JOIN TEST2 B ON A.ID=B.ID,我们可以看到规则阻挡了该语句,这时Validated属性为false。

当我们代码变成 DELETE A FROM #TEST1 A INNER JOIN TEST2 B ON A.ID=B.ID,我们看到规则通过了该段代码,且Validated属性为true。

当我们在DELETE A FROM TEST1 A INNER JOIN TEST2 B ON A.ID=B.ID 语句加上WHILE再看下呢,恭喜通过了该规则的验证。

以下是客户端调用的代码

using module '.\Code Analysis\Rule.psm1'

$files = Get-ChildItem -Path "E:\BackupE\QueryFile" -Filter "*.sql" -File
$rules = [BaseRule]::GetAllRules()
$result = [CustomParser]::Analysis($files.FullName, $rules)
$result.Where({ $_.ResponseCode -eq [ResponseCode]::Success -and $_.ValidationResults.Where({ -not $_.Validated }).Count -gt 0 }) |`
    Select-Object -Property FileName, DocumentName -ExpandProperty ValidationResults |`
    Select-Object -ExpandProperty AnalysisCodeResults -ExcludeProperty Validated , AnalysisCodeResults

自此,整个代码就介绍完了,如果需要代码的话可以到以转到以下地址(下载地址)。前文提到的用ANTLR去做Code Analysis的话,需要自己去维护语法文档(文档地址),此外还需相关的工具将语法文件生成语法分析库然后调用即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/870453.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

静态库和动态库制作

文章目录 前言一、静态库和动态库介绍1、静态库2、动态库 二、静态库的制作及使用1、准备好源码2、编译源码生成 .o 文件3、制作静态库4、使用静态库 三、动态库的制作及使用1、生成位置无关的 .o 文件2、制作动态库3、使用动态库4、指定动态库路径并使其生效 四、对比1、静态库…

初步制作做一个AI智能工具网站,持续更新

文章目录 介绍AI对话AI绘画AI音视频AI图片处理AI小工具体验 介绍 网页有五大部分:AI对话、AI绘画、AI音视频、AI 图片处理、AI小工具。 AI对话 AI对话是指人工智能技术在模拟人类对话交流方面的应用。通过使用自然语言处理和机器学习算法,AI对话系统可…

Flink CDC系列之:基于 Flink CDC 构建 MySQL 和 Postgres 的 Streaming ETL

Flink CDC系列之:基于 Flink CDC 构建 MySQL 和 Postgres 的 Streaming ETL 一、技术路线二、MySQL数据库建表三、PostgreSQL数据库建表四、在 Flink SQL CLI 中使用 Flink DDL 创建表五、关联订单数据并且将其写入 Elasticsearch 中六、Kibana查看商品和物流信息的…

基于Java+SpringBoot+Vue的书籍学习平台设计与实现(源码+LW+部署文档等)

博主介绍: 大家好,我是一名在Java圈混迹十余年的程序员,精通Java编程语言,同时也熟练掌握微信小程序、Python和Android等技术,能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

Chrome

Chrome 简介下载 简介 Chrome 是由 Google 开发的一款流行的网络浏览器。它以其快速的性能、强大的功能和用户友好的界面而闻名,并且在全球范围内被广泛使用。Chrome 支持多种操作系统,包括 Windows、macOS、Linux 和移动平台。 Chrome官网: https://ww…

深度剖析堆栈指针

为什么打印root的值与&root->value的值是一样的呢 测试结果: *号一个变量到底取出来的是什么? 以前我写过一句话,就是说,如果看到一个*变量,那就是直逼这个变量所保存的内存地址,然后取出里面保存的…

Java负载均衡算法实现与原理分析(轮询、随机、哈希、加权、最小连接)

文章目录 一、负载均衡算法概述二、轮询(RoundRobin)算法1、概述2、Java实现轮询算法3、优缺点 三、随机(Random)算法1、概述2、Java实现随机算法 四、源地址哈希(Hash)算法1、概述2、Java实现地址哈希算法…

在Java中对XML的简单应用

XML 数据传输格式1 XML 概述1.1 什么是 XML1.2 XML 与 HTML 的主要差异1.3 XML 不是对 HTML 的替代 2 XML 语法2.1 基本语法2.2 快速入门2.3 组成部分2.3.1 文档声明格式属性 2.3.2 指令(了解):结合CSS2.3.3 元素2.3.4 属性**XML 元素 vs. 属…

c++ 学习系列 -- 智能指针

一 为什么引入智能指针?解决了什么问题? C 程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。但使用普通指针,容易造成内存泄露(忘记释放)、二次释放、程序发生异常时内存泄…

Springboot整合RabbitMq,详细步骤

Springboot整合RabbitMq,详细步骤 1 添加springboot-starter依赖2 添加连接配置3 在启动类上添加开启注解EnableRabbit4 创建RabbitMq的配置类,用于创建交换机,队列,绑定关系等基础信息。5 生产者推送消息6 消费者接收消息7 生产者…

闭环控制方法及其应用:优缺点、场景和未来发展

闭环控制是一种基本的控制方法,它通过对系统输出与期望值之间的误差进行反馈,从而调整系统输入,使系统输出更加接近期望值。闭环控制的主要目标是提高系统的稳定性、精确性和鲁棒性。在实际应用中,闭环控制有多种方法,…

开源代码分享(13)—整合本地电力市场与级联批发市场的投标策略(附matlab代码)

1.引言 1.1摘要 本地电力市场是在分配层面促进可再生能源的效率和使用的一种有前景的理念。然而,作为一个新概念,如何设计和将这些本地市场整合到现有市场结构中,并从中获得最大利润仍然不清楚。在本文中,我们提出了一个本地市场…

linux添加磁盘

一、linux虚拟机添加一块新的硬盘 四步: (1) (2)为硬盘进行分区 (3)初始化硬盘分区 (4)挂载 在虚拟机上添加一块硬盘 (1)、 虚拟机添加一块新的硬盘作为数据盘 (2) ls…

Idea Live Template 功能总结

文章目录 Java自带的template属性模板psf——public static finalpsfi——public static final intpsfi——public static final StringSt——String 方法模板psvm——main方法sout——打印语句iter——for迭代循环fori——for循环 代码块模板if-e —— if elseelse-if 自定义自…

中国首款量子计算机操作系统本源司南 PilotOS正式上线

中国安徽省量子计算工程研究中心近日宣布,中国国产量子计算机操作系统本源司南 PilotOS 客户端正式上线。 如果把量子芯片比喻成人的“心脏”,那么量子计算机操作系统就相当于人的“大脑”,量子计算应用软件则是人的“四肢”。 据安徽省量子…

Linux 终端命令之文件浏览(1) cat

Linux 文件浏览命令 cat, more, less, head, tail,此五个文件浏览类的命令皆为外部命令。 hannHannYang:~$ which cat /usr/bin/cat hannHannYang:~$ which more /usr/bin/more hannHannYang:~$ which less /usr/bin/less hannHannYang:~$ which head /usr/bin/he…

论文总结《Towards Evaluating the Robustness of Neural Networks(CW)》

原文链接 C&W 这篇论文更像是在讲一个优化问题,后面讲述如何针对生成对抗样本的不可解问题近似为一个可解的问题,很有启发。本文后面将总结论文各个部分的内容。 Motivation 文章提出了一个通用的设计生成对抗样本的方法,根据该论文提…

YAPi在线接口文档简单案例(结合Vue前端Demo)

在前后端分离开发中,我们都是基于文档进行开发,那前端人员有时候无法马上拿到后端的数据,该怎么办?我们一般采用mock模拟伪造数据直接进行测试,本篇文章主要介绍YApi在线接口文档的简单使用,并结合Vue的小d…

【C++学习】STL容器——stack和queue

目录 一、stack的介绍和使用 1.1 stack的介绍 1.2 stack的使用 1.3 stack的模拟实现 二、queue的介绍和使用 2.1 queue的介绍 2.2 queue的使用 2.3 queue的模拟实现 三、priority_queue的介绍和使用 3.1 priority_queue的介绍和使用 3.2 priority_queue的使用 3.4 p…

【Powershell 】(Windows下)常用命令 | 命令别名 | 运行Windows命令行工具 | 运行用户程序(vim、gcc、gdb)

微软官方Powershell文档:https://learn.microsoft.com/zh-cn/powershell/ 命令详细说明,在PDF的最后面: 一、Powershell及命令简介1.1 命令格式1.2 命令的别名 二、cmdlet别名三、cmdlet分类介绍3.1 基础命令1. Get-Command2. Get-Help3. S…