From 29957865477f9fcbdc3e14f28a442c2f3174322c Mon Sep 17 00:00:00 2001 From: Ha Ly Bang Date: Fri, 29 Dec 2017 11:07:21 +0700 Subject: [PATCH] Add PostgreSQL JSONB data type. --- hexya/models/db_postgres.go | 6 ++ hexya/models/fields_defs.go | 63 +++++++++++++++ hexya/models/fieldtype/fieldtype.go | 4 + hexya/models/types/json.go | 118 ++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 hexya/models/types/json.go diff --git a/hexya/models/db_postgres.go b/hexya/models/db_postgres.go index f2fc808b..b3d4b2cf 100644 --- a/hexya/models/db_postgres.go +++ b/hexya/models/db_postgres.go @@ -55,6 +55,7 @@ var pgTypes = map[fieldtype.Type]string{ fieldtype.Selection: "character varying", fieldtype.Many2One: "integer", fieldtype.One2One: "integer", + fieldtype.JSON: "jsonb", } var pgDefaultValues = map[fieldtype.Type]string{ @@ -68,6 +69,7 @@ var pgDefaultValues = map[fieldtype.Type]string{ fieldtype.HTML: "''", fieldtype.Binary: "''", fieldtype.Selection: "''", + fieldtype.JSON: "'{}'", } // operatorSQL returns the sql string and placeholders for the given DomainOperator @@ -130,6 +132,10 @@ func (d *postgresAdapter) fieldIsNotNull(fi *Field) bool { } return false } + switch fi.fieldType { + case fieldtype.JSON: + return false + } return true } diff --git a/hexya/models/fields_defs.go b/hexya/models/fields_defs.go index d41142ac..3b5946e4 100644 --- a/hexya/models/fields_defs.go +++ b/hexya/models/fields_defs.go @@ -347,6 +347,69 @@ func (df DateTimeField) DeclareField(fc *FieldsCollection, name string) { fc.add(fInfo) } +// A JSONField is a field for storing jsonb. + +type JSONField struct { + JSON string + String string + Help string + Stored bool + Required bool + Unique bool + Index bool + Compute Methoder + Depends []string + Related string + GroupOperator string + NoCopy bool + GoType interface{} + Translate bool + OnChange Methoder + Constraint Methoder + Inverse Methoder + Default func(Environment, FieldMap) interface{} +} + +// DeclareField adds this text field to the given FieldsCollection with the given name. +func (jsonb JSONField) DeclareField(fc *FieldsCollection, name string) { + typ := reflect.TypeOf(*new(types.JSONText)) + if jsonb.GoType != nil { + typ = reflect.TypeOf(jsonb.GoType).Elem() + } + structField := reflect.StructField{ + Name: name, + Type: typ, + } + fieldType := fieldtype.JSON + json, str := getJSONAndString(name, fieldType, jsonb.JSON, jsonb.String) + compute, inverse, onchange, constraint := getFuncNames(jsonb.Compute, jsonb.Inverse, jsonb.OnChange, jsonb.Constraint) + fInfo := &Field{ + model: fc.model, + acl: security.NewAccessControlList(), + name: name, + json: json, + description: str, + help: jsonb.Help, + stored: jsonb.Stored, + required: jsonb.Required, + unique: jsonb.Unique, + index: jsonb.Index, + compute: compute, + inverse: inverse, + depends: jsonb.Depends, + relatedPath: jsonb.Related, + groupOperator: strutils.GetDefaultString(jsonb.GroupOperator, "sum"), + noCopy: jsonb.NoCopy, + structField: structField, + fieldType: fieldType, + defaultFunc: jsonb.Default, + translate: jsonb.Translate, + onChange: onchange, + constraint: constraint, + } + fc.add(fInfo) +} + // A FloatField is a field for storing decimal numbers. type FloatField struct { JSON string diff --git a/hexya/models/fieldtype/fieldtype.go b/hexya/models/fieldtype/fieldtype.go index 4690f05f..3f07afb0 100644 --- a/hexya/models/fieldtype/fieldtype.go +++ b/hexya/models/fieldtype/fieldtype.go @@ -6,6 +6,7 @@ package fieldtype import ( "reflect" + "github.com/hexya-erp/hexya/hexya/models/types" "github.com/hexya-erp/hexya/hexya/models/types/dates" ) @@ -31,6 +32,7 @@ const ( Reference Type = "reference" Selection Type = "selection" Text Type = "text" + JSON Type = "json" ) // IsRelationType returns true if this type is a relation. @@ -81,6 +83,8 @@ func (t Type) DefaultGoType() reflect.Type { return reflect.TypeOf(*new(dates.Date)) case DateTime: return reflect.TypeOf(*new(dates.DateTime)) + case JSON: + return reflect.TypeOf(*new(types.JSONText)) case Float: return reflect.TypeOf(*new(float64)) case Integer, Many2One, One2One, Rev2One: diff --git a/hexya/models/types/json.go b/hexya/models/types/json.go new file mode 100644 index 00000000..a8e37db5 --- /dev/null +++ b/hexya/models/types/json.go @@ -0,0 +1,118 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +// Source: https://github.com/jmoiron/sqlx/blob/master/types/types.go +// +// For HEXYA: In db.sanitizeQuery() function, call to sqlx.Rebind(sqlx.BindType(db.DriverName()), q) +// causes invalid SQL statement with json.RawMessage type. So, I change JSONText to string type + +//type JSONText json.RawMessage +type JSONText string + +var emptyJSON = JSONText("{}") + +//// MarshalJSON returns the *j as the JSON encoding of j. +//func (j JSONText) MarshalJSON() ([]byte, error) { +// if len(j) == 0 { +// return emptyJSON, nil +// } +// return j, nil +//} + +//// UnmarshalJSON sets *j to a copy of data +//func (j *JSONText) UnmarshalJSON(data []byte) error { +// if j == nil { +// return errors.New("JSONText: UnmarshalJSON on nil pointer") +// } +// *j = append((*j)[0:0], data...) +// return nil +//} + +// Value returns j as a value. This does a validating unmarshal into another +// RawMessage. If j is invalid json, it returns an error. +func (j JSONText) Value() (driver.Value, error) { + var m json.RawMessage + var err = j.Unmarshal(&m) + if err != nil { + return "{}", err + } + return string(j), nil +} + +// Scan stores the src in *j. No validation is done. +func (j *JSONText) Scan(src interface{}) error { + if src == nil { + *j = emptyJSON + } else { + *j = JSONText(string(src.([]uint8))) + } + return nil +} + +// Unmarshal unmarshal's the json in j to v, as in json.Unmarshal. +func (j *JSONText) Unmarshal(v interface{}) error { + if len(*j) == 0 { + *j = emptyJSON + } + return json.Unmarshal([]byte(*j), v) +} + +// String supports pretty printing for JSONText types. +func (j JSONText) String() string { + return string(j) +} + +// NullJSONText represents a JSONText that may be null. +// NullJSONText implements the scanner interface so +// it can be used as a scan destination, similar to NullString. +type NullJSONText struct { + JSONText + Valid bool // Valid is true if JSONText is not NULL +} + +// Scan implements the Scanner interface. +func (n *NullJSONText) Scan(value interface{}) error { + if value == nil { + n.JSONText, n.Valid = emptyJSON, false + return nil + } + n.Valid = true + return n.JSONText.Scan(value) +} + +// Value implements the driver Valuer interface. +func (n NullJSONText) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.JSONText.Value() +} + +// BitBool is an implementation of a bool for the MySQL type BIT(1). +// This type allows you to avoid wasting an entire byte for MySQL's boolean type TINYINT. +type BitBool bool + +// Value implements the driver.Valuer interface, +// and turns the BitBool into a bitfield (BIT(1)) for MySQL storage. +func (b BitBool) Value() (driver.Value, error) { + if b { + return []byte{1}, nil + } + return []byte{0}, nil +} + +// Scan implements the sql.Scanner interface, +// and turns the bitfield incoming from MySQL into a BitBool +func (b *BitBool) Scan(src interface{}) error { + v, ok := src.([]byte) + if !ok { + return errors.New("bad []byte type assertion") + } + *b = v[0] == 1 + return nil +}