Compare commits

...

3 Commits

Author SHA1 Message Date
Guy Ben-Aharon
a636985251 chore(main): release 1.17.0 2025-10-19 18:54:40 +03:00
Guy Ben-Aharon
62dec48572 fix: use flag for custom types (#951) 2025-10-19 18:54:08 +03:00
Jonathan Fishner
49328d8fbd fix: add support for arrays (#949)
* feat: add array field support with diff visualization

* some refactor

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-10-19 17:47:39 +03:00
35 changed files with 1321 additions and 169 deletions

View File

@@ -1,5 +1,27 @@
# Changelog
## [1.17.0](https://github.com/chartdb/chartdb/compare/v1.16.0...v1.17.0) (2025-10-19)
### Features
* create relationships on canvas modal ([#946](https://github.com/chartdb/chartdb/issues/946)) ([34475ad](https://github.com/chartdb/chartdb/commit/34475add32f11323589ef092ccf2a8e9152ff272))
### Bug Fixes
* add auto-increment field detection in smart-query import ([#935](https://github.com/chartdb/chartdb/issues/935)) ([57b3b87](https://github.com/chartdb/chartdb/commit/57b3b8777fd0a445abf0ba6603faab612d469d5c))
* add rels export dbml ([#937](https://github.com/chartdb/chartdb/issues/937)) ([c3c646b](https://github.com/chartdb/chartdb/commit/c3c646bf7cbb1328f4b2eb85c9a7e929f0fcd3b9))
* add support for arrays ([#949](https://github.com/chartdb/chartdb/issues/949)) ([49328d8](https://github.com/chartdb/chartdb/commit/49328d8fbd7786f6c0c04cd5605d43a24cbf10ea))
* add support for parsing default values in DBML ([#948](https://github.com/chartdb/chartdb/issues/948)) ([459698b](https://github.com/chartdb/chartdb/commit/459698b5d0a1ff23a3719c2e55e4ab2e2384c4fe))
* add timestampz and int as datatypes to postgres ([#940](https://github.com/chartdb/chartdb/issues/940)) ([b15bc94](https://github.com/chartdb/chartdb/commit/b15bc945acb96d7cb3832b3b1b607dfcaef9e5ca))
* auto-enter edit mode when creating new tables from canvas ([#943](https://github.com/chartdb/chartdb/issues/943)) ([bcd8aa9](https://github.com/chartdb/chartdb/commit/bcd8aa9378aa563f40a2b6802cc503be4c882356))
* dbml diff fields types preview ([#934](https://github.com/chartdb/chartdb/issues/934)) ([bb03309](https://github.com/chartdb/chartdb/commit/bb033091b1f64b888822be1423a80f16f5314f6b))
* exit table edit on area click ([#945](https://github.com/chartdb/chartdb/issues/945)) ([38fedce](https://github.com/chartdb/chartdb/commit/38fedcec0c10ea2b3f0b7fc92ca1f5ac9e540389))
* manipulate schema directly from the canvas ([#947](https://github.com/chartdb/chartdb/issues/947)) ([7ad0e77](https://github.com/chartdb/chartdb/commit/7ad0e7712de975a23b2a337dc0a4a7fb4b122bd1))
* prevent text input glitch when editing table field names ([#944](https://github.com/chartdb/chartdb/issues/944)) ([498655e](https://github.com/chartdb/chartdb/commit/498655e7b77e57eaf641ba86263ce1ef60b93e16))
* use flag for custom types ([#951](https://github.com/chartdb/chartdb/issues/951)) ([62dec48](https://github.com/chartdb/chartdb/commit/62dec4857211b705a8039691da1772263ea986fe))
## [1.16.0](https://github.com/chartdb/chartdb/compare/v1.15.1...v1.16.0) (2025-09-24)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.16.0",
"version": "1.17.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.16.0",
"version": "1.17.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.13.9",

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.16.0",
"version": "1.17.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -105,6 +105,11 @@ export interface DiffContext {
}: {
fieldId: string;
}) => { old: number; new: number } | null;
getFieldNewIsArray: ({
fieldId,
}: {
fieldId: string;
}) => { old: boolean; new: boolean } | null;
// relationship diff
checkIfNewRelationship: ({

View File

@@ -447,6 +447,30 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
[diffMap]
);
const getFieldNewIsArray = useCallback<DiffContext['getFieldNewIsArray']>(
({ fieldId }) => {
const fieldKey = getDiffMapKey({
diffObject: 'field',
objectId: fieldId,
attribute: 'isArray',
});
if (diffMap.has(fieldKey)) {
const diff = diffMap.get(fieldKey);
if (diff?.type === 'changed') {
return {
old: diff.oldValue as boolean,
new: diff.newValue as boolean,
};
}
}
return null;
},
[diffMap]
);
const checkIfNewRelationship = useCallback<
DiffContext['checkIfNewRelationship']
>(
@@ -520,6 +544,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
getFieldNewCharacterMaximumLength,
getFieldNewScale,
getFieldNewPrecision,
getFieldNewIsArray,
// relationship diff
checkIfNewRelationship,

View File

@@ -140,7 +140,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
if (importMethod === 'dbml') {
// Validate DBML by parsing it
const validateResponse = verifyDBML(scriptResult);
const validateResponse = verifyDBML(scriptResult, { databaseType });
if (!validateResponse.hasError) {
setErrorMessage('');
setSqlValidation({

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import { useChartDB } from './use-chartdb';
import { useDebounce } from './use-debounce-v2';
import type { DBField, DBTable } from '@/lib/domain';
import type { DatabaseType, DBField, DBTable } from '@/lib/domain';
import type {
SelectBoxOption,
SelectBoxProps,
@@ -9,49 +9,60 @@ import type {
import {
dataTypeDataToDataType,
sortedDataTypeMap,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types';
import { generateDBFieldSuffix } from '@/lib/domain/db-field';
import type { DataTypeData } from '@/lib/data/data-types/data-types';
const generateFieldRegexPatterns = (
dataType: DataTypeData
dataType: DataTypeData,
databaseType: DatabaseType
): {
regex?: string;
extractRegex?: RegExp;
} => {
const typeName = dataType.name;
const supportsArrays = supportsArrayDataType(dataType.id, databaseType);
const arrayPattern = supportsArrays ? '(\\[\\])?' : '';
if (!dataType.fieldAttributes) {
return { regex: undefined, extractRegex: undefined };
// For types without field attributes, support plain type + optional array notation
return {
regex: `^${typeName}${arrayPattern}$`,
extractRegex: new RegExp(`^${typeName}${arrayPattern}$`),
};
}
const typeName = dataType.name;
const fieldAttributes = dataType.fieldAttributes;
if (fieldAttributes.hasCharMaxLength) {
if (fieldAttributes.hasCharMaxLengthOption) {
return {
regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)$`,
extractRegex: /\((\d+|max)\)/i,
regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)${arrayPattern}$`,
extractRegex: supportsArrays
? /\((\d+|max)\)(\[\])?/i
: /\((\d+|max)\)/i,
};
}
return {
regex: `^${typeName}\\(\\d+\\)$`,
extractRegex: /\((\d+)\)/,
regex: `^${typeName}\\(\\d+\\)${arrayPattern}$`,
extractRegex: supportsArrays ? /\((\d+)\)(\[\])?/ : /\((\d+)\)/,
};
}
if (fieldAttributes.precision && fieldAttributes.scale) {
return {
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)$`,
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)${arrayPattern}$`,
extractRegex: new RegExp(
`${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)`
`${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)${arrayPattern}`
),
};
}
if (fieldAttributes.precision) {
return {
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)$`,
extractRegex: /\((\d+)\)/,
regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)${arrayPattern}$`,
extractRegex: supportsArrays ? /\((\d+)\)(\[\])?/ : /\((\d+)\)/,
};
}
@@ -118,7 +129,10 @@ export const useUpdateTableField = (
const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
databaseType
].map((type) => {
const regexPatterns = generateFieldRegexPatterns(type);
const regexPatterns = generateFieldRegexPatterns(
type,
databaseType
);
return {
label: type.name,
@@ -162,8 +176,13 @@ export const useUpdateTableField = (
let characterMaximumLength: string | undefined = undefined;
let precision: number | undefined = undefined;
let scale: number | undefined = undefined;
let isArray: boolean | undefined = undefined;
if (regexMatches?.length) {
// Check if the last captured group is the array indicator []
const lastMatch = regexMatches[regexMatches.length - 1];
const hasArrayIndicator = lastMatch === '[]';
if (dataType?.fieldAttributes?.hasCharMaxLength) {
characterMaximumLength = regexMatches[1]?.toLowerCase();
} else if (
@@ -177,6 +196,17 @@ export const useUpdateTableField = (
} else if (dataType?.fieldAttributes?.precision) {
precision = parseInt(regexMatches[1]);
}
// Set isArray if the array indicator was found and the type supports arrays
if (hasArrayIndicator) {
const typeId = value as string;
if (supportsArrayDataType(typeId, databaseType)) {
isArray = true;
}
} else {
// Explicitly set to false/undefined if no array indicator
isArray = undefined;
}
} else {
if (
dataType?.fieldAttributes?.hasCharMaxLength &&
@@ -198,6 +228,7 @@ export const useUpdateTableField = (
characterMaximumLength,
precision,
scale,
isArray,
increment: undefined,
default: undefined,
type: dataTypeDataToDataType(
@@ -299,11 +330,17 @@ export const useUpdateTableField = (
// Utility function to generate field suffix for display
const generateFieldSuffix = useCallback(
(typeId?: string) => {
return generateDBFieldSuffix(field, {
databaseType,
forceExtended: true,
typeId,
});
return generateDBFieldSuffix(
{
...field,
isArray: field.isArray && typeId === field.type.id,
},
{
databaseType,
forceExtended: true,
typeId,
}
);
},
[field, databaseType]
);

View File

@@ -129,9 +129,6 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [
{ name: 'enum', id: 'enum' },
{ name: 'lowcardinality', id: 'lowcardinality' },
// Array Type
{ name: 'array', id: 'array' },
// Tuple Type
{ name: 'tuple', id: 'tuple' },
{ name: 'map', id: 'map' },

View File

@@ -1,5 +1,6 @@
import { z } from 'zod';
import { DatabaseType } from '../../domain/database-type';
import { databaseSupportsArrays } from '../../domain/database-capabilities';
import { clickhouseDataTypes } from './clickhouse-data-types';
import { genericDataTypes } from './generic-data-types';
import { mariadbDataTypes } from './mariadb-data-types';
@@ -165,3 +166,22 @@ export const supportsAutoIncrementDataType = (
'decimal',
].includes(dataTypeName.toLocaleLowerCase());
};
const ARRAY_INCOMPATIBLE_TYPES = [
'serial',
'bigserial',
'smallserial',
] as const;
export const supportsArrayDataType = (
dataTypeName: string,
databaseType: DatabaseType
): boolean => {
if (!databaseSupportsArrays(databaseType)) {
return false;
}
return !ARRAY_INCOMPATIBLE_TYPES.includes(
dataTypeName.toLowerCase() as (typeof ARRAY_INCOMPATIBLE_TYPES)[number]
);
};

View File

@@ -97,7 +97,6 @@ export const postgresDataTypes: readonly DataTypeData[] = [
{ name: 'tsvector', id: 'tsvector' },
{ name: 'tsquery', id: 'tsquery' },
{ name: 'xml', id: 'xml' },
{ name: 'array', id: 'array' },
{ name: 'int4range', id: 'int4range' },
{ name: 'int8range', id: 'int8range' },
{ name: 'numrange', id: 'numrange' },

View File

@@ -0,0 +1,356 @@
import { describe, it, expect } from 'vitest';
import { generateId } from '@/lib/utils';
import { exportBaseSQL } from '../export-sql-script';
import { DatabaseType } from '@/lib/domain/database-type';
import type { Diagram } from '@/lib/domain/diagram';
describe('SQL Export - Array Fields (Fantasy RPG Theme)', () => {
it('should export array fields for magical spell components', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Magical Spell System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: generateId(),
name: 'spells',
schema: '',
fields: [
{
id: generateId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: generateId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
characterMaximumLength: '200',
},
{
id: generateId(),
name: 'components',
type: { id: 'text', name: 'text' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
isArray: true,
comments: 'Magical components needed for the spell',
},
{
id: generateId(),
name: 'elemental_types',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
characterMaximumLength: '50',
isArray: true,
comments:
'Elements involved: fire, water, earth, air',
},
],
indexes: [],
x: 0,
y: 0,
color: '#3b82f6',
isView: false,
createdAt: Date.now(),
order: 0,
},
],
relationships: [],
createdAt: new Date(),
updatedAt: new Date(),
};
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
expect(sql).toContain('CREATE TABLE "spells"');
expect(sql).toContain('"components" text[]');
expect(sql).toContain('"elemental_types" varchar(50)[]');
});
it('should export array fields for hero inventory system', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'RPG Inventory System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: generateId(),
name: 'heroes',
schema: 'game',
fields: [
{
id: generateId(),
name: 'id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: generateId(),
name: 'name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
characterMaximumLength: '100',
},
{
id: generateId(),
name: 'abilities',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
characterMaximumLength: '100',
isArray: true,
comments:
'Special abilities like Stealth, Fireball, etc',
},
{
id: generateId(),
name: 'inventory_slots',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
isArray: true,
comments: 'Item IDs in inventory',
},
{
id: generateId(),
name: 'skill_levels',
type: { id: 'decimal', name: 'decimal' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
precision: 5,
scale: 2,
isArray: true,
comments: 'Skill proficiency levels',
},
],
indexes: [],
x: 0,
y: 0,
color: '#ef4444',
isView: false,
createdAt: Date.now(),
order: 0,
},
],
relationships: [],
createdAt: new Date(),
updatedAt: new Date(),
};
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
expect(sql).toContain('CREATE TABLE "game"."heroes"');
expect(sql).toContain('"abilities" varchar(100)[]');
expect(sql).toContain('"inventory_slots" integer[]');
expect(sql).toContain('"skill_levels" decimal(5, 2)[]');
});
it('should export non-array fields normally when isArray is false or undefined', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Quest System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: generateId(),
name: 'quests',
schema: '',
fields: [
{
id: generateId(),
name: 'id',
type: { id: 'uuid', name: 'uuid' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: generateId(),
name: 'title',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
characterMaximumLength: '200',
isArray: false,
},
{
id: generateId(),
name: 'description',
type: { id: 'text', name: 'text' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
// isArray is undefined - should not be treated as array
},
],
indexes: [],
x: 0,
y: 0,
color: '#8b5cf6',
isView: false,
createdAt: Date.now(),
order: 0,
},
],
relationships: [],
createdAt: new Date(),
updatedAt: new Date(),
};
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
expect(sql).toContain('"title" varchar(200)');
expect(sql).not.toContain('"title" varchar(200)[]');
expect(sql).toContain('"description" text');
expect(sql).not.toContain('"description" text[]');
});
it('should handle mixed array and non-array fields in magical creatures table', () => {
const diagram: Diagram = {
id: 'test-diagram',
name: 'Bestiary System',
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: generateId(),
name: 'magical_creatures',
schema: 'bestiary',
fields: [
{
id: generateId(),
name: 'id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: generateId(),
name: 'species_name',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
characterMaximumLength: '100',
},
{
id: generateId(),
name: 'habitats',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
characterMaximumLength: '80',
isArray: true,
comments:
'Preferred habitats: forest, mountain, swamp',
},
{
id: generateId(),
name: 'danger_level',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
default: '1',
},
{
id: generateId(),
name: 'resistances',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
characterMaximumLength: '50',
isArray: true,
comments: 'Damage resistances',
},
{
id: generateId(),
name: 'is_tameable',
type: { id: 'boolean', name: 'boolean' },
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
default: 'false',
},
],
indexes: [],
x: 0,
y: 0,
color: '#10b981',
isView: false,
createdAt: Date.now(),
order: 0,
},
],
relationships: [],
createdAt: new Date(),
updatedAt: new Date(),
};
const sql = exportBaseSQL({
diagram,
targetDatabaseType: DatabaseType.POSTGRESQL,
isDBMLFlow: true,
});
expect(sql).toContain('CREATE TABLE "bestiary"."magical_creatures"');
expect(sql).toContain('"species_name" varchar(100)');
expect(sql).not.toContain('"species_name" varchar(100)[]');
expect(sql).toContain('"habitats" varchar(80)[]');
expect(sql).toContain('"danger_level" integer');
expect(sql).not.toContain('"danger_level" integer[]');
expect(sql).toContain('"resistances" varchar(50)[]');
expect(sql).toContain('"is_tameable" boolean');
expect(sql).not.toContain('"is_tameable" boolean[]');
});
});

View File

@@ -1,9 +1,6 @@
import type { Diagram } from '../../domain/diagram';
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
import {
DatabaseType,
databaseTypesWithCommentSupport,
} from '@/lib/domain/database-type';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table';
import { dataTypeMap, type DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
@@ -12,6 +9,10 @@ import { exportPostgreSQL } from './export-per-type/postgresql';
import { exportSQLite } from './export-per-type/sqlite';
import { exportMySQL } from './export-per-type/mysql';
import { escapeSQLComment } from './export-per-type/common';
import {
databaseTypesWithCommentSupport,
supportsCustomTypes,
} from '@/lib/domain/database-capabilities';
// Function to format default values with proper quoting
const formatDefaultValue = (value: string): string => {
@@ -200,10 +201,7 @@ export const exportBaseSQL = ({
// or if we rely on the DBML generator to create Enums separately (as currently done)
// For now, let's assume PostgreSQL-style for demonstration if isDBMLFlow is false.
// If isDBMLFlow is true, we let TableDBML.tsx handle Enum syntax directly.
if (
targetDatabaseType === DatabaseType.POSTGRESQL &&
!isDBMLFlow
) {
if (supportsCustomTypes(targetDatabaseType) && !isDBMLFlow) {
const enumValues = customType.values
.map((v) => `'${v.replace(/'/g, "''")}'`)
.join(', ');
@@ -216,10 +214,7 @@ export const exportBaseSQL = ({
) {
// For PostgreSQL, generate CREATE TYPE ... AS (...)
// This is crucial for composite types to be recognized by the DBML importer
if (
targetDatabaseType === DatabaseType.POSTGRESQL ||
isDBMLFlow
) {
if (supportsCustomTypes(targetDatabaseType) || isDBMLFlow) {
// Assume other DBs might not support this or DBML flow needs it
const compositeFields = customType.fields
.map((f) => `${f.field} ${simplifyDataType(f.type)}`)
@@ -234,13 +229,12 @@ export const exportBaseSQL = ({
(ct.kind === 'enum' &&
ct.values &&
ct.values.length > 0 &&
targetDatabaseType === DatabaseType.POSTGRESQL &&
supportsCustomTypes(targetDatabaseType) &&
!isDBMLFlow) ||
(ct.kind === 'composite' &&
ct.fields &&
ct.fields.length > 0 &&
(targetDatabaseType === DatabaseType.POSTGRESQL ||
isDBMLFlow))
(supportsCustomTypes(targetDatabaseType) || isDBMLFlow))
)
) {
sqlScript += '\n';
@@ -300,7 +294,7 @@ export const exportBaseSQL = ({
if (
customEnumType &&
targetDatabaseType === DatabaseType.POSTGRESQL &&
supportsCustomTypes(targetDatabaseType) &&
!isDBMLFlow
) {
typeName = customEnumType.schema
@@ -343,6 +337,7 @@ export const exportBaseSQL = ({
}
const quotedFieldName = getQuotedFieldName(field.name, isDBMLFlow);
sqlScript += ` ${quotedFieldName} ${typeName}`;
// Add size for character types
@@ -385,6 +380,11 @@ export const exportBaseSQL = ({
}
}
// Add array suffix if field is an array (after type size and precision)
if (field.isArray) {
sqlScript += '[]';
}
// Handle NOT NULL constraint
if (!field.nullable) {
sqlScript += ' NOT NULL';

View File

@@ -10,6 +10,7 @@ import { defaultTableColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
import { supportsCustomTypes } from '@/lib/domain/database-capabilities';
// Common interfaces for SQL entities
export interface SQLColumn {
@@ -663,7 +664,7 @@ export function convertToChartDBDiagram(
// Ensure integer types are preserved
mappedType = { id: 'integer', name: 'integer' };
} else if (
sourceDatabaseType === DatabaseType.POSTGRESQL &&
supportsCustomTypes(sourceDatabaseType) &&
parserResult.enums &&
parserResult.enums.some(
(e) => e.name.toLowerCase() === column.type.toLowerCase()

View File

@@ -0,0 +1,317 @@
import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { generateDBMLFromDiagram } from '../../dbml-export/dbml-export';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Array Fields - Fantasy RPG Theme', () => {
describe('Import - Spell and Magic Arrays', () => {
it('should import spell components as array fields', async () => {
const dbml = `
Table "magic"."spells" {
"id" uuid [pk, not null]
"name" varchar(200) [not null]
"level" integer [not null]
"components" text[] [note: 'Magical components: bat wing, dragon scale, phoenix feather']
"elemental_types" varchar(50)[] [note: 'Elements: fire, water, earth, air']
"mana_cost" integer [not null]
"created_at" timestamp [not null]
Indexes {
(name, level) [unique, name: "unique_spell"]
}
}
`;
const result = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(result.tables).toHaveLength(1);
const table = result.tables![0];
expect(table.name).toBe('spells');
expect(table.schema).toBe('magic');
// Find the array fields
const components = table.fields.find(
(f) => f.name === 'components'
);
const elementalTypes = table.fields.find(
(f) => f.name === 'elemental_types'
);
// Verify they are marked as arrays
expect(components).toBeDefined();
expect(components?.isArray).toBe(true);
expect(components?.type.name).toBe('text');
expect(elementalTypes).toBeDefined();
expect(elementalTypes?.isArray).toBe(true);
expect(elementalTypes?.type.name).toBe('varchar');
expect(elementalTypes?.characterMaximumLength).toBe('50');
// Verify non-array fields don't have isArray set
const idField = table.fields.find((f) => f.name === 'id');
expect(idField?.isArray).toBeUndefined();
});
it('should import hero inventory with various array types', async () => {
const dbml = `
Table "heroes" {
"id" bigint [pk]
"name" varchar(100) [not null]
"abilities" varchar(100)[]
"inventory_slots" integer[]
"skill_levels" decimal(5, 2)[]
"quest_log" text[]
}
`;
const result = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = result.tables![0];
const abilities = table.fields.find((f) => f.name === 'abilities');
expect(abilities?.isArray).toBe(true);
expect(abilities?.type.name).toBe('varchar');
expect(abilities?.characterMaximumLength).toBe('100');
const inventorySlots = table.fields.find(
(f) => f.name === 'inventory_slots'
);
expect(inventorySlots?.isArray).toBe(true);
expect(inventorySlots?.type.name).toBe('integer');
const skillLevels = table.fields.find(
(f) => f.name === 'skill_levels'
);
expect(skillLevels?.isArray).toBe(true);
expect(skillLevels?.type.name).toBe('decimal');
expect(skillLevels?.precision).toBe(5);
expect(skillLevels?.scale).toBe(2);
const questLog = table.fields.find((f) => f.name === 'quest_log');
expect(questLog?.isArray).toBe(true);
expect(questLog?.type.name).toBe('text');
});
it('should handle mixed array and non-array fields in creature table', async () => {
const dbml = `
Table "bestiary"."creatures" {
"id" uuid [pk]
"species_name" varchar(100) [not null]
"habitats" varchar(50)[]
"danger_level" integer [not null]
"resistances" varchar(50)[]
"is_tameable" boolean [not null]
}
`;
const result = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = result.tables![0];
// Non-array fields
const id = table.fields.find((f) => f.name === 'id');
expect(id?.isArray).toBeUndefined();
const speciesName = table.fields.find(
(f) => f.name === 'species_name'
);
expect(speciesName?.isArray).toBeUndefined();
const dangerLevel = table.fields.find(
(f) => f.name === 'danger_level'
);
expect(dangerLevel?.isArray).toBeUndefined();
// Array fields
const habitats = table.fields.find((f) => f.name === 'habitats');
expect(habitats?.isArray).toBe(true);
const resistances = table.fields.find(
(f) => f.name === 'resistances'
);
expect(resistances?.isArray).toBe(true);
});
});
describe('Round-trip - Quest and Adventure Arrays', () => {
it('should preserve quest rewards array through export and re-import', async () => {
const originalDbml = `
Table "adventures"."quests" {
"id" uuid [pk, not null]
"title" varchar(200) [not null]
"difficulty" varchar(20) [not null]
"reward_items" text[] [note: 'Legendary sword, enchanted armor, healing potion']
"required_skills" varchar(100)[]
"experience_points" integer [not null]
"gold_reward" decimal(10, 2) [not null]
"created_at" timestamp [not null]
Indexes {
(title, difficulty) [unique, name: "unique_quest"]
}
}
`;
// Import the DBML
const diagram = await importDBMLToDiagram(originalDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify array fields were imported correctly
const table = diagram.tables![0];
const rewardItems = table.fields.find(
(f) => f.name === 'reward_items'
);
const requiredSkills = table.fields.find(
(f) => f.name === 'required_skills'
);
expect(rewardItems?.isArray).toBe(true);
expect(requiredSkills?.isArray).toBe(true);
// Export back to DBML
const { standardDbml: exportedDbml } =
generateDBMLFromDiagram(diagram);
// Verify the exported DBML contains array syntax
expect(exportedDbml).toContain('text[]');
expect(exportedDbml).toContain('"reward_items" text[]');
expect(exportedDbml).toContain('"required_skills" varchar(100)[]');
// Re-import the exported DBML
const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify array fields are still marked as arrays
const reimportedTable = reimportedDiagram.tables![0];
const reimportedRewards = reimportedTable.fields.find(
(f) => f.name === 'reward_items'
);
const reimportedSkills = reimportedTable.fields.find(
(f) => f.name === 'required_skills'
);
expect(reimportedRewards?.isArray).toBe(true);
expect(reimportedSkills?.isArray).toBe(true);
});
it('should handle guild members with different array types in round-trip', async () => {
const originalDbml = `
Table "guilds"."members" {
"id" uuid [pk]
"name" varchar(100) [not null]
"class_specializations" varchar(50)[]
"completed_quest_ids" integer[]
"skill_ratings" decimal(3, 1)[]
"titles_earned" text[]
}
`;
// Import
const diagram = await importDBMLToDiagram(originalDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
// Export
const { standardDbml: exportedDbml } =
generateDBMLFromDiagram(diagram);
// Verify exported DBML has correct array syntax with types
expect(exportedDbml).toContain('varchar(50)[]');
expect(exportedDbml).toContain('integer[]');
expect(exportedDbml).toContain('decimal(3,1)[]');
expect(exportedDbml).toContain('text[]');
// Re-import
const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = reimportedDiagram.tables![0];
const classSpecs = table.fields.find(
(f) => f.name === 'class_specializations'
);
expect(classSpecs?.isArray).toBe(true);
expect(classSpecs?.characterMaximumLength).toBe('50');
const questIds = table.fields.find(
(f) => f.name === 'completed_quest_ids'
);
expect(questIds?.isArray).toBe(true);
const skillRatings = table.fields.find(
(f) => f.name === 'skill_ratings'
);
expect(skillRatings?.isArray).toBe(true);
expect(skillRatings?.precision).toBe(3);
expect(skillRatings?.scale).toBe(1);
const titles = table.fields.find((f) => f.name === 'titles_earned');
expect(titles?.isArray).toBe(true);
});
it('should preserve dungeon loot tables with mixed array and non-array fields', async () => {
const originalDbml = `
Table "dungeons"."loot_tables" {
"id" bigint [pk]
"dungeon_name" varchar(150) [not null]
"boss_name" varchar(100)
"common_drops" text[]
"rare_drops" text[]
"legendary_drops" text[]
"gold_range_min" integer [not null]
"gold_range_max" integer [not null]
"drop_rates" decimal(5, 2)[]
}
`;
// Import, export, and re-import
const diagram = await importDBMLToDiagram(originalDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const { standardDbml: exportedDbml } =
generateDBMLFromDiagram(diagram);
const reimportedDiagram = await importDBMLToDiagram(exportedDbml, {
databaseType: DatabaseType.POSTGRESQL,
});
const table = reimportedDiagram.tables![0];
// Verify non-array fields
expect(
table.fields.find((f) => f.name === 'id')?.isArray
).toBeUndefined();
expect(
table.fields.find((f) => f.name === 'dungeon_name')?.isArray
).toBeUndefined();
expect(
table.fields.find((f) => f.name === 'gold_range_min')?.isArray
).toBeUndefined();
// Verify array fields
expect(
table.fields.find((f) => f.name === 'common_drops')?.isArray
).toBe(true);
expect(
table.fields.find((f) => f.name === 'rare_drops')?.isArray
).toBe(true);
expect(
table.fields.find((f) => f.name === 'legendary_drops')?.isArray
).toBe(true);
expect(
table.fields.find((f) => f.name === 'drop_rates')?.isArray
).toBe(true);
});
});
});

View File

@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest';
import { importDBMLToDiagram } from '../dbml-import';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Import - Fantasy Examples', () => {
describe('Magical Academy System', () => {
@@ -149,7 +150,9 @@ Table ranks {
max_spell_level integer [not null]
}`;
const diagram = await importDBMLToDiagram(magicalAcademyDBML);
const diagram = await importDBMLToDiagram(magicalAcademyDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify tables
expect(diagram.tables).toHaveLength(8);
@@ -366,7 +369,9 @@ Note marketplace_note {
'This marketplace handles both standard purchases and barter trades'
}`;
const diagram = await importDBMLToDiagram(marketplaceDBML);
const diagram = await importDBMLToDiagram(marketplaceDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify tables
expect(diagram.tables).toHaveLength(7);
@@ -567,7 +572,9 @@ Note quest_system_note {
'Quest difficulty and status use enums that will be converted to varchar'
}`;
const diagram = await importDBMLToDiagram(questSystemDBML);
const diagram = await importDBMLToDiagram(questSystemDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify tables
expect(diagram.tables).toHaveLength(7);
@@ -657,7 +664,9 @@ Table projects {
priority enum // inline enum without values - will be converted to varchar
}`;
const diagram = await importDBMLToDiagram(dbmlWithEnums);
const diagram = await importDBMLToDiagram(dbmlWithEnums, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify customTypes are created for enums
expect(diagram.customTypes).toBeDefined();
@@ -744,7 +753,9 @@ Table orders {
status order_status [not null]
}`;
const diagram = await importDBMLToDiagram(dbmlWithEnumNotes);
const diagram = await importDBMLToDiagram(dbmlWithEnumNotes, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify enum is created
expect(diagram.customTypes).toHaveLength(1);
@@ -788,7 +799,9 @@ Table admin.users {
status admin.status
}`;
const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames);
const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify both enums are created
expect(diagram.customTypes).toHaveLength(2);
@@ -891,7 +904,9 @@ Note dragon_note {
'Dragons are very protective of their hoards!'
}`;
const diagram = await importDBMLToDiagram(edgeCaseDBML);
const diagram = await importDBMLToDiagram(edgeCaseDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify preprocessing worked
expect(diagram.tables).toHaveLength(2);
@@ -956,7 +971,9 @@ Note dragon_note {
it('should handle empty DBML gracefully', async () => {
const emptyDBML = '';
const diagram = await importDBMLToDiagram(emptyDBML);
const diagram = await importDBMLToDiagram(emptyDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(0);
expect(diagram.relationships).toHaveLength(0);
@@ -969,7 +986,9 @@ Note dragon_note {
/* Multi-line
comment */
`;
const diagram = await importDBMLToDiagram(commentOnlyDBML);
const diagram = await importDBMLToDiagram(commentOnlyDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(0);
expect(diagram.relationships).toHaveLength(0);
@@ -980,7 +999,9 @@ Note dragon_note {
Table empty_table {
id int
}`;
const diagram = await importDBMLToDiagram(minimalDBML);
const diagram = await importDBMLToDiagram(minimalDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(1);
expect(diagram.tables?.[0]?.fields).toHaveLength(1);
@@ -996,7 +1017,9 @@ Table "aa"."users" {
Table "bb"."users" {
id integer [primary key]
}`;
const diagram = await importDBMLToDiagram(dbml);
const diagram = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(2);
@@ -1071,7 +1094,9 @@ Table "public_3"."comments" {
id [unique, name: "public_3_index_1"]
}
}`;
const diagram = await importDBMLToDiagram(dbml);
const diagram = await importDBMLToDiagram(dbml, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify tables
expect(diagram.tables).toHaveLength(3);
@@ -1256,7 +1281,9 @@ Table products {
Note: 'This table stores product information'
}`;
const diagram = await importDBMLToDiagram(dbmlWithTableNote);
const diagram = await importDBMLToDiagram(dbmlWithTableNote, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(1);
const productsTable = diagram.tables?.[0];
@@ -1273,7 +1300,9 @@ Table orders {
total numeric(10,2) [note: 'Order total including tax']
}`;
const diagram = await importDBMLToDiagram(dbmlWithFieldNote);
const diagram = await importDBMLToDiagram(dbmlWithFieldNote, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(1);
const ordersTable = diagram.tables?.[0];

View File

@@ -5,6 +5,7 @@ import {
importDBMLToDiagram,
} from '../dbml-import';
import { Parser } from '@dbml/core';
import { DatabaseType } from '@/lib/domain/database-type';
describe('DBML Import', () => {
describe('preprocessDBML', () => {
@@ -22,7 +23,7 @@ TableGroup "Test Group" [color: #CA4243] {
Table posts {
id int
}`;
const result = preprocessDBML(dbml);
const { content: result } = preprocessDBML(dbml);
expect(result).not.toContain('TableGroup');
expect(result).toContain('Table users');
expect(result).toContain('Table posts');
@@ -37,20 +38,20 @@ Table users {
Note note_test {
'This is a note'
}`;
const result = preprocessDBML(dbml);
const { content: result } = preprocessDBML(dbml);
expect(result).not.toContain('Note');
expect(result).toContain('Table users');
});
it('should convert array types to text', () => {
it('should remove array syntax while preserving base type', () => {
const dbml = `
Table users {
tags text[]
domains varchar[]
}`;
const result = preprocessDBML(dbml);
const { content: result } = preprocessDBML(dbml);
expect(result).toContain('tags text');
expect(result).toContain('domains text');
expect(result).toContain('domains varchar');
expect(result).not.toContain('[]');
});
@@ -60,7 +61,7 @@ Table users {
status enum
verification_type enum // comment here
}`;
const result = preprocessDBML(dbml);
const { content: result } = preprocessDBML(dbml);
expect(result).toContain('status varchar');
expect(result).toContain('verification_type varchar');
expect(result).not.toContain('enum');
@@ -71,7 +72,7 @@ Table users {
Table users [headercolor: #24BAB1] {
id int
}`;
const result = preprocessDBML(dbml);
const { content: result } = preprocessDBML(dbml);
expect(result).toContain('Table users {');
expect(result).not.toContain('headercolor');
});
@@ -105,7 +106,9 @@ Note note_test {
'This is a test note'
}`;
const diagram = await importDBMLToDiagram(complexDBML);
const diagram = await importDBMLToDiagram(complexDBML, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(2);
expect(diagram.relationships).toHaveLength(1);
@@ -149,7 +152,7 @@ Note note_1750185617764 {
}`;
// Test that preprocessing handles all issues
const preprocessed = preprocessDBML(problematicDBML);
const { content: preprocessed } = preprocessDBML(problematicDBML);
const sanitized = sanitizeDBML(preprocessed);
// Should not throw

View File

@@ -38,7 +38,9 @@ Note test_note {
'This is a test note'
}`;
const diagram = await importDBMLToDiagram(dbmlContent);
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.POSTGRESQL,
});
// Verify basic structure
expect(diagram).toBeDefined();
@@ -96,7 +98,9 @@ Table products [headercolor: #FF0000] {
Ref: products.id < users.favorite_product_id`;
const diagram = await importDBMLToDiagram(dbmlContent);
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.POSTGRESQL,
});
expect(diagram.tables).toHaveLength(2);
@@ -119,12 +123,16 @@ Ref: products.id < users.favorite_product_id`;
it('should handle empty or invalid DBML gracefully', async () => {
// Empty DBML
const emptyDiagram = await importDBMLToDiagram('');
const emptyDiagram = await importDBMLToDiagram('', {
databaseType: DatabaseType.POSTGRESQL,
});
expect(emptyDiagram.tables).toHaveLength(0);
expect(emptyDiagram.relationships).toHaveLength(0);
// Only comments
const commentDiagram = await importDBMLToDiagram('// Just a comment');
const commentDiagram = await importDBMLToDiagram('// Just a comment', {
databaseType: DatabaseType.POSTGRESQL,
});
expect(commentDiagram.tables).toHaveLength(0);
expect(commentDiagram.relationships).toHaveLength(0);
});
@@ -133,7 +141,9 @@ Ref: products.id < users.favorite_product_id`;
const dbmlContent = `Table test {
id int [pk]
}`;
const diagram = await importDBMLToDiagram(dbmlContent);
const diagram = await importDBMLToDiagram(dbmlContent, {
databaseType: DatabaseType.GENERIC,
});
// Default values
expect(diagram.name).toBe('DBML Import');

View File

@@ -1,4 +1,6 @@
import type { CompilerError } from '@dbml/core/types/parse/error';
import type { DatabaseType } from '@/lib/domain/database-type';
import { databaseSupportsArrays } from '@/lib/domain/database-capabilities';
export interface DBMLError {
message: string;
@@ -6,8 +8,59 @@ export interface DBMLError {
column: number;
}
export class DBMLValidationError extends Error {
public readonly dbmlError: DBMLError;
constructor(message: string, line: number, column: number = 1) {
super(message);
this.name = 'DBMLValidationError';
this.dbmlError = { message, line, column };
}
}
export const getPositionFromIndex = (
content: string,
matchIndex: number
): { line: number; column: number } => {
const lines = content.substring(0, matchIndex).split('\n');
return {
line: lines.length,
column: lines[lines.length - 1].length + 1,
};
};
export const validateArrayTypesForDatabase = (
content: string,
databaseType: DatabaseType
): void => {
// Only validate if database doesn't support arrays
if (databaseSupportsArrays(databaseType)) {
return;
}
const arrayFieldPattern = /"?(\w+)"?\s+(\w+(?:\(\d+(?:,\s*\d+)?\))?)\[\]/g;
const matches = [...content.matchAll(arrayFieldPattern)];
for (const match of matches) {
const fieldName = match[1];
const dataType = match[2];
const { line, column } = getPositionFromIndex(content, match.index!);
throw new DBMLValidationError(
`Array types are not supported for ${databaseType} database. Field "${fieldName}" has array type "${dataType}[]" which is not allowed.`,
line,
column
);
}
};
export function parseDBMLError(error: unknown): DBMLError | null {
try {
// Check for our custom DBMLValidationError
if (error instanceof DBMLValidationError) {
return error.dbmlError;
}
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {

View File

@@ -14,13 +14,21 @@ import {
DBCustomTypeKind,
type DBCustomType,
} from '@/lib/domain/db-custom-type';
import { validateArrayTypesForDatabase } from './dbml-import-error';
export const defaultDBMLDiagramName = 'DBML Import';
// Preprocess DBML to handle unsupported features
export const preprocessDBML = (content: string): string => {
interface PreprocessDBMLResult {
content: string;
arrayFields: Map<string, Set<string>>;
}
export const preprocessDBML = (content: string): PreprocessDBMLResult => {
let processed = content;
// Track array fields found during preprocessing
const arrayFields = new Map<string, Set<string>>();
// Remove TableGroup blocks (not supported by parser)
processed = processed.replace(/TableGroup\s+[^{]*\{[^}]*\}/gs, '');
@@ -30,8 +38,37 @@ export const preprocessDBML = (content: string): string => {
// Don't remove enum definitions - we'll parse them
// processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
// Handle array types by converting them to text
processed = processed.replace(/(\w+)\[\]/g, 'text');
// Handle array types by tracking them and converting syntax for DBML parser
// Note: DBML doesn't officially support array syntax, so we convert type[] to type
// but track which fields should be arrays
// First, find all array field declarations and track them
const tablePattern =
/Table\s+(?:"([^"]+)"\.)?(?:"([^"]+)"|(\w+))\s*(?:\[[^\]]*\])?\s*\{([^}]+)\}/gs;
let match;
while ((match = tablePattern.exec(content)) !== null) {
const schema = match[1] || '';
const tableName = match[2] || match[3];
const tableBody = match[4];
const fullTableName = schema ? `${schema}.${tableName}` : tableName;
// Find array field declarations within this table
const fieldPattern = /"?(\w+)"?\s+(\w+(?:\([^)]+\))?)\[\]/g;
let fieldMatch;
while ((fieldMatch = fieldPattern.exec(tableBody)) !== null) {
const fieldName = fieldMatch[1];
if (!arrayFields.has(fullTableName)) {
arrayFields.set(fullTableName, new Set());
}
arrayFields.get(fullTableName)!.add(fieldName);
}
}
// Now convert array syntax for DBML parser (keep the base type, remove [])
processed = processed.replace(/(\w+(?:\(\d+(?:,\s*\d+)?\))?)\[\]/g, '$1');
// Handle inline enum types without values by converting to varchar
processed = processed.replace(
@@ -46,7 +83,7 @@ export const preprocessDBML = (content: string): string => {
'Table $1 {'
);
return processed;
return { content: processed, arrayFields };
};
// Simple function to replace Spanish special characters
@@ -85,6 +122,7 @@ interface DBMLField {
pk?: boolean;
not_null?: boolean;
increment?: boolean;
isArray?: boolean;
characterMaximumLength?: string | null;
precision?: number | null;
scale?: number | null;
@@ -190,8 +228,8 @@ const determineCardinality = (
export const importDBMLToDiagram = async (
dbmlContent: string,
options?: {
databaseType?: DatabaseType;
options: {
databaseType: DatabaseType;
}
): Promise<Diagram> => {
try {
@@ -208,9 +246,13 @@ export const importDBMLToDiagram = async (
};
}
// Validate array types BEFORE preprocessing (preprocessing removes [])
validateArrayTypesForDatabase(dbmlContent, options.databaseType);
const parser = new Parser();
// Preprocess and sanitize DBML content
const preprocessedContent = preprocessDBML(dbmlContent);
const { content: preprocessedContent, arrayFields } =
preprocessDBML(dbmlContent);
const sanitizedContent = sanitizeDBML(preprocessedContent);
// Handle content that becomes empty after preprocessing
@@ -344,11 +386,24 @@ export const importDBMLToDiagram = async (
const rawDefault = String(
field.dbdefault.value
);
// Remove ALL quotes (single, double, backticks) to clean the value
// The SQL export layer will handle adding proper quotes when needed
defaultValue = rawDefault.replace(/['"`]/g, '');
}
// Check if this field should be an array
const fullTableName = schemaName
? `${schemaName}.${table.name}`
: table.name;
let isArray = arrayFields
.get(fullTableName)
?.has(field.name);
if (!isArray && schemaName) {
isArray = arrayFields
.get(table.name)
?.has(field.name);
}
return {
name: field.name,
type: field.type,
@@ -356,6 +411,7 @@ export const importDBMLToDiagram = async (
pk: field.pk,
not_null: field.not_null,
increment: field.increment,
isArray: isArray || undefined,
note: field.note,
default: defaultValue,
...getFieldExtraAttributes(field, allEnums),
@@ -503,6 +559,8 @@ export const importDBMLToDiagram = async (
characterMaximumLength: field.characterMaximumLength,
precision: field.precision,
scale: field.scale,
...(field.increment ? { increment: field.increment } : {}),
...(field.isArray ? { isArray: field.isArray } : {}),
...(fieldComment ? { comments: fieldComment } : {}),
...(field.default ? { default: field.default } : {}),
};

View File

@@ -1,10 +1,19 @@
import { Parser } from '@dbml/core';
import { preprocessDBML, sanitizeDBML } from './dbml-import';
import type { DBMLError } from './dbml-import-error';
import { parseDBMLError } from './dbml-import-error';
import {
parseDBMLError,
validateArrayTypesForDatabase,
} from './dbml-import-error';
import type { DatabaseType } from '@/lib/domain/database-type';
export const verifyDBML = (
content: string
content: string,
{
databaseType,
}: {
databaseType: DatabaseType;
}
):
| {
hasError: true;
@@ -16,8 +25,12 @@ export const verifyDBML = (
hasError: false;
} => {
try {
const preprocessedContent = preprocessDBML(content);
// Validate array types BEFORE preprocessing (preprocessing removes [])
validateArrayTypesForDatabase(content, databaseType);
const { content: preprocessedContent } = preprocessDBML(content);
const sanitizedContent = sanitizeDBML(preprocessedContent);
const parser = new Parser();
parser.parse(sanitizedContent, 'dbmlv2');
} catch (e) {

View File

@@ -0,0 +1,57 @@
import { DatabaseType } from './database-type';
export interface DatabaseCapabilities {
supportsArrays?: boolean;
supportsCustomTypes?: boolean;
supportsSchemas?: boolean;
supportsComments?: boolean;
}
export const DATABASE_CAPABILITIES: Record<DatabaseType, DatabaseCapabilities> =
{
[DatabaseType.POSTGRESQL]: {
supportsArrays: true,
supportsCustomTypes: true,
supportsSchemas: true,
supportsComments: true,
},
[DatabaseType.COCKROACHDB]: {
supportsArrays: true,
supportsSchemas: true,
supportsComments: true,
},
[DatabaseType.MYSQL]: {},
[DatabaseType.MARIADB]: {},
[DatabaseType.SQL_SERVER]: {
supportsSchemas: true,
},
[DatabaseType.SQLITE]: {},
[DatabaseType.CLICKHOUSE]: {
supportsSchemas: true,
},
[DatabaseType.ORACLE]: {
supportsSchemas: true,
supportsComments: true,
},
[DatabaseType.GENERIC]: {},
};
export const getDatabaseCapabilities = (
databaseType: DatabaseType
): DatabaseCapabilities => {
return DATABASE_CAPABILITIES[databaseType];
};
export const databaseSupportsArrays = (databaseType: DatabaseType): boolean => {
return getDatabaseCapabilities(databaseType).supportsArrays ?? false;
};
export const databaseTypesWithCommentSupport: DatabaseType[] = Object.keys(
DATABASE_CAPABILITIES
).filter(
(dbType) => DATABASE_CAPABILITIES[dbType as DatabaseType].supportsComments
) as DatabaseType[];
export const supportsCustomTypes = (databaseType: DatabaseType): boolean => {
return getDatabaseCapabilities(databaseType).supportsCustomTypes ?? false;
};

View File

@@ -9,9 +9,3 @@ export enum DatabaseType {
COCKROACHDB = 'cockroachdb',
ORACLE = 'oracle',
}
export const databaseTypesWithCommentSupport: DatabaseType[] = [
DatabaseType.POSTGRESQL,
DatabaseType.COCKROACHDB,
DatabaseType.ORACLE,
];

View File

@@ -2,9 +2,10 @@ import { z } from 'zod';
import {
dataTypeSchema,
findDataTypeDataById,
supportsArrayDataType,
type DataType,
} from '../data/data-types/data-types';
import type { DatabaseType } from './database-type';
import { DatabaseType } from './database-type';
export interface DBField {
id: string;
@@ -14,6 +15,7 @@ export interface DBField {
unique: boolean;
nullable: boolean;
increment?: boolean | null;
isArray?: boolean | null;
createdAt: number;
characterMaximumLength?: string | null;
precision?: number | null;
@@ -31,6 +33,7 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
unique: z.boolean(),
nullable: z.boolean(),
increment: z.boolean().or(z.null()).optional(),
isArray: z.boolean().or(z.null()).optional(),
createdAt: z.number(),
characterMaximumLength: z.string().or(z.null()).optional(),
precision: z.number().or(z.null()).optional(),
@@ -52,11 +55,26 @@ export const generateDBFieldSuffix = (
typeId?: string;
} = {}
): string => {
let suffix = '';
if (databaseType && forceExtended && typeId) {
return generateExtendedSuffix(field, databaseType, typeId);
suffix = generateExtendedSuffix(field, databaseType, typeId);
} else {
suffix = generateStandardSuffix(field);
}
return generateStandardSuffix(field);
// Add array notation if field is an array
if (
field.isArray &&
supportsArrayDataType(
typeId ?? field.type.id,
databaseType ?? DatabaseType.GENERIC
)
) {
suffix += '[]';
}
return suffix;
};
const generateExtendedSuffix = (

View File

@@ -1,4 +1,5 @@
import { DatabaseType } from './database-type';
import { DATABASE_CAPABILITIES } from './database-capabilities';
import type { DatabaseType } from './database-type';
export interface DBSchema {
id: string;
@@ -18,10 +19,8 @@ export const schemaNameToDomainSchemaName = (
? undefined
: schema?.trim();
export const databasesWithSchemas: DatabaseType[] = [
DatabaseType.POSTGRESQL,
DatabaseType.SQL_SERVER,
DatabaseType.CLICKHOUSE,
DatabaseType.COCKROACHDB,
DatabaseType.ORACLE,
];
export const databasesWithSchemas: DatabaseType[] = Object.keys(
DATABASE_CAPABILITIES
).filter(
(dbType) => DATABASE_CAPABILITIES[dbType as DatabaseType].supportsSchemas
) as DatabaseType[];

View File

@@ -28,6 +28,16 @@ export function getDiffMapKey({
: `${diffObject}-${objectId}`;
}
const isOneOfDefined = (
...values: (string | number | boolean | undefined | null)[]
): boolean => {
return values.some((value) => value !== undefined && value !== null);
};
const normalizeBoolean = (value: boolean | undefined | null): boolean => {
return value === true;
};
export interface GenerateDiffOptions {
includeTables?: boolean;
includeFields?: boolean;
@@ -552,6 +562,8 @@ function compareFieldProperties({
'characterMaximumLength',
'scale',
'precision',
'increment',
'isArray',
];
const changedAttributes: FieldDiffAttribute[] = [];
@@ -620,6 +632,24 @@ function compareFieldProperties({
changedAttributes.push('precision');
}
if (
attributesToCheck.includes('increment') &&
isOneOfDefined(newField.increment, oldField.increment) &&
normalizeBoolean(oldField.increment) !==
normalizeBoolean(newField.increment)
) {
changedAttributes.push('increment');
}
if (
attributesToCheck.includes('isArray') &&
isOneOfDefined(newField.isArray, oldField.isArray) &&
normalizeBoolean(oldField.isArray) !==
normalizeBoolean(newField.isArray)
) {
changedAttributes.push('isArray');
}
if (changedAttributes.length > 0) {
for (const attribute of changedAttributes) {
diffMap.set(

View File

@@ -15,7 +15,9 @@ export type FieldDiffAttribute =
| 'comments'
| 'characterMaximumLength'
| 'precision'
| 'scale';
| 'scale'
| 'increment'
| 'isArray';
export const fieldDiffAttributeSchema: z.ZodType<FieldDiffAttribute> = z.union([
z.literal('name'),

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { KeyRound, Trash2 } from 'lucide-react';
import { Input } from '@/components/input/input';
import { generateDBFieldSuffix, type DBField } from '@/lib/domain/db-field';
import type { DBTable } from '@/lib/domain';
import type { DatabaseType, DBTable } from '@/lib/domain';
import { useUpdateTableField } from '@/hooks/use-update-table-field';
import {
Tooltip,
@@ -18,10 +18,11 @@ export interface TableEditModeFieldProps {
table: DBTable;
field: DBField;
focused?: boolean;
databaseType: DatabaseType;
}
export const TableEditModeField: React.FC<TableEditModeFieldProps> = React.memo(
({ table, field, focused = false }) => {
({ table, field, focused = false, databaseType }) => {
const { t } = useTranslation();
const [showHighlight, setShowHighlight] = React.useState(false);
@@ -102,7 +103,9 @@ export const TableEditModeField: React.FC<TableEditModeFieldProps> = React.memo(
'side_panel.tables_section.table.field_type'
)}
value={field.type.id}
valueSuffix={generateDBFieldSuffix(field)}
valueSuffix={generateDBFieldSuffix(field, {
databaseType,
})}
optionSuffix={(option) =>
generateFieldSuffix(option.value)
}
@@ -119,9 +122,9 @@ export const TableEditModeField: React.FC<TableEditModeFieldProps> = React.memo(
</TooltipTrigger>
<TooltipContent>
{field.type.name}
{field.characterMaximumLength
? `(${field.characterMaximumLength})`
: ''}
{generateDBFieldSuffix(field, {
databaseType,
})}
</TooltipContent>
</Tooltip>
</div>

View File

@@ -300,6 +300,7 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
table={table}
field={field}
focused={focusFieldId === field.id}
databaseType={databaseType}
/>
</div>
))}

View File

@@ -67,6 +67,7 @@ const arePropsEqual = (
nextProps.field.characterMaximumLength &&
prevProps.field.precision === nextProps.field.precision &&
prevProps.field.scale === nextProps.field.scale &&
prevProps.field.isArray === nextProps.field.isArray &&
prevProps.focused === nextProps.focused &&
prevProps.highlighted === nextProps.highlighted &&
prevProps.visible === nextProps.visible &&
@@ -77,7 +78,8 @@ const arePropsEqual = (
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
const { relationships, readonly, highlightedCustomType } = useChartDB();
const { relationships, readonly, highlightedCustomType, databaseType } =
useChartDB();
const updateNodeInternals = useUpdateNodeInternals();
const connection = useConnection();
@@ -152,6 +154,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
getFieldNewCharacterMaximumLength,
getFieldNewPrecision,
getFieldNewScale,
getFieldNewIsArray,
checkIfFieldHasChange,
isSummaryOnly,
} = useDiff();
@@ -170,6 +173,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
fieldDiffChangedPrimaryKey: ReturnType<
typeof getFieldNewPrimaryKey
>;
fieldDiffChangedIsArray: ReturnType<typeof getFieldNewIsArray>;
isDiffFieldChanged: boolean;
}>({
isDiffFieldRemoved: false,
@@ -181,6 +185,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
fieldDiffChangedScale: null,
fieldDiffChangedPrecision: null,
fieldDiffChangedPrimaryKey: null,
fieldDiffChangedIsArray: null,
isDiffFieldChanged: false,
});
@@ -214,6 +219,9 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
fieldDiffChangedPrecision: getFieldNewPrecision({
fieldId: field.id,
}),
fieldDiffChangedIsArray: getFieldNewIsArray({
fieldId: field.id,
}),
isDiffFieldChanged: checkIfFieldHasChange({
fieldId: field.id,
tableId: tableNodeId,
@@ -232,6 +240,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
getFieldNewCharacterMaximumLength,
getFieldNewPrecision,
getFieldNewScale,
getFieldNewIsArray,
field.id,
tableNodeId,
]);
@@ -247,8 +256,23 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
fieldDiffChangedCharacterMaximumLength,
fieldDiffChangedScale,
fieldDiffChangedPrecision,
fieldDiffChangedIsArray,
} = diffState;
const isFieldAttributeChanged = useMemo(() => {
return (
fieldDiffChangedCharacterMaximumLength ||
fieldDiffChangedScale ||
fieldDiffChangedPrecision ||
fieldDiffChangedIsArray
);
}, [
fieldDiffChangedCharacterMaximumLength,
fieldDiffChangedScale,
fieldDiffChangedPrecision,
fieldDiffChangedIsArray,
]);
const isCustomTypeHighlighted = useMemo(() => {
if (!highlightedCustomType) return false;
return field.type.name === highlightedCustomType.name;
@@ -342,17 +366,14 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
</>
)}
<div
className={cn(
'flex items-center gap-1 min-w-0 flex-1 text-left',
{
'font-semibold': field.primaryKey || field.unique,
}
)}
className={cn('flex items-center gap-1 min-w-0 text-left', {
'font-semibold': field.primaryKey || field.unique,
})}
>
{isDiffFieldRemoved ? (
<SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
<SquareMinus className="size-3.5 shrink-0 text-red-800 dark:text-red-200" />
) : isDiffNewField ? (
<SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
<SquarePlus className="size-3.5 shrink-0 text-green-800 dark:text-green-200" />
) : isDiffFieldChanged && !isSummaryOnly ? (
<SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
) : null}
@@ -392,13 +413,17 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
) : null}
</div>
<div className="ml-2 flex shrink-0 items-center justify-end gap-1.5">
<div
className={cn(
'ml-auto flex shrink-0 items-center gap-1 min-w-0',
!readonly ? 'group-hover:hidden' : ''
)}
>
{(field.primaryKey && !fieldDiffChangedPrimaryKey?.old) ||
fieldDiffChangedPrimaryKey?.new ? (
<div
className={cn(
'text-muted-foreground',
!readonly ? 'group-hover:hidden' : '',
'text-muted-foreground shrink-0',
isDiffFieldRemoved
? 'text-red-800 dark:text-red-200'
: '',
@@ -416,12 +441,9 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
<KeyRound size={14} />
</div>
) : null}
<div
className={cn(
'content-center text-right text-xs text-muted-foreground overflow-hidden max-w-[8rem]',
field.primaryKey ? 'min-w-0' : 'min-w-[3rem]',
!readonly ? 'group-hover:hidden' : '',
'text-right text-xs text-muted-foreground overflow-hidden min-w-0',
isDiffFieldRemoved
? 'text-red-800 dark:text-red-200'
: '',
@@ -437,39 +459,85 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
)}
>
<span className="block truncate">
{fieldDiffChangedType ? (
{isFieldAttributeChanged || fieldDiffChangedType ? (
<>
<span className="line-through">
{
fieldDiffChangedType.old.name.split(
' '
)[0]
(
fieldDiffChangedType?.old
?.name ?? field.type.name
).split(' ')[0]
}
{showFieldAttributes
? generateDBFieldSuffix(
{
...field,
...{
precision:
fieldDiffChangedPrecision?.old ??
field.precision,
scale:
fieldDiffChangedScale?.old ??
field.scale,
characterMaximumLength:
fieldDiffChangedCharacterMaximumLength?.old ??
field.characterMaximumLength,
isArray:
fieldDiffChangedIsArray?.old ??
field.isArray,
},
},
{
databaseType,
}
)
: field.isArray
? '[]'
: ''}
</span>{' '}
{
fieldDiffChangedType.new.name.split(
' '
)[0]
(
fieldDiffChangedType?.new?.name ??
field.type.name
).split(' ')[0]
}
{showFieldAttributes
? generateDBFieldSuffix(
{
...field,
...{
precision:
fieldDiffChangedPrecision?.new ??
field.precision,
scale:
fieldDiffChangedScale?.new ??
field.scale,
characterMaximumLength:
fieldDiffChangedCharacterMaximumLength?.new ??
field.characterMaximumLength,
isArray:
fieldDiffChangedIsArray?.new ??
field.isArray,
},
},
{
databaseType,
}
)
: (fieldDiffChangedIsArray?.new ??
field.isArray)
? '[]'
: ''}
</>
) : (
`${field.type.name.split(' ')[0]}${
showFieldAttributes
? generateDBFieldSuffix({
...field,
...{
precision:
fieldDiffChangedPrecision?.new ??
field.precision,
scale:
fieldDiffChangedScale?.new ??
field.scale,
characterMaximumLength:
fieldDiffChangedCharacterMaximumLength?.new ??
field.characterMaximumLength,
},
? generateDBFieldSuffix(field, {
databaseType,
})
: ''
: field.isArray
? '[]'
: ''
}`
)}
{fieldDiffChangedNullable ? (
@@ -485,21 +553,21 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
)}
</span>
</div>
{readonly ? null : (
<div className="hidden flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
openEditTableOnField();
}}
>
<Pencil className="!size-3.5 text-pink-600" />
</Button>
</div>
)}
</div>
{readonly ? null : (
<div className="ml-2 hidden shrink-0 flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
openEditTableOnField();
}}
>
<Pencil className="!size-3.5 text-pink-600" />
</Button>
</div>
)}
</div>
);
},

View File

@@ -27,7 +27,7 @@ import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { useTheme } from '@/hooks/use-theme';
import { useChartDB } from '@/hooks/use-chartdb';
import { DatabaseType } from '@/lib/domain/database-type';
import { supportsCustomTypes } from '@/lib/domain/database-capabilities';
import { useDialog } from '@/hooks/use-dialog';
import { Separator } from '@/components/separator/separator';
@@ -110,7 +110,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
},
active: selectedSidebarSection === 'areas',
},
...(databaseType === DatabaseType.POSTGRESQL
...(supportsCustomTypes(databaseType)
? [
{
title: t('editor_sidebar.custom_types'),

View File

@@ -15,7 +15,7 @@ import { useChartDB } from '@/hooks/use-chartdb';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { AreasSection } from './areas-section/areas-section';
import { CustomTypesSection } from './custom-types-section/custom-types-section';
import { DatabaseType } from '@/lib/domain/database-type';
import { supportsCustomTypes } from '@/lib/domain/database-capabilities';
import { DBMLSection } from './dbml-section/dbml-section';
import { RefsSection } from './refs-section/refs-section';
@@ -54,7 +54,7 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
<SelectItem value="areas">
{t('side_panel.areas_section.areas')}
</SelectItem>
{databaseType === DatabaseType.POSTGRESQL ? (
{supportsCustomTypes(databaseType) ? (
<SelectItem value="customTypes">
{t(
'side_panel.custom_types_section.custom_types'

View File

@@ -8,6 +8,7 @@ import type { FieldAttributeRange } from '@/lib/data/data-types/data-types';
import {
findDataTypeDataById,
supportsAutoIncrementDataType,
supportsArrayDataType,
} from '@/lib/data/data-types/data-types';
import {
Popover,
@@ -89,6 +90,7 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
unique: localField.unique,
default: localField.default,
increment: localField.increment,
isArray: localField.isArray,
});
}
prevFieldRef.current = localField;
@@ -104,6 +106,11 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
[field.type.name]
);
const supportsArray = useMemo(
() => supportsArrayDataType(field.type.name, databaseType),
[field.type.name, databaseType]
);
return (
<Popover
open={isOpen}
@@ -172,6 +179,26 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
/>
</div>
) : null}
{supportsArray ? (
<div className="flex items-center justify-between">
<Label
htmlFor="isArray"
className="text-subtitle"
>
Array
</Label>
<Checkbox
checked={localField.isArray ?? false}
disabled={readonly}
onCheckedChange={(value) =>
setLocalField((current) => ({
...current,
isArray: !!value,
}))
}
/>
</div>
) : null}
<div className="flex flex-col gap-2">
<Label htmlFor="default" className="text-subtitle">
{t(

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { GripVertical, KeyRound } from 'lucide-react';
import { Input } from '@/components/input/input';
import { generateDBFieldSuffix, type DBField } from '@/lib/domain/db-field';
import { useChartDB } from '@/hooks/use-chartdb';
import { useUpdateTableField } from '@/hooks/use-update-table-field';
import {
Tooltip,
@@ -15,13 +14,15 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { SelectBox } from '@/components/select-box/select-box';
import { TableFieldPopover } from './table-field-modal/table-field-modal';
import type { DBTable } from '@/lib/domain';
import type { DatabaseType, DBTable } from '@/lib/domain';
export interface TableFieldProps {
table: DBTable;
field: DBField;
updateField: (attrs: Partial<DBField>) => void;
removeField: () => void;
databaseType: DatabaseType;
readonly?: boolean;
}
export const TableField: React.FC<TableFieldProps> = ({
@@ -29,8 +30,9 @@ export const TableField: React.FC<TableFieldProps> = ({
field,
updateField,
removeField,
databaseType,
readonly = false,
}) => {
const { databaseType, readonly } = useChartDB();
const { t } = useTranslation();
const { attributes, listeners, setNodeRef, transform, transition } =
@@ -99,7 +101,9 @@ export const TableField: React.FC<TableFieldProps> = ({
'side_panel.tables_section.table.field_type'
)}
value={field.type.id}
valueSuffix={generateDBFieldSuffix(field)}
valueSuffix={generateDBFieldSuffix(field, {
databaseType,
})}
optionSuffix={(option) =>
generateFieldSuffix(option.value)
}

View File

@@ -49,6 +49,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
updateIndex,
updateTable,
readonly,
databaseType,
} = useChartDB();
const { t } = useTranslation();
const { color } = table;
@@ -183,6 +184,8 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
removeField={() =>
removeField(table.id, field.id)
}
databaseType={databaseType}
readonly={readonly}
/>
))}
</SortableContext>

View File

@@ -2418,9 +2418,10 @@ export const examples: Example[] = [
id: 'yqrnjmizqeu2w7mpfhze3clbj',
name: 'special_features',
type: {
id: 'array',
name: 'array',
id: 'text',
name: 'text',
},
isArray: true,
primaryKey: false,
unique: false,
nullable: true,