Upgrade Guide
- Upgrading from 0.43.x to 0.44.x
- 1) flow-php/etl-adapter-json - to_json()/to_json_lines() write list/map/structure/array entries as nested JSON
- 2) flow-php/etl - Floe inferred schema keeps the first batch's nullability
- 3) flow-php/types - detectType([]) returns list<null>
- 4) flow-php/types - detectType() returns different types for arrays
- 5) flow-php/types - cast() refuses values it used to coerce, fabricate or wrap
- 6) flow-php/etl - FilesystemStreams replaced by FilesSink, and a failed run discards its sink
- 7) flow-php/etl - RetryLoader no longer retries InvalidLogicException by default
- 8) flow-php/etl - operations inside a Transformation answer for the whole stream
- 9) flow-php/etl - to_branch()->withTransformation() drives its Transformation once over the whole stream
- 10) flow-php/etl-adapter-doctrine, -postgresql - transactional loaders run closure() in a transaction
- 11) flow-php/etl - Floe on-disk format v2, existing .floe files must be rewritten
- 12) flow-php/etl - Floe rejects columns whose type is only known per value
- 13) flow-php/etl - Floe validates every value against its column type
- 14) flow-php/etl - aggregate result type no longer depends on the value
- 15) flow-php/etl - aggregates ignore a row missing the aggregated column
- 16) flow-php/types - EnumType::isValid() requires an object
- 17) flow-php/etl - filesystems are passed to sources and sinks, engine algorithms take a storage
- 18) flow-php/etl - equals() / notEquals() return null when either side is null
- 19) flow-php/etl - execution modes are removed, a function given bad data throws
- 20) flow-php/etl - not(), all(), any() and isType() return null for a null operand
- 21) flow-php/etl - expressions and steps are checked before the first row is read
- 22) flow-php/etl - functions return only values their column type can hold
- 23) flow-php/etl - function arguments that decide the output type take plain values
- 24) flow-php/etl - cast() accepts column types only
- 25) flow-php/etl - EntryReference is renamed to UnresolvedReference and is immutable
- 26) flow-php/etl - custom functions declare their output type and their children
- 27) flow-php/etl - user callbacks removed, call() takes a ScalarFunction and a return type
- 28) flow-php/etl - Transformer requires bind()
- 29) flow-php/etl - Extractor gains schema() and withSchema()
- 30) flow-php/etl - Entry and the *_entry() functions are removed, a row is a name-keyed array under a Schema
- 31) flow-php/etl - Rows holds only rows that match its schema
- 32) flow-php/etl / flow-php/etl-adapter-excel - Entry parameters become the value and its Definition
- 33) flow-php/etl - Config options move to the extractors or are removed
- 34) flow-php/etl - extractors yield batches
- 35) flow-php/etl - LimitableExtractor replaced by LimitPushDown
- 36) flow-php/etl - ErrorHandler answers per stage: extraction, transformation, loading
- 37) flow-php/etl - Cache gains schema()
- 38) flow-php/etl - data cached or serialized by 0.43.x cannot be read back
- 39) flow-php/etl - in-memory sources infer one nullable schema from the first 20,480 rows
- 40) flow-php/etl - Hydrator, array_to_row() and array_to_rows() require a Schema
- 41) flow-php/etl / flow-php/types - autoCast() removed
- 42) flow-php/etl - a value a declared column refuses throws SchemaMismatchException
- 43) flow-php/etl - DataFrame::schema() describes the plan without running it
- 44) flow-php/etl - printSchema() prints the plan's schema and takes no limit
- 45) flow-php/etl - display() and printRows() render one table
- 46) flow-php/etl - select() and groupBy() refuse an undeclared column
- 47) flow-php/etl - renaming onto an existing column throws
- 48) flow-php/etl - float values compare exactly
- 49) flow-php/etl - Window and window functions are immutable
- 50) flow-php/etl - group and join key extraction reads the schema
- 51) flow-php/etl - a join producing a duplicate column throws SchemaDefinitionNotUniqueException
- 52) flow-php/etl - Joiner takes JoinSide and no EntryFactory, Expression drop helpers removed
- 53) flow-php/etl - a join against an empty side keeps that side's columns
- 54) flow-php/etl - a global aggregate() over zero rows returns one row
- 55) flow-php/etl - pivot() takes its values
- 56) flow-php/etl - unpack() / array_unpack() take the Schema of the columns they produce
- 57) flow-php/etl - on_each() requires an operand that declares its element type
- 58) flow-php/etl - isIn() compares numbers loosely and refuses incomparable types
- 59) flow-php/etl - Calculator::divide() always returns float
- 60) flow-php/etl - DataFrame::partitionBy() removed, loaders partition, Rows carry no partitions
- 61) flow-php/etl - filterPartitions() compares the raw string partition value
- 62) flow-php/etl / flow-php/types - missing-column and type-mismatch messages changed
- 63) flow-php/etl - pipeline and generator extractors and Schema::fromPipeline() removed
- 64) flow-php/etl - until() stops at the first row failing its predicate
- 65) flow-php/etl - UnserializeTransformer requires the payload schema
- 66) flow-php/etl - Floe reader drops RowPadding, conform: and lenient:
- 67) flow-php/etl / flow-php/etl-adapter-parquet - a Floe or Parquet glob checks every file against the first
- 68) flow-php/etl and every package with DSL functions - documentation attributes leave the packages
- 69) flow-php/etl / flow-php/types - TypeMerge moved to flow-php/types as TypeWidener
- 70) flow-php/types - structure optional fields are declared inline with structure_element()
- 71) flow-php/types - time, date, string and Json accept and refuse different values
- 72) flow-php/types - StringTypeNarrower types compact digit dates as integer and month-only dates as string
- 73) flow-php/filesystem - Partition takes a value and its type, encodes reserved characters and allows null
- 74) flow-php/filesystem - SourceStream::readLines() strips the separator and yields nothing for 0 bytes
- 75) flow-php/parquet - FLOAT32 values read without ext-arrow are no longer rounded to 7 decimals
- 76) flow-php/parquet - FlatColumn::decimal() sizes FIXED_LEN_BYTE_ARRAY by the Parquet spec
- 77) flow-php/postgresql - Client gains describe(), and execute() accepts ConvertedParameters
- 78) flow-php/postgresql - arrays parsed, oid as integer, timetz without offset, duplicates cast by name
- 79) flow-php/doctrine-dbal-bulk - Dialect requires maxBindParameters()
- 80) flow-php/etl-adapter-csv, -json, -excel, -google-sheet - a read without a schema infers one schema
- 81) flow-php/etl-adapter-csv - cells are typed, not strings
- 82) flow-php/etl-adapter-doctrine - from_dbal_*() type columns from the query
- 83) flow-php/etl-adapter-doctrine, -postgresql - withPageSize() / withFetchSize() become withBatchSize()
- 84) flow-php/etl-adapter-excel - text cells narrow to uuid, json and timezone, to_excel() writes date cells
- 85) flow-php/etl-adapter-excel - requires openspout/openspout ~5.3.0
- 86) flow-php/etl-adapter-google-sheet - FORMATTED_VALUE cells are typed, and '' reads as null
- 87) flow-php/etl-adapter-http - response_body and request_body hold the raw body text
- 88) flow-php/etl-adapter-json - schema_from_json_schema() rejects a property with more than one non-null type
- 89) flow-php/etl-adapter-json - keys outside the sample are dropped, missing keys are null
- 90) flow-php/etl-adapter-json - from_json_lines() skips blank lines
- 91) flow-php/etl-adapter-parquet - to_parquet() does not validate values against the Parquet schema
- 92) flow-php/etl-adapter-postgresql - from_pgsql_*() type columns from the query instead of reading strings
- 93) flow-php/etl-adapter-postgresql - pgsql_table_to_flow_schema() maps arrays, text-like types, oid, timetz
- 94) flow-php/etl-adapter-postgresql - InsertQueryBuilder::build() takes the client's converters
- 95) flow-php/etl-adapter-xml - from_xml() keeps each element as the source wrote it
- 96) flow-php/cli - --schema-auto-cast removed, flow schema ignores the row window
- 97) flow-php/cli - command classes name themselves with #[AsCommand]
- 98) flow-php/flow-php-ext - the flow_php extension must be reinstalled
- 99) flow-php/etl-adapter-csv - withSeparator(), withEnclosure() and withEscape() take a single byte
- 100) flow-php/etl - array_expand() nested in an expression gives rows, and is refused outside withEntry()
- 101) flow-php/array-dot - \ escapes every path grammar character, array_dot_steps() deprecated
- 102) flow-php/array-dot - array_dot_set() and array_dot_rename() change only the addressed key
- 103) flow-php/etl - array_get_collection() reads its keys as literal keys
- 104) flow-php/etl - array_expand() over a null list gives no rows
- 105) flow-php/array-dot, flow-php/etl - a nullsafe multimatch over an empty array reads null
- 106) flow-php/filesystem - every filesystem matches glob patterns the same way
- 107) flow-php/parquet - ParquetEngine::openForWrite() returns a ParquetFileWriter
- 108) flow-php/etl-adapter-parquet - path-only partition columns leave the file body
- 109) flow-php/etl-adapter-postgresql - a failed from_pgsql_cursor() read throws its own error and rolls back
- 110) flow-php/etl-adapter-postgresql - from_pgsql_*() read exactly one read-only SELECT or VALUES statement
- 111) flow-php/postgresql - declare_cursor() over SQL takes exactly one SELECT or VALUES
- 112) flow-php/postgresql - SelectStatement::hasIntoClause() sees SELECT ... INTO in a UNION / INTERSECT / EXCEPT
- 113) flow-php/postgresql - sql_to_*_query() and the pagination modifiers take exactly one read-only SELECT or VALUES
- 114) flow-php/etl-adapter-postgresql - from_pgsql_limit_offset() requires the query's own ORDER BY
- Upgrading from 0.42.x to 0.43.x
- 1) flow-php/etl - to_transformation() expands a Transformation once per loader, not once per batch
- 2) flow-php/telemetry - Tracer::span() no longer activates the span
- 3) flow-php/etl - window aggregates use the SQL default frame
- 4) flow-php/etl - count() over a window is SQL COUNT
- 5) flow-php/etl - partitionBy() no longer sets orderBy()
- 6) flow-php/etl - WindowFunction::apply() receives a WindowContext
- 7) flow-php/postgresql - query recording moved to flow-php/symfony-postgresql-bundle
- 8) flow-php/symfony-telemetry-bundle - cache pools and PSR-18 clients that were silently skipped are now traced
- 9) flow-php/types - a Type implementation's generic parameter is the value it represents
- Upgrading from 0.41.x to 0.42.x
- 1) flow-php/symfony-telemetry-bundle - messenger tracing simplified
- 2) flow-php/symfony-telemetry-bundle - cache span names unified to dotted lowercase
- 3) flow-php/telemetry - Sampler::shouldSample() receives the parent Context
- 4) flow-php/telemetry - ResettableContextStorage removed; MemoryContextStorage::reset() removed
- 5) flow-php/symfony-telemetry-bundle - messenger tracing middleware auto-injected into all buses
- 6) flow-php/symfony-telemetry-bundle - http_kernel.exclude_paths now suppresses the whole request
- 7) flow-php/postgresql, flow-php/symfony-postgresql-bundle - traceTransactions bool replaced by
- 8) flow-php/symfony-telemetry-bundle - DBAL spans, metrics and config aligned with the PostgreSQL client
- 9) flow-php/symfony-telemetry-bundle - custom attribute keys moved out of reserved OTel namespaces
- 10) flow-php/symfony-telemetry-bundle - HTTP spans aligned with the stable HTTP semconv
- 11) flow-php/symfony-telemetry-bundle - messenger destination is the transport; metrics_duration_unit removed
- 12) flow-php/phpunit-telemetry-bridge - test.* keys aligned with the OTel test registry; durations in seconds
- 13) flow-php/filesystem, flow-php/etl, flow-php/postgresql - flow-custom keys under flow.*; UCUM units
- 14) flow-php/telemetry, flow-php/psr18-telemetry-bridge - shared semconv constants; UCUM time units
- 15) flow-php/symfony-telemetry-bundle - runtime_mode removed; terminate flushes, process end shuts down
- 16) flow-php/telemetry - Telemetry::registerShutdownFunction() holds a weak reference
- 17) flow-php/etl - SchemaValidator::isValid() replaced by validate(): ValidationContext
- 18) flow-php/filesystem - Partition name and value forbid { and }
- 19) flow-php/etl - Cache stores only Rows; cache indexes are stored as Rows
- 20) flow-php/etl - cache and serialization use the Floe (.floe) binary format; datetime subclasses rejected
- 21) flow-php/symfony-telemetry-bundle - HttpKernelSpanSubscriber takes a
- 22) flow-php/etl - sort algorithm is an explicit choice; external sort is the default
- 23) flow-php/etl - joins emit every matching right row and follow SQL semantics
- 24) flow-php/etl - join buckets storage is configurable, spills to disk by default
- 25) flow-php/etl - Serializer works on Rows and filesystem streams
- 26) flow-php/etl - EntryFactory API reshaped
- 27) flow-php/etl - Encoder/Hydrator contracts; hydrator configured on the context
- 28) flow-php/etl-adapter-csv, -json, -parquet, -xml, -excel, -doctrine, -postgresql, -seal, -text,
- 29) flow-php/etl - null is a first-class NullEntry/NullDefinition; from_null metadata removed
- 30) flow-php/etl - group by runs on bucketing storage; only referenced columns are spilled
- 31) flow-php/etl - DataFrame::pivot() removed; pivot is declared between groupBy() and aggregate()
- 32) flow-php/etl-adapter-csv, -excel, -json,
- 33) flow-php/etl - Schema and Schema\Definition mutators return a new instance
- Upgrading from 0.40.x to 0.41.x
- 1) Removal of Elasticsearch Adapter
- 2) flow-php/symfony-telemetry-bundle -
- 3) flow-php/telemetry - trace id derived from the active span; root spans start a new trace
- 4) flow-php/symfony-telemetry-bundle - each consumed Messenger message is its own trace
- 5) flow-php/telemetry-otlp-bridge, flow-php/symfony-telemetry-bundle - curl is synchronous; async moved to
- 6) flow-php/symfony-postgresql-bundle - migrations run against a single configured connection
- 7) flow-php/symfony-telemetry-bundle - static resource cache file is keyed by kernel environment
- 8) flow-php/symfony-telemetry-bundle - messenger worker receive cycle is traced; link_to_worker config removed
- 9) flow-php/symfony-telemetry-bundle - messenger instrumentation also emits messaging metrics
- 10) flow-php/etl, flow-php/filesystem, flow-php/postgresql, flow-php/symfony-telemetry-bundle,
- 11) flow-php/symfony-telemetry-bundle - messenger trace/link config; propagation_style removed
- Upgrading from 0.39.x to 0.40.x
- 1) flow-php/postgresql - column and domain defaults are modeled as ColumnDefault
- 2) flow-php/telemetry - Log severity filtering moved to a pipeline middleware
- 2) flow-php/symfony-telemetry-bundle - severity_filtering log processor type replaced by pipeline
- 3) flow-php/symfony-telemetry-bundle - named scope attributes split into scope and signal
- 4) flow-php/postgresql - DateTimeConverter split into TimestampConverter and TimestampTzConverter
- 5) flow-php/etl-adapter-postgresql - DateTimeEntry maps to timestamp instead of timestamptz
- 6) flow-php/symfony-postgresql-messenger - messenger_messages time columns use timestamp instead of
- 7) flow-php/telemetry - OTEL_RESOURCE_ATTRIBUTES keys and values are percent-decoded, not backslash-escaped
- Upgrading from 0.37.x to 0.38.x
- Upgrading from 0.36.x to 0.37.x
- 1) flow-php/telemetry - Per-signal exporter contracts merged into Exporter
- 2) flow-php/telemetry - Transport contract relocated to OTLP bridge
- 3) flow-php/telemetry - Processor interfaces
- 4) flow-php/telemetry - Serializer contract removed
- 5) flow-php/telemetry - ErrorHandler contract added
- 6) flow-php/telemetry-otlp-bridge - HttpTransport removed
- 7) flow-php/telemetry-otlp-bridge - Per-signal OTLP exporters merged
- 8) flow-php/telemetry-otlp-bridge - Curl/gRPC timeouts switched to milliseconds
- 9) flow-php/telemetry-otlp-bridge - StreamTransport added
- 10) flow-php/telemetry-otlp-bridge - open-telemetry/gen-otlp-protobuf dependency dropped
- 11) flow-php/symfony-telemetry-bundle - Configuration schema rewrite
- 12) flow-php/phpunit-telemetry-bridge - Configuration parameters
- Upgrading from 0.35.x to 0.36.x
- 1) flow-php/postgresql - RawCondition and RawExpression removed
- 2) flow-php/postgresql - Condition now extends Expression
- 3) flow-php/postgresql - DSL condition function renames
- 4) flow-php/postgresql - Schema builder methods accept Expression/Condition instead of strings
- 5) flow-php/postgresql - DSL functions split into separate files
- 6) flow-php/filesystem - Protocol and Backend removed, Mount rewritten
- 7) flow-php/filesystem - NativeLocalFilesystem::list() no longer sorts results
- 8) flow-php/symfony-filesystem-bundle - YAML schema now uses type: + protocol-as-key
- 9) flow-php/symfony-filesystem-bundle - FilesystemFactory interface and attribute changed
- 10) flow-php/symfony-filesystem-bundle - flow:filesystem:ls CLI flags reshuffled
- 11) flow-php/symfony-filesystem-bundle - flow:filesystem:stat rejects pattern paths
- 12) flow-php/filesystem-async-aws-bridge,
- 13) flow-php/filesystem - path_memory() and path_stdout() DSL helpers removed
- 14) flow-php/etl - ConfigBuilder::cacheFilesystem() and externalSortFilesystem() added
- 15) flow-php/symfony-http-foundation-bridge - Output interface collapsed to a single loader(Path)
- 16) flow-php/filesystem - StdOutFilesystem tracks open streams per php:// target
- Upgrading from 0.34.x to 0.35.x
- Upgrading from 0.31.x to 0.32.x
- 1) Removal of Meilisearch Adapter
- 2) Removed deprecated DSL functions
- 3) Removed deprecated DataFrame methods
- 4) Removed deprecated Schema methods
- 5) Removed deprecated Definition methods
- 6) Removed deprecated FileExtractor and PathFiltering methods
- 7) Removed deprecated ScalarFunctionChain methods
- 8) Removed deprecated Config constants
- 9) Removed deprecated Transformers
- 10) Removed deprecated classes
- Upgrading from 0.28.x to 0.29.x
- Upgrading from 0.26.x to 0.27.x
- Upgrading from 0.16.x to 0.17.x
- Upgrading from 0.15.x to 0.16.x
- Upgrading from 0.14.x to 0.15.x
- Upgrading from 0.11.x to 0.14.x
- Upgrading from 0.10.x to 0.11.x
- Upgrading from 0.8.x to 0.10.x
- Upgrading from 0.7.x to 0.8.x
- Upgrading from 0.6.x to 0.7.x
- Upgrading from 0.5.x to 0.6.x
- Upgrading from 0.4.x to 0.5.x
- 1) Entry factory moved from extractors to FlowContext
- 2) Invalid schema has no fallback in NativeEntryFactory
- 3) BufferLoader was removed
- 4) Pipeline Closure
- 5) Parallelize
- 6) Rows in batch - Extractors
- 7) GoogleSheetExtractor
- 8) DataFrame::threadSafe() method was replaced by DataFrame::appendSafe()
- 9) Loaders - chunk size
- 10) Removed DSL functions: datetime_string(), json_string()
- 11) Removed Asynchronous Processing
- 12) CollectionEntry removal
- 13) Removed from*() methods from scalar entries
- 14) Removed deprecated Sha1IdFactory
- 15) Deprecate DSL Static classes
- Upgrading from 0.3.x to 0.4.x
This document provides guidelines for upgrading between versions of Flow PHP. Please follow the instructions for your specific version to ensure a smooth upgrade process.
Upgrading from 0.43.x to 0.44.x
1) flow-php/etl-adapter-json - to_json()/to_json_lines() write list/map/structure/array entries as nested JSON
| Before | After |
|---|---|
{"tags":"[1,2,3]"} (escaped JSON string) |
{"tags":[1,2,3]} |
nested DateTimeInterface/Uuid/UnitEnum json_encoded as-is |
datetime format / canonical string / case name |
non-array value under a container type -> "" |
null |
2) flow-php/etl - Floe inferred schema keeps the first batch's nullability
| Before | After |
|---|---|
| inferred schema - every column nullable | nullability taken from the first batch's values |
null in batch >= 2 in a column non-nullable in batch 1 - written |
throws |
Columns that may only become null in later batches, declare the schema explicitly:
to_floe($path)->withSchema($schema).
3) flow-php/types - detectType([]) returns list<null>
| Before | After |
|---|---|
(new TypeDetector())->detectType([]) -> array<mixed> |
list<null> |
4) flow-php/types - detectType() returns different types for arrays
| Before | After |
|---|---|
detectType([5 => 'a', 6 => []]) -> map<integer, string> |
array<mixed> |
detectType(['a', []]) -> list<string> |
array<mixed> |
detectType([['id' => '1'], []]) -> list<structure{id: string}> |
array<mixed> |
detectType([[1, 2], [null]]) -> list<list<integer>> |
list<array<mixed>> |
detectType([[], [1, 2]]) -> list<array<mixed>> |
list<list<?integer>> |
detectType([1 => 1, 5 => 2.5]) -> array<mixed> |
map<integer, float> |
detectType(['name' => 'x', 'latlng' => [33, 65.5]]) -> structure{name: string, latlng: array<mixed>} |
structure{name: string, latlng: list<float>} |
5) flow-php/types - cast() refuses values it used to coerce, fabricate or wrap
| Before | After |
|---|---|
type_structure(['id' => type_integer(), 'name' => type_string()])->cast(['id' => 1]) -> ['id' => 1, 'name' => ''] |
throws CastingException |
same type, ->cast(['id' => 1, 'name' => null]) -> ['id' => 1, 'name' => ''] |
throws CastingException |
same type, ->cast([]), ->cast(null) -> ['id' => 0, 'name' => ''] |
throws CastingException |
type_structure(['id' => type_integer()], ['name' => type_string()])->cast(['id' => 1, 'name' => null]) -> ['id' => 1, 'name' => ''] |
throws CastingException |
type_list(type_string())->cast(null) -> [''] |
throws CastingException |
type_structure(['id' => type_integer()])->cast('{"id":"1"}') -> throws |
['id' => 1] |
type_list(type_integer())->cast('["1","2"]') -> throws |
[1, 2] |
type_integer()->cast('abc') -> 0 |
throws CastingException |
type_integer()->cast('2024-01-01') -> 2024 |
throws CastingException |
type_integer()->cast([1, 2, 3]) -> 1 |
throws CastingException |
type_integer()->cast(null) -> 0 |
throws CastingException |
type_integer()->cast('9223372036854775808') -> 9223372036854775807; also '9223372036854775807.0', type_positive_integer() |
throws CastingException |
type_integer()->cast(1e300) -> 0 |
throws CastingException |
type_integer()->cast(new DOMElement('e', 'abc')) -> 0 |
throws CastingException |
type_float()->cast('abc') -> 0.0 |
throws CastingException |
type_float()->cast([1]) -> 1.0 |
throws CastingException |
type_float()->cast(null) -> 0.0 |
throws CastingException |
type_boolean()->cast('weird') -> true |
throws CastingException |
type_boolean()->cast('') -> false |
throws CastingException |
type_boolean()->cast([1, 2, 3]) / ->cast(new DateTimeImmutable()) -> true |
throws CastingException |
type_boolean()->cast(null) -> false |
throws CastingException |
type_date()->cast('now') / type_datetime()->cast('now') -> current time; also '', 't', '+12', 'yesterday', '10:00:00' |
throws CastingException |
type_datetime()->cast('@1609459200') -> 2021-01-01; also '2024-01', '2024-001', and under type_date() |
throws CastingException |
type_string()->cast(null) / type_scalar()->cast(null) -> '' |
throws CastingException |
type_list(type_string())->cast([null]) -> [''] |
throws CastingException |
type_array()->cast('abc') -> ['abc'] |
throws CastingException |
type_list(type_integer())->cast(5) -> [5] |
throws CastingException |
type_array()->cast(null) -> [] |
throws CastingException |
type_integer()->cast(new DateTimeImmutable('@1700000000')) -> 1700000000000000 |
1700000000 |
type_float()->cast(new DateTimeImmutable('@1700000000.5')) -> 1700000000500000.0 |
1700000000.5 |
type_integer()->cast(new DateInterval('PT90S')) -> 90000000 |
90 |
type_float()->cast(new DateInterval('PT90S')) -> 90000000.0 |
90.0 |
6) flow-php/etl - FilesystemStreams replaced by FilesSink, and a failed run discards its sink
| Before | After |
|---|---|
Flow\ETL\Filesystem\FilesystemStreams |
Flow\ETL\Filesystem\FilesSink |
FilesystemStreams::FLOW_TMP_FILE_PREFIX |
FilesSink::FLOW_TMP_FILE_PREFIX |
new FilesystemStreams() + ->setMode($mode) |
new FilesSink($filesystem, $destination, $mode) |
$streams->writeTo($filesystem, $path, $partitions) |
$files->writeTo($partitions) |
$streams->isOpen($path, $partitions) |
$files->touched($partitions) |
$streams->listOpenStreams($path) |
$files->openStreams() |
$streams->closeStreams($filesystem, $path) |
$files->publish() / $files->abandon() |
$streams->read() / ->rm() / ->exists() / count() / getIterator() |
removed - use the Filesystem directly |
saveMode() on one DataFrame applied to every later DataFrame on the same Config |
saveMode() is set on the sink and belongs to that sink alone |
a failed run left its DestinationStream registered; the retry appended to it |
the failed run's sink is discarded, the retry starts clean |
an abandoned run left its ._flow_php_tmp. file behind under Overwrite |
the abandoned run removes it and never renames it over the destination |
| a failed run left its partial file at the destination under the other save modes | it removes any file it created; a destination it did not create is untouched |
| a run that threw or was abandoned never reached its loaders | loaders implementing Flow\ETL\Loader\Discardable receive discard(FlowContext $context) |
Config::filesystemStreams() |
removed |
new Config(..., FilesystemStreams $filesystemStreams, ...) constructor parameter |
removed |
Build Config through Config::builder() / Config::default(). A Loader holding per-run state (an open stream, a
writer, a counter) implements Discardable and drops that state in discard().
7) flow-php/etl - RetryLoader no longer retries InvalidLogicException by default
| Before | After |
|---|---|
new RetryLoader($loader) default strategy new AnyThrowable(3) |
new AnyThrowableExcept([InvalidLogicException::class], 3) |
write_with_retries($loader) default strategy new AnyThrowable(3) |
new AnyThrowableExcept([InvalidLogicException::class], 3) |
an InvalidLogicException was attempted 4 times with delays between |
attempted once, no delay |
| - | retry_any_throwable_except([InvalidLogicException::class], 3) |
To keep retrying every throwable:
write_with_retries($loader, retry_any_throwable(3));
8) flow-php/etl - operations inside a Transformation answer for the whole stream
| Before | After |
|---|---|
$df->sortBy(ref('id')) inside a Transformation sorted each batch on its own |
sorts the whole stream |
$df->aggregate(...) / groupBy()->aggregate() / pivot() / window functions inside a Transformation answered per batch |
answer for the whole stream |
$df->offset(2) inside a Transformation lost rows |
skips exactly the offset across the stream |
$df->cache($id) inside a Transformation persisted one batch |
persists the whole stream |
$df->batchBy(...) / batch_size(...) cut chunks at the incoming batches |
cut chunks over the stream |
$df->join(...) inside a Transformation emitted rows in input order |
emits rows grouped by key |
$df->partitionBy(...) inside a Transformation |
removed - $df->repartition(...) groups the whole stream by key, see 60) |
a Transformation calling $df->fetch() / count() / schema() silently answered over an empty stream |
throws InvalidLogicException |
write_with_retries($loader) around to_transformation(...) (any wrapped step) or around a to_branch(...) armed with withTransformation(...), at any nesting depth |
throws InvalidLogicException at the first load(), use to_transformation(..., write_with_retries($loader)) or to_branch(..., write_with_retries($loader))->withTransformation(...) |
Flow\ETL\Extractor\SwappableRowsExtractor |
removed |
9) flow-php/etl - to_branch()->withTransformation() drives its Transformation once over the whole stream
| Before | After |
|---|---|
the Transformation ran on each filtered batch in its own DataFrame |
one nested pipeline spans the stream |
$df->sortBy(...) in a branch transformation sorted each batch alone |
sorts the whole branch stream |
$df->aggregate(...) / limit() / other Processor-backed operations answered per batch |
answer once for the stream |
a Transformation calling $df->fetch() / count() / schema() returned per-batch answers |
throws InvalidLogicException |
the wrapped loader received exactly one load() per outer batch |
receives output as the transformation produces it; blocking operations deliver at closure() |
telemetry flow.etl.loading.rows counted post-filter, post-transformation rows |
counts the rows offered to the branch |
10) flow-php/etl-adapter-doctrine, -postgresql - transactional loaders run closure() in a transaction
| Before | After |
|---|---|
to_dbal_transaction() / to_pgsql_transaction() never called closure() on wrapped loaders |
forwards closure() to every wrapped loader, inside one final transaction |
blocking operations inside a wrapped Transformation answered per batch, each batch in its own transaction |
answer for the whole stream, delivered at closure() in a single transaction |
| - | a failure during the final transaction rolls back the drained delivery and rethrows |
withIsolationLevel() applied to per-batch transactions |
applies to every transaction the wrapper opens |
11) flow-php/etl - Floe on-disk format v2, existing .floe files must be rewritten
| Before | After |
|---|---|
header version byte 0x01 |
0x02 |
| uuid payload - 36 raw bytes | 4-byte little-endian length prefix + bytes |
| partition values stored in the file | not stored - read from the directory path only |
| reading a v1 file | throws Floe does not support format version 1 |
Files written by 0.43.x cannot be read. Regenerate them from their source, or export them with 0.43.x to another format before upgrading.
12) flow-php/etl - Floe rejects columns whose type is only known per value
| Before | After |
|---|---|
list<mixed> element - written with a tag |
Floe does not support values of type "mixed" |
union_schema() column - written |
Floe does not support columns of type "integer|string" |
type_structure(..., allow_extra: true) |
Floe does not support structures that allow extra values |
map key other than integer/string |
Floe does not support map keys of type "..." |
Declare an element type, or declare the column with json_schema() when the shape is dynamic.
13) flow-php/etl - Floe validates every value against its column type
| Before | After |
|---|---|
'AB-1' into an integer column - wrote 0 |
throws IncompatibleSchemaException |
1.5 into an integer column - wrote 1 |
throws |
1000 into a string column - raw TypeError |
throws IncompatibleSchemaException |
int into a float column - written |
throws |
null into a non-nullable column - written |
throws |
| a batch without a NOT NULL session column - written | throws IncompatibleSchemaException |
| a batch without a nullable session column | unchanged, reads back as null |
IncompatibleSchemaException - column "x" (...) is not compatible with the session type (...) |
a Missing Definitions: / Mismatched Definitions: block |
floe_options(validate_data: false) / Options::withValidateData() |
removed |
floe_options($validate_data, $buffer_size, $codec) |
floe_options($buffer_size, $codec) |
new Options($validateData, $bufferSize, $codec) |
new Options($bufferSize, $codec) |
14) flow-php/etl - aggregate result type no longer depends on the value
| Before | After |
|---|---|
sum() over a float column, whole total - int |
float |
avg()/min()/max() over a float column, whole result - int |
float |
sum() over an integer column - int |
float |
15) flow-php/etl - aggregates ignore a row missing the aggregated column
| Before | After |
|---|---|
missing column under $df->mode(execution_strict()) - Sum error: Entry "amount" does not exist |
contributes nothing |
| missing column in the default mode - contributed nothing | unchanged |
| a column no row declares - contributed nothing | throws SchemaDefinitionNotFoundException |
16) flow-php/types - EnumType::isValid() requires an object
| Before | After |
|---|---|
type_enum(Suit::class)->isValid('Suit') |
false (was true) |
type_enum(Suit::class)->assert('Suit') |
throws InvalidTypeException (was raw TypeError) |
type_enum(Suit::class)->cast('Suit') |
throws CastingException (was raw TypeError) |
17) flow-php/etl - filesystems are passed to sources and sinks, engine algorithms take a storage
Filesystems
| Before | After |
|---|---|
config_builder()->mount(aws_s3_filesystem(...)) + from_csv(path('aws-s3://x.csv')) |
from_csv(path('aws-s3://x.csv'), filesystem: aws_s3_filesystem(...)) |
config_builder()->unmount($fs) |
removed |
$config->fstab() |
removed - use Flow\Filesystem\DSL\fstab() for file_copy() / file_move() |
$context->filesystem($path) |
removed - pass filesystem: to the source or sink |
$context->streams()->list($path, $filter) -> SourceStream |
(new Flow\Filesystem\FileListing($filesystem))->list($path, $filter) -> FileStatus |
$context->streams()->writeTo($path, $partitions) |
$files->writeTo($partitions) on a sink-held FilesSink - see 6) |
$context->streams()->closeStreams($path) in an extractor |
removed |
new FilesystemStreams($filesystemTable) |
new FilesSink($filesystem, $destination, $saveMode) - see 6) |
a custom file source reading through FlowContext |
takes Filesystem $filesystem = new NativeLocalFilesystem() as its last constructor argument |
schema_from_json_schema($s) resolved local $refs through fstab() |
takes a trailing Filesystem $filesystem = new NativeLocalFilesystem() |
a $ref on memory:// or stdout:// |
throws - pass the filesystem that serves it |
ChartJSLoader::withOutputPath($path) / ::withTemplate($path) |
both take a trailing Filesystem $filesystem = new NativeLocalFilesystem(); withOutputPath() throws on a path with no extension |
FilePathArgument::getExisting($input, $config) / ::getNotExisting(...) (CLI) |
getExisting($input) / getNotExisting($input); the constructor takes the Filesystem |
a custom FileLoader |
implements saveMode() and Discardable - see 6) |
a custom Flow\Filesystem\Filesystem implementation |
adds public function supports(Path $path): bool - return $this->mount()->supports($path); |
Two memory_filesystem() calls are two separate stores: pass the same instance to the writer and the reader.
Filesystem telemetry
| Before | After |
|---|---|
withTelemetry() traced every filesystem: filesystem.read / filesystem.write spans and filesystem metrics |
filesystems are not traced - wrap the filesystem with traceable_filesystem() |
telemetry_options(filesystem: filesystem_telemetry_options(...)) |
removed |
TelemetryOptions::filesystem() / ->filesystem |
removed |
the pipeline-start debug log's fstab field |
a spill field naming the three spill storages |
After:
$fs = traceable_filesystem(
aws_s3_filesystem($bucket, $client),
filesystem_telemetry_config($telemetry, $clock, filesystem_telemetry_options(trace_streams: true)),
);
data_frame()->read(from_csv(path('aws-s3://x.csv'), filesystem: $fs))->run();
Engine algorithms
| Before | After |
|---|---|
config_builder()->cacheFilesystem('s3') |
config_builder()->cache($cache) |
external_sort()->filesystemProtocol('file') |
external_sort()->storage(new FilesystemBuckets($fs, $path)) |
hash_join()->filesystemProtocol(...) / hash_group_by()->filesystemProtocol(...) |
->storage(BucketsStorage $storage) |
CacheConfig::$filesystemMount |
removed |
SortAlgorithmBuilder::build(FilesystemTable, Path) |
build(Path $spillRoot) |
new ExternalSortConfig($bucketing, $runSize) |
new ExternalSortConfig($bucketing, ?BucketsStorage $merge = null, $runSize) - use named arguments |
| - | external_sort()->mergeStorage($s) - merged runs only |
Per-operation algorithm overrides
| Before | After |
|---|---|
$df->sortBy(ref('a'), ref('b')) |
$df->sortBy([ref('a'), ref('b')]) |
$df->groupBy('a', 'b') |
$df->groupBy(['a', 'b']) |
$df->aggregate(sum(ref('a')), ...) |
$df->aggregate([sum(ref('a')), ...]) |
| - | $df->sortBy([ref('a')], external_sort()->storage(new MemoryBuckets())) |
| - | $df->groupBy(['a'], hash_group_by()->storage($s)) |
| - | $df->join($right, $on, Join::left, hash_join()->storage($s)) |
| - | $df->cache('report', cache: $psrCache) + from_cache('report', cache: $psrCache) |
GroupedDataFrame::aggregate() stays variadic: $df->groupBy(['a'])->aggregate(sum(ref('b'))).
Save mode, the CLI and the HTTP bridge
| Before | After |
|---|---|
$df->saveMode(overwrite()) / $df->mode(overwrite()) |
to_csv($path)->saveMode(overwrite()) - per sink |
$df->saveMode(overwrite())->write(write_with_retries(to_csv($p))) |
$df->write(write_with_retries(to_csv($p)->saveMode(overwrite()))) |
to_text($path) returned Loader |
returns TextLoader |
LoaderFactory::get() returned Loader |
returns Loader&FileLoader |
flow read --config .flow.php aws-s3://bucket/x.csv |
flow pipeline:run pipeline.php, with the filesystem built inside the pipeline file |
new FlowBufferedResponse(..., filesystem: 'memory') |
new FlowBufferedResponse(..., filesystem: new MemoryFilesystem()) |
new FlowStreamedResponse(..., filesystem: 'stdout') |
new FlowStreamedResponse(..., filesystem: new StdOutFilesystem()) |
Output::loader(Path $path) |
Output::loader(Path $path, Filesystem $filesystem) |
18) flow-php/etl - equals() / notEquals() return null when either side is null
| Before | After |
|---|---|
ref('a')->equals(ref('b')), both null - true, filter() keeps the row |
null, filter() drops the row |
ref('a')->equals(lit(5)), a is null - false |
null |
ref('a')->notEquals(lit(5)), a is null - true, filter() keeps the row |
null, filter() drops the row |
ref('a')->same(ref('b')) / ref('a')->notSame(lit(5)) |
unchanged - null same as null |
To keep the old result, switch to same() / notSame():
Before:
$df->filter(ref('a')->equals(ref('b')));
$df->filter(ref('a')->notEquals(lit(5)));
After:
$df->filter(ref('a')->same(ref('b')));
$df->filter(ref('a')->notSame(lit(5)));
same() compares with ===: over an integer column ref('i')->same(lit(1.0)) is false where equals() is true.
19) flow-php/etl - execution modes are removed, a function given bad data throws
| Before | After |
|---|---|
default mode: a function given bad data returned null |
throws - wrap it: optional($function) |
$df->mode(execution_lenient()) |
removed - optional($function) per function |
$df->mode(execution_strict()) |
removed - strict is the only behaviour |
coalesce($a, $b) skipped a branch that threw |
rethrows - coalesce(optional($a), $b) |
cast($v, type_integer()) over null returned null |
throws - cast($v, type_optional(type_integer())) |
ref('l')->onEach($fn) set an element whose $fn threw to null |
throws - ref('l')->onEach(optional($fn)) |
ref('s')->indexOf('a') / ->indexOfLast('a') over null - false |
throws - optional(ref('s')->indexOf('a')) |
DataFrame::mode() |
removed |
Flow\ETL\Function\ExecutionMode, execution_lenient(), execution_strict() |
removed |
Flow\ETL\Function\Functions, FlowContext::functions() |
removed - a custom function throws instead of calling invalidResult() |
Before:
data_frame()->read($extractor)->withEntry('payload', ref('json')->jsonDecode())->run();
After:
data_frame()->read($extractor)->withEntry('payload', optional(ref('json')->jsonDecode()))->run();
20) flow-php/etl - not(), all(), any() and isType() return null for a null operand
| Before | After |
|---|---|
not(ref('a')->equals(lit(1))), a is null - true, filter() keeps the row |
null, filter() drops the row |
all(ref('a')->equals(lit(1)), lit(true)), a is null - false |
null |
any(ref('a')->equals(lit(1)), lit(false)), a is null - false |
null |
ref('a')->isType(type_null()), a is null - true |
null - use ref('a')->isNull() |
isNull(), isNotNull(), same(), notSame() and exists() still return true or false.
21) flow-php/etl - expressions and steps are checked before the first row is read
| Before | After |
|---|---|
$df->filter(ref('id')->greaterThan(ref('created_at'))), integer vs datetime - threw at the first row |
throws at schema() / run(), before any row |
the same failure under onError(skip_rows_handler()) - handled per row by the error handler |
throws; the error handler is not consulted |
ref('a')->plus(ref('d')) (integer + date) over a source with no rows - no rows |
throws Cannot combine types "integer", "date" - an explicit cast is required. |
$df->filter(ref('id')) / $df->until(ref('id')) / to_branch(ref('id'), $loader) over an integer column - accepted |
throws - compare explicitly: ref('id')->notEquals(lit(0)) |
greatest(ref('i'), ref('s')) / least(...) over integer and string - 'apple' for 5, 'apple' |
throws InvalidTypeException - cast one side |
coalesce(ref('i'), ref('s')) / when($c, ref('i'), ref('s')) / match_cases() with integer and string arms - typed per value |
string column |
coalesce(ref('i'), lit(true)) - typed per value |
throws InvalidTypeException |
array_expand(ref('j')) over a json column - typed per value |
throws SchemaNotDerivableException |
ref('u') / select('u') over a column declared integer|string - one type per value |
throws UnsupportedUnionTypeException - declare one type, e.g. str_schema('u') |
Applies to frames read through from_data_frame() and to to_transformation() bodies: the inner error fails the
outer run() / schema().
22) flow-php/etl - functions return only values their column type can hold
| Before | After |
|---|---|
ref('d')->toDateTime('Y-m-d') over an unparseable string - false |
null |
ref('j')->jsonDecode() over scalar JSON ('1') - 1 |
throws - cast() scalar JSON |
ulid() - Symfony\Component\Uid\Ulid |
base32 string |
ref('x')->domElementParent() of a root element - its DOMDocument |
null |
ref('x')->xpath($path) - every matched node, text and attribute nodes included |
elements only, null when none match |
when($condition, $then, $else) with $else of 0, false, '' or [] - null |
$else |
round($v, 0) / ref('v')->round(0) - int |
float |
23) flow-php/etl - function arguments that decide the output type take plain values
| Before | After |
|---|---|
array_sort($a, $sort_function) - ScalarFunction|Sort|null |
?Sort |
array_reverse($a, $preserveKeys) / ->arrayReverse($preserveKeys) - ScalarFunction|bool |
bool |
->onEach($fn, $preserveKeys) - ScalarFunction|bool |
bool |
regex() / regex_all() / ->regex() / ->regexAll() $flags - ScalarFunction|int |
int; PREG_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL (and PREG_SET_ORDER for regex_all()) throw at construction |
array_get($ref, $path) / ->arrayGet($path) - ScalarFunction|string |
string; a wildcard path throws - use array_get_collection() |
array_key_rename($ref, $path, $newName) - ScalarFunction|string |
string |
uuid_v7() / uuid_v7(null) |
uuid_v7($value) - $value required |
->domElementNextSibling() / ->domElementPreviousSibling() - next / previous node, text included |
next / previous element |
->domElementNextSibling(true) / ->domElementPreviousSibling(true) |
argument removed |
ref('a')->isType('nope') - threw at the first row |
throws at construction |
new Round($value) - $precision defaults to 0 |
defaults to 2, as round() |
24) flow-php/etl - cast() accepts column types only
| Before | After |
|---|---|
cast($v, 'json_pretty') |
removed - throws at construction; use 'json' |
cast($v, 'object') / 'mixed' / 'callable' / 'resource' |
throws InvalidArgumentException at construction |
25) flow-php/etl - EntryReference is renamed to UnresolvedReference and is immutable
| Before | After |
|---|---|
Flow\ETL\Row\EntryReference |
renamed to Flow\ETL\Row\UnresolvedReference |
ref() / col() / entry() return EntryReference |
return UnresolvedReference |
$ref->as('b') / ->asc() / ->desc() changed $ref |
return a copy - use the returned reference |
26) flow-php/etl - custom functions declare their output type and their children
| Before | After |
|---|---|
class MyFunction extends ScalarFunctionChain |
final class MyFunction implements ScalarFunction + use ScalarFunctionChain; |
ScalarFunction - eval() |
also returns(): Type, children(): array, withChildren(array $children): static, resolved(): bool |
a custom Reference |
implements the ScalarFunction members above - Reference extends ScalarFunction |
AggregatingFunction::result(EntryFactory $entryFactory): Entry |
removed - implement outputName(): string, value(): mixed, returns(): Type and the FunctionTree members |
WindowFunction |
adds returns(): Type and the FunctionTree members; over() returns a copy |
Flow\ETL\Function\ScalarFunction\ScalarResult, ScalarResult::from() |
removed - return the plain value, declare the type in returns() |
FrameAccumulator::value(): mixed |
value(): float|int|null |
Parameter::as*() over a malformed value - null / false / $default |
throws InvalidArgumentException |
Parameter::asBoolean(): bool |
?bool |
Parameter::asType() |
removed |
Before:
final class Shout extends ScalarFunctionChain
{
public function __construct(private readonly ScalarFunction $value) {}
public function eval(Row $row, FlowContext $context): ?string
{
$value = (new Parameter($this->value))->asString($row, $context);
return $value === null ? null : strtoupper($value);
}
}
After:
final class Shout implements ScalarFunction
{
use ScalarFunctionChain;
use ResolvesFromChildren;
public function __construct(private readonly ScalarFunction $value) {}
public function eval(Row $row, FlowContext $context): ?string
{
$value = (new Parameter($this->value))->asString($row, $context);
return $value === null ? null : strtoupper($value);
}
public function returns(): Type
{
return type_optional(type_string());
}
public function children(): array
{
return [$this->value];
}
public function withChildren(array $children): static
{
return new self($children[0]);
}
}
27) flow-php/etl - user callbacks removed, call() takes a ScalarFunction and a return type
| Before | After |
|---|---|
$df->map($callback) |
removed |
Flow\ETL\Transformer\CallbackRowTransformer |
removed |
to_callable($callable) / Flow\ETL\Loader\CallbackLoader |
removed - implement Flow\ETL\Loader |
Flow\ETL\Transformer\StyleConverter\ArrayKeyConverter |
removed |
call('strtoupper', [ref('name')]) |
call(lit('strtoupper'), type_string(), [ref('name')]) |
call($callable, $parameters, $return_type = null) |
call(lit($callable), $return_type, $parameters) - return type required |
->call($callable, $arguments, $refAlias, $returnType = null) |
->call(lit($callable), $returnType, $arguments, $refAlias) |
new CallUserFunc($callable, $parameters, $returnType) |
new CallUserFunc($callable, $returnType, $parameters) |
call(..., type_string()) column - string |
?string |
lit($closure), or a Closure inside a lit() array |
throws InvalidArgumentException |
(new TypeDetector())->detectType($closure) |
throws Flow\Types\Exception\InvalidArgumentException |
Before:
$df->map(fn (Row $row): Row => $row->set(str_entry('name', $row->valueOf('name') ?? 'default')));
After:
$df->withEntry('name', coalesce(ref('name'), lit('default')));
Logic no scalar function expresses: implement Flow\ETL\Transformer and pass it to $df->with($transformer).
28) flow-php/etl - Transformer requires bind()
| Before | After |
|---|---|
Transformer::transform(Rows $rows, FlowContext $context): Rows |
plus bind(Schema $input): Flow\ETL\Pipeline\BoundStep |
Return new BoundStep($this, $output), where $output is the schema the step emits for $input.
29) flow-php/etl - Extractor gains schema() and withSchema()
| Before | After |
|---|---|
Extractor declares only extract(FlowContext $context): Generator |
also schema(): Schema and withSchema(Schema $schema): static |
a custom Extractor implementing only extract() |
fatal at load |
A source that cannot describe its output before reading throws
Flow\ETL\Exception\SchemaNotDerivableException::extractor(self::class) from schema().
30) flow-php/etl - Entry and the *_entry() functions are removed, a row is a name-keyed array under a Schema
| Before | After |
|---|---|
Flow\ETL\Row\Entry and the 18 Flow\ETL\Row\Entry\*Entry classes |
removed |
Flow\ETL\Row\Entries, entries(...) |
removed |
row(Entry ...$entries), Row::create(...), Row::with(...) |
row(array $values) - row(['id' => 1]) |
rows(Row ...$rows), new Rows(Row ...$rows) |
rows(Schema $schema, Row ...$rows), new Rows(Schema $schema, Row ...$rows) - see 31) |
$row->get('id') returned an Entry |
returns the value |
$row->valueOf('id') |
$row->get('id') |
$row->entries() |
$row->values() / $row->names() |
$row->schema() |
$rows->schema() |
$row->hash() |
$row->hash($rows->schema()) |
$row->add() / set() / remove() / keep() / rename() / renameMany() / map() / merge() / isEqual() / sortEntries() |
removed |
$rows->entries() |
removed |
Metadata per entry, free to differ row to row: int_entry('id', 1, $metadata) |
one Metadata per column: int_schema('id', metadata: $metadata) |
compare_entries_by_name() / compare_entries_by_name_desc() / compare_entries_by_type() / compare_entries_by_type_desc() / compare_entries_by_type_and_name() |
removed - sort a schema: $schema->sort(schema_sort_by_name()) |
Flow\ETL\Transformer\OrderEntries\{Comparator, CombinedComparator, NameComparator, TypeComparator, TypePriorities, Order} |
removed - implement Flow\ETL\Schema\SortingStrategy::compare(Definition $left, Definition $right): int |
Before:
$rows = rows(
row(int_entry('id', 1), str_entry('name', 'Norbert')),
row(int_entry('id', 2), str_entry('name', null)),
);
After:
$rows = rows(
schema(int_schema('id'), str_schema('name', nullable: true)),
row(['id' => 1, 'name' => 'Norbert']),
row(['id' => 2, 'name' => null]),
);
| Before | Column in schema(...) |
Value in row([...]) |
|---|---|---|
bool_entry('x', true), boolean_entry('x', true) |
bool_schema('x') |
true |
int_entry('x', 1), integer_entry('x', 1) |
int_schema('x'), integer_schema('x') |
1 |
float_entry('x', 1.5) |
float_schema('x') |
1.5 |
float_entry('x', 1), float_entry('x', '1.5') |
float_schema('x') |
type_float()->cast(1), type_float()->cast('1.5') |
str_entry('x', 'a'), string_entry('x', 'a') |
str_schema('x'), string_schema('x') |
'a' |
null_entry('x') |
null_schema('x') |
null |
datetime_entry('x', '2024-01-02 10:00:00') |
datetime_schema('x') |
type_datetime()->cast('2024-01-02 10:00:00') |
date_entry('x', '2024-01-02') |
date_schema('x') |
type_date()->cast('2024-01-02') |
time_entry('x', 'PT1H') |
time_schema('x') |
type_time()->cast('PT1H') |
enum_entry('x', Suit::Hearts) |
enum_schema('x', Suit::class) |
Suit::Hearts |
json_entry('x', ['a' => 1]), json_entry('x', '{"a":1}') |
json_schema('x') |
type_json()->cast(['a' => 1]), type_json()->cast('{"a":1}') |
json_object_entry('x', []) |
json_schema('x') |
Flow\Types\Value\Json::fromArray([], asObject: true) |
uuid_entry('x', $uuid) |
uuid_schema('x') |
type_uuid()->cast($uuid) |
xml_entry('x', '<a/>') |
xml_schema('x') |
type_xml()->cast('<a/>') |
xml_element_entry('x', '<a/>') |
xml_element_schema('x') |
type_xml_element()->cast('<a/>') |
html_entry('x', $html) |
html_schema('x') |
type_html()->cast($html) |
html_element_entry('x', $html) |
html_element_schema('x') |
type_html_element()->cast($html) |
structure_entry('x', $value, $type), struct_entry(...) |
structure_schema('x', $type) |
$value |
list_entry('x', $value, $type) |
list_schema('x', $type) |
$value |
map_entry('x', $value, $type) |
map_schema('x', $type) |
$value |
to_entry('x', $value) |
the *_schema('x') matching $value |
$value |
any *_entry('x', null) |
*_schema('x', nullable: true) |
null |
any *_entry('x', $value, $metadata) |
*_schema('x', metadata: $metadata) |
$value |
31) flow-php/etl - Rows holds only rows that match its schema
| Before | After |
|---|---|
| any row accepted | new Rows($schema, ...$rows) throws Flow\ETL\Exception\SchemaMismatchException on a wrong-typed value, a null in a NOT NULL column, an undeclared column or a missing NOT NULL column |
| - | Flow\ETL\Exception\ColumnMismatchException, the getPrevious() of SchemaMismatchException |
| a row without a declared nullable column | padded with null |
$rows->toArray() - keys in each row's insertion order |
schema order |
$rows->map() / flatMap() / each() / filter() / find() / findOne() / reduce() / sort($callable) |
removed - see below |
$rows->sortEntries() / $df->reorderEntries() / Flow\ETL\Transformer\OrderEntriesTransformer |
removed - $df->select('a', 'b') sets the column order |
Schema::isSame() ignored column order |
order-sensitive |
$rows->merge($other), $other with other columns or another column order - rows concatenated |
throws InvalidArgumentException: Cannot merge Rows with different schemas: [a: integer] and [a: string] |
declared NOT NULL column, empty cell or short line (from_csv($p)->withSchema($schema)) - null |
throws SchemaMismatchException - declare the column nullable: true |
from_rows($a, $b) - each batch kept its own columns |
every batch carries every column of the combined schema, missing ones as null |
batches($extractor, $n) / batched_by(...) over child batches of different shapes - mixed in one batch |
later child batches matched to the first: missing nullable column padded, extra column throws SchemaMismatchException |
Before:
$adults = $rows->filter(fn (Row $row): bool => $row->valueOf('age') >= 18);
After:
$adults = new Rows($rows->schema(), ...array_filter($rows->all(), fn (Row $row): bool => $row->get('age') >= 18));
32) flow-php/etl / flow-php/etl-adapter-excel - Entry parameters become the value and its Definition
| Before | After |
|---|---|
Definition::matches(Entry $entry) |
matches(mixed $value) - the caller checks the name |
Row\Comparator::equals(Row $row, Row $nextRow) |
equals(Row $row, Row $nextRow, Schema $schema) |
RenameEntryStrategy::rename(Row $row): Row |
renames(Schema $schema): array - current_name => new_name, [] when nothing is renamed |
CellStyler::style(Entry $entry, int $rowNumber, int $columnIndex, string $sheetName) |
style(mixed $value, Definition $definition, int $rowNumber, int $columnIndex, string $sheetName) |
new ASCIIValue($value) - an Entry or a scalar/array |
new ASCIIValue(Type $type, mixed $value) |
Dataset\Statistics\Column::__construct(Entry $entry) |
__construct(Definition $definition, mixed $value) |
Dataset\Statistics\Column::calculate(Entry $entry) |
add(Definition $definition, mixed $value) |
Dataset\Statistics\Columns::add(Entry $entry) |
add(Definition $definition, mixed $value) |
new NullRowBuilder(EntryFactory $entryFactory) + ->collect($row) |
new NullRowBuilder(Schema $schema), collect() removed |
new RowsBuffer(int $size) |
new RowsBuffer(Schema $schema, int $size) |
Rows::joinLeft($right, $on, $entryFactory) / Rows::joinRight($right, $on, $entryFactory) |
third argument removed |
Flow\ETL\Row\EntryFactory, Row\Entry\EntryInstantiator, Row\Entry\Instantiators |
removed |
EntryTypeResolver::fromDefinition() |
removed |
33) flow-php/etl - Config options move to the extractors or are removed
| Before | After |
|---|---|
data_frame(config_builder()->putInputIntoRows())->read(from_csv($path)) |
data_frame()->read(from_csv($path)->withMetadataColumns(true)) |
ConfigBuilder::putInputIntoRows() / dontPutInputIntoRows(), Config::shouldPutInputIntoRows() |
removed |
config_builder()->extractorBatchSize(500) |
from_csv($path)->withBatchSize(500) - on each extractor |
Config::extractorBatchSize() |
removed - $extractor->batchSize() |
FlowContext::entryFactory() |
removed |
new Config(...) with $filesystemTable, $putInputIntoRows, $extractorBatchSize, $entryFactory |
those parameters removed, HashRepartitionConfig $repartition added - build through Config::builder() |
withMetadataColumns() is on from_csv(), from_json(), from_json_lines(), from_parquet(), from_text(),
from_xml(), from_excel(), from_google_sheet(), from_google_sheet_columns() and from_floe().
34) flow-php/etl - extractors yield batches
| Before | After |
|---|---|
every extractor yielded one row per Rows |
up to batchSize() rows per Rows: 100 by default, 1000 for DbalLimitOffsetExtractor, DbalKeySetExtractor, PostgreSqlLimitOffsetExtractor, PostgreSqlKeySetExtractor and PostgreSqlCursorExtractor |
Flow\ETL\Pipeline\Optimizer\BatchSizeOptimization re-cut batches to 1000 rows before DbalLoader / PostgreSqlLoader |
removed - $df->batchSize(1000) before write() |
new BatchExtractor($extractor, chunkSize: 10) |
new BatchExtractor($extractor, batchSize: 10) - a size below 1 throws |
$df->duplicateRow(...) appended the copies after the batch's last row |
each copy follows the row it duplicates |
35) flow-php/etl - LimitableExtractor replaced by LimitPushDown
| Before | After |
|---|---|
Flow\ETL\Extractor\LimitableExtractor |
Flow\ETL\Extractor\LimitPushDown |
Flow\ETL\Extractor\Limitable (trait) |
Flow\ETL\Extractor\PushesLimit |
changeLimit(int $limit): void |
pushLimit(int $limit): void - a second push can only lower the limit |
limit(): ?int |
pushedLimit(): ?int |
isLimited(): bool |
removed - pushedLimit() !== null |
incrementReturnedRows() / reachedLimit() / resetLimit() |
removed |
a pushed limit replaced the limit() step, so the extractor had to stop at it |
the limit() step stays; stopping early is optional |
new LimitReachedException($limit, $previous) |
new LimitReachedException($limit, $rows, $previous) - pass previous: by name |
36) flow-php/etl - ErrorHandler answers per stage: extraction, transformation, loading
| Before | After |
|---|---|
ErrorHandler::throw(Throwable $error, Rows $rows): bool |
onExtraction(ExtractionError $error): ExtractionAction - propagate / endSource |
ErrorHandler::skipRows(Throwable $error, Rows $rows): bool |
onTransformation(TransformationError $error): TransformationAction - propagate / skipBatch |
| - | onLoading(LoadingError $error): LoadingAction - propagate / skipLoader |
| an extractor failure always propagated | goes to onExtraction(): IgnoreError and SkipRows end the source, ThrowError rethrows |
IgnoreError: a failed transformation ran the remaining steps on the half-transformed batch |
the batch is dropped |
SkipRows / skip_rows_handler(): a failed transformation emitted the half-transformed batch |
the batch is dropped |
SkipRows: a failed loader was swallowed and the batch's remaining steps skipped |
rethrown |
SkipRows: a drain failure of to_transformation() / to_branch() at closure() was swallowed |
rethrown |
The error objects and the action enums are in Flow\ETL\ErrorHandler.
Before:
final class SkipInvalidBatches implements ErrorHandler
{
public function throw(Throwable $error, Rows $rows): bool
{
return !$error instanceof InvalidArgumentException;
}
public function skipRows(Throwable $error, Rows $rows): bool
{
return true;
}
}
After:
final class SkipInvalidBatches implements ErrorHandler
{
public function onExtraction(ExtractionError $error): ExtractionAction
{
return ExtractionAction::propagate;
}
public function onTransformation(TransformationError $error): TransformationAction
{
return $error->cause instanceof InvalidArgumentException
? TransformationAction::skipBatch
: TransformationAction::propagate;
}
public function onLoading(LoadingError $error): LoadingAction
{
return $error->cause instanceof InvalidArgumentException
? LoadingAction::skipLoader
: LoadingAction::propagate;
}
}
37) flow-php/etl - Cache gains schema()
| Before | After |
|---|---|
| - | Cache::schema(string $key): Schema, throws KeyNotInCacheException for an unknown key |
a custom Flow\ETL\Cache implementation without it |
fatal at load |
38) flow-php/etl - data cached or serialized by 0.43.x cannot be read back
| Before | After |
|---|---|
Rows stored by FilesystemCache, PSRSimpleCache, ApcuCache, PSRCacheBuckets or serialize_to_string() |
cannot be read |
Clear persistent caches and regenerate stored serialize_to_string() payloads after upgrading.
39) flow-php/etl - in-memory sources infer one nullable schema from the first 20,480 rows
| Before | After |
|---|---|
df()->read(from_array([['id' => 1]]))->schema() - id: integer |
id: ?integer; the same for from_memory() and from_sequence_*() |
from_array() / from_memory() / from_sequence_*() typed each row on its own |
columns inferred from the first 20,480 rows |
| a row past the sample whose value the inferred type refuses - kept with its own type | throws Flow\ETL\Exception\InferredSchemaException |
| - | from_array($rows)->inferSchema(infer_schema()->sampleSize(-1)) - infer from every row |
from_array([['a' => 1], ['a' => 2, 'b' => 'x']]) - first row has no b |
first row 'b' => null |
from_array([['a' => 'x'], ['a' => 1000]]) - second row 1000 |
'1000' - values follow the widened column type |
from_array([['tz' => new DateTimeZone('Europe/Warsaw')]]) - 'Europe/Warsaw', column string |
DateTimeZone object, column ?timezone |
from_array([['s' => ['j' => null]]]) - s: structure{j: null} |
s: ?structure{j: ?string} |
from_array([['o' => new stdClass()]]) - InvalidArgumentException on schema() and on read |
schema() gives o: ?string; the read throws SchemaMismatchException |
from_array($generator) - streamed |
read in full into <spillRoot>/flow-php-source/ before the first row; an endless generator never returns |
from_array($generator) read twice - Cannot traverse an already closed generator |
replayed from the spill file |
| - | from_array($generator, $schema) or ->withSchema($schema) - streamed, no spill, one read only |
| - | from_array($generator, spillRoot: path($dir)) - default is the filesystem's system tmp dir |
from_memory($memory) read again after $memory->save() - saw the new columns |
keeps the schema of its first non-empty read; create a new from_memory($memory) |
InferredSchemaException is not a SchemaMismatchException; the mismatch is its getPrevious().
40) flow-php/etl - Hydrator, array_to_row() and array_to_rows() require a Schema
| Before | After |
|---|---|
Hydrator::cast(array $batch, ?Schema $schema = null) |
removed - use hydrate($batch, $schema) |
Hydrator::hydrate(array $batch, ?Schema $schema = null) |
hydrate(array $batch, Schema $schema) |
array_to_row($data, $hydrator, $partitions, $schema) |
array_to_row($data, $schema, $hydrator, $partitions) |
array_to_rows($data, $hydrator, $partitions, $schema) |
array_to_rows($data, $schema, $hydrator) - put partition columns into $data |
array_to_rows($data) |
df()->read(from_array($data))->fetch() |
41) flow-php/etl / flow-php/types - autoCast() removed
| Before | After |
|---|---|
DataFrame::autoCast() |
removed |
Flow\ETL\Transformer\AutoCastTransformer |
removed |
Flow\Types\Type\AutoCaster |
removed |
Before:
df()->read(from_csv($path))->autoCast()->fetch();
df()->read(from_array([['id' => '1', 'total' => '10.5']]))->autoCast()->fetch();
After:
df()->read(from_csv($path))->fetch();
df()->read(from_array([['id' => '1', 'total' => '10.5']], schema(int_schema('id'), float_schema('total'))))->fetch();
from_array() types by PHP value: numeric strings stay ?string unless the schema is declared.
42) flow-php/etl - a value a declared column refuses throws SchemaMismatchException
| Before | After |
|---|---|
from_csv($path)->withSchema($schema), 'n/a' in a datetime column - throws Flow\Types\Exception\CastingException |
throws Flow\ETL\Exception\SchemaMismatchException: Rows do not match their schema: column "d" (row 1): could not convert 'n/a' (string) to datetime |
getPrevious() of the thrown exception |
Flow\ETL\Exception\ColumnMismatchException, never the CastingException |
type_datetime()->cast('n/a') - throws CastingException |
unchanged |
Replace catch (CastingException $e) around a read with catch (SchemaMismatchException $e).
43) flow-php/etl - DataFrame::schema() describes the plan without running it
| Before | After |
|---|---|
$df->schema() ran the pipeline, loaders included, and merged every batch's schema |
derived from the source's schema() through each step; no row is read, no loader runs |
$df->schema() on a plan containing joinEach() - ran the pipeline |
throws DataDependentSchemaException |
$df->schema() over a source that cannot describe its schema, also through from_data_frame($inner) - ran the pipeline |
throws SchemaNotDerivableException |
schema() reported dataFrameCompleted / dataFrameFailed telemetry |
reports neither |
$df->fetch() returning no rows, $df->void()->fetch() - Rows with an empty schema |
Rows carrying the plan's columns |
44) flow-php/etl - printSchema() prints the plan's schema and takes no limit
| Before | After |
|---|---|
printSchema(?int $limit = 20, SchemaFormatter $formatter = new ASCIISchemaFormatter()) |
printSchema(SchemaFormatter $formatter = new ASCIISchemaFormatter()) |
$df->printSchema(20, $formatter) |
$df->printSchema($formatter) |
| ran the pipeline, loaders included, printing one schema per batch | prints $df->schema() once and runs nothing; throws where schema() throws |
$df->printSchema(); $df->fetch() - at most 20 rows |
every row |
45) flow-php/etl - display() and printRows() render one table
| Before | After |
|---|---|
$df->display() / $df->printRows() - one table per batch |
one table |
$df->printRows(null) streamed the frame batch by batch |
collects the whole frame first |
46) flow-php/etl - select() and groupBy() refuse an undeclared column
| Before | After |
|---|---|
$df->select('missing') - added a missing string column of null |
throws SchemaDefinitionNotFoundException: Schema definition for entry "missing" not found. |
$df->groupBy(['missing']) - grouped every row under a null key |
throws SchemaDefinitionNotFoundException |
47) flow-php/etl - renaming onto an existing column throws
| Before | After |
|---|---|
$df->rename('a_b', 'ab') while ab exists - a_b replaced ab |
throws SchemaDefinitionNotUniqueException: Entry definitions must be unique, duplicated entries: [ab], all: [ab, ab] |
$df->renameEach(rename_replace('_', '')) producing an existing name - replaced it |
throws SchemaDefinitionNotUniqueException |
48) flow-php/etl - float values compare exactly
| Before | After |
|---|---|
$rows->unique() / diffLeft() / diffRight() - floats sharing an integer part were equal (1.5 == 1.9) |
exact: 1.5 != 1.9, 0.1 + 0.2 != 0.3 |
rank() / dense_rank() peers over a float column - same rule |
exact |
49) flow-php/etl - Window and window functions are immutable
| Before | After |
|---|---|
$window->partitionBy(...) / ->orderBy(...) / ->rowsBetween(...) changed $window |
return a copy - use the returned window |
$function->over($window) changed $function |
returns a copy |
window()->partitionBy(ref('a'))->partitions() - array<Reference> |
References; ->all() returns the array |
window()->partitionBy(ref('nope')) - every row in one partition |
throws SchemaDefinitionNotFoundException before the first row |
window()->orderBy(ref('nope')) - threw per row |
throws SchemaDefinitionNotFoundException before the first row |
50) flow-php/etl - group and join key extraction reads the schema
| Before | After |
|---|---|
GroupBy::keyValues(Row $row) |
keyValues(Row $row, Schema $input) |
GroupBy::aggregatedRow(GroupKey $key, Aggregators $aggregators, EntryFactory $entryFactory) |
aggregatedRow(GroupKey $key, Aggregators $aggregators, Schema $output) |
KeyValues::ofRow(Row $row) |
ofRow(Row $row, Schema $schema) |
new KeyValues($refs, nullOnMissing: true) |
parameter removed - a missing key is null under a nullable column, throws under NOT NULL |
new HashBucketing(..., nullOnMissing: true) |
parameter removed |
join() over a row without the join column - threw |
the row is unmatched |
51) flow-php/etl - a join producing a duplicate column throws SchemaDefinitionNotUniqueException
| Before | After |
|---|---|
$df->join($right, join_on(['id' => 'id'])), both sides carry name - JoinException (RuntimeException): Merged entries names must be unique, given: [id, name] + [name] try to use a different join prefix than: "" |
SchemaDefinitionNotUniqueException (InvalidArgumentException): Entry definitions must be unique, duplicated entries: [name], all: [id, name, name] |
| thrown when the first pair of rows was merged | thrown when the pipeline starts, before any row is read |
$rows->joinCross($right, '') over shared columns - InvalidArgumentException: ... + [id, name]. Please consider using join prefix option |
SchemaDefinitionNotUniqueException: Entry definitions must be unique, duplicated entries: [id, name], all: [id, name, id, name] |
catch (JoinException $e) around a join |
catch (SchemaDefinitionNotUniqueException $e), or give the right side a prefix: join_on(['id' => 'id'], 'joined_') |
52) flow-php/etl - Joiner takes JoinSide and no EntryFactory, Expression drop helpers removed
| Before | After |
|---|---|
Flow\ETL\Join\Expression::dropDuplicateLeftEntries() / dropDuplicateRightEntries() |
removed |
$joiner->join($left, $right, $nullLeftRow, $nullRightRow, $buildLeft) |
$joiner->join(JoinSide::of($left, $nullLeftRow, $leftSchema), JoinSide::of($right, $nullRightRow, $rightSchema), $buildLeft) |
new Joiner($expression, $type, $entryFactory, $batchSize) |
new Joiner($expression, $type, $batchSize) |
53) flow-php/etl - a join against an empty side keeps that side's columns
| Before | After |
|---|---|
$df->join($emptyRight, $on, Join::left) - right-hand columns dropped |
kept, null |
$df->join($right, $on, Join::right) with an empty left side - left-hand columns dropped |
kept, null |
$rows->joinCross($empty) / $empty->joinCross($rows) - the non-empty side's rows |
zero rows carrying both sides' columns |
54) flow-php/etl - a global aggregate() over zero rows returns one row
| Before | After |
|---|---|
$df->aggregate(sum(ref('a')), count(ref('a'))) over zero rows, or after a filter() removed every row - no rows |
one row, ['a_sum' => null, 'a_count' => 0] |
collect() / collect_unique() / string_agg() over zero rows - no rows |
[] / [] / '' |
$df->groupBy('k')->aggregate(...) over zero rows - no rows |
unchanged |
55) flow-php/etl - pivot() takes its values
Before:
$df->groupBy('date')->pivot(ref('product'))->aggregate(sum(ref('amount')));
After:
$df->groupBy(['date'])->pivot(ref('product'), pivot_values('A', 'B'))->aggregate(sum(ref('amount')));
// or
$df->groupBy(['date'])->pivot(ref('product'), discover_pivot_values())->aggregate(sum(ref('amount')));
| Before | After |
|---|---|
GroupedDataFrame::pivot(Reference $ref) |
pivot(Reference $ref, PivotValues $values) |
GroupBy::pivot(Reference $ref) |
GroupBy::pivot(Reference $ref, DeclaredPivotValues $values) |
| one column per value found in the data | pivot_values(...): one per declared value, null where no row carries it, undeclared values dropped |
| - | discover_pivot_values(): one per non-null value in the frame, sorted, at most maxValues (default 10_000) |
pivot value 0 or '0' - column e00 |
column 0 |
pivot value 7 - column e07 |
column 7 |
| pivot value equal to a group-by column name - its column dropped | throws InvalidArgumentException |
GroupBy::pivotResult($rows, $context, $batchSize) |
removed - (new PivotAggregation($batchSize))->aggregate($rows, $context, $groupBy) |
A custom extractor read by discover_pivot_values() must implement Flow\ETL\Extractor\RewindableExtractor.
56) flow-php/etl - unpack() / array_unpack() take the Schema of the columns they produce
Before:
$df->withEntry('row', ref('row')->unpack(['internal_id']));
After:
$df->withEntry('row', ref('row')->unpack(schema(int_schema('id'), str_schema('name'))));
| Before | After |
|---|---|
ref('x')->unpack($skipKeys, $entryPrefix) |
ref('x')->unpack(Schema $schema) |
array_unpack($array, $skip_keys, $entry_prefix) |
array_unpack($array, Schema $schema) |
every payload key except $skipKeys became a column |
only the declared columns; other keys are dropped |
| column type inferred from each value | the declared type, nullable |
| a declared key missing from one payload - no entry in that row | null |
$entryPrefix prepended to each key |
removed - columns are named <withEntry name>.<key> |
A custom Flow\ETL\Function\ScalarFunction\UnpackResults function must return a StructureType of its columns from
returns().
57) flow-php/etl - on_each() requires an operand that declares its element type
| Before | After |
|---|---|
ref('a')->onEach(ref('element')) with a = [1, 'x'] -> [1, 'x'] |
throws SchemaNotDerivableException |
| - | ref('a')->cast(type_list(type_string()))->onEach(ref('element')) -> ['1', 'x'] |
58) flow-php/etl - isIn() compares numbers loosely and refuses incomparable types
| Before | After |
|---|---|
ref('a')->isIn(lit([1])) with a = '1' - false |
true, as ref('a')->equals(lit(1)) |
ref('a')->isIn(lit([new DateTimeImmutable('2024-01-01')])) with a a string - false |
throws Can't compare '(string == date)' |
ref('a')->isIn(lit([1, 2])) with a = null - false |
null, filter() drops the row |
59) flow-php/etl - Calculator::divide() always returns float
| Before | After |
|---|---|
(new Calculator())->divide(4, 2) - 2 |
2.0 |
ref('a')->divide(lit(2)) over 4 - 2 |
2.0 |
60) flow-php/etl - DataFrame::partitionBy() removed, loaders partition, Rows carry no partitions
| Before | After |
|---|---|
$df->partitionBy('year')->write(to_csv($path)) |
$df->write(to_csv($path)->partitionBy(partition_by('year'))) |
| partitioned write - the partition column is written into the file body as well | path only; partition_by('year')->writeColumns() keeps it in the body |
$df->partitionBy('year') without a partitioned write |
$df->repartition('year') |
$df->dropPartitions() |
removed |
$df->dropPartitions(true) |
$df->drop('year') |
Rows::partitioned() / rows_partitioned() |
removed |
Rows::partitionBy() / partitions() / isPartitioned() / dropPartitions() |
removed |
Flow\ETL\Transformer\DropPartitionsTransformer |
removed |
reading year=2024/month=01/ - partition columns appended in path order (year, month) |
appended in name order (month, year) |
reading year=__HIVE_DEFAULT_PARTITION__/ - year is the string '__HIVE_DEFAULT_PARTITION__' |
null, and year is nullable |
same read with withSchema() declaring year NOT NULL - the string |
throws InvalidArgumentException |
reading year=abc/ with withSchema() declaring year as integer - 0 |
throws InvalidArgumentException naming the column, file, type and value |
display() / printRows() / to_output() printed a Partitions: footer |
no footer |
61) flow-php/etl - filterPartitions() compares the raw string partition value
| Before | After |
|---|---|
filterPartitions(ref('date')->equals(lit(new DateTimeImmutable('2024-01-01')))) - matched date=2024-01-01 |
throws Can't compare '(string == date)'; use lit('2024-01-01'), or from_csv($path)->partitionTypes(partition_types(date: type_date())) |
filterPartitions(ref('active')->equals(lit(false))) - matched active=false |
throws Can't compare '(string == boolean)'; use lit('false'), or partition_types(active: type_boolean()) |
filterPartitions(ref('date')->equals(lit('2024-01-01'))) - throws Can't compare '(date == string)' |
matches date=2024-01-01 |
a DateTimeImmutable or bool literal over files() / from_path_partitions() - matched |
matches nothing; compare a string literal |
new ScalarFunctionFilter($function, $entryFactory, $caster, $context) |
new ScalarFunctionFilter($function, Schema $partitions, $context) |
62) flow-php/etl / flow-php/types - missing-column and type-mismatch messages changed
| Before | After |
|---|---|
Schema definition for entry "x" not found |
Schema definition for entry "x" not found., plus Did you mean one of: [a]? when up to 3 column names are close |
$row->get('x'): Entry "x" does not exist. Did you mean one of the following? ["every", "column"] |
Column "x" does not exist., plus Did you mean one of the following? ["a"] when up to 3 column names are close |
Can't compare '(?integer > ?date)' due to data type mismatch. |
Can't compare '(?integer > ?date)' due to data type mismatch - an explicit cast is required. |
63) flow-php/etl - pipeline and generator extractors and Schema::fromPipeline() removed
| Before | After |
|---|---|
from_pipeline($pipeline) / new PipelineExtractor($pipeline) |
removed - from_data_frame($df) |
Flow\ETL\Extractor\GeneratorExtractor |
removed |
Schema::fromPipeline($pipeline, $context, $maxRows) |
removed - $df->schema() |
64) flow-php/etl - until() stops at the first row failing its predicate
| Before | After |
|---|---|
$df->until(ref('v')->lessThan(lit(3))) over [1, 2, 5, 1, 2] in one batch - [1, 2, 1, 2] |
[1, 2] |
65) flow-php/etl - UnserializeTransformer requires the payload schema
| Before | After |
|---|---|
new UnserializeTransformer($source, $merge, $mergePrefix) |
new UnserializeTransformer($source, $schema, $merge, $mergePrefix) |
| payload missing, not a string, not unserializable or not one row - row returned unchanged | declared payload columns added as null |
66) flow-php/etl - Floe reader drops RowPadding, conform: and lenient:
| Before | After |
|---|---|
Flow\Floe\RowPadding |
removed |
FloeStreamReader::rows($batchSize, $offset, $limit, $conform) |
rows($batchSize, $offset, $limit) |
FrameReader::frames(lenient: true) |
frames() - a truncated stream always throws FloeException |
67) flow-php/etl / flow-php/etl-adapter-parquet - a Floe or Parquet glob checks every file against the first
| Before | After |
|---|---|
from_parquet($glob) / from_floe($glob), a later file with other columns or column types - rows kept that file's columns |
throws InferredSchemaException naming both files - read with ->unionByName() or ->withSchema(...) |
| same read, a later file with the same columns in another order - rows kept that file's order | rows follow the first file's column order |
| - | ->unionByName() reads every file under the merged schema; a column a file lacks is null |
68) flow-php/etl and every package with DSL functions - documentation attributes leave the packages
| Before | After |
|---|---|
Flow\ETL\Attribute\DocumentationDSL |
Flow\Documentation\Attribute\DocumentationDSL |
Flow\ETL\Attribute\DocumentationExample |
Flow\Documentation\Attribute\DocumentationExample |
Flow\ETL\Attribute\Module |
Flow\Documentation\Attribute\Module |
Flow\ETL\Attribute\Type |
Flow\Documentation\Attribute\Type |
No installable package ships Flow\Documentation\Attribute\*: ReflectionAttribute::newInstance() on them throws
Attribute class "..." not found. Read them with getName() / getArguments().
69) flow-php/etl / flow-php/types - TypeMerge moved to flow-php/types as TypeWidener
| Before | After |
|---|---|
Flow\ETL\Schema\Definition\TypeMerge |
Flow\Types\Type\TypeWidener |
merge($left, $right) |
widen($left, $right) |
mergeLists() / mergeMaps() / mergeStructures() |
widenLists() / widenMaps() / widenStructures() |
70) flow-php/types - structure optional fields are declared inline with structure_element()
| Before | After |
|---|---|
type_structure(['id' => type_integer()], ['nick' => type_string()]) |
type_structure(['id' => type_integer(), 'nick' => structure_element('nick', type_string(), optional: true)]) |
type_structure($elements, $optionalElements, true) |
type_structure($elements, true) - an array second argument throws TypeError |
new StructureType($elements, $optionalElements, $allowExtra) |
StructureType::fromElements($elements, $allowExtra) |
StructureType::elements() - array<name, Type>, required fields only |
list<StructureElement>, optional fields included |
$structure->elements()['id'] |
$structure->element('id')?->type |
StructureType::optionalElements() |
removed - read StructureElement::$optional |
normalize() - {"type": "structure", "elements": {...}, "optional_elements": {...}} |
{"type": "structure_v2", "fields": [{"name": ..., "type": ..., "optional": ...}]} |
type_from_array() / schema_from_json() on a structure payload |
throws InvalidArgumentException: Unknown type 'structure' |
Regenerate any stored schema_to_json() output that contains a structure.
71) flow-php/types - time, date, string and Json accept and refuse different values
| Before | After |
|---|---|
type_time()->isValid(new DateInterval('P1M')) - true |
false |
type_time()->cast(new DateInterval('P1M')) - returned as-is |
throws CastingException: ... Relative DateInterval (with months/years) can't be cast to time |
type_time()->cast('12:34:56') - throws CastingException |
DateInterval of 12h 34m 56s |
type_time()->cast('12:34:56.5') - throws CastingException |
DateInterval with f = 0.5 |
type_union(type_time(), type_integer())->cast('12:34:56') - 12 |
DateInterval |
type_date()->isValid(new DateTimeImmutable('2024-01-02 00:00:00.5')) - true |
false |
Json::fromArray([1 => 'a'], asObject: true) - {"1":"a"} |
throws InvalidArgumentException: All keys of a JSON object must be strings |
type_string()->cast(new DateInterval('PT1H2M3S')) - throws CastingException |
'01:02:03'; P1DT2H -> '26:00:00'; a fraction adds .uuuuuu |
72) flow-php/types - StringTypeNarrower types compact digit dates as integer and month-only dates as string
| Before | After |
|---|---|
(new StringTypeNarrower())->narrow('20240101') -> date |
integer |
(new StringTypeNarrower())->narrow('2024-01') -> date |
string |
73) flow-php/filesystem - Partition takes a value and its type, encodes reserved characters and allows null
| Before | After |
|---|---|
Partition::valueFromRow(Reference $ref, Row $row) |
Partition::fromValue(string $name, Type $type, mixed $value): string |
$partition->reference() |
removed |
Flow\ETL\Row\Entry\JsonEntry can't be used as a partition |
Column "d" of type json can't be used as a partition |
Partition::$value - string |
?string |
new Partition('path', 'a/b') - throws Partition value contains one of forbidden characters |
->segment() - path=a%2Fb |
new Partition('a/b', '1') - throws Partition name contains one of forbidden characters |
->segment() - a%2Fb=1 |
value New York written to city=New York/ |
city=New%20York/ |
null partition value on write - throws Partition value can't be empty |
year=__HIVE_DEFAULT_PARTITION__/ |
new Partitions(new Partition('a', '1'), new Partition('a', '2')) |
throws InvalidArgumentException |
Partition / Partitions throw Flow\ETL\Exception\InvalidArgumentException |
throw Flow\Filesystem\Exception\InvalidArgumentException |
74) flow-php/filesystem - SourceStream::readLines() strips the separator and yields nothing for 0 bytes
| Before | After |
|---|---|
memory_filesystem() stream: each line kept its trailing "\n" |
separator stripped |
memory_filesystem() stream: $separator ignored |
honoured |
memory_filesystem() stream: 0 bytes yielded one '' |
yields nothing |
StringSourceStream / MemorySourceStream dropped empty lines |
yield them as '' |
from_text() over an empty memory:// file - one empty row |
no rows |
from_json_lines() over an empty memory:// file - threw |
no rows |
from_csv()->withCharactersReadInLine($n) on memory:// - a line longer than $n split into rows |
one row per line |
A custom SourceStream::readLines() must strip the separator, yield nothing for a 0-byte stream and yield an empty
line as ''.
75) flow-php/parquet - FLOAT32 values read without ext-arrow are no longer rounded to 7 decimals
| FLOAT32 value written | Before (read) | After (read) |
|---|---|---|
0.1 |
0.1 |
0.10000000149011612 |
18.52 |
18.5200005 |
18.520000457763672 |
1.0E-8 |
0.0 |
9.99999993922529E-9 |
76) flow-php/parquet - FlatColumn::decimal() sizes FIXED_LEN_BYTE_ARRAY by the Parquet spec
FlatColumn::decimal($name, $precision) |
Before (bytes) | After (bytes) |
|---|---|---|
| precision 7 | 3 | 4 |
| precision 12 | 5 | 6 |
| precision 19 | 8 | 9 |
| precision 24 | 10 | 11 |
| precision 36 | 15 | 16 |
77) flow-php/postgresql - Client gains describe(), and execute() accepts ConvertedParameters
| Before | After |
|---|---|
a custom Flow\PostgreSql\Client\Client implementation |
adds describe(Sql|string $sql, array $parameters = []): array returning list<array{name: string, type: ColumnType}> |
execute(Sql|string $sql, array $parameters = []): int |
execute(Sql|string $sql, array|ConvertedParameters $parameters = []): int |
A custom Client::execute() binds ConvertedParameters::$values as they are, without a converter.
78) flow-php/postgresql - arrays parsed, oid as integer, timetz without offset, duplicates cast by name
| Before | After |
|---|---|
ResultCaster::cast() returns bool|float|int|string |
returns array|bool|float|int|string |
_int4 value '{1,NULL,3}' |
[1, null, 3] - every _-prefixed array type, elements cast by element type |
oid value '42' |
42 |
timetz value '12:34:56+02' |
'12:34:56' |
SELECT 1::int8 AS a, 'x' AS a - a is 0 |
'x' |
SELECT 'x' AS a, 1::int8 AS a - a is '1' |
1 |
Applies to rows returned by Client::fetch*() and Client::cursor().
79) flow-php/doctrine-dbal-bulk - Dialect requires maxBindParameters()
| Before | After |
|---|---|
a custom Flow\Doctrine\Bulk\Dialect\Dialect implementation |
adds public function maxBindParameters(): int - the platform's bind-parameter limit, at least 1 |
80) flow-php/etl-adapter-csv, -json, -excel, -google-sheet - a read without a schema infers one schema
| Before | After | |
|---|---|---|
schema of a read without withSchema() |
each yielded Rows typed from its own values |
one schema for the whole read, inferred from a sample |
| nullability of an inferred column | nullable only where that Rows held a null |
always nullable (?integer, ?string, ...) |
| sample | - | first 20,480 rows over the first 10 files; Google Sheet: first 100 rows |
| value past the sample that the inferred type refuses | read, typed on its own | throws Flow\ETL\Exception\SchemaMismatchException |
| CSV/Excel file or Google Sheet whose header differs from the inferred one (header-only files too) | read with its own columns | throws Flow\ETL\Exception\InferredSchemaException |
| row key order | the row's own; a body column named like a partition is overwritten in place | the schema's: body columns (JSON: first seen across the sample), then partition columns |
flow file:schema over CSV / Excel |
every column string, nullable: false unless a value was empty |
typed, every column nullable: true |
| infer from every row | - | ->inferSchema(infer_schema()->sampleSize(-1)) |
| infer from every file | - | ->inferSchema(infer_schema()->filesToSniff(-1)) |
every column ?string |
- | ->inferSchema(infer_schema()->allStrings()) |
| files with different columns | - | ->inferSchema(infer_schema()->unionByName()) |
| no inference | - | ->withSchema($schema) |
On from_google_sheet(), ->inferSchema(infer_schema()) without ->sampleSize() samples 20,480 rows, not 100.
81) flow-php/etl-adapter-csv - cells are typed, not strings
| Cell | Before | After |
|---|---|---|
1 |
string |
?integer |
1.5 |
string |
?float |
true / false |
string |
?boolean |
20240101 |
string |
?integer |
2024-01-01 / 2024-01-01 10:00:00 |
string |
?date / ?datetime |
| uuid / JSON text | string |
?uuid / ?json |
01234, 2024-01, 12 (padded) |
string |
?string |
Keep every column a string: from_csv($path)->inferSchema(infer_schema()->allStrings()).
82) flow-php/etl-adapter-doctrine - from_dbal_*() type columns from the query
| Driver | Column | Before | After |
|---|---|---|---|
pgsql |
NUMERIC |
string |
float |
pgsql |
DATE, TIMESTAMP, TIMESTAMPTZ |
string |
DateTimeImmutable |
pgsql |
TIME |
string |
DateInterval |
pgsql |
UUID |
string |
Flow\Types\Value\Uuid |
pgsql |
JSON, JSONB |
string |
Flow\Types\Value\Json |
pgsql |
XML |
string |
DOMDocument |
pgsql |
any other type (INTERVAL, INET, MONEY, OID, arrays, ranges, enums, ...) |
string |
throws SchemaNotDerivableException |
mysqli |
DECIMAL |
string |
float |
mysqli |
DATE, DATETIME, TIMESTAMP |
string |
DateTimeImmutable |
mysqli |
TIME |
string |
DateInterval |
mysqli |
JSON |
string |
Flow\Types\Value\Json |
mysqli |
any other type (BIT, GEOMETRY, ...) |
string |
throws SchemaNotDerivableException |
sqlite3, pdo_sqlite |
INTEGER, REAL |
1, 1.5 |
'1', '1.5' - every column is ?string |
pdo_pgsql, pdo_mysql, any other |
every column | driver values | throws SchemaNotDerivableException - use the pgsql / mysqli driver or ->withSchema(...) |
Applies to from_dbal_query(), from_dbal_queries(), from_dbal_limit_offset(), from_dbal_limit_offset_qb() and
from_dbal_key_set_qb() without ->withSchema(). A missing table or column throws what the read throws
(TableNotFoundException, InvalidFieldNameException, on SQLite DriverException); any other query the driver
refuses to describe throws
SchemaNotDerivableException with the DBAL exception as getPrevious(). Declare ->withSchema(...) to pick the types.
83) flow-php/etl-adapter-doctrine, -postgresql - withPageSize() / withFetchSize() become withBatchSize()
| Before | After |
|---|---|
from_pgsql_cursor(...)->withFetchSize(500) |
from_pgsql_cursor(...)->withBatchSize(500) |
from_pgsql_limit_offset(...)->withPageSize(500) |
->withBatchSize(500) |
from_pgsql_key_set(...)->withPageSize(500) |
->withBatchSize(500) |
from_dbal_limit_offset(...)->withPageSize(500), from_dbal_limit_offset_qb(...)->withPageSize(500) |
->withBatchSize(500) |
from_dbal_key_set_qb(...)->withPageSize(500) |
->withBatchSize(500) |
Page size must be greater than 0, got 0 / Fetch size must be greater than 0, got 0 |
Batch size must be greater than 0, got 0 |
84) flow-php/etl-adapter-excel - text cells narrow to uuid, json and timezone, to_excel() writes date cells
| Before | After | |
|---|---|---|
| text cell holding a uuid / JSON / timezone name | string |
?uuid / ?json / ?timezone |
any other text cell (TRUE, 12.9, 2023-10-02) |
string |
?string |
blank cell under withConvertEmptyToNull(false) |
'' in that row |
the whole column is ?string (1 reads as '1') |
to_excel() date / datetime value |
text cell | date cell |
to_excel()->withDateFormat() / ->withDateTimeFormat() |
PHP date() format, default 'Y-m-d' / 'Y-m-d H:i:s' |
Excel number format, default 'yyyy-mm-dd' / 'yyyy-mm-dd hh:mm:ss' |
to_excel() then from_excel(), date / datetime column |
string |
?date / ?datetime |
| ZIP-signed file without an extension that is not a workbook | ValueError: Invalid or uninitialized Zip object |
Flow\ETL\Exception\InvalidArgumentException: Unsupported file format: n/a |
Before:
to_excel($path)->withDateFormat('Y-m-d')->withDateTimeFormat('Y-m-d H:i:s');
After:
to_excel($path)->withDateFormat('yyyy-mm-dd')->withDateTimeFormat('yyyy-mm-dd hh:mm:ss');
85) flow-php/etl-adapter-excel - requires openspout/openspout ~5.3.0
| Dependency | Before | After |
|---|---|---|
openspout/openspout |
^5.2 |
~5.3.0 |
86) flow-php/etl-adapter-google-sheet - FORMATTED_VALUE cells are typed, and '' reads as null
| Before | After | |
|---|---|---|
cell under FORMATTED_VALUE (the default) |
string |
typed as a CSV cell, see 81): '1234' -> ?integer, 'TRUE' -> ?boolean, '2024-01-01' -> ?date |
'' cell |
'' |
null; keep '' with ->withEmptyToNull(false) |
87) flow-php/etl-adapter-http - response_body and request_body hold the raw body text
| Before | After |
|---|---|
response_body / request_body of a JSON or XML message - decoded array |
?string |
a JSON body that fails to decode - threw RuntimeException |
kept as text |
Decode in the pipeline: ->withEntry('response_body', ref('response_body')->jsonDecode()).
88) flow-php/etl-adapter-json - schema_from_json_schema() rejects a property with more than one non-null type
| Before | After |
|---|---|
anyOf / oneOf of string and integer - integer|string column |
throws Flow\ETL\Exception\UnsupportedUnionTypeException |
enum: ["a", 1] - integer|string column |
throws UnsupportedUnionTypeException |
type: ["string", "integer"] - integer|string column |
throws UnsupportedUnionTypeException |
anyOf of string and null - ?string |
unchanged |
Give the property a single type in the JSON Schema, or declare the column by hand: str_schema('v'),
json_schema('v').
89) flow-php/etl-adapter-json - keys outside the sample are dropped, missing keys are null
| Before | After | |
|---|---|---|
| key only a later file carries | kept in that file's rows | dropped; keep it with ->inferSchema(infer_schema()->unionByName()) |
| key first seen past the sample | kept in that row | dropped; keep it with ->inferSchema(infer_schema()->sampleSize(-1)) |
| key missing from a record | absent from that row | null |
| nested key every sampled record carries, missing later | read | throws SchemaMismatchException |
| nested key first seen past the sample | kept | dropped |
0-byte file, from_json() |
throws JsonMachine\Exception\SyntaxErrorException |
skipped, no rows |
Applies to from_json() and from_json_lines() unless the row names one.
90) flow-php/etl-adapter-json - from_json_lines() skips blank lines
| Before | After |
|---|---|
an empty or whitespace-only line - threw SyntaxErrorException |
skipped, in schema() and extract() |
91) flow-php/etl-adapter-parquet - to_parquet() does not validate values against the Parquet schema
| Before | After |
|---|---|
to_parquet($path) wrote with Option::VALIDATE_DATA true |
false |
| - | to_parquet($path)->withOptions(Options::default()) validates again |
92) flow-php/etl-adapter-postgresql - from_pgsql_*() type columns from the query instead of reading strings
| PostgreSQL column | Before | After |
|---|---|---|
numeric |
string |
float |
date, timestamp, timestamptz |
string |
DateTimeImmutable |
time |
string |
DateInterval |
timetz |
string with offset |
DateInterval, offset dropped |
uuid |
string |
Flow\Types\Value\Uuid |
json, jsonb |
string |
Flow\Types\Value\Json |
xml |
string |
DOMDocument |
oid |
string |
int |
one-dimensional array (int4[], text[], ...) |
string '{1,NULL,3}' |
[1, null, 3], null elements kept |
| multi-dimensional array | string |
throws while hydrating |
record, point, line, lseg, box, path, polygon, circle |
string |
throws SchemaNotDerivableException |
Applies to from_pgsql_cursor(), from_pgsql_limit_offset() and from_pgsql_key_set() without ->withSchema(). A
missing table, column, function, type or privilege throws PostgreSQL's QueryException, as the read does; a query
that writes (a data-modifying WITH, SELECT ... INTO) throws InvalidArgumentException before any query runs.
Declare ->withSchema(...) to pick the types.
93) flow-php/etl-adapter-postgresql - pgsql_table_to_flow_schema() maps arrays, text-like types, oid, timetz
| PostgreSQL column | Before | After |
|---|---|---|
inet, cidr, macaddr, money, int4range, int8range, numrange, tsrange, tstzrange, daterange |
throws TypeMappingException |
string |
oid |
throws TypeMappingException |
integer |
timetz (time with time zone) |
throws TypeMappingException |
time |
integer[], any array column |
its element type, integer |
list<?integer> |
Applies to pgsql_table_to_flow_schema() and EntryTypesMap::toFlowType().
| Before | After |
|---|---|
TypeMappingException::ambiguousEntryType() / ::unsupportedEntryType() |
removed |
94) flow-php/etl-adapter-postgresql - InsertQueryBuilder::build() takes the client's converters
| Before | After |
|---|---|
$builder->build($values, $schema, $options) |
$builder->build($values, $schema, $client->converters(), $options) |
returns array{Sql, list<?TypedValue>} |
returns array{Sql, ConvertedParameters} - pass the second element to $client->execute() |
95) flow-php/etl-adapter-xml - from_xml() keeps each element as the source wrote it
| Before | After |
|---|---|
node re-serialized with added indentation; comments dropped |
the element's source text, whitespace and comments included |
every ancestor namespace declared on node |
only the ancestor namespaces the element uses |
| a truncated document returned the elements before the cut | throws RuntimeException |
| expat error messages | libxml error messages, e.g. XML Error: Premature end of data in tag root line 1 at line 1 |
XMLParserExtractor::startElementHandler() / endElementHandler() / characterDataHandler() |
removed |
96) flow-php/cli - --schema-auto-cast removed, flow schema ignores the row window
| Before | After |
|---|---|
--schema-auto-cast on file:analyze, file:convert, file:read, file:schema |
removed |
flow schema <file> --input-file-offset=N / --input-file-limit=N - schema of the selected rows |
the file's schema; both options have no effect |
97) flow-php/cli - command classes name themselves with #[AsCommand]
| Before | After |
|---|---|
new FileConvertCommand() - named file:read |
file:convert |
new FileRowsCountCommand() - named file:schema |
file:rows:count |
new PipelineRunCommand() - named run |
pipeline:run |
Flow\CLI\Command\* classes carried no aliases |
run, read, schema, count, convert, analyze, format, as the flow binary registers them |
Registering the commands in your own console application: drop the setName() / setAliases() calls.
98) flow-php/flow-php-ext - the flow_php extension must be reinstalled
| Before | After |
|---|---|
flow_php extension 0.1.0 |
same version as this flow-php/etl release |
Reinstall it with the new release: pie install flow-php/flow-php-ext.
99) flow-php/etl-adapter-csv - withSeparator(), withEnclosure() and withEscape() take a single byte
| Before | After |
|---|---|
withSeparator('||') / withEnclosure('||') / withEscape('ab') - PHP 8.3: first byte used; PHP 8.4+: ValueError on read |
throws Flow\ETL\Exception\InvalidArgumentException |
withSeparator('') / withEnclosure('') - PHP 8.3: , / " used; PHP 8.4+: ValueError on read |
throws Flow\ETL\Exception\InvalidArgumentException |
100) flow-php/etl - array_expand() nested in an expression gives rows, and is refused outside withEntry()
| Before | After |
|---|---|
withEntry('s', concat(ref('id'), ref('tags')->expand())) - one row, list JSON-encoded: a["x","y"] |
one row per element: ax, ay |
withEntry('v', ref('lists')->expand()->expand()) - one row per outer element, inner list JSON-encoded |
throws InvalidArgumentException - expand one level per withEntry() |
array_expand() in filter(), until(), duplicateRow(), aggregate(), over() |
throws InvalidArgumentException at schema() / run() |
array_expand() in an onEach() body |
throws InvalidArgumentException when onEach() is called |
101) flow-php/array-dot - \ escapes every path grammar character, array_dot_steps() deprecated
| Before | After |
|---|---|
\?x, a\*b, a\\ read keys \?x, a\*b, a\\ |
read keys ?x, a*b, a\ |
?{a} reads key {a} |
throws InvalidPathException - ?\{a\} |
x.*.a\.b reads a -> b of each element |
reads key a.b of each element |
x.{a\.b,c} result keys a__ESCAPED_DOT__b, c |
a.b, c |
x.{a?b} result key ab |
a?b |
array_dot_steps($path) |
deprecated - Path::fromString($path)->steps |
102) flow-php/array-dot - array_dot_set() and array_dot_rename() change only the addressed key
| Before | After |
|---|---|
array_dot_set(['a' => ['x' => 1]], 'a.y', 2) -> ['a' => ['y' => 2]] |
['a' => ['x' => 1, 'y' => 2]] |
array_dot_set([5 => 'a', 7 => 'b'], '5', 'c') -> ['a', 'b', 'c'] |
[5 => 'c', 7 => 'b'] |
array_dot_set([], '\{a\}', 1) / '?a' write keys \{a\} / ?a |
write keys {a} / a |
array_dot_set($array, 'x.*', $value) throws |
sets every element of x |
array_dot_set($array, 'x.{a,b}', $value) writes key {a,b} |
throws InvalidPathException |
array_dot_rename(['{a}' => 1], '\{a\}', 'b') -> warning, ['{a}' => 1, 'b' => null] |
['b' => 1] |
array_dot_rename(['a' => 1], '?missing', 'b') -> warning, ['a' => 1, 'b' => null] |
['a' => 1] |
array_dot_rename($array, 'x.*', 'c') / 'x.{a,b}' -> warning, adds c => null under x |
throws InvalidPathException |
array_dot_set(['x' => 5], 'x.*', 1) throws InvalidPathException |
['x' => []] |
array_dot_set([], 'x.*', 1) throws InvalidPathException |
['x' => []] |
array_dot_rename(['a' => 1, 'b' => 2], 'a', 'a') -> ['b' => 2] |
['a' => 1, 'b' => 2] |
103) flow-php/etl - array_get_collection() reads its keys as literal keys
| Before | After |
|---|---|
key a.b reads a -> b, result key a_b |
reads key a.b, result key a.b |
key k,l throws InvalidArgumentException |
reads key k,l |
104) flow-php/etl - array_expand() over a null list gives no rows
| Before | After |
|---|---|
null list throws InvalidArgumentException |
no rows for that row |
105) flow-php/array-dot, flow-php/etl - a nullsafe multimatch over an empty array reads null
| Before | After |
|---|---|
array_dot_get([], '{?a}') throws InvalidPathException |
['a' => null] |
array_dot_get([], '{a}') message Path "{a}" does not exists ... |
Path "a" does not exists ... |
array_get_collection(ref('c'), ['id']) over [['name' => 'a'], []] throws InvalidArgumentException |
[['id' => null], ['id' => null]] |
106) flow-php/filesystem - every filesystem matches glob patterns the same way
Files: data/flat.parquet, data/.hidden.parquet, data/.dir/x.parquet, data/date=2026-09-01/one.parquet,
data/id=1/date=2026-09-01/two.parquet
| pattern | local, before | memory / S3 / Azure, before | every filesystem, after |
|---|---|---|---|
data/**.parquet |
flat |
all five | .hidden, flat |
data/**/*.parquet |
all five | .dir/x, one, two |
all five |
data/** |
flat |
all five | all five |
data/*.parquet |
flat |
.hidden, flat |
.hidden, flat |
data/*/*.parquet |
one |
.dir/x, one |
.dir/x, one |
data/**/[!f]*.parquet |
flat |
none | .dir/x, .hidden, one, two |
data/[a-g]*.parquet |
flat |
none | flat |
Recurse with data/**/*.parquet, not data/**.parquet. webmozart/glob is no longer installed with
flow-php/filesystem or flow-php/etl.
107) flow-php/parquet - ParquetEngine::openForWrite() returns a ParquetFileWriter
| Before | After |
|---|---|
$engine->openForWrite($stream, $schema, $c, $o); $engine->writeBatch($rows); $engine->closeWrite(); |
$file = $engine->openForWrite($stream, $schema, $c, $o); $file->writeBatch($rows); $file->close(); |
ParquetEngine::closeWrite(), writeBatch(), writeRow() |
removed - on ParquetFileWriter |
two Writers sharing one engine overwrote each other |
every openForWrite() returns an independent writer |
ArrowParquetEngine left the destination stream open |
ParquetFileWriter::close() closes it on every engine |
108) flow-php/etl-adapter-parquet - path-only partition columns leave the file body
| Before | After |
|---|---|
partitionBy(partition_by('date')) - file body carries an all-null date column |
file body without date |
from_parquet() types date from that body column, e.g. datetime |
string - declare it: from_parquet($path)->partitionTypes(partition_types(date: type_datetime())) |
109) flow-php/etl-adapter-postgresql - a failed from_pgsql_cursor() read throws its own error and rolls back
| Before | After |
|---|---|
QueryException [25P02] Invalid transaction state. SQL: CLOSE flow_cursor_..., real error in getPrevious() |
the failing statement's own QueryException |
the client left inside an aborted transaction - every later query fails with 25P02 |
the extractor's own transaction rolled back |
| a failure while reading rows (e.g. a row that does not match the schema) committed the transaction | rolled back |
110) flow-php/etl-adapter-postgresql - from_pgsql_*() read exactly one read-only SELECT or VALUES statement
| Query | Before | After |
|---|---|---|
INSERT ... RETURNING through from_pgsql_cursor() |
the PHP process crashes (segfault) | InvalidArgumentException, nothing runs |
INSERT ... RETURNING through from_pgsql_key_set() |
the INSERT runs, then QueryException 08P01 |
InvalidArgumentException, nothing runs |
two statements through from_pgsql_cursor() |
the second statement silently dropped | InvalidArgumentException |
two statements through from_pgsql_limit_offset() / from_pgsql_key_set() |
QueryException 42601 |
InvalidArgumentException |
a data-modifying WITH through from_pgsql_key_set() |
the write runs once per page - an INSERT twice, an UPDATE never ends |
InvalidArgumentException, nothing runs |
SELECT ... INTO through from_pgsql_key_set() |
the table is created | InvalidArgumentException, nothing runs |
either through from_pgsql_cursor() / from_pgsql_limit_offset() |
QueryException after a round trip |
InvalidArgumentException, nothing runs |
111) flow-php/postgresql - declare_cursor() over SQL takes exactly one SELECT or VALUES
| Before | After |
|---|---|
declare_cursor('c', 'INSERT INTO t VALUES (1) RETURNING id')->toSql() - segfault |
InvalidArgumentException |
declare_cursor('c', 'SELECT 1; SELECT 2') - the second statement silently dropped |
InvalidArgumentException |
112) flow-php/postgresql - SelectStatement::hasIntoClause() sees SELECT ... INTO in a UNION / INTERSECT / EXCEPT
sql_parse($sql)->statements()->first()->hasIntoClause(), $sql |
Before | After |
|---|---|---|
SELECT id INTO t FROM x UNION SELECT 1 |
false |
true |
113) flow-php/postgresql - sql_to_*_query() and the pagination modifiers take exactly one read-only SELECT or VALUES
| Before | After |
|---|---|
sql_to_paginated_query('UPDATE t SET a = 1 RETURNING id', 10) - returned unchanged |
InvalidStatementException |
sql_to_limited_query('SELECT 1; SELECT 2', 10) - every statement paginated |
InvalidStatementException |
sql_to_keyset_query() over a data-modifying WITH - the write paginated |
InvalidStatementException |
sql_to_count_query('SELECT id INTO t FROM x') - counted |
InvalidStatementException |
Applies to PaginationModifier, CountModifier and KeysetPaginationModifier passed to ParsedQuery::traverse().
114) flow-php/etl-adapter-postgresql - from_pgsql_limit_offset() requires the query's own ORDER BY
| Query | Before | After |
|---|---|---|
SELECT * FROM (SELECT id FROM t ORDER BY id) s |
pages in no defined order | InvalidArgumentException |
Upgrading from 0.42.x to 0.43.x
1) flow-php/etl - to_transformation() expands a Transformation once per loader, not once per batch
| Before | After |
|---|---|
to_transformation(limit(3), $loader), 6 batches → 6 rows loaded |
3 rows loaded |
to_transformation(add_row_index('n'), $loader), 6 batches → n = [1,1,1,1,1,1] |
n = [1,2,3,4,5,6] |
nested DataFrame span per batch |
one nested span per run |
Unchanged: any pipeline using ->collect(), drop(), select(), mask_columns(), batch_size(), batch_by().
2) flow-php/telemetry - Tracer::span() no longer activates the span
| Before | After |
|---|---|
$tracer->span('x') makes the span current |
does not; $tracer->activate($span): Scope does |
$tracer->complete($span) also detaches the scope |
ends the span only |
span(..., SpanContext $parentContext) |
span(..., Context $parent) |
trace(..., SpanContext $parentContext) |
trace(..., Context $parent) |
Scope::detach() always returns 0 |
returns Scope::DETACHED, Scope::INACTIVE or Scope::MISMATCH |
Call sites relying on implicit nesting still compile and silently produce siblings. Rewrite each one that needs children:
Before:
$span = $tracer->span('parent');
try {
// ...
} finally {
$tracer->complete($span);
}
After:
$span = $tracer->span('parent');
$scope = $tracer->activate($span);
try {
// ...
} finally {
$scope->detach();
$tracer->complete($span);
}
3) flow-php/etl - window aggregates use the SQL default frame
On d = 2024-01-01, 2024-01-02, 2024-01-03, 2024-01-04 and s = 100, 200, 300, 400:
| Before | After |
|---|---|
sum(ref('s'))->over(window()->orderBy(ref('d'))) → 1000, 1000, 1000, 1000 |
100, 300, 600, 1000 |
average(), count() over an ordered window - whole partition |
rows up to the current row's peers |
window()->partitionBy(ref('dept')) - whole partition |
unchanged |
| empty frame | sum()/average() → null, count() → 0 |
Restore the previous result:
sum(ref('s'))->over(window()->orderBy(ref('d'))->rowsBetween(unbounded_preceding(), unbounded_following()));
4) flow-php/etl - count() over a window is SQL COUNT
On s = 100, 100, 300 ordered by a distinct column:
| Before | After |
|---|---|
count(ref('s')) counts rows sharing the current row's value → 2, 2, 1 |
counts non-null values in the frame → 1, 2, 3 |
count() threw Count WindowFunction function requires a reference. |
counts every row in the frame (COUNT(*)) |
5) flow-php/etl - partitionBy() no longer sets orderBy()
| Before | After |
|---|---|
window()->orderBy(ref('date'))->partitionBy(ref('dept'))->order() → ['dept'] |
['date'] |
window()->partitionBy(ref('dept'))->order() → ['dept'] |
[] |
rank()/dense_rank()/row_number() over a partitionBy()-only window ranked by the partition column |
throws ... requires to be ordered by one column |
Add the ordering explicitly:
rank()->over(window()->partitionBy(ref('dept'))->orderBy(ref('salary')->desc()));
6) flow-php/etl - WindowFunction::apply() receives a WindowContext
| Before | After |
|---|---|
apply(Row $row, Rows $partition, FlowContext $context) |
apply(WindowContext $window) |
$row |
$window->row() |
$partition |
$window->partition() |
$context |
$window->flowContext() |
| - | $window->frame() - rows within the current row's frame |
| - | $window->index() - position in the ordered partition |
row_number() on duplicate rows → 1, 1, 3 |
1, 2, 3 |
Implementations must no longer sort; $window->partition() and $window->frame() are already ordered.
7) flow-php/postgresql - query recording moved to flow-php/symfony-postgresql-bundle
| Before | After |
|---|---|
Flow\PostgreSql\Client\Debug\RecordingClient |
Flow\Bridge\Symfony\PostgreSqlBundle\Profiler\ProfilerClient |
Flow\PostgreSql\Client\Debug\QueryLog |
Flow\Bridge\Symfony\PostgreSqlBundle\Profiler\QueryRecorder |
Flow\PostgreSql\Client\Debug\QueryLogOptions |
Flow\Bridge\Symfony\PostgreSqlBundle\Profiler\QueryRecorderOptions |
Flow\PostgreSql\Client\Debug\RecordedQuery |
Flow\Bridge\Symfony\PostgreSqlBundle\Profiler\RecordedQuery |
QueryLogOptions::$maxParameters |
QueryRecorderOptions::$maxRetainedParameters |
QueryLogOptions::maxParameters() |
QueryRecorderOptions::maxRetainedParameters() |
service flow.postgresql.profiler.query_log |
flow.postgresql.profiler.query_recorder |
config flow_postgresql.profiler.max_parameters |
flow_postgresql.profiler.max_retained_parameters |
Applies to flow-php/postgresql users only through the bundle; Client\Telemetry is unchanged.
8) flow-php/symfony-telemetry-bundle - cache pools and PSR-18 clients that were silently skipped are now traced
| Before | After |
|---|---|
cache.system, cache.validator, cache.serializer, cache.property_info, cache.app, cache.doctrine.*, cache.http_client.pool - not traced |
traced: cache.* spans and flow.cache.hits / flow.cache.misses |
pool or client whose class is a %parameter% - not traced |
traced |
tag-aware pool whose class is a %parameter% - got the non-tag-aware decorator |
gets TagAwareTraceableCacheAdapter |
| PSR-18 client behind an autoconfigured or abstract parent definition - container build failed with "has a reference to an abstract definition" | compiles; the client is traced |
instrumentation.cache.exclude_pools entries for framework pools - had no effect |
take effect |
To keep the previous set of traced pools, exclude the framework's own:
flow_telemetry:
instrumentation:
cache:
exclude_pools:
- 'cache.system'
- 'cache.validator'
- 'cache.serializer'
- 'cache.property_info'
- '/^cache\.doctrine\..*/'
- 'cache.http_client.pool'
9) flow-php/types - a Type implementation's generic parameter is the value it represents
| Before | After |
|---|---|
ListType<string> |
ListType<list<string>> |
MapType<string, int> |
MapType<array<string, int>> |
StructureType<mixed> |
StructureType<array<array-key, mixed>> |
ClassStringType<Foo> |
ClassStringType<class-string<Foo>> |
ListType::element(): Type<T> |
Type<value-of<T>> |
MapType::key(): Type<TKey> |
Type<key-of<T>> |
MapType::value(): Type<TValue> |
Type<value-of<T>> |
type_string(): Type |
type_string(): StringType |
Update ListType, MapType, StructureType and ClassStringType parameters in your own docblocks.
Upgrading from 0.41.x to 0.42.x
1) flow-php/symfony-telemetry-bundle - messenger tracing simplified
| Before (0.41) | After (0.42) |
|---|---|
instrumentation.messenger.trace: worker/handlers/both/none |
instrumentation.messenger.trace: true/false (default true) |
instrumentation.messenger.link: dispatcher/worker/both |
removed |
messenger.receive worker-cycle span |
removed |
The worker-cycle span and the trace: worker/both modes are gone; a consumed message is a process span and a
produced one a send span. The messenger:consume worker loop is suppressed via
instrumentation.console.exclude_commands - which now fully suppresses matching commands (previously it only
skipped the console span) and defaults to ['messenger:consume'] - so transport-poll/idle-tick work does not surface
and the long-lived messenger:consume console span is dropped. Per-message handler traces are still recorded. Set
console.exclude_commands: [] to trace the worker loop.
2) flow-php/symfony-telemetry-bundle - cache span names unified to dotted lowercase
| Before | After |
|---|---|
Cache Commit {pool} |
cache.commit |
Cache Save {key} {pool} |
cache.save |
Cache SaveDeferred {key} {pool} |
cache.save_deferred |
Cache Delete {key} {pool} |
cache.delete |
Cache DeleteItem {key} {pool} |
cache.delete_item |
Cache DeleteItems {pool} |
cache.delete_items |
Cache Clear {pool} |
cache.clear |
Cache Prune {pool} |
cache.prune |
Cache Reset {pool} |
cache.reset |
Cache InvalidateTags {pool} |
cache.invalidate_tags |
cache.operation: saveDeferred/deleteItem/deleteItems/invalidateTags |
save_deferred/delete_item/delete_items/invalidate_tags |
Cache spans now match the DBAL/messenger convention (dotted lowercase, low cardinality). The {key} and {pool}
that were baked into the span name move out of it - they were already available as the cache.key and cache.pool
attributes. Rename these series in dashboards and alerts, and update any filters on the camelCase cache.operation
values.
3) flow-php/telemetry - Sampler::shouldSample() receives the parent Context
| Before | After |
|---|---|
shouldSample(Span $span): SamplingResult |
shouldSample(Context $parentContext, Span $span): SamplingResult |
$sampler->shouldSample($span) |
$sampler->shouldSample($context, $span) |
Custom Sampler implementations (including a sampler: { type: service } service in
flow-php/symfony-telemetry-bundle) must update the signature and forward $parentContext to any delegated sampler.
4) flow-php/telemetry - ResettableContextStorage removed; MemoryContextStorage::reset() removed
| Before | After |
|---|---|
Flow\Telemetry\Context\ResettableContextStorage |
removed |
MemoryContextStorage::reset() |
removed |
The context storage is no longer tagged kernel.reset in flow-php/symfony-telemetry-bundle; scope balance is
maintained by attach/detach alone. A custom context_storage service no longer needs a reset() method.
5) flow-php/symfony-telemetry-bundle - messenger tracing middleware auto-injected into all buses
The tracing middleware is now injected into every message bus automatically. If you previously added
flow.telemetry.messenger.middleware to a bus's framework.messenger.buses.*.middleware list by hand, remove it to
avoid duplicate spans.
6) flow-php/symfony-telemetry-bundle - http_kernel.exclude_paths now suppresses the whole request
| Before | After |
|---|---|
| Excluded path only skips its own request span | Excluded path suppresses tracing for the entire request |
DBAL/cache/kernel.terminate work still recorded |
DBAL/cache/kernel.terminate work produces no spans |
An excluded path now attaches the OpenTelemetry suppression key for the request's duration (through
kernel.terminate), so lower-level auto-instrumentation and terminate-phase database writes no longer emit orphan root
spans (e.g. the /_wdt toolbar fetch writing an audit row after its response). If you relied on those child spans being
recorded for an excluded path, remove the path from exclude_paths.
7) flow-php/postgresql, flow-php/symfony-postgresql-bundle - traceTransactions bool replaced by
transactionSpans mode
| Before | After |
|---|---|
postgresql_telemetry_options(traceTransactions: true) |
postgresql_telemetry_options(transactionSpans: TransactionSpanMode::GROUPED) |
postgresql_telemetry_options(traceTransactions: false) |
postgresql_telemetry_options(transactionSpans: TransactionSpanMode::OFF) |
PostgreSqlTelemetryOptions 2nd arg bool $traceTransactions |
TransactionSpanMode $transactionSpans |
$options->traceTransactions(false) |
$options->transactionSpans(TransactionSpanMode::OFF) |
$options->traceTransactions (property) |
$options->transactionSpans (TransactionSpanMode) |
config telemetry.trace_transactions: true |
config telemetry.transaction_spans: grouped |
config telemetry.trace_transactions: false |
config telemetry.transaction_spans: off |
TransactionSpanMode::PER_OPERATION emits a short span per BEGIN/COMMIT/ROLLBACK; GROUPED (default) keeps the
single long-lived transaction span.
The db.client.operation.duration metric now uses only low-cardinality dimensions: queries are tagged with
db.system.name, db.namespace, db.operation.name, db.collection.name; transactions with db.system.name,
db.namespace, db.operation.name (begin/commit/rollback), db.transaction.nesting_level. db.query.text,
db.query.parameter.*, server.address and db.transaction.savepoint are no longer metric dimensions.
8) flow-php/symfony-telemetry-bundle - DBAL spans, metrics and config aligned with the PostgreSQL client
| Before | After |
|---|---|
doctrine.dbal.transaction.begin/.commit/.rollback (always) |
one BEGIN TRANSACTION span per transaction (grouped) |
doctrine.dbal.connection.exec/.query span names |
semconv {db.operation.name} {db.collection.name} (e.g. SELECT users) |
doctrine.dbal.statement.execute/.prepare span names |
semconv {db.operation.name} {db.collection.name} |
config instrumentation.dbal.log_sql |
removed - db.query.text is always recorded (bounded by max_sql_length) |
| - | config instrumentation.dbal.transaction_spans: grouped (default)/per_operation/off |
| - | config instrumentation.dbal.collect_metrics (default true) |
| - | config instrumentation.dbal.include_parameters/max_parameters/max_parameter_length |
Query spans now carry db.system.name, db.namespace, server.address, server.port, db.operation.name,
db.collection.name, db.response.returned_rows and (on error) db.response.status_code, and the instrumentation
emits db.client.operation.duration and db.client.response.returned_rows metrics. If you matched DBAL spans by their
doctrine.dbal.* names, switch to the semantic names above.
9) flow-php/symfony-telemetry-bundle - custom attribute keys moved out of reserved OTel namespaces
| Before | After |
|---|---|
controller |
flow.symfony.controller |
controller.argument |
flow.symfony.controller.argument |
code.namespace + code.function |
code.function.name (Class::method) |
command.name |
flow.symfony.command.name |
command.class |
flow.symfony.command.class |
process.signal |
flow.symfony.command.signal |
process.exit_code |
process.exit.code |
db.connection.name |
flow.db.connection.name |
db.transaction.nesting_level |
flow.db.transaction.nesting_level |
http.client.name |
flow.http.client.name |
log.channel |
flow.log.channel |
10) flow-php/symfony-telemetry-bundle - HTTP spans aligned with the stable HTTP semconv
| Before | After |
|---|---|
server span url.full |
removed |
server span url.path = URI incl. query |
path only; query moves to url.query |
| - | server span user_agent.original |
client span name {method} {host} |
{method} |
client span server.port only when non-default |
always set (scheme default 80/443 filled in) |
Applies to the HttpKernel server span and to the http_client/psr18_client client spans.
11) flow-php/symfony-telemetry-bundle - messenger destination is the transport; metrics_duration_unit removed
| Before | After |
|---|---|
messaging.destination.name = short message class |
transport name (consume side); absent on dispatch |
messaging.transport |
removed (folded into messaging.destination.name) |
messaging.consumer.group.name (metric attribute) |
removed |
messaging.message.class |
flow.messenger.message.class |
messaging.symfony.bus |
flow.messenger.bus |
consume span process {ShortClass} |
process {transport} |
dispatch span send {ShortClass} |
send |
config instrumentation.messenger.metrics_duration_unit: s/ms |
removed - messaging.process.duration is always seconds |
Instrumentation\Messenger\MessengerMetricDurationUnit |
removed |
12) flow-php/phpunit-telemetry-bridge - test.* keys aligned with the OTel test registry; durations in seconds
| Before | After |
|---|---|
test.name |
test.case.name |
test.status |
test.case.result.status |
test.suite |
test.suite.name |
| - | test.suite.run.status: success/failure/skipped |
test.id |
flow.phpunit.test.id |
test.class |
flow.phpunit.test.class |
test.method |
flow.phpunit.test.method |
test.suite.test_count |
flow.phpunit.suite.test_count |
test.suite.is_root |
flow.phpunit.suite.is_root |
test.memory.peak_bytes |
flow.phpunit.test.memory.peak |
test.memory.delta_bytes |
flow.phpunit.test.memory.delta |
test.duration_ms span attribute |
removed (use the span duration) |
exception.message span attribute |
removed (message stays in the span status description) |
flow.phpunit.test.duration unit ms |
s (values rescaled) |
flow.phpunit.suite.duration unit ms |
s (values rescaled) |
flow.phpunit.test.memory.* unit bytes |
By |
flow.phpunit.test.count/suite.test_count unit |
{test} |
| - | resource attribute telemetry.sdk.version |
13) flow-php/filesystem, flow-php/etl, flow-php/postgresql - flow-custom keys under flow.*; UCUM units
| Before | After |
|---|---|
path.uri/path.to |
flow.filesystem.path.uri/.path.to |
stream.type |
flow.filesystem.stream.type |
bytes.total_read/bytes.total_written |
flow.filesystem.bytes.total_read/.total_written |
filesystem.operation/filesystem.protocol |
flow.filesystem.operation/.protocol |
spans Read {file}/Write {file} |
filesystem.read/filesystem.write |
flow.filesystem.*.size unit bytes |
By |
flow.filesystem.*.operations unit operations |
{operation} |
dataframe.id/dataframe.name |
flow.etl.dataframe.id/.dataframe.name |
rows.total/rows.throughput.per_second |
flow.etl.rows.total/.rows.throughput.per_second |
memory.min.mb/memory.max.mb |
flow.etl.memory.min/.memory.max (values stay MB) |
loader.class/transformer.class |
flow.etl.loader.class/.transformer.class |
destination.uri/loading.rows |
flow.etl.destination.uri/.loading.rows |
transformation.input_rows/.output_rows |
flow.etl.transformation.input_rows/.output_rows |
join.type/scalar.function |
flow.etl.join.type/.scalar.function |
spans Cache Set {key}/Cache Delete {key}/Cache Clear |
cache.set/cache.delete/cache.clear |
flow.cache.hits/.misses unit operations |
{operation} |
flow.etl.rows.processed unit rows |
{row} |
flow.etl.rows.throughput unit rows/s/sec |
{row}/s |
db.transaction.savepoint |
flow.db.transaction.savepoint |
db.transaction.nesting_level |
flow.db.transaction.nesting_level |
14) flow-php/telemetry, flow-php/psr18-telemetry-bridge - shared semconv constants; UCUM time units
| Before | After |
|---|---|
official keys duplicated per package (DbAttributes, PostgreSqlTelemetryAttributes, inline strings) |
Flow\Telemetry\SemConvAttributes / SemConvMetrics |
TimeUnit::SECONDS->value = 'sec' |
's' |
TimeUnit::MICROSECONDS->value = 'µs' |
'us' |
PSR-18 client span name {method} {host} |
{method} |
PSR-18 server.port only when non-default |
always set (scheme default 80/443 filled in) |
FilesystemTelemetryAttributes::ATTR_BYTES_READ/ATTR_BYTES_WRITTEN/ATTR_PATH_FROM/ATTR_PATH_IS_PATTERN/ATTR_ERROR_TYPE |
removed |
PostgreSqlTelemetryAttributes::DB_QUERY_SUMMARY + official-key constants |
removed (officials via SemConvAttributes) |
15) flow-php/symfony-telemetry-bundle - runtime_mode removed; terminate flushes, process end shuts down
| Before | After |
|---|---|
flow_telemetry.runtime_mode: auto/classic/worker |
removed |
Flow\Bridge\Symfony\TelemetryBundle\Runtime\RuntimeModeResolver |
removed |
Flow\Bridge\Symfony\TelemetryBundle\Runtime\RuntimeMode |
removed |
Flow\Bridge\Symfony\TelemetryBundle\Runtime\WorkerModeDetector |
removed |
Flow\Bridge\Symfony\TelemetryBundle\Runtime\EnvironmentWorkerModeDetector |
removed |
shutdown on kernel.terminate/console.terminate (classic mode) |
flush on terminate; shutdown at process end |
Drop the runtime_mode key from flow_telemetry config and remove any WorkerModeDetector service overrides.
16) flow-php/telemetry - Telemetry::registerShutdownFunction() holds a weak reference
| Before | After |
|---|---|
| strong reference; instance kept alive until exit | weak reference; garbage-collected instances are not shut down |
Keep the registered Telemetry instance referenced for as long as it should be shut down at process end.
17) flow-php/etl - SchemaValidator::isValid() replaced by validate(): ValidationContext
| Before | After |
|---|---|
SchemaValidator::isValid(...): bool |
SchemaValidator::validate(...): ValidationContext |
$validator->isValid($expected, $given) |
$validator->validate($expected, $given)->isValid() |
schema_validate(...): bool |
schema_validate(...): ValidationContext |
new SchemaValidationException($exp, $given) |
new SchemaValidationException($exp, $given, ValidationContext) |
| - | SchemaValidationException::context(): ValidationContext |
Custom SchemaValidator implementations must return a Flow\ETL\Schema\Validator\ValidationContext
built from the missing, mismatched (MismatchedDefinition), and unexpected definitions they reject.
18) flow-php/filesystem - Partition name and value forbid { and }
| Before | After |
|---|---|
new Partition('na{me', 'a}b') |
throws InvalidArgumentException |
partitionBy() values with {} |
throws InvalidArgumentException |
{name} in a path is now a partition placeholder resolved from partitionBy() columns; strip braces from partition
values before partitioning.
19) flow-php/etl - Cache stores only Rows; cache indexes are stored as Rows
| Before | After |
|---|---|
Cache::get(string): Row|Rows|CacheIndex |
Cache::get(string): Rows |
Cache::set(string, Row|Rows|CacheIndex): void |
Cache::set(string, Rows): void |
$cache->set($id, $row) |
$cache->set($id, rows($row)) |
$cache->set($id, $cacheIndex) |
$cache->set($id, $cacheIndex->toRows()) |
$cache->get($id) returning CacheIndex |
CacheIndex::fromRows($id, $cache->get($id)) |
Custom Cache implementations must adopt the Rows-only signatures.
20) flow-php/etl - cache and serialization use the Floe (.floe) binary format; datetime subclasses rejected
| Before | After |
|---|---|
config_builder()->serializer() default Base64Serializer(new NativePHPSerializer()) |
Flow\Floe\FloeSerializer |
filesystem_cache(..., Serializer $serializer = new NativePHPSerializer()) |
filesystem_cache(..., Serializer $serializer = new FloeSerializer()) |
new FilesystemCache($filesystem, $serializer, $cacheDir) |
new FilesystemCache($filesystem, $cacheDir, $serializer) |
caching or serializing a DateTime/DateTimeImmutable subclass (e.g. Carbon) |
throws Flow\Floe\Exception\FloeException |
Delete cache directories written by 0.41 - the old serialized payloads are unreadable. Convert
DateTime/DateTimeImmutable subclasses to DateTime/DateTimeImmutable before caching or serializing.
A Floe file carries exactly one schema, and sort / join / group-by spill batches through it - a pipeline with a drifting
or schemaless source must declare column types at the source so nullable columns carry a concrete type
(DataFrame::match($schema) validates but does not retype values):
->read(from_array($data, schema(str_schema('email'), float_schema('discount', nullable: true))))
21) flow-php/symfony-telemetry-bundle - HttpKernelSpanSubscriber takes a
RouteNamePathMap instead of the router
| Before | After |
|---|---|
new HttpKernelSpanSubscriber(..., router: $router, ...) |
new HttpKernelSpanSubscriber(..., routePaths: $routeNamePathMap, ...) |
?RouterInterface $router = null |
?RouteNamePathMap $routePaths = null |
Applies only to direct construction; services wired by the bundle need no change.
22) flow-php/etl - sort algorithm is an explicit choice; external sort is the default
| Before | After |
|---|---|
SortAlgorithms enum |
removed; ConfigBuilder::sort(memory_sort() / external_sort()) |
SortAlgorithms::MEMORY_FALLBACK_EXTERNAL_SORT (default) |
removed; default is external_sort() |
SortAlgorithms::SQLITE_SORT |
removed |
SortAlgorithms::useMemory() |
removed |
ConfigBuilder::sortMemoryLimit(Unit $unit) |
removed |
SortConfigBuilder |
removed; external_sort() builder |
SortConfigBuilder::sortMemoryLimit(Unit $unit) |
removed |
SortConfig |
MemorySortConfig | ExternalSortConfig (Config::$sort) |
SortConfig::$memoryLimit |
removed |
SortConfig::SORT_MAX_MEMORY_ENV / FLOW_SORT_MAX_MEMORY env |
removed |
new MemorySort(Unit $maximumMemory) |
removed; sort(memory_sort()) |
MemorySort throwing Flow\ETL\Exception\OutOfMemoryException |
removed |
new ExternalSort($cache, $bucketsCount) |
removed; sortBy() pipelines BucketingProcessor + MergeSortProcessor |
ConfigBuilder::externalSortFilesystem(string) |
external_sort()->filesystemProtocol(string) |
| - | external_sort()->runSize(int) (default 10000) |
| - | external_sort()->bucketsCount(int) (default 100) |
| - | external_sort()->batchSize(int) (default 1000) |
| - | external_sort()->storage(BucketsStorage) (default FilesystemBuckets) |
To sort in memory, opt in explicitly:
data_frame(config_builder()->sort(memory_sort()))->read(...)->sortBy(ref('id'))->run();
23) flow-php/etl - joins emit every matching right row and follow SQL semantics
| Before | After |
|---|---|
| inner/left/right join: first matching row per probe row | one output row per matching pair |
| left/left_anti join: left row dropped on hash collision without match | left row kept |
| duplicated right side rows collapsed into one | preserved |
DataFrame::join(..., Join::inner): duplicated join columns kept |
duplicated join columns dropped (empty prefix) |
Equal on object values (Uuid, etc.): always false |
compared with == |
Flow\ETL\Processor\HashJoin\HashTable |
Flow\ETL\Join\HashJoin\HashTable |
Flow\ETL\Processor\HashJoin\Bucket |
removed |
| - | Expression::comparison(), All::comparisons(), Any::comparisons() |
Rows::joinRight() emits matched rows in left-probe order and unmatched right rows last.
24) flow-php/etl - join buckets storage is configurable, spills to disk by default
| Before | After |
|---|---|
join() holds right side in memory, keeps left row order |
join buckets spilled to disk (Floe files), left row order not preserved |
| - | ConfigBuilder::join(hash_join()) |
| - | hash_join()->storage(BucketsStorage) (default FilesystemBuckets) |
| - | hash_join()->bucketsCount(int) (default 64) |
| - | hash_join()->batchSize(int) (default 1000) |
| - | Flow\ETL\Bucketing\ResidentBucketsStorage |
| - | Flow\ETL\Bucketing\Storage\MemoryBuckets |
To keep the right side in memory and preserve left row order:
data_frame(config_builder()->join(hash_join()->storage(new MemoryBuckets())))
->read(...)
->join(...)
->run();
25) flow-php/etl - Serializer works on Rows and filesystem streams
| Before | After |
|---|---|
Serializer::serialize(object $serializable): string |
Serializer::serialize(Rows $rows, DestinationStream $destination): void |
Serializer::unserialize(string $serialized, array $classes): object |
Serializer::unserialize(SourceStream $source): Rows |
CompressingSerializer/NativePHPSerializer throw Flow\ETL\Exception\RuntimeException |
throw Flow\Serializer\Exception\SerializationException |
| - | Flow\Floe\FloeSerializer (the default) |
26) flow-php/etl - EntryFactory API reshaped
| Before | After |
|---|---|
EntryFactory::create(string $entryName, mixed $value, Schema|Definition|null $schema = null) |
EntryFactory::create(string $name, mixed $value, ?Type $type = null, ?Metadata $metadata = null) |
EntryFactory::createAs(string $entryName, mixed $value, Type $type, ?Metadata $metadata = null) |
EntryFactory::cast(string $name, mixed $value, Type $type, ?Metadata $metadata = null) |
| - | EntryFactory::fromDefinition(Definition $definition, mixed $value): Entry |
to_entry($name, $data, EntryFactory $entryFactory) |
to_entry($name, $data, EntryFactory $entryFactory = new EntryFactory()) |
ConfigBuilder::build(EntryFactory $entryFactory = new EntryFactory()) |
ConfigBuilder::build() |
27) flow-php/etl - Encoder/Hydrator contracts; hydrator configured on the context
| Before | After |
|---|---|
| - | Flow\ETL\Row\Encoder<TPhysical>: encode(list<TypedRowValues>): list<TPhysical>, decode(list<TPhysical>): list<RawRowValues> |
| - | Flow\ETL\Row\Hydrator: cast(list<RawRowValues>, ?Schema): Rows, hydrate(list<RawRowValues>, ?Schema): Rows, dehydrate(Rows): list<TypedRowValues> |
| - | Flow\ETL\Row\RawRowValues, Flow\ETL\Row\TypedRowValues |
| - | Flow\ETL\Row\PhpRowHydrator, NativeRowHydrator, AdaptiveRowHydrator (the default) |
| - | ConfigBuilder::hydrator(Hydrator), Config::hydrator(), FlowContext::hydrator() |
array_to_row($data, EntryFactory $entryFactory, ...) |
array_to_row($data, Hydrator $hydrator = new AdaptiveRowHydrator(), ...) |
array_to_rows($data, EntryFactory $entryFactory, ...) |
array_to_rows($data, Hydrator $hydrator = new AdaptiveRowHydrator(), ...) |
cast() coerces untrusted values through the schema (null schema infers types); hydrate() instantiates trusted,
already-typed values and requires a schema. A custom extractor reading raw values (CSV, JSON, XML, Excel, Text, Google
Sheet, PostgreSQL, Doctrine) calls $context->hydrator()->cast(...); a self-describing source (Parquet, Floe)
calls $context->hydrator()->hydrate(...).
Under a schema, entries emit in schema-definition order (was data-key order with missing columns appended); a schema
column missing from the source hydrates as a typed null (was a StringEntry null); a column absent from the schema is
dropped. When the flow_php extension is loaded, AdaptiveRowHydrator runs hydration natively - pin the pure-PHP
engine with config_builder()->hydrator(new PhpRowHydrator()).
28) flow-php/etl-adapter-csv, -json, -parquet, -xml, -excel, -doctrine, -postgresql, -seal, -text,
-google-sheet - per-format Encoders replace normalizers
| Before | After |
|---|---|
Flow\ETL\Adapter\CSV\RowsNormalizer, RowsNormalizer\EntryNormalizer, RowsNormalizer\ScalarCast |
Flow\ETL\Adapter\CSV\CSVEncoder |
Flow\ETL\Adapter\JSON\RowsNormalizer, RowsNormalizer\EntryNormalizer |
Flow\ETL\Adapter\JSON\JSONEncoder |
Flow\ETL\Adapter\Parquet\RowsNormalizer |
Flow\ETL\Adapter\Parquet\ParquetEncoder |
Flow\ETL\Adapter\XML\RowsNormalizer, RowsNormalizer\EntryNormalizer, RowsNormalizer\EntryNormalizer\PHPValueNormalizer |
Flow\ETL\Adapter\XML\XMLEncoder |
Flow\ETL\Adapter\Excel\RowsNormalizer\ExcelRowsNormalizer |
Flow\ETL\Adapter\Excel\ExcelEncoder |
Flow\ETL\Adapter\Doctrine\RowsNormalizer |
Flow\ETL\Adapter\Doctrine\DbalEncoder |
Flow\ETL\Adapter\Seal\RowsNormalizer, RowsNormalizer\EntryNormalizer |
Flow\ETL\Adapter\Seal\SealEncoder |
| - | Flow\ETL\Adapter\Text\TextEncoder, Flow\ETL\Adapter\PostgreSql\PostgreSqlEncoder, Flow\ETL\Adapter\GoogleSheet\GoogleSheetEncoder |
Flow\ETL\Adapter\Doctrine\TypesMap::flowRowTypes(Row) |
TypesMap::flowSchemaTypes(Schema) |
Flow\ETL\Adapter\PostgreSql\EntryTypesMap::mapEntry(Entry) |
EntryTypesMap::map(string $column, Type $type, mixed $value) |
InsertQueryBuilder::build(Rows, ...) |
InsertQueryBuilder::build(array $values, Schema, ...) |
UpdateQueryBuilder::build(Row, ...), DeleteQueryBuilder::build(Row, ...) |
build(array $value, Schema, ...) |
CSV/JSON/XML/Excel/Seal writers (no dateFormat) |
dateFormat = 'Y-m-d' encoder constructor param |
Excel timeFormat = 'H:i:s' |
timeFormat = '%H:%I:%S' |
| - | withDateFormat() on CSVLoader, JsonLoader, JsonLinesLoader, XMLLoader, ExcelLoader, SealLoader; SealLoader::withDateTimeFormat() |
Behavioural changes: CSV/JSON/XML/Excel/Seal date columns render with dateFormat (Y-m-d, was dateTimeFormat);
XML date/time columns render (was InvalidArgumentException); Seal time columns render as microseconds (was
null); Google Sheet _spread_sheet_id/_sheet_name are appended to the schema under putInputIntoRows() (were
dropped when a schema was set); Excel now appends _input_file_uri under putInputIntoRows().
29) flow-php/etl - null is a first-class NullEntry/NullDefinition; from_null metadata removed
| Before | After |
|---|---|
null_entry($name) → StringEntry (type string, from_null metadata) |
null_entry($name) → Flow\ETL\Row\Entry\NullEntry (type null) |
null_schema($name) → StringDefinition + from_null metadata |
null_schema($name) → Flow\ETL\Schema\Definition\NullDefinition (always nullable) |
Flow\ETL\Row\Entry\StringEntry::fromNull($name, $metadata) |
null_entry($name, $metadata) |
new StringEntry($name, $value, $metadata, fromNull: true) |
$fromNull constructor flag removed |
Flow\ETL\Schema\Metadata::FROM_NULL |
removed |
30) flow-php/etl - group by runs on bucketing storage; only referenced columns are spilled
| Before | After |
|---|---|
| grouping state held in memory | rows partitioned into buckets through BucketsStorage (default FilesystemBuckets) |
| rows carry all columns into grouping | only groupBy refs + aggregator refs are spilled |
STRICT schema mode throws for a missing aggregate column under groupBy() |
row gets a null entry for it; aggregators skip null |
| group output follows input order (incidental) | follows hash-bucket order |
| - | AggregatingFunction::references(): ?array |
| - | ConfigBuilder::groupBy(hash_group_by()) |
| - | hash_group_by()->storage(BucketsStorage) (default FilesystemBuckets) |
| - | hash_group_by()->bucketsCount(int) (default 64) |
| - | hash_group_by()->batchSize(int) (default 1000) |
Custom aggregators must implement references() - return the references the aggregator reads, or
null when they cannot be statically enumerated (disables spill column pruning).
31) flow-php/etl - DataFrame::pivot() removed; pivot is declared between groupBy() and aggregate()
| Before | After |
|---|---|
->groupBy(...)->aggregate(...)->pivot(ref('x')) |
->groupBy(...)->pivot(ref('x'))->aggregate(...) |
DataFrame::pivot() |
removed; GroupedDataFrame::pivot() only |
32) flow-php/etl-adapter-csv, -excel, -json,
-xml - explicit schema no longer projects partition columns away
| Before | After |
|---|---|
| partition columns undeclared in the schema are dropped from rows | force-added to rows as non-nullable string columns |
Declare the partition column in the schema to control its type.
33) flow-php/etl - Schema and Schema\Definition mutators return a new instance
| Before | After |
|---|---|
$schema->add(str_schema('x')); |
$schema = $schema->add(str_schema('x')); |
$schema->addMetadata('id', 'k', 'v'); |
$schema = $schema->addMetadata('id', 'k', 'v'); |
$definition->setMetadata($metadata); |
$definition = $definition->setMetadata($metadata); |
Assign the result of every Schema mutator - add, addAfter, addBefore, addMetadata, gracefulRemove,
insertAt, keep, makeNullable, merge, moveAfter, moveBefore, moveTo, prepend, remove, rename,
reorder, replace, setMetadata, sort - and of Definition::addMetadata() / Definition::setMetadata().
Discarding it is a silent no-op.
Upgrading from 0.40.x to 0.41.x
1) Removal of Elasticsearch Adapter
The Elasticsearch adapter has been removed from Flow PHP and replaced by the SEAL
adapter (flow-php/etl-adapter-seal), a search engine abstraction layer that supports Elasticsearch, OpenSearch,
Meilisearch, Solr, Typesense, Algolia, RediSearch and Loupe.
To migrate, install the SEAL adapter together with the engine adapter for your backend:
composer require flow-php/etl-adapter-seal cmsig/seal-elasticsearch-adapter
Then build a CmsIg\Seal\Engine and pass it to to_seal_upsert() instead of the previous to_es_bulk_index() (or
Meilisearch) DSL functions:
use CmsIg\Seal\Engine;
use CmsIg\Seal\Adapter\Elasticsearch\ElasticsearchAdapter;
use function Flow\ETL\Adapter\Seal\to_seal_upsert;
$engine = new Engine(
new ElasticsearchAdapter($client),
$schema,
);
data_frame()
->read(/* ... */)
->write(to_seal_upsert($engine, 'index_name'))
->run();
2) flow-php/symfony-telemetry-bundle -
flow-php/symfony-http-foundation-telemetry-bridge is now an optional dependency
| Before | After |
|---|---|
| installed transitively by the bundle | install explicitly to enable HTTP trace context propagation |
instrumentation.http_kernel.context_propagation is silently disabled when the bridge is absent. To keep extracting
incoming and injecting outgoing W3C trace headers:
composer require flow-php/symfony-http-foundation-telemetry-bridge
3) flow-php/telemetry - trace id derived from the active span; root spans start a new trace
| Before | After |
|---|---|
Context::create() |
Context::root() |
Context::withTraceId(TraceId) |
removed |
$context->traceId (property) |
$context->traceId(): ?TraceId (derived from the active span) |
Context::withActiveSpan(SpanId) |
Context::withActiveSpan(SpanContext) |
context(?TraceId, ?Baggage) (DSL) |
context(?Baggage) |
| a root span reused the context trace id | each root span generates a new TraceId |
Context no longer stores a standalone trace id; attach the active span as a SpanContext to keep subsequent spans in
the same trace.
4) flow-php/symfony-telemetry-bundle - each consumed Messenger message is its own trace
| Before | After |
|---|---|
all messages in a messenger:consume run shared one trace |
each handled message is a new trace root, linked to the producer |
5) flow-php/telemetry-otlp-bridge, flow-php/symfony-telemetry-bundle - curl is synchronous; async moved to
async_curl
| Before | After |
|---|---|
CurlTransport async (curl_multi, fire-and-forget send()) |
CurlTransport synchronous; send() blocks and throws on failure |
async via CurlTransport |
async via AsyncCurlTransport / otlp_async_curl_transport() / transport.type: 'async_curl' |
CurlTransportOptions::DEFAULT_TIMEOUT_MS 250 |
10000 |
bundle curl timeout_ms 250 |
10000 |
| - | async_curl connect_timeout_ms 1500, pump_timeout_ms 100 |
6) flow-php/symfony-postgresql-bundle - migrations run against a single configured connection
| Before | After |
|---|---|
flow:migrations:* --connection=<name> (-c) |
removed - every migration command uses the configured migrations connection |
| migrator stack registered for every connection | registered only for the migrations connection |
| - | flow_postgresql.migrations.connection: <name> (defaults to the first connection) |
To run migrations against a non-default connection, set migrations.connection instead of passing -c:
flow_postgresql:
migrations:
enabled: true
connection: analytics
7) flow-php/symfony-telemetry-bundle - static resource cache file is keyed by kernel environment
| Before | After |
|---|---|
sys_get_temp_dir()/flow_telemetry_resource.cache |
sys_get_temp_dir()/flow_telemetry_resource_<kernel.env>.cache |
Delete the orphaned flow_telemetry_resource.cache from the temp dir; a per-env file is written on the next run.
8) flow-php/symfony-telemetry-bundle - messenger worker receive cycle is traced; link_to_worker config removed
| Before | After |
|---|---|
instrumentation.messenger.link_to_worker: true |
removed (always on under messenger:consume) |
flow.messenger.worker link → console worker span |
→ per-pass messenger.receive span |
| transport poll query = one standalone trace each | grouped under one messenger.receive root span per pass |
Remove the link_to_worker key from config. Idle passes (no message received) are marked
messaging.symfony.worker.idle: true.
9) flow-php/symfony-telemetry-bundle - messenger instrumentation also emits messaging metrics
| Before | After |
|---|---|
| messenger instrumentation emitted spans only | also emits messaging.client.consumed.messages, messaging.client.sent.messages, messaging.process.duration |
Disable with instrumentation.messenger.metrics: false; set the messaging.process.duration unit via
instrumentation.messenger.metrics_duration_unit (s default, or ms).
10) flow-php/etl, flow-php/filesystem, flow-php/postgresql, flow-php/symfony-telemetry-bundle,
flow-php/phpunit-telemetry-bridge - emitted metric names standardized
| Before | After |
|---|---|
rows_processed |
flow.etl.rows.processed |
rows_throughput |
flow.etl.rows.throughput |
cache_hits |
flow.cache.hits |
cache_misses |
flow.cache.misses |
cache.hits |
flow.cache.hits |
cache.misses |
flow.cache.misses |
write_size |
flow.filesystem.write.size |
write_operations |
flow.filesystem.write.operations |
read_size |
flow.filesystem.read.size |
read_operations |
flow.filesystem.read.operations |
operation_duration |
db.client.operation.duration |
response_returned_rows |
db.client.response.returned_rows |
phpunit.test.duration |
flow.phpunit.test.duration |
phpunit.test.count |
flow.phpunit.test.count |
phpunit.test.memory.peak |
flow.phpunit.test.memory.peak |
phpunit.test.memory.delta |
flow.phpunit.test.memory.delta |
phpunit.suite.duration |
flow.phpunit.suite.duration |
phpunit.suite.test_count |
flow.phpunit.suite.test_count |
Rename these series in dashboards and alerts.
11) flow-php/symfony-telemetry-bundle - messenger trace/link config; propagation_style removed
| Before | After |
|---|---|
instrumentation.messenger.propagation_style |
removed (consumed messages always start their own trace) |
| (no span selection) | instrumentation.messenger.trace: worker/handlers/both (default)/none |
| (no link selection) | instrumentation.messenger.link: dispatcher/worker/both (default) |
Remove propagation_style from config - continue mode is gone (it made the consumer span absorb the queue wait time).
trace selects which spans are emitted (handlers = the process/send message spans; none = metrics only);
link selects the consumer span's links. link: worker/both requires trace to include the worker, otherwise the
config is rejected. When trace excludes the worker, the transport's poll instrumentation (Doctrine DBAL, HTTP
client, ...) is suppressed during the receive loop so it does not surface as orphan spans - no messenger.receive span,
no orphans either way.
Upgrading from 0.39.x to 0.40.x
1) flow-php/postgresql - column and domain defaults are modeled as ColumnDefault
| Before | After |
|---|---|
Column::$default / Domain::$default type ?string |
?Flow\PostgreSql\Schema\ColumnDefault |
new Column('c', $type, true, "'0'") |
new Column('c', $type, true, ColumnDefault::fromExpression("'0'", $type)) |
$column->default (string) |
$column->default?->literal / $column->default?->applicableSql() |
ColumnShape['default'] / DomainShape['default'] ?string |
?array{literal: string, type: ?ColumnTypeShape, kind: string} |
Column::create() / Domain::create() still accept bool|float|int|string|Expression|null. Schema arrays serialized
by Column::normalize() / Domain::normalize() before 0.40 must be regenerated - fromArray() reads the nested
default shape only.
2) flow-php/telemetry - Log severity filtering moved to a pipeline middleware
| Before | After |
|---|---|
Flow\Telemetry\Logger\Processor\SeverityFilteringLogProcessor |
Flow\Telemetry\Logger\Middleware\SeverityFilteringLogMiddleware |
new SeverityFilteringLogProcessor($inner, $minSeverity) |
new PipelineLogProcessor([new SeverityFilteringLogMiddleware($minSeverity)], $inner) |
severity_filtering_log_processor($processor, $minSeverity) |
pipeline_log_processor([severity_filtering_log_middleware($minSeverity)], $processor) |
The previously wrapped processor is now the pipeline's sink and must implement
Flow\Telemetry\Logger\LogSink (the built-in batching, pass_through, memory, void and composite
log processors already do).
Before:
$processor = severity_filtering_log_processor(
batching_log_processor($exporter),
Severity::WARN,
);
After:
$processor = pipeline_log_processor(
[severity_filtering_log_middleware(Severity::WARN)],
batching_log_processor($exporter),
);
2) flow-php/symfony-telemetry-bundle - severity_filtering log processor type replaced by pipeline
The severity_filtering processor type (with its inner_processor) is no longer a logger_provider
processor type; it is a middleware inside a pipeline.
Before:
flow_telemetry:
logger_provider:
processor:
type: severity_filtering
minimum_severity: warn
inner_processor:
type: batching
exporter: otlp
After:
flow_telemetry:
logger_provider:
processor:
type: pipeline
middleware:
- { type: severity_filtering, minimum_severity: warn }
sink:
type: batching
exporter: otlp
3) flow-php/symfony-telemetry-bundle - named scope attributes split into scope and signal
Applies to tracers, meters, and loggers. The attributes map is no longer a flat list of scope attributes; scope
attributes move under attributes.scope.
Before:
flow_telemetry:
loggers:
audit:
attributes:
team: checkout
After:
flow_telemetry:
loggers:
audit:
attributes:
scope:
team: checkout
4) flow-php/postgresql - DateTimeConverter split into TimestampConverter and TimestampTzConverter
| Before | After |
|---|---|
Flow\PostgreSql\Client\Types\Converter\DateTimeConverter |
TimestampConverter (TIMESTAMP) and TimestampTzConverter (TIMESTAMPTZ) |
typed($value, ValueType::TIMESTAMP) keeps the offset |
typed($value, ValueType::TIMESTAMP) normalizes the value to UTC |
timestamp column read as 2024-01-15 10:30:00 |
timestamp column read as 2024-01-15 10:30:00+00:00 |
5) flow-php/etl-adapter-postgresql - DateTimeEntry maps to timestamp instead of timestamptz
| Flow type | Before | After |
|---|---|---|
DateTimeEntry (binding) |
TIMESTAMPTZ |
TIMESTAMP |
DateTimeType (DDL) |
timestamptz |
timestamp |
To keep the previous behavior, pass overrides to EntryTypesMap:
to_pgsql_table($client, 'users')->withTypesMap(new EntryTypesMap(
[DateTimeEntry::class => ValueType::TIMESTAMPTZ],
[DateTimeType::class => ColumnType::timestamptz()],
));
6) flow-php/symfony-postgresql-messenger - messenger_messages time columns use timestamp instead of
timestamptz
Column type for created_at, available_at, delivered_at |
Before | After |
|---|---|---|
MessengerCatalogProvider (DDL) |
timestamptz |
timestamp |
Connection bindings |
TIMESTAMPTZ |
TIMESTAMP |
Existing tables, realign the column type (UTC instants preserved):
ALTER TABLE messenger_messages
ALTER COLUMN created_at TYPE TIMESTAMP USING created_at AT TIME ZONE 'UTC',
ALTER COLUMN available_at TYPE TIMESTAMP USING available_at AT TIME ZONE 'UTC',
ALTER COLUMN delivered_at TYPE TIMESTAMP USING delivered_at AT TIME ZONE 'UTC';
7) flow-php/telemetry - OTEL_RESOURCE_ATTRIBUTES keys and values are percent-decoded, not backslash-escaped
Escaping a , or = in OTEL_RESOURCE_ATTRIBUTES |
Before | After |
|---|---|---|
| literal comma in a value | key=value\,with\,commas |
key=value%2Cwith%2Ccommas |
literal = in a value |
not supported | key=a%3Db |
Re-encode any OTEL_RESOURCE_ATTRIBUTES that relied on backslash escaping; both keys and values are now
percent-decoded.
Upgrading from 0.37.x to 0.38.x
1) flow-php/types - PHPStan extension extracted to flow-php/phpstan-types-bridge
The StructureTypeReturnTypeExtension - which narrows the return type of type_structure() for PHPStan - has been
moved out of flow-php/types into a dedicated package, flow-php/phpstan-types-bridge.
flow-php/types no longer ships any PHPStan code.
| Before | After |
|---|---|
Flow\Types\PHPStan\StructureTypeReturnTypeExtension |
Flow\Bridge\PHPStan\Types\StructureTypeReturnTypeExtension |
shipped inside flow-php/types |
shipped inside flow-php/phpstan-types-bridge |
If you used type_structure() together with PHPStan, install the new package:
composer require --dev flow-php/phpstan-types-bridge
With phpstan/extension-installer the extension is registered
automatically. If you registered it manually, update your phpstan.neon:
Before:
services:
-
class: Flow\Types\PHPStan\StructureTypeReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
After:
includes:
- vendor/flow-php/phpstan-types-bridge/extension.neon
Upgrading from 0.36.x to 0.37.x
1) flow-php/telemetry - Per-signal exporter contracts merged into Exporter
| Before | After |
|---|---|
Flow\Telemetry\Tracer\SpanExporter (interface) |
Flow\Telemetry\Exporter\Exporter |
Flow\Telemetry\Meter\MetricExporter (interface) |
Flow\Telemetry\Exporter\Exporter |
Flow\Telemetry\Logger\LogExporter (interface) |
Flow\Telemetry\Exporter\Exporter |
VoidSpanExporter, VoidMetricExporter, VoidLogExporter |
Flow\Telemetry\Provider\Void\VoidExporter |
MemorySpanExporter, MemoryMetricExporter, MemoryLogExporter |
Flow\Telemetry\Provider\Memory\MemoryExporter |
ConsoleSpanExporter, ConsoleMetricExporter, ConsoleLogExporter |
Flow\Telemetry\Provider\Console\ConsoleExporter |
void_span_exporter() / void_metric_exporter() / void_log_exporter() |
void_exporter() |
memory_span_exporter() / memory_metric_exporter() / memory_log_exporter() |
memory_exporter() |
console_span_exporter() / console_metric_exporter() / console_log_exporter() |
console_exporter() |
MemoryLogExporter::entries() |
MemoryExporter::logs() |
MemorySpanExporter::spans() |
MemoryExporter::spans() |
MemoryMetricExporter::metrics() |
MemoryExporter::metrics() |
Exporter::transports() |
removed |
(Span|Metric|Log)Exporter::export(array $items) : bool |
Exporter::export(Flow\Telemetry\Signal\Signals $signal) : bool |
| - | Exporter::shutdown() : void (added) |
2) flow-php/telemetry - Transport contract relocated to OTLP bridge
| Before | After |
|---|---|
Flow\Telemetry\Transport\Transport |
Flow\Bridge\Telemetry\OTLP\Transport\Transport |
Flow\Telemetry\Transport\TransportException |
Flow\Bridge\Telemetry\OTLP\Transport\TransportException |
Flow\Telemetry\Transport\VoidTransport |
removed |
Transport::sendSpans() / sendMetrics() / sendLogs() |
Transport::send(Flow\Telemetry\Signal\Signals $signal) |
Flow\Bridge\Telemetry\OTLP\Exception\Exception |
removed |
Flow\Bridge\Telemetry\OTLP\Exception\TransportException |
Flow\Bridge\Telemetry\OTLP\Transport\TransportException |
3) flow-php/telemetry - Processor interfaces
Applies to SpanProcessor, MetricProcessor, LogProcessor.
| Before | After |
|---|---|
exporter() : SpanExporter (etc.) |
removed |
| - | shutdown() : void (added) |
4) flow-php/telemetry - Serializer contract removed
| Before | After |
|---|---|
Flow\Telemetry\Serializer\Serializer |
removed |
Flow\Bridge\Telemetry\OTLP\Serializer\GrpcSerializer (interface) |
renamed to Flow\Bridge\Telemetry\OTLP\Serializer\GrpcRequestFactory (class) |
CurlTransport(string $endpoint, Serializer $serializer, ...) |
CurlTransport(string $endpoint, JsonSerializer|ProtobufSerializer $serializer = new JsonSerializer(), ...) |
GrpcTransport(string $endpoint, ProtobufSerializer $serializer, ...) |
GrpcTransport(string $endpoint, ...) - serializer parameter removed |
5) flow-php/telemetry - ErrorHandler contract added
New namespace Flow\Telemetry\ErrorHandler with: ErrorHandler (interface), ErrorLogHandler, NullErrorHandler,
StreamHandler, SyslogHandler, UdpSyslogHandler, CompositeErrorHandler.
| Before | After |
|---|---|
otlp_exporter($transport) |
otlp_exporter($transport, ErrorHandler $errorHandler = new ErrorLogHandler()) |
telemetry_handler($logger, $converter, $level, $bubble) (Monolog) |
telemetry_handler($logger, $converter, $level, $bubble, ErrorHandler $errorHandler = new ErrorLogHandler()) |
6) flow-php/telemetry-otlp-bridge - HttpTransport removed
| Before | After |
|---|---|
Flow\Bridge\Telemetry\OTLP\Transport\HttpTransport |
removed |
otlp_http_transport() |
removed (use otlp_curl_transport()) |
psr/http-client (runtime require) |
removed (dev only) |
psr/http-factory (runtime require) |
removed (dev only) |
7) flow-php/telemetry-otlp-bridge - Per-signal OTLP exporters merged
| Before | After |
|---|---|
OTLPSpanExporter, OTLPMetricExporter, OTLPLogExporter |
Flow\Bridge\Telemetry\OTLP\Exporter\OTLPExporter |
otlp_span_exporter($transport) |
otlp_exporter($transport) |
otlp_metric_exporter($transport) |
otlp_exporter($transport) |
otlp_log_exporter($transport) |
otlp_exporter($transport) |
8) flow-php/telemetry-otlp-bridge - Curl/gRPC timeouts switched to milliseconds
| Before | After |
|---|---|
CurlTransportOptions::withTimeout(int $seconds), default 30 |
CurlTransportOptions::withTimeout(int $milliseconds), default 250 |
CurlTransportOptions::withConnectTimeout(int $seconds), default 10 |
CurlTransportOptions::withConnectTimeout(int $milliseconds), default 250 |
CurlTransportOptions::timeout() |
CurlTransportOptions::timeoutMs() |
CurlTransportOptions::connectTimeout() |
CurlTransportOptions::connectTimeoutMs() |
| - | CurlTransportOptions::withShutdownTimeout(int $milliseconds) / shutdownTimeoutMs(), default 5000 (added) |
otlp_curl_transport(string $endpoint, Serializer $serializer, CurlTransportOptions $options) |
otlp_curl_transport(string $endpoint, JsonSerializer|ProtobufSerializer $serializer = new JsonSerializer(), CurlTransportOptions $options = new CurlTransportOptions(), ?Transport $failover = null) |
otlp_grpc_transport(string $endpoint, ProtobufSerializer $serializer, array $headers = [], bool $insecure = true) |
otlp_grpc_transport(string $endpoint, array $headers = [], bool $insecure = true, int $timeoutMs = 250, int $shutdownTimeoutMs = 5000, ?Transport $failover = null) |
9) flow-php/telemetry-otlp-bridge - StreamTransport added
New transport, no removal counterpart.
| Before | After |
|---|---|
| - | Flow\Bridge\Telemetry\OTLP\Transport\StreamTransport |
| - | otlp_stream_transport(string $destination, int $filePermissions = 0644, bool $createDirectories = true) |
10) flow-php/telemetry-otlp-bridge - open-telemetry/gen-otlp-protobuf dependency dropped
| Before | After |
|---|---|
open-telemetry/gen-otlp-protobuf (required dep) |
removed; protobuf classes shipped inside the bridge under the same Opentelemetry\Proto\... namespace |
11) flow-php/symfony-telemetry-bundle - Configuration schema rewrite
No BC shim. Configurations from 0.36 must be rewritten.
| Before | After |
|---|---|
exporters.<name>.type: otlp|service|console|memory|void |
sub-block keyed by implementation: otlp:, service:, console:, memory:, void: |
exporters.<name>.service_id: <id> (under type: service) |
exporters.<name>.service: { id: <id> } |
Inline processor.exporter: { type: otlp, transport: {...} } |
processor.exporter: <name> referencing top-level exporters: map |
transport.type: http |
removed |
transport.timeout (seconds, default 30) |
transport.timeout_ms (default 250) |
transport.connect_timeout (seconds, default 10) |
transport.connect_timeout_ms (default 250) |
transport.http_client_service_id / request_factory_service_id / stream_factory_service_id |
removed |
| - | transport.type: stream (added) |
| - | transport.shutdown_timeout_ms default 5000 (added) |
| - | transport.failover: { type: ..., ... } on curl/grpc primaries (added) |
| - | top-level error_handlers: map (added) |
| - | error_handler: <name> references on providers, processors, otlp exporter (added) |
| - | top-level framework_logger: <name> (added) |
Before:
flow_telemetry:
exporters:
otlp:
type: otlp
transport:
type: curl
endpoint: 'http://otel-collector:4318'
timeout: 30
connect_timeout: 10
custom: { type: service, service_id: 'app.x' }
debug: { type: console }
tracer_provider:
processor:
type: batching
exporter: { type: otlp, transport: { type: curl, endpoint: 'http://otel-collector:4318' } }
After:
flow_telemetry:
error_handlers:
default: { type: error_log }
exporters:
otlp:
otlp:
transport:
type: curl
endpoint: 'http://otel-collector:4318'
timeout_ms: 250
connect_timeout_ms: 250
custom: { service: { id: 'app.x' } }
debug: { console: ~ }
tracer_provider:
processor:
type: batching
exporter: otlp
12) flow-php/phpunit-telemetry-bridge - Configuration parameters
| Before (parameter / default) | After (parameter / default) |
|---|---|
curl_timeout / 30 (seconds) |
curl_timeout_ms / 250 (ms) |
curl_connect_timeout / 10 (s) |
curl_connect_timeout_ms / 250 (ms) |
transport: curl|grpc |
transport: curl|grpc|stream |
| - | grpc_timeout_ms / 250 (added) |
| - | shutdown_timeout_ms / 5000 (added) |
| - | batch_size / 512 (added) |
| - | error_handler / error_log and error_handler_* family (added) |
| - | stream_file_permissions / 0644, stream_create_directories / true (added) |
| Default span/metric/log processors: pass-through | Default span/metric/log processors: batching (batch_size: 512) |
otel_collector_url / FLOW_PHPUNIT_OTEL_COLLECTOR_URL remain deprecated aliases for endpoint with
transport=curl.
Upgrading from 0.35.x to 0.36.x
1) flow-php/postgresql - RawCondition and RawExpression removed
The raw_cond() and raw_expr() escape hatches have been removed. All query builder operations are now covered by
type-safe DSL functions.
| Removed | Replacement |
|---|---|
raw_cond('NOT col') |
not_(is_true(col('col'))) |
raw_cond('a = ANY(b)') |
any_(col('a'), ComparisonOperator::EQ, col('b')) |
raw_cond("x IN ('a', 'b')") |
in_(col('x'), [literal('a'), literal('b')]) |
raw_cond("x NOT LIKE 'pg_%'") |
not_like(col('x'), literal('pg_%')) |
raw_expr('NOT col') |
not_(col('col')) |
raw_expr('a || b') |
concat(col('a'), col('b')) or binary_expr(col('a'), '||', col('b')) |
raw_expr('CASE x WHEN ...') |
case_when([when(...)], operand: col('x')) |
raw_expr('array_agg(DISTINCT ...)') |
agg('array_agg', [...], distinct: true)->withOrderBy(...) |
RawCondition class |
Use specific condition classes |
RawExpression class |
Use specific expression classes |
2) flow-php/postgresql - Condition now extends Expression
Conditions are now expressions - they can be used in SELECT lists, CASE WHEN, ORDER BY, etc.
// Conditions can now be aliased and used as expressions:
select(eq(col('a'), col('b'))->as('is_equal'));
// NOT works in both WHERE and SELECT:
not_(col('is_deleted'))->as('is_active');
// CASE WHEN accepts conditions directly:
case_when([when(eq(col('x'), literal(0)), literal('zero'))]);
3) flow-php/postgresql - DSL condition function renames
Function names have been unified following standard SQL builder conventions (jOOQ, Diesel, SQLAlchemy).
| Removed | Replacement | Reason |
|---|---|---|
neq() |
ne() |
Standard short form |
lte() |
le() |
Standard short form |
gte() |
ge() |
Standard short form |
is_in() |
in_() |
Drop is_ prefix, trailing underscore for PHP keyword |
is_distinct_from() |
distinct_from() |
Drop is_ prefix |
cond_and() |
and_() |
Drop cond_ prefix, trailing underscore for PHP keyword |
cond_or() |
or_() |
Drop cond_ prefix, trailing underscore for PHP keyword |
cond_not() |
not_() |
Drop cond_ prefix, trailing underscore for PHP keyword |
any_sub_select() |
any_() |
Unified - accepts both Expression and SelectFinalStep |
all_sub_select() |
all_() |
Unified - accepts both Expression and SelectFinalStep |
cond_true() |
is_true(literal(true)) |
Use is_true() with literal |
cond_false() |
is_true(literal(false)) |
Use is_true() with literal |
bool_cond() |
is_true() |
Wraps expression as boolean condition |
any_array() |
any_() |
Merged into unified any_() |
all_array() |
all_() |
Merged into unified all_() |
New functions added:
| Function | Purpose |
|---|---|
is_true(Expression) |
Wrap expression as boolean condition for WHERE |
not_like(Expression, Expression) |
NOT LIKE condition |
concat(Expression, ...) |
String concatenation with || operator |
Before:
use function Flow\PostgreSql\DSL\{cond_and, cond_not, cond_true, neq, lte, gte, is_in, any_sub_select};
select(col('name'))
->where(cond_and(
neq(col('status'), literal('deleted')),
lte(col('age'), literal(65)),
gte(col('age'), literal(18)),
is_in(col('role'), [literal('admin'), literal('user')]),
));
After:
use function Flow\PostgreSql\DSL\{and_, ne, le, ge, in_};
select(col('name'))
->where(and_(
ne(col('status'), literal('deleted')),
le(col('age'), literal(65)),
ge(col('age'), literal(18)),
in_(col('role'), [literal('admin'), literal('user')]),
));
4) flow-php/postgresql - Schema builder methods accept Expression/Condition instead of strings
Methods that previously accepted raw SQL strings now require typed Expression or Condition objects.
| Method | Before (string) | After (typed) |
|---|---|---|
ColumnDefinition::check() |
->check('age > 0') |
->check(gt(col('age'), literal(0))) |
ColumnDefinition::defaultRaw() |
->defaultRaw('CURRENT_TIMESTAMP') |
->defaultRaw(current_timestamp()) |
ColumnDefinition::generatedAs() |
->generatedAs("a || b") |
->generatedAs(concat(col('a'), col('b'))) |
CheckConstraint::create() |
::create('age > 0') |
::create(gt(col('age'), literal(0))) |
ExcludeConstraint::element() |
->element('col', '=') |
->element(col('col'), '=') |
ExcludeConstraint::where() |
->where('active = true') |
->where(eq(col('active'), literal(true))) |
CreateDomainBuilder::check() |
->check('VALUE > 0') |
->check(gt(col('VALUE'), literal(0))) |
CreateDomainBuilder::default() |
->default("'text'") |
->default(literal('text')) |
AlterDomainBuilder::addConstraint() |
->addConstraint('name', 'VALUE > 0') |
->addConstraint('name', gt(col('VALUE'), literal(0))) |
AlterDomainBuilder::setDefault() |
->setDefault('100') |
->setDefault(literal(100)) |
AlterTableBuilder::alterColumnSetDefault() |
->alterColumnSetDefault('col', "'val'") |
->alterColumnSetDefault('col', literal('val')) |
CreateRuleBuilder::where() |
->where("OLD.role = 'admin'") |
->where(eq(col('role', 'OLD'), literal('admin'))) |
5) flow-php/postgresql - DSL functions split into separate files
The monolithic functions.php has been split into 5 focused files (same namespace, no import changes needed):
| File | Purpose |
|---|---|
query.php |
Query building, expressions, tables, ordering, CTE, window, locking, transactions, cursors |
condition.php |
Comparisons, predicates, logic, JSON/array/regex operators |
schema.php |
DDL, constraints, indexes, maintenance, privileges, types, schema definitions |
client.php |
Connections, telemetry, mappers |
parser.php |
SQL parsing, formatting, analysis |
6) flow-php/filesystem - Protocol and Backend removed, Mount rewritten
The filesystem library has been redesigned around a single mount-protocol string. The Protocol and
Backend value types are gone; Mount now wraps just a protocol name.
Protocol class removed. Path::protocol() now returns string instead of a Protocol object. Callsites that
unpacked Protocol::$name / Protocol::scheme() / Protocol::is() are mechanical updates:
| Before | After |
|---|---|
$path->protocol()->name |
$path->protocol() |
$path->protocol()->scheme() |
$path->protocol() . '://' |
$path->protocol()->is('file') |
$path->protocol() === 'file' |
$fs->protocol()->validateScheme($path) |
$fs->mount()->supports($path) || throw new InvalidSchemeException(...) |
new Protocol('file') |
new Mount('file') (if you need a Mount) or plain 'file' (FilesystemTable::for accepts strings) |
Backend enum removed. There's no closed set of backends anymore - any filesystem can mount under any protocol. The
Symfony bundle schema now uses a plain string type: field (see below). If you branched on Backend cases in
application code, replace with string comparisons against the factory type() or the mount protocol, whichever fits.
Mount rewritten. The shape is now:
final readonly class Mount
{
public string $protocol;
public function __construct(string $protocol); // validates against PROTOCOL_REGEX
public function supports(Path|string $path) : bool;
}
Filesystem::protocol() renamed to Filesystem::mount(). The return type changed from
Protocol to Mount. Every Filesystem implementation must rename the method.
Filesystem ctors take Mount directly. NativeLocalFilesystem, MemoryFilesystem,
StdOutFilesystem, AsyncAWSS3Filesystem, AzureBlobFilesystem now accept Mount as the first constructor argument
(local filesystems have a sensible default). DSL factory functions (native_local_filesystem, memory_filesystem,
stdout_filesystem, aws_s3_filesystem,
azure_filesystem) accept string $protocol as the last argument with a sensible default ('file', 'memory',
'stdout', 'aws-s3', 'azure-blob') and build the Mount internally - no caller change needed unless you
instantiate the filesystem class directly or mount two filesystems of the same backend under distinct protocols.
Auto-alias dropped. Previously, mounting a single filesystem of a given backend would auto-register its canonical
scheme as an additional alias (e.g. mounting S3 as warehouse also made aws-s3
available). That behavior is gone - every mount is registered under exactly the protocol you pick. If you need two
protocols for the same filesystem, mount it twice explicitly.
FilesystemTable::for(Path|Protocol) → for(Path|string). Pass a Path or a plain protocol string.
7) flow-php/filesystem - NativeLocalFilesystem::list() no longer sorts results
Glob::glob() was replaced with lazy Webmozart\Glob\Iterator\GlobIterator to avoid materializing the entire matching
set up front (this gives a ~30× speedup on large trees when the caller only needs the first N entries).
Side effect: NativeLocalFilesystem::list() no longer returns results in alphabetical order. Output now follows
filesystem traversal order. If your code depends on sort order, sort client-side after consuming the generator:
$statuses = iterator_to_array($fs->list(path('/some/dir/**/*.txt')));
usort($statuses, static fn (FileStatus $a, FileStatus $b) => $a->path->uri() <=> $b->path->uri());
8) flow-php/symfony-filesystem-bundle - YAML schema now uses type: + protocol-as-key
The configuration schema changed significantly. The YAML key under filesystems: is now the mount protocol (any
valid URI scheme), and a separate type: field picks the factory.
Before:
flow_filesystem:
fstabs:
default:
filesystems:
file: ~
memory: ~
aws-s3:
bucket: '%env(S3_BUCKET)%'
After:
flow_filesystem:
fstabs:
default:
filesystems:
file:
type: file
memory:
type: memory
aws-s3: # mount protocol - can be any valid URI scheme
type: aws_s3 # factory lookup key
bucket: '%env(S3_BUCKET)%'
Benefits of the new shape:
- Mount the same backend twice under different protocols (e.g.
warehouse+archivebothtype: aws_s3with different buckets). - Protocol names are no longer tied to factory names - pick whatever reads well in your application.
Built-in type values: file, memory, stdout, aws_s3, azure_blob.
9) flow-php/symfony-filesystem-bundle - FilesystemFactory interface and attribute changed
// Before
interface FilesystemFactory
{
public function protocol() : Protocol;
public function create(string $mountName, array $config) : Filesystem;
}
#[AsFilesystemFactory(protocol: 'my-fs')]
// After
interface FilesystemFactory
{
public function type() : string;
public function create(string $protocol, array $config) : Filesystem;
}
#[AsFilesystemFactory(type: 'my_backend')]
The DI tag attribute is renamed from protocol to type. FilesystemFactoryRegistry::get() takes a
string $type instead of a Backend.
10) flow-php/symfony-filesystem-bundle - flow:filesystem:ls CLI flags reshuffled
| Before | After |
|---|---|
--long (default: off) |
4-column output is now the default; use --short to drop it |
--no-limit |
Removed - default is unlimited now. Use --limit=N to cap. |
(no --page-size) |
New --page-size=N (default 10) controls table page size |
(no --offset) |
New --offset=N skips the first N entries |
--format=json → JSON array |
--format=json now emits NDJSON (one JSON object per line) |
Default behavior: list all entries, paginated in tables of 10 rows; interactive terminals prompt between pages (Enter continues, "no" stops), piped output flows continuously. Size is formatted with binary units, Modified as ISO-8601 - both read from the backend listing response, no per-file HEAD.
11) flow-php/symfony-filesystem-bundle - flow:filesystem:stat rejects pattern paths
stat now returns Command::FAILURE with a clear error when given a pattern path (memory://*.txt,
**/*.parquet, ...). Previously it returned metadata for the first match - confusing semantics. Use
flow:filesystem:ls for pattern inspection.
12) flow-php/filesystem-async-aws-bridge,
flow-php/filesystem-azure-bridge - DSL protocol is the last argument with a default
The DSL factories expose the mount protocol as an optional last argument, defaulted to the conventional scheme. Common cases work without passing it:
aws_s3_filesystem($bucket, $client); // mounts under 'aws-s3'
azure_filesystem($blobService); // mounts under 'azure-blob'
// Pick a different protocol - e.g. mount the same bucket twice
aws_s3_filesystem($bucket, $client, protocol: 'warehouse');
azure_filesystem($blobService, protocol: 'archive');
13) flow-php/filesystem - path_memory() and path_stdout() DSL helpers removed
Build Path directly instead:
// Before
$mem = path_memory();
$out = path_stdout(['stream' => 'output']);
// After
$mem = path('memory://' . bin2hex(random_bytes(16)) . '.memory');
$out = path('stdout://' . bin2hex(random_bytes(16)) . '.stdout', ['stream' => 'output']);
14) flow-php/etl - ConfigBuilder::cacheFilesystem() and externalSortFilesystem() added
Point cache and external-sort mechanisms at any mounted protocol; defaults remain 'file'. The
CacheConfig and SortConfig value objects expose the chosen protocol as ->filesystemProtocol.
$config = config_builder()
->mount(aws_s3_filesystem($bucket, $client, protocol: 'sort-scratch'))
->externalSortFilesystem('sort-scratch')
->build();
15) flow-php/symfony-http-foundation-bridge - Output interface collapsed to a single loader(Path)
Output::memoryLoader(string $id) and Output::stdoutLoader() were replaced by
Output::loader(Path $path). FlowBufferedResponse gained a string $filesystem = 'memory'
constructor argument (buffer protocol); FlowStreamedResponse gained
string $stdoutFilesystemProtocol = 'stdout'. Each response builds the path with its configured protocol and passes it
to the Output.
// Before
new FlowBufferedResponse($extractor, new CsvOutput(), $transformations);
// After - same defaults, new constructor param available
new FlowBufferedResponse($extractor, new CsvOutput(), $transformations, filesystem: 'memory');
16) flow-php/filesystem - StdOutFilesystem tracks open streams per php:// target
Previously the "only one stdout stream" guard lived in FilesystemStreams (ETL core) and fired when two writing streams
used the stdout:// protocol. The check now lives in StdOutFilesystem itself and is precise per underlying php://
target (stdout / stderr / output): two streams with
['stream' => 'stdout'] conflict; one stdout stream + one stderr stream do not. Error message changed from "Only one
stdout filesystem stream can be open at the same time" to "Only one stream can be open at the same time for php:
//{target}".
Upgrading from 0.34.x to 0.35.x
1) flow-php/postgresql - DataType renamed to ColumnType
The DataType class used for schema/DDL definitions has been renamed to ColumnType to better communicate its purpose.
All related DSL functions have been renamed from data_type_* to column_type_*.
| Removed | Replacement |
|---|---|
Flow\PostgreSql\QueryBuilder\Schema\DataType |
Flow\PostgreSql\QueryBuilder\Schema\ColumnType |
Flow\PostgreSql\Parser\DataTypeParser |
Flow\PostgreSql\Parser\ColumnTypeParser |
data_type_integer() |
column_type_integer() |
data_type_smallint() |
column_type_smallint() |
data_type_bigint() |
column_type_bigint() |
data_type_boolean() |
column_type_boolean() |
data_type_text() |
column_type_text() |
data_type_varchar() |
column_type_varchar() |
data_type_char() |
column_type_char() |
data_type_numeric() |
column_type_numeric() |
data_type_decimal() |
column_type_decimal() |
data_type_real() |
column_type_real() |
data_type_double_precision() |
column_type_double_precision() |
data_type_date() |
column_type_date() |
data_type_time() |
column_type_time() |
data_type_timestamp() |
column_type_timestamp() |
data_type_timestamptz() |
column_type_timestamptz() |
data_type_interval() |
column_type_interval() |
data_type_uuid() |
column_type_uuid() |
data_type_json() |
column_type_json() |
data_type_jsonb() |
column_type_jsonb() |
data_type_bytea() |
column_type_bytea() |
data_type_inet() |
column_type_inet() |
data_type_cidr() |
column_type_cidr() |
data_type_macaddr() |
column_type_macaddr() |
data_type_serial() |
column_type_serial() |
data_type_smallserial() |
column_type_smallserial() |
data_type_bigserial() |
column_type_bigserial() |
data_type_array() |
column_type_array() |
data_type_custom() |
column_type_custom() |
data_type_from_string() |
column_type_from_string() |
Before:
use Flow\PostgreSql\QueryBuilder\Schema\DataType;
use function Flow\PostgreSql\DSL\data_type_integer;
use function Flow\PostgreSql\DSL\data_type_varchar;
column('age', data_type_integer());
column('name', data_type_varchar(255));
cast(ref('id'), DataType::bigint());
After:
use Flow\PostgreSql\QueryBuilder\Schema\ColumnType;
use function Flow\PostgreSql\DSL\column_type_integer;
use function Flow\PostgreSql\DSL\column_type_varchar;
column('age', column_type_integer());
column('name', column_type_varchar(255));
cast(ref('id'), ColumnType::bigint());
2) flow-php/postgresql - PostgreSqlType renamed to ValueType
The PostgreSqlType enum used for value binding/casting has been renamed to ValueType to better communicate its
purpose. All related DSL functions have been renamed from pgsql_type_* to value_type_*.
| Removed | Replacement |
|---|---|
Flow\PostgreSql\Client\Types\PostgreSqlType |
Flow\PostgreSql\Client\Types\ValueType |
pgsql_type_text() |
value_type_text() |
pgsql_type_varchar() |
value_type_varchar() |
pgsql_type_integer() |
value_type_integer() |
pgsql_type_bigint() |
value_type_bigint() |
pgsql_type_smallint() |
value_type_smallint() |
pgsql_type_boolean() |
value_type_boolean() |
pgsql_type_float4() |
value_type_float4() |
pgsql_type_float8() |
value_type_float8() |
pgsql_type_numeric() |
value_type_numeric() |
pgsql_type_date() |
value_type_date() |
pgsql_type_timestamp() |
value_type_timestamp() |
pgsql_type_timestamptz() |
value_type_timestamptz() |
pgsql_type_json() |
value_type_json() |
pgsql_type_jsonb() |
value_type_jsonb() |
pgsql_type_uuid() |
value_type_uuid() |
pgsql_type_bytea() |
value_type_bytea() |
pgsql_type_inet() |
value_type_inet() |
pgsql_type_cidr() |
value_type_cidr() |
All other pgsql_type_*() functions |
Corresponding value_type_*() functions |
Before:
use Flow\PostgreSql\Client\Types\PostgreSqlType;
use function Flow\PostgreSql\DSL\pgsql_type_uuid;
use function Flow\PostgreSql\DSL\pgsql_type_text_array;
typed('550e8400-e29b-41d4-a716-446655440000', pgsql_type_uuid());
typed(['tag1', 'tag2'], pgsql_type_text_array());
typed(42, PostgreSqlType::INT4);
After:
use Flow\PostgreSql\Client\Types\ValueType;
use function Flow\PostgreSql\DSL\value_type_uuid;
use function Flow\PostgreSql\DSL\value_type_text_array;
typed('550e8400-e29b-41d4-a716-446655440000', value_type_uuid());
typed(['tag1', 'tag2'], value_type_text_array());
typed(42, ValueType::INT4);
Upgrading from 0.31.x to 0.32.x
1) Removal of Meilisearch Adapter
The Meilisearch adapter has been removed from Flow PHP. If you were using it, please migrate to Elasticsearch adapter.
2) Removed deprecated DSL functions
All type-related DSL functions have been moved from Flow\ETL\DSL to Flow\Types\DSL. Update your imports accordingly.
| Removed Function | Replacement |
|---|---|
chunks_from() |
batches() |
type_structure() |
\Flow\Types\DSL\type_structure() |
type_union() |
\Flow\Types\DSL\type_union() |
type_optional() |
\Flow\Types\DSL\type_optional() |
type_from_array() |
\Flow\Types\DSL\type_from_array() |
is_nullable() |
\Flow\Types\DSL\type_is_nullable() |
type_equals() |
\Flow\Types\DSL\type_equals() |
types() |
\Flow\Types\DSL\types() |
type_list() |
\Flow\Types\DSL\type_list() |
type_map() |
\Flow\Types\DSL\type_map() |
type_json() |
\Flow\Types\DSL\type_json() |
type_datetime() |
\Flow\Types\DSL\type_datetime() |
type_date() |
\Flow\Types\DSL\type_date() |
type_time() |
\Flow\Types\DSL\type_time() |
type_xml() |
\Flow\Types\DSL\type_xml() |
type_xml_element() |
\Flow\Types\DSL\type_xml_element() |
type_uuid() |
\Flow\Types\DSL\type_uuid() |
type_int() |
\Flow\Types\DSL\type_integer() |
type_integer() |
\Flow\Types\DSL\type_integer() |
type_string() |
\Flow\Types\DSL\type_string() |
type_float() |
\Flow\Types\DSL\type_float() |
type_boolean() |
\Flow\Types\DSL\type_boolean() |
type_instance_of() |
\Flow\Types\DSL\type_instance_of() |
type_resource() |
\Flow\Types\DSL\type_resource() |
type_array() |
\Flow\Types\DSL\type_array() |
type_callable() |
\Flow\Types\DSL\type_callable() |
type_null() |
\Flow\Types\DSL\type_null() |
type_enum() |
\Flow\Types\DSL\type_enum() |
struct_schema() |
structure_schema() |
get_type() |
\Flow\Types\DSL\get_type() |
print_schema() |
schema_to_ascii() |
type_is() |
\Flow\Types\DSL\type_is() |
type_is_any() |
\Flow\Types\DSL\type_is_any() |
dom_element_to_string() |
\Flow\Types\DSL\dom_element_to_string() |
3) Removed deprecated DataFrame methods
| Removed Method | Replacement |
|---|---|
DataFrame::validate() |
DataFrame::match() |
DataFrame::renameAll() |
DataFrame::renameEach(rename_replace(...)) |
DataFrame::renameAllLowerCase() |
DataFrame::renameEach(rename_style(StringStyles::LOWER)) |
DataFrame::renameAllUpperCase() |
DataFrame::renameEach(rename_style(StringStyles::UPPER)) |
DataFrame::renameAllUpperCaseFirst() |
DataFrame::renameEach(rename_style(StringStyles::UCFIRST)) |
DataFrame::renameAllUpperCaseWord() |
DataFrame::renameEach(rename_style(StringStyles::UCWORDS)) |
DataFrame::renameAllStyle() |
DataFrame::renameEach(rename_style(...)) |
4) Removed deprecated Schema methods
| Removed Method | Replacement |
|---|---|
Schema::entries() |
Schema::references()->all() |
Schema::getDefinition() |
Schema::get() |
Schema::nullable() |
Schema::makeNullable() |
5) Removed deprecated Definition methods
| Removed Method | Replacement |
|---|---|
Definition::nullable() |
Definition::makeNullable() |
This applies to all Definition implementations: BooleanDefinition, DateDefinition, DateTimeDefinition,
EnumDefinition, FloatDefinition, HTMLDefinition, HTMLElementDefinition, IntegerDefinition, JsonDefinition,
ListDefinition, MapDefinition, StringDefinition, StructureDefinition, TimeDefinition, UuidDefinition,
XMLDefinition, XMLElementDefinition.
6) Removed deprecated FileExtractor and PathFiltering methods
| Removed Method | Replacement |
|---|---|
FileExtractor::addFilter() |
FileExtractor::withPathFilter() |
PathFiltering::addFilter() |
PathFiltering::withPathFilter() |
7) Removed deprecated ScalarFunctionChain methods
| Removed Method | Replacement |
|---|---|
ScalarFunctionChain::domElementAttribute() |
ScalarFunctionChain::domElementAttributeValue() |
8) Removed deprecated Config constants
| Removed Constant | Replacement |
|---|---|
Config::CACHE_DIR_ENV |
CacheConfig::CACHE_DIR_ENV |
Config::SORT_MAX_MEMORY_ENV |
SortConfig::SORT_MAX_MEMORY_ENV |
9) Removed deprecated Transformers
| Removed Transformer | Replacement |
|---|---|
EntryNameStyleConverterTransformer |
Use DataFrame::renameEach(rename_style(...)) |
RenameAllCaseTransformer |
Use DataFrame::renameEach(rename_style(...)) |
RenameStrReplaceAllEntriesTransformer |
Use DataFrame::renameEach(rename_replace(...)) |
10) Removed deprecated classes
| Removed Class | Replacement |
|---|---|
Flow\ETL\Function\StyleConverter\StringStyles |
Flow\ETL\String\StringStyles |
Upgrading from 0.28.x to 0.29.x
1) JsonType now uses Json value object instead of string
The JsonType has been refactored to use a dedicated Json value object (similar to Uuid/UuidType pattern). This
allows static analysis tools to distinguish between regular strings and JSON strings.
Breaking Changes:
JsonType::assert()now returnsJsoninstance instead ofstringJsonType::cast()now returnsJsoninstance instead ofstringJsonType::isValid()now checks forJsoninstance (plain strings are no longer valid)Cast::cast('json', $value)function now returnsJsonobject instead of stringtype_json()return type annotation changed fromType<string>toType<Json>JsonEntry::value()now returns?Jsoninstead of?array(consistent withUuidEntry::value()returning?Uuid)JsonEntry::json()method removed (usevalue()instead)
Migration:
If you were using type_json()->cast($value) and expected a string, use ->toString():
Before:
$jsonString = type_json()->cast($array); // was string
After:
$json = type_json()->cast($array); // now Json object
$jsonString = $json->toString(); // get the string
$jsonArray = $json->toArray(); // get as array
If you were using JsonEntry::value() and expected an array:
Before:
$entry = json_entry('data', ['key' => 'value']);
$array = $entry->value(); // was array
After:
$entry = json_entry('data', ['key' => 'value']);
$json = $entry->value(); // now Json object
$array = $json?->toArray(); // get as array
$string = $json?->toString(); // get as string
If you were using JsonEntry::json():
Before:
$json = $entry->json();
After:
$json = $entry->value(); // json() method removed, use value() instead
New Json value object features:
use Flow\Types\Value\Json;
// Create from string
$json = new Json('{"key": "value"}');
// Create from array
$json = Json::fromArray(['key' => 'value']);
// Check if valid JSON
Json::isValid('{"key": "value"}'); // true
// Convert to string/array
$json->toString(); // '{"key":"value"}'
$json->toArray(); // ['key' => 'value']
// Json implements Stringable
(string) $json; // '{"key":"value"}'
// Json implements JsonSerializable
json_encode($json); // '{"key":"value"}'
Note: JsonEntry::value() now returns ?Json for consistency with UuidEntry::value() returning ?Uuid. Use
->toArray() or ->toString() on the Json object to get the underlying data.
Row methods behavior:
// Row::toArray() converts Json to array automatically (for convenient serialization)
$row->toArray(); // Returns ['data' => ['key' => 'value']] not ['data' => Json(...)]
// Row::valueOf() returns the raw value (Json object for json entries)
$row->valueOf('data'); // Returns Json object (use ->toArray() if you need array)
// Entry value() returns the typed value
$row->get('data')->value(); // Returns Json object (use ->toArray() if you need array)
Upgrading from 0.26.x to 0.27.x
1) Force EntryFactory $entryFactory to be required on array_to_row & array_to_row(s)
Before:
to_entry('name', 'data');
array_to_row([]);
array_to_rows([]);
After:
to_entry('name', 'data', flow_context(config())->entryFactory());
array_to_row([], flow_context(config())->entryFactory());
array_to_rows([], flow_context(config())->entryFactory());
Upgrading from 0.16.x to 0.17.x
1) Removed $nullable property from all types
Before:
type_string(nullable:true)->toString() // ?string
After:
type_optional(string())->toString() // ?string
2) Removed precision from float_type()
Before float_type() use to have default precision 6. This means that any operations on float had to round values to
given precision. The problem with this approach is that all operations now need to receive a dedicated rounding option.
Instead, end users should handle precision of float columns through round() scalar function.
3) Moved all Types to Flow\Types\Type namespace
Before
\Flow\ETL\DSL\type_string(); // now deprecated, alias for \Flow\Types\DSL\type_string();
After
\Flow\Types\DSL\type_string();
Upgrading from 0.15.x to 0.16.x
1) Deprecated Flow\ETL\DataFrame::renameAll* methods
Methods:
Flow\ETL\DataFrame::renameAll(),Flow\ETL\DataFrame::renameAllLowerCase(),Flow\ETL\DataFrame::renameAllUpperCase(),Flow\ETL\DataFrame::renameAllUpperCaseFirst(),Flow\ETL\DataFrame::renameAllUpperCaseWord(),
Were deprecated in favor of using new method: DataFrame::renameEach() with proper RenameEntryStrategy object.
2) Deprecated RenameAllCaseTransformer & RenameStrReplaceAllEntriesTransformer
Selected transformers were deprecated in favor of using DataFrame::renameEach() with related RenameEntryStrategy:
RenameAllCaseTransformer->RenameCaseTransformer,RenameStrReplaceAllEntriesTransformer->RenameReplaceStrategy,
Upgrading from 0.14.x to 0.15.x
1) Removed Flow\ETL\Row\Schema\Matcher and implementations
Schema Matcher was the initial attempt to implement a schema evolution next to schema validation that over time got replaced with a different implementation of Schema Validator.
2) Renamed Flow\ETL\Row\Schema namespace into Flow\ETL\Schema.
This means all classes related to Schema now live under Flow\ETL\Schema namespace.
Upgrading from 0.11.x to 0.14.x
1) Replaced Flow\ETL\DataFrame::validate() with Flow\ETL\DataFrame::match()
The old method is now deprecated and will be removed in the next release.
2) Replaced Flow\ETL\Function\ScalarFunction\TypedScalarFunction with
Flow\ETL\Function\ScalarFunction\ScalarResult.
The old interface was used to allow defining the return type of the ScalarFunctions. It was replaced with a ScalarResult value object that is much more flexible than the interface, because it's allowing to return any type dynamically without making the scalar function stateful.
Upgrading from 0.10.x to 0.11.x
1) Removed StructureElement/struct_element/structure_element from StructureType Definition
Before:
type_structure([
struct_element('name', string()),
struct_element('age', integer()),
]);
After:
type_structure([
'name' => string(),
'age' => integer(),
]);
2) Doctrine DBAL Adapter
From now options for:
to_dbal_table_insert()to_db_table_update()
are passed as objects (instance of UpdateOptions|InsertOptions interfaces) and they are platform specific, so please use the proper class for the platform you are using.
- PostgreSQL
- PostgreSQLInsertOptions
- PostgreSQLUpdateOptions
- MySQL
- MySQLInsertOptions
- MySQLUpdateOptions
- Sqlite
- SQLiteInsertOptions
- SQLiteUpdateOptions
Upgrading from 0.8.x to 0.10.x
1) Providing multiple paths to a single extractor
From now to read from multiple locations use from_all(Extractor ...$extractors) : Exctractor extractor.
Before:
<?php
from_parquet([
path(__DIR__ . '/data/1.parquet'),
path(__DIR__ . '/data/2.parquet'),
]);
After:
<?php
from_all(
from_parquet(path(__DIR__ . '/data/1.parquet')),
from_parquet(path(__DIR__ . '/data/2.parquet')),
);
2) Passing optional arguments to extractors/loaders
From now all extractors/loaders are accepting only mandatory arguments, all optional arguments should be passed through
with* methods and fluent interface.
Before:
<?php
from_parquet(path(__DIR__ . '/data/1.parquet'), schema: $schema);
After:
<?php
from_parquet(path(__DIR__ . '/data/1.parquet'))->withSchema($schema);
Upgrading from 0.7.x to 0.8.x
1) Joins
To support joining bigger datasets, we had to move from initial NestedLoop join algorithm into Hash Join algorithm.
- the only supported coin expression is
=(equals) that can be grouped withANDandORoperators. joinPrefixis now always required, and by default is set to 'joined_'- join will always result all columns from both datasets, columns used in join condition will be prefixed with
joinPrefix.
Other than that, API stays the same.
Above changes were introduced in all 3 types of joins:
DataFrame::join()DataFrame::joinEach()DataFrame::crossJoin()
2) GroupBy
From now on, DataFrame::groupBy() method will return GroupedDataFrame object, which is nothing more than a GroupBy
statement Builder. To get the results, you first need to define the aggregation functions or optionally pivot the data.
Upgrading from 0.6.x to 0.7.x
1) DataFrame::appendSafe () method was removed
DataFrame::appendSafe() aka DataFrame::threadSafe() method was removed as it was introducing additional complexity
and was not used in any of the adapters.
Upgrading from 0.5.x to 0.6.x
1) Rows::merge () accepts single instance of Rows
Before:
Rows::merge(Rows ...$rows) : Rows
After:
Rows::merge(Rows $rows) : Rows
Upgrading from 0.4.x to 0.5.x
1) Entry factory moved from extractors to FlowContext
To improve code quality and reduce code coupling EntryFactory was removed from all constructors of extractors, in
favor of passing it into FlowContext & re-using same entry factory in a whole pipeline.
2) Invalid schema has no fallback in NativeEntryFactory
Before, passing Schema into NativeEntryFactory::create() had fallback when the given entry was not found in a passed
schema, now the schema has higher priority & fallback is no longer available, instead when the definition is missing in
a passed schema, InvalidArgumentException will be thrown.
3) BufferLoader was removed
BufferLoader was removed in favor of DataFrame::collect(int $batchSize = null) method which now accepts additional
argument $batchSize that will keep collecting Rows from Extractor until the given batch size is reached. Which does
exactly the same thing as BufferLoader did, but in a more generic way.
4) Pipeline Closure
Pipeline Closure was reduced to be only Loader Closure and it was moved to \Flow\ETL\Loader namespace. Additionally, \Closure::close method no longer requires Rows to be passed as an argument.
5) Parallelize
DataFrame::parallelize () method is deprecated, and it will be removed, instead use DataFrame::batchSize (int $size) method.
6) Rows in batch - Extractors
From now, file-based Extractors will always throw one Row at time, in order to merge them into bigger groups use
DataFrame::batchSize(int $size) just after extractor method.
Before:
<?php
(new Flow())
->read(CSV::from(__DIR__ . '/1_mln_rows.csv', rows_in_batch: 100))
->write(To::output())
->count();
After:
(new Flow())
->read(CSV::from(__DIR__ . '/1_mln_rows.csv',))
->batchSize(100)
->write(To::output())
->count();
Affected extractors:
- CSV
- Parquet
- JSON
- Text
- XML
- Avro
- DoctrineDBAL -
rows_in_batchwasn't removed, but now results are thrown row by row, instead of whole page. - GoogleSheet
7) GoogleSheetExtractor
Argument $rows_in_batch was renamed to $rows_per_page which no longer determines the size of the batch, but the size
of the page that will be fetched from Google API. Rows are yielded one by one.
8) DataFrame::threadSafe() method was replaced by DataFrame::appendSafe()
DataFrame::appendSafe() is doing exactly the same thing as the old method, it's just more descriptive and
self-explanatory. It's no longer mandatory to set this flat to true when using SaveMode::APPEND, it's now set
automatically.
9) Loaders - chunk size
Loaders are no longer accepting chunk_size parameter, from now in order to control the number of rows saved at once use
DataFrame::batchSize(int $size) method.
10) Removed DSL functions: datetime_string(), json_string()
Those functions were removed in favor of accepting string values in related DSL functions:
datetime_string()=>datetime(),json_string()=>json()&json_object()
11) Removed Asynchronous Processing
More details can be found in this issue.
- Removed etl-adapter-amphp
- Removed etl-adapter-reactphp
- Removed
LocalSocketPipeline - Removed
DataFrame::pipeline()
12) CollectionEntry removal
After adding native & logical types into the Flow, we remove the CollectionEntry as obsolete. New types that cover it
better are: ListType, MapType & StructureType along with related new entry types.
13) Removed from*() methods from scalar entries
Removed BooleanEntry::from(), FloatEntry::from(), IntegerEntry::from(), StringEntry::fromDateTime() methods in
favor of using DSL functions.
14) Removed deprecated Sha1IdFactory
Class Sha1IdFactory was removed, use HashIdFactory class:
(new HashIdFactory('entry_name'))->withAlgorithm('sha1');
15) Deprecate DSL Static classes
DSL static classes were deprecated in favor of using functions defined in src/core/etl/src/Flow/ETL/DSL/functions.php
file.
Deprecated classes:
src/core/etl/src/Flow/ETL/DSL/From.phpsrc/core/etl/src/Flow/ETL/DSL/Handler.phpsrc/core/etl/src/Flow/ETL/DSL/To.phpsrc/core/etl/src/Flow/ETL/DSL/Transform.phpsrc/core/etl/src/Flow/ETL/DSL/Partitions.phpsrc/adapter/etl-adapter-avro/src/Flow/ETL/DSL/Avro.phpsrc/adapter/etl-adapter-chartjs/src/Flow/ETL/DSL/ChartJS.phpsrc/adapter/etl-adapter-csv/src/Flow/ETL/DSL/CSV.phpsrc/adapter/etl-adapter-doctrine/src/Flow/ETL/DSL/Dbal.phpsrc/adapter/etl-adapter-elasticsearch/src/Flow/ETL/DSL/Elasticsearch.phpsrc/adapter/etl-adapter-google-sheet/src/Flow/ETL/DSL/GoogleSheet.phpsrc/adapter/etl-adapter-json/src/Flow/ETL/DSL/Json.phpsrc/adapter/etl-adapter-meilisearch/src/Flow/ETL/DSL/Meilisearch.phpsrc/adapter/etl-adapter-parquet/src/Flow/ETL/DSL/Parquet.phpsrc/adapter/etl-adapter-text/src/Flow/ETL/DSL/Text.phpsrc/adapter/etl-adapter-xml/src/Flow/ETL/DSL/XML.php
Upgrading from 0.3.x to 0.4.x
1) Transformers replaced with scalar functions
Transformers are a really powerful tool that was used in Flow since the beginning, but that tool was too powerful for the simple cases that were needed, and introduced additional complexity and maintenance issues when they were handwritten.
We reworked most of the internal transformers to new scalar functions and entry scalar functions (based on the built-in functions), and we still internally use that powerful tool, but we don't expose it to end users, instead, we provide easy-to-use, covering all user needs functions.
All available functions can be found in ETL\Row\Function folder or in
ETL\DSL\functions file, and entry scalar functions are defined in
EntryScalarFunction.
Before:
<?php
use Flow\ETL\Extractor\MemoryExtractor;
use Flow\ETL\Flow;
use Flow\ETL\DSL\Transform;
(new Flow())
->read(new MemoryExtractor())
->rows(Transform::string_concat(['name', 'last name'], ' ', 'name'))
After:
<?php
use function Flow\ETL\DSL\concat;
use function Flow\ETL\DSL\lit;
use Flow\ETL\Extractor\MemoryExtractor;
use Flow\ETL\Flow;
(new Flow())
->read(new MemoryExtractor())
->withEntry('name', concat(ref('name'), lit(' '), ref('last name')))
2) ref function nullability
ref("entry_name") is no longer returning null when the entry is not found. Instead, it throws an exception. The same
behavior can be achieved through using a newly introduced optional function:
Before:
<?php
use function Flow\ETL\DSL\optional;
use function Flow\ETL\DSL\ref;
ref('non_existing_column')->cast('string');
After:
<?php
use function Flow\ETL\DSL\optional;
use function Flow\ETL\DSL\ref;
optional(ref('non_existing_column'))->cast('string');
// or
optional(ref('non_existing_column')->cast('string'));
3) Extractors output
Affected extractors:
- CSV
- JSON
- Avro
- DBAL
- GoogleSheet
- Parquet
- Text
- XML
Extractors are no longer returning data under an array entry called row, thanks to this unpacking row become
redundant.
Because of that all DSL functions are no longer expecting $entry_row_name parameter, if it was used anywhere, please
remove it.
Before:
<?php
(new Flow())
->read(From::array([['id' => 1, 'array' => ['a' => 1, 'b' => 2, 'c' => 3]]]))
->withEntry('row', ref('row')->unpack())
->renameAll('row.', '')
->drop('row')
->withEntry('array', ref('array')->arrayMerge(lit(['d' => 4])))
->write(To::memory($memory = new ArrayMemory()))
->run();
After:
<?php
(new Flow())
->read(From::array([['id' => 1, 'array' => ['a' => 1, 'b' => 2, 'c' => 3]]]))
->withEntry('array', ref('array')->arrayMerge(lit(['d' => 4])))
->write(To::memory($memory = new ArrayMemory()))
->run();
4) ConfigBuilder::putInputIntoRows () output is now prefixed with _ (underscore)
In order to avoid collisions with datasets columns, additional columns created after using putInputIntoRows ()
would now be prefixed with _ (underscore) symbol.
Before:
<?php
$rows = (new Flow(Config::builder()->putInputIntoRows()))
->read(Json::from(__DIR__ . '/../Fixtures/timezones.json', 5))
->fetch();
foreach ($rows as $row) {
$this->assertSame(
[
...
'_input_file_uri',
],
\array_keys($row->toArray())
);
}
After:
<?php
$rows = (new Flow(Config::builder()->putInputIntoRows()))
->read(Json::from(__DIR__ . '/../Fixtures/timezones.json', 5))
->fetch();
foreach ($rows as $row) {
$this->assertSame(
[
...
'_input_file_uri',
],
\array_keys($row->toArray())
);
}
Found a typo or an outdated section? Edit this page on GitHub