/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Given a string shorthand for CREATE table, create a VoltTable
* that has the desired schema, and annotate it with enough metadata
* to do two things:
* 1) Create the "CREATE TABLE..." statement needed to create the table.
* 2) Enforce column widths for strings and varbinaries.
* Note that no SQL constraints like unique or not null will be enforced.
*
* Syntax is:
* [TABLENAME] (COLUMN1, COLUMN2, .. COLUMNN) [PK(PKEYCOL1, PKEYCOL2, .. PKEYCOLN)] [P(PARTITIONCOL)]
* Where COLUMNX is of the form:
* [NAME:]TYPE[SIZE]-[U][N][/'default value']
* And PKEYCOLX and PARTITIONCOL are either a column index or name.
*
* Unnamed tables are named "T". Unnamed columns are named "CX" where X is their 0-based-index.
* The U or N after the dash imply unique and not null respectively.
* Default values are provided by / followed by a single-quoted string with no way to escape.
* Unspecified lengths for VARCHAR or VARBINARY types default to 255.
*
* The simplest possible table would be something like:
* "(BIGINT)"
* Which would lead to "CREATE TABLE T (C0 BIGINT);"
*
* A more complex example might be:
* "FOO (BIGINT-N, BAR:TINYINT, A:VARCHAR12-U/'foo') PK(2,BAR) P(0)"
* Which creates:
* CREATE TABLE FOO (
* C0 BIGINT NOT NULL,
* BAR TINYINT,
* A VARCHAR(12) UNIQUE DEFAULT 'foo',
* PRIMARY KEY (A,BAR)
* );
* PARTITION TABLE FOO ON COLUMN C0;
*
* Test cases are in org.voltdb.TestTableHelper
*/
public class TableShorthand {
static Pattern m_namePattern;
static Pattern m_columnsPattern;
static Pattern m_pkeyPattern;
static Pattern m_partitionPattern;
static Pattern m_colTypePattern;
static Pattern m_colSizePattern;
static Pattern m_colMetaPattern;
static Pattern m_colDefaultPattern;
// init static regex patterns in a static block that prints stacks on failure
// otherwise it's very hard to debug static initializers when your regex is
// messed up
static {
try {
m_namePattern = Pattern.compile("^\\w*(?=\\s+\\()");
m_columnsPattern = Pattern.compile("(?<=\\()[^\\)]*(?=\\))");
m_pkeyPattern = Pattern.compile("(?<=PK\\()[^\\)]*(?=\\))");
m_partitionPattern = Pattern.compile("(?<=P\\()[^\\)]*(?=\\))");
m_colTypePattern = Pattern.compile("^[A-Za-z]*");
m_colSizePattern = Pattern.compile("(?<=[A-Za-z])\\d+");
m_colMetaPattern = Pattern.compile("-[A-Za-z]*");
m_colDefaultPattern = Pattern.compile("(?<=/[\\s*'])[^']*(?=')");
}
catch (Exception e) {
e.printStackTrace();
assert(false);
}
}
static VoltTable.ColumnInfo parseColumnShorthand(String colShorthand, int index) {
String name;
VoltType type = VoltType.BIGINT;
int size = 255;
boolean unique = false;
boolean nullable = true;
String defaultValue = VoltTable.ColumnInfo.NO_DEFAULT_VALUE; // stupid reserved work
try {
String[] parts = colShorthand.trim().split(":", 2);
if (parts.length > 2) throw new Exception();
// name
if (parts.length == 2) name = parts[0].trim();
else name = "C" + String.valueOf(index);
String rest = parts[parts.length - 1].trim();
// type (required)
Matcher typeMatcher = m_colTypePattern.matcher(rest);
typeMatcher.find();
type = VoltType.typeFromString(typeMatcher.group());
// size
Matcher sizeMatcher = m_colSizePattern.matcher(rest);
if (sizeMatcher.find()) {
String val = sizeMatcher.group();
if (val.length() > 0) {
size = Integer.parseInt(sizeMatcher.group());
}
}
// flags
Matcher metaMatcher = m_colMetaPattern.matcher(rest);
if (metaMatcher.find()) {
String meta = metaMatcher.group().toUpperCase();
if (meta.contains("N")) nullable = false;
if (meta.contains("U")) unique = true;
}
// default value
Matcher defaultMatcher = m_colDefaultPattern.matcher(rest);
if (defaultMatcher.find()) {
defaultValue = defaultMatcher.group();
}
}
catch (Exception e) {
String msg = String.format("Parse error while parsing column %d", index);
throw new RuntimeException(msg, e);
}
return new VoltTable.ColumnInfo(name,
type,
size,
nullable,
unique,
defaultValue);
}
/**
* Parse the shorthand according to the syntax as described
* in the class comment.
*/
public static VoltTable tableFromShorthand(String schema) {
String name = "T";
VoltTable.ColumnInfo[] columns = null;
// get a name
Matcher nameMatcher = m_namePattern.matcher(schema);
if (nameMatcher.find()) {
name = nameMatcher.group().trim();
}
// get the column schema
Matcher columnDataMatcher = m_columnsPattern.matcher(schema);
if (!columnDataMatcher.find()) {
throw new IllegalArgumentException("No column data found in shorthand");
}
String[] columnData = columnDataMatcher.group().trim().split("\\s*,\\s*");
int columnCount = columnData.length;
columns = new VoltTable.ColumnInfo[columnCount];
for (int i = 0; i < columnCount; i++) {
columns[i] = parseColumnShorthand(columnData[i], i);
}
// get the pkey
Matcher pkeyMatcher = m_pkeyPattern.matcher(schema);
int[] pkeyIndexes = new int[0]; // default no pkey
if (pkeyMatcher.find()) {
String[] pkeyColData = pkeyMatcher.group().trim().split("\\s*,\\s*");
pkeyIndexes = new int[pkeyColData.length];
for (int pkeyIndex = 0; pkeyIndex < pkeyColData.length; pkeyIndex++) {
String pkeyCol = pkeyColData[pkeyIndex];
// numeric means index of column
if (Character.isDigit(pkeyCol.charAt(0))) {
int colIndex = Integer.parseInt(pkeyCol);
pkeyIndexes[pkeyIndex] = colIndex;
}
else {
for (int colIndex = 0; colIndex < columnCount; colIndex++) {
if (columns[colIndex].name.equals(pkeyCol)) {
pkeyIndexes[pkeyIndex] = colIndex;
break;
}
}
}
}
}
// get any partitioning
Matcher partitionMatcher = m_partitionPattern.matcher(schema);
int partitionColumnIndex = -1; // default to replicated
if (partitionMatcher.find()) {
String partitionColStr = partitionMatcher.group().trim();
// numeric means index of column
if (Character.isDigit(partitionColStr.charAt(0))) {
partitionColumnIndex = Integer.parseInt(partitionColStr);
}
else {
for (int colIndex = 0; colIndex < columnCount; colIndex++) {
if (columns[colIndex].name.equals(partitionColStr)) {
partitionColumnIndex = colIndex;
break;
}
}
}
assert(partitionColumnIndex != -1) : "Regex match here means there is a partitioning column";
}
VoltTable table = new VoltTable(
new VoltTable.ExtraMetadata(name, partitionColumnIndex, pkeyIndexes, columns),
columns,
columns.length);
return table;
}
}