From b852bd95d1510a9cfeb490ee407c002cc9a14992 Mon Sep 17 00:00:00 2001 From: Nikolai Amelichev Date: Thu, 31 Jul 2025 20:59:55 +0200 Subject: [PATCH] #149: Support NOT NULL columns via `@Column(notNull=true)` Defaulting ID fields to required (`NOT NULL`) can be enabled via a Java system property, `tech.ydb.yoj.repository.db.id.nullability`, which will influence the schema checker (`YdbSchemaCompatibilityChecker`), programmatic table creation via `YdbSchemaOperations`, and YQL code generation in `YqlStatement`s that take into account the `isOptional()` `JavaField` attribute. The new attribute is introduced by this change, so the only supporting code generators initially are the standard `YqlStatement` implementations. The valid System property values are: - `USE_COLUMN_ANNOTATION` (Default): Consider ID fields optional unless annotated with the `@Column(notNull=true, ...)` annotation or the `@NotNullColumn` meta-annotation. - `ALWAYS_NULL`: Force all ID fields to be treated as optional, even though YOJ does **not** allow to save entities with NULL in ID columns. - `ALWAYS_NOT_NULL`: Force all ID fields to be treated as required. --- .../yoj/databind/converter/NotNullColumn.java | 22 ++++++ .../tech/ydb/yoj/databind/schema/Column.java | 12 ++++ .../tech/ydb/yoj/databind/schema/Schema.java | 69 ++++++++++++++++--- .../schema/naming/BadMetaAnnotatedEntity.java | 18 +++++ .../schema/naming/MetaAnnotatedEntity.java | 18 +++++ .../schema/naming/MetaAnnotationTest.java | 36 ++++++++++ .../ydb/client/YdbSchemaOperations.java | 24 ++++++- .../YdbSchemaCompatibilityChecker.java | 24 +++++-- .../ydb/statement/FindRangeStatement.java | 13 ++-- .../statement/MultipleVarsYqlStatement.java | 4 +- .../ydb/statement/UpdateSetParam.java | 2 +- .../ydb/YdbRepositoryIntegrationTest.java | 67 ++++++++++++------ .../ydb/model/IndexedEntityNotNull.java | 28 ++++++++ .../db/EntityIdFieldNullability.java | 16 +++++ .../ydb/yoj/repository/db/EntityIdSchema.java | 11 +++ .../ydb/yoj/repository/db/EntitySchema.java | 15 ++++ .../ydb/yoj/repository/db/ViewSchema.java | 15 ++++ .../repository/db/cache/RepositoryCache.java | 23 ++----- .../tech/ydb/yoj/util/lang/Annotations.java | 21 ++++-- 19 files changed, 367 insertions(+), 71 deletions(-) create mode 100644 databind/src/main/java/tech/ydb/yoj/databind/converter/NotNullColumn.java create mode 100644 databind/src/test/java/tech/ydb/yoj/databind/schema/naming/BadMetaAnnotatedEntity.java create mode 100644 databind/src/test/java/tech/ydb/yoj/databind/schema/naming/MetaAnnotatedEntity.java create mode 100644 databind/src/test/java/tech/ydb/yoj/databind/schema/naming/MetaAnnotationTest.java create mode 100644 repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/model/IndexedEntityNotNull.java create mode 100644 repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdFieldNullability.java diff --git a/databind/src/main/java/tech/ydb/yoj/databind/converter/NotNullColumn.java b/databind/src/main/java/tech/ydb/yoj/databind/converter/NotNullColumn.java new file mode 100644 index 00000000..e9cc3814 --- /dev/null +++ b/databind/src/main/java/tech/ydb/yoj/databind/converter/NotNullColumn.java @@ -0,0 +1,22 @@ +package tech.ydb.yoj.databind.converter; + +import tech.ydb.yoj.databind.schema.Column; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.RECORD_COMPONENT; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Signifies that the column stored in the database does not accept {@code NULL} values. + * + * @see Column#notNull + */ +@Column(notNull = true) +@Target({FIELD, RECORD_COMPONENT, ANNOTATION_TYPE}) +@Retention(RUNTIME) +public @interface NotNullColumn { +} diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java index c8dbe0c4..034291e9 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java @@ -58,6 +58,18 @@ */ String dbTypeQualifier() default ""; + /** + * Specifies whether only non-{@code NULL} values can be stored in this column. Defaults to {@code false} (allow {@code NULL} values).
+ * Note that this is orthogonal to Java nullness annotations, because YOJ uses {@code null} values for ID fields as a convention + * for "range over all possible values of this ID field" (see {@code Entity.Id.isPartial()}). + *

Tip: Use the {@link tech.ydb.yoj.databind.converter.NotNullColumn} annotation if you only need to overide + * {@code Column.notNull} to {@code true}. + * + * @see #149 + */ + @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/149") + boolean notNull() default false; + /** * Determines whether the {@link FieldValueType#COMPOSITE composite field} will be: *