alignment sql export scripts (#784)

This commit is contained in:
Guy Ben-Aharon
2025-07-23 21:00:52 +03:00
committed by GitHub
parent 6df588f40e
commit fb92be7d3e
5 changed files with 725 additions and 666 deletions

View File

@@ -73,7 +73,13 @@ function parseMSSQLDefault(field: DBField): string {
return `'${defaultValue}'`;
}
export function exportMSSQL(diagram: Diagram): string {
export function exportMSSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -83,134 +89,139 @@ export function exportMSSQL(diagram: Diagram): string {
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
if (!onlyRelationships) {
const schemas = new Set<string>();
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`;
});
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`;
});
return `${
table.comments ? formatMSSQLTableComment(table.comments) : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
return `${
table.comments
? formatMSSQLTableComment(table.comments)
: ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
const notNull = field.nullable ? '' : ' NOT NULL';
const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n${(() => {
const validIndexes = table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
})
.filter(Boolean);
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.filter(Boolean);
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n${(() => {
const validIndexes = table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});`
: '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
// Generate foreign keys
if (relationships.length > 0) {

View File

@@ -170,7 +170,13 @@ function mapMySQLType(typeName: string): string {
return typeName;
}
export function exportMySQL(diagram: Diagram): string {
export function exportMySQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -181,226 +187,236 @@ export function exportMySQL(diagram: Diagram): string {
// Start SQL script
let sqlScript = '-- MySQL database export\n';
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\n';
if (!onlyRelationships) {
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
sqlScript += 'START TRANSACTION;\n';
// Create databases (schemas) if they don't exist
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
schemas.forEach((schema) => {
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
});
if (schemas.size > 0) {
sqlScript += '\n';
}
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
// Create databases (schemas) if they don't exist
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Use schema prefix if available
const tableName = table.schema
? `\`${table.schema}\`.\`${table.name}\``
: `\`${table.name}\``;
schemas.forEach((schema) => {
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
});
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
if (schemas.size > 0) {
sqlScript += '\n';
}
return `${
table.comments ? formatTableComment(table.comments) : ''
}\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `\`${field.name}\``;
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Handle type name - map to MySQL compatible types
const typeName = mapMySQLType(field.type.name);
// Use schema prefix if available
const tableName = table.schema
? `\`${table.schema}\`.\`${table.name}\``
: `\`${table.name}\``;
// Handle MySQL specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'varbinary'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
// Get primary key fields
const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
return `${
table.comments ? formatTableComment(table.comments) : ''
}\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `\`${field.name}\``;
// Handle type name - map to MySQL compatible types
const typeName = mapMySQLType(field.type.name);
// Handle MySQL specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'varbinary'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
} else if (field.precision && field.scale) {
// Set a default size for VARCHAR columns if not specified
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
typeName.toLowerCase() === 'varchar' &&
!field.characterMaximumLength
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
typeWithSize = `${typeName}(255)`;
}
} else if (field.precision) {
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
// Set a default size for VARCHAR columns if not specified
if (
typeName.toLowerCase() === 'varchar' &&
!field.characterMaximumLength
) {
typeWithSize = `${typeName}(255)`;
}
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
let autoIncrement = '';
if (
field.primaryKey &&
(field.default?.toLowerCase().includes('identity') ||
field.default
field.primaryKey &&
(field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTO_INCREMENT';
}
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTO_INCREMENT';
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
? ` DEFAULT ${parseMySQLDefault(field)}`
// Handle default value
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
? ` DEFAULT ${parseMySQLDefault(field)}`
: '';
// MySQL supports inline comments
const comment = field.comments
? ` COMMENT '${escapeSQLComment(field.comments)}'`
: '';
// MySQL supports inline comments
const comment = field.comments
? ` COMMENT '${escapeSQLComment(field.comments)}'`
: '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''
}\n)${
// MySQL supports table comments
table.comments
? ` COMMENT='${escapeSQLComment(table.comments)}'`
: ''
};\n${
// Add indexes - MySQL creates them separately from the table definition
(() => {
const validIndexes = table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `\`${f.name}\``)
.join(', ')})`
: ''
}\n)${
// MySQL supports table comments
table.comments
? ` COMMENT='${escapeSQLComment(table.comments)}'`
: ''
};\n${
// Add indexes - MySQL creates them separately from the table definition
(() => {
const validIndexes = table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) =>
field && field.id === pk.id
)
)
)
) {
return '';
}
) {
return '';
}
// Create a unique index name by combining table name, field names, and a unique/non-unique indicator
const fieldNamesForIndex = indexFields
.map((field) => field?.name || '')
.join('_');
const uniqueIndicator = index.unique
? '_unique'
: '';
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
// Create a unique index name by combining table name, field names, and a unique/non-unique indicator
const fieldNamesForIndex = indexFields
.map((field) => field?.name || '')
.join('_');
const uniqueIndicator = index.unique
? '_unique'
: '';
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `\`${field.name}\`` : ''
)
.filter(Boolean);
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `\`${field.name}\`` : ''
)
.filter(Boolean);
// Check for text/blob fields that need special handling
const hasTextOrBlob = indexFields.some((field) => {
const typeName =
field?.type.name.toLowerCase() || '';
return (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
// Check for text/blob fields that need special handling
const hasTextOrBlob = indexFields.some(
(field) => {
const typeName =
field?.type.name.toLowerCase() ||
'';
return (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
);
}
);
});
// If there are TEXT/BLOB fields, need to add prefix length
const indexFieldsWithPrefix = hasTextOrBlob
? indexFieldNames.map((name) => {
const field = indexFields.find(
(f) => `\`${f?.name}\`` === name
);
if (!field) return name;
// If there are TEXT/BLOB fields, need to add prefix length
const indexFieldsWithPrefix = hasTextOrBlob
? indexFieldNames.map((name) => {
const field = indexFields.find(
(f) => `\`${f?.name}\`` === name
);
if (!field) return name;
const typeName =
field.type.name.toLowerCase();
if (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
) {
// Add a prefix length for TEXT/BLOB fields (required in MySQL)
return `${name}(255)`;
}
return name;
})
: indexFieldNames;
const typeName =
field.type.name.toLowerCase();
if (
typeName === 'text' ||
typeName === 'mediumtext' ||
typeName === 'longtext' ||
typeName === 'blob'
) {
// Add a prefix length for TEXT/BLOB fields (required in MySQL)
return `${name}(255)`;
}
return name;
})
: indexFieldNames;
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
: '';
})
.filter(Boolean);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});`
: '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
// Generate foreign keys
if (relationships.length > 0) {

View File

@@ -145,7 +145,13 @@ function exportCustomTypes(customTypes: DBCustomType[]): string {
return typesSql ? typesSql + '\n' : '';
}
export function exportPostgreSQL(diagram: Diagram): string {
export function exportPostgreSQL({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -156,252 +162,262 @@ export function exportPostgreSQL(diagram: Diagram): string {
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
if (!onlyRelationships) {
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Also collect schemas from custom types
customTypes.forEach((customType) => {
if (customType.schema) {
schemas.add(customType.schema);
}
});
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
if (schemas.size > 0) {
sqlScript += '\n';
}
// Add custom types (enums and composite types)
sqlScript += exportCustomTypes(customTypes);
// Add sequence creation statements
const sequences = new Set<string>();
tables.forEach((table) => {
table.fields.forEach((field) => {
if (field.default) {
// Match nextval('schema.sequence_name') or nextval('sequence_name')
const match = field.default.match(
/nextval\('([^']+)'(?:::[^)]+)?\)/
);
if (match) {
sequences.add(match[1]);
}
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
});
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
if (sequences.size > 0) {
sqlScript += '\n';
}
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
// Also collect schemas from custom types
customTypes.forEach((customType) => {
if (customType.schema) {
schemas.add(customType.schema);
}
});
const tableName = table.schema
? `"${table.schema}"."${table.name}"`
: `"${table.name}"`;
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
});
if (schemas.size > 0) {
sqlScript += '\n';
}
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
// Add custom types (enums and composite types)
sqlScript += exportCustomTypes(customTypes);
return `${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Add sequence creation statements
const sequences = new Set<string>();
// Handle type name - map problematic types to PostgreSQL compatible types
const typeName = mapPostgresType(
field.type.name,
field.name
tables.forEach((table) => {
table.fields.forEach((field) => {
if (field.default) {
// Match nextval('schema.sequence_name') or nextval('sequence_name')
const match = field.default.match(
/nextval\('([^']+)'(?:::[^)]+)?\)/
);
// Handle PostgreSQL specific type formatting
let typeWithSize = typeName;
let serialType = null;
if (field.increment && !field.nullable) {
if (
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
serialType = 'SERIAL';
} else if (typeName.toLowerCase() === 'bigint') {
serialType = 'BIGSERIAL';
} else if (typeName.toLowerCase() === 'smallint') {
serialType = 'SMALLSERIAL';
}
if (match) {
sequences.add(match[1]);
}
}
});
});
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'character varying' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'character'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
sequences.forEach((sequence) => {
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
});
if (sequences.size > 0) {
sqlScript += '\n';
}
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize = typeWithSize.replace('[]', '') + '[]';
}
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
const notNull = field.nullable ? '' : ' NOT NULL';
const tableName = table.schema
? `"${table.schema}"."${table.name}"`
: `"${table.name}"`;
// Handle identity generation
let identity = '';
if (field.default && field.default.includes('nextval')) {
// PostgreSQL already handles this with DEFAULT nextval()
} else if (
field.default &&
field.default.toLowerCase().includes('identity')
) {
identity = ' GENERATED BY DEFAULT AS IDENTITY';
}
// Get primary key fields
const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
// Only add UNIQUE constraint if the field is not part of the primary key
// This avoids redundant uniqueness constraints
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
return `${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Handle default value using PostgreSQL specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parsePostgresDefault(field)}`
: '';
// Handle type name - map problematic types to PostgreSQL compatible types
const typeName = mapPostgresType(
field.type.name,
field.name
);
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);${
// Add table comments
table.comments
? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';`
: ''
}${
// Add column comments
table.fields
.filter((f) => f.comments)
.map(
(f) =>
`\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';`
)
.join('')
}${
// Add indexes only for non-primary key fields or composite indexes
// This avoids duplicate indexes on primary key columns
(() => {
const validIndexes = table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Handle PostgreSQL specific type formatting
let typeWithSize = typeName;
let serialType = null;
// Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes
if (field.increment && !field.nullable) {
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
return '';
serialType = 'SERIAL';
} else if (typeName.toLowerCase() === 'bigint') {
serialType = 'BIGSERIAL';
} else if (typeName.toLowerCase() === 'smallint') {
serialType = 'SMALLSERIAL';
}
}
// Create unique index name using table name and index name
// This ensures index names are unique across the database
const safeTableName = table.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
const safeIndexName = index.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
// Limit index name length to avoid PostgreSQL's 63-character identifier limit
let combinedName = `${safeTableName}_${safeIndexName}`;
if (combinedName.length > 60) {
// If too long, use just the index name or a truncated version
combinedName =
safeIndexName.length > 60
? safeIndexName.substring(0, 60)
: safeIndexName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() ===
'character varying' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'character'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
const indexName = `"${combinedName}"`;
// Handle array types (check if the type name ends with '[]')
if (typeName.endsWith('[]')) {
typeWithSize =
typeWithSize.replace('[]', '') + '[]';
}
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean);
const notNull = field.nullable ? '' : ' NOT NULL';
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
// Handle identity generation
let identity = '';
if (
field.default &&
field.default.includes('nextval')
) {
// PostgreSQL already handles this with DEFAULT nextval()
} else if (
field.default &&
field.default.toLowerCase().includes('identity')
) {
identity = ' GENERATED BY DEFAULT AS IDENTITY';
}
// Only add UNIQUE constraint if the field is not part of the primary key
// This avoids redundant uniqueness constraints
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using PostgreSQL specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parsePostgresDefault(field)}`
: '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);${
// Add table comments
table.comments
? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';`
: ''
}${
// Add column comments
table.fields
.filter((f) => f.comments)
.map(
(f) =>
`\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';`
)
.join('')
}${
// Add indexes only for non-primary key fields or composite indexes
// This avoids duplicate indexes on primary key columns
(() => {
const validIndexes = table.indexes
.map((index) => {
// Get the list of fields for this index
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
// This prevents creating redundant indexes
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) =>
field && field.id === pk.id
)
)
) {
return '';
}
// Create unique index name using table name and index name
// This ensures index names are unique across the database
const safeTableName = table.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
const safeIndexName = index.name.replace(
/[^a-zA-Z0-9_]/g,
'_'
);
// Limit index name length to avoid PostgreSQL's 63-character identifier limit
let combinedName = `${safeTableName}_${safeIndexName}`;
if (combinedName.length > 60) {
// If too long, use just the index name or a truncated version
combinedName =
safeIndexName.length > 60
? safeIndexName.substring(0, 60)
: safeIndexName;
}
const indexName = `"${combinedName}"`;
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
: '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
// Generate foreign keys
if (relationships.length > 0) {

View File

@@ -140,7 +140,13 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
return typeName;
}
export function exportSQLite(diagram: Diagram): string {
export function exportSQLite({
diagram,
onlyRelationships = false,
}: {
diagram: Diagram;
onlyRelationships?: boolean;
}): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
@@ -166,159 +172,167 @@ export function exportSQLite(diagram: Diagram): string {
'sqlite_master',
];
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
if (!onlyRelationships) {
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
// Skip SQLite system tables
if (sqliteSystemTables.includes(table.name.toLowerCase())) {
return `-- Skipping SQLite system table: "${table.name}"\n`;
}
// Skip SQLite system tables
if (sqliteSystemTables.includes(table.name.toLowerCase())) {
return `-- Skipping SQLite system table: "${table.name}"\n`;
}
// SQLite doesn't use schema prefixes, so we use just the table name
// Include the schema in a comment if it exists
const schemaComment = table.schema
? `-- Original schema: ${table.schema}\n`
: '';
const tableName = `"${table.name}"`;
// SQLite doesn't use schema prefixes, so we use just the table name
// Include the schema in a comment if it exists
const schemaComment = table.schema
? `-- Original schema: ${table.schema}\n`
: '';
const tableName = `"${table.name}"`;
// Get primary key fields
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
// Get primary key fields
const primaryKeyFields = table.fields.filter(
(f) => f.primaryKey
);
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
const singleIntegerPrimaryKey =
primaryKeyFields.length === 1 &&
(primaryKeyFields[0].type.name.toLowerCase() === 'integer' ||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
const singleIntegerPrimaryKey =
primaryKeyFields.length === 1 &&
(primaryKeyFields[0].type.name.toLowerCase() ===
'integer' ||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
return `${schemaComment}${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
return `${schemaComment}${
table.comments ? formatTableComment(table.comments) : ''
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `"${field.name}"`;
// Handle type name - map to SQLite compatible types
const typeName = mapSQLiteType(
field.type.name,
field.primaryKey
);
// Handle type name - map to SQLite compatible types
const typeName = mapSQLiteType(
field.type.name,
field.primaryKey
);
// SQLite ignores length specifiers, so we don't add them
// We'll keep this simple without size info
const typeWithoutSize = typeName;
// SQLite ignores length specifiers, so we don't add them
// We'll keep this simple without size info
const typeWithoutSize = typeName;
const notNull = field.nullable ? '' : ' NOT NULL';
const notNull = field.nullable ? '' : ' NOT NULL';
// Handle autoincrement - only works with INTEGER PRIMARY KEY
let autoIncrement = '';
if (
field.primaryKey &&
singleIntegerPrimaryKey &&
(field.default?.toLowerCase().includes('identity') ||
field.default
// Handle autoincrement - only works with INTEGER PRIMARY KEY
let autoIncrement = '';
if (
field.primaryKey &&
singleIntegerPrimaryKey &&
(field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTOINCREMENT';
}
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value - Special handling for datetime() function
let defaultValue = '';
if (
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
) {
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
if (field.default.includes("datetime(''now'')")) {
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
} else {
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
.includes('identity') ||
field.default
?.toLowerCase()
.includes('autoincrement') ||
field.default?.includes('nextval'))
) {
autoIncrement = ' AUTOINCREMENT';
}
}
// Add PRIMARY KEY inline only for single INTEGER primary key
const primaryKey =
field.primaryKey && singleIntegerPrimaryKey
? ' PRIMARY KEY' + autoIncrement
: '';
// Only add UNIQUE constraint if the field is not part of the primary key
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
(() => {
const validIndexes = table.indexes
.map((index) => {
// Skip indexes that exactly match the primary key
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) => field && field.id === pk.id
)
)
) {
return '';
// Handle default value - Special handling for datetime() function
let defaultValue = '';
if (
field.default &&
!field.default.toLowerCase().includes('identity') &&
!field.default
.toLowerCase()
.includes('autoincrement') &&
!field.default.includes('nextval')
) {
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
if (field.default.includes("datetime(''now'')")) {
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
} else {
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
}
}
// Create safe index name
const safeIndexName = `${table.name}_${index.name}`
.replace(/[^a-zA-Z0-9_]/g, '_')
.substring(0, 60);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});`
// Add PRIMARY KEY inline only for single INTEGER primary key
const primaryKey =
field.primaryKey && singleIntegerPrimaryKey
? ' PRIMARY KEY' + autoIncrement
: '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
})
.join(',\n')}${
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
? `,\n PRIMARY KEY (${primaryKeyFields
.map((f) => `"${f.name}"`)
.join(', ')})`
: ''
}\n);\n${
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
(() => {
const validIndexes = table.indexes
.map((index) => {
// Skip indexes that exactly match the primary key
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? field : null;
})
.filter(Boolean);
// Get the properly quoted field names
const indexFieldNames = indexFields
.map((field) =>
field ? `"${field.name}"` : ''
)
.filter(Boolean);
// Skip if this index exactly matches the primary key fields
if (
primaryKeyFields.length ===
indexFields.length &&
primaryKeyFields.every((pk) =>
indexFields.some(
(field) =>
field && field.id === pk.id
)
)
) {
return '';
}
// Create safe index name
const safeIndexName =
`${table.name}_${index.name}`
.replace(/[^a-zA-Z0-9_]/g, '_')
.substring(0, 60);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});`
: '';
})
.filter(Boolean);
return validIndexes.length > 0
? `\n-- Indexes\n${validIndexes.join('\n')}`
: '';
})()
}\n`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
}
// Generate table constraints and triggers for foreign keys
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
// But we'll also provide individual ALTER TABLE statements as comments for reference

View File

@@ -36,10 +36,12 @@ export const exportBaseSQL = ({
diagram,
targetDatabaseType,
isDBMLFlow = false,
onlyRelationships = false,
}: {
diagram: Diagram;
targetDatabaseType: DatabaseType;
isDBMLFlow?: boolean;
onlyRelationships?: boolean;
}): string => {
const { tables, relationships } = diagram;
@@ -50,16 +52,16 @@ export const exportBaseSQL = ({
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
switch (diagram.databaseType) {
case DatabaseType.SQL_SERVER:
return exportMSSQL(diagram);
return exportMSSQL({ diagram, onlyRelationships });
case DatabaseType.POSTGRESQL:
return exportPostgreSQL(diagram);
return exportPostgreSQL({ diagram, onlyRelationships });
case DatabaseType.SQLITE:
return exportSQLite(diagram);
return exportSQLite({ diagram, onlyRelationships });
case DatabaseType.MYSQL:
case DatabaseType.MARIADB:
return exportMySQL(diagram);
return exportMySQL({ diagram, onlyRelationships });
default:
return exportPostgreSQL(diagram);
return exportPostgreSQL({ diagram, onlyRelationships });
}
}