こんにちは、株式会社ZOZOで25卒の内定者アルバイトをしている村井です。この記事では業務で取り組んでいる、BigQueryで使うSQLのリンターの作成方法について紹介します。 目次 目次 課題と解決策 課題 解決策 BigQueryのアンチパターン認識ツール ミニマムな使い方 日本語がSQL内に含まれている際の問題 アンチパターンを定義する リンターとしてBigQueryのアンチパターン認識ツールを使用する際に生じる課題と解決策 構成 APIサーバ化 Chrome拡張 動作例 まとめ 課題と解決策 課題 社内では様々なチームがSQLを書いており、動作はするものの良くない書き方をしている場合があります。そういった構文を検知して、前もって修正する必要があります。 解決策 BigQueryのコンソールで入力されたSQLの不正構文を検知、修正案を提示できるようにしました。 BigQueryのアンチパターン認識ツール BigQueryのアンチパターン認識ツール とはGoogleが作成しているBigQueryのアンチパターンを教えてくれるツールです。 自分でアンチパターンを定義せずに使用した場合、以下のアンチパターンを検知して教えてくれます。 Selecting all columns 以下のように、アスタリスクによって全てのカラムが選択されている際のアンチパターンです。 SELECT * FROM `project.dataset.users`; SEMI-JOIN without aggregation DISTINCTのないサブクエリでINを使用しているときに発生します。 SELECT u.name FROM `project.dataset.users` u WHERE u.id IN ( SELECT id FROM `project.dataset.orders` o WHERE o.status = ' shipped ' ); Multiple CTEs referenced more than twice CTEが複数参照されているときに発生します。 WITH a AS (SELECT col1 FROM `project.dataset.table1`), b AS (SELECT col2 FROM a), c AS (SELECT col1 FROM a) SELECT a.col1, b.col2, c.col1 FROM a, b, c; Using ORDER BY without LIMIT ORDER BY句をLIMIT無しで使用したときに発生します。 SELECT name, age FROM `project.dataset.employees` ORDER BY age DESC ; Using REGEXP_CONTAINS when LIKE is an option LIKE句で十分表現でき、正規表現を使う必要がない場合に発生します。 SELECT username FROM `project.dataset.users` WHERE REGEXP_CONTAINS(username, r ' .*admin.* ' ); Using an analytic functions to determine latest record 分析関数を用いて最新のレコードを特定するときに発生します。以下のコードはrow_numberを使用していますが、ORDER BYとLIMITを使って書き直すことができます。 SELECT id, fare FROM ( SELECT id, fare, row_number() over(partition by id order by fare desc ) rn FROM `project.dataset.table1` ) WHERE rn = 1 ; Convert Dynamic Predicates into Static Dynamic PredicateをStatic Predicateに変えるとパフォーマンスが向上するかもしれないときに発生します。以下のコードは、WHERE句に含まれるサブクエリが動的に条件を生成します。 SELECT * FROM `project.dataset.users` u WHERE u.id IN ( SELECT id FROM `project.dataset.customers` WHERE region = ' US ' ); Where order, apply most selective expression first WHERE句の中のフィルタ条件が不適切な順序であるときに発生します。以下のコードには price > 1000 と category = 'electronics' のフィルタがあります。この場合選択性が高いのは category = 'electronics' なので price > 1000 より前に書くべきであるということです。 SELECT id, name FROM `project.dataset.products` WHERE price > 1000 AND category = ' electronics ' ; Missing DROP Statement TEMP TABLEをDROPしないときに発生します。 CREATE TEMP TABLE `project.dataset.temp_table` (id INT64, name STRING); Dropped Persistent Table TEMP TABLEで事足りる際に、永続的なテーブルをCREATEして最後にDROPしているときに発生します。 CREATE TABLE `project.dataset.temp_table` (id INT64, name STRING); SELECT * FROM `project.dataset.temp_table`; DROP TABLE `project.dataset.temp_table`; 今回はこれに加えて自ら定義したアンチパターンを検知したいので、のちに追加します。 ミニマムな使い方 まず、ローカルでBigQueryのアンチパターン認識ツールを使う方法をご紹介します。 最初に、 BigQueryのアンチパターン認識ツールのリポジトリ をcloneします。このシステムはJavaで作られており、以下のコマンドでjibを使ってDockerコンテナイメージをビルドします。 mvn clean package jib:dockerBuild -DskipTests ビルドができたら以下でクエリの解析結果が返ってきます。 docker run \ -i bigquery-antipattern-recognition \ --query " SELECT * FROM \` project.dataset.table1 \` " また、特定のSQLファイルを解析するには以下のようなコマンドを入力します。 export INPUT_FOLDER = $( pwd ) /samples/queries/input export INPUT_FILE_NAME =multipleCTEs.sql docker run \ -v $INPUT_FOLDER : $INPUT_FOLDER \ -i bigquery-antipattern-recognition \ --input_file_path $INPUT_FOLDER / $INPUT_FILE_NAME 日本語がSQL内に含まれている際の問題 今回BigQueryのアンチパターン認識ツールを使用するにあたって、以下のようにSQLに日本語が入るとエラーが発生するという問題が発生しました。 --日本語ああああああああああああああああ SELECT title, language FROM `bigquery- public -data.samples.wikipedia` WHERE REGEXP_CONTAINS(title, ' .*aaaaa.* ' ) エラー内容は以下の通りです。 ERROR com.google.zetasql.toolkit.antipattern.util.AntiPatternHelper - index 138,length 138 java.lang.StringIndexOutOfBoundsException: index 138,length 138 at java.base/java.lang.String.checkIndex(String.java:3278) at java.base/java.lang.StringUTF16.checkIndex(StringUTF16.java:1470) at java.base/java.lang.StringUTF16.charAt(StringUTF16.java:1267) at java.base/java.lang.String.charAt(String.java:695) at com.google.zetasql.toolkit.antipattern.util.ZetaSQLStringParsingHelper.countLine(ZetaSQLStringParsingHelper.java:67) at com.google.zetasql.toolkit.antipattern.parser.visitors.IdentifyRegexpContainsVisitor.visit(IdentifyRegexpContainsVisitor.java:63) at com.google.zetasql.parser.ASTNodes$ASTFunctionCall.accept(ASTNodes.java:3592) at com.google.zetasql.parser.ParseTreeVisitor.descend(ParseTreeVisitor.java:45) (略) Javaの例外であるStringIndexOutOfBoundsExceptionが出ています。要約すると、文字列の指定されたインデックスが文字列長を超えているというエラーです。 このエラーの原因は、BigQueryのアンチパターン認識ツールがバイト長で文字列長をカウントしていることでした。アンチパターンが起こっている行を示すためにBigQueryのアンチパターン認識ツールは文字列長をカウントします。その手法こそがバイト長を文字列長として扱うというものでした。SQLがアルファベットや数字など1バイト文字だけで構成されている場合、バイト長と文字列長が一致するので問題ありません。しかし、SQLに日本語などのマルチバイト文字が入っている場合、バイト長>文字列長となってしまいエラーが発生します。 この解決手段として、バイト長を文字列長と一致させるようBigQueryのアンチパターン認識ツールのコードを改変しました。 改変内容は、BigQueryのアンチパターン認識ツールに PR を出しマージされたので、現在この問題は発生しません。 アンチパターンを定義する 今回は定義済みのアンチパターンに加えて不正なテーブル名をアンチパターンとして検出するという要件がありました。具体的には、以下のようにSQL内に出現するテーブル名の中にプロジェクト名が入っていない場合、プロジェクト名まで含めるよう促すというものです。 OK FROM `project_name.dataset_name.table_name` NG FROM `dataset_name.table_name` -- プロジェクト名が省略されている このように、自分で定義したアンチパターンを追加する方法をご紹介します。 前提として、BigQueryのアンチパターン認識ツールはSQLをAST(抽象構文木)に変換して、そのASTをトラバースして構文解析します。このときVisitorパターンを用います。したがって、新たなアンチパターンを定義するときには該当ノードをトラバースするVisitorを作成する必要があります。AntiPatternVisitorを実装する形でParseTreeVisitorを継承した新しいVisitorクラスを定義していきます。 // このソースコードは `src/main/java/com/google/zetasql/toolkit/antipattern/parser/visitors` フォルダに配置してください。既存のVisitorが置かれています。 public class IdentifyTableVisitor extends ParseTreeVisitor implements AntiPatternVisitor { public static final String NAME = "Table" ; private Set<String> tableNames = new HashSet<>(); private Set<String> withNames = new HashSet<>(); private ArrayList<String> result = new ArrayList<String>(); private final String SUGGESTION_MESSAGE = "テーブル名が不正です。プロジェクト名をテーブル名に追加してください %s." ; public IdentifyTableVisitor(String query) { this .query = query; } public void visit(ASTNodes.ASTTableExpression tableExpression) { if (tableExpression instanceof ASTNodes.ASTTablePathExpression) { visit((ASTTablePathExpression) tableExpression); } else if (tableExpression instanceof ASTNodes.ASTJoin) { visit(((ASTNodes.ASTJoin) tableExpression).getLhs()); visit(((ASTNodes.ASTJoin) tableExpression).getRhs()); } else if (tableExpression instanceof ASTNodes.ASTTableSubquery) { ASTNodes.ASTQueryExpression queryExpression = ((ASTNodes.ASTTableSubquery) tableExpression).getSubquery().getQueryExpr(); if (queryExpression instanceof ASTNodes.ASTSelect) { ASTNodes.ASTTableExpression tableExpression1 = ((ASTSelect) queryExpression).getFromClause().getTableExpression(); visit(tableExpression1); } } } @Override public void visit(ASTTablePathExpression tablePathExpression) { if (tablePathExpression.getPathExpr() != null ) { List<String> namePaths = tablePathExpression.getPathExpr().getNames().stream() .map(ASTIdentifier::getIdString).collect(Collectors.toList()); tableNames.addAll(namePaths); } if (tablePathExpression.getUnnestExpr() != null ) { String unNestExpressions = tablePathExpression.getUnnestExpr().getExpression().toString(); withNames.add(unNestExpressions); } } // ここでWITH句で定義されたテーブル名を抽出 @Override public void visit(ASTWithClause withClause) { List<ASTAliasedQuery> namePaths = withClause.getWith().stream().collect(Collectors.toList()); for (ASTAliasedQuery value : namePaths) { value.accept( this ); withNames.add(value.getAlias().getIdString()); } } private int countDot(String str) { int count = 0 ; for ( int i = 0 ; i < str.length(); i++) { if (str.charAt(i) == '.' ) { count++; } } return count; } public String getResult() { for (String tableName : tableNames) { int count = countDot(tableName); if (count != 2 && !withNames.stream().anyMatch(set -> set.contains(tableName))) { result.add(String.format(SUGGESTION_MESSAGE, tableName)); } } return result.stream().distinct().collect(Collectors.joining( " \n " )); } @Override public String getName() { return NAME; } } このコードで、テーブル名に当たるASTのノードを訪問し、ドットの数でテーブル名にプロジェクト名が含まれているかどうかを判断します。ドットが2個未満の場合はプロジェクト名が含まれていないという判定をします。しかし、WITH句で定義されたテーブル名に関してはこの限りではないので除外できるようにします。 そして以下のファイルのgetParserVisitorListに、定義したAntiPatternVisitorのインスタンスを追加します。 public List<AntiPatternVisitor> getParserVisitorList(String query) { return new ArrayList<>(Arrays.asList( new IdentifySimpleSelectStarVisitor(), new IdentifyInSubqueryWithoutAggVisitor(query), new IdentifyDynamicPredicateVisitor(query), new IdentifyOrderByWithoutLimitVisitor(query), new IdentifyRegexpContainsVisitor(query), new IdentifyCTEsEvalMultipleTimesVisitor(query), new IdentifyLatestRecordVisitor(query), new IdentifyWhereOrderVisitor(query), new IdentifyMissingDropStatementVisitor(query), new IdentifyDroppedPersistentTableVisitor(query), new IdentifyTableVisitor(query) // 追加 )); } これで不正なテーブル名をアンチパターンとして警告できるようになりました。 リンターとしてBigQueryのアンチパターン認識ツールを使用する際に生じる課題と解決策 BigQueryのアンチパターン認識ツールをそのまま使用する場合、Dockerコンテナを建てるかビルド済みのjarファイルをコマンドライン上で動作させます。使ってもらう際、各々の環境の違いもある中で動作環境を整え、解析対象のSQLを参照しコマンドを実行してもらう方法では手間がかかりすぎます。さらにエンジニア以外の使用も想定されるため、現実的ではありません。 そこで、Chromeの拡張機能として、BigQueryのコンソールで入力されたSQLをボタン1つで解析できるようにしました。SQLを投げると解析結果が返ってくるAPIを作成し、そのAPIをユーザが呼び出すという構成です。これによりコンソールにSQLを入力するだけで誰でも解析を掛けられるようになりました。 構成 作成したリンターを実際にChromeの拡張機能として使用する方法を紹介していきます。 APIサーバ化 BigQueryのアンチパターン認識ツールは、Spring Bootを使ってWebサービスとして使う環境が最初から整っています。それを利用してSQLをリクエストとして解析結果を返すAPIサーバを作成します。Spring Bootをセットアップする際のおおまかな手順は以下の通りです。 mainメソッドの変更 Spring BootのControllerを記述 出力メッセージのクラスを作成 まず、mainメソッドでSpring Bootを起動させられるようにします。 @SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main. class , args); } } 続いて、実際に構文解析するコードをSpring BootのControllerとして書き直します。基本的に改変する前のmainメソッドと同様ですが、1つのSQLを受け取り1つの解析結果を返せるようにします。 public class QueryRequest { private String query; public String getQuery() { return query; } } @RestController public class MainController { private static int countQueriesWithAntipattern = 0 ; @PostMapping ( "/" ) public Map<String, Object> processQuery( @RequestBody QueryRequest queryRequest) { try { String query = queryRequest.getQuery(); String replies[] = new String[ 1 ]; AntiPatternCommandParser cmdParser = new AntiPatternCommandParser( new String[] {}); AntiPatternHelper antiPatternHelper = new AntiPatternHelper( cmdParser.getProcessingProject(), cmdParser.useAnalyzer()); OutputWriterForResponse outputWriter = new LogOutputWriterForResponse(); Boolean rewriteSQL = cmdParser.rewriteSQL(); outputWriter.setRewriteSQL(rewriteSQL); InputQuery inputQuery = new InputQuery(query, "query provided by param:" ); StringBuilder result = executeAntiPatternsInQuery(inputQuery, outputWriter, cmdParser, antiPatternHelper); result.append(logResultStats()); outputWriter.close(); replies[ 0 ] = result.toString(); return Map.of( "replies" , replies); } catch (Exception e) { return Map.of( "errorMessage" , e.toString()); } } private StringBuilder executeAntiPatternsInQuery(InputQuery inputQuery, OutputWriterForResponse outputWriter, AntiPatternCommandParser cmdParser, AntiPatternHelper antiPatternHelper) { StringBuilder stringBuilder = new StringBuilder(); try { List<AntiPatternVisitor> visitorsThatFoundAntiPatterns = new ArrayList<>(); // parser visitors antiPatternHelper.checkForAntiPatternsInQueryWithParserVisitors(inputQuery, visitorsThatFoundAntiPatterns); // analyzer visitor if (antiPatternHelper.getUseAnalizer()) { antiPatternHelper.checkForAntiPatternsInQueryWithAnalyzerVisitors(inputQuery, visitorsThatFoundAntiPatterns); } // rewrite if (cmdParser.rewriteSQL()) { GeminiRewriter.rewriteSQL(inputQuery, visitorsThatFoundAntiPatterns, antiPatternHelper, cmdParser.getLlmRetriesSQL(), cmdParser.getLlmStrictValidation()); } // write output if (!visitorsThatFoundAntiPatterns.isEmpty()) { return outputWriter.writeRecForQuery(inputQuery, visitorsThatFoundAntiPatterns, cmdParser); } return stringBuilder; } catch (Exception e) { System.out.println(e); return stringBuilder; } } private static String logResultStats() { StringBuilder statsString = new StringBuilder(); statsString.append( " \n * Queries with anti patterns: " + countQueriesWithAntipattern); return statsString.toString(); } } そして、レスポンスで使う出力メッセージ作成クラスを追加します。 // このソースコードは `src/main/java/com/google/zetasql/toolkit/antipattern/output` フォルダに配置してください。 public abstract class OutputWriterForResponse { private boolean rewriteSQL = false ; public abstract StringBuilder writeRecForQuery(InputQuery inputQuery, List<AntiPatternVisitor> visitorsThatFoundPatterns, AntiPatternCommandParser cmdParser) throws IOException; public void close() throws IOException {}; public void setRewriteSQL( boolean rewriteSQL) { this .rewriteSQL = rewriteSQL; } } // このソースコードは `src/main/java/com/google/zetasql/toolkit/antipattern/output` フォルダに配置してください。 public class LogOutputWriterForResponse extends OutputWriterForResponse { private static final Logger logger = LoggerFactory.getLogger(LogOutputWriter. class ); public StringBuilder writeRecForQuery(InputQuery inputQuery, List<AntiPatternVisitor> visitorsThatFoundPatterns, AntiPatternCommandParser cmdParser) { StringBuilder outputStrBuilder = new StringBuilder(); outputStrBuilder.append( " \n " + "-" .repeat( 50 )); outputStrBuilder.append( " \n Recommendations for query: " + inputQuery.getQueryId()); for (AntiPatternVisitor visitor: visitorsThatFoundPatterns) { outputStrBuilder.append( " \n * " + visitor.getName() + ": " + visitor.getResult()); } if (cmdParser.rewriteSQL() && inputQuery.getOptimizedQuery() != null ) { outputStrBuilder.append( " \n * Optimized query: \n " ); outputStrBuilder.append(inputQuery.getOptimizedQuery()); } outputStrBuilder.append( " \n " + "-" .repeat( 50 )); outputStrBuilder.append( " \n\n " ); return outputStrBuilder; } } これで構文解析の機能をAPIリクエストでSQLを投げることで使用できるようになりました。 次に作成したAPIサーバをデプロイします。Cloud Runにデプロイするまでの流れは以下の通りです。 Artifact Registryにリポジトリを作成 Docker imageをビルド、タグ付け Docker imageのpush Cloud Runにデプロイ まず、Artifact Registryにリポジトリを作成します。形式はDockerを選択します。 次に、実際にpushします。 以下でDocker imageをビルドします。 mvn clean package jib:dockerBuild -DskipTests Docker imageにタグ付けします。 docker tag bigquery-antipattern-recognition [ REGION ] -docker.pkg.dev/ [ PROJECT_ID ] / [ REPOSITORY_NAME ] / [ IMAGE ] そして、pushします。 docker push [ REGION ] -docker.pkg.dev/ [ PROJECT_ID ] / [ REPOSITORY_NAME ] / [ IMAGE ] 次にイメージをCloud Runにデプロイします。 gcloud run deploy --image [ REGION ] -docker.pkg.dev/ [ PROJECT_ID ] / [ REPOSITORY_NAME ] / [ IMAGE ] : [ TAG ] --platform = managed --project =[ PROJECT_ID ] これでCloud Run上に、POSTリクエストでSQLをbodyに含めれば解析結果が返ってくるAPIサーバをデプロイできました。次に社員のみがこのAPIを使用できるようにするため、IAPによる認証をつけます。IAPはロードバランサ上で動作するので、まずロードバランサを作成します。そしてCloud Runサービスをバックエンドサービスとしてロードバランサに紐づける作業を先にします。具体的な流れは以下の通りです。 外部静的アドレスを取得 DNSの設定 ロードバランサを作成 作成したCloud Runサービスをロードバランサに紐づける ロードバランサのフロントエンドの設定 IAPの設定 まず、ロードバランサに接続するための外部静的アドレスを「VPCネットワーク - IPアドレス」から予約します。 次にCloud DNSからレコードセットを作成します。リソースレコードのタイプはAとし、IPv4アドレスには予約したIPアドレスを紐づけます。IPアドレスの設定ができたらロードバランサを作成します。 「ロードバランサの作成」を選択し、外部のアプリケーションロードバランサを作成します。 次にバックエンドの構成を設定します。「バックエンドサービスとバックエンドバケット」からバックエンドサービスを作成します。バックエンドタイプを「サーバーレスネットワークエンドポイントグループ」に設定し、新しいバックエンドとして先ほど作成したCloud Runサービスを指定します。 これでロードバランサとCloud Runサービスが紐づきました。 フロントエンドの構成では、プロトコルをHTTPSにします。IPアドレスに先ほど設定したものを指定します。証明書は、「新しい証明書を作成」で先ほど作成したドメインを指定し、Googleマネージドの証明書を作成します。 ロードバランサを作成できました。次に、このロードバランサに対してIAPで認証をかけます。Identity-Aware Proxyのコンソール画面から設定します。 バックエンドサービスの中からIAPの認証をかけたいものを選び、IAPのトグルをオンにします。そして、アクセス権の設定をします。プリンシパルを追加し、「IAP-secured Web App User」ロールを割り当てます。割り当てられたプリンシパルは今回作成したAPIサーバへアクセスできるようになります。 最後に当該バックエンドサービスの設定画面で、最下部の「HTTPオプションを有効にする」にチェックを入れておきます。こちらについては後述します。 Chrome拡張 次に、クライアントサイドの作成方法を紹介します。Chrome拡張を用いてBigQueryのコンソール画面に解析ボタンを設置しました。ユーザが解析ボタンを押すと、エディタに入力したSQLをリクエストボディとして先ほど作成した解析用のAPIリクエストを送信できるようにしました。解析結果はモーダルで表示します。 Chrome拡張を作成する際、以下のようなディレクトリ構造になります。 linter-extension ├── content.js ├── manifest.json └── styles.css manifest.jsonは、拡張の構成や権限、動作方法を定義します。host_permissionsにAPIサーバのURLを記述、content_scriptsのmatchesにDOMを操作するサイトのURLを記述しておきます。 { " manifest_version ": 3 , " name ": " linter ", " version ": " 1.0 ", " permissions ": [ " cookies ", " activeTab " , ] , " action ": { " default_popup ": " popup.html " } , " content_scripts ": [ { " matches ": [ " https://console.cloud.google.com/bigquery* " ] , " js ": [ " content.js " ] , " css ": [ " styles.css " ] } ] , " host_permissions ": [ " https://[作成したAPIサーバのドメイン]/* " ] } content.jsには、実際にページのDOMを操作して要素を追加、削除、変更するJavaScriptを書きます。今回の場合は以下のような内容を書きます。 コンソール画面へのボタンの追加 エディタに書かれたSQLの読み取り APIをリクエストする 初回の認証を通すコードは IAP セッションの管理 を参照しました。 // エディタからSQLを取得するコード function getCombinedText () { const formParent = document . querySelector ( '[エディタのセレクタ]' ) ; if ( formParent ) { console . log ( formParent ) const content = formParent . innerText || formParent . textContent ; return content . replace (/ [\r\n \ \ ] + / g , '' ) ; } else { return "" ; } } // 初回の認証を通すコード var iapSessionRefreshWindow = null ; function sessionRefreshClicked () { if ( iapSessionRefreshWindow == null ) { iapSessionRefreshWindow = window . open ( "/?gcp-iap-mode=DO_SESSION_REFRESH" ) ; window . setTimeout ( checkSessionRefresh , 500 ) ; } return false ; } function checkSessionRefresh () { if ( iapSessionRefreshWindow ! = null && ! iapSessionRefreshWindow . closed ) { fetch ( "/" , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , credentials : 'include' , }) . then ( function ( response ) { if ( response . status === 401 ) { window . setTimeout ( checkSessionRefresh , 500 ) ; } else { iapSessionRefreshWindow . close () ; iapSessionRefreshWindow = null ; } }) ; } else { iapSessionRefreshWindow = null ; } } // APIを叩くボタンを設置する function addButton () { const linterUrl = '[APIサーバのURL]' ; const modalHTML = ` <div id="modal" class="modal"> <div class="modal-content"> <span class="close">×</span> <div id="modalText"></div> </div> </div> ` ; document . body . insertAdjacentHTML ( 'beforeend' , modalHTML ) ; const modal = document . getElementById ( "modal" ) ; const modalText = document . getElementById ( "modalText" ) ; const close = document . getElementsByClassName ( "close" )[ 0 ] ; function openModal ( message ) { modalText . textContent = message ; modal . style . display = "block" ; } close . onclick = function () { modal . style . display = "none" ; } window . onclick = function ( event ) { if ( event . target === modal ) { modal . style . display = "none" ; } } function addButtonToActionBars () { const parentDiv = document . querySelectorAll ( "[ボタンを追加したい親要素]" ) ; const newDiv = document . createElement ( 'div' ) ; const button = document . createElement ( 'button' ) ; newDiv . appendChild ( button ) ; parentDiv . appendChild ( newDiv ) ; button . addEventListener ( 'click' , async () => { const query = getCombinedText () ; try { const response = await fetch ( linterUrl , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , body : JSON . stringify ({ "query" : query }) , credentials : 'include' , }) ; if ( response . status === 401 ) { button . onclick = sessionRefreshClicked () ; } else if ( ! response . ok ) { console . log ( response ) ; throw new Error ( `HTTP error! Status: ${ response . status } ` ) ; } else { const data = await response . json () ; openModal ( data . replies . join ( '\n' )) ; } } catch ( err ) { openModal ( err ) ; } }) ; } ; addButtonToActionBars () ; } window .onload = addButton ; 今回はBigQueryのコンソールからAPIサーバにクロスオリジンでリクエストを送っています。 かつ、Content-Typeヘッダにapplication/jsonを指定してPOSTリクエストをしているため、リクエストの前にプリフライトリクエストが発生します。先述した「HTTPオプションを有効にする」をチェックしない場合プリフライトリクエストが正常にサーバ側に届かないので注意してください。 動作例 最後に作成したChrome拡張を有効化します。Chromeでchrome://extensions/にアクセスして拡張機能ページを開き、画面右上のデベロッパーモードをオンにします。 そして画面左上の「パッケージ化されていない拡張機能を読み込む」をクリックします。すると拡張機能のディレクトリを選択できるようになるので、作成した拡張機能のディレクトリを選択します。すべての拡張機能の欄に作成したものが追加されたことを確認してください。 拡張機能を有効にすると以下のような解析ボタンが現れます。 クエリを入力し、解析ボタンを押すと解析されます。 まとめ 今回はChrome拡張としてBigQueryのアンチパターン認識ツールを利用して独自のSQLリンターを作成できました。ぜひ参考にしていただけると幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com