Skip to main content

mz_storage/source/sql_server/
replication.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Code to render the ingestion dataflow of a [`SqlServerSourceConnection`].
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::rc::Rc;
14use std::sync::Arc;
15use std::time::Instant;
16
17use differential_dataflow::AsCollection;
18use futures::StreamExt;
19use itertools::Itertools;
20use mz_ore::cast::CastFrom;
21use mz_ore::collections::HashMap;
22use mz_ore::future::InTask;
23use mz_repr::{Diff, GlobalId, Row, RowArena};
24use mz_sql_server_util::SqlServerCdcMetrics;
25use mz_sql_server_util::cdc::{CdcEvent, Lsn, Operation as CdcOperation};
26use mz_sql_server_util::desc::SqlServerRowDecoder;
27use mz_sql_server_util::inspect::{
28    ensure_database_cdc_enabled, ensure_sql_server_agent_running, get_latest_restore_history_id,
29};
30use mz_storage_types::dyncfgs::SQL_SERVER_SOURCE_VALIDATE_RESTORE_HISTORY;
31use mz_storage_types::errors::{DataflowError, DecodeError, DecodeErrorKind};
32use mz_storage_types::sources::SqlServerSourceConnection;
33use mz_storage_types::sources::sql_server::{MAX_LSN_WAIT, SNAPSHOT_PROGRESS_REPORT_INTERVAL};
34use mz_timely_util::builder_async::{
35    AsyncOutputHandle, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
36};
37use mz_timely_util::containers::stack::FueledBuilder;
38use timely::container::CapacityContainerBuilder;
39use timely::dataflow::operators::vec::Map;
40use timely::dataflow::operators::{CapabilitySet, Concat};
41use timely::dataflow::{Scope, StreamVec};
42use timely::progress::{Antichain, Timestamp};
43
44use crate::metrics::source::sql_server::SqlServerSourceMetrics;
45use crate::source::RawSourceCreationConfig;
46use crate::source::sql_server::{
47    DefiniteError, ReplicationError, SourceOutputInfo, TransientError,
48};
49use crate::source::types::{FuelSize, SignaledFuture, SourceMessage, StackedCollection};
50
51/// Used as a partition ID to determine the worker that is responsible for
52/// reading data from SQL Server.
53///
54/// TODO(sql_server2): It's possible we could have different workers
55/// replicate different tables, if we're using SQL Server's CDC features.
56static REPL_READER: &str = "reader";
57
58pub(crate) fn render<'scope>(
59    scope: Scope<'scope, Lsn>,
60    config: RawSourceCreationConfig,
61    outputs: BTreeMap<GlobalId, SourceOutputInfo>,
62    source: SqlServerSourceConnection,
63    metrics: SqlServerSourceMetrics,
64) -> (
65    StackedCollection<'scope, Lsn, (u64, Result<SourceMessage, DataflowError>)>,
66    StreamVec<'scope, Lsn, ReplicationError>,
67    PressOnDropButton,
68) {
69    let op_name = format!("SqlServerReplicationReader({})", config.id);
70    let mut builder = AsyncOperatorBuilder::new(op_name, scope);
71
72    let (data_output, data_stream) = builder.new_output::<FueledBuilder<_>>();
73
74    // Captures DefiniteErrors that affect the entire source, including all outputs
75    let (definite_error_handle, definite_errors) =
76        builder.new_output::<CapacityContainerBuilder<_>>();
77
78    let (button, transient_errors) = builder.build_fallible(move |caps| {
79        let busy_signal = Arc::clone(&config.busy_signal);
80        Box::pin(SignaledFuture::new(busy_signal, async move {
81            let [
82                data_cap_set,
83                definite_error_cap_set,
84            ]: &mut [_; 2] = caps.try_into().unwrap();
85
86            let connection_config = source
87                .connection
88                .resolve_config(
89                    &config.config.connection_context.secrets_reader,
90                    &config.config,
91                    InTask::Yes,
92                )
93                .await?;
94            let mut client = mz_sql_server_util::Client::connect(connection_config).await?;
95
96            let worker_id = config.worker_id;
97
98            // The decoder is specific to the export, and each export pulls data from a specific capture instance.
99            let mut decoder_map: BTreeMap<_, _> = BTreeMap::new();
100            // Maps the 'capture instance' to the output index for only those outputs that this worker will snapshot
101            let mut capture_instance_to_snapshot: BTreeMap<Arc<str>, Vec<_>> = BTreeMap::new();
102            // Maps the 'capture instance' to the output index for all outputs of this worker
103            let mut capture_instances: BTreeMap<Arc<str>, Vec<_>> = BTreeMap::new();
104            // Export statistics for a given capture instance
105            let mut export_statistics: BTreeMap<_, Vec<_>> = BTreeMap::new();
106            // Maps the included columns for each output index so we can check
107            // whether schema updates are valid on a per-output basis
108            let mut included_columns: HashMap<u64, Vec<Arc<str>>> = HashMap::new();
109
110            for (export_id, output) in outputs.iter() {
111                let key = output.partition_index;
112                if decoder_map.insert(key, Arc::clone(&output.decoder)).is_some() {
113                    panic!("Multiple decoders for output index {}", output.partition_index);
114                }
115                // Collect the included columns from decoder for schema
116                // change validation. The decoder serves as an effective
117                // source of truth for which columns are "included", as we
118                // only care about the columns that are being decoded and
119                // replicated
120                let included_cols = output.decoder.included_column_names();
121                included_columns.insert(output.partition_index, included_cols);
122
123                capture_instances
124                    .entry(Arc::clone(&output.capture_instance))
125                    .or_default()
126                    .push(output.partition_index);
127
128                if *output.resume_upper == [Lsn::minimum()] {
129                    capture_instance_to_snapshot
130                        .entry(Arc::clone(&output.capture_instance))
131                        .or_default()
132                        .push((output.partition_index, output.initial_lsn));
133                }
134                export_statistics.entry(Arc::clone(&output.capture_instance))
135                    .or_default()
136                    .push(
137                        config
138                            .statistics
139                            .get(export_id)
140                            .expect("statistics have been intialized")
141                            .clone(),
142                    );
143            }
144
145            // Eagerly emit an event if we have tables to snapshot.
146            // A worker *must* emit a count even if not responsible for snapshotting a table
147            // as statistic summarization will return null if any worker hasn't set a value.
148            // This will also reset snapshot stats for any exports not snapshotting.
149            metrics.snapshot_table_count.set(u64::cast_from(capture_instance_to_snapshot.len()));
150            if !capture_instance_to_snapshot.is_empty() {
151                for stats in config.statistics.values() {
152                    stats.set_snapshot_records_known(0);
153                    stats.set_snapshot_records_staged(0);
154                }
155            }
156            // We need to emit statistics before we exit
157            // TODO(sql_server2): Run ingestions across multiple workers.
158            if !config.responsible_for(REPL_READER) {
159                return Ok::<_, TransientError>(());
160            }
161
162            let snapshot_instances = capture_instance_to_snapshot
163                    .keys()
164                    .map(|i| i.as_ref());
165
166            // TODO (maz): we can avoid this query by using SourceOutputInfo
167            let snapshot_tables =
168                mz_sql_server_util::inspect::get_tables_for_capture_instance(
169                    &mut client,
170                    snapshot_instances,
171                )
172                .await?;
173
174            // validate that the restore_history_id hasn't changed
175            let current_restore_history_id = get_latest_restore_history_id(&mut client).await?;
176            if current_restore_history_id != source.extras.restore_history_id {
177                if SQL_SERVER_SOURCE_VALIDATE_RESTORE_HISTORY.get(config.config.config_set()) {
178                    let definite_error = DefiniteError::RestoreHistoryChanged(
179                        source.extras.restore_history_id.clone(),
180                        current_restore_history_id.clone()
181                    );
182                    tracing::warn!(?definite_error, "Restore detected, exiting");
183
184                    return_definite_error(
185                            definite_error,
186                            capture_instances.values().flat_map(|indexes| indexes.iter().copied()),
187                            data_output,
188                            data_cap_set,
189                            definite_error_handle,
190                            definite_error_cap_set,
191                        ).await;
192                    return Ok(());
193                } else {
194                    tracing::warn!(
195                        "Restore history mismatch ignored: expected={expected:?} actual={actual:?}",
196                        expected=source.extras.restore_history_id,
197                        actual=current_restore_history_id
198                    );
199                }
200            }
201
202            // For AOAG, it's possible that the dataflow restarted and is now connected to a
203            // different SQL Server, which may not have CDC enabled correctly.
204            ensure_database_cdc_enabled(&mut client).await?;
205            ensure_sql_server_agent_running(&mut client).await?;
206
207            // We first calculate all the total rows we need to fetch across all tables. Since this
208            // happens outside the snapshot transaction the totals might be off, so we won't assert
209            // that we get exactly this many rows later.
210            for table in &snapshot_tables {
211                let qualified_table_name = format!("{schema_name}.{table_name}",
212                    schema_name = table.schema_name,
213                    table_name = table.name);
214                let size_calc_start = Instant::now();
215                let table_total =
216                    mz_sql_server_util::inspect::snapshot_size(
217                        &mut client,
218                        &table.schema_name,
219                        &table.name,
220                    )
221                    .await?;
222                metrics.set_snapshot_table_size_latency(
223                    &qualified_table_name,
224                    size_calc_start.elapsed().as_secs_f64()
225                );
226                for export_stat in export_statistics.get(&table.capture_instance.name).unwrap() {
227                    export_stat.set_snapshot_records_known(u64::cast_from(table_total));
228                    export_stat.set_snapshot_records_staged(0);
229                }
230            }
231            let cdc_metrics = PrometheusSqlServerCdcMetrics{inner: &metrics};
232            let mut cdc_handle = client
233                .cdc(capture_instances.keys().cloned(), cdc_metrics)
234                .max_lsn_wait(MAX_LSN_WAIT.get(config.config.config_set()));
235
236            // Snapshot any instance that requires it.
237            // Each table snapshot will have its own LSN captured at the moment of snapshotting.
238            let snapshot_lsns: BTreeMap<Arc<str>, Lsn> = {
239                // Before starting a transaction where the LSN will not advance, ensure
240                // the upstream DB is ready for CDC.
241                cdc_handle.wait_for_ready().await?;
242
243                // Intentionally logging this at info for debugging. This section won't get entered
244                // often, but if there are problems here, it will be much easier to troubleshoot
245                // knowing where stall/hang might be happening.
246                tracing::info!(%config.worker_id, "timely-{worker_id} upstream is ready");
247
248                let report_interval =
249                    SNAPSHOT_PROGRESS_REPORT_INTERVAL.handle(config.config.config_set());
250                let mut last_report = Instant::now();
251                let mut snapshot_lsns = BTreeMap::new();
252
253                for table in snapshot_tables {
254                    // TODO(sql_server3): filter columns to only select columns required for Source.
255                    let (snapshot_lsn, snapshot) = cdc_handle
256                        .snapshot(&table, config.worker_id, config.id)
257                        .await?;
258
259                    tracing::info!(
260                        %config.id,
261                        %table.name,
262                        %table.schema_name,
263                        %snapshot_lsn,
264                        "timely-{worker_id} snapshot start",
265                    );
266
267                    let mut snapshot = std::pin::pin!(snapshot);
268
269                    snapshot_lsns.insert(
270                        Arc::clone(&table.capture_instance.name),
271                        snapshot_lsn,
272                    );
273
274                    let ci_name = &table.capture_instance.name;
275                    let partition_indexes = capture_instance_to_snapshot
276                        .get(ci_name)
277                        .unwrap_or_else(|| {
278                            panic!(
279                                "no snapshot outputs in known capture \
280                                 instances [{}] for capture instance: \
281                                 '{}'",
282                                capture_instance_to_snapshot
283                                    .keys()
284                                    .join(","),
285                                ci_name,
286                            );
287                        });
288
289                    let mut snapshot_staged = 0;
290                    while let Some(result) = snapshot.next().await {
291                        let sql_server_row =
292                            result.map_err(TransientError::from)?;
293
294                        if last_report.elapsed() > report_interval.get() {
295                            last_report = Instant::now();
296                            let stats =
297                                export_statistics.get(ci_name).unwrap();
298                            for export_stat in stats {
299                                export_stat.set_snapshot_records_staged(
300                                    snapshot_staged,
301                                );
302                            }
303                        }
304
305                        for (partition_idx, _) in partition_indexes {
306                            // Decode the SQL Server row into an MZ one.
307                            let mut mz_row = Row::default();
308                            let arena = RowArena::default();
309
310                            let decoder = decoder_map
311                                .get(partition_idx)
312                                .expect("decoder for output");
313                            // Try to decode a row, returning a SourceError
314                            // if it fails.
315                            let message = decode(
316                                decoder,
317                                &sql_server_row,
318                                &mut mz_row,
319                                &arena,
320                                None,
321                            );
322                            let update =
323                                ((*partition_idx, message), Lsn::minimum(), Diff::ONE);
324                            let size = update.fuel_size();
325                            data_output
326                                .give_fueled(&data_cap_set[0], update, size)
327                                .await;
328                        }
329                        snapshot_staged += 1;
330                    }
331
332                    tracing::info!(
333                        %config.id,
334                        %table.name,
335                        %table.schema_name,
336                        %snapshot_lsn,
337                        "timely-{worker_id} snapshot complete",
338                    );
339                    metrics.snapshot_table_count.dec();
340                    // final update for snapshot_staged, using the staged
341                    // values as the total is an estimate
342                    let stats = export_statistics.get(ci_name).unwrap();
343                    for export_stat in stats {
344                        export_stat.set_snapshot_records_staged(snapshot_staged);
345                        export_stat.set_snapshot_records_known(snapshot_staged);
346                    }
347                }
348
349                snapshot_lsns
350            };
351
352            // Rewinds need to keep track of 2 timestamps to ensure that
353            // all replicas emit the same set of updates for any given timestamp.
354            // These are the initial_lsn and snapshot_lsn, where initial_lsn must be
355            // less than or equal to snapshot_lsn.
356            //
357            // - events at an LSN less than or equal to initial_lsn are ignored
358            // - events at an LSN greater than initial_lsn and less than or equal to
359            //   snapshot_lsn are retracted at Lsn::minimum(), and emitted at the commit_lsn
360            // - events at an LSN greater than snapshot_lsn are emitted at the commit_lsn
361            //
362            // where the commit_lsn is the upstream LSN that the event was committed at
363            //
364            // If initial_lsn == snapshot_lsn, all CDC events at LSNs up to and including the
365            // snapshot_lsn are ignored, and no rewinds are issued.
366            let mut rewinds: BTreeMap<_, _> = capture_instance_to_snapshot
367                .iter()
368                .flat_map(|(capture_instance, export_ids)|{
369                    let snapshot_lsn = snapshot_lsns.get(capture_instance).expect("snapshot lsn must be collected for capture instance");
370                    export_ids
371                        .iter()
372                        .map(|(idx, initial_lsn)| (*idx, (*initial_lsn, *snapshot_lsn)))
373                }).collect();
374
375            // For now, we assert that initial_lsn captured during purification is less
376            // than or equal to snapshot_lsn. If that was not true, it would mean that
377            // we observed a SQL server DB that appeared to go back in time.
378            // TODO (maz): not ideal to do this after snapshot, move this into
379            // CdcStream::snapshot after https://github.com/MaterializeInc/materialize/pull/32979 is merged.
380            for (initial_lsn, snapshot_lsn) in rewinds.values() {
381                assert!(
382                    initial_lsn <= snapshot_lsn,
383                    "initial_lsn={initial_lsn} snapshot_lsn={snapshot_lsn}"
384                );
385            }
386
387            tracing::debug!("rewinds to process: {rewinds:?}");
388
389            capture_instance_to_snapshot.clear();
390
391            // Resumption point is the minimum LSN that has been observed per capture instance.
392            let mut resume_lsns = BTreeMap::new();
393            for src_info in outputs.values() {
394                // initial_lsn is the max lsn observed, but the resume lsn
395                // is the next lsn that should be read.  After a snapshot, initial_lsn
396                // has been read, so replication will start at the next available lsn.
397                let resume_lsn = src_info.resume_lsn_or(src_info.initial_lsn.increment());
398                resume_lsns.entry(Arc::clone(&src_info.capture_instance))
399                    .and_modify(|existing| *existing = std::cmp::min(*existing, resume_lsn))
400                    .or_insert(resume_lsn);
401            }
402
403            tracing::info!(%config.id, ?resume_lsns, "timely-{} replication starting", config.worker_id);
404            for instance in capture_instances.keys() {
405                let resume_lsn = resume_lsns
406                    .get(instance)
407                    .expect("resume_lsn exists for capture instance");
408                cdc_handle = cdc_handle.start_lsn(instance, *resume_lsn);
409            }
410
411            // Off to the races! Replicate data from SQL Server.
412            let cdc_stream = cdc_handle
413                .poll_interval(config.timestamp_interval)
414                .into_stream();
415            let mut cdc_stream = std::pin::pin!(cdc_stream);
416
417            let mut errored_partitions = BTreeSet::new();
418
419            // TODO(sql_server2): We should emit `ProgressStatisticsUpdate::SteadyState` messages
420            // here, when we receive progress events. What stops us from doing this now is our
421            // 10-byte LSN doesn't fit into the 8-byte integer that the progress event uses.
422            let mut log_rewinds_complete = true;
423
424            // deferred_updates temporarily stores rows for UPDATE operation to support Large Object
425            // Data (LOD) types (i.e. varchar(max), nvarchar(max)). The value of a
426            // LOD column will be NULL for the old row (operation = 3) if the value of the
427            // field did not change. The field data will be available in the new row
428            // (operation = 4).
429            // The CDC stream implementation emits a [`CdcEvent::Data`] event, which contains a
430            // batch of operations.  There is no guarantee that both old and new rows will
431            // exist in a single batch, so deferred updates must be tracked across multiple data
432            // events.
433            //
434            // In the current implementation schema change events won't be emitted between old
435            // and new rows.
436            //
437            // See <https://learn.microsoft.com/en-us/sql/relational-databases/system-tables/cdc-capture-instance-ct-transact-sql?view=sql-server-ver17#large-object-data-types>
438            let mut deferred_updates = BTreeMap::new();
439
440            while let Some(event) = cdc_stream.next().await {
441                let event = event.map_err(TransientError::from)?;
442                tracing::trace!(?config.id, ?event, "got replication event");
443
444                tracing::trace!("deferred_updates = {deferred_updates:?}");
445                match event {
446                    // We've received all of the changes up-to this LSN, so
447                    // downgrade our capability.
448                    CdcEvent::Progress { next_lsn } => {
449                        tracing::debug!(?config.id, ?next_lsn, "got a closed lsn");
450                        // cannot downgrade capability until rewinds have been processed,
451                        // we must be able to produce data at the minimum offset.
452                        rewinds.retain(|_, (_, snapshot_lsn)| next_lsn <= *snapshot_lsn);
453                        if rewinds.is_empty() {
454                            if log_rewinds_complete {
455                                tracing::debug!("rewinds complete");
456                                log_rewinds_complete = false;
457                            }
458                            data_cap_set.downgrade(Antichain::from_elem(next_lsn));
459                        } else {
460                            tracing::debug!("rewinds remaining: {:?}", rewinds);
461                        }
462
463                        // Events are emitted in LSN order for a given capture instance, if any
464                        // deferred updates remain when the LSN progresses, it is a bug.
465                        if let Some(((deferred_lsn, _seqval), _row)) =
466                            deferred_updates.first_key_value()
467                            && *deferred_lsn < next_lsn
468                        {
469                            panic!(
470                                "deferred update lsn {deferred_lsn} \
471                                 < progress lsn {next_lsn}: {:?}",
472                                deferred_updates.keys()
473                            );
474                        }
475
476                    }
477                    // We've got new data! Let's process it.
478                    CdcEvent::Data {
479                        capture_instance,
480                        lsn,
481                        changes,
482                    } => {
483                        let Some(partition_indexes) =
484                            capture_instances.get(&capture_instance)
485                        else {
486                            let definite_error =
487                                DefiniteError::ProgrammingError(format!(
488                                    "capture instance didn't exist: \
489                                     '{capture_instance}'"
490                                ));
491                            return_definite_error(
492                                definite_error,
493                                capture_instances
494                                    .values()
495                                    .flat_map(|indexes| {
496                                        indexes.iter().copied()
497                                    }),
498                                data_output,
499                                data_cap_set,
500                                definite_error_handle,
501                                definite_error_cap_set,
502                            )
503                            .await;
504                            return Ok(());
505                        };
506
507                        let (valid_partitions, err_partitions) =
508                            partition_indexes
509                                .iter()
510                                .partition::<Vec<u64>, _>(
511                                    |&partition_idx| {
512                                        !errored_partitions
513                                            .contains(partition_idx)
514                                    },
515                                );
516
517                        if err_partitions.len() > 0 {
518                            metrics.ignored.inc_by(u64::cast_from(changes.len()));
519                        }
520
521                        handle_data_event(
522                            changes,
523                            &valid_partitions,
524                            &decoder_map,
525                            lsn,
526                            &rewinds,
527                            &data_output,
528                            data_cap_set,
529                            &metrics,
530                            &mut deferred_updates,
531                        ).await?
532                    },
533                    CdcEvent::SchemaUpdate {
534                        capture_instance,
535                        table,
536                        ddl_event,
537                    } => {
538                        let Some(partition_indexes) =
539                            capture_instances.get(&capture_instance)
540                        else {
541                            let definite_error =
542                                DefiniteError::ProgrammingError(format!(
543                                    "capture instance didn't exist: \
544                                     '{capture_instance}'"
545                                ));
546                            return_definite_error(
547                                definite_error,
548                                capture_instances
549                                    .values()
550                                    .flat_map(|indexes| {
551                                        indexes.iter().copied()
552                                    }),
553                                data_output,
554                                data_cap_set,
555                                definite_error_handle,
556                                definite_error_cap_set,
557                            )
558                            .await;
559                            return Ok(());
560                        };
561                        let error =
562                            DefiniteError::IncompatibleSchemaChange(
563                                capture_instance.to_string(),
564                                table.to_string(),
565                            );
566                        for partition_idx in partition_indexes {
567                            let cols = included_columns
568                                .get(partition_idx)
569                                .unwrap_or_else(|| {
570                                    panic!(
571                                        "Partition index didn't \
572                                         exist: '{partition_idx}'"
573                                    )
574                                });
575                            if !errored_partitions
576                                .contains(partition_idx)
577                                && !ddl_event.is_compatible(cols)
578                            {
579                                let msg = Err(
580                                    error.clone().into(),
581                                );
582                                let update = (
583                                    (*partition_idx, msg),
584                                    ddl_event.lsn,
585                                    Diff::ONE,
586                                );
587                                let size = update.fuel_size();
588                                data_output
589                                    .give_fueled(&data_cap_set[0], update, size)
590                                    .await;
591                                errored_partitions.insert(*partition_idx);
592                            }
593                        }
594                    }
595                };
596            }
597            Err(TransientError::ReplicationEOF)
598        }))
599    });
600
601    let error_stream = definite_errors.concat(transient_errors.map(ReplicationError::Transient));
602
603    (
604        data_stream.as_collection(),
605        error_stream,
606        button.press_on_drop(),
607    )
608}
609
610async fn handle_data_event(
611    changes: Vec<CdcOperation>,
612    partition_indexes: &[u64],
613    decoder_map: &BTreeMap<u64, Arc<SqlServerRowDecoder>>,
614    commit_lsn: Lsn,
615    rewinds: &BTreeMap<u64, (Lsn, Lsn)>,
616    data_output: &StackedAsyncOutputHandle<Lsn, (u64, Result<SourceMessage, DataflowError>)>,
617    data_cap_set: &CapabilitySet<Lsn>,
618    metrics: &SqlServerSourceMetrics,
619    deferred_updates: &mut BTreeMap<(Lsn, Lsn), CdcOperation>,
620) -> Result<(), TransientError> {
621    let mut mz_row = Row::default();
622    let arena = RowArena::default();
623
624    for change in changes {
625        // deferred_update is only valid for single iteration of the loop.  It is set once both
626        // old and new update rows are seen. It will be decoded and emitted to appropriate outputs.
627        // Its life now fullfilled, it will return to whence it came.
628        let mut deferred_update: Option<_> = None;
629        let (sql_server_row, diff): (_, _) = match change {
630            CdcOperation::Insert(sql_server_row) => {
631                metrics.inserts.inc();
632                (sql_server_row, Diff::ONE)
633            }
634            CdcOperation::Delete(sql_server_row) => {
635                metrics.deletes.inc();
636                (sql_server_row, Diff::MINUS_ONE)
637            }
638
639            // Updates are not ordered by seqval, so either old or new row could be observed first.
640            // The first update row is stashed, when the second arrives, both are processed.
641            CdcOperation::UpdateNew(seqval, sql_server_row) => {
642                // arbitrarily choosing to update metrics on the the new row
643                metrics.updates.inc();
644                deferred_update = deferred_updates.remove(&(commit_lsn, seqval));
645                if deferred_update.is_none() {
646                    tracing::trace!("capture deferred UpdateNew ({commit_lsn}, {seqval})");
647                    deferred_updates.insert(
648                        (commit_lsn, seqval),
649                        CdcOperation::UpdateNew(seqval, sql_server_row),
650                    );
651                    continue;
652                }
653                // this is overriden below when the updates are ordered
654                (sql_server_row, Diff::ZERO)
655            }
656            CdcOperation::UpdateOld(seqval, sql_server_row) => {
657                deferred_update = deferred_updates.remove(&(commit_lsn, seqval));
658                if deferred_update.is_none() {
659                    tracing::trace!("capture deferred UpdateOld ({commit_lsn}, {seqval})");
660                    deferred_updates.insert(
661                        (commit_lsn, seqval),
662                        CdcOperation::UpdateOld(seqval, sql_server_row),
663                    );
664                    continue;
665                }
666                // this is overriden below when the updates are ordered
667                (sql_server_row, Diff::ZERO)
668            }
669        };
670
671        // Try to decode the input row for each output.
672        for partition_idx in partition_indexes {
673            let decoder = decoder_map.get(partition_idx).unwrap();
674
675            let rewind = rewinds.get(partition_idx);
676            // We must continue here to avoid decoding and emitting. We don't have to compare with
677            // snapshot_lsn as we are guaranteed that initial_lsn <= snapshot_lsn.
678            if rewind.is_some_and(|(initial_lsn, _)| commit_lsn <= *initial_lsn) {
679                continue;
680            }
681
682            let (message, diff) = if let Some(ref deferred_update) = deferred_update {
683                let (old_row, new_row) = match deferred_update {
684                    CdcOperation::UpdateOld(_seqval, row) => (row, &sql_server_row),
685                    CdcOperation::UpdateNew(_seqval, row) => (&sql_server_row, row),
686                    CdcOperation::Insert(_) | CdcOperation::Delete(_) => unreachable!(),
687                };
688
689                let update_old = decode(decoder, old_row, &mut mz_row, &arena, Some(new_row));
690                if rewind.is_some_and(|(_, snapshot_lsn)| commit_lsn <= *snapshot_lsn) {
691                    let update = (
692                        (*partition_idx, update_old.clone()),
693                        Lsn::minimum(),
694                        Diff::ONE,
695                    );
696                    let size = update.fuel_size();
697                    data_output
698                        .give_fueled(&data_cap_set[0], update, size)
699                        .await;
700                }
701                let update = ((*partition_idx, update_old), commit_lsn, Diff::MINUS_ONE);
702                let size = update.fuel_size();
703                data_output
704                    .give_fueled(&data_cap_set[0], update, size)
705                    .await;
706
707                (
708                    decode(decoder, new_row, &mut mz_row, &arena, None),
709                    Diff::ONE,
710                )
711            } else {
712                (
713                    decode(decoder, &sql_server_row, &mut mz_row, &arena, None),
714                    diff,
715                )
716            };
717            assert_ne!(Diff::ZERO, diff);
718            if rewind.is_some_and(|(_, snapshot_lsn)| commit_lsn <= *snapshot_lsn) {
719                let update = ((*partition_idx, message.clone()), Lsn::minimum(), -diff);
720                let size = update.fuel_size();
721                data_output
722                    .give_fueled(&data_cap_set[0], update, size)
723                    .await;
724            }
725            let update = ((*partition_idx, message), commit_lsn, diff);
726            let size = update.fuel_size();
727            data_output
728                .give_fueled(&data_cap_set[0], update, size)
729                .await;
730        }
731    }
732    Ok(())
733}
734
735type StackedAsyncOutputHandle<T, D> =
736    AsyncOutputHandle<T, FueledBuilder<CapacityContainerBuilder<Vec<(D, T, Diff)>>>>;
737
738/// Helper method to decode a row from a [`tiberius::Row`] (or 2 of them in the case of update)
739/// to a [`Row`]. This centralizes the decode and mapping to result.
740fn decode(
741    decoder: &SqlServerRowDecoder,
742    row: &tiberius::Row,
743    mz_row: &mut Row,
744    arena: &RowArena,
745    new_row: Option<&tiberius::Row>,
746) -> Result<SourceMessage, DataflowError> {
747    match decoder.decode(row, mz_row, arena, new_row) {
748        Ok(()) => Ok(SourceMessage {
749            key: Row::default(),
750            value: mz_row.clone(),
751            metadata: Row::default(),
752        }),
753        Err(e) => {
754            let kind = DecodeErrorKind::Text(e.to_string().into());
755            // TODO(sql_server2): Get the raw bytes from `tiberius`.
756            let raw = format!("{row:?}");
757            Err(DataflowError::DecodeError(Box::new(DecodeError {
758                kind,
759                raw: raw.as_bytes().to_vec(),
760            })))
761        }
762    }
763}
764
765/// Helper method to return a "definite" error upstream.
766async fn return_definite_error(
767    err: DefiniteError,
768    outputs: impl Iterator<Item = u64>,
769    data_handle: StackedAsyncOutputHandle<Lsn, (u64, Result<SourceMessage, DataflowError>)>,
770    data_capset: &CapabilitySet<Lsn>,
771    errs_handle: AsyncOutputHandle<Lsn, CapacityContainerBuilder<Vec<ReplicationError>>>,
772    errs_capset: &CapabilitySet<Lsn>,
773) {
774    for output_idx in outputs {
775        let update = (
776            (output_idx, Err(err.clone().into())),
777            // Select an LSN that should not conflict with a previously observed LSN.  Ideally
778            // we could identify the LSN that resulted in the definite error so that all replicas
779            // would emit the same updates for the same times.
780            Lsn {
781                vlf_id: u32::MAX,
782                block_id: u32::MAX,
783                record_id: u16::MAX,
784            },
785            Diff::ONE,
786        );
787        let size = update.fuel_size();
788        data_handle.give_fueled(&data_capset[0], update, size).await;
789    }
790    errs_handle.give(
791        &errs_capset[0],
792        ReplicationError::DefiniteError(Rc::new(err)),
793    );
794}
795
796/// Provides an implemntation of [`SqlServerCdcMetrics`] that will update [`SqlServerSourceMetrics`]`
797struct PrometheusSqlServerCdcMetrics<'a> {
798    inner: &'a SqlServerSourceMetrics,
799}
800
801impl<'a> SqlServerCdcMetrics for PrometheusSqlServerCdcMetrics<'a> {
802    fn snapshot_table_lock_start(&self, table_name: &str) {
803        self.inner.update_snapshot_table_lock_count(table_name, 1);
804    }
805
806    fn snapshot_table_lock_end(&self, table_name: &str) {
807        self.inner.update_snapshot_table_lock_count(table_name, -1);
808    }
809}