|
17 | 17 | import java.sql.Connection; |
18 | 18 | import java.sql.PreparedStatement; |
19 | 19 | import java.sql.ResultSet; |
| 20 | +import java.sql.Timestamp; |
20 | 21 | import java.util.ArrayList; |
21 | 22 | import java.util.Collections; |
22 | 23 | import java.util.HashMap; |
@@ -96,6 +97,12 @@ public static void init() throws IOException { |
96 | 97 | postgresConfig.put("url", postgresConnectionUrl); |
97 | 98 | postgresConfig.put("user", "postgres"); |
98 | 99 | postgresConfig.put("password", "postgres"); |
| 100 | + postgresConfig.put( |
| 101 | + "postgres.collectionConfigs." + FLAT_COLLECTION_NAME + ".timestampFields.created", |
| 102 | + "createdTime"); |
| 103 | + postgresConfig.put( |
| 104 | + "postgres.collectionConfigs." + FLAT_COLLECTION_NAME + ".timestampFields.lastUpdated", |
| 105 | + "lastUpdateTime"); |
99 | 106 |
|
100 | 107 | postgresDatastore = |
101 | 108 | DatastoreProvider.getDatastore("Postgres", ConfigFactory.parseMap(postgresConfig)); |
@@ -126,7 +133,9 @@ private static void createFlatCollectionSchema() { |
126 | 133 | + "\"big_number\" BIGINT," |
127 | 134 | + "\"rating\" REAL," |
128 | 135 | + "\"created_date\" DATE," |
129 | | - + "\"weight\" DOUBLE PRECISION" |
| 136 | + + "\"weight\" DOUBLE PRECISION," |
| 137 | + + "\"createdTime\" BIGINT," |
| 138 | + + "\"lastUpdateTime\" TIMESTAMP WITH TIME ZONE" |
130 | 139 | + ");", |
131 | 140 | FLAT_COLLECTION_NAME); |
132 | 141 |
|
@@ -3830,4 +3839,209 @@ void testDrop() { |
3830 | 3839 | assertThrows(UnsupportedOperationException.class, () -> flatCollection.drop()); |
3831 | 3840 | } |
3832 | 3841 | } |
| 3842 | + |
| 3843 | + @Nested |
| 3844 | + @DisplayName("Timestamp Auto-Population Tests") |
| 3845 | + class TimestampTests { |
| 3846 | + |
| 3847 | + @Test |
| 3848 | + @DisplayName( |
| 3849 | + "Should auto-populate createdTime (BIGINT) and lastUpdateTime (TIMESTAMPTZ) on create") |
| 3850 | + void testTimestampsOnCreate() throws Exception { |
| 3851 | + long beforeCreate = System.currentTimeMillis(); |
| 3852 | + |
| 3853 | + ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); |
| 3854 | + objectNode.put("id", "ts-test-1"); |
| 3855 | + objectNode.put("item", "TimestampTestItem"); |
| 3856 | + objectNode.put("price", 100); |
| 3857 | + Document document = new JSONDocument(objectNode); |
| 3858 | + Key key = new SingleValueKey(DEFAULT_TENANT, "ts-test-1"); |
| 3859 | + |
| 3860 | + CreateResult result = flatCollection.create(key, document); |
| 3861 | + assertTrue(result.isSucceed()); |
| 3862 | + |
| 3863 | + long afterCreate = System.currentTimeMillis(); |
| 3864 | + |
| 3865 | + PostgresDatastore pgDatastore = (PostgresDatastore) postgresDatastore; |
| 3866 | + try (Connection conn = pgDatastore.getPostgresClient(); |
| 3867 | + PreparedStatement ps = |
| 3868 | + conn.prepareStatement( |
| 3869 | + String.format( |
| 3870 | + "SELECT \"createdTime\", \"lastUpdateTime\" FROM \"%s\" WHERE \"id\" = '%s'", |
| 3871 | + FLAT_COLLECTION_NAME, key)); |
| 3872 | + ResultSet rs = ps.executeQuery()) { |
| 3873 | + assertTrue(rs.next()); |
| 3874 | + |
| 3875 | + long createdTime = rs.getLong("createdTime"); |
| 3876 | + assertFalse(rs.wasNull(), "createdTime should not be NULL"); |
| 3877 | + assertTrue( |
| 3878 | + createdTime >= beforeCreate && createdTime <= afterCreate, |
| 3879 | + "createdTime should be within test execution window"); |
| 3880 | + |
| 3881 | + Timestamp lastUpdateTime = rs.getTimestamp("lastUpdateTime"); |
| 3882 | + assertNotNull(lastUpdateTime, "lastUpdateTime should not be NULL"); |
| 3883 | + assertTrue( |
| 3884 | + lastUpdateTime.getTime() >= beforeCreate && lastUpdateTime.getTime() <= afterCreate, |
| 3885 | + "lastUpdateTime should be within test execution window"); |
| 3886 | + } |
| 3887 | + } |
| 3888 | + |
| 3889 | + @Test |
| 3890 | + @DisplayName("Should preserve createdTime and update lastUpdateTime on upsert") |
| 3891 | + void testTimestampsOnUpsert() throws Exception { |
| 3892 | + // First create |
| 3893 | + ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); |
| 3894 | + objectNode.put("id", "ts-test-2"); |
| 3895 | + objectNode.put("item", "UpsertTimestampTest"); |
| 3896 | + objectNode.put("price", 100); |
| 3897 | + Document document = new JSONDocument(objectNode); |
| 3898 | + Key key = new SingleValueKey(DEFAULT_TENANT, "ts-test-2"); |
| 3899 | + |
| 3900 | + flatCollection.create(key, document); |
| 3901 | + |
| 3902 | + PostgresDatastore pgDatastore = (PostgresDatastore) postgresDatastore; |
| 3903 | + long originalCreatedTime; |
| 3904 | + long originalLastUpdateTime; |
| 3905 | + try (Connection conn = pgDatastore.getPostgresClient(); |
| 3906 | + PreparedStatement ps = |
| 3907 | + conn.prepareStatement( |
| 3908 | + String.format( |
| 3909 | + "SELECT \"createdTime\", \"lastUpdateTime\" FROM \"%s\" WHERE \"id\" = '%s'", |
| 3910 | + FLAT_COLLECTION_NAME, key.toString())); |
| 3911 | + ResultSet rs = ps.executeQuery()) { |
| 3912 | + assertTrue(rs.next()); |
| 3913 | + originalCreatedTime = rs.getLong("createdTime"); |
| 3914 | + originalLastUpdateTime = rs.getTimestamp("lastUpdateTime").getTime(); |
| 3915 | + } |
| 3916 | + |
| 3917 | + // Wait a bit to ensure time difference |
| 3918 | + Thread.sleep(50); |
| 3919 | + |
| 3920 | + // Upsert (update existing) |
| 3921 | + long beforeUpsert = System.currentTimeMillis(); |
| 3922 | + objectNode.put("price", 200); |
| 3923 | + Document updatedDoc = new JSONDocument(objectNode); |
| 3924 | + flatCollection.createOrReplace(key, updatedDoc); |
| 3925 | + long afterUpsert = System.currentTimeMillis(); |
| 3926 | + |
| 3927 | + try (Connection conn = pgDatastore.getPostgresClient(); |
| 3928 | + PreparedStatement ps = |
| 3929 | + conn.prepareStatement( |
| 3930 | + String.format( |
| 3931 | + "SELECT \"createdTime\", \"lastUpdateTime\" FROM \"%s\" WHERE \"id\" = '%s'", |
| 3932 | + FLAT_COLLECTION_NAME, key.toString())); |
| 3933 | + ResultSet rs = ps.executeQuery()) { |
| 3934 | + assertTrue(rs.next()); |
| 3935 | + |
| 3936 | + long newCreatedTime = rs.getLong("createdTime"); |
| 3937 | + assertEquals( |
| 3938 | + originalCreatedTime, newCreatedTime, "createdTime should be preserved on upsert"); |
| 3939 | + |
| 3940 | + long newLastUpdateTime = rs.getTimestamp("lastUpdateTime").getTime(); |
| 3941 | + assertTrue(newLastUpdateTime > originalLastUpdateTime, "lastUpdateTime should be updated"); |
| 3942 | + assertTrue( |
| 3943 | + newLastUpdateTime >= beforeUpsert && newLastUpdateTime <= afterUpsert, |
| 3944 | + "lastUpdateTime should be within upsert execution window"); |
| 3945 | + } |
| 3946 | + } |
| 3947 | + |
| 3948 | + @Test |
| 3949 | + @DisplayName( |
| 3950 | + "Should not throw exception when timestampFields config is missing - cols remain NULL") |
| 3951 | + void testNoExceptionWhenTimestampConfigMissing() throws Exception { |
| 3952 | + // Create a collection WITHOUT timestampFields config |
| 3953 | + String postgresConnectionUrl = |
| 3954 | + String.format("jdbc:postgresql://localhost:%s/", postgres.getMappedPort(5432)); |
| 3955 | + |
| 3956 | + Map<String, String> configWithoutTimestamps = new HashMap<>(); |
| 3957 | + configWithoutTimestamps.put("url", postgresConnectionUrl); |
| 3958 | + configWithoutTimestamps.put("user", "postgres"); |
| 3959 | + configWithoutTimestamps.put("password", "postgres"); |
| 3960 | + // Note: NO customParams.timestampFields config |
| 3961 | + |
| 3962 | + Datastore datastoreWithoutTimestamps = |
| 3963 | + DatastoreProvider.getDatastore( |
| 3964 | + "Postgres", ConfigFactory.parseMap(configWithoutTimestamps)); |
| 3965 | + Collection collectionWithoutTimestamps = |
| 3966 | + datastoreWithoutTimestamps.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT); |
| 3967 | + |
| 3968 | + // Create a document - should NOT throw exception |
| 3969 | + ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); |
| 3970 | + objectNode.put("id", "ts-test-no-config"); |
| 3971 | + objectNode.put("item", "NoTimestampConfigTest"); |
| 3972 | + objectNode.put("price", 100); |
| 3973 | + Document document = new JSONDocument(objectNode); |
| 3974 | + Key key = new SingleValueKey(DEFAULT_TENANT, "ts-test-no-config"); |
| 3975 | + |
| 3976 | + CreateResult result = collectionWithoutTimestamps.create(key, document); |
| 3977 | + assertTrue(result.isSucceed()); |
| 3978 | + |
| 3979 | + // Verify timestamp columns are NULL |
| 3980 | + PostgresDatastore pgDatastore = (PostgresDatastore) postgresDatastore; |
| 3981 | + try (Connection conn = pgDatastore.getPostgresClient(); |
| 3982 | + PreparedStatement ps = |
| 3983 | + conn.prepareStatement( |
| 3984 | + String.format( |
| 3985 | + "SELECT \"createdTime\", \"lastUpdateTime\" FROM \"%s\" WHERE \"id\" = '%s'", |
| 3986 | + FLAT_COLLECTION_NAME, key)); |
| 3987 | + ResultSet rs = ps.executeQuery()) { |
| 3988 | + assertTrue(rs.next()); |
| 3989 | + |
| 3990 | + assertNull( |
| 3991 | + rs.getObject("createdTime"), "createdTime should be NULL when config is missing"); |
| 3992 | + assertNull( |
| 3993 | + rs.getObject("lastUpdateTime"), "lastUpdateTime should be NULL when config is missing"); |
| 3994 | + } |
| 3995 | + } |
| 3996 | + |
| 3997 | + @Test |
| 3998 | + @DisplayName( |
| 3999 | + "Should not throw exception when timestampFields config is invalid JSON - cols remain NULL") |
| 4000 | + void testNoExceptionWhenTimestampConfigInvalidJson() throws Exception { |
| 4001 | + // Create a collection with INVALID JSON in timestampFields config |
| 4002 | + String postgresConnectionUrl = |
| 4003 | + String.format("jdbc:postgresql://localhost:%s/", postgres.getMappedPort(5432)); |
| 4004 | + |
| 4005 | + Map<String, String> configWithInvalidJson = new HashMap<>(); |
| 4006 | + configWithInvalidJson.put("url", postgresConnectionUrl); |
| 4007 | + configWithInvalidJson.put("user", "postgres"); |
| 4008 | + configWithInvalidJson.put("password", "postgres"); |
| 4009 | + // Invalid JSON - missing quotes, malformed |
| 4010 | + configWithInvalidJson.put("customParams.timestampFields", "not valid json {{{"); |
| 4011 | + |
| 4012 | + Datastore datastoreWithInvalidConfig = |
| 4013 | + DatastoreProvider.getDatastore("Postgres", ConfigFactory.parseMap(configWithInvalidJson)); |
| 4014 | + Collection collectionWithInvalidConfig = |
| 4015 | + datastoreWithInvalidConfig.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT); |
| 4016 | + |
| 4017 | + // Create a document - should NOT throw exception |
| 4018 | + ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); |
| 4019 | + objectNode.put("id", "ts-test-invalid-json"); |
| 4020 | + objectNode.put("item", "InvalidJsonConfigTest"); |
| 4021 | + objectNode.put("price", 100); |
| 4022 | + Document document = new JSONDocument(objectNode); |
| 4023 | + Key key = new SingleValueKey(DEFAULT_TENANT, "ts-test-invalid-json"); |
| 4024 | + |
| 4025 | + CreateResult result = collectionWithInvalidConfig.create(key, document); |
| 4026 | + assertTrue(result.isSucceed()); |
| 4027 | + |
| 4028 | + // Verify timestamp columns are NULL (config parsing failed gracefully) |
| 4029 | + PostgresDatastore pgDatastore = (PostgresDatastore) postgresDatastore; |
| 4030 | + try (Connection conn = pgDatastore.getPostgresClient(); |
| 4031 | + PreparedStatement ps = |
| 4032 | + conn.prepareStatement( |
| 4033 | + String.format( |
| 4034 | + "SELECT \"createdTime\", \"lastUpdateTime\" FROM \"%s\" WHERE \"id\" = '%s'", |
| 4035 | + FLAT_COLLECTION_NAME, key.toString())); |
| 4036 | + ResultSet rs = ps.executeQuery()) { |
| 4037 | + assertTrue(rs.next()); |
| 4038 | + |
| 4039 | + rs.getLong("createdTime"); |
| 4040 | + assertTrue(rs.wasNull(), "createdTime should be NULL when config JSON is invalid"); |
| 4041 | + |
| 4042 | + rs.getTimestamp("lastUpdateTime"); |
| 4043 | + assertTrue(rs.wasNull(), "lastUpdateTime should be NULL when config JSON is invalid"); |
| 4044 | + } |
| 4045 | + } |
| 4046 | + } |
3833 | 4047 | } |
0 commit comments