In-flight processing

Row filtering

Replicate only the rows that match a condition, with worked examples of common filters, and see how changes apply when rows move in or out.

A row filter keeps only the rows that match a condition, such as one region or the active customers. Set it in the Keep rows where field of the Configure dialog, for one table or for All tables.

Before you begin

  • Every filtered table needs a primary key or a unique key on the source.
  • A SQL Server source needs ALLOW_SNAPSHOT_ISOLATION on. See Validation and limits.

Write a condition

  1. Open Configure for the target.
  2. Select a table, or All tables.
  3. Type the condition in Keep rows where (for All tables, Keep rows where (all tables)). A blank field keeps every row.
status = 'active'
region_id IN (1, 2, 3) AND deleted_at IS NULL

Use unqualified column names of the table. A row is kept only when the condition is true. When it is false or NULL (unknown, for example status = 'active' on a row whose status is NULL), the row isn't kept.

Examples

The examples are written in T-SQL for a SQL Server source and use a table dbo.Orders with these columns:

ColumnType
order_idint, primary key
statusvarchar(20)
regionnvarchar(10)
region_idint
prioritytinyint
quantityint
unit_pricedecimal(10,2)
discountdecimal(10,2), nullable
skunvarchar(20)
warehouse_codechar(5)
is_testbit
is_deletedbit, nullable
order_date_keyint, such as 20240501
shipped_at, deleted_atdatetime2, nullable

Match one value

status = 'shipped'
region_id = 3

Ranges

Write a range as two comparisons. This keeps the orders with 10 to 99 items:

quantity >= 10 AND quantity < 100

Arithmetic works on numbers. This keeps the orders worth at least 1,000:

quantity * unit_price >= 1000

A number compared with a decimal column must fit the column's precision and scale. On the decimal(10,2) column unit_price, unit_price > 9.995 is refused with processing_source_predicate_unsupported.

Lists of values

region_id IN (1, 2, 3)
region_id NOT IN (4, 5)

A list holds 1 to 128 literals. Don't put NULL in a NOT IN list: it makes every row that isn't in the list unknown, so no row is kept.

Missing values

Keep the rows that aren't soft-deleted, or the orders that haven't shipped yet. IS NULL and IS NOT NULL work on a column of any type except binary:

deleted_at IS NULL
shipped_at IS NULL

A comparison with a NULL value is unknown, so status <> 'closed' drops the rows whose status is NULL. Add the NULL case to keep them:

status <> 'closed' OR status IS NULL
statusstatus <> 'closed'status <> 'closed' OR status IS NULL
'open'KeptKept
'closed'Not keptNot kept
NULLNot keptKept

COALESCE replaces a NULL before the comparison. This keeps the orders with a discount, and treats a missing discount as 0:

COALESCE(discount, 0) > 0

Boolean columns

A bit column is a condition on its own. Keep the real orders and drop the test orders:

NOT is_test

A NULL in a bit column is unknown, so NOT is_deleted drops it. Keep the rows where is_deleted is false or NULL:

is_deleted IS NULL OR NOT is_deleted

Don't compare a bit column with a number: is_test = 0 is refused with incompatible-expression-types.

Text matching

Text compares exactly, as described in Text comparisons are exact. To match several spellings, list them:

status IN ('active', 'Active', 'ACTIVE')

A char(n) column holds its value padded with spaces to n characters, so warehouse_code = 'LON' never matches the char(5) value 'LON '. Remove the spaces first:

TRIM(warehouse_code) = 'LON'

There is no LIKE. To keep the values that start with a prefix, compare with the prefix and with the prefix whose last character is the next one. This keeps every sku that starts with AB, such as 'AB-100', but not 'ab-100' (lowercase letters sort after uppercase ones) or 'AC-200':

sku >= 'AB' AND sku < 'AC'

Date windows

Row conditions can't compare date and time values: shipped_at >= '2024-01-01' is refused with incompatible-expression-types, and a comparison with a cast date with processing_source_predicate_unsupported.

What works:

  • IS NULL and IS NOT NULL on a date column, as in shipped_at IS NOT NULL.

  • A range on a numeric column that carries the date. This keeps the orders of 2024:

    order_date_key >= 20240101 AND order_date_key < 20250101
  • A range on an increasing key, such as order_id >= 5000000.

Combine conditions

AND binds tighter than OR, so use parentheses. This keeps the orders of regions 1 and 2 and every priority 1 order, when the order isn't a test order and isn't deleted:

(region_id IN (1, 2) OR priority = 1) AND NOT is_test AND deleted_at IS NULL

CASE sets a different threshold per value. This keeps the EU orders worth at least 100 and the other orders worth at least 500:

quantity * unit_price >= CASE region WHEN 'EU' THEN 100 ELSE 500 END

Conditions for all tables

Under All tables, a condition filters every table that has all the columns it names, joined with AND to the table's own condition. A tenant filter is a typical example:

tenant_id IN (1, 2, 3)

With that condition under All tables and status = 'active' on dbo.Orders:

TableColumnsRows kept
dbo.OrdersHas tenant_id and statustenant_id IN (1, 2, 3) AND status = 'active'
dbo.CustomersHas tenant_idtenant_id IN (1, 2, 3)
dbo.CurrenciesNo tenant_idEvery row

A table that has the columns with a different type isn't skipped: if tenant_id is a text column in one table, the start is refused with incompatible-expression-types.

Conditions that are refused

A condition that SQL Flow can't apply exactly is refused before the start. Test rule shows the code, and the start reports it followed by where it is, such as incompatible-expression-types at expression.

Besides the refusals in the examples above:

ConditionWhyRefusal
status = 42Text is compared with a number.incompatible-expression-types
LOWER(status) = 'active'LOWER and UPPER on a column aren't allowed in conditions.processing_source_predicate_unsupported
CAST(region_id AS VARCHAR(10)) = '3'An int can need 11 characters. VARCHAR(11) is accepted.processing_source_predicate_unsupported
PARTIAL_MASK(sku, 2, 0) = 'AB****'Masks aren't allowed in conditions.processing_source_predicate_unsupported
HASH_MASK(sku, 'customer') = 'a1b2'Conditions read the real values, never a keyed hash.mask-in-predicate
quantityA number isn't true or false.boolean-type
region_id IN (home_region_id, 2)A list holds literals only.in-literal-required
region_id IN (SELECT region_id FROM dbo.Regions)A list can't be a query.unsupported-in-list
o.status = 'active'Column names are unqualified.qualified-column
quantity / 2 > 10There is no division.unsupported-operator

LIKE, BETWEEN and functions that aren't in the Expression reference, such as ISNULL or YEAR, are refused too.

How each change is applied

SQL Flow checks the condition on the row as it is after each change, then writes the target by key.

Source changeThe new row matchesThe new row doesn't match
InsertThe row is written.Nothing is kept. Any target row with that key is deleted.
UpdateThe whole row is written. A row that moves into the filter appears at the target.The row is deleted at the target. A row that moves out of the filter disappears.
Update that changes the keyThe row under the old key is deleted, then the new row is written.The row under the old key is deleted.
DeleteThe row is deleted at the target. Deleting a row that was never kept has no effect.Same
TRUNCATE TABLE, where the capture mode captures itThe target table is emptied.Same

The target therefore always holds exactly the source rows that currently match. For example, with the condition status = 'active':

Source changeTarget
Insert order 7 with status = 'draft'No row 7
Update order 7 to status = 'active'Row 7 appears
Update order 7 to status = 'closed'Row 7 is deleted
Delete order 7Nothing changes

The initial load

The initial load filters rows in the query that reads the source table, so every condition must translate into the source database's SQL. That is why conditions accept fewer forms than output columns. A condition that can't run in the source query is refused before the start with processing_source_predicate_unsupported.

In a conditionAccepted
Comparisons and IN lists on numbers and textYes
IS NULL, IS NOT NULL on a columnYes
AND, OR, NOT, CASE, COALESCEYes
+, -, * on numbers, with results of up to 38 digitsYes
CONCAT, TRIMYes
A Boolean (bit) column used as a conditionYes
A CAST that holds every value of its input, such as CAST(region_id AS BIGINT) or CAST(region_id AS VARCHAR(11))Yes
A CAST that could lose digits, such as CAST(unit_price AS INT)No
LOWER, UPPER, masksNo
Comparisons on date and time, float, real or uniqueidentifier columnsNo

A text column in a condition needs a declared length of at most 349,525 characters, so (max), text and ntext columns can't be compared. On SQL Server, a varchar or char column in a condition needs a Latin1_General, SQL_Latin1_General_CP1 or UTF-8 (_UTF8) collation; nvarchar and nchar columns work with any collation.

Text comparisons are exact

Text compares as binary UTF-16, whatever the column's collation. Comparisons are case-sensitive and accent-sensitive, and trailing spaces count: status = 'active' doesn't match 'Active' or 'active '. The initial load compares the same way, so the load and streaming always agree.

When a filtered table has foreign keys to or from other selected tables, SQL Flow keeps the target referentially intact:

  • A parent table without its own condition receives the rows that the kept child rows reference. If another selected child table copies every row, the parent keeps every row too.
  • A parent table with its own condition must keep every row that its kept children reference. Where SQL Flow can prove a mismatch before the start, it refuses it with processing_filter_foreign_key_closure_unproved.

For example, with region_id IN (1, 2) on dbo.Orders and no condition on its parent dbo.Customers, the target receives the customers that the kept orders reference.

These relationships must meet the following conditions, or the start is refused with the code shown:

RequirementRefusal
Both tables of every related foreign key are selected, the key is enabled and trusted, and it has no ON DELETE or ON UPDATE action.processing_filter_foreign_key_relationship_unsupported
The child and parent key columns have the same type, and the parent columns are NOT NULL.processing_filter_foreign_key_type_unsupported
The related tables form no cycle.processing_filter_foreign_key_cycle_unsupported
On SQL Server, the source login has VIEW DEFINITION on the database, to read the foreign keys.processing_fk_catalog_view_definition_required
On Oracle, the source login has SELECT ANY DICTIONARY.processing_fk_catalog_dictionary_visibility_required

Check how many rows match

In the wizard, select Test rule under the table's rule. The result lists, for each table, whether the rule applies and which columns it filters on. It warns when a filtered column has no index that starts with it: "Some filtering columns have no usable leading index. Testing may scan source rows."

To count matches, choose Sample rows (1 to 100,000 rows per table) or Exact full scan, then select Test ALL matches. On SQL Server, counting needs READ_COMMITTED_SNAPSHOT or ALLOW_SNAPSHOT_ISOLATION on the source database, because the count never takes locks.

See also

Previous
Overview

If this was useful,
our newsletter will be too.

Get monthly insights on data engineering, AI, and building critical infrastructure - direct from the Spectral Core team and CEO Damir Bulic.