common api - null equivalence (#15926)

closes https://github.com/twentyhq/core-team-issues/issues/1629

To do before requesting review : 
- filter update

Migration to come in an other PR


Strat : 
1/  Null transformation

- [x] Transform NULL equivalent value to NULL in field validation in
common api - pre-query - with feature flag
- [ ] Same logic in ORM (Not done, complex to handle feature flag here)
- [x] Transform NULL value to equivalent in data formatting in ORM -
post-query

2/ Migration (in other PR) for fieldMetadata not nullable with default
defaultValue (empty string, ...)

- [ ] Remove NOT NULL db constraint
- [ ] Update record value to NULL
- [ ] Update field metadata : isNullable:true
- [ ] Update uniqueIndex whereClause (also for standard uniqueIndex) 
- [ ] Activate feature flag

3/ Update metadata creation

- [x] No more default default value
- [x] Update standard field nullability
- [x] Remove index default whereClause for standard field

4/ Update filter

- [x] When filtering on NULL or empty string, be sure all records are
returned (the one with NULL + the one with "")

5/ Test

- [ ] Strat. to do
This commit is contained in:
Etienne
2025-11-24 18:57:47 +01:00
committed by GitHub
parent 3ed6d2a16b
commit 208c0857ee
59 changed files with 884 additions and 300 deletions
@@ -46,7 +46,7 @@ describe('WorkspaceMigrationIndexFactory', () => {
expect(firstMigration.indexes[0].columns).toEqual(['simpleField']);
expect(firstMigration.indexes[0].type).toBe('BTREE');
expect(firstMigration.indexes[0].isUnique).toBe(true);
expect(firstMigration.indexes[0].where).toBe('"simpleField" != \'\'');
expect(firstMigration.indexes[0].where).toBeNull();
});
it('should create index migrations for relation fields', async () => {
@@ -1,7 +1,7 @@
import {
FieldMetadataType,
type CompositeType,
compositeTypeDefinitions,
FieldMetadataType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -92,17 +92,13 @@ export const createIndexMigration = async (
.flat()
.filter(isDefined);
const defaultWhereClause = indexMetadata.isUnique
? `${columns.map((column) => `"${column}"`).join(" != '' AND ")} != ''`
: null;
return {
name: indexMetadata.name,
action: WorkspaceMigrationIndexActionType.CREATE,
isUnique: indexMetadata.isUnique,
columns,
type: indexMetadata.indexType,
where: indexMetadata.indexWhereClause ?? defaultWhereClause,
where: indexMetadata.indexWhereClause,
};
});
@@ -8,7 +8,6 @@ import {
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util';
import { FlatEntityMapsExceptionCode } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
@@ -147,11 +146,7 @@ export class FlatIndexValidatorService {
if (flatIndexToValidate.isUnique) {
if (
isDefined(relatedFlatField.defaultValue) &&
!isValidUniqueFieldDefaultValueCombination({
defaultValue: relatedFlatField.defaultValue,
isUnique: relatedFlatField.isUnique ?? false,
type: relatedFlatField.type,
})
relatedFlatField.isUnique
) {
const fieldName = relatedFlatField.name;
const fieldType = relatedFlatField.type;