diff --git a/debezium-ddl-parser/src/main/java/io/debezium/antlr/ProxyParseTreeListener.java b/debezium-ddl-parser/src/main/java/io/debezium/antlr/ProxyParseTreeListener.java new file mode 100644 index 000000000..10b67f060 --- /dev/null +++ b/debezium-ddl-parser/src/main/java/io/debezium/antlr/ProxyParseTreeListener.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2012-2017 The ANTLR Project. All rights reserved. + * + * Licensed under the BSD 3-clause license, available at https://raw.githubusercontent.com/antlr/antlr4/master/LICENSE.txt + */ + +package io.debezium.antlr; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ErrorNode; +import org.antlr.v4.runtime.tree.ParseTreeListener; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Instances of this class allows multiple listeners to receive events + * while walking the parse tree. For example: + * + *
+ * ProxyParseTreeListener proxy = new ProxyParseTreeListener();
+ * ParseTreeListener listener1 = ... ;
+ * ParseTreeListener listener2 = ... ;
+ * proxy.add( listener1 );
+ * proxy.add( listener2 );
+ * ParseTreeWalker.DEFAULT.walk( proxy, ctx );
+ * 
+ */ +public class ProxyParseTreeListener implements ParseTreeListener { + private List listeners; + + /** + * Creates a new proxy without an empty list of listeners. Add + * listeners before walking the tree. + */ + public ProxyParseTreeListener() { + // Setting the listener to null automatically instantiates a new list. + this( null ); + } + + /** + * Creates a new proxy with the given list of listeners. + * + * @param listeners A list of listerners to receive events. + */ + public ProxyParseTreeListener( List listeners ) { + setListeners( listeners ); + } + + @Override + public void enterEveryRule( ParserRuleContext ctx ) { + for( ParseTreeListener listener : getListeners() ) { + listener.enterEveryRule( ctx ); + ctx.enterRule( listener ); + } + } + + @Override + public void exitEveryRule( ParserRuleContext ctx ) { + for( ParseTreeListener listener : getListeners() ) { + ctx.exitRule( listener ); + listener.exitEveryRule( ctx ); + } + } + + @Override + public void visitErrorNode( ErrorNode node ) { + for( ParseTreeListener listener : getListeners() ) { + listener.visitErrorNode( node ); + } + } + + @Override + public void visitTerminal( TerminalNode node ) { + for( ParseTreeListener listener : getListeners() ) { + listener.visitTerminal( node ); + } + } + + /** + * Adds the given listener to the list of event notification recipients. + * + * @param listener A listener to begin receiving events. + */ + public void add( ParseTreeListener listener ) { + getListeners().add( listener ); + } + + /** + * Removes the given listener to the list of event notification recipients. + * + * @param listener A listener to stop receiving events. + * @return false The listener was not registered to receive events. + */ + public boolean remove( ParseTreeListener listener ) { + return getListeners().remove( listener ); + } + + /** + * Returns the list of listeners. + * + * @return The list of listeners to receive tree walking events. + */ + private List getListeners() { + return this.listeners; + } + + /** + * Changes the list of listeners to receive events. If the given list of + * listeners is null, an empty list will be created. + * + * @param listeners A list of listeners to receive tree walking + * events. + */ + public void setListeners( List listeners ) { + if( listeners == null ) { + listeners = createParseTreeListenerList(); + } + + this.listeners = listeners; + } + + /** + * Creates a CopyOnWriteArrayList to permit concurrent mutative + * operations. + * + * @return A thread-safe, mutable list of event listeners. + */ + protected List createParseTreeListenerList() { + return new CopyOnWriteArrayList(); + } +} \ No newline at end of file diff --git a/debezium-ddl-parser/src/main/java/io/debezium/antlr/mysql/MySqlAntlrDdlParser.java b/debezium-ddl-parser/src/main/java/io/debezium/antlr/mysql/MySqlAntlrDdlParser.java index 61b399816..79646e17b 100644 --- a/debezium-ddl-parser/src/main/java/io/debezium/antlr/mysql/MySqlAntlrDdlParser.java +++ b/debezium-ddl-parser/src/main/java/io/debezium/antlr/mysql/MySqlAntlrDdlParser.java @@ -7,6 +7,7 @@ package io.debezium.antlr.mysql; import io.debezium.antlr.AntlrDdlParser; +import io.debezium.antlr.ProxyParseTreeListener; import io.debezium.ddl.parser.mysql.generated.MySqlLexer; import io.debezium.ddl.parser.mysql.generated.MySqlParser; import io.debezium.ddl.parser.mysql.generated.MySqlParserBaseListener; @@ -33,12 +34,22 @@ public class MySqlAntlrDdlParser extends AntlrDdlParser private Tables databaseTables; + private TableEditor tableEditor; + private ColumnEditor columnEditor; + @Override protected void parse(MySqlParser parser, Tables databaseTables) { this.databaseTables = databaseTables; MySqlParser.RootContext root = parser.root(); - ParseTreeWalker.DEFAULT.walk(new MySqlDdlParserListener(), root); + ProxyParseTreeListener proxyParseTreeListener = new ProxyParseTreeListener(); + proxyParseTreeListener.add(new CreateTableParserListener()); + proxyParseTreeListener.add(new DropTableParserListener()); + proxyParseTreeListener.add(new AlterTableParserListener()); + proxyParseTreeListener.add(new ColumnDefinitionParserListener()); + proxyParseTreeListener.add(new ExitSqlStatementParserListener()); + + ParseTreeWalker.DEFAULT.walk(proxyParseTreeListener, root); } @Override @@ -76,23 +87,79 @@ private String parseColumnName(MySqlParser.UidContext uidContext) { return uidContext.getText(); } + private void resolveColumnDataType(MySqlParser.DataTypeContext dataTypeContext) { + String dataTypeName; + int jdbcType = Types.NULL; + if (dataTypeContext instanceof MySqlParser.StringDataTypeContext) { + // CHAR | VARCHAR | TINYTEXT | TEXT | MEDIUMTEXT | LONGTEXT + MySqlParser.StringDataTypeContext stringDataTypeContext = (MySqlParser.StringDataTypeContext) dataTypeContext; + dataTypeName = stringDataTypeContext.typeName.getText(); + + if (stringDataTypeContext.lengthOneDimension() != null) { + Integer length = Integer.valueOf(stringDataTypeContext.lengthOneDimension().decimalLiteral().getText()); + columnEditor.length(length); + } + } else if (dataTypeContext instanceof MySqlParser.DimensionDataTypeContext) { + // TINYINT | SMALLINT | MEDIUMINT | INT | INTEGER | BIGINT + // REAL | DOUBLE | FLOAT + // DECIMAL | NUMERIC | DEC | FIXED + // BIT | TIME | TIMESTAMP | DATETIME | BINARY | VARBINARY | YEAR + MySqlParser.DimensionDataTypeContext dimensionDataTypeContext = (MySqlParser.DimensionDataTypeContext) dataTypeContext; + dataTypeName = dimensionDataTypeContext.typeName.getText(); + + Integer length = null; + Integer scale = null; + if (dimensionDataTypeContext.lengthOneDimension() != null) { + length = Integer.valueOf(dimensionDataTypeContext.lengthOneDimension().decimalLiteral().getText()); + } + + if (dimensionDataTypeContext.lengthTwoDimension() != null) { + List decimalLiterals = dimensionDataTypeContext.lengthTwoDimension().decimalLiteral(); + length = Integer.valueOf(decimalLiterals.get(0).getText()); + scale = Integer.valueOf(decimalLiterals.get(1).getText()); + } + + if (dimensionDataTypeContext.lengthTwoOptionalDimension() != null) { + List decimalLiterals = dimensionDataTypeContext.lengthTwoOptionalDimension().decimalLiteral(); + length = Integer.valueOf(decimalLiterals.get(0).getText()); + + if (decimalLiterals.size() > 1) { + scale = Integer.valueOf(decimalLiterals.get(1).getText()); + } + } + if (length != null) { + columnEditor.length(length); + } + if (scale != null) { + columnEditor.scale(scale); + } + // TODO: resolve jdbc type + } else if (dataTypeContext instanceof MySqlParser.SimpleDataTypeContext) { + // DATE | TINYBLOB | BLOB | MEDIUMBLOB | LONGBLOB | BOOL | BOOLEAN + dataTypeName = ((MySqlParser.SimpleDataTypeContext) dataTypeContext).typeName.getText(); + // TODO: resolve jdbc type + } else if (dataTypeContext instanceof MySqlParser.CollectionDataTypeContext) { + // ENUM | SET + // do not care about charsetName or collationName + dataTypeName = ((MySqlParser.CollectionDataTypeContext) dataTypeContext).typeName.getText(); + // TODO: resolve jdbc type + } else if (dataTypeContext instanceof MySqlParser.SpatialDataTypeContext) { + // GEOMETRYCOLLECTION | LINESTRING | MULTILINESTRING | MULTIPOINT | MULTIPOLYGON | POINT | POLYGON + dataTypeName = ((MySqlParser.SpatialDataTypeContext) dataTypeContext).typeName.getText(); + // TODO: resolve jdbc type + } else { + throw new IllegalStateException("Not recognized instance of data type context for " + dataTypeContext.getText()); + } + + columnEditor.type(dataTypeName); + columnEditor.jdbcType(jdbcType); + } + /** - * Parser listener for MySQL alter table queries. - * - * @author Roman Kuchár . + * Parser listener for MySQL create table queries. */ - // TODO: Do we want to split one big listener into a smaller ones? - // TODO: Can be used by some proxy listener described here: https://github.com/antlr/antlr4/issues/841 - private class MySqlDdlParserListener extends MySqlParserBaseListener { + private class CreateTableParserListener extends MySqlParserBaseListener { - private TableEditor tableEditor; - private ColumnEditor columnEditor; - private int parsingColumnIndex = 0; - private List columnEditors; - - /* - * START - Listening event for create table statements - */ @Override public void enterQueryCreateTable(MySqlParser.QueryCreateTableContext ctx) { TableId tableId = parseQualifiedTableId(ctx.tableName()); @@ -169,13 +236,13 @@ public void enterPrimaryKeyTableConstraint(MySqlParser.PrimaryKeyTableConstraint tableEditor.setPrimaryKeyNames(pkColumnNames); super.enterPrimaryKeyTableConstraint(ctx); } - /* - * END - Listening event for create table statements - */ + } + + /** + * Parser listener for MySQL drop table queries. + */ + private class DropTableParserListener extends MySqlParserBaseListener { - /* - * Drop table listener - */ @Override public void enterDropTable(MySqlParser.DropTableContext ctx) { Interval interval = new Interval(ctx.start.getStartIndex(), ctx.tables().start.getStartIndex() - 1); @@ -188,14 +255,21 @@ public void enterDropTable(MySqlParser.DropTableContext ctx) { }); super.enterDropTable(ctx); } + } + + /** + * Parser listener for MySQL alter table queries. + */ + private class AlterTableParserListener extends MySqlParserBaseListener { + + private static final int STARTING_INDEX = 1; + + private int parsingColumnIndex = STARTING_INDEX; + private List columnEditors; - /* - * START - Listening events for alter table statements - */ @Override public void enterAlterTable(MySqlParser.AlterTableContext ctx) { TableId tableId = parseQualifiedTableId(ctx.tableName()); - // TODO: should be table created if it does not exists in memory model? tableEditor = databaseTables.editOrCreateTable(tableId); super.enterAlterTable(ctx); } @@ -204,7 +278,6 @@ public void enterAlterTable(MySqlParser.AlterTableContext ctx) { public void enterAlterByAddColumn(MySqlParser.AlterByAddColumnContext ctx) { String columnName = parseColumnName(ctx.uid(0)); columnEditor = Column.editor().name(columnName); - // TODO: how can i set a column position and update other existing columns position? if (ctx.FIRST() != null) { //TODO: this new column should have the first position in table } else if (ctx.AFTER() != null) { @@ -222,6 +295,7 @@ public void enterAlterByAddColumns(MySqlParser.AlterByAddColumnsContext ctx) { String columnName = parseColumnName(uidContext); columnEditors.add(Column.editor().name(columnName)); } + columnEditor = columnEditors.get(0); super.enterAlterByAddColumns(ctx); } @@ -244,19 +318,32 @@ public void exitAlterTable(MySqlParser.AlterTableContext ctx) { debugParsed(ctx.getParent()); super.exitAlterTable(ctx); } - /* - * END - Listening events for alter table statements - */ - /* - * START - Listening events for column definition - */ @Override - public void enterColumnDefinition(MySqlParser.ColumnDefinitionContext ctx) { + public void exitColumnDefinition(MySqlParser.ColumnDefinitionContext ctx) { if (columnEditors != null) { // column editor list is not null when a multiple columns are parsed in one statement - columnEditor = columnEditors.get(parsingColumnIndex++); + if (columnEditors.size() > parsingColumnIndex) { + // assign next column editor to parse another column definition + columnEditor = columnEditors.get(parsingColumnIndex++); + } else { + // all columns parsed + // reset global variables for next parsed statement + columnEditors = null; + parsingColumnIndex = STARTING_INDEX; + } } + super.exitColumnDefinition(ctx); + } + } + + /** + * Parser listener for MySQL column definition queries. + */ + private class ColumnDefinitionParserListener extends MySqlParserBaseListener { + + @Override + public void enterColumnDefinition(MySqlParser.ColumnDefinitionContext ctx) { resolveColumnDataType(ctx.dataType()); super.enterColumnDefinition(ctx); } @@ -288,91 +375,20 @@ public void enterAutoIncrementColumnConstraint(MySqlParser.AutoIncrementColumnCo columnEditor.generated(true); super.enterAutoIncrementColumnConstraint(ctx); } - /* - * END - Listening events for column definition - */ + } + + /** + * Parser listener for MySQL alter table queries. + */ + private class ExitSqlStatementParserListener extends MySqlParserBaseListener { - /* - * Last caught event for sql statement - */ @Override public void exitSqlStatement(MySqlParser.SqlStatementContext ctx) { // reset global values for next statement that could be parsed with this instance tableEditor = null; columnEditor = null; - columnEditors = null; - parsingColumnIndex = 0; super.exitSqlStatement(ctx); } - - private void resolveColumnDataType(MySqlParser.DataTypeContext dataTypeContext) { - String dataTypeName; - int jdbcType = Types.NULL; - if (dataTypeContext instanceof MySqlParser.StringDataTypeContext) { - // CHAR | VARCHAR | TINYTEXT | TEXT | MEDIUMTEXT | LONGTEXT - MySqlParser.StringDataTypeContext stringDataTypeContext = (MySqlParser.StringDataTypeContext) dataTypeContext; - dataTypeName = stringDataTypeContext.typeName.getText(); - - if (stringDataTypeContext.lengthOneDimension() != null) { - Integer length = Integer.valueOf(stringDataTypeContext.lengthOneDimension().decimalLiteral().getText()); - columnEditor.length(length); - } - } else if (dataTypeContext instanceof MySqlParser.DimensionDataTypeContext) { - // TINYINT | SMALLINT | MEDIUMINT | INT | INTEGER | BIGINT - // REAL | DOUBLE | FLOAT - // DECIMAL | NUMERIC | DEC | FIXED - // BIT | TIME | TIMESTAMP | DATETIME | BINARY | VARBINARY | YEAR - MySqlParser.DimensionDataTypeContext dimensionDataTypeContext = (MySqlParser.DimensionDataTypeContext) dataTypeContext; - dataTypeName = dimensionDataTypeContext.typeName.getText(); - - Integer length = null; - Integer scale = null; - if (dimensionDataTypeContext.lengthOneDimension() != null) { - length = Integer.valueOf(dimensionDataTypeContext.lengthOneDimension().decimalLiteral().getText()); - } - - if (dimensionDataTypeContext.lengthTwoDimension() != null) { - List decimalLiterals = dimensionDataTypeContext.lengthTwoDimension().decimalLiteral(); - length = Integer.valueOf(decimalLiterals.get(0).getText()); - scale = Integer.valueOf(decimalLiterals.get(1).getText()); - } - - if (dimensionDataTypeContext.lengthTwoOptionalDimension() != null) { - List decimalLiterals = dimensionDataTypeContext.lengthTwoOptionalDimension().decimalLiteral(); - length = Integer.valueOf(decimalLiterals.get(0).getText()); - - if (decimalLiterals.size() > 1) { - scale = Integer.valueOf(decimalLiterals.get(1).getText()); - } - } - if (length != null) { - columnEditor.length(length); - } - if (scale != null) { - columnEditor.scale(scale); - } - // TODO: resolve jdbc type - } else if (dataTypeContext instanceof MySqlParser.SimpleDataTypeContext) { - // DATE | TINYBLOB | BLOB | MEDIUMBLOB | LONGBLOB | BOOL | BOOLEAN - dataTypeName = ((MySqlParser.SimpleDataTypeContext) dataTypeContext).typeName.getText(); - // TODO: resolve jdbc type - } else if (dataTypeContext instanceof MySqlParser.CollectionDataTypeContext) { - // ENUM | SET - // do not care about charsetName or collationName - dataTypeName = ((MySqlParser.CollectionDataTypeContext) dataTypeContext).typeName.getText(); - // TODO: resolve jdbc type - } else if (dataTypeContext instanceof MySqlParser.SpatialDataTypeContext) { - // GEOMETRYCOLLECTION | LINESTRING | MULTILINESTRING | MULTIPOINT | MULTIPOLYGON | POINT | POLYGON - dataTypeName = ((MySqlParser.SpatialDataTypeContext) dataTypeContext).typeName.getText(); - // TODO: resolve jdbc type - } else { - throw new IllegalStateException("Not recognized instance of data type context for " + dataTypeContext.getText()); - } - - columnEditor.type(dataTypeName); - columnEditor.jdbcType(jdbcType); - } - } }