package schema import ( "fmt" "strings" "aijianzhan/platform/internal/blueprint" ) // BuildPostgresDDL 仅拼接白名单标识符与固定类型映射。 func BuildPostgresDDL(bp *blueprint.Blueprint) ([]string, error) { if bp.Storage.SchemaName == "" { return nil, fmt.Errorf("schema_name empty") } schema := bp.Storage.SchemaName stmts := []string{ fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", quoteIdent(schema)), } for _, e := range bp.Entities { cols := make([]string, 0, len(e.Fields)+4) hasTenant := false hasOrgUnit := false hasCreatedAt := false hasUpdatedAt := false hasCreatedBy := false for _, f := range e.Fields { sqlType, err := mapType(f) if err != nil { return nil, err } var col string switch { case f.Name == e.PrimaryKey && f.Type == "bigint": col = fmt.Sprintf("%s BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY", quoteIdent(f.Name)) case f.Name == e.PrimaryKey && f.Type == "int": col = fmt.Sprintf("%s INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY", quoteIdent(f.Name)) default: nullSQL := "NULL" if !blueprint.BoolOr(f.Nullable, true) { nullSQL = "NOT NULL" } col = fmt.Sprintf("%s %s %s", quoteIdent(f.Name), sqlType, nullSQL) if f.Name == e.PrimaryKey { col += " PRIMARY KEY" } else if f.Unique { col += " UNIQUE" } if f.Type == "enum" && len(f.EnumValues) > 0 { // 枚举值仅作 UI 提示,不写死 CHECK,避免导入新值被拒 } } cols = append(cols, col) switch f.Name { case "tenant_id": hasTenant = true case "org_unit_id": hasOrgUnit = true case "created_at": hasCreatedAt = true case "updated_at": hasUpdatedAt = true case "created_by": hasCreatedBy = true } } // 系统强制列 if !hasTenant { cols = append(cols, "tenant_id BIGINT NOT NULL") } if !hasOrgUnit { cols = append(cols, "org_unit_id BIGINT") } if !hasCreatedBy { cols = append(cols, "created_by BIGINT") } if !hasCreatedAt { cols = append(cols, "created_at TIMESTAMPTZ NOT NULL DEFAULT now()") } if !hasUpdatedAt { cols = append(cols, "updated_at TIMESTAMPTZ NOT NULL DEFAULT now()") } create := fmt.Sprintf( "CREATE TABLE IF NOT EXISTS %s.%s (\n %s\n)", quoteIdent(schema), quoteIdent(e.Table), strings.Join(cols, ",\n "), ) stmts = append(stmts, create) for _, idx := range e.Indexes { unique := "" if idx.Unique { unique = "UNIQUE " } colsQuoted := make([]string, 0, len(idx.Columns)) for _, c := range idx.Columns { colsQuoted = append(colsQuoted, quoteIdent(c)) } stmts = append(stmts, fmt.Sprintf( "CREATE %sINDEX IF NOT EXISTS %s ON %s.%s (%s)", unique, quoteIdent(idx.Name), quoteIdent(schema), quoteIdent(e.Table), strings.Join(colsQuoted, ", "), )) } } return stmts, nil } func mapType(f blueprint.Field) (string, error) { switch f.Type { case "string": n := f.MaxLength if n <= 0 { n = 255 } return fmt.Sprintf("VARCHAR(%d)", n), nil case "text": return "TEXT", nil case "int": return "INTEGER", nil case "bigint": return "BIGINT", nil case "decimal": p, s := f.Precision, f.Scale if p <= 0 { p = 18 } if s < 0 { s = 2 } return fmt.Sprintf("NUMERIC(%d,%d)", p, s), nil case "boolean": return "BOOLEAN", nil case "date": return "DATE", nil case "datetime": return "TIMESTAMPTZ", nil case "enum": return "VARCHAR(64)", nil case "json": return "JSONB", nil case "file_ref": return "VARCHAR(512)", nil default: return "", fmt.Errorf("unknown field type: %s", f.Type) } } func enumCheck(col string, values []string) string { quoted := make([]string, 0, len(values)) for _, v := range values { quoted = append(quoted, "'"+strings.ReplaceAll(v, "'", "''")+"'") } return fmt.Sprintf("CHECK (%s IN (%s))", quoteIdent(col), strings.Join(quoted, ", ")) } func quoteIdent(name string) string { // 调用方已白名单校验;仍用双引号包裹防止关键字冲突 return `"` + strings.ReplaceAll(name, `"`, ``) + `"` }