| 459 | } |
| 460 | |
| 461 | Pipe ReadFromSystemNumbersStep::makePipe() |
| 462 | { |
| 463 | auto & numbers_storage = storage->as<StorageSystemNumbers &>(); |
| 464 | |
| 465 | /// For non-multithreaded variants (`system.numbers` / `numbers(...)`), keep a single stream to preserve ordering. |
| 466 | if (!numbers_storage.multithreaded) |
| 467 | num_streams = 1; |
| 468 | |
| 469 | Pipe pipe; |
| 470 | const auto header = NumbersSource::createHeader(numbers_storage.column_name); |
| 471 | |
| 472 | auto add_null_source = [&] { NumbersLikeUtils::addNullSource(pipe, header); }; |
| 473 | |
| 474 | /// Pushdown rationale: |
| 475 | /// - Filter pushdown: |
| 476 | /// - `numbers` is a value-domain source. For bounded sources (`numbers(limit)` / `numbers(offset, limit[, step])`), |
| 477 | /// storage arguments define the table domain (with step and possible wrap), and extracted ranges are intersected |
| 478 | /// with that domain to preserve semantics while reducing generation. |
| 479 | /// Example: `SELECT number FROM numbers(10, 10) WHERE number >= 15` should read only `[15, 19]`. |
| 480 | /// - For unbounded sources (`system.numbers(_mt)` / `numbers(_mt)()`), if no useful bounds are extracted |
| 481 | /// and query LIMIT/OFFSET cannot be safely pushed down, we keep the fast unbounded generator. |
| 482 | /// Otherwise we use ranged generation from extracted ranges/bounds (or from pushed query LIMIT/OFFSET). |
| 483 | /// Example: `SELECT number FROM system.numbers WHERE number BETWEEN 100 AND 130`. |
| 484 | /// - LIMIT/OFFSET pushdown: |
| 485 | /// - Safe only when extracted ranges are exact. With conservative bounds, pre-filter LIMIT may stop too early. |
| 486 | /// Example: `SELECT number FROM system.numbers WHERE number % 3 = 1 AND number < 100 LIMIT 5`. |
| 487 | /// Here `number < 100` gives only a conservative bound, so pushing LIMIT first could return too few rows |
| 488 | /// (e.g. from `[0, 1, 2, 3, 4]` we keep only `[1, 4]` instead of `[1, 4, 7, 10, 13]`). |
| 489 | /// |
| 490 | /// Storage-level LIMIT 0 (`numbers(..., 0, ...)`) is an empty table regardless of the WHERE clause. |
| 491 | if (numbers_storage.limit.has_value() && (numbers_storage.limit.value() == 0)) |
| 492 | { |
| 493 | add_null_source(); |
| 494 | return pipe; |
| 495 | } |
| 496 | |
| 497 | chassert(numbers_storage.step != UInt64{0}); |
| 498 | |
| 499 | /// Descending series (e.g. `generate_series(10, 0, -1)`): compute exact output count |
| 500 | /// from domain size and step, then use `SimpleSteppedNumbersSource` with a wrapping step. |
| 501 | /// The step stored in storage is always the positive absolute value; the wrapping trick |
| 502 | /// (UInt64(0) - step) makes unsigned addition equivalent to subtraction. |
| 503 | /// TODO: Support filter pushdown for negative step as well. |
| 504 | if (numbers_storage.descending) |
| 505 | { |
| 506 | chassert(numbers_storage.limit.has_value()); |
| 507 | UInt128 domain_size = *numbers_storage.limit; |
| 508 | UInt128 count128 = (domain_size + numbers_storage.step - 1) / numbers_storage.step; |
| 509 | |
| 510 | chassert(count128 <= std::numeric_limits<UInt64>::max()); |
| 511 | |
| 512 | UInt64 total_count = static_cast<UInt64>(count128); |
| 513 | |
| 514 | /// We cannot push down LIMIT if we have a filter. Consider: |
| 515 | /// SELECT * FROM generate_series(10, 0, -1) WHERE generate_series < 3 LIMIT 1 |
| 516 | /// Pushing LIMIT 1 would generate only {10}, which the filter discards, returning |
| 517 | /// empty instead of the correct {2}. |
| 518 | if (limit.has_value() && !filter_actions_dag) |
nothing calls this directly
no test coverage detected