目的
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)
}
全コード
最後に全部をまとめて動作するコードです。
[package]
name = "create_table"
version = "0.1.0"
edition = "2021"
[dependencies]
nom = "7.1.0"
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);
}