Code review comment: Two main comments:  1) Let's filter out tables from the schema that are not tracked by change streams. We don't want to cache tables in the schema if they are not tracked by change streams.  2) Let's filter out columns from a change stream-tracked table if they are not tracked by change streams.  Three potential cases:  CREATE CHANGE STREAM changeStream FOR table(); // This statement will track only primary key columns. CREATE CHANGE STREAM changeStream FOR table(column1); this statement will track only primary key columns and column1. CREATE CHANGE STREAM changeStream FOR table; // This statement will track all columns

Code under review:
public PCollectionRowTuple expand(PCollectionRowTuple input) {
            Pipeline p = input.getPipeline();
            
            Schema tableChangesSchema = getTableSchema(configuration);
            SpannerIO.ReadChangeStream readChangeStream =
                SpannerIO.readChangeStream()
                    .withSpannerConfig(
                        SpannerConfig.create()
                            .withProjectId(configuration.getProjectId())
                            .withInstanceId(configuration.getInstanceId())
                            .withDatabaseId(configuration.getDatabaseId()))
                    .withChangeStreamName(configuration.getChangeStreamName())
                    .withInclusiveStartAt(
                        Timestamp.parseTimestamp(configuration.getStartAtTimestamp()))
                    .withDatabaseId(configuration.getDatabaseId())
                    .withProjectId(configuration.getProjectId())
                    .withInstanceId(configuration.getInstanceId());

            if (configuration.getEndAtTimestamp() != null) {
              String endTs =
                  Objects.requireNonNull(Objects.requireNonNull(configuration.getEndAtTimestamp()));
              readChangeStream =
                  readChangeStream.withInclusiveEndAt(Timestamp.parseTimestamp(endTs));
            }
            return PCollectionRowTuple.of(
                "output",
                p.apply(readChangeStream)
                    .apply(
                        ParDo.of(
                            new DataChangeRecordToRow(
                                configuration.getTable(), tableChangesSchema)))
                    .setRowSchema(tableChangesSchema));
          }

Line commented on: Schema tableChangesSchema = getTableSchema(configuration);

Revised code:
public PCollectionRowTuple expand(PCollectionRowTuple input) {
            Pipeline p = input.getPipeline();
            
            Schema tableChangesSchema = getTableSchema(configuration);
            SpannerIO.ReadChangeStream readChangeStream =
                SpannerIO.readChangeStream()
                    .withSpannerConfig(
                        SpannerConfig.create()
                            .withProjectId(configuration.getProjectId())
                            .withInstanceId(configuration.getInstanceId())
                            .withDatabaseId(configuration.getDatabaseId()))
                    .withChangeStreamName(configuration.getChangeStreamName())
                    .withInclusiveStartAt(
                        Timestamp.parseTimestamp(configuration.getStartAtTimestamp()))
                    .withDatabaseId(configuration.getDatabaseId())
                    .withProjectId(configuration.getProjectId())
                    .withInstanceId(configuration.getInstanceId());

            if (configuration.getEndAtTimestamp() != null) {
              String endTs =
                  Objects.requireNonNull(Objects.requireNonNull(configuration.getEndAtTimestamp()));
              readChangeStream =
                  readChangeStream.withInclusiveEndAt(Timestamp.parseTimestamp(endTs));
            }
            return PCollectionRowTuple.of(
                "output",
                p.apply(readChangeStream)
                    .apply(
                        ParDo.of(
                            new DataChangeRecordToRow(
                                configuration.getTable(), tableChangesSchema)))
                    .setRowSchema(tableChangesSchema));
          }