Skip to content

Commit fc0be5d

Browse files
committed
refactor(format-table): serve format tables through the generic entry points
`TableScan`, `TableRead`, `FileStoreWrite` and `FileStoreCommit` now recognise a format table from the schema under the table path and dispatch to it, so a caller holding a path uses the same interface it uses for every other table, the way Java Paimon serves both through one `ReadBuilder` and one `BatchWriteBuilder`. The `FormatTable*` classes stay public for a caller that already holds a `FormatTable`. One `FormatTableLoader` answers the "is this a format table" question for all four and hands back the schema it read, so the managed path does not read the same file a second time. Anything a context carries that a format table cannot honour is refused by name rather than dropped. A read opens its data files through `DataFileReaderFactory`, the component the managed table path opens its own with, so prefetch, the read-ahead cache and the cache a `ReadContext` carries all apply here too. Partition columns are completed by `FieldMappingReader` and the row limit is left to the caller, which retires `PartitionCompletingBatchReader` and `LimitBatchReader`. Batches carry the leading `_VALUE_KIND` field every `BatchReader` promises, filled with inserts, so `BatchReader`'s contract is unchanged after all and an engine reading by field index sees one layout for every table. Options are read through `CoreOptions`, so a default lives in one place: `file.suffix.include.compression`, `format-table.partition-path-only-value`, `metastore.partitioned-table` and the format table compression chain join it there. A write stages its files under `_temporary/.tmp.<uuid>` beside where they will be published, the layout Java's `RenamingTwoPhaseOutputStream` stages under. - `FormatDataSplit` and `FormatCommitMessage` lose their serialized form. A format table's plan has no cross-runtime encoding, so `Split::Serialize()` and `CommitMessage::Serialize()` refuse them and a plan is read within the process that made it. - `LazyConcatBatchReader` keeps answering with the failure it stopped at, as `BatchReader` requires, rather than moving on to the next file. - Schema validation runs one path for a `TableSchema` and a `DataSchema`, and `Table::Create()` reuses `CatalogUtils::CheckManagedTableType()`. - `FormatFileListing` and `FormatPathValidation` are utility classes rather than free functions, `FormatFileNaming` takes the data file prefix rather than defaulting it, and the constants all three carry follow the `kName` convention. `docs/source/user_guide/format_table.rst` records what is still missing against Java, including `dynamic-partition-overwrite` and partition filters beyond equality.
1 parent fd0c899 commit fc0be5d

64 files changed

Lines changed: 3247 additions & 2503 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/source/api/format_table.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Read
3535
:members:
3636
:undoc-members:
3737

38-
.. doxygenclass:: paimon::FormatDataSplit
38+
.. doxygenstruct:: paimon::FormatDataSplit
3939
:members:
4040
:undoc-members:
4141

@@ -50,7 +50,7 @@ Write
5050
:members:
5151
:undoc-members:
5252

53-
.. doxygenclass:: paimon::FormatCommitMessage
53+
.. doxygenstruct:: paimon::FormatCommitMessage
5454
:members:
5555
:undoc-members:
5656

docs/source/user_guide/format_table.rst

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,86 @@ A partitioned format table uses the standard Hive directory layout, and its part
3333
discovered from that layout rather than from metadata. By default a partition directory is named
3434
``key=value``; setting ``format-table.partition-path-only-value`` names it by the value alone.
3535

36-
Because a directory of plain files records no row identity, a format table only accepts inserts,
37-
and reads return the table's own columns with no ``_VALUE_KIND`` field.
36+
Because a directory of plain files records no row identity, a format table only accepts inserts.
37+
Reads still carry the leading ``_VALUE_KIND`` field every ``BatchReader`` promises, so an engine
38+
that reads a batch by field index sees the same layout it does for a managed table; every row of a
39+
format table is an insert.
3840

3941
Reading and writing
4042
-------------------
4143
A format table is not served through :cpp:func:`Catalog::GetTable`, which describes a managed
42-
table; use :cpp:func:`Catalog::GetFormatTable` instead. It is then read and written through
43-
``FormatTableScan``, ``FormatTableRead``, ``FormatTableWrite`` and ``FormatTableCommit`` rather
44-
than through the managed table path.
44+
table; use :cpp:func:`Catalog::GetFormatTable` instead.
4545

46-
A write is two-phase, since a directory has no metadata to switch atomically: written files are
47-
staged under hidden names that a scan skips, and only the commit renames them into place.
46+
Reading and writing take either of two routes. The generic entry points read the table's schema -
47+
from under the table path, or from the one the context carries - and dispatch to a format table
48+
when its ``type`` says so, so a caller holding a table path uses the same interface it uses for
49+
every other table - ``TableScan::Create``, ``TableRead::Create``, ``FileStoreWrite::Create``
50+
and ``FileStoreCommit::Create``, each from its usual context builder. That is what Java Paimon does
51+
through ``FormatTable.newReadBuilder()`` and ``newBatchWriteBuilder()``. The format-specific classes
52+
``FormatTableScan``, ``FormatTableRead``, ``FormatTableWrite`` and ``FormatTableCommit`` remain
53+
public and take a ``FormatTable`` directly, for a caller that already has one and wants the narrower
54+
interface; Java keeps its own equivalents public for the same reason.
55+
56+
Not all of the generic interfaces fit. ``FileStoreCommit`` is mostly about snapshots and manifests -
57+
expiring them, rolling back to one, filtering by a commit identifier recorded in one - and a format
58+
table keeps none of that state, so those calls are refused rather than quietly doing nothing.
59+
``FileStoreWrite::Compact()`` is refused for the same reason, and both write and commit take batch
60+
writes only, since there is no snapshot to record a commit identifier or a watermark in.
61+
``TableScan`` takes a partition filter and a limit; a predicate or a bucket filter is refused. Java
62+
refuses a predicate from ``FormatTableScan.withFilter`` too, but ``FormatReadBuilder.newScan()``
63+
splits one first and hands the partition half to the scan, so there a predicate over partition
64+
columns still prunes directories. See the limits below.
65+
66+
Options given at the call win over the ones the schema stored, as they do for every other table -
67+
except ``type``, which is structural and is read from the schema alone, so one read or write
68+
cannot decide what kind of table this is. The merged result is validated, not the schema's own
69+
options, so an option a format table refuses - ``metastore.partitioned-table``, a file format
70+
nothing here can read - is refused wherever it comes from rather than dropped in silence.
71+
72+
A setting the format path cannot act on is refused by name rather than quietly dropped:
73+
74+
* ``ReadContextBuilder::SetReadSchema()``. A projected read schema can rename a column, prune a
75+
nested one and give it metadata of its own; a format table's projection is a list of top-level
76+
names, so name the columns instead.
77+
* ``WithStreamingMode()`` on a scan or a write, a global index result on a scan, and a real-time
78+
context on a scan, a read or a write: a format table has no snapshots, no real-time store and no
79+
index.
80+
* ``WriteContextBuilder::WithWriteSchema()``, which names a subset of the columns to write.
81+
* ``WriteContextBuilder::WithWriteId()``, which prefixes a postpone-bucket writer's files so one
82+
compaction reader can put them back in order; a format table has no buckets.
83+
* ``CommitContextBuilder::IgnoreEmptyCommit(false)``, ``UseRESTCatalogCommit(true)`` and
84+
``AppendCommitCheckConflict(true)``. Keeping an empty commit means writing a snapshot that adds
85+
no files, a rest-catalog commit sends that snapshot to a catalog, and the conflict check reads
86+
the manifests of concurrent commits - none of which exist here. Each is refused only when set
87+
away from its default, so an ordinary commit is unaffected.
88+
* A scan predicate or bucket filter, as above, and more than one partition filter: a scan descends
89+
one directory layout, so it takes the values of a single partition rather than a set of them.
90+
91+
What a data file is opened with is not one of the refusals. ``EnablePrefetch()``, the read-ahead
92+
cache and its ``CacheConfig``, and the ``Cache`` a read carries all apply, because a format table
93+
opens its files through the same component the managed table path opens its own with. What differs
94+
between the two paths is which files there are and how a row is put back together, not how a file
95+
is read.
96+
97+
Some settings are not refused because they describe machinery the format path never reaches, and
98+
refusing them would refuse the defaults: ``EnableMultiThreadRowToBatch()`` on a read, a write's
99+
temporary directory and spill configuration, and ``WithIgnoreNumBucketCheck()`` and
100+
``WithIgnorePreviousFiles()`` on a write. They have no effect here: a format read hands out the
101+
batches parquet or orc already produced rather than assembling them from rows, a format write
102+
buffers in memory and never spills, and a table with no buckets has no bucket count to check and
103+
no previous files to read back.
104+
105+
A write is two-phase, since a directory has no metadata to switch atomically: a file is written
106+
into a ``_temporary`` directory beside where it will end up, under a hidden name of its own, and
107+
only the commit renames it into place. That is the layout Java Paimon's
108+
``RenamingTwoPhaseOutputStream`` stages under. The directory and the name are both hidden, the
109+
convention a Hive-style directory uses for output that is not committed table data, and what a scan
110+
of this table skips. The ``_temporary`` directory is shared with every other writer
111+
of the same table and is left behind after a commit.
112+
113+
A plan is in-memory only. ``FormatDataSplit`` has no serialized form - ``Split::Serialize()``
114+
refuses it - and neither has ``FormatCommitMessage``: a format table's plan has no cross-runtime
115+
encoding, so plan, read and commit within one process.
48116

49117
A ``FormatTableWrite`` and a ``FormatTableCommit`` are each driven by one thread, but separate
50118
ones may fill and add to a table at once: each write stages its files under a uuid of its own, and
@@ -65,15 +133,20 @@ closer to the target.
65133
Aborting a write
66134
----------------
67135
``FormatTableWrite::Abort()`` removes the files the write staged. It is the one call still allowed
68-
after ``PrepareCommit()``, so a commit that is prepared and then abandoned can still be cleaned up.
136+
after ``PrepareCommit()``, and that is what it is for: a write dropped *before* preparing clears
137+
its staged files from its own destructor, so only a commit that is prepared and then abandoned
138+
needs it.
69139

70140
Path containment is checked on the path text, which stops a ``..`` from leaving the table but not
71141
a symbolic link pointing out of it - the same as Java's own local file system behaviour.
72142

73143
``FormatTableCommit::Abort()`` does the same for the messages a commit was given. **Neither undoes
74144
a commit that succeeded**: once a file has been renamed into place it is no longer staged, and
75-
nothing here will take it back. Both are best effort and never fail, so a warning in the log is the
76-
only signal that a file could not be removed.
145+
nothing here will take it back. Java's committer removes the published path as well as the staged
146+
one, which matters there because a commit publishes file by file with nothing watching; here a
147+
commit that fails part way takes its own published files back before it returns, so an abort is
148+
left with the staged files alone. Both are best effort and never fail, so a warning in the log is
149+
the only signal that a file could not be removed.
77150

78151
Give ``FormatTableCommit`` only the messages this job's own writers produced. A message names a
79152
staged file by path, and a commit can tell that the path belongs to this table, sits in the
@@ -102,14 +175,32 @@ instead. The one exception is the value standing for a null partition, ``partiti
102175
which the scan reads at a partition level by design. Under the ``key=value`` layout the question
103176
does not arise, since the key in front of the value keeps the directory name visible.
104177

105-
Two smaller differences come from this library's own conventions:
178+
A few smaller differences come from this library's own conventions:
106179

107180
* a write takes one partition per batch: the batch declares it through
108181
``RecordBatch::SetPartition()``, every row is checked against that declaration, and a batch
109182
mixing partitions is refused. Java routes row by row, so one write call there may land in any
110183
number of partitions;
184+
* a write takes its partition from ``RecordBatch::SetPartition()`` rather than from the rows, so
185+
the values arrive as text. They are still read into their column types and rendered back out
186+
before anything is named after them - the round trip Java's writer makes when it renders a
187+
partition out of the row it is writing, through the partition computer its
188+
``FileStorePathFactory`` holds. The table therefore decides the directory name and the commit
189+
message, not the spelling the caller used: with ``partition.legacy-name`` on, its default, a
190+
``DATE`` partition is written as its day count whether the caller wrote ``19723`` or
191+
``2024-01-01``, and as ``YYYY-MM-DD`` when the option is off. A value that cannot be read into
192+
its column type is refused. ``FormatTableCommit``'s static partition is *not* put through that
193+
round trip and is used as given, which is what Java's ``FormatTableCommit.buildPartitionPath``
194+
does with it too;
195+
* a commit message carries the partition its file belongs to, and a commit checks that it agrees
196+
with the directory the file sits in. Java's message carries none and derives the partition from
197+
the committer's target path, so the two cannot disagree there. A message here is a public struct
198+
a caller may have built itself, so the value is checked rather than trusted;
111199
* a projection that names the same column twice is rejected when the read is built. Java reads
112-
such a column once per entry.
200+
such a column once per entry;
201+
* a row limit is the caller's: ``FormatTableScan`` takes one so that a plan can drop splits it
202+
cannot need, but ``FormatTableRead`` does not bound the reader it hands out, and the caller stops
203+
calling ``NextBatch()`` once it has enough. Java wraps its reader in a ``LimitRecordReader``.
113204

114205
Current limits
115206
--------------
@@ -129,10 +220,13 @@ Compared with Java Paimon, this implementation does not yet support:
129220
Partition discovery here also lists one directory level at a time and applies the filter to each
130221
name, while Java turns a leading run of equality constraints into a path and starts listing
131222
below it; a table with many partitions therefore costs more listings here than in Java;
132-
* ``scan.ignore-corrupt-file`` and ``scan.ignore-lost-file``, which are not implemented: a
223+
* ``scan.ignore-corrupt-files`` and ``scan.ignore-lost-files``, which are not implemented: a
133224
corrupt or missing data file fails the read rather than being skipped;
134-
* ``partition.legacy-name``, which changes how a partition value is rendered into its directory
135-
name;
225+
* ``dynamic-partition-overwrite``. An overwrite here always replaces the partitions the commit
226+
actually writes to, which is what Java does under that option's default of ``true``. Java also
227+
has the other mode: with it off, and always for an unpartitioned table, an overwrite empties
228+
everything the table holds - so a statement whose query returns nothing still clears the table.
229+
A commit with no messages therefore clears nothing here where Java would;
136230
* ``format-table.commit-hive-sync-url``, which registers committed partitions with a Hive
137231
metastore;
138232
* column default values. Java replaces a null in a column whose schema field declares a default
@@ -141,8 +235,12 @@ Compared with Java Paimon, this implementation does not yet support:
141235
out of what it writes, leaving files that carry nothing but a row count; here such a schema is
142236
refused when the table is created and when it is opened;
143237
* ``TIMESTAMP``, ``DECIMAL``, ``FLOAT`` and ``DOUBLE`` partition columns, which Java allows. This
144-
is a restriction of the whole library rather than of format tables, and a table Java created
145-
with such a partition column fails to open here rather than at the first read.
238+
is a restriction of the whole library rather than of format tables. The types that do work are
239+
``BOOLEAN``, ``TINYINT``, ``SMALLINT``, ``INT``, ``BIGINT``, ``STRING`` and ``DATE`` - the set
240+
the managed table path reads and writes partitions in. Any other, ``BINARY`` among them, is
241+
refused when the table is created and when it is opened, rather than at the first read or write:
242+
validation asks by building the partition computer that does the round trip, so there is one
243+
answer rather than a list of types that could fall out of step with it.
146244

147245
``data-file.path-directory`` has no effect here, and none in Java either: Java's format table
148246
writer builds its paths from the table root rather than from that directory.

include/paimon/reader/batch_reader.h

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,9 @@ class PAIMON_EXPORT BatchReader {
4747
/// avoid potential issues during conversion through the Arrow C Data Interface.
4848
///
4949
/// @return A result containing a `::ReadBatch`, which consists of a unique pointer to
50-
/// `ArrowArray` and a unique pointer to `ArrowSchema`. A reader over a table that records row
51-
/// kinds puts a `_VALUE_KIND` field first to carry them; one over a table where every row is
52-
/// an insert, such as a format table, has no such field. Deleted or index-filtered rows are
53-
/// removed.
50+
/// `ArrowArray` and a unique pointer to `ArrowSchema`. Returned array contains a `_VALUE_KIND`
51+
/// field (the first field) to indicate the row kind of each row. Deleted or index-filtered rows
52+
/// are removed.
5453
virtual Result<ReadBatch> NextBatch() = 0;
5554

5655
/// Retrieves the next batch of data.
@@ -62,12 +61,12 @@ class PAIMON_EXPORT BatchReader {
6261
/// avoid potential issues during conversion through the Arrow C Data Interface.
6362
///
6463
/// @return A result containing a `::ReadBatch` and a valid bitmap. `::ReadBatch` consists of a
65-
/// unique pointer to `ArrowArray` and a unique pointer to `ArrowSchema`. The `_VALUE_KIND`
66-
/// field is present or absent exactly as in `NextBatch()`. Deleted or index-filtered records
67-
/// maybe maintained in `::ReadBatch`, while bitmap indicates valid row id. If deletion vector
68-
/// or index are enabled, this function is more efficient than `NextBatch()`. The default
69-
/// implementation calls `NextBatch()` and adds all rows to valid bitmap. Noted that the
70-
/// returned bitmap has at least one valid row id.
64+
/// unique pointer to `ArrowArray` and a unique pointer to `ArrowSchema`. Returned array
65+
/// contains a _VALUE_KIND field (the first field) to indicate the row kind of each row. Deleted
66+
/// or index-filtered records maybe maintained in `::ReadBatch`, while bitmap indicates valid
67+
/// row id. If deletion vector or index are enabled, this function is more efficient than
68+
/// `NextBatch()`. The default implementation calls `NextBatch()` and adds all rows to valid
69+
/// bitmap. Noted that the returned bitmap has at least one valid row id.
7170
virtual Result<ReadBatchWithBitmap> NextBatchWithBitmap();
7271

7372
/// Retrieves the reader's metrics.

0 commit comments

Comments
 (0)