mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-02 13:03:17 +00:00
fix: resolve dbml increment & nullable attributes issue (#954)
* fix: resolve dbml increment attribute * fix nullable * fix
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
|||||||
dataTypeDataToDataType,
|
dataTypeDataToDataType,
|
||||||
sortedDataTypeMap,
|
sortedDataTypeMap,
|
||||||
supportsArrayDataType,
|
supportsArrayDataType,
|
||||||
|
autoIncrementAlwaysOn,
|
||||||
|
requiresNotNull,
|
||||||
} from '@/lib/data/data-types/data-types';
|
} from '@/lib/data/data-types/data-types';
|
||||||
import { generateDBFieldSuffix } from '@/lib/domain/db-field';
|
import { generateDBFieldSuffix } from '@/lib/domain/db-field';
|
||||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
||||||
@@ -224,12 +226,17 @@ export const useUpdateTableField = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newTypeName = dataType?.name ?? (value as string);
|
||||||
|
const typeRequiresNotNull = requiresNotNull(newTypeName);
|
||||||
|
const shouldForceIncrement = autoIncrementAlwaysOn(newTypeName);
|
||||||
|
|
||||||
updateField(table.id, field.id, {
|
updateField(table.id, field.id, {
|
||||||
characterMaximumLength,
|
characterMaximumLength,
|
||||||
precision,
|
precision,
|
||||||
scale,
|
scale,
|
||||||
isArray,
|
isArray,
|
||||||
increment: undefined,
|
...(typeRequiresNotNull ? { nullable: false } : {}),
|
||||||
|
increment: shouldForceIncrement ? true : undefined,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
type: dataTypeDataToDataType(
|
type: dataTypeDataToDataType(
|
||||||
dataType ?? {
|
dataType ?? {
|
||||||
@@ -267,9 +274,16 @@ export const useUpdateTableField = (
|
|||||||
const debouncedNullableUpdate = useDebounce(
|
const debouncedNullableUpdate = useDebounce(
|
||||||
useCallback(
|
useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
updateField(table.id, field.id, { nullable: value });
|
const updates: Partial<DBField> = { nullable: value };
|
||||||
|
|
||||||
|
// If setting to nullable, clear increment (auto-increment requires NOT NULL)
|
||||||
|
if (value && field.increment) {
|
||||||
|
updates.increment = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateField(table.id, field.id, updates);
|
||||||
},
|
},
|
||||||
[updateField, table.id, field.id]
|
[updateField, table.id, field.id, field.increment]
|
||||||
),
|
),
|
||||||
100 // 100ms debounce for toggle
|
100 // 100ms debounce for toggle
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -167,6 +167,18 @@ export const supportsAutoIncrementDataType = (
|
|||||||
].includes(dataTypeName.toLocaleLowerCase());
|
].includes(dataTypeName.toLocaleLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const autoIncrementAlwaysOn = (dataTypeName: string): boolean => {
|
||||||
|
return ['serial', 'bigserial', 'smallserial'].includes(
|
||||||
|
dataTypeName.toLowerCase()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requiresNotNull = (dataTypeName: string): boolean => {
|
||||||
|
return ['serial', 'bigserial', 'smallserial'].includes(
|
||||||
|
dataTypeName.toLowerCase()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ARRAY_INCOMPATIBLE_TYPES = [
|
const ARRAY_INCOMPATIBLE_TYPES = [
|
||||||
'serial',
|
'serial',
|
||||||
'bigserial',
|
'bigserial',
|
||||||
|
|||||||
@@ -395,10 +395,27 @@ export const exportBaseSQL = ({
|
|||||||
sqlScript += ` UNIQUE`;
|
sqlScript += ` UNIQUE`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle AUTO INCREMENT - add as a comment for AI to process
|
// Handle AUTO INCREMENT
|
||||||
if (field.increment) {
|
if (field.increment) {
|
||||||
|
if (isDBMLFlow) {
|
||||||
|
// For DBML flow, generate proper database-specific syntax
|
||||||
|
if (
|
||||||
|
targetDatabaseType === DatabaseType.MYSQL ||
|
||||||
|
targetDatabaseType === DatabaseType.MARIADB
|
||||||
|
) {
|
||||||
|
sqlScript += ` AUTO_INCREMENT`;
|
||||||
|
} else if (targetDatabaseType === DatabaseType.SQL_SERVER) {
|
||||||
|
sqlScript += ` IDENTITY(1,1)`;
|
||||||
|
} else if (targetDatabaseType === DatabaseType.SQLITE) {
|
||||||
|
// SQLite AUTOINCREMENT only works with INTEGER PRIMARY KEY
|
||||||
|
// Will be handled when PRIMARY KEY is added
|
||||||
|
}
|
||||||
|
// PostgreSQL/CockroachDB: increment attribute added by restoreIncrementAttribute in DBML export
|
||||||
|
} else {
|
||||||
|
// For non-DBML flow, add as a comment for AI to process
|
||||||
sqlScript += ` /* AUTO_INCREMENT */`;
|
sqlScript += ` /* AUTO_INCREMENT */`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle DEFAULT value
|
// Handle DEFAULT value
|
||||||
if (field.default && !field.increment) {
|
if (field.default && !field.increment) {
|
||||||
@@ -450,6 +467,17 @@ export const exportBaseSQL = ({
|
|||||||
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
|
const pkIndex = table.indexes.find((idx) => idx.isPrimaryKey);
|
||||||
if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) {
|
if (field.primaryKey && !hasCompositePrimaryKey && !pkIndex?.name) {
|
||||||
sqlScript += ' PRIMARY KEY';
|
sqlScript += ' PRIMARY KEY';
|
||||||
|
|
||||||
|
// For SQLite with DBML flow, add AUTOINCREMENT after PRIMARY KEY
|
||||||
|
if (
|
||||||
|
isDBMLFlow &&
|
||||||
|
field.increment &&
|
||||||
|
targetDatabaseType === DatabaseType.SQLITE &&
|
||||||
|
(typeName.toLowerCase() === 'integer' ||
|
||||||
|
typeName.toLowerCase() === 'int')
|
||||||
|
) {
|
||||||
|
sqlScript += ' AUTOINCREMENT';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a comma after each field except the last one (or before PK constraint)
|
// Add a comma after each field except the last one (or before PK constraint)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Table "public"."orders" {
|
Table "public"."orders" {
|
||||||
"order_id" integer [pk, not null]
|
"order_id" integer [pk, not null, increment]
|
||||||
"customer_id" integer [not null]
|
"customer_id" integer [not null]
|
||||||
"order_date" date [not null, default: `CURRENT_DATE`]
|
"order_date" date [not null, default: `CURRENT_DATE`]
|
||||||
"total_amount" numeric [not null, default: 0]
|
"total_amount" numeric [not null, default: 0]
|
||||||
|
|||||||
14
src/lib/dbml/dbml-export/__tests__/cases/6.dbml
Normal file
14
src/lib/dbml/dbml-export/__tests__/cases/6.dbml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Table "users" {
|
||||||
|
"id" integer [pk, not null, increment]
|
||||||
|
"username" varchar(100) [unique, not null]
|
||||||
|
"email" varchar(255) [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
Table "posts" {
|
||||||
|
"post_id" bigint [pk, not null, increment]
|
||||||
|
"user_id" integer [not null]
|
||||||
|
"title" varchar(200) [not null]
|
||||||
|
"order_num" integer [not null, increment]
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref "fk_0_fk_posts_users":"users"."id" < "posts"."user_id"
|
||||||
1
src/lib/dbml/dbml-export/__tests__/cases/6.json
Normal file
1
src/lib/dbml/dbml-export/__tests__/cases/6.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"id":"test_auto_increment","name":"Auto Increment Test (mysql)","createdAt":"2025-01-20T00:00:00.000Z","updatedAt":"2025-01-20T00:00:00.000Z","databaseType":"mysql","tables":[{"id":"table1","name":"users","order":1,"fields":[{"id":"field1","name":"id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":true,"unique":false,"default":"","increment":true,"createdAt":1705708800000},{"id":"field2","name":"username","type":{"id":"varchar","name":"varchar","fieldAttributes":{"hasCharMaxLength":true}},"nullable":false,"primaryKey":false,"unique":true,"default":"","increment":false,"characterMaximumLength":"100","createdAt":1705708800000},{"id":"field3","name":"email","type":{"id":"varchar","name":"varchar","fieldAttributes":{"hasCharMaxLength":true}},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":false,"characterMaximumLength":"255","createdAt":1705708800000}],"indexes":[],"x":100,"y":100,"color":"#8eb7ff","isView":false,"createdAt":1705708800000},{"id":"table2","name":"posts","order":2,"fields":[{"id":"field4","name":"post_id","type":{"id":"bigint","name":"bigint"},"nullable":false,"primaryKey":true,"unique":false,"default":"","increment":true,"createdAt":1705708800000},{"id":"field5","name":"user_id","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":false,"createdAt":1705708800000},{"id":"field6","name":"title","type":{"id":"varchar","name":"varchar","fieldAttributes":{"hasCharMaxLength":true}},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":false,"characterMaximumLength":"200","createdAt":1705708800000},{"id":"field7","name":"order_num","type":{"id":"integer","name":"integer"},"nullable":false,"primaryKey":false,"unique":false,"default":"","increment":true,"createdAt":1705708800000}],"indexes":[],"x":300,"y":100,"color":"#8eb7ff","isView":false,"createdAt":1705708800000}],"relationships":[{"id":"rel1","name":"fk_posts_users","sourceTableId":"table2","targetTableId":"table1","sourceFieldId":"field5","targetFieldId":"field1","type":"one_to_many","sourceCardinality":"many","targetCardinality":"one","createdAt":1705708800000}],"dependencies":[],"storageMode":"project","areas":[],"creationMethod":"manual","customTypes":[]}
|
||||||
@@ -66,4 +66,12 @@ describe('DBML Export cases', () => {
|
|||||||
it('should handle case 5 diagram', { timeout: 30000 }, async () => {
|
it('should handle case 5 diagram', { timeout: 30000 }, async () => {
|
||||||
testCase('5');
|
testCase('5');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
'should handle case 6 diagram - auto increment',
|
||||||
|
{ timeout: 30000 },
|
||||||
|
async () => {
|
||||||
|
testCase('6');
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -583,6 +583,54 @@ const fixMultilineTableNames = (dbml: string): string => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Restore increment attribute for auto-incrementing fields
|
||||||
|
const restoreIncrementAttribute = (dbml: string, tables: DBTable[]): string => {
|
||||||
|
if (!tables || tables.length === 0) return dbml;
|
||||||
|
|
||||||
|
let result = dbml;
|
||||||
|
|
||||||
|
tables.forEach((table) => {
|
||||||
|
// Find fields with increment=true
|
||||||
|
const incrementFields = table.fields.filter((f) => f.increment);
|
||||||
|
|
||||||
|
incrementFields.forEach((field) => {
|
||||||
|
// Build the table identifier pattern
|
||||||
|
const tableIdentifier = table.schema
|
||||||
|
? `"${table.schema.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"\\."${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`
|
||||||
|
: `"${table.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`;
|
||||||
|
|
||||||
|
// Escape field name for regex
|
||||||
|
const escapedFieldName = field.name.replace(
|
||||||
|
/[.*+?^${}()|[\]\\]/g,
|
||||||
|
'\\$&'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pattern to match the field line with existing attributes in brackets
|
||||||
|
// Matches: "field_name" type [existing, attributes]
|
||||||
|
const fieldPattern = new RegExp(
|
||||||
|
`(Table ${tableIdentifier} \\{[^}]*?^\\s*"${escapedFieldName}"[^\\[\\n]+)(\\[[^\\]]*\\])`,
|
||||||
|
'gms'
|
||||||
|
);
|
||||||
|
|
||||||
|
result = result.replace(
|
||||||
|
fieldPattern,
|
||||||
|
(match, fieldPart, brackets) => {
|
||||||
|
// Check if increment already exists
|
||||||
|
if (brackets.includes('increment')) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add increment to the attributes
|
||||||
|
const newBrackets = brackets.replace(']', ', increment]');
|
||||||
|
return fieldPart + newBrackets;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
// Restore composite primary key names in the DBML
|
// Restore composite primary key names in the DBML
|
||||||
const restoreCompositePKNames = (dbml: string, tables: DBTable[]): string => {
|
const restoreCompositePKNames = (dbml: string, tables: DBTable[]): string => {
|
||||||
if (!tables || tables.length === 0) return dbml;
|
if (!tables || tables.length === 0) return dbml;
|
||||||
@@ -888,6 +936,9 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
|||||||
// Restore composite primary key names
|
// Restore composite primary key names
|
||||||
standard = restoreCompositePKNames(standard, uniqueTables);
|
standard = restoreCompositePKNames(standard, uniqueTables);
|
||||||
|
|
||||||
|
// Restore increment attribute for auto-incrementing fields
|
||||||
|
standard = restoreIncrementAttribute(standard, uniqueTables);
|
||||||
|
|
||||||
// Prepend Enum DBML to the standard output
|
// Prepend Enum DBML to the standard output
|
||||||
if (enumsDBML) {
|
if (enumsDBML) {
|
||||||
standard = enumsDBML + '\n\n' + standard;
|
standard = enumsDBML + '\n\n' + standard;
|
||||||
|
|||||||
@@ -342,4 +342,85 @@ describe('DBML Import cases', () => {
|
|||||||
);
|
);
|
||||||
expect(createdAtField?.default).toBe('now()');
|
expect(createdAtField?.default).toBe('now()');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle auto-increment fields correctly', async () => {
|
||||||
|
const dbmlContent = `Table "public"."table_1" {
|
||||||
|
"id" integer [pk, not null, increment]
|
||||||
|
"field_2" bigint [increment]
|
||||||
|
"field_3" serial [increment]
|
||||||
|
"field_4" varchar(100) [not null]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const result = await importDBMLToDiagram(dbmlContent, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.tables).toHaveLength(1);
|
||||||
|
const table = result.tables![0];
|
||||||
|
expect(table.name).toBe('table_1');
|
||||||
|
expect(table.fields).toHaveLength(4);
|
||||||
|
|
||||||
|
// field with [pk, not null, increment] - should be not null and increment
|
||||||
|
const idField = table.fields.find((f) => f.name === 'id');
|
||||||
|
expect(idField?.increment).toBe(true);
|
||||||
|
expect(idField?.nullable).toBe(false);
|
||||||
|
expect(idField?.primaryKey).toBe(true);
|
||||||
|
|
||||||
|
// field with [increment] only - should be not null and increment
|
||||||
|
// (auto-increment requires NOT NULL even if not explicitly stated)
|
||||||
|
const field2 = table.fields.find((f) => f.name === 'field_2');
|
||||||
|
expect(field2?.increment).toBe(true);
|
||||||
|
expect(field2?.nullable).toBe(false); // CRITICAL: must be false!
|
||||||
|
|
||||||
|
// SERIAL type with [increment] - should be not null and increment
|
||||||
|
const field3 = table.fields.find((f) => f.name === 'field_3');
|
||||||
|
expect(field3?.increment).toBe(true);
|
||||||
|
expect(field3?.nullable).toBe(false);
|
||||||
|
expect(field3?.type?.name).toBe('serial');
|
||||||
|
|
||||||
|
// Regular field with [not null] - should be not null, no increment
|
||||||
|
const field4 = table.fields.find((f) => f.name === 'field_4');
|
||||||
|
expect(field4?.increment).toBeUndefined();
|
||||||
|
expect(field4?.nullable).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle SERIAL types without increment attribute', async () => {
|
||||||
|
const dbmlContent = `Table "public"."test_table" {
|
||||||
|
"id" serial [pk]
|
||||||
|
"counter" bigserial
|
||||||
|
"small_counter" smallserial
|
||||||
|
"regular" integer
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const result = await importDBMLToDiagram(dbmlContent, {
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.tables).toHaveLength(1);
|
||||||
|
const table = result.tables![0];
|
||||||
|
expect(table.fields).toHaveLength(4);
|
||||||
|
|
||||||
|
// SERIAL type without [increment] - should STILL be not null (type requires it)
|
||||||
|
const idField = table.fields.find((f) => f.name === 'id');
|
||||||
|
expect(idField?.type?.name).toBe('serial');
|
||||||
|
expect(idField?.nullable).toBe(false); // CRITICAL: Type requires NOT NULL
|
||||||
|
expect(idField?.primaryKey).toBe(true);
|
||||||
|
|
||||||
|
// BIGSERIAL without [increment] - should be not null
|
||||||
|
const counterField = table.fields.find((f) => f.name === 'counter');
|
||||||
|
expect(counterField?.type?.name).toBe('bigserial');
|
||||||
|
expect(counterField?.nullable).toBe(false); // CRITICAL: Type requires NOT NULL
|
||||||
|
|
||||||
|
// SMALLSERIAL without [increment] - should be not null
|
||||||
|
const smallCounterField = table.fields.find(
|
||||||
|
(f) => f.name === 'small_counter'
|
||||||
|
);
|
||||||
|
expect(smallCounterField?.type?.name).toBe('smallserial');
|
||||||
|
expect(smallCounterField?.nullable).toBe(false); // CRITICAL: Type requires NOT NULL
|
||||||
|
|
||||||
|
// Regular INTEGER - should be nullable by default
|
||||||
|
const regularField = table.fields.find((f) => f.name === 'regular');
|
||||||
|
expect(regularField?.type?.name).toBe('integer');
|
||||||
|
expect(regularField?.nullable).toBe(true); // No NOT NULL constraint
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import type { DBTable } from '@/lib/domain/db-table';
|
|||||||
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
|
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
||||||
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
|
import {
|
||||||
|
findDataTypeDataById,
|
||||||
|
requiresNotNull,
|
||||||
|
} from '@/lib/data/data-types/data-types';
|
||||||
import { defaultTableColor } from '@/lib/colors';
|
import { defaultTableColor } from '@/lib/colors';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type Field from '@dbml/core/types/model_structure/field';
|
import type Field from '@dbml/core/types/model_structure/field';
|
||||||
@@ -552,7 +555,10 @@ export const importDBMLToDiagram = async (
|
|||||||
...options,
|
...options,
|
||||||
enums: extractedData.enums,
|
enums: extractedData.enums,
|
||||||
}),
|
}),
|
||||||
nullable: !field.not_null,
|
nullable:
|
||||||
|
field.increment || requiresNotNull(field.type.type_name)
|
||||||
|
? false
|
||||||
|
: !field.not_null,
|
||||||
primaryKey: field.pk || false,
|
primaryKey: field.pk || false,
|
||||||
unique: field.unique || field.pk || false, // Primary keys are always unique
|
unique: field.unique || field.pk || false, // Primary keys are always unique
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { SelectBox } from '@/components/select-box/select-box';
|
import { SelectBox } from '@/components/select-box/select-box';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { TableFieldToggle } from './table-field-toggle';
|
import { TableFieldToggle } from './table-field-toggle';
|
||||||
|
import { requiresNotNull } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
export interface TableEditModeFieldProps {
|
export interface TableEditModeFieldProps {
|
||||||
table: DBTable;
|
table: DBTable;
|
||||||
@@ -41,6 +42,8 @@ export const TableEditModeField: React.FC<TableEditModeFieldProps> = React.memo(
|
|||||||
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const typeRequiresNotNull = requiresNotNull(field.type.name);
|
||||||
|
|
||||||
// Animate the highlight after mount if focused
|
// Animate the highlight after mount if focused
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
@@ -135,6 +138,7 @@ export const TableEditModeField: React.FC<TableEditModeFieldProps> = React.memo(
|
|||||||
<TableFieldToggle
|
<TableFieldToggle
|
||||||
pressed={nullable}
|
pressed={nullable}
|
||||||
onPressedChange={handleNullableToggle}
|
onPressedChange={handleNullableToggle}
|
||||||
|
disabled={typeRequiresNotNull}
|
||||||
>
|
>
|
||||||
N
|
N
|
||||||
</TableFieldToggle>
|
</TableFieldToggle>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
findDataTypeDataById,
|
findDataTypeDataById,
|
||||||
supportsAutoIncrementDataType,
|
supportsAutoIncrementDataType,
|
||||||
supportsArrayDataType,
|
supportsArrayDataType,
|
||||||
|
autoIncrementAlwaysOn,
|
||||||
} from '@/lib/data/data-types/data-types';
|
} from '@/lib/data/data-types/data-types';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -111,6 +112,18 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
|||||||
[field.type.name, databaseType]
|
[field.type.name, databaseType]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if this is a SERIAL-type that is inherently auto-incrementing
|
||||||
|
const forceAutoIncrement = useMemo(
|
||||||
|
() => autoIncrementAlwaysOn(field.type.name) && !localField.nullable,
|
||||||
|
[field.type.name, localField.nullable]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-increment is disabled if the field is nullable (auto-increment requires NOT NULL)
|
||||||
|
const isIncrementDisabled = useMemo(
|
||||||
|
() => localField.nullable || readonly || forceAutoIncrement,
|
||||||
|
[localField.nullable, readonly, forceAutoIncrement]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
@@ -166,10 +179,12 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={localField.increment ?? false}
|
checked={
|
||||||
disabled={
|
forceAutoIncrement
|
||||||
!localField.primaryKey || readonly
|
? true
|
||||||
|
: (localField.increment ?? false)
|
||||||
}
|
}
|
||||||
|
disabled={isIncrementDisabled}
|
||||||
onCheckedChange={(value) =>
|
onCheckedChange={(value) =>
|
||||||
setLocalField((current) => ({
|
setLocalField((current) => ({
|
||||||
...current,
|
...current,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { CSS } from '@dnd-kit/utilities';
|
|||||||
import { SelectBox } from '@/components/select-box/select-box';
|
import { SelectBox } from '@/components/select-box/select-box';
|
||||||
import { TableFieldPopover } from './table-field-modal/table-field-modal';
|
import { TableFieldPopover } from './table-field-modal/table-field-modal';
|
||||||
import type { DatabaseType, DBTable } from '@/lib/domain';
|
import type { DatabaseType, DBTable } from '@/lib/domain';
|
||||||
|
import { requiresNotNull } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
export interface TableFieldProps {
|
export interface TableFieldProps {
|
||||||
table: DBTable;
|
table: DBTable;
|
||||||
@@ -55,6 +56,8 @@ export const TableField: React.FC<TableFieldProps> = ({
|
|||||||
transition,
|
transition,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const typeRequiresNotNull = requiresNotNull(field.type.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-1 touch-none flex-row justify-between gap-2 p-1"
|
className="flex flex-1 touch-none flex-row justify-between gap-2 p-1"
|
||||||
@@ -130,7 +133,7 @@ export const TableField: React.FC<TableFieldProps> = ({
|
|||||||
<TableFieldToggle
|
<TableFieldToggle
|
||||||
pressed={nullable}
|
pressed={nullable}
|
||||||
onPressedChange={handleNullableToggle}
|
onPressedChange={handleNullableToggle}
|
||||||
disabled={readonly}
|
disabled={readonly || typeRequiresNotNull}
|
||||||
>
|
>
|
||||||
N
|
N
|
||||||
</TableFieldToggle>
|
</TableFieldToggle>
|
||||||
|
|||||||
Reference in New Issue
Block a user