CREATE TABLE sütunları kopyala

This commit is contained in:
Sedat Öztürk 2026-03-21 17:42:36 +03:00
parent 283fb2eba2
commit 52e52bb03c

View file

@ -224,6 +224,8 @@ const TENANT_COLUMN: ColumnDefinition = {
description: 'Tenant ID for multi-tenancy',
}
const CREATE_TABLE_SCRIPT_STORAGE_KEY = 'sqlQueryManager.lastCreateTableScript'
// ─── T-SQL Generator ──────────────────────────────────────────────────────────
function colToSqlLine(col: ColumnDefinition, addComma = true): string {
@ -377,6 +379,136 @@ function dbColToColumnDef(col: {
}
}
function splitByTopLevelComma(input: string): string[] {
const parts: string[] = []
let current = ''
let depth = 0
for (let i = 0; i < input.length; i++) {
const ch = input[i]
if (ch === '(') depth++
if (ch === ')') depth = Math.max(0, depth - 1)
if (ch === ',' && depth === 0) {
if (current.trim()) parts.push(current.trim())
current = ''
continue
}
current += ch
}
if (current.trim()) parts.push(current.trim())
return parts
}
function unwrapDefaultExpression(value: string): string {
let result = value.trim()
while (result.startsWith('(') && result.endsWith(')')) {
result = result.slice(1, -1).trim()
}
return result
}
function mapSqlTypeToDesigner(dataTypeRaw: string, lengthRaw: string): {
dataType: SqlDataType
maxLength: string
} {
const dt = dataTypeRaw.toLowerCase()
const len = lengthRaw.toLowerCase()
if (dt === 'nvarchar' || dt === 'varchar' || dt === 'char' || dt === 'nchar') {
if (len === 'max') {
return { dataType: 'nvarchar(MAX)', maxLength: '' }
}
return { dataType: 'nvarchar', maxLength: len || '100' }
}
if (dt === 'int') return { dataType: 'int', maxLength: '' }
if (dt === 'bigint') return { dataType: 'bigint', maxLength: '' }
if (dt === 'decimal' || dt === 'numeric') return { dataType: 'decimal', maxLength: '' }
if (dt === 'float' || dt === 'real') return { dataType: 'float', maxLength: '' }
if (dt === 'bit') return { dataType: 'bit', maxLength: '' }
if (dt.startsWith('datetime') || dt === 'smalldatetime' || dt === 'time') {
return { dataType: 'datetime2', maxLength: '' }
}
if (dt === 'date') return { dataType: 'date', maxLength: '' }
if (dt === 'uniqueidentifier') return { dataType: 'uniqueidentifier', maxLength: '' }
if (dt === 'money' || dt === 'smallmoney') return { dataType: 'money', maxLength: '' }
return { dataType: 'nvarchar', maxLength: '100' }
}
function parseCreateTableColumns(script: string): ColumnDefinition[] {
if (!script?.trim()) return []
const createIdx = script.search(/\bCREATE\s+TABLE\b/i)
if (createIdx < 0) return []
const openParenIdx = script.indexOf('(', createIdx)
if (openParenIdx < 0) return []
let depth = 0
let closeParenIdx = -1
for (let i = openParenIdx; i < script.length; i++) {
const ch = script[i]
if (ch === '(') depth++
if (ch === ')') {
depth--
if (depth === 0) {
closeParenIdx = i
break
}
}
}
if (closeParenIdx < 0) return []
const body = script.slice(openParenIdx + 1, closeParenIdx)
const defs = splitByTopLevelComma(body)
const columns: ColumnDefinition[] = []
for (const def of defs) {
const trimmed = def.trim()
if (!trimmed) continue
if (
/^(CONSTRAINT|PRIMARY\s+KEY|UNIQUE\s+KEY|UNIQUE|FOREIGN\s+KEY|CHECK)\b/i.test(trimmed)
) {
continue
}
const colMatch = trimmed.match(/^\[([^\]]+)\]\s+(.+)$/i)
if (!colMatch) continue
const columnName = colMatch[1].trim()
const rest = colMatch[2].trim()
const typeMatch = rest.match(/^\[?([A-Za-z0-9_]+)\]?\s*(?:\(([^)]*)\))?/)
if (!typeMatch) continue
const sqlType = typeMatch[1] ?? 'nvarchar'
const sqlLength = (typeMatch[2] ?? '').trim()
const mapped = mapSqlTypeToDesigner(sqlType, sqlLength)
const isNullable = !/\bNOT\s+NULL\b/i.test(rest)
const defaultMatch = rest.match(/\bDEFAULT\b\s+(.+)$/i)
const defaultValue = defaultMatch ? unwrapDefaultExpression(defaultMatch[1]) : ''
columns.push({
id: crypto.randomUUID(),
columnName,
dataType: mapped.dataType,
maxLength: mapped.maxLength,
isNullable,
defaultValue,
description: '',
})
}
return columns
}
/** Generate ALTER TABLE diff SQL (edit mode) */
function generateAlterTableSql(
originalCols: ColumnDefinition[],
@ -1025,6 +1157,84 @@ const SqlTableDesignerDialog = ({
}
}
const importColumnsFromRememberedCreateTable = async () => {
let script = ''
try {
const clipboardText = await navigator.clipboard.readText()
if (/\bCREATE\s+TABLE\b/i.test(clipboardText)) {
script = clipboardText
localStorage.setItem(CREATE_TABLE_SCRIPT_STORAGE_KEY, clipboardText)
}
} catch {
// Clipboard read can fail due to browser permissions; fallback to saved script.
}
if (!script) {
script = localStorage.getItem(CREATE_TABLE_SCRIPT_STORAGE_KEY) || ''
}
if (!script.trim()) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
CREATE TABLE script bulunamadı. Scripti panoya kopyalayıp tekrar deneyin.
</Notification>,
{ placement: 'top-center' },
)
return
}
const parsedColumns = parseCreateTableColumns(script)
if (parsedColumns.length === 0) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
CREATE TABLE scriptinden sütunlar ayrıştırılamadı.
</Notification>,
{ placement: 'top-center' },
)
return
}
let addedCount = 0
let skippedCount = 0
setColumns((prev) => {
const existingNonEmpty = prev.filter((c) => c.columnName.trim() !== '')
const existingNames = new Set(
existingNonEmpty.map((c) => c.columnName.trim().toLowerCase()),
)
const toAdd: ColumnDefinition[] = []
for (const col of parsedColumns) {
const key = col.columnName.trim().toLowerCase()
if (!key) {
skippedCount++
continue
}
if (existingNames.has(key)) {
skippedCount++
continue
}
existingNames.add(key)
toAdd.push(col)
}
addedCount = toAdd.length
return [...existingNonEmpty, ...toAdd, createEmptyColumn()]
})
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{addedCount > 0
? `${addedCount} sütun eklendi${skippedCount > 0 ? `, ${skippedCount} sütun zaten mevcut olduğu için atlandı.` : '.'}`
: 'Yeni sütun eklenmedi. Scriptteki sütunların tamamı zaten mevcut.'}
</Notification>,
{ placement: 'top-center' },
)
}
const removeColumn = (id: string) => setColumns((prev) => prev.filter((c) => c.id !== id))
const moveColumn = (id: string, direction: 'up' | 'down') => {
@ -1257,8 +1467,7 @@ const SqlTableDesignerDialog = ({
if (isEditMode) return true // table name is locked in edit mode
const baseOk =
!!settings.tableName.trim() && !!settings.entityName.trim() && !!settings.menuValue
if (!baseOk) return false
return columns.some((c) => c.columnName.trim().toLowerCase() === 'id')
return baseOk
}
return true
}
@ -1395,6 +1604,15 @@ const SqlTableDesignerDialog = ({
<Button size="xs" variant="solid" color="green-600" onClick={addMultiTenantColumns}>
{translate('::App.SqlQueryManager.AddMultiTenantColumns')}
</Button>
<Button
size="xs"
variant="solid"
color="amber-600"
onClick={importColumnsFromRememberedCreateTable}
disabled={isEditMode}
>
CREATE TABLE'dan Kolonları Yapıştır
</Button>
</div>
<div className="flex items-center gap-2">
<Button
@ -1565,10 +1783,11 @@ const SqlTableDesignerDialog = ({
})}
</div>
{/* Id warning */}
{/* PK guidance */}
{!isEditMode && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
<div className="px-2 py-1.5 mt-2 bg-orange-50 dark:bg-orange-900/20 border border-orange-300 dark:border-orange-700 rounded text-xs text-orange-700 dark:text-orange-300">
{translate('::App.SqlQueryManager.NoIdColumnWarning')}
PK için Id kolonu (int veya uniqueidentifier) önerilir. İsterseniz bir sonraki adımda
Primary Key de tanımlayabilirsiniz.
</div>
)}
</>
@ -1668,8 +1887,9 @@ const SqlTableDesignerDialog = ({
{/* Warning: no Id column */}
{!isEditMode && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
<div className="px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-300 dark:border-red-700 rounded text-xs text-red-700 dark:text-red-300">
{translate('::App.SqlQueryManager.NoIdColumnError')}
<div className="px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700 rounded text-xs text-amber-700 dark:text-amber-300">
Full Audited kolonları seçmediğiniz için PK kolonu manuel tanımlanmalıdır. Bu kolon
int veya uniqueidentifier olabilir.
</div>
)}
</div>