Skip to content

Commit 3fd3c15

Browse files
committed
feat(format-table): support reading and writing format tables
A format table is a directory of data files of one format with no snapshots and no manifests, laid out like a standard Hive table. Only `parquet` and `orc` are supported. `Catalog::GetFormatTable()` loads one, and the new `FormatTableCatalog` interface lets a catalog serve that call itself; `FormatTableScan`, `FormatTableRead`, `FormatTableWrite` and `FormatTableCommit` then take the place of the managed table path. The scan discovers partitions by descending the directory layout - `key=value` by default, the bare value under `format-table.partition-path-only-value` - and packs a partition's files into splits of about `source.split.target-size`, each holding whole files. The read takes a projection, a predicate and a limit, and rebuilds the partition columns from the directory names, since the data files themselves do not carry them. The write is two-phase: it stages its output under hidden names that a scan skips, and the commit renames them into place. An overwriting commit first clears the partitions it writes to, or the static partition it was given. A split and a commit message are both public and both decodable from untrusted bytes, so each is checked for path containment, visibility and partition binding. - `Catalog::GetTable()` and `Table::Create()` refuse a format table, and every other table type this library does not implement, instead of opening it as a managed table and looking for snapshots it never had. - Every catalog validates a new table's schema through one entry point, so none accepts a table another could not open; `RestCatalog::CreateTable()` runs it now too. - `PartitionPathUtils::GeneratePartitionPath()` takes the value-only layout as a parameter and refuses a partition value that cannot name a directory. The managed table path goes through it too. - `Split::Serialize()` and `Split::Deserialize()` carry a third split type, and `BatchReader` no longer promises a `_VALUE_KIND` field for every table. `docs/source/user_guide/format_table.rst` describes the layout, the options and the limits that remain.
1 parent 223e566 commit 3fd3c15

68 files changed

Lines changed: 8717 additions & 62 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.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ API Reference
2727
api/commit
2828
api/scan
2929
api/read
30+
api/format_table
3031
api/predicate
3132
api/file_format
3233
api/file_system

docs/source/api/catalog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Interface
2727
.. doxygenclass:: paimon::Catalog
2828
:members:
2929

30+
.. doxygenclass:: paimon::FormatTableCatalog
31+
:members:
32+
:undoc-members:
33+
3034
.. doxygenclass:: paimon::Identifier
3135
:members:
3236
:undoc-members:

docs/source/api/format_table.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
.. Licensed to the Apache Software Foundation (ASF) under one
2+
.. or more contributor license agreements. See the NOTICE file
3+
.. distributed with this work for additional information
4+
.. regarding copyright ownership. The ASF licenses this file
5+
.. to you under the Apache License, Version 2.0 (the
6+
.. "License"); you may not use this file except in compliance
7+
.. with the License. You may obtain a copy of the License at
8+
9+
.. http://www.apache.org/licenses/LICENSE-2.0
10+
11+
.. Unless required by applicable law or agreed to in writing,
12+
.. software distributed under the License is distributed on an
13+
.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
.. KIND, either express or implied. See the License for the
15+
.. specific language governing permissions and limitations
16+
.. under the License.
17+
18+
============
19+
Format Table
20+
============
21+
22+
.. _cpp-api-format-table:
23+
24+
Table
25+
=====
26+
27+
.. doxygenclass:: paimon::FormatTable
28+
:members:
29+
:undoc-members:
30+
31+
Read
32+
====
33+
34+
.. doxygenclass:: paimon::FormatTableScan
35+
:members:
36+
:undoc-members:
37+
38+
.. doxygenclass:: paimon::FormatDataSplit
39+
:members:
40+
:undoc-members:
41+
42+
.. doxygenclass:: paimon::FormatTableRead
43+
:members:
44+
:undoc-members:
45+
46+
Write
47+
=====
48+
49+
.. doxygenclass:: paimon::FormatTableWrite
50+
:members:
51+
:undoc-members:
52+
53+
.. doxygenclass:: paimon::FormatCommitMessage
54+
:members:
55+
:undoc-members:
56+
57+
.. doxygenclass:: paimon::FormatTableCommit
58+
:members:
59+
:undoc-members:

docs/source/user_guide.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ User Guide
3333
user_guide/data_types
3434
user_guide/primary_key_table
3535
user_guide/append_only_table
36+
user_guide/format_table
3637
user_guide/system_tables
3738
user_guide/write
3839
user_guide/commit
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
.. Licensed to the Apache Software Foundation (ASF) under one
2+
.. or more contributor license agreements. See the NOTICE file
3+
.. distributed with this work for additional information
4+
.. regarding copyright ownership. The ASF licenses this file
5+
.. to you under the Apache License, Version 2.0 (the
6+
.. "License"); you may not use this file except in compliance
7+
.. with the License. You may obtain a copy of the License at
8+
9+
.. http://www.apache.org/licenses/LICENSE-2.0
10+
11+
.. Unless required by applicable law or agreed to in writing,
12+
.. software distributed under the License is distributed on an
13+
.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
.. KIND, either express or implied. See the License for the
15+
.. specific language governing permissions and limitations
16+
.. under the License.
17+
18+
.. Ported from the Paimon documentation:
19+
.. https://github.com/apache/paimon/blob/master/docs/docs/concepts/rest/tables.mdx
20+
21+
.. default-domain:: cpp
22+
.. highlight:: cpp
23+
24+
Format Table
25+
============
26+
A format table is a directory that holds multiple files of the same format. It carries no
27+
snapshots and no manifests: the files in the directory are the table, so reading it lists
28+
directories and writing to it adds files. A table is a format table when its ``type`` option is
29+
``format-table``; ``file.format`` then names the format of every file in it, which here is
30+
``parquet`` or ``orc``.
31+
32+
A partitioned format table uses the standard Hive directory layout, and its partitions are
33+
discovered from that layout rather than from metadata. By default a partition directory is named
34+
``key=value``; setting ``format-table.partition-path-only-value`` names it by the value alone.
35+
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.
38+
39+
Reading and writing
40+
-------------------
41+
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.
45+
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.
48+
49+
A ``FormatTableWrite`` and a ``FormatTableCommit`` are each driven by one thread, but separate
50+
ones may fill and add to a table at once: each write stages its files under a uuid of its own, and
51+
each commit publishes only the files its own messages name. Two *overwriting* commits over the
52+
same directory race, since an overwrite clears what is committed there before publishing anything.
53+
A ``FormatTableScan`` may be shared, since planning leaves it as it was.
54+
55+
``TableRead::CreateCountReader()`` is not implemented for a format table, so counting its rows
56+
means reading them. That is a gap here rather than something the layout forces: ``parquet`` and
57+
``orc`` both record a row count in their own footer.
58+
59+
A writer starts a new file once the one it is filling reaches ``target-file-row-num`` rows or
60+
``target-file-size`` bytes. Both are checked between batches rather than between rows, because a
61+
batch is the unit this API writes in, so a file may pass either target by up to one batch. Java
62+
checks the row count on every row and the size every thousand rows, and its files therefore sit
63+
closer to the target.
64+
65+
Aborting a write
66+
----------------
67+
``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.
69+
70+
Path containment is checked on the path text, which stops a ``..`` from leaving the table but not
71+
a symbolic link pointing out of it - the same as Java's own local file system behaviour.
72+
73+
``FormatTableCommit::Abort()`` does the same for the messages a commit was given. **Neither undoes
74+
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.
77+
78+
Give ``FormatTableCommit`` only the messages this job's own writers produced. A message names a
79+
staged file by path, and a commit can tell that the path belongs to this table, sits in the
80+
partition the message declares, and is staged rather than already published - not whose staged file
81+
it is. A well-formed message from somewhere else is published, or discarded by ``Abort()``, like
82+
any other.
83+
84+
Relationship to Java Paimon
85+
---------------------------
86+
Java serves format tables from a Hive or REST catalog, which holds the schema. This implementation
87+
also serves them from a file system catalog, which keeps the schema under the table directory - an
88+
extension Java does not have. Only for such a table are the ``schema`` and ``branch`` directories
89+
below the location treated as metadata rather than as data.
90+
91+
A file system catalog keeps a table's schema in ``schema`` and its branches in ``branch`` below
92+
the table location, so under ``format-table.partition-path-only-value`` the first partition value
93+
may not be ``schema`` or ``branch``: the directory a write would use is the one holding the
94+
table's own metadata. Such a write is refused, as is an overwrite naming that partition - which
95+
would otherwise delete the schema. A table served from a REST or Hive catalog keeps its schema
96+
elsewhere, so there these are ordinary partition values and are read and written like any other.
97+
98+
Under that same layout a partition value may not start with ``_`` or ``.`` either, whichever
99+
catalog serves the table: the value is the whole directory name, and a scan skips every hidden
100+
name. Java writes such a directory and then cannot read it back; here the write is refused
101+
instead. The one exception is the value standing for a null partition, ``partition.default-name``,
102+
which the scan reads at a partition level by design. Under the ``key=value`` layout the question
103+
does not arise, since the key in front of the value keeps the directory name visible.
104+
105+
Two smaller differences come from this library's own conventions:
106+
107+
* a write takes one partition per batch: the batch declares it through
108+
``RecordBatch::SetPartition()``, every row is checked against that declaration, and a batch
109+
mixing partitions is refused. Java routes row by row, so one write call there may land in any
110+
number of partitions;
111+
* a projection that names the same column twice is rejected when the read is built. Java reads
112+
such a column once per entry.
113+
114+
Current limits
115+
--------------
116+
Compared with Java Paimon, this implementation does not yet support:
117+
118+
* the ``csv``, ``json``, ``text`` and ``mosaic`` file formats, leaving ``parquet`` and ``orc``.
119+
All four are line-delimited text in Java, which shares one line-reading layer between them;
120+
this library has no text file format at all, so the first of them to be added has to bring that
121+
layer with it;
122+
* cutting one large data file into byte ranges so that several readers share it. Java does this
123+
only for its line-delimited text formats, which are the ones missing here; ``parquet`` and
124+
``orc`` each record where their own row groups and stripes begin, and a reader handed a byte
125+
range of one would have to find that out for itself;
126+
* ``metastore.partitioned-table``, which moves partition visibility into the catalog, and the
127+
Hive partition sync that goes with it;
128+
* partition filters beyond equality on partition values, where Java accepts a full predicate.
129+
Partition discovery here also lists one directory level at a time and applies the filter to each
130+
name, while Java turns a leading run of equality constraints into a path and starts listing
131+
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
133+
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;
136+
* ``format-table.commit-hive-sync-url``, which registers committed partitions with a Hive
137+
metastore;
138+
* column default values. Java replaces a null in a column whose schema field declares a default
139+
with that default as it writes; here the null is written as it came;
140+
* a table every one of whose columns is a partition column. Java projects the partition columns
141+
out of what it writes, leaving files that carry nothing but a row count; here such a schema is
142+
refused when the table is created and when it is opened;
143+
* ``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.
146+
147+
``data-file.path-directory`` has no effect here, and none in Java either: Java's format table
148+
writer builds its paths from the table root rather than from that directory.
149+
``format-table.implementation`` is honoured by the engines rather than by the table - in Java
150+
Spark it selects between Paimon's own implementation and the engine's ``FileTable`` - so it has
151+
no meaning inside this library.
152+
153+
Validation
154+
----------
155+
A table Java can serve and this library cannot is refused at creation rather than accepted and
156+
then found unopenable, whichever catalog it is created through. It can still reach a catalog
157+
another way - written by Java, or by an older client - so the same checks run again when the
158+
table is opened.
159+
160+
Whitespace in a partition value is judged by ASCII rules here, while Java uses
161+
``Character.isWhitespace``; a value made only of non-ASCII whitespace therefore lands in a
162+
partition of its own rather than in the default one.

include/paimon/api.h

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,30 @@
2020

2121
#pragma once
2222

23-
#include "paimon/commit_context.h" // IWYU pragma: export
24-
#include "paimon/defs.h" // IWYU pragma: export
25-
#include "paimon/factories/factory.h" // IWYU pragma: export
26-
#include "paimon/file_store_commit.h" // IWYU pragma: export
27-
#include "paimon/file_store_write.h" // IWYU pragma: export
28-
#include "paimon/fs/file_system_factory.h" // IWYU pragma: export
29-
#include "paimon/memory/memory_pool.h" // IWYU pragma: export
30-
#include "paimon/predicate/predicate.h" // IWYU pragma: export
31-
#include "paimon/read_context.h" // IWYU pragma: export
32-
#include "paimon/reader/batch_reader.h" // IWYU pragma: export
33-
#include "paimon/record_batch.h" // IWYU pragma: export
34-
#include "paimon/result.h" // IWYU pragma: export
35-
#include "paimon/scan_context.h" // IWYU pragma: export
36-
#include "paimon/status.h" // IWYU pragma: export
37-
#include "paimon/table/source/table_read.h" // IWYU pragma: export
38-
#include "paimon/table/source/table_scan.h" // IWYU pragma: export
39-
#include "paimon/write_context.h" // IWYU pragma: export
23+
#include "paimon/commit_context.h" // IWYU pragma: export
24+
#include "paimon/defs.h" // IWYU pragma: export
25+
#include "paimon/factories/factory.h" // IWYU pragma: export
26+
#include "paimon/file_store_commit.h" // IWYU pragma: export
27+
#include "paimon/file_store_write.h" // IWYU pragma: export
28+
#include "paimon/fs/file_system_factory.h" // IWYU pragma: export
29+
#include "paimon/memory/memory_pool.h" // IWYU pragma: export
30+
#include "paimon/predicate/predicate.h" // IWYU pragma: export
31+
#include "paimon/read_context.h" // IWYU pragma: export
32+
#include "paimon/reader/batch_reader.h" // IWYU pragma: export
33+
#include "paimon/record_batch.h" // IWYU pragma: export
34+
#include "paimon/result.h" // IWYU pragma: export
35+
#include "paimon/scan_context.h" // IWYU pragma: export
36+
#include "paimon/status.h" // IWYU pragma: export
37+
#include "paimon/table/format/format_commit_message.h" // IWYU pragma: export
38+
#include "paimon/table/format/format_data_split.h" // IWYU pragma: export
39+
#include "paimon/table/format/format_table.h" // IWYU pragma: export
40+
#include "paimon/table/format/format_table_commit.h" // IWYU pragma: export
41+
#include "paimon/table/format/format_table_read.h" // IWYU pragma: export
42+
#include "paimon/table/format/format_table_scan.h" // IWYU pragma: export
43+
#include "paimon/table/format/format_table_write.h" // IWYU pragma: export
44+
#include "paimon/table/source/table_read.h" // IWYU pragma: export
45+
#include "paimon/table/source/table_scan.h" // IWYU pragma: export
46+
#include "paimon/write_context.h" // IWYU pragma: export
4047

4148
// IWYU pragma: begin_exports
4249
#include "paimon/realtime/realtime_context.h"

include/paimon/catalog/catalog.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ namespace paimon {
4040
using Instant = std::variant<std::string, int64_t>;
4141

4242
class Database;
43+
class FormatTable;
4344
class Table;
4445
class View;
4546
class Schema;
@@ -208,6 +209,23 @@ class PAIMON_EXPORT Catalog {
208209
/// snapshot id ascending, or an error status.
209210
virtual Result<std::vector<SnapshotInfo>> ListSnapshots(
210211
const Identifier& identifier, const std::string& branch = "") const = 0;
212+
213+
/// Gets a format table: a directory of data files laid out like a standard Hive table.
214+
///
215+
/// A format table is read and written through `FormatTableScan`, `FormatTableRead`,
216+
/// `FormatTableWrite` and `FormatTableCommit` rather than through the managed table path, so
217+
/// it is loaded through its own method instead of `GetTable()`.
218+
///
219+
/// Not virtual, since `Catalog` is derived from outside this library: a catalog that can load
220+
/// a format table itself derives from `FormatTableCatalog` as well, and this finds it with a
221+
/// `dynamic_cast`. Otherwise it reads the location and the schema through the methods above,
222+
/// which costs two requests that can disagree, and treats everything below the location as
223+
/// data.
224+
///
225+
/// @param identifier Identifier of the table to get.
226+
/// @return A result containing the format table, or an error status if the table does not
227+
/// exist or its `type` option is not `format-table`.
228+
Result<std::shared_ptr<FormatTable>> GetFormatTable(const Identifier& identifier) const;
211229
};
212230

213231
} // namespace paimon
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
#pragma once
20+
21+
#include <memory>
22+
23+
#include "paimon/result.h"
24+
#include "paimon/visibility.h"
25+
26+
namespace paimon {
27+
28+
class FormatTable;
29+
class Identifier;
30+
31+
/// What a catalog implements when it can load a format table itself.
32+
///
33+
/// It is a separate base rather than a virtual on `Catalog`, which is exported and derived from
34+
/// outside this library: a virtual added there would have no slot in an already-compiled subclass.
35+
/// A catalog inherits this alongside `Catalog`, and `Catalog::GetFormatTable()` finds it with a
36+
/// `dynamic_cast`.
37+
///
38+
/// A catalog that does not implement it still serves format tables: `Catalog::GetFormatTable()`
39+
/// falls back to reading the location and the schema through the methods every catalog has.
40+
class PAIMON_EXPORT FormatTableCatalog {
41+
public:
42+
virtual ~FormatTableCatalog() = default;
43+
44+
/// Loads `identifier` as a format table.
45+
///
46+
/// The catalog decides both halves the fallback has to guess at: whether the location and the
47+
/// schema can be read in one round trip, and whether it put this table's metadata under the
48+
/// table path.
49+
///
50+
/// @param identifier Identifier of the table to load.
51+
/// @return A result containing the format table, or an error status if the table does not
52+
/// exist or its `type` option is not `format-table`.
53+
virtual Result<std::shared_ptr<FormatTable>> LoadFormatTable(
54+
const Identifier& identifier) const = 0;
55+
};
56+
57+
} // namespace paimon

0 commit comments

Comments
 (0)