2828import org .apache .flink .cdc .common .annotation .Internal ;
2929import org .apache .flink .cdc .common .annotation .PublicEvolving ;
3030import org .apache .flink .cdc .common .annotation .VisibleForTesting ;
31+ import org .apache .flink .cdc .common .event .TableId ;
3132import org .apache .flink .cdc .common .lineage .LineageUtils ;
3233import org .apache .flink .cdc .connectors .mysql .MySqlValidator ;
3334import org .apache .flink .cdc .connectors .mysql .debezium .DebeziumUtils ;
5960import org .apache .flink .streaming .api .lineage .LineageVertexProvider ;
6061import org .apache .flink .util .FlinkRuntimeException ;
6162
63+ import io .debezium .connector .mysql .MySqlConnection ;
6264import io .debezium .jdbc .JdbcConnection ;
65+ import org .slf4j .Logger ;
66+ import org .slf4j .LoggerFactory ;
6367
6468import java .io .Serializable ;
6569import java .lang .reflect .Method ;
6670import java .util .ArrayList ;
71+ import java .util .HashMap ;
6772import java .util .LinkedHashMap ;
6873import java .util .List ;
6974import java .util .Map ;
7075import java .util .function .Supplier ;
76+ import java .util .stream .Collectors ;
7177
7278/**
7379 * The MySQL CDC Source based on FLIP-27 and Watermark Signal Algorithm which supports parallel
@@ -105,13 +111,15 @@ public class MySqlSource<T>
105111
106112 private static final long serialVersionUID = 1L ;
107113
114+ private static final Logger LOG = LoggerFactory .getLogger (MySqlSource .class );
115+
108116 private static final String ENUMERATOR_SERVER_NAME = "mysql_source_split_enumerator" ;
109117
110118 private final MySqlSourceConfigFactory configFactory ;
111119 private final DebeziumDeserializationSchema <T > deserializationSchema ;
112120 private final RecordEmitterSupplier <T > recordEmitterSupplier ;
113121 private final List <String > tableList ;
114- private final Map <String , LinkedHashMap <String , String >> tableSchemas ;
122+ private transient Map <String , LinkedHashMap <String , String >> fetchedLineageTableSchemas ;
115123
116124 // Actions to perform during the snapshot phase.
117125 // This field is introduced for testing purpose, for example testing if changes made in the
@@ -148,20 +156,18 @@ public static <T> MySqlSourceBuilder<T> builder() {
148156 MySqlSourceConfigFactory configFactory ,
149157 DebeziumDeserializationSchema <T > deserializationSchema ,
150158 RecordEmitterSupplier <T > recordEmitterSupplier ) {
151- this (configFactory , deserializationSchema , recordEmitterSupplier , null , null );
159+ this (configFactory , deserializationSchema , recordEmitterSupplier , null );
152160 }
153161
154162 MySqlSource (
155163 MySqlSourceConfigFactory configFactory ,
156164 DebeziumDeserializationSchema <T > deserializationSchema ,
157165 RecordEmitterSupplier <T > recordEmitterSupplier ,
158- List <String > tableList ,
159- Map <String , LinkedHashMap <String , String >> tableSchemas ) {
166+ List <String > tableList ) {
160167 this .configFactory = configFactory ;
161168 this .deserializationSchema = deserializationSchema ;
162169 this .recordEmitterSupplier = recordEmitterSupplier ;
163- this .tableList = tableList ;
164- this .tableSchemas = tableSchemas ;
170+ this .tableList = tableList == null ? null : new ArrayList <>(tableList );
165171 }
166172
167173 public MySqlSourceConfigFactory getConfigFactory () {
@@ -181,13 +187,80 @@ public Boundedness getBoundedness() {
181187 @ Override
182188 public LineageVertex getLineageVertex () {
183189 MySqlSourceConfig sourceConfig = configFactory .createConfig (0 );
190+
191+ /*
192+ * Note: Flink collects lineage during job graph construction, so this includes tables that
193+ * already exist when the job is submitted. If `scan.binlog.newly-added-table.enabled` later
194+ * captures newly created tables while the job is running, those tables appear in lineage
195+ * after the next submission.
196+ */
184197 return LineageUtils .sourceLineageVertex (
185198 "mysql" ,
186199 sourceConfig .getHostname (),
187200 sourceConfig .getPort (),
188201 sourceConfig .getStartupOptions ().isSnapshotOnly (),
189202 tableList ,
190- tableSchemas );
203+ fetchLineageTableSchemas (sourceConfig ));
204+ }
205+
206+ /** Fetch table schemas for lineage schema facets, only called when lineage is enabled. */
207+ private synchronized Map <String , LinkedHashMap <String , String >> fetchLineageTableSchemas (
208+ MySqlSourceConfig sourceConfig ) {
209+ if (fetchedLineageTableSchemas != null ) {
210+ return fetchedLineageTableSchemas ;
211+ }
212+ Map <String , LinkedHashMap <String , String >> schemas = new HashMap <>();
213+ if (tableList == null || tableList .isEmpty ()) {
214+ fetchedLineageTableSchemas = schemas ;
215+ return fetchedLineageTableSchemas ;
216+ }
217+ List <TableId > tableIds =
218+ tableList .stream ().map (TableId ::parse ).collect (Collectors .toList ());
219+ tableList .forEach (table -> schemas .put (table , new LinkedHashMap <>()));
220+ try (MySqlConnection jdbc = DebeziumUtils .createMySqlConnection (sourceConfig )) {
221+ jdbc .query (
222+ "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE "
223+ + "FROM information_schema.COLUMNS WHERE "
224+ + tableFilter (tableIds )
225+ + " ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION" ,
226+ rs -> {
227+ while (rs .next ()) {
228+ String table =
229+ TableId .tableId (
230+ rs .getString ("TABLE_SCHEMA" ),
231+ rs .getString ("TABLE_NAME" ))
232+ .toString ();
233+ LinkedHashMap <String , String > fields = schemas .get (table );
234+ if (fields != null ) {
235+ String type = rs .getString ("COLUMN_TYPE" );
236+ String nullable = rs .getString ("IS_NULLABLE" );
237+ fields .put (
238+ rs .getString ("COLUMN_NAME" ),
239+ "YES" .equals (nullable ) ? type : type + " NOT NULL" );
240+ }
241+ }
242+ });
243+ } catch (Exception e ) {
244+ LOG .warn ("Failed to fetch table schemas for lineage: {}" , e .getMessage ());
245+ }
246+ fetchedLineageTableSchemas = schemas ;
247+ return fetchedLineageTableSchemas ;
248+ }
249+
250+ private static String tableFilter (List <TableId > tableIds ) {
251+ return tableIds .stream ()
252+ .map (
253+ tableId ->
254+ "(TABLE_SCHEMA = "
255+ + quoteStringLiteral (tableId .getSchemaName ())
256+ + " AND TABLE_NAME = "
257+ + quoteStringLiteral (tableId .getTableName ())
258+ + ")" )
259+ .collect (Collectors .joining (" OR " ));
260+ }
261+
262+ private static String quoteStringLiteral (String literal ) {
263+ return "'" + literal .replace ("'" , "''" ) + "'" ;
191264 }
192265
193266 @ Override
0 commit comments