Pull to refresh

Валидация Sql кода с помощью .net и git-hook

Reading time5 min
Views6.8K
Здравствуй Хабр!

Совсем недавно в нашей компании проходил очередной Хакатон. И в его рамках мне захотелось убить время поинтересней сделать полезную вещь, как для себя так и для других разработчиков. Выбор пал на этакий валидатор sql кода, который бы проверял его на разные правила что не под силу компилятору и те что могут пропустить ребята которые делают Code Review. Таких правил можно придумать массу, начиная от простого “Добавлять GO в конце запроса” и заканчивая более сложными “Использовать View вместо Table”. И самое главное, этот валидатор никоим образом не должен добавлять время разработчику на его использование, т.е. попросту говоря, он должен валидировать себе где-то автоматически, вне зависимости от действий разработчика.

Так уж исторически сложилось, что весь sql-код перед тем как выйти в продакшн (т.е. исполнится на основной БД) сохраняется у нас в GIT репозитории, куда попадает напрямую от разработчиков (естественно после Code Review). Так вот, возникла идея добавить git-hook в этом репозитории который бы валидировал sql-код и если он не валидный то коммит бы возвращался разработчику на доработку. Немного тяжело представить, легче нарисовать:




Git hook


Итак, для начала я решил копнуть в сторону git-hooks, чтобы разобратся как же все-таки отменить коммит (если понадобиться). Вкратце, что такое git-hook: это некий bash-script который исполняется всякий раз когда происходит некое определенное событие связанное с изменением состояния репозитория. Есть два типа таких скриптов: клиентские и серверные. Да и событий, как и скриптов, есть немало. Более подробно можно почитать вот тут.
Для наших целей как нельзя лучше подходит git-hook “update” (серверный скрипт). Он запускается аккурат перед тем как обновить состояние ветки(ок) на сервере, по сути перед тем как ваши изменения отправленные на сервер будут применены. И, что немаловажно, если exit code такого скрипта будет отличен от нуля, изменения отвергнутся. Скрипт имеет три входящих параметра:

  • Имя ветки которая обновляется
  • Ссылка на коммит перед обновлением ветки
  • Ссылка на коммит после обновления ветки

Имея такой набор параметров можно, для начала, получить список файлов которые были изменены используя второй и третий параметры нашего скрипта:

git diff --name-only --diff-filter=[AMCR] $2 $3

Получив список файлов, нужно достучаться до контента каждого файла с помощью команды git-show. И имея уже сам контент, можно попробовать его проверить. Ниже идет листинг скрипта git-hook update, а также двух вспомагательных скриптов:

#!/bin/sh
ex=0
okResult="OK"
originalPath='/git/test.git/' #путь к репозиторию git

printf "---- 'Sql checker' hook ----"

listOfFiles=$(git diff --name-only --diff-filter=[AMCR] $2 $3) #получаем список файлов

for changedFile in $listOfFiles
do
   printf "checking:$changedFile"
   fullFilePath="$3:$changedFile"
   git-show $fullFilePath >tmp_sql
   result=$($originalPath/hooks/check-sql tmp_sql) #запускаем проверку
   if [ "$result" != $okResult ]
   then
      res=${result//|/\\n    }
      printf "    $res\n" #выводим ошибки
      ex=1
   else
      printf "ok!\n"
   fi

done

printf "---- Done ----"

exit $ex

Cкрипт check-sql делает запрос на внешний ресурс для проверки файла с sql кодом, а также проводит разбор результата полученого в виде soap:

#!/bin/sh
soap-template $1 tmp_soap
wget -qO- --post-file=tmp_soap --header="Content-type: text/xml; charset=utf-8" 127.0.0.1/check-sql.asmx | gawk -v re="<CheckFileResult.*>(.*)/CheckFileResult>" '{match($0,re,RMATCH); print RMATCH[1]}'

И вдогонку скрипт soap-template, который sql-код оформляет в soap запрос:

#!/bin/sh
encodedFile=$(base64 $1)
echo '<?xml version="1.0" encoding="utf-8"?>' >$2
echo '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSc
ma" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' >> $2
echo '  <soap:Body>' >>$2
echo '    <CheckFile xmlns="http://tempuri.org/">' >>$2
echo "      <base64>$encodedFile</base64>" >>$2
echo '    </CheckFile>' >>$2
echo '  </soap:Body>' >>$2
echo '</soap:Envelope>' >>$2

Так вот, я не так силен в bash-скриптах чтобы делать проверку sql-кода на них, по сему, ее я решил вынести в любимую среду .Net-а.

.Net часть


Комуникационным мостом между git-hook и .Net я выбрал .net web service, посему именно скрипт «check-sql» в нашем git-hook скрипте делает запрос к этому сервису с параметром который содержит контент sql-файла:

[WebMethod]
public string CheckFile(string base64)
{
    var errors = new List<SqlCheckError>();
    string result = OkResult;
    try
    {
        //получаем контент sql-файла
        string sqlScript = Encoding.Default.GetString(Convert.FromBase64String(base64));
        //создаем контейнер списка правил ISqlCheck
        var containerProvider = new SqlCheckUnityProvider();
        //запускаем контент на валидацию
        var checker = new SqlCheckProcess(containerProvider);
        errors.AddRange(checker.CheckSql(sqlScript));

    }
    catch (Exception e)
    {
        Log(e); //логируем  
        return OkResult; //и возвращаем ОК чтобы не отменять коммит, так как возникла ошибка в нашем фреймворке.
    }

    //все ошибки (если есть) возвращаем назад в наш git-hook
    if (errors.Count > 0)
    {
        result = string.Join("|", errors.Select(error => string.Format("{1}: {0}", error.Message, error.Type)));
    }

    return result;
}

Имея уже .net среду и предоставленый нам sql-code для валидации, можно придумать много чего. Я создал маленький фреймворк для такой валидации, но приводить весь его код здесь, думаю, не очень уместно. В ближайшем будущем выложу его на github. В двух словах, все основывается на вот таком нехитром интерфейсе:

public interface ISqlCheck
{
    bool CheckSql(string sqlCode);
    SqlCheckError GetError(string sqlCode);
}

Этот интерфейс представляет собой правило валидации. Имея уйму реализаций этого интерфейса в связке с Unity Container мы можем управлять набором правил применяемым к валидации в целом. Так же имея встроенный в .net sql-парсер (TSql100Parser) можно реализовать проверку практически любого правила.

Практика


Эта нехитрая система уже начала действовать у нас в компании, пока рано делать выводы о какой-то громадной пользе, но имея потенциально большой запас для расширения возможностей, можно уверенно сказать, что пользы от этого будет значительно больше чем вреда.
В заключение привожу лог консоли при работе данной системы:

$ git push origin master
Counting objects: 7, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 347 bytes, done.
Total 4 (delta 2), reused 0 (delta 0)
remote:
remote: ---- 'Sql checker' hook ----
remote:
remote: checking: test/create_sp.sql
remote:     Error: Missed 'GO' statement after stored procedure.
remote:
remote: ---- Done ----
remote:
remote: error: hook declined to update refs/heads/master
To ssh://user@git.local/git/test.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'ssh://user@git.local/git/test.git'

Как видите с предпоследней строки, коммит был отвергнут системой из-за наличия ошибок в изменяемом sql-срипте.

Вот собвственно и все о чем я хотел поведать этим постом.
Спасибо за внимание. Удобной всем разработки!
Tags:
Hubs:
+9
Comments2

Articles