Skip to content

Commit a6de756

Browse files
author
teable-bot
committed
[sync] fix(t1756): sync Link field notNull constraint to database (#1102)
Synced from teableio/teable-ee@c581baf
1 parent 6bacc71 commit a6de756

File tree

3 files changed

+170
-26
lines changed

3 files changed

+170
-26
lines changed

apps/nestjs-backend/src/features/field/field.service.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,7 @@ export class FieldService implements IReadonlyAdapterService {
751751
: matchedIndexes.forEach((indexName) => table.dropUnique([dbFieldName], indexName));
752752
}
753753

754-
// TODO: add to db provider
755-
if (key === 'notNull' && type !== FieldType.Link) {
754+
if (key === 'notNull') {
756755
newValue ? table.dropNullable(dbFieldName) : table.setNullable(dbFieldName);
757756
}
758757
})

apps/nestjs-backend/test/base-duplicate.e2e-spec.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { IFieldRo, ILinkFieldOptions, ILookupOptionsRo } from '@teable/core
44
import {
55
DriverClient,
66
FieldAIActionType,
7+
FieldKeyType,
78
FieldType,
89
Relationship,
910
Role,
@@ -20,6 +21,7 @@ import {
2021
createPluginPanel,
2122
createSpace,
2223
deleteBase,
24+
deleteRecords,
2325
deleteSpace,
2426
duplicateBase,
2527
EMAIL_SPACE_INVITATION,
@@ -735,7 +737,14 @@ describe('OpenAPI Base Duplicate (e2e)', () => {
735737
it('should duplicate base with bidirectional link field', async () => {
736738
const table1 = await createTable(base.id, { name: 'table1' });
737739
const table2 = await createTable(base.id, { name: 'table2' });
738-
740+
await deleteRecords(
741+
table1.id,
742+
table1.records.map((r) => r.id)
743+
);
744+
await deleteRecords(
745+
table2.id,
746+
table2.records.map((r) => r.id)
747+
);
739748
// Create bidirectional link field with dbFieldName 'link'
740749
const linkFieldRo: IFieldRo = {
741750
name: 'link field',
@@ -758,34 +767,31 @@ describe('OpenAPI Base Duplicate (e2e)', () => {
758767
...linkFieldRo,
759768
notNull: true,
760769
});
761-
770+
await createRecords(table2.id, {
771+
fieldKeyType: FieldKeyType.Id,
772+
records: [{ fields: {} }, { fields: {} }, { fields: {} }],
773+
});
762774
// Get records
763-
const table1Records = await getRecords(table1.id);
764775
const table2Records = await getRecords(table2.id);
765-
766-
// Fill all link relationships
767-
await updateRecord(table1.id, table1Records.records[0].id, {
768-
record: {
769-
fields: {
770-
[linkField.name]: [{ id: table2Records.records[0].id }],
776+
await createRecords(table1.id, {
777+
fieldKeyType: FieldKeyType.Name,
778+
records: [
779+
{
780+
fields: {
781+
[linkField.name]: [{ id: table2Records.records[0].id }],
782+
},
771783
},
772-
},
773-
});
774-
775-
await updateRecord(table1.id, table1Records.records[1].id, {
776-
record: {
777-
fields: {
778-
[linkField.name]: [{ id: table2Records.records[1].id }],
784+
{
785+
fields: {
786+
[linkField.name]: [{ id: table2Records.records[1].id }],
787+
},
779788
},
780-
},
781-
});
782-
783-
await updateRecord(table1.id, table1Records.records[2].id, {
784-
record: {
785-
fields: {
786-
[linkField.name]: [{ id: table2Records.records[2].id }],
789+
{
790+
fields: {
791+
[linkField.name]: [{ id: table2Records.records[2].id }],
792+
},
787793
},
788-
},
794+
],
789795
});
790796

791797
// Duplicate base with records
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* T1756: Link field NOT NULL constraint sync bug
3+
*
4+
* Steps to reproduce:
5+
* 1. Create a Number field
6+
* 2. Set notNull=true on the Number field
7+
* 3. Convert it to a Link field
8+
* 4. Edit the Link field and turn off notNull
9+
* 5. Try to create a record with empty Link value - FAILS because DB constraint still exists
10+
*/
11+
import type { INestApplication } from '@nestjs/common';
12+
import { FieldKeyType, FieldType, Relationship } from '@teable/core';
13+
import type { ITableFullVo } from '@teable/openapi';
14+
import {
15+
createField,
16+
createTable,
17+
convertField,
18+
createRecords,
19+
getField,
20+
initApp,
21+
permanentDeleteTable,
22+
deleteRecords,
23+
getRecords,
24+
} from './utils/init-app';
25+
26+
describe('T1756: Link field NOT NULL constraint sync bug', () => {
27+
let app: INestApplication;
28+
const baseId = globalThis.testConfig.baseId;
29+
30+
beforeAll(async () => {
31+
const appCtx = await initApp();
32+
app = appCtx.app;
33+
});
34+
35+
afterAll(async () => {
36+
await app.close();
37+
});
38+
39+
describe('bug reproduction', () => {
40+
let table1: ITableFullVo;
41+
let table2: ITableFullVo;
42+
43+
beforeEach(async () => {
44+
table1 = await createTable(baseId, { name: `table1-${Date.now()}` });
45+
table2 = await createTable(baseId, { name: `table2-${Date.now()}` });
46+
47+
// Clear default records
48+
const records1 = await getRecords(table1.id);
49+
const records2 = await getRecords(table2.id);
50+
if (records1.records.length) {
51+
await deleteRecords(
52+
table1.id,
53+
records1.records.map((r) => r.id)
54+
);
55+
}
56+
if (records2.records.length) {
57+
await deleteRecords(
58+
table2.id,
59+
records2.records.map((r) => r.id)
60+
);
61+
}
62+
});
63+
64+
afterEach(async () => {
65+
await permanentDeleteTable(baseId, table1.id);
66+
await permanentDeleteTable(baseId, table2.id);
67+
});
68+
69+
it('should allow creating record with empty Link after removing notNull constraint', async () => {
70+
// Step 1: Create a Number field
71+
const numberField = await createField(table1.id, {
72+
name: 'TestField',
73+
type: FieldType.Number,
74+
});
75+
76+
// Step 2: Set notNull=true on the Number field
77+
await convertField(table1.id, numberField.id, {
78+
...numberField,
79+
notNull: true,
80+
});
81+
82+
// Step 3: Convert to Link field
83+
const linkField = await convertField(table1.id, numberField.id, {
84+
type: FieldType.Link,
85+
options: {
86+
relationship: Relationship.ManyOne,
87+
foreignTableId: table2.id,
88+
},
89+
});
90+
91+
// Step 4: Turn off notNull on the Link field
92+
const linkFieldFull = await getField(table1.id, linkField.id);
93+
const updatedLinkField = await convertField(table1.id, linkField.id, {
94+
...linkFieldFull,
95+
notNull: false,
96+
});
97+
98+
// Verify metadata shows notNull is false
99+
expect(updatedLinkField.notNull).toBeFalsy();
100+
101+
// Step 5: Try to create a record with empty Link value
102+
// BUG: This should succeed since notNull is false in metadata
103+
// But it fails because DB still has NOT NULL constraint
104+
const result = await createRecords(
105+
table1.id,
106+
{
107+
fieldKeyType: FieldKeyType.Id,
108+
records: [{ fields: {} }], // Empty record, no Link value
109+
},
110+
201 // Expect success (201), but will get 500 due to DB constraint
111+
);
112+
113+
expect(result.records).toHaveLength(1);
114+
});
115+
116+
it('should not allow creating record with empty Link after setting notNull constraint', async () => {
117+
const linkField = await createField(table1.id, {
118+
type: FieldType.Link,
119+
options: {
120+
relationship: Relationship.ManyOne,
121+
foreignTableId: table2.id,
122+
},
123+
});
124+
const linkFieldFull = await getField(table1.id, linkField.id);
125+
await convertField(table1.id, linkField.id, {
126+
...linkFieldFull,
127+
notNull: true,
128+
});
129+
await createRecords(
130+
table1.id,
131+
{
132+
fieldKeyType: FieldKeyType.Id,
133+
records: [{ fields: {} }], // Empty record, no Link value
134+
},
135+
400 // Expect success (201), but will get 500 due to DB constraint
136+
);
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)