package com.zendesk.maxwell.schema.ddl; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.antlr.v4.runtime.TokenStream; import org.antlr.v4.runtime.TokenStreamRewriter; import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.ErrorNode; import com.zendesk.maxwell.schema.columndef.ColumnDef; import com.zendesk.maxwell.schema.ddl.mysqlParser.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MysqlParserListener extends mysqlBaseListener { final Logger LOGGER = LoggerFactory.getLogger(MysqlParserListener.class); private String tableName; private final ArrayList<SchemaChange> schemaChanges; private final String currentDatabase; private final TokenStream tokenStream; private ColumnPosition columnPosition; public List<SchemaChange> getSchemaChanges() { return schemaChanges; } private final LinkedList<ColumnDef> columnDefs = new LinkedList<>(); private ArrayList<String> pkColumns; MysqlParserListener(String currentDatabase, TokenStream tokenStream) { this.pkColumns = null; // null indicates no change in primary keys this.schemaChanges = new ArrayList<>(); this.currentDatabase = currentDatabase; this.tokenStream = tokenStream; } private String unquote(String ident) { return ident.replaceFirst("^`", "").replaceFirst("`$", ""); } private String unquote_literal(String ident) { return unquote(ident.replaceAll("^'", "").replaceAll("'$", "")); } private String getDB(Table_nameContext t) { if ( t.db_name() != null ) return unquote(t.db_name().getText()); else return currentDatabase; } private String getTable(Table_nameContext t) { return unquote(t.name().getText()); } private TableAlter alterStatement() { return (TableAlter)schemaChanges.get(0); } private String getCharset(List<Column_optionsContext> list) { for ( Column_optionsContext ctx : list ) { if ( ctx.charset_def() != null ) { if ( ctx.charset_def().ASCII() != null ) { return "latin1"; } else { return unquote_literal(ctx.charset_def().character_set().charset_name().getText()); } } } return null; } @Override public void visitErrorNode(ErrorNode node) { this.schemaChanges.clear(); String error = node.getParent().toStringTree(new mysqlParser(null)); LOGGER.error(error); throw new MaxwellSQLSyntaxError(node.getText()); } private boolean isSigned(List<Int_flagsContext> flags) { for ( Int_flagsContext flag : flags ) { if ( flag.UNSIGNED() != null ) { return false; } } return true; } private ColumnPosition getColumnPosition() { // any time there's a possibility of a column position, we'll // want to clear it out so we don't re-use it next time. visitors might be better in this case. ColumnPosition p = this.columnPosition; this.columnPosition = null; if ( p == null ) return new ColumnPosition(); else return p; } @Override public void exitAlter_database(Alter_databaseContext ctx) { String dbName = unquote(ctx.name().getText()); DatabaseAlter alter = new DatabaseAlter(dbName); List<Default_character_setContext> charSet = ctx.alter_database_definition().default_character_set(); if ( charSet.size() > 0 ) { alter.charset = unquote_literal(charSet.get(0).charset_name().getText()); } this.schemaChanges.add(alter); } @Override public void exitAlter_table_preamble(Alter_table_preambleContext ctx) { String dbName = getDB(ctx.table_name()); String tableName = getTable(ctx.table_name()); TableAlter alterStatement = new TableAlter(dbName, tableName); this.tableName = alterStatement.table; this.schemaChanges.add(alterStatement); } // After we're done parsing the whole alter @Override public void exitAlter_table(mysqlParser.Alter_tableContext ctx) { alterStatement().pks = this.pkColumns; } @Override public void enterAlter_view(Alter_viewContext ctx) { throw new ParseCancellationException("Not finishing parse of ALTER VIEW"); } @Override public void enterCreate_view(Create_viewContext ctx) { throw new ParseCancellationException("Not finishing parse of CREATE VIEW"); } @Override public void exitAdd_column(Add_columnContext ctx) { ColumnDef c = this.columnDefs.removeFirst(); alterStatement().columnMods.add(new AddColumnMod(c.getName(), c, getColumnPosition())); } @Override public void exitAdd_column_parens(mysqlParser.Add_column_parensContext ctx) { while ( this.columnDefs.size() > 0 ) { ColumnDef c = this.columnDefs.removeFirst(); // unable to choose a position in this form alterStatement().columnMods.add(new AddColumnMod(c.getName(), c, new ColumnPosition())); } } @Override public void exitChange_column(mysqlParser.Change_columnContext ctx) { String oldColumnName = unquote(ctx.old_col_name().getText()); ColumnDef c = this.columnDefs.removeFirst(); alterStatement().columnMods.add(new ChangeColumnMod(oldColumnName, c, getColumnPosition())); } @Override public void exitModify_column(mysqlParser.Modify_columnContext ctx) { ColumnDef c = this.columnDefs.removeFirst(); alterStatement().columnMods.add(new ChangeColumnMod(c.getName(), c, getColumnPosition())); } @Override public void exitDrop_column(mysqlParser.Drop_columnContext ctx) { alterStatement().columnMods.add(new RemoveColumnMod(unquote(ctx.old_col_name().getText()))); } @Override public void exitCol_position(mysqlParser.Col_positionContext ctx) { this.columnPosition = new ColumnPosition(); if ( ctx.FIRST() != null ) { this.columnPosition.position = ColumnPosition.Position.FIRST; } else if ( ctx.AFTER() != null ) { this.columnPosition.position = ColumnPosition.Position.AFTER; this.columnPosition.afterColumn = unquote(ctx.name().getText()); } } @Override public void exitAlter_rename_table(Alter_rename_tableContext ctx) { alterStatement().newTableName = getTable(ctx.table_name()); alterStatement().newDatabase = getDB(ctx.table_name()); } @Override public void exitConvert_to_character_set(mysqlParser.Convert_to_character_setContext ctx) { alterStatement().convertCharset = unquote_literal(ctx.charset_name().getText()); } @Override public void exitDefault_character_set(mysqlParser.Default_character_setContext ctx) { // definitely hacky here; showing the fallacy of trying to mix and match listener // style parsing with more visitor-y stuff (in the exit nodes) if ( ctx.parent instanceof Alter_specificationContext ) alterStatement().defaultCharset = unquote_literal(ctx.charset_name().getText()); } @Override public void exitCreate_table_preamble(Create_table_preambleContext ctx) { String dbName = getDB(ctx.table_name()); String tblName = getTable(ctx.table_name()); boolean ifNotExists = ctx.if_not_exists() != null; TableCreate createStatement = new TableCreate(dbName, tblName, ifNotExists); this.tableName = createStatement.table; this.schemaChanges.add(createStatement); } @Override public void exitCreate_like_tbl(Create_like_tblContext ctx) { TableCreate tableCreate = (TableCreate) schemaChanges.get(0); tableCreate.likeDB = getDB(ctx.table_name()); tableCreate.likeTable = getTable(ctx.table_name()); } @Override public void exitCreate_specifications(Create_specificationsContext ctx) { TableCreate tableCreate = (TableCreate) schemaChanges.get(0); tableCreate.columns.addAll(this.columnDefs); tableCreate.pks = this.pkColumns; } @Override public void exitCreation_character_set(Creation_character_setContext ctx) { SchemaChange change = schemaChanges.get(0); /* due to an unfortunate duplication of syntaxes (DEFAULT CHARACTER SET and CHARACTER SET), * it's possible that a table alter will end up down this parse path (depending on how options are ordered) */ if ( change instanceof TableCreate ) { TableCreate tableCreate = (TableCreate) change; tableCreate.charset = unquote_literal(ctx.charset_name().getText()); } else if ( change instanceof TableAlter ) { ((TableAlter) change).defaultCharset = unquote_literal(ctx.charset_name().getText()); } } @Override public void exitDrop_table(mysqlParser.Drop_tableContext ctx) { boolean ifExists = ctx.if_exists() != null; for ( Table_nameContext t : ctx.table_name()) { schemaChanges.add(new TableDrop(getDB(t), getTable(t), ifExists)); } } @Override public void exitDrop_database(mysqlParser.Drop_databaseContext ctx) { boolean ifExists = ctx.if_exists() != null; String dbName = unquote(ctx.name().getText()); schemaChanges.add(new DatabaseDrop(dbName, ifExists)); } private String spliceParens(int startIndex, int initialParenCount) { TokenStreamRewriter r = new TokenStreamRewriter(tokenStream); int i = startIndex, parens = initialParenCount; for ( ; i < tokenStream.size(); i++ ) { String tokenText = tokenStream.get(i).getText(); if ( tokenText.equals("(") ) parens++; else if ( tokenText.equals(")") ) parens--; if ( parens == 0 ) break; } r.insertBefore(startIndex, "/__MAXWELL__/"); r.delete(startIndex, i); return r.getText(); } /* we enter this code twice. the first time, we gobble up parens. The second time, we just have the __MAXWELL__ token, and we can continue. */ @Override public void enterSkip_parens(Skip_parensContext ctx) { if ( ctx.MAXWELL_ELIDED_PARSE_ISSUE() == null ) throw new ReparseSQLException(spliceParens(ctx.getStart().getTokenIndex(), 0)); } @Override public void enterSkip_parens_inside_partition_definitions(Skip_parens_inside_partition_definitionsContext ctx) { if ( ctx.MAXWELL_ELIDED_PARSE_ISSUE() == null ) throw new ReparseSQLException(spliceParens(ctx.getStart().getTokenIndex(), 1)); } @Override public void exitIndex_type_pk(mysqlParser.Index_type_pkContext ctx) { this.pkColumns = new ArrayList<>(); for ( Index_columnContext column : ctx.index_column_list().index_columns().index_column() ) { NameContext n = column.name(); this.pkColumns.add(unquote(n.getText())); } } @Override public void exitDrop_primary_key(mysqlParser.Drop_primary_keyContext ctx) { this.pkColumns = new ArrayList<>(); } private Long extractColumnLength(LengthContext l) { if ( l == null ) return null; else return Long.valueOf(l.INTEGER_LITERAL().getText()); } @Override public void exitColumn_definition(mysqlParser.Column_definitionContext ctx) { Long columnLength = null; Boolean longStringFlag = false; String colType = null, colCharset = null; String[] enumValues = null; List<Column_optionsContext> colOptions = null; boolean signed = true; boolean byteFlagToStringColumn = false; String name = unquote(ctx.col_name.getText()); Data_typeContext dctx = ctx.data_type(); if ( dctx.generic_type() != null ) { colType = dctx.generic_type().col_type.getText(); colOptions = dctx.generic_type().column_options(); columnLength = extractColumnLength(dctx.generic_type().length()); } else if ( dctx.signed_type() != null ) { colType = dctx.signed_type().col_type.getText(); signed = isSigned(dctx.signed_type().int_flags()); colOptions = dctx.signed_type().column_options(); if ( colType.toLowerCase().equals("serial") ) signed = false; } else if ( dctx.string_type() != null ) { colType = dctx.string_type().col_type.getText(); colCharset = getCharset(dctx.string_type().column_options()); if ( dctx.string_type().utf8 ) // forced into UTF-8 by NATIONAL-fu colCharset = "utf8"; if ( dctx.string_type().BYTE().size() > 0 ) byteFlagToStringColumn = true; if ( dctx.string_type().UNICODE().size() > 0 ) colCharset = "ucs2"; columnLength = extractColumnLength(dctx.string_type().length()); colOptions = dctx.string_type().column_options(); longStringFlag = (dctx.string_type().long_flag() != null); } else if ( dctx.enumerated_type() != null ) { List<Enum_valueContext> valueList = dctx.enumerated_type().enumerated_values().enum_value(); colType = dctx.enumerated_type().col_type.getText(); colCharset = getCharset(dctx.enumerated_type().column_options()); colOptions = dctx.enumerated_type().column_options(); enumValues = new String[valueList.size()]; int i = 0; for ( Enum_valueContext v : valueList ) { enumValues[i++] = unquote_literal(v.getText()); } } colType = ColumnDef.unalias_type(colType.toLowerCase(), longStringFlag, columnLength, byteFlagToStringColumn); ColumnDef c; c = ColumnDef.build( name, colCharset, colType.toLowerCase(), -1, signed, enumValues, columnLength ); this.columnDefs.add(c); if ( colOptions != null ) { for (Column_optionsContext opt : colOptions) { if (opt.primary_key() != null) { this.pkColumns = new ArrayList<>(); this.pkColumns.add(name); } } } } @Override public void exitRename_table_spec(Rename_table_specContext ctx) { Table_nameContext oldTableContext = ctx.table_name(0); Table_nameContext newTableContext = ctx.table_name(1); TableAlter t = new TableAlter(getDB(oldTableContext), getTable(oldTableContext)); t.newDatabase = getDB(newTableContext); t.newTableName = getTable(newTableContext); this.schemaChanges.add(t); } @Override public void exitCreate_database(mysqlParser.Create_databaseContext ctx) { String dbName = unquote(ctx.name().getText()); boolean ifNotExists = ctx.if_not_exists() != null; String charset = null; if ( ctx.default_character_set().size() > 0 ) { charset = unquote_literal(ctx.default_character_set().get(0).charset_name().getText()); } this.schemaChanges.add(new DatabaseCreate(dbName, ifNotExists, charset)); } }