LoginSignup
1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-02

目的

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

コード

対象

CREATE TABLE users (
    uuid UUID NOT NULL PRIMARY KEY
    ,name TEXT NOT NULL DEFAULT ''
)

ゴール

以下のstructを完成させます。

#[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 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つ

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

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は定義した関数を簡単に利用できるのがうれしいです。ちなみにこのパーサーだと最後にカンマが残ってても有効になってしまうのが問題です。

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を入れておきます。

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)
}

全コード

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

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

[dependencies]
nom = "7.1.0"
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);
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0