Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd9c37cd25 |
@@ -218,23 +218,6 @@ see [`bun`'s `--array-nullable`](./pkg/writers/bun/README.md#nullablearrays)
|
||||
flag for nullable-array handling. The `SqlXxxArray` wrapper types remain
|
||||
available in `pkg/sqltypes` and are still used by the `gorm` writer.
|
||||
|
||||
#### Custom type mapping
|
||||
|
||||
Override the built-in SQL → Go mapping of the `bun` and `gorm` writers with the
|
||||
repeatable `--type-map sqltype=gotype` flag:
|
||||
|
||||
```bash
|
||||
relspec convert --from pgsql --from-conn "$DSN" --to gorm --to-path models.go \
|
||||
--type-map uuid=string --type-map jsonb=json.RawMessage
|
||||
```
|
||||
|
||||
SQL type names are matched case-insensitively on the base type (modifiers such
|
||||
as `(10,2)` are ignored; aliases like `int4` resolve to `integer`). NOT NULL
|
||||
columns use the Go type verbatim, nullable columns get a `*` prefix (unless the
|
||||
type is already a pointer, slice, map or `any`), and arrays become `[]gotype`.
|
||||
Unmapped types keep their defaults. The flag does not add imports: use types
|
||||
that need none, or add the import afterwards (e.g. with `goimports`).
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Register or sign in with GitHub at [git.warky.dev](https://git.warky.dev)
|
||||
|
||||
@@ -27,7 +27,6 @@ func newWriterOptions(outputPath, packageName string, flattenSchema bool, nullab
|
||||
FlattenSchema: flattenSchema,
|
||||
NullableTypes: nullableTypes,
|
||||
NullableArrays: nullableArrays,
|
||||
TypeMappings: typeMappings,
|
||||
Prisma7: prisma7,
|
||||
ContinueOnError: continueOnError,
|
||||
StrictDirectives: strictDirectives,
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/buildinfo"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
)
|
||||
|
||||
// version/buildDate mirror pkg/buildinfo so existing call sites keep working.
|
||||
@@ -18,8 +17,6 @@ var (
|
||||
noVersion bool
|
||||
silent bool
|
||||
strictDirectives bool
|
||||
typeMapFlags []string
|
||||
typeMappings map[string]string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -31,11 +28,6 @@ bidirectional conversion between various database schema formats.
|
||||
It reads database schemas from multiple sources (live databases, DBML,
|
||||
DCTX, DrawDB, etc.) and writes them to various formats (GORM, Bun,
|
||||
JSON, YAML, SQL, etc.).`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
typeMappings, err = writers.ParseTypeMappings(typeMapFlags)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -52,7 +44,6 @@ func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
rootCmd.AddCommand(reportCmd)
|
||||
rootCmd.PersistentFlags().BoolVar(&prisma7, "prisma7", false, "Use Prisma 7 generator conventions when reading/writing Prisma schemas")
|
||||
rootCmd.PersistentFlags().StringArrayVar(&typeMapFlags, "type-map", nil, "Override a SQL-to-Go type mapping for bun/gorm output as sqltype=gotype (repeatable), e.g. --type-map uuid=uuid.UUID --type-map numeric=decimal.Decimal")
|
||||
rootCmd.PersistentFlags().BoolVar(&noVersion, "no-version", false, "Suppress the RelSpec version header")
|
||||
rootCmd.PersistentFlags().BoolVar(&silent, "silent", false, "Suppress progress and status messages (errors are still shown)")
|
||||
rootCmd.PersistentFlags().BoolVar(&strictDirectives, "strict-directives", false, "Fail on unknown or untranslatable DBML dialect directives (@postgres:, @sqlite:, …)")
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package sqltypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSqlNull_ValueScalarCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlNull[any]
|
||||
want driver.Value
|
||||
}{
|
||||
{name: "invalid", input: SqlNull[any]{}, want: nil},
|
||||
{name: "integer", input: Null[any](int64(42), true), want: int64(42)},
|
||||
{name: "string", input: Null[any]("hello", true), want: "hello"},
|
||||
{name: "boolean", input: Null[any](true, true), want: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.input.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("Value returned error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Value() = %v (%T), want %v (%T)", got, got, tt.want, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_Int64Conversions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlNull[any]
|
||||
want int64
|
||||
}{
|
||||
{name: "invalid", input: SqlNull[any]{}, want: 0},
|
||||
{name: "signed integer", input: Null[any](int32(-12), true), want: -12},
|
||||
{name: "unsigned integer", input: Null[any](uint16(12), true), want: 12},
|
||||
{name: "float truncates", input: Null[any](float64(12.9), true), want: 12},
|
||||
{name: "numeric string", input: Null[any]("123", true), want: 123},
|
||||
{name: "invalid string", input: Null[any]("not a number", true), want: 0},
|
||||
{name: "true", input: Null[any](true, true), want: 1},
|
||||
{name: "false", input: Null[any](false, true), want: 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.input.Int64(); got != tt.want {
|
||||
t.Errorf("Int64() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_Float64Conversions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input SqlNull[any]
|
||||
want float64
|
||||
}{
|
||||
{name: "invalid", input: SqlNull[any]{}, want: 0},
|
||||
{name: "float", input: Null[any](float32(1.25), true), want: 1.25},
|
||||
{name: "signed integer", input: Null[any](int64(-12), true), want: -12},
|
||||
{name: "unsigned integer", input: Null[any](uint16(12), true), want: 12},
|
||||
{name: "numeric string", input: Null[any]("12.5", true), want: 12.5},
|
||||
{name: "invalid string", input: Null[any]("not a number", true), want: 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.input.Float64(); got != tt.want {
|
||||
t.Errorf("Float64() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlDate_JSONNullAndInvalid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
valid bool
|
||||
}{
|
||||
{name: "null", json: "null", valid: false},
|
||||
{name: "invalid date", json: `"not-a-date"`, valid: false},
|
||||
{name: "valid date", json: `"2024-01-15"`, valid: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got SqlDate
|
||||
if err := json.Unmarshal([]byte(tt.json), &got); err != nil {
|
||||
t.Fatalf("UnmarshalJSON returned error: %v", err)
|
||||
}
|
||||
if got.Valid != tt.valid {
|
||||
t.Errorf("Valid = %v, want %v", got.Valid, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if data, err := json.Marshal(SqlDate{}); err != nil {
|
||||
t.Fatalf("MarshalJSON returned error: %v", err)
|
||||
} else if string(data) != "null" {
|
||||
t.Errorf("MarshalJSON() = %s, want null", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlTypeNowConstructors(t *testing.T) {
|
||||
before := time.Now()
|
||||
timestamp := SqlTimeStampNow()
|
||||
date := SqlDateNow()
|
||||
tm := SqlTimeNow()
|
||||
after := time.Now()
|
||||
|
||||
for name, got := range map[string]time.Time{
|
||||
"timestamp": timestamp.Time(),
|
||||
"date": date.Time(),
|
||||
"time": tm.Time(),
|
||||
} {
|
||||
if !got.After(before) && !got.Equal(before) || got.After(after) {
|
||||
t.Errorf("%s constructor returned %v outside [%v, %v]", name, got, before, after)
|
||||
}
|
||||
}
|
||||
if !timestamp.Valid || !date.Valid || !tm.Valid {
|
||||
t.Fatal("Now constructors must return valid values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSqlAndToJSONDT(t *testing.T) {
|
||||
if got := NewSql[int64]("42"); !got.Valid || got.Val != 42 {
|
||||
t.Errorf("NewSql[int64](\"42\") = %#v, want valid 42", got)
|
||||
}
|
||||
if got := NewSql[int64](nil); got.Valid {
|
||||
t.Errorf("NewSql[int64](nil) = %#v, want invalid", got)
|
||||
}
|
||||
if got := NewSqlFloat32(1.5); !got.Valid || got.Val != 1.5 {
|
||||
t.Errorf("NewSqlFloat32(1.5) = %#v, want valid 1.5", got)
|
||||
}
|
||||
|
||||
when := time.Date(2024, 1, 15, 10, 30, 45, 0, time.UTC)
|
||||
if got := ToJSONDT(when); got != "2024-01-15T10:30:45Z" {
|
||||
t.Errorf("ToJSONDT() = %q, want RFC3339 timestamp", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSqlNull_Float64PreservesInfinity(t *testing.T) {
|
||||
got := Null[float64](math.Inf(1), true).Float64()
|
||||
if !math.IsInf(got, 1) {
|
||||
t.Errorf("Float64() = %v, want +Inf", got)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
// TypeMapper handles type conversions between SQL and Go types for Bun
|
||||
type TypeMapper struct {
|
||||
sqlTypesAlias string
|
||||
typeMappings map[string]string
|
||||
typeStyle string // writers.NullableTypeSqlTypes | writers.NullableTypeStdlib | writers.NullableTypeBaselib
|
||||
arrayNullable string // writers.NullableArraysSlice | writers.NullableArraysPointerSlice
|
||||
}
|
||||
@@ -38,10 +37,6 @@ func NewTypeMapper(typeStyle, arrayNullable string) *TypeMapper {
|
||||
|
||||
// SQLTypeToGoType converts a SQL type to its Go equivalent.
|
||||
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
||||
if goType, ok := tm.overrideGoType(sqlType, notNull); ok {
|
||||
return goType
|
||||
}
|
||||
|
||||
// Array columns always use a native Go slice, regardless of typeStyle.
|
||||
if pgsql.IsArrayType(sqlType) {
|
||||
goType := tm.arrayGoType(tm.extractBaseType(sqlType))
|
||||
@@ -73,26 +68,6 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
||||
return tm.bunGoType(baseType)
|
||||
}
|
||||
|
||||
// SetTypeMappings installs user-configured SQL-to-Go type overrides.
|
||||
func (tm *TypeMapper) SetTypeMappings(mappings map[string]string) {
|
||||
tm.typeMappings = mappings
|
||||
}
|
||||
|
||||
// overrideGoType applies a configured override, if any, for the column type.
|
||||
func (tm *TypeMapper) overrideGoType(sqlType string, notNull bool) (string, bool) {
|
||||
if len(tm.typeMappings) == 0 {
|
||||
return "", false
|
||||
}
|
||||
goType, ok := writers.LookupTypeMapping(tm.typeMappings, tm.extractBaseType(sqlType))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if pgsql.IsArrayType(sqlType) {
|
||||
return "[]" + goType, true
|
||||
}
|
||||
return writers.ApplyTypeMapping(goType, notNull), true
|
||||
}
|
||||
|
||||
// extractBaseType extracts the base type from a SQL type string
|
||||
func (tm *TypeMapper) extractBaseType(sqlType string) string {
|
||||
return pgsql.CanonicalizeBaseType(pgsql.ExtractBaseTypeLower(sqlType))
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package bun
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTypeMapper_CustomTypeMappings(t *testing.T) {
|
||||
mapper := NewTypeMapper("", "")
|
||||
mapper.SetTypeMappings(map[string]string{"uuid": "uuid.UUID", "numeric": "decimal.Decimal"})
|
||||
|
||||
tests := []struct {
|
||||
sqlType string
|
||||
notNull bool
|
||||
want string
|
||||
}{
|
||||
{"uuid", true, "uuid.UUID"},
|
||||
{"uuid", false, "*uuid.UUID"},
|
||||
{"numeric(10,2)", true, "decimal.Decimal"},
|
||||
{"UUID[]", false, "[]uuid.UUID"},
|
||||
{"bigint", true, "int64"}, // unmapped types keep defaults
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := mapper.SQLTypeToGoType(tt.sqlType, tt.notNull); got != tt.want {
|
||||
t.Errorf("SQLTypeToGoType(%q, %v) = %q, want %q", tt.sqlType, tt.notNull, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,6 @@ func NewWriter(options *writers.WriterOptions) *Writer {
|
||||
config: LoadMethodConfigFromMetadata(options.Metadata),
|
||||
}
|
||||
|
||||
w.typeMapper.SetTypeMappings(options.TypeMappings)
|
||||
|
||||
// Initialize templates
|
||||
tmpl, err := NewTemplates()
|
||||
if err != nil {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
// TypeMapper handles type conversions between SQL and Go types
|
||||
type TypeMapper struct {
|
||||
sqlTypesAlias string
|
||||
typeMappings map[string]string
|
||||
typeStyle string // writers.NullableTypeSqlTypes | writers.NullableTypeStdlib | writers.NullableTypeBaselib
|
||||
}
|
||||
|
||||
@@ -31,10 +30,6 @@ func NewTypeMapper(typeStyle string) *TypeMapper {
|
||||
|
||||
// SQLTypeToGoType converts a SQL type to its Go equivalent.
|
||||
func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
||||
if goType, ok := tm.overrideGoType(sqlType, notNull); ok {
|
||||
return goType
|
||||
}
|
||||
|
||||
// Array types are handled separately for both styles.
|
||||
if pgsql.IsArrayType(sqlType) {
|
||||
return tm.arrayGoType(tm.extractBaseType(sqlType))
|
||||
@@ -62,26 +57,6 @@ func (tm *TypeMapper) SQLTypeToGoType(sqlType string, notNull bool) string {
|
||||
return tm.nullableGoType(baseType)
|
||||
}
|
||||
|
||||
// SetTypeMappings installs user-configured SQL-to-Go type overrides.
|
||||
func (tm *TypeMapper) SetTypeMappings(mappings map[string]string) {
|
||||
tm.typeMappings = mappings
|
||||
}
|
||||
|
||||
// overrideGoType applies a configured override, if any, for the column type.
|
||||
func (tm *TypeMapper) overrideGoType(sqlType string, notNull bool) (string, bool) {
|
||||
if len(tm.typeMappings) == 0 {
|
||||
return "", false
|
||||
}
|
||||
goType, ok := writers.LookupTypeMapping(tm.typeMappings, tm.extractBaseType(sqlType))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if pgsql.IsArrayType(sqlType) {
|
||||
return "[]" + goType, true
|
||||
}
|
||||
return writers.ApplyTypeMapping(goType, notNull), true
|
||||
}
|
||||
|
||||
// extractBaseType extracts the base type from a SQL type string
|
||||
// Examples: varchar(100) → varchar, numeric(10,2) → numeric
|
||||
func (tm *TypeMapper) extractBaseType(sqlType string) string {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package gorm
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTypeMapper_CustomTypeMappings(t *testing.T) {
|
||||
mapper := NewTypeMapper("")
|
||||
mapper.SetTypeMappings(map[string]string{"uuid": "uuid.UUID", "numeric": "decimal.Decimal"})
|
||||
|
||||
tests := []struct {
|
||||
sqlType string
|
||||
notNull bool
|
||||
want string
|
||||
}{
|
||||
{"uuid", true, "uuid.UUID"},
|
||||
{"uuid", false, "*uuid.UUID"},
|
||||
{"numeric(10,2)", true, "decimal.Decimal"},
|
||||
{"UUID[]", false, "[]uuid.UUID"},
|
||||
{"bigint", true, "int64"}, // unmapped types keep defaults
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := mapper.SQLTypeToGoType(tt.sqlType, tt.notNull); got != tt.want {
|
||||
t.Errorf("SQLTypeToGoType(%q, %v) = %q, want %q", tt.sqlType, tt.notNull, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,6 @@ func NewWriter(options *writers.WriterOptions) *Writer {
|
||||
config: LoadMethodConfigFromMetadata(options.Metadata),
|
||||
}
|
||||
|
||||
w.typeMapper.SetTypeMappings(options.TypeMappings)
|
||||
|
||||
// Initialize templates
|
||||
tmpl, err := NewTemplates()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/pgsql"
|
||||
)
|
||||
|
||||
// ParseTypeMappings parses "sqltype=gotype" entries (as given to --type-map)
|
||||
// into a map keyed by the canonical lower-case SQL base type, so that
|
||||
// "VARCHAR", "character varying" and "varchar(50)" all address one entry.
|
||||
// It returns nil for empty input.
|
||||
func ParseTypeMappings(entries []string) (map[string]string, error) {
|
||||
if len(entries) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make(map[string]string, len(entries))
|
||||
for _, entry := range entries {
|
||||
sqlType, goType, ok := strings.Cut(entry, "=")
|
||||
sqlType, goType = strings.TrimSpace(sqlType), strings.TrimSpace(goType)
|
||||
if !ok || sqlType == "" || goType == "" {
|
||||
return nil, fmt.Errorf("invalid type mapping %q: expected sqltype=gotype", entry)
|
||||
}
|
||||
out[pgsql.CanonicalizeBaseType(pgsql.ExtractBaseTypeLower(sqlType))] = goType
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// LookupTypeMapping returns the user-configured override for baseType, which
|
||||
// the caller must already have canonicalized.
|
||||
func LookupTypeMapping(mappings map[string]string, baseType string) (string, bool) {
|
||||
goType, ok := mappings[baseType]
|
||||
return goType, ok
|
||||
}
|
||||
|
||||
// ApplyTypeMapping wraps an overridden Go type for nullability: NOT NULL uses
|
||||
// the type verbatim; nullable columns get a pointer prefix unless the type is
|
||||
// already a pointer, slice, map or interface.
|
||||
func ApplyTypeMapping(goType string, notNull bool) string {
|
||||
if notNull || strings.HasPrefix(goType, "*") || strings.HasPrefix(goType, "[]") ||
|
||||
strings.HasPrefix(goType, "map[") || goType == "any" || goType == "interface{}" {
|
||||
return goType
|
||||
}
|
||||
return "*" + goType
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package writers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseTypeMappings(t *testing.T) {
|
||||
got, err := ParseTypeMappings([]string{"UUID=uuid.UUID", " varchar(50) = MyString ", "INT4=MyInt"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]string{"uuid": "uuid.UUID", "varchar": "MyString", "integer": "MyInt"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
if m, err := ParseTypeMappings(nil); m != nil || err != nil {
|
||||
t.Errorf("empty input: got %v, %v", m, err)
|
||||
}
|
||||
for _, bad := range []string{"uuid", "=string", "uuid="} {
|
||||
if _, err := ParseTypeMappings([]string{bad}); err == nil {
|
||||
t.Errorf("expected error for %q", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyTypeMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
goType string
|
||||
notNull bool
|
||||
want string
|
||||
}{
|
||||
{"uuid.UUID", true, "uuid.UUID"},
|
||||
{"uuid.UUID", false, "*uuid.UUID"},
|
||||
{"*uuid.UUID", false, "*uuid.UUID"},
|
||||
{"[]byte", false, "[]byte"},
|
||||
{"map[string]any", false, "map[string]any"},
|
||||
{"any", false, "any"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := ApplyTypeMapping(tt.goType, tt.notNull); got != tt.want {
|
||||
t.Errorf("ApplyTypeMapping(%q, %v) = %q, want %q", tt.goType, tt.notNull, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,13 +83,6 @@ type WriterOptions struct {
|
||||
// SqlXxxArray wrapper types.
|
||||
NullableArrays string
|
||||
|
||||
// TypeMappings overrides the SQL-to-Go type mapping of the code-generation
|
||||
// writers (bun, gorm). Keys are SQL base types (aliases are canonicalized,
|
||||
// see ParseTypeMappings), values are Go type expressions. Array columns
|
||||
// use the override for their element type. Unmapped types keep the
|
||||
// built-in defaults.
|
||||
TypeMappings map[string]string
|
||||
|
||||
// Prisma7 enables Prisma 7-specific output for Prisma writers.
|
||||
Prisma7 bool
|
||||
|
||||
|
||||
Reference in New Issue
Block a user