@@ -1587,42 +1587,137 @@ macro_rules! __scalar_matrix_scale_case {
15871587 ) ;
15881588
15891589 let values: & [ $scalar] = <$scalar as ScalarType >:: fixture_values( ) ;
1590- anyhow:: ensure!( values. len( ) >= 2 ,
1591- "scale test requires >= 2 fixture rows for distinct filler/pivot" ) ;
1592- let filler = values[ 0 ] . clone( ) ;
1593- let pivot = values[ values. len( ) / 2 ] . clone( ) ;
1594- let filler_payload =
1595- $crate:: scalar_domains:: fetch_fixture_payload:: <$scalar>( & pool, filler) . await ?;
1596- let pivot_payload =
1597- $crate:: scalar_domains:: fetch_fixture_payload:: <$scalar>( & pool, pivot) . await ?;
1590+ // Distinct, sorted fixture values so MIN / MID / MAX are well
1591+ // defined regardless of fixture order. ONE data shape serves
1592+ // every op-class a combo can carry — equality combos hold `=`;
1593+ // the ordered combos hold `=` plus `<`/`<=`/`>`/`>=`, all sharing
1594+ // a single extractor (so one functional index serves them):
1595+ //
1596+ // 5000 identical MID rows (the bulk) + ONE MIN row + ONE MAX
1597+ // row = 5002 rows.
1598+ //
1599+ // Each op then anchors its predicate so EXACTLY ONE row matches,
1600+ // making the predicate ~1/5002 selective and the functional index
1601+ // the cheap plan with `enable_seqscan` left ON (Fact 4). A single
1602+ // MIN-bulk table cannot do this for both range directions at once
1603+ // (`value > MIN` would match every non-MIN row); a MID bulk with
1604+ // one MIN and one MAX pivot makes every op single-row-selective:
1605+ // `=` anchor MIN -> the single MIN row (bulk is MID)
1606+ // `<` anchor MID -> the single MIN row (MID < MID is false)
1607+ // `<=` anchor MIN -> the single MIN row
1608+ // `>` anchor MID -> the single MAX row
1609+ // `>=` anchor MAX -> the single MAX row
1610+ let mut sorted: Vec <$scalar> = values. to_vec( ) ;
1611+ sorted. sort( ) ;
1612+ sorted. dedup( ) ;
1613+ anyhow:: ensure!( sorted. len( ) >= 3 ,
1614+ "scale test requires >= 3 distinct fixture values for \
1615+ min/mid/max single-row selectivity") ;
1616+ let min_v = sorted[ 0 ] . clone( ) ;
1617+ let max_v = sorted[ sorted. len( ) - 1 ] . clone( ) ;
1618+ let mid_v = sorted[ sorted. len( ) / 2 ] . clone( ) ;
1619+
1620+ let min_payload =
1621+ $crate:: scalar_domains:: fetch_fixture_payload:: <$scalar>( & pool, min_v) . await ?;
1622+ let mid_payload =
1623+ $crate:: scalar_domains:: fetch_fixture_payload:: <$scalar>( & pool, mid_v) . await ?;
1624+ let max_payload =
1625+ $crate:: scalar_domains:: fetch_fixture_payload:: <$scalar>( & pool, max_v) . await ?;
15981626
15991627 let mut tx = pool. begin( ) . await ?;
16001628 sqlx:: query( & format!(
16011629 "CREATE TEMP TABLE {table} (value {d}) ON COMMIT DROP" ,
16021630 ) ) . execute( & mut * tx) . await ?;
1631+ // The bulk: 5000 identical MID rows.
16031632 sqlx:: query( & format!(
16041633 "INSERT INTO {table}(value) \
16051634 SELECT $1::jsonb::{d} FROM generate_series(1, 5000)",
1606- ) ) . bind( & filler_payload) . execute( & mut * tx) . await ?;
1635+ ) ) . bind( & mid_payload) . execute( & mut * tx) . await ?;
1636+ // The two selective pivots: exactly one MIN row and one MAX row.
16071637 sqlx:: query( & format!(
1608- "INSERT INTO {table}(value) VALUES ($1::jsonb::{d})" ,
1609- ) ) . bind( & pivot_payload ) . execute( & mut * tx) . await ?;
1638+ "INSERT INTO {table}(value) VALUES ($1::jsonb::{d}), ($2::jsonb::{d}) " ,
1639+ ) ) . bind( & min_payload ) . bind ( & max_payload ) . execute( & mut * tx) . await ?;
16101640 sqlx:: query( & format!(
16111641 "CREATE INDEX {index} ON {table} USING {using} ({extractor}(value))" , using = $using, extractor = extractor,
16121642 ) ) . execute( & mut * tx) . await ?;
16131643 sqlx:: query( & format!( "ANALYZE {table}" ) )
16141644 . execute( & mut * tx) . await ?;
1615-
1616- let lit = pivot_payload. replace( '\'' , "''" ) ;
1617- $crate:: matrix:: assert_index_scan_uses(
1618- & mut * tx,
1619- & format!( "SELECT * FROM {table} WHERE value = '{lit}'::jsonb::{d}" ) ,
1620- index,
1621- & format!(
1622- "with seqscan enabled the planner must prefer the {extractor} {using} index for a selective =" ,
1623- extractor = extractor, using = $using,
1624- ) ,
1625- ) . await ?;
1645+ // enable_seqscan LEFT ON — this is the cost-PREFERENCE proof, not
1646+ // the usability proof (the sibling `*_index_engages_*` arm forces
1647+ // seqscan off over the ~17-row fixture). See Fact 1 / Fact 4.
1648+
1649+ // Both RHS forms (`::{domain}` and bare `::jsonb`) and BOTH the
1650+ // natural operator form and the explicit extractor form are
1651+ // asserted per op, mirroring the validity arm
1652+ // (`__scalar_matrix_index_case!`) minus the forced seqscan-off.
1653+ let rhs_casts = [ format!( "::{d}" , d = d) , String :: new( ) ] ;
1654+ $(
1655+ // `<>` is never index-selective over 5000 rows and is not a
1656+ // member of any index combo; guard it out defensively.
1657+ if $op != "<>" {
1658+ // Per-op anchor giving a single-row match against the
1659+ // bulk-MID / one-MIN / one-MAX table (see the header).
1660+ let anchor: & str = match $op {
1661+ "=" => & min_payload,
1662+ "<" => & mid_payload,
1663+ "<=" => & min_payload,
1664+ ">" => & mid_payload,
1665+ ">=" => & max_payload,
1666+ _ => & min_payload,
1667+ } ;
1668+ let lit = anchor. replace( '\'' , "''" ) ;
1669+ for rhs_cast in & rhs_casts {
1670+ // Natural bare-operator form: `value {op} <lit>`. This
1671+ // is the inlinability tripwire — a broken inline flips
1672+ // it to Seq Scan.
1673+ let natural = format!(
1674+ "SELECT * FROM {table} WHERE value {op} '{lit}'::jsonb{cast}" ,
1675+ op = $op, cast = rhs_cast,
1676+ ) ;
1677+ $crate:: matrix:: assert_index_scan_uses(
1678+ & mut * tx, & natural, index,
1679+ & format!(
1680+ "scale: natural-form `{op}` (rhs {cast:?}) must PREFER the \
1681+ {extractor} {using} index for a single-row predicate (seqscan ON)",
1682+ op = $op, cast = rhs_cast,
1683+ extractor = extractor, using = $using,
1684+ ) ,
1685+ ) . await ?;
1686+
1687+ // Explicit extractor form: `{extractor}(value) {op}
1688+ // {extractor}(<lit>)`. Complements the natural form;
1689+ // a divergence between the two surfaces an inlining
1690+ // break.
1691+ //
1692+ // ONLY the domain-cast RHS (`::{d}`) — never bare
1693+ // `::jsonb`. A standalone `eq_term`/`ord_term` call on
1694+ // a bare-jsonb argument is ambiguous: the extractor is
1695+ // overloaded across the domain family, and bare jsonb
1696+ // implicitly casts to several of them, so Postgres
1697+ // raises `function eql_v3.<extractor>(jsonb) is not
1698+ // unique`. The natural operator form above already
1699+ // exercises the bare-jsonb RHS path (the operator
1700+ // signature pins the domain), so skipping it here loses
1701+ // no coverage.
1702+ if !rhs_cast. is_empty( ) {
1703+ let extracted = format!(
1704+ "SELECT * FROM {table} \
1705+ WHERE {extractor}(value) {op} {extractor}('{lit}'::jsonb{cast})",
1706+ extractor = extractor, op = $op, cast = rhs_cast,
1707+ ) ;
1708+ $crate:: matrix:: assert_index_scan_uses(
1709+ & mut * tx, & extracted, index,
1710+ & format!(
1711+ "scale: extractor-form `{op}` (rhs {cast:?}) must PREFER the \
1712+ {extractor} {using} index for a single-row predicate (seqscan ON)",
1713+ op = $op, cast = rhs_cast,
1714+ extractor = extractor, using = $using,
1715+ ) ,
1716+ ) . await ?;
1717+ }
1718+ }
1719+ }
1720+ ) +
16261721
16271722 tx. commit( ) . await ?;
16281723 Ok ( ( ) )
0 commit comments