From 7664b165af0a9f68d6d187a1e19bf295c8fad71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Niewrza=C5=82?= Date: Fri, 9 Jan 2026 11:33:23 +0100 Subject: [PATCH] Fix different timestamp between INSERT and SELECT --- backend/actions/evaluated_column.cc | 13 ++++++++--- backend/actions/evaluated_column.h | 3 ++- backend/query/query_engine.cc | 9 +++++++- .../cases/column_default_value_read_write.cc | 23 +++++++++++++++++++ .../data/schemas/column_default_value.test | 9 ++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/backend/actions/evaluated_column.cc b/backend/actions/evaluated_column.cc index 5e87c1c9..c5a8f814 100644 --- a/backend/actions/evaluated_column.cc +++ b/backend/actions/evaluated_column.cc @@ -273,7 +273,7 @@ absl::Status EvaluatedColumnEffector::Effect(const ActionContext* ctx, } return Effect(ctx, op.key, &column_values, /*is_update_op=*/false, - /*apply_on_update=*/false); + /*apply_on_update=*/false, /*origin_is_dml=*/op.origin_is_dml); } absl::Status EvaluatedColumnEffector::Effect(const ActionContext* ctx, @@ -306,13 +306,13 @@ absl::Status EvaluatedColumnEffector::Effect(const ActionContext* ctx, column_values[op.columns[i]->Name()] = op.values[i]; } return Effect(ctx, op.key, &column_values, /*is_update_op=*/true, - apply_on_update); + apply_on_update, /*origin_is_dml=*/op.origin_is_dml); } absl::Status EvaluatedColumnEffector::Effect( const ActionContext* ctx, const Key& key, zetasql::ParameterValueMap* column_values, bool is_update_op, - bool apply_on_update) const { + bool apply_on_update, bool origin_is_dml) const { ZETASQL_RET_CHECK(for_keys_ == false); std::vector evaluated_values; evaluated_values.reserve(evaluated_columns_.size()); @@ -342,6 +342,13 @@ absl::Status EvaluatedColumnEffector::Effect( continue; } + // For DML-originated mutations, default values have already been evaluated + // by ZetaSQL and included in the mutation. Skip re-evaluation to avoid + // timestamp differences between RETURNING clause and stored values. + if (evaluated_column->has_default_value() && origin_is_dml) { + continue; + } + ZETASQL_ASSIGN_OR_RETURN( zetasql::Value value, ComputeEvaluatedColumnValue(evaluated_column, *column_values)); diff --git a/backend/actions/evaluated_column.h b/backend/actions/evaluated_column.h index 22d3c519..986d4e04 100644 --- a/backend/actions/evaluated_column.h +++ b/backend/actions/evaluated_column.h @@ -78,7 +78,8 @@ class EvaluatedColumnEffector : public Effector { absl::Status Effect(const ActionContext* ctx, const Key& key, zetasql::ParameterValueMap* column_values, - bool is_update_op, bool apply_on_update) const; + bool is_update_op, bool apply_on_update, + bool origin_is_dml) const; const Table* table_; diff --git a/backend/query/query_engine.cc b/backend/query/query_engine.cc index c78a04de..9dd5ee76 100644 --- a/backend/query/query_engine.cc +++ b/backend/query/query_engine.cc @@ -216,6 +216,10 @@ bool IsGenerated(const zetasql::Column* column) { return column->GetAs()->wrapped_column()->is_generated(); } +bool HasDefaultValue(const zetasql::Column* column) { + return column->GetAs()->wrapped_column()->has_default_value(); +} + // Simple visitor to traverse an expression and check if it references // a pending commit timestamp value column (that is, a column set to // PENDING_COMMIT_TIMESTAMP() in this DML). Used to validate THEN RETURN. @@ -332,7 +336,10 @@ absl::StatusOr> BuildInsert( } continue; } - if (!insert_column_names.contains(column_name) && !is_key) { + // Don't exclude columns with default values - ZetaSQL has already evaluated + // them and we need to include those evaluated values in the mutation. + bool has_default = HasDefaultValue(table->GetColumn(i)); + if (!insert_column_names.contains(column_name) && !is_key && !has_default) { excluded_column_idx.insert(i); continue; } diff --git a/tests/conformance/cases/column_default_value_read_write.cc b/tests/conformance/cases/column_default_value_read_write.cc index c321e0d7..0eebe6d0 100644 --- a/tests/conformance/cases/column_default_value_read_write.cc +++ b/tests/conformance/cases/column_default_value_read_write.cc @@ -441,6 +441,29 @@ TEST_P(ColumnDefaultValueReadWriteTest, DefaultValueWithUDF) { IsOkAndHoldsRows({{42, "abc"}, {42, "def"}, {42, "ghi"}, {42, "jkl"}})); } +TEST_P(ColumnDefaultValueReadWriteTest, InsertReturningWithCurrentTimestamp) { + // Test for issue #295: Verify that INSERT...RETURNING returns the same + // timestamp that gets stored when using CURRENT_TIMESTAMP() as a default. + auto result = Query( + "INSERT INTO timestamp_table (id) VALUES (1) THEN RETURN created_at"); + ZETASQL_ASSERT_OK(result); + ASSERT_EQ(result->rows.size(), 1); + + // Get the timestamp from RETURNING clause + auto returning_timestamp = result->rows[0].values()[0]; + + // SELECT the same row to get the stored timestamp + auto select_result = Query("SELECT created_at FROM timestamp_table WHERE id = 1"); + ZETASQL_ASSERT_OK(select_result); + ASSERT_EQ(select_result->rows.size(), 1); + + // Get the timestamp from SELECT + auto stored_timestamp = select_result->rows[0].values()[0]; + + // They should be identical - no time difference between RETURNING and storage + EXPECT_EQ(returning_timestamp, stored_timestamp); +} + class DefaultPrimaryKeyReadWriteTest : public DatabaseTest, public testing::WithParamInterface { diff --git a/tests/conformance/data/schemas/column_default_value.test b/tests/conformance/data/schemas/column_default_value.test index 45284f39..94aa7977 100644 --- a/tests/conformance/data/schemas/column_default_value.test +++ b/tests/conformance/data/schemas/column_default_value.test @@ -30,6 +30,10 @@ CREATE TABLE udf_table ( int64_col INT64 DEFAULT(udf_default_value()), string_col STRING(MAX), ) PRIMARY KEY (string_col); +CREATE TABLE timestamp_table ( + id INT64, + created_at TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP()), +) PRIMARY KEY (id); === @Dialect=POSTGRESQL CREATE TABLE t( @@ -58,3 +62,8 @@ CREATE TABLE fk( FOREIGN KEY (d) REFERENCES t(d2), PRIMARY KEY (k) ); +CREATE TABLE timestamp_table ( + id bigint, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id) +);