
io.debezium.relational.Tables Maven / Gradle / Ivy
/*
* Copyright Debezium Authors.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.relational;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.kafka.connect.data.Schema;
import io.debezium.annotation.ThreadSafe;
import io.debezium.function.Predicates;
import io.debezium.schema.DataCollectionFilters.DataCollectionFilter;
import io.debezium.schema.DatabaseSchema;
import io.debezium.util.Collect;
import io.debezium.util.FunctionalReadWriteLock;
/**
* Structural definitions for a set of tables in a JDBC database.
*
* @author Randall Hauch
*/
@ThreadSafe
public final class Tables {
/**
* A filter for tables.
*/
@FunctionalInterface
public interface TableFilter extends DataCollectionFilter {
/**
* Determines whether the given table should be included in the current {@link DatabaseSchema}.
*/
@Override
boolean isIncluded(TableId tableId);
/**
* Creates a {@link TableFilter} from the given predicate.
*/
public static TableFilter fromPredicate(Predicate predicate) {
return t -> predicate.test(t);
}
/**
* Creates a {@link TableFilter} that includes all tables.
*/
public static TableFilter includeAll() {
return t -> true;
}
}
public static class ColumnNameFilterFactory {
/**
* Build the {@link ColumnNameFilter} that determines whether a column identified by a given {@link ColumnId} is to be included,
* using the given comma-separated regular expression patterns defining which columns (if any) should be excluded.
*
* Note that this predicate is completely independent of the table selection predicate, so it is expected that this predicate
* be used only after the table selection predicate determined the table containing the column(s) is to be used.
*
* @param fullyQualifiedColumnNames the comma-separated list of fully-qualified column names to exclude; may be null or
* @return a column name filter; never null
*/
public static ColumnNameFilter createExcludeListFilter(String fullyQualifiedColumnNames, ColumnFilterMode columnFilterMode) {
Predicate delegate = Predicates.excludes(fullyQualifiedColumnNames, ColumnId::toString);
return (catalogName, schemaName, tableName, columnName) -> delegate
.test(new ColumnId(columnFilterMode.getTableIdForFilter(catalogName, schemaName, tableName), columnName));
}
/**
* Build the {@link ColumnNameFilter} that determines whether a column identified by a given {@link ColumnId} is to be included,
* using the given comma-separated regular expression patterns defining which columns (if any) should be included.
*
* Note that this predicate is completely independent of the table selection predicate, so it is expected that this predicate
* be used only after the table selection predicate determined the table containing the column(s) is to be used.
*
* @param fullyQualifiedColumnNames the comma-separated list of fully-qualified column names to include; may be null or
* @return a column name filter; never null
*/
public static ColumnNameFilter createIncludeListFilter(String fullyQualifiedColumnNames, ColumnFilterMode columnFilterMode) {
Predicate delegate = Predicates.includes(fullyQualifiedColumnNames, ColumnId::toString);
return (catalogName, schemaName, tableName, columnName) -> delegate
.test(new ColumnId(columnFilterMode.getTableIdForFilter(catalogName, schemaName, tableName), columnName));
}
}
/**
* A filter for columns.
*/
@FunctionalInterface
public interface ColumnNameFilter {
/**
* Determine whether the named column should be included in the table's {@link Schema} definition.
*
* @param catalogName the name of the database catalog that contains the table; may be null if the JDBC driver does not
* show a schema for this table
* @param schemaName the name of the database schema that contains the table; may be null if the JDBC driver does not
* show a schema for this table
* @param tableName the name of the table
* @param columnName the name of the column
* @return {@code true} if the table should be included, or {@code false} if the table should be excluded
*/
boolean matches(String catalogName, String schemaName, String tableName, String columnName);
}
private final FunctionalReadWriteLock lock = FunctionalReadWriteLock.reentrant();
private final TablesById tablesByTableId;
private final TableIds changes;
private final boolean tableIdCaseInsensitive;
/**
* Create an empty set of definitions.
*
* @param tableIdCaseInsensitive - true if lookup is case insensitive (typical for MySQL on Windows)
*/
public Tables(boolean tableIdCaseInsensitive) {
this.tableIdCaseInsensitive = tableIdCaseInsensitive;
this.tablesByTableId = new TablesById(tableIdCaseInsensitive);
this.changes = new TableIds(tableIdCaseInsensitive);
}
/**
* Create case sensitive empty set of definitions.
*/
public Tables() {
this(false);
}
protected Tables(Tables other, boolean tableIdCaseInsensitive) {
this(tableIdCaseInsensitive);
this.tablesByTableId.putAll(other.tablesByTableId);
}
public void clear() {
lock.write(() -> {
tablesByTableId.clear();
changes.clear();
});
}
@Override
public Tables clone() {
return new Tables(this, tableIdCaseInsensitive);
}
/**
* Get the number of tables that are in this object.
*
* @return the table count
*/
public int size() {
return lock.read(tablesByTableId::size);
}
public Set drainChanges() {
return lock.write(() -> {
Set result = changes.toSet();
changes.clear();
return result;
});
}
/**
* Add or update the definition for the identified table.
*
* @param tableId the identifier of the table
* @param columnDefs the list of column definitions; may not be null or empty
* @param primaryKeyColumnNames the list of the column names that make up the primary key; may be null or empty
* @param defaultCharsetName the name of the character set that should be used by default
* @return the previous table definition, or null if there was no prior table definition
*/
public Table overwriteTable(TableId tableId, List columnDefs, List primaryKeyColumnNames,
String defaultCharsetName) {
return lock.write(() -> {
Table updated = Table.editor()
.tableId(tableId)
.addColumns(columnDefs)
.setPrimaryKeyNames(primaryKeyColumnNames)
.setDefaultCharsetName(defaultCharsetName)
.create();
Table existing = tablesByTableId.get(tableId);
if (existing == null || !existing.equals(updated)) {
// Our understanding of the table has changed ...
changes.add(tableId);
tablesByTableId.put(tableId, updated);
}
return tablesByTableId.get(tableId);
});
}
/**
* Add or update the definition for the identified table.
*
* @param table the definition for the table; may not be null
* @return the previous table definition, or null if there was no prior table definition
*/
public Table overwriteTable(Table table) {
return lock.write(() -> {
TableImpl updated = new TableImpl(table);
try {
return tablesByTableId.put(updated.id(), updated);
}
finally {
changes.add(updated.id());
}
});
}
public void removeTablesForDatabase(String schemaName) {
removeTablesForDatabase(schemaName, null);
}
public void removeTablesForDatabase(String catalogName, String schemaName) {
lock.write(() -> {
tablesByTableId.entrySet().removeIf(tableIdTableEntry -> {
TableId tableId = tableIdTableEntry.getKey();
boolean equalCatalog = Objects.equals(catalogName, tableId.catalog());
boolean equalSchema = Objects.equals(schemaName, tableId.schema());
return equalSchema && equalCatalog;
});
});
}
/**
* Rename an existing table.
*
* @param existingTableId the identifier of the existing table to be renamed; may not be null
* @param newTableId the new identifier for the table; may not be null
* @return the previous table definition, or null if there was no prior table definition
*/
public Table renameTable(TableId existingTableId, TableId newTableId) {
return lock.write(() -> {
Table existing = forTable(existingTableId);
if (existing == null) {
return null;
}
tablesByTableId.remove(existing.id());
TableImpl updated = new TableImpl(newTableId, existing.columns(),
existing.primaryKeyColumnNames(), existing.defaultCharsetName());
try {
return tablesByTableId.put(updated.id(), updated);
}
finally {
changes.add(existingTableId);
changes.add(updated.id());
}
});
}
/**
* Add or update the definition for the identified table.
*
* @param tableId the identifier of the table
* @param changer the function that accepts the current {@link Table} and returns either the same or an updated
* {@link Table}; may not be null
* @return the previous table definition, or null if there was no prior table definition
*/
public Table updateTable(TableId tableId, Function changer) {
return lock.write(() -> {
Table existing = tablesByTableId.get(tableId);
Table updated = changer.apply(existing);
if (updated != existing) {
tablesByTableId.put(tableId, new TableImpl(tableId, updated.columns(),
updated.primaryKeyColumnNames(), updated.defaultCharsetName()));
}
changes.add(tableId);
return existing;
});
}
/**
* Remove the definition of the identified table.
*
* @param tableId the identifier of the table
* @return the existing table definition that was removed, or null if there was no prior table definition
*/
public Table removeTable(TableId tableId) {
return lock.write(() -> {
changes.add(tableId);
return tablesByTableId.remove(tableId);
});
}
/**
* Obtain the definition of the identified table.
*
* @param tableId the identifier of the table
* @return the table definition, or null if there was no definition for the identified table
*/
public Table forTable(TableId tableId) {
return lock.read(() -> tablesByTableId.get(tableId));
}
/**
* Obtain the definition of the identified table.
*
* @param catalogName the name of the database catalog that contains the table; may be null if the JDBC driver does not
* show a schema for this table
* @param schemaName the name of the database schema that contains the table; may be null if the JDBC driver does not
* show a schema for this table
* @param tableName the name of the table
* @return the table definition, or null if there was no definition for the identified table
*/
public Table forTable(String catalogName, String schemaName, String tableName) {
return forTable(new TableId(catalogName, schemaName, tableName));
}
/**
* Get the set of {@link TableId}s for which there is a {@link Schema}.
*
* @return the immutable set of table identifiers; never null
*/
public Set tableIds() {
return lock.read(() -> Collect.unmodifiableSet(tablesByTableId.ids()));
}
/**
* Obtain an editor for the table with the given ID. This method does not lock the set of table definitions, so use
* with caution. The resulting editor can be used to modify the table definition, but when completed the new {@link Table}
* needs to be added back to this object via {@link #overwriteTable(Table)}.
*
* @param tableId the identifier of the table
* @return the editor for the table, or null if there is no table with the specified ID
*/
public TableEditor editTable(TableId tableId) {
Table table = forTable(tableId);
return table == null ? null : table.edit();
}
/**
* Obtain an editor for the table with the given ID. This method does not lock or modify the set of table definitions, so use
* with caution. The resulting editor can be used to modify the table definition, but when completed the new {@link Table}
* needs to be added back to this object via {@link #overwriteTable(Table)}.
*
* @param tableId the identifier of the table
* @return the editor for the table, or null if there is no table with the specified ID
*/
public TableEditor editOrCreateTable(TableId tableId) {
Table table = forTable(tableId);
return table == null ? Table.editor().tableId(tableId) : table.edit();
}
@Override
public int hashCode() {
return tablesByTableId.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof Tables) {
Tables that = (Tables) obj;
return this.tablesByTableId.equals(that.tablesByTableId);
}
return false;
}
public Tables subset(TableFilter filter) {
if (filter == null) {
return this;
}
return lock.read(() -> {
Tables result = new Tables(tableIdCaseInsensitive);
tablesByTableId.forEach((tableId, table) -> {
if (filter.isIncluded(tableId)) {
result.overwriteTable(table);
}
});
return result;
});
}
@Override
public String toString() {
return lock.read(() -> {
StringBuilder sb = new StringBuilder();
sb.append("Tables {");
if (!tablesByTableId.isEmpty()) {
sb.append(System.lineSeparator());
tablesByTableId.forEach((tableId, table) -> {
sb.append(" ").append(tableId).append(": {").append(System.lineSeparator());
if (table instanceof TableImpl) {
((TableImpl) table).toString(sb, " ");
}
else {
sb.append(table.toString());
}
sb.append(" }").append(System.lineSeparator());
});
}
sb.append("}");
return sb.toString();
});
}
/**
* A map of tables by id. Table names are stored lower-case if required as per the config.
*/
private static class TablesById {
private final boolean tableIdCaseInsensitive;
private final ConcurrentMap values;
public TablesById(boolean tableIdCaseInsensitive) {
this.tableIdCaseInsensitive = tableIdCaseInsensitive;
this.values = new ConcurrentHashMap<>();
}
public Set ids() {
return values.keySet();
}
boolean isEmpty() {
return values.isEmpty();
}
public void putAll(TablesById tablesByTableId) {
if (tableIdCaseInsensitive) {
tablesByTableId.values.entrySet()
.forEach(e -> put(e.getKey().toLowercase(), e.getValue()));
}
else {
values.putAll(tablesByTableId.values);
}
}
public Table remove(TableId tableId) {
return values.remove(toLowerCaseIfNeeded(tableId));
}
public Table get(TableId tableId) {
return values.get(toLowerCaseIfNeeded(tableId));
}
public Table put(TableId tableId, Table updated) {
return values.put(toLowerCaseIfNeeded(tableId), updated);
}
int size() {
return values.size();
}
void forEach(BiConsumer super TableId, ? super Table> action) {
values.forEach(action);
}
Set> entrySet() {
return values.entrySet();
}
void clear() {
values.clear();
}
private TableId toLowerCaseIfNeeded(TableId tableId) {
return tableIdCaseInsensitive ? tableId.toLowercase() : tableId;
}
@Override
public int hashCode() {
return values.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TablesById other = (TablesById) obj;
return values.equals(other.values);
}
}
/**
* A set of table ids. Table names are stored lower-case if required as per the config.
*/
private static class TableIds {
private final boolean tableIdCaseInsensitive;
private final Set values;
public TableIds(boolean tableIdCaseInsensitive) {
this.tableIdCaseInsensitive = tableIdCaseInsensitive;
this.values = new HashSet<>();
}
public void add(TableId tableId) {
values.add(toLowerCaseIfNeeded(tableId));
}
public Set toSet() {
return new HashSet<>(values);
}
public void clear() {
values.clear();
}
private TableId toLowerCaseIfNeeded(TableId tableId) {
return tableIdCaseInsensitive ? tableId.toLowercase() : tableId;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy