Ramin Gharib created FLINK-41351:
------------------------------------
Summary: Release Testing: Verify FLIP-578: In-place Table to
Materialized Table conversion
Key: FLINK-41351
URL: https://issues.apache.org/jira/browse/FLINK-41351
Project: Flink
Issue Type: Sub-task
Components: Table SQL / Gateway
Reporter: Ramin Gharib
Fix For: 2.4.0
[FLIP-578|https://cwiki.apache.org/confluence/display/FLINK/FLIP-578%3A+In-place+Table+to+Materialized+Table+conversion]
lets {{CREATE OR ALTER MATERIALIZED TABLE}} turn an existing regular table
into a materialized table in place. The catalog entry and its storage stay.
Everything else comes from the statement, and nothing is copied from the old
table. The feature is off unless
{{table.materialized-table.conversion-from-table.enabled: true}} is set in
{{config.yaml}}. If the refresh job cannot start, the table stays a
materialized table in {{SUSPENDED}} status.
Docs: [Converting a Table to a Materialized
Table|https://nightlies.apache.org/flink/flink-docs-release-2.4/docs/sql/materialized-table/statements/#converting-a-table-to-a-materialized-table].
The schema rules are covered by unit tests. This ticket is about the
end-to-end path on a real cluster.
h3. 0. Setup
Build {{release-2.4}}, or use the 2.4.0 RC and take
{{flink-table-filesystem-test-utils-2.4.0.jar}} from the staging repository.
{code:bash}
./mvnw clean install -DskipTests -Dfast -Pskip-webui-build -T1C
cd build-target
cp
../flink-test-utils-parent/flink-table-filesystem-test-utils/target/flink-table-filesystem-test-utils-2.4-SNAPSHOT.jar
lib/
mkdir -p /tmp/flip578/catalog/mydb /tmp/flip578/catalog-store
/tmp/flip578/checkpoints /tmp/flip578/savepoints
{code}
In {{conf/config.yaml}}, change {{numberOfTaskSlots: 1}} to {{4}}, and append
the following. Leave the conversion flag out for now.
{code:yaml}
execution.checkpointing.dir: file:///tmp/flip578/checkpoints
execution.checkpointing.savepoint-dir: file:///tmp/flip578/savepoints
table.catalog-store.kind: file
table.catalog-store.file.path: /tmp/flip578/catalog-store
workflow-scheduler.type: embedded
sql-gateway.endpoint.rest.address: 127.0.0.1
sql-gateway.endpoint.rest.port: 8083
{code}
Start the cluster with {{./bin/start-cluster.sh}} and {{./bin/sql-gateway.sh
start}}. Then connect with {{./bin/sql-client.sh gateway --endpoint
http://127.0.0.1:8083}}. Do not set {{table.dml-sync}}, because it makes the
conversion hang.
{code:sql}
SET 'sql-client.execution.result-mode' = 'tableau';
CREATE CATALOG mt_cat WITH ('type' = 'test-filesystem', 'path' =
'/tmp/flip578/catalog', 'default-database' = 'mydb');
USE CATALOG mt_cat;
CREATE TABLE orders (user_id BIGINT NOT NULL, amount BIGINT, order_time
TIMESTAMP(3), ds STRING)
WITH ('format' = 'json', 'source.monitor-interval' = '10s');
INSERT INTO orders VALUES
(1, 10, TIMESTAMP '2026-11-01 10:00:00', '2026-11-01'),
(2, 20, TIMESTAMP '2026-11-01 11:00:00', '2026-11-01'),
(1, 30, TIMESTAMP '2026-11-02 09:00:00', '2026-11-02');
-- Regular tables to convert, each with a row written before the conversion
CREATE TABLE large_orders (
user_id BIGINT NOT NULL, amount BIGINT, order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH ('format' = 'json');
INSERT INTO large_orders VALUES (100, 500, TIMESTAMP '2026-10-31 09:00:00');
CREATE TABLE daily_spending (ds STRING, user_id BIGINT, total_amount BIGINT)
PARTITIONED BY (ds) WITH ('format' = 'json');
INSERT INTO daily_spending VALUES ('2026-10-31', 100, 1000);
{code}
h3. 1. The flag
{code:sql}
CREATE OR ALTER MATERIALIZED TABLE large_orders
WITH ('format' = 'json', 'sink.rolling-policy.rollover-interval' = '10s',
'sink.rolling-policy.check-interval' = '10s')
FRESHNESS = INTERVAL '30' SECOND
AS SELECT user_id, amount, order_time FROM orders WHERE amount >= 20;
-- [ERROR] ... ValidationException: Regular table does not support create or
alter operation.
SET 'table.materialized-table.conversion-from-table.enabled' = 'true';
-- The same statement fails the same way. The flag is read from the cluster
configuration only.
-- SHOW CREATE TABLE large_orders is still CREATE TABLE.
{code}
Add {{table.materialized-table.conversion-from-table.enabled: true}} to
{{config.yaml}}. Restart the gateway, reconnect, and run {{USE CATALOG
mt_cat;}} again.
h3. 2. Continuous mode
Run the {{large_orders}} statement from section 1 again.
{code:sql}
SHOW CREATE MATERIALIZED TABLE large_orders;
-- REFRESH_MODE = CONTINUOUS, and 'path' =
'/tmp/flip578/catalog/mydb/large_orders/data' as before
DESCRIBE large_orders;
-- No watermark. It is not carried over.
SHOW JOBS;
-- Materialized_table_`mt_cat`.`mydb`.`large_orders`_continuous_refresh_job
RUNNING
-- About a minute later, once files are committed on a checkpoint:
SET 'execution.runtime-mode' = 'batch';
SELECT * FROM large_orders ORDER BY order_time;
-- 100 | 500 | 2026-10-31 09:00:00.000 <- written before the conversion
-- 2 | 20 | 2026-11-01 11:00:00.000
-- 1 | 30 | 2026-11-02 09:00:00.000
{code}
h3. 3. Full mode
{code:sql}
SET 'execution.runtime-mode' = 'streaming';
INSERT INTO orders VALUES (3, 7, LOCALTIMESTAMP, CAST(CURRENT_DATE AS STRING));
CREATE OR ALTER MATERIALIZED TABLE daily_spending
PARTITIONED BY (ds)
WITH ('format' = 'json', 'partition.fields.ds.date-formatter' = 'yyyy-MM-dd')
FRESHNESS = INTERVAL '1' MINUTE
REFRESH_MODE = FULL
AS SELECT ds, user_id, SUM(amount) AS total_amount FROM orders GROUP BY ds,
user_id;
ALTER MATERIALIZED TABLE daily_spending REFRESH PARTITION (ds = '2026-11-01');
-- After the next full minute:
SET 'execution.runtime-mode' = 'batch';
SELECT * FROM daily_spending ORDER BY ds, user_id;
-- 2026-10-31 | 100 | 1000 <- written before the conversion, not touched
-- 2026-11-01 | 1 | 10 <- REFRESH PARTITION
-- 2026-11-01 | 2 | 20
-- <today> | 3 | 7 <- the periodic refresh
{code}
h3. 4. Suspend, resume and gateway restart
{code:sql}
SET 'execution.runtime-mode' = 'streaming';
ALTER MATERIALIZED TABLE large_orders SUSPEND; -- the refresh job goes to
FINISHED with a savepoint
ALTER MATERIALIZED TABLE large_orders RESUME; -- a new refresh job restores
from that savepoint
{code}
Restart the gateway and reconnect. {{SUSPEND}} and {{RESUME}} on
{{large_orders}} still work, because the refresh handler is in the catalog.
h3. 5. The refresh job fails to start
{code:sql}
CREATE TABLE spending_json (user_id BIGINT NOT NULL, total_amount BIGINT) WITH
('format' = 'json');
-- 'json' cannot store the updates that a GROUP BY produces
CREATE OR ALTER MATERIALIZED TABLE spending_json
WITH ('format' = 'json')
FRESHNESS = INTERVAL '30' SECOND
AS SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id;
-- [ERROR] ... TableException: Table sink 'mt_cat.mydb.spending_json' doesn't
support consuming update changes ...
-- With SET 'sql-client.verbose' = 'true', the trace also says the table "was
converted and left in SUSPENDED status".
SHOW CREATE MATERIALIZED TABLE spending_json; -- now a materialized table
-- Expected per the docs: fix the cause, then RESUME
ALTER MATERIALIZED TABLE spending_json SET ('format' = 'debezium-json');
ALTER MATERIALIZED TABLE spending_json RESUME;
-- Actual: both fail with NullPointerException: Cannot read the array length
because "buf" is null. This is FLINK-41350.
-- Only DROP MATERIALIZED TABLE spending_json works.
{code}
Clean up with {{DROP MATERIALIZED TABLE}} on each converted table. Their
refresh jobs are cancelled and their directories deleted.
h3. Exploratory
* Convert a table while another job is still writing into it.
* Convert a partitioned table with different partition keys, or none.
h3. Reporting
Comment with the commit or RC and give pass or fail per section. File new bugs
as linked issues with Fix Version 2.4.0.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)