Skip to content

Commit 6c57a3e

Browse files
hantangwangdtdcmeehan
authored andcommitted
[Iceberg]Support timestamp without timezone in time travel expressions
1 parent 27d38bf commit 6c57a3e

File tree

4 files changed

+117
-11
lines changed

4 files changed

+117
-11
lines changed

presto-docs/src/main/sphinx/connector/iceberg.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,9 +1631,11 @@ In this example, SYSTEM_TIME can be used as an alias for TIMESTAMP.
16311631

16321632
// In following query, timestamp string is matching with second inserted record.
16331633
SELECT * FROM ctas_nation FOR TIMESTAMP AS OF TIMESTAMP '2023-10-17 13:29:46.822 America/Los_Angeles';
1634+
SELECT * FROM ctas_nation FOR TIMESTAMP AS OF TIMESTAMP '2023-10-17 13:29:46.822';
16341635

16351636
// Same example using SYSTEM_TIME as an alias for TIMESTAMP
16361637
SELECT * FROM ctas_nation FOR SYSTEM_TIME AS OF TIMESTAMP '2023-10-17 13:29:46.822 America/Los_Angeles';
1638+
SELECT * FROM ctas_nation FOR SYSTEM_TIME AS OF TIMESTAMP '2023-10-17 13:29:46.822';
16371639

16381640
.. code-block:: text
16391641

@@ -1643,8 +1645,12 @@ In this example, SYSTEM_TIME can be used as an alias for TIMESTAMP.
16431645
20 | canada | 2 | comment
16441646
(2 rows)
16451647

1646-
The option following FOR TIMESTAMP AS OF can accept any expression that returns a timestamp with time zone value.
1647-
For example, `TIMESTAMP '2023-10-17 13:29:46.822 America/Los_Angeles'` is a constant string for the expression.
1648+
.. note::
1649+
1650+
Timestamp without timezone will be parsed and rendered in the session time zone. See `TIMESTAMP <https://prestodb.io/docs/current/language/types.html#timestamp>`_.
1651+
1652+
The option following FOR TIMESTAMP AS OF can accept any expression that returns a timestamp or timestamp with time zone value.
1653+
For example, `TIMESTAMP '2023-10-17 13:29:46.822 America/Los_Angeles'` and `TIMESTAMP '2023-10-17 13:29:46.822'` are both valid timestamps. The first specifies the timestamp within the timezone `America/Los_Angeles`. The second will use the timestamp based on the user's session timezone.
16481654
In the following query, the expression CURRENT_TIMESTAMP returns the current timestamp with time zone value.
16491655

16501656
.. code-block:: sql
@@ -1665,6 +1671,7 @@ In the following query, the expression CURRENT_TIMESTAMP returns the current tim
16651671
// In following query, timestamp string is matching with second inserted record.
16661672
// BEFORE clause returns first record which is less than timestamp of the second record.
16671673
SELECT * FROM ctas_nation FOR TIMESTAMP BEFORE TIMESTAMP '2023-10-17 13:29:46.822 America/Los_Angeles';
1674+
SELECT * FROM ctas_nation FOR TIMESTAMP BEFORE TIMESTAMP '2023-10-17 13:29:46.822';
16681675

16691676
.. code-block:: text
16701677

presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.facebook.presto.common.predicate.TupleDomain;
2020
import com.facebook.presto.common.type.BigintType;
2121
import com.facebook.presto.common.type.SqlTimestampWithTimeZone;
22+
import com.facebook.presto.common.type.TimestampType;
2223
import com.facebook.presto.common.type.TimestampWithTimeZoneType;
2324
import com.facebook.presto.common.type.TypeManager;
2425
import com.facebook.presto.common.type.VarcharType;
@@ -981,6 +982,11 @@ private static long getSnapshotIdForTableVersion(Table table, ConnectorTableVers
981982
long millisUtc = new SqlTimestampWithTimeZone((long) tableVersion.getTableVersion()).getMillisUtc();
982983
return getSnapshotIdTimeOperator(table, millisUtc, tableVersion.getVersionOperator());
983984
}
985+
else if (tableVersion.getVersionExpressionType() instanceof TimestampType) {
986+
long timestampValue = (long) tableVersion.getTableVersion();
987+
long millisUtc = ((TimestampType) tableVersion.getVersionExpressionType()).getPrecision().toMillis(timestampValue);
988+
return getSnapshotIdTimeOperator(table, millisUtc, tableVersion.getVersionOperator());
989+
}
984990
throw new PrestoException(NOT_SUPPORTED, "Unsupported table version expression type: " + tableVersion.getVersionExpressionType());
985991
}
986992
if (tableVersion.getVersionType() == VersionType.VERSION) {

presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergTableVersion.java

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,31 @@
1414
package com.facebook.presto.iceberg;
1515

1616
import com.facebook.presto.Session;
17+
import com.facebook.presto.Session.SessionBuilder;
18+
import com.facebook.presto.common.type.TimeZoneKey;
1719
import com.facebook.presto.testing.QueryRunner;
1820
import com.facebook.presto.tests.AbstractTestQueryFramework;
1921
import com.facebook.presto.tests.DistributedQueryRunner;
2022
import com.google.common.collect.ImmutableMap;
2123
import org.testng.annotations.AfterClass;
2224
import org.testng.annotations.BeforeClass;
25+
import org.testng.annotations.DataProvider;
2326
import org.testng.annotations.Test;
2427

2528
import java.nio.file.Path;
29+
import java.time.Instant;
30+
import java.time.LocalDateTime;
31+
import java.time.ZoneId;
32+
import java.time.format.DateTimeFormatter;
2633
import java.util.Map;
2734

35+
import static com.facebook.presto.SystemSessionProperties.LEGACY_TIMESTAMP;
2836
import static com.facebook.presto.iceberg.CatalogType.HIVE;
2937
import static com.facebook.presto.iceberg.IcebergQueryRunner.ICEBERG_CATALOG;
3038
import static com.facebook.presto.iceberg.IcebergQueryRunner.getIcebergDataDirectoryPath;
3139
import static com.facebook.presto.testing.TestingSession.testSessionBuilder;
40+
import static java.lang.String.format;
41+
import static org.testng.Assert.assertTrue;
3242

3343
public class TestIcebergTableVersion
3444
extends AbstractTestQueryFramework
@@ -271,6 +281,55 @@ public void testTableVersionMisc()
271281
assertQuery("SELECT count(*) FROM " + viewName3 + " INNER JOIN " + viewName4 + " ON " + viewName3 + ".id = " + viewName4 + ".id", "VALUES 2");
272282
}
273283

284+
@DataProvider(name = "timezones")
285+
public Object[][] timezones()
286+
{
287+
return new Object[][] {
288+
{"UTC", true},
289+
{"America/Los_Angeles", true},
290+
{"Asia/Shanghai", true},
291+
{"UTC", false}};
292+
}
293+
294+
@Test(dataProvider = "timezones")
295+
public void testTableVersionWithTimestamp(String zoneId, boolean legacyTimestamp)
296+
{
297+
Session session = sessionForTimezone(zoneId, legacyTimestamp);
298+
String tableName = schemaName + "." + "table_version_with_timestamp";
299+
try {
300+
assertUpdate(session, "CREATE TABLE " + tableName + " (id integer, desc varchar) WITH(partitioning = ARRAY['id'])");
301+
assertUpdate(session, "INSERT INTO " + tableName + " VALUES(1, 'aaa')", 1);
302+
waitUntilAfter(System.currentTimeMillis());
303+
304+
long timestampMillis1 = System.currentTimeMillis();
305+
String timestampWithoutTZ1 = getTimestampString(timestampMillis1, zoneId);
306+
waitUntilAfter(timestampMillis1);
307+
308+
assertUpdate(session, "INSERT INTO " + tableName + " VALUES(2, 'bbb')", 1);
309+
waitUntilAfter(System.currentTimeMillis());
310+
311+
long timestampMillis2 = System.currentTimeMillis();
312+
String timestampWithoutTZ2 = getTimestampString(timestampMillis2, zoneId);
313+
waitUntilAfter(timestampMillis2);
314+
315+
assertUpdate(session, "INSERT INTO " + tableName + " VALUES(3, 'ccc')", 1);
316+
waitUntilAfter(System.currentTimeMillis());
317+
318+
long timestampMillis3 = System.currentTimeMillis();
319+
String timestampWithoutTZ3 = getTimestampString(timestampMillis3, zoneId);
320+
321+
assertQuery(session, "SELECT desc FROM " + tableName + " FOR TIMESTAMP AS OF TIMESTAMP " + "'" + timestampWithoutTZ1 + "'", "VALUES 'aaa'");
322+
assertQuery(session, "SELECT desc FROM " + tableName + " FOR TIMESTAMP BEFORE TIMESTAMP " + "'" + timestampWithoutTZ1 + "'", "VALUES 'aaa'");
323+
assertQuery(session, "SELECT desc FROM " + tableName + " FOR TIMESTAMP AS OF TIMESTAMP " + "'" + timestampWithoutTZ2 + "'", "VALUES 'aaa', 'bbb'");
324+
assertQuery(session, "SELECT desc FROM " + tableName + " FOR TIMESTAMP BEFORE TIMESTAMP " + "'" + timestampWithoutTZ2 + "'", "VALUES 'aaa', 'bbb'");
325+
assertQuery(session, "SELECT desc FROM " + tableName + " FOR TIMESTAMP AS OF TIMESTAMP " + "'" + timestampWithoutTZ3 + "'", "VALUES 'aaa', 'bbb', 'ccc'");
326+
assertQuery(session, "SELECT desc FROM " + tableName + " FOR TIMESTAMP BEFORE TIMESTAMP " + "'" + timestampWithoutTZ3 + "'", "VALUES 'aaa', 'bbb', 'ccc'");
327+
}
328+
finally {
329+
assertQuerySucceeds("DROP TABLE IF EXISTS " + tableName);
330+
}
331+
}
332+
274333
@Test
275334
public void testTableVersionErrors()
276335
{
@@ -284,23 +343,56 @@ public void testTableVersionErrors()
284343
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR VERSION AS OF " + tab2VersionId1 + " - " + tab2VersionId1, "Iceberg snapshot ID does not exists: 0");
285344
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR VERSION AS OF CAST (100 AS BIGINT)", "Iceberg snapshot ID does not exists: 100");
286345

287-
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF 100", ".* Type integer is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.");
288-
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF 'bad'", ".* Type varchar\\(3\\) is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.");
346+
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF 100", ".* Type integer is invalid. Supported table version AS OF/BEFORE expression type is Timestamp or Timestamp with Time Zone.");
347+
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF 'bad'", ".* Type varchar\\(3\\) is invalid. Supported table version AS OF/BEFORE expression type is Timestamp or Timestamp with Time Zone.");
289348
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF id", ".* cannot be resolved");
290349
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF (SELECT CURRENT_TIMESTAMP)", ".* Constant expression cannot contain a subquery");
291350
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF NULL", "Table version AS OF/BEFORE expression cannot be NULL for .*");
292351
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF TIMESTAMP " + "'" + tab2Timestamp1 + "' - INTERVAL '1' MONTH", "No history found based on timestamp for table \"test_tt_schema\".\"test_table_version_tab2\"");
293352
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF CAST ('2023-01-01' AS TIMESTAMP WITH TIME ZONE)", "No history found based on timestamp for table \"test_tt_schema\".\"test_table_version_tab2\"");
294-
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF CAST ('2023-01-01' AS TIMESTAMP)", ".* Type timestamp is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.");
295-
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF CAST ('2023-01-01' AS DATE)", ".* Type date is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.");
296-
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF CURRENT_DATE", ".* Type date is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.");
297-
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF TIMESTAMP '2023-01-01 00:00:00.000'", ".* Type timestamp is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.");
353+
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF CAST ('2023-01-01' AS TIMESTAMP)", "No history found based on timestamp for table \"test_tt_schema\".\"test_table_version_tab2\"");
354+
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF CAST ('2023-01-01' AS DATE)", ".* Type date is invalid. Supported table version AS OF/BEFORE expression type is Timestamp or Timestamp with Time Zone.");
355+
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF CURRENT_DATE", ".* Type date is invalid. Supported table version AS OF/BEFORE expression type is Timestamp or Timestamp with Time Zone.");
356+
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP AS OF TIMESTAMP '2023-01-01 00:00:00.000'", "No history found based on timestamp for table \"test_tt_schema\".\"test_table_version_tab2\"");
298357

299358
assertQueryFails("SELECT desc FROM " + tableName1 + " FOR VERSION BEFORE " + tab1VersionId1 + " ORDER BY 1", "No history found based on timestamp for table \"test_tt_schema\".\"test_table_version_tab1\"");
300359
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP BEFORE TIMESTAMP " + "'" + tab2Timestamp1 + "' - INTERVAL '1' MONTH", "No history found based on timestamp for table \"test_tt_schema\".\"test_table_version_tab2\"");
301360
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR VERSION BEFORE 100", ".* Type integer is invalid. Supported table version AS OF/BEFORE expression type is BIGINT or VARCHAR");
302361
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR VERSION BEFORE " + tab2VersionId1 + " - " + tab2VersionId1, "Iceberg snapshot ID does not exists: 0");
303-
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP BEFORE 'bad'", ".* Type varchar\\(3\\) is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.");
362+
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP BEFORE 'bad'", ".* Type varchar\\(3\\) is invalid. Supported table version AS OF/BEFORE expression type is Timestamp or Timestamp with Time Zone.");
304363
assertQueryFails("SELECT desc FROM " + tableName2 + " FOR TIMESTAMP BEFORE NULL", "Table version AS OF/BEFORE expression cannot be NULL for .*");
305364
}
365+
366+
private Session sessionForTimezone(String zoneId, boolean legacyTimestamp)
367+
{
368+
SessionBuilder sessionBuilder = Session.builder(getSession())
369+
.setSystemProperty(LEGACY_TIMESTAMP, String.valueOf(legacyTimestamp));
370+
if (legacyTimestamp) {
371+
sessionBuilder.setTimeZoneKey(TimeZoneKey.getTimeZoneKey(zoneId));
372+
}
373+
return sessionBuilder.build();
374+
}
375+
376+
private long waitUntilAfter(long snapshotTimeMillis)
377+
{
378+
long currentTimeMillis = System.currentTimeMillis();
379+
assertTrue(snapshotTimeMillis - currentTimeMillis <= 10,
380+
format("Snapshot time %s is greater than the current time %s by more than 10ms", snapshotTimeMillis, currentTimeMillis));
381+
382+
while (currentTimeMillis <= snapshotTimeMillis) {
383+
currentTimeMillis = System.currentTimeMillis();
384+
}
385+
return currentTimeMillis;
386+
}
387+
388+
private String getTimestampString(long timeMillsUtc, String zoneId)
389+
{
390+
Instant instant = Instant.ofEpochMilli(timeMillsUtc);
391+
LocalDateTime localDateTime = instant
392+
.atZone(ZoneId.of(zoneId))
393+
.toLocalDateTime();
394+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
395+
formatter = formatter.withZone(ZoneId.of(zoneId));
396+
return localDateTime.format(formatter);
397+
}
306398
}

presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.facebook.presto.common.type.MapType;
2828
import com.facebook.presto.common.type.RealType;
2929
import com.facebook.presto.common.type.RowType;
30+
import com.facebook.presto.common.type.TimestampType;
3031
import com.facebook.presto.common.type.TimestampWithTimeZoneType;
3132
import com.facebook.presto.common.type.Type;
3233
import com.facebook.presto.common.type.VarcharType;
@@ -1415,9 +1416,9 @@ private Optional<TableHandle> processTableVersion(Table table, QualifiedObjectNa
14151416
}
14161417
Object evalStateExpr = evaluateConstantExpression(stateExpr, stateExprType, metadata, session, analysis.getParameters());
14171418
if (tableVersionType == TIMESTAMP) {
1418-
if (!(stateExprType instanceof TimestampWithTimeZoneType)) {
1419+
if (!(stateExprType instanceof TimestampWithTimeZoneType || stateExprType instanceof TimestampType)) {
14191420
throw new SemanticException(TYPE_MISMATCH, stateExpr,
1420-
"Type %s is invalid. Supported table version AS OF/BEFORE expression type is Timestamp with Time Zone.",
1421+
"Type %s is invalid. Supported table version AS OF/BEFORE expression type is Timestamp or Timestamp with Time Zone.",
14211422
stateExprType.getDisplayName());
14221423
}
14231424
}

0 commit comments

Comments
 (0)