Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions backend/actions/evaluated_column.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<zetasql::Value> evaluated_values;
evaluated_values.reserve(evaluated_columns_.size());
Expand Down Expand Up @@ -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));
Expand Down
3 changes: 2 additions & 1 deletion backend/actions/evaluated_column.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;

Expand Down
9 changes: 8 additions & 1 deletion backend/query/query_engine.cc
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ bool IsGenerated(const zetasql::Column* column) {
return column->GetAs<QueryableColumn>()->wrapped_column()->is_generated();
}

bool HasDefaultValue(const zetasql::Column* column) {
return column->GetAs<QueryableColumn>()->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.
Expand Down Expand Up @@ -332,7 +336,10 @@ absl::StatusOr<std::tuple<Mutation, int64_t, bool>> 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;
}
Expand Down
23 changes: 23 additions & 0 deletions tests/conformance/cases/column_default_value_read_write.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<database_api::DatabaseDialect> {
Expand Down
9 changes: 9 additions & 0 deletions tests/conformance/data/schemas/column_default_value.test
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
);