| 172 | } |
| 173 | |
| 174 | Pipe ReadFromSystemPrimesStep::makePipe() |
| 175 | { |
| 176 | auto & primes_storage = storage->as<StorageSystemPrimes &>(); |
| 177 | chassert(primes_storage.step != 0); |
| 178 | |
| 179 | Pipe pipe; |
| 180 | const auto header = PrimesSource::createHeader(primes_storage.column_name); |
| 181 | |
| 182 | auto add_null_source = [&] { NumbersLikeUtils::addNullSource(pipe, header); }; |
| 183 | |
| 184 | /// Pushdown rationale: |
| 185 | /// - Filter pushdown: |
| 186 | /// - Bounded sources (`primes(N)` / `primes(offset, length[, step])`) define a prime-index domain. |
| 187 | /// Value-space filtering is a later step; applying it during generation would change that domain and alter results. |
| 188 | /// Example: `SELECT prime FROM primes(100) WHERE prime > 1000`. |
| 189 | /// Correct behavior is to generate `primes(100)` first (the first 100 primes) and then filter, which is empty. |
| 190 | /// If we pushed `prime > 1000` into generation, we would switch to a value-domain scan and lose index-domain semantics. |
| 191 | /// - Unbounded sources (`primes()` / `system.primes`) have no bounded prime-index domain to preserve. |
| 192 | /// Therefore extracted value ranges/bounds can be applied during generation; if none are extractable, we scan normally. |
| 193 | /// Example: `SELECT prime FROM primes() WHERE prime BETWEEN 100 AND 130`. |
| 194 | /// This is equivalent to post-filtering because the source is unbounded in index space. |
| 195 | /// - LIMIT/OFFSET pushdown: |
| 196 | /// - Safe only when generated rows already satisfy WHERE (no filter, or exact extracted ranges). |
| 197 | /// Otherwise pre-filter LIMIT can cut off rows that would match later. |
| 198 | /// Example: `SELECT prime FROM system.primes WHERE prime % 10 = 1 LIMIT 5`. |
| 199 | /// If LIMIT were pushed first, we would take `[2, 3, 5, 7, 11]`, filter to `[11]`, and return too few rows. |
| 200 | /// Correct output is `[11, 31, 41, 61, 71]`. |
| 201 | /// With conservative ranges (e.g. `prime % 3 = 1 AND prime < 100`), bounds only restrict generation domain; |
| 202 | /// they do not guarantee all generated rows satisfy WHERE, so LIMIT still cannot be pushed down. |
| 203 | /// |
| 204 | /// This is the row-limit we pass down to the source. |
| 205 | /// It starts as the storage/table-function limit (`primes(N)`), and may be additionally capped |
| 206 | /// by the query LIMIT/OFFSET when it is safe to push that down. |
| 207 | std::optional<UInt64> effective_limit = primes_storage.limit; |
| 208 | |
| 209 | /// Calculate how many primes the sieve must generate for `primes(offset, limit, step)`. |
| 210 | /// Total primes generated is: `offset + (limit - 1) * step + 1` |
| 211 | auto estimate_rows_to_read = [&](UInt64 num_output_rows) -> UInt64 |
| 212 | { |
| 213 | if (num_output_rows == 0) |
| 214 | return 0; |
| 215 | UInt128 last_prime_index |
| 216 | = static_cast<UInt128>(primes_storage.offset) + static_cast<UInt128>(num_output_rows - 1) * primes_storage.step; |
| 217 | UInt128 total_primes_to_generate = last_prime_index + 1; |
| 218 | return static_cast<UInt64>(std::min<UInt128>(total_primes_to_generate, std::numeric_limits<UInt64>::max())); |
| 219 | }; |
| 220 | |
| 221 | /// Storage-level LIMIT 0 (e.g. `primes(0)`) is an empty table regardless of the WHERE clause. |
| 222 | if (effective_limit && *effective_limit == 0) |
| 223 | { |
| 224 | add_null_source(); |
| 225 | return pipe; |
| 226 | } |
| 227 | |
| 228 | /// Cap `effective_limit` by the query LIMIT/OFFSET, but only in paths where doing so is correct. |
| 229 | /// (The query LIMIT is applied after WHERE; pushing it into the source makes it effectively pre-filter, |
| 230 | /// so it is only safe when this source already generates only rows that satisfy the filter.) |
| 231 | auto apply_query_limit = [&] { NumbersLikeUtils::applyQueryLimit(effective_limit, limit); }; |
nothing calls this directly
no test coverage detected