RustのnomでCREATE TABLEをパースする

# 目的
Rustのパーサーコンビネーターnomを使えばCREATE TABLEくらいなら簡単にパースできるという紹介です。正規表現では太刀打ちできない文字列の分析ではnomを使うと良いと思います。

# コード
## 対象
```sql
CREATE TABLE users (
    uuid UUID NOT NULL PRIMARY KEY
    ,name TEXT NOT NULL DEFAULT ''
)
```

## ゴール
以下のstructを完成させます。

```rust
#[derive(Debug)]
struct Table {
    name: String,
    columns: Vec<TableColumn>,
}

#[derive(Debug)]
struct TableColumn {
    name: String,
    sql_type: String,
    primary_key_flag: bool,
    not_null_flag: bool,
    default_value: Option<String>,
}
```

## テーブル名
テーブル名のパースします。

```rust
fn is_ident(ch: char) -> bool {
    is_alphanumeric(ch as u8) || ch == '_' || ch == '-'
}

fn parse_table_header(input: &str) -> IResult<&str, &str> {
    map(
        tuple((
            multispace0,
            tag_no_case("create"),
            multispace1,
            tag_no_case("table"),
            multispace1,
            take_while1(is_ident),
        )),
        |(_, _, _, _, _, name)| name,
    )(input)
}
```
余計な空白の除きつつ予約語のCREATE TABLEを読み取ります。ほしいのはテーブル名だけなのでその先の文字列を読み取ります。is_identを使って空白や記号で無い単語を回収しています。

## カラム1つ
一つのカラムを読み取ります。カンマはここでは取り扱わないです。

```rust
fn parse_table_column(input: &str) -> IResult<&str, TableColumn> {
    let (input, (_, name, _, sql_type, not_null_flag, default_value, primary_key_flag)) =
        tuple((
            multispace0,
            take_while1(is_ident), // カラム名
            multispace0,
            take_while1(is_ident), // SQL型
            opt(tuple((
                multispace0,
                tag_no_case("not"),
                multispace1,
                tag_no_case("null"),
            ))),
            opt(tuple((
                multispace0,
                tag_no_case("default"),
                multispace1,
                delimited(tag("'"), take_while(is_ident), tag("'")),
            ))),
            opt(tuple((
                multispace0,
                tag_no_case("primary"),
                multispace1,
                tag_no_case("key"),
            ))),
        ))(input)?;
    Ok((
        input,
        TableColumn {
            name: name.to_owned(),
            sql_type: sql_type.to_owned(),
            not_null_flag: not_null_flag.is_some(),
            primary_key_flag: primary_key_flag.is_some(),
            default_value: default_value.map(|x| x.3.to_owned()),
        },
    ))
}
```
長いですが、やっていることはカラムの要素の出現順に待ち構るだけです。
NOT NULL, DEFAULT, PRIMARY KEYは出現したりしなかったりするのでoptで扱います。
デフォルト値は今回か簡便のため文字列しか来ない想定です。

## カラム複数
上記の関数を利用して複数のカラムをパースします。nomは定義した関数を簡単に利用できるのがうれしいです。ちなみにこのパーサーだと最後にカンマが残ってても有効になってしまうのが問題です。

```rust
fn parse_table_columns(input: &str) -> IResult<&str, Vec<TableColumn>> {
    many0(map(
        tuple((parse_table_column, opt(pair(multispace0, tag(","))))),
        |phrase| phrase.0,
    ))(input)
}
```

## テーブル本体
parse_table_headerとparse_table_columnsを使ってTable構造体を作ります。右丸括弧の前に空白がくる可能性があるのでmultispace0を入れておきます。

```rust
fn parse_table(input: &str) -> IResult<&str, Table> {
    map(
        tuple((
            parse_table_header,
            multispace0,
            delimited(tag("("), pair(parse_table_columns, multispace0), tag(")")),
        )),
        |(name, _, columns)| Table {
            name: name.to_owned(),
            columns: columns.0,
        },
    )(input)
}
```

### 全コード
最後に全部をまとめて動作するコードです。

```rust:Cargo.toml
[package]
name = "create_table"
version = "0.1.0"
edition = "2021"

[dependencies]
nom = "7.1.0"
```

```rust:main.rs
use nom::{
    bytes::complete::{tag, tag_no_case, take_while, take_while1},
    character::{
        complete::{multispace0, multispace1},
        is_alphanumeric,
    },
    combinator::{map, opt},
    multi::many0,
    sequence::{delimited, pair, tuple},
    IResult,
};

fn is_ident(ch: char) -> bool {
    is_alphanumeric(ch as u8) || ch == '_' || ch == '-'
}

#[derive(Debug)]
struct Table {
    name: String,
    columns: Vec<TableColumn>,
}

#[derive(Debug)]
struct TableColumn {
    name: String,
    sql_type: String,
    primary_key_flag: bool,
    not_null_flag: bool,
    default_value: Option<String>,
}

fn parse_table_header(input: &str) -> IResult<&str, &str> {
    map(
        tuple((
            multispace0,
            tag_no_case("create"),
            multispace1,
            tag_no_case("table"),
            multispace1,
            take_while1(is_ident),
        )),
        |(_, _, _, _, _, name)| name,
    )(input)
}

fn parse_table_column(input: &str) -> IResult<&str, TableColumn> {
    let (input, (_, name, _, sql_type, not_null_flag, default_value, primary_key_flag)) =
        tuple((
            multispace0,
            take_while1(is_ident),
            multispace0,
            take_while1(is_ident),
            opt(tuple((
                multispace0,
                tag_no_case("not"),
                multispace1,
                tag_no_case("null"),
            ))),
            opt(tuple((
                multispace0,
                tag_no_case("default"),
                multispace1,
                delimited(tag("'"), take_while(is_ident), tag("'")),
            ))),
            opt(tuple((
                multispace0,
                tag_no_case("primary"),
                multispace1,
                tag_no_case("key"),
            ))),
        ))(input)?;
    Ok((
        input,
        TableColumn {
            name: name.to_owned(),
            sql_type: sql_type.to_owned(),
            not_null_flag: not_null_flag.is_some(),
            primary_key_flag: primary_key_flag.is_some(),
            default_value: default_value.map(|x| x.3.to_owned()),
        },
    ))
}

fn parse_table_columns(input: &str) -> IResult<&str, Vec<TableColumn>> {
    many0(map(
        tuple((parse_table_column, opt(pair(multispace0, tag(","))))),
        |phrase| phrase.0,
    ))(input)
}

fn parse_table(input: &str) -> IResult<&str, Table> {
    map(
        tuple((
            parse_table_header,
            multispace0,
            delimited(tag("("), pair(parse_table_columns, multispace0), tag(")")),
        )),
        |(name, _, columns)| Table {
            name: name.to_owned(),
            columns: columns.0,
        },
    )(input)
}

fn main() {
    let src = r#"
        CREATE TABLE users (
            uuid UUID NOT NULL PRIMARY KEY
            ,name TEXT NOT NULL DEFAULT ''
        )
    "#;
    let (input, res) = parse_table(src).unwrap();
    println!("{:?}", res);
    println!("{:?}", input);
}
```