AltSQLを(少しだけ)考える

# 最初に

SQLって嫌ですよね。何が嫌かって

* コーディングスタイルのバラツキ
  * キーワードを大文字で書くか小文字で書くか流派が分かれている
  * , を先頭に書くのか末尾に書くのかで流派が分かれている
  * and or を 先頭に書くのか末尾に書くのかで流派が分かれている
  * join の書き方に流派がある
  * TABか空白か・・・
* 文法の違い
  * DB製品によって文法に細かな違いがある
  * 文字コードはDBの設定による
* その他
  * row_number() over (partition by ... order by ...)って長すぎ・・
  * ビューを定義しないと、一文でSQL文を完結しなければならなくなる。結果、複雑なSQLになる(withなどを使っても同じで1文の中でしか使い回しできない)

特にコーディングスタイルの揺れについて不満を持っていて、何かスタンダードなコーディングスタイルがないかと探してました。

* [分析SQLのコーディングスタイル](https://techlife.cookpad.com/entry/2016/11/09/000033)
* [SQLスタイルガイド](https://qiita.com/taise/items/18c14d9b01a5dfd6d35e)

しかし、やっぱり自分の考えとは微妙に違ったりして、いまいち受け入れられません。そんなおりに、ふと思いました。これってAltSQLでっち上げてトランスパイラを作れば多くの問題が解決するのではないかと。

そこで思考実験でAltSQLの文法を考えたいと思います。
**思いつきで小1時間程度でつらつらと考えただけです。実装する予定はありません。**

# 基本方針

* キーワードは小文字とし、キーワードの大文字小文字は厳密に区別(大文字派閥の撲滅)
* インデントによる文法解釈をメインにする(, や and の排除)
* 簡単なSQLは簡単に書けるようにしたい(インデントによる構文とは別にメソッド呼び出しのような式で表現できるようにしたい)
* 書きやすさよりは読みやすさを重視する

# トランスパイラの動作についてメモ

* SQLの落とし穴を静的検査で回避できるなら積極的に型チェックする(文字列と数値の比較→暗黙の数値変換によりインデックスが効かなくなるため、暗黙な型変換を期待する演算はエラーにするなど)
* トランスパイル時にテーブル構造は参照できるものとする

# 制限事項

* select 文だけを対象とする
* まだつらつらと考えているだけなので厳密な文法構文は後回しにし、通常のSQLをAltSQLではこう書くというアイデアをまとめていきたい

# 文法例1: select 〜 where 〜

```sql:sql
select
  column1
 ,column2
 ,column3
from
  tableA
where column1 = 'a'
  and (column2 = 'b' or column3 = 'c')
;
```

```sql:AltSQL
select tableA
  where
    column1 = 'a'
    column2 = 'b' or column3 = 'c'
  column
    column1
    column2
    column3
```

ここでのポイントは以下

* select の後の改行は禁止
* インデント位置とキーワードで構文を判断(`,` や `and` を削除)
* where句の条件式は縦に連ねるとandで繋げたことになる。or は明示的に記入する必要がある(条件式の1行は`()`で囲んだかのように解釈することでand,orの優先順位に悩まなくて済むようにする)

インデントについては`()`で囲むことでインデントに依存しないようにすることも可能とする（サブクエリや複雑な条件式を書くために)

```sql:AltSQL
select tableA
  where
    column1 = 'a'
    (column2 = 'b'
     or column3 = 'c')
  column
    column1
    column2
    column3
```

```sql:AltSQL
select tableA
  where
    column1 = 'a'
    column2, column3 in (
      select tableB b
        column
          b.column2
          b.column3
    )
  column
    column1
    column2
    column3
```

ここでサブクエリを使ってますが、サブクエリは別途定義できるものとします

```sql:AltSQL
defquery subqueryB
  select tableB b
    column
      b.column2
      b.column3
```

そうすると、先のSQLは以下のように記述できます。

```sql:AltSQL
select tableA
  where
    column1 = 'a'
    column2, column3 in subqueryB
  column
    column1
    column2
    column3
```

ポイント

* クエリ定義文はそれだけで完結しており、定義以降に現れたクエリ名はサブクエリとして解釈される
* クエリ定義文は標準SQLのwith句のようにクエリの使い回しができ、かつ1文に繋げる必要がない

また、この場合、サブクエリを使うときに列名を指定したい場合があるでしょう。その時は以下のように書きます。


```sql:AltSQL
select tableA
  where
    column1 = 'a'
    column2, column3 in subqueryB.select(column2, column3)
  column
    column1
    column2
    column3
```

このメソッド呼び出し形式のselectを使うと、そもそも以下のようにサブクエリを定義する必要がありませんでした。

```sql:AltSQL
select tableA
  where
    column1 = 'a'
    column2, column3 in tableB.select(column2, column3)
  column
    column1
    column2
    column3
```

# エイリアス

* テーブル名のエイリアスはテーブル名の右に別名を記述します(`as`は書きません)
* where句等で利用するための、カラム名のエイリアスは別途エイリアス定義句を使います
* column句の列名の右に書いたエイリアスは出力のカラム名になります

```sql:sql
select
  column1 as c1
 ,column2 as c2
 ,column3 as c3
from
  tableA a
-- DB製品によると思いますが、ここでc1とは普通書けません
where a.c1 = 'a'
  and (a.c2 = 'b' or a.c3 = 'c')
;
```

```sql:AltSQL
select tableA a
  alias
    c1: column1
    c2: column2
    c3: column3
  where
    a.c1 = 'a'
    a.c2 = 'b' or a.c3 = 'c'
  column
    c1 a
    c2 b
    c3 c
```

ポイント

* 先にエリアス定義を書かせることで、エディタの補完機能を実装しやすくする
* column 句の項目名の右に別名を書いた場合、その別名はそのクエリの項目名になる(その項目名をwhere句で使うことはできない)

## 別案

column 句の別名を左に書いた方が、その定義内容をインデントして書くことができて良いかもしれない(矛盾なく書けるのかの評価は必要)
その場合、テーブルの別名の書き方も揃えた方が良いでしょう

```sql:AltSQL
select tableA
  column
    a: c1
    b: c2
    rn:
      row_number()
        partition
          c1, c2
        order
          c3
    cnt
      count(*)
```


# join

```sql:AltSQL
select tableA a
  alias
    c1: column1
    c2: column2
    c3: column3
  join tableB b
    a.c1 = b.c1
  ljoin tableC c
    c2
  column
    a.c1 a
    b.c2 b
    c.c3 c
```

ポイント

* エイリアス定義(c1,c2,c3)は複数のテーブルに跨って適用される
* 各種 join は全て一単語で表す(これもキーワードの補完をしやすくするため)
   * inner join => join
   * left join => ljoin
   * right join => rjoin
   * cross join => cjoin
   * full join => fjoin
   * など
* join の下の行に項目名だけを書いた場合はusing句として扱う

# group by 〜 order by 〜

```sql:AltSQL
select tableA a
  alias
    c1: column1
    c2: column2
    c3: column3
  column
    a.c1 a
    a.c2 b
    a.c3 c
    count(*) cnt
  group
    a.c1 a
    a.c2 b
    a.c3 c
  order
    a.c1 a
    a.c2 b
    a.c3 c
```

ポイント

* by は書かない(キーワードは基本1単語)
* column句と同じ別名を書いても良い(単に無視される。column句に書いた内容を単純にコピーペーストしたい)

例のように、column句と同じ場合は省略できるようなのも検討したい

# column句をwhere区の後にする理由

ところで、通常のSQLでは、select column table where group order の順ですが。
AltSQLではcolumnを後にしています。これには以下の意図があります

1. alias の参照など下から上を参照する形にしたい

    column が上にあるとその別名をwhere句で参照できそうに感じます。
1. column と group, order は近くにあって欲しい

    column を変更すると group, order も変更する必要があるので記述箇所は近い方が良い

1. 普通、where句やjoinの結合条件を先に考えて、何を出力するかは最後になる。

    通常、試行錯誤しながらSQLを書くときは

    ```sql:sql
    select
      *
    from tableA
    where ...
    ```

    と、とりあえず、選択リストを `*` にしておいて、where句や結合条件があっているかを確認しながら書きます。そうして、条件があっていることが確認できた後に、何を出力するかを決定します。そのような思考順序を反映しています。(私だけかも知れませんが)

# limit, for update, SQLヒント などの特殊用途な構文(草案)

これらの構文は以下のように meta 区の後にオプション指定として記述します。
(limit が meta なのか？は検討が必要)

```sql:AltSQL
select tableA
  meta
    limit 10
    for_update  -- 無理やり1語にするのかは要検討
    result_cache
    use_index index_name
```

ポイント

* 様々な予約語をmeta区内に閉じ込めることでDB製品毎に異なる予約語のバッティングを避ける
* 特定DB固有の機能を指定するなどの拡張を記述する余地を残す
*