From caf0a09dda0ad0c5871d4979431260ab51030cc7 Mon Sep 17 00:00:00 2001 From: Dinh Duong Ha Date: Tue, 23 Apr 2019 16:28:46 +0700 Subject: [PATCH] Add PostgreSQL JSONB data type. --- src/models/db_postgres.go | 1 + src/models/fields_json_defs.go | 42 +++++++++++ src/models/fieldtype/fieldtype.go | 4 + src/models/types/json.go | 118 ++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 src/models/fields_json_defs.go create mode 100644 src/models/types/json.go diff --git a/src/models/db_postgres.go b/src/models/db_postgres.go index dd7ce519..4372b051 100644 --- a/src/models/db_postgres.go +++ b/src/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", } // connectionString returns the connection string for the given parameters diff --git a/src/models/fields_json_defs.go b/src/models/fields_json_defs.go new file mode 100644 index 00000000..8d178ddc --- /dev/null +++ b/src/models/fields_json_defs.go @@ -0,0 +1,42 @@ + +package models + +import ( + + "github.com/hexya-erp/hexya/src/models/fieldtype" + "github.com/hexya-erp/hexya/src/models/types" +) + +// A JSONField is a field for storing jsonb. +type JSONField struct { + JSON string + String string + Help string + Stored bool + Required bool + ReadOnly bool + RequiredFunc func(Environment) (bool, Conditioner) + ReadOnlyFunc func(Environment) (bool, Conditioner) + InvisibleFunc func(Environment) (bool, Conditioner) + Unique bool + Index bool + Compute Methoder + Depends []string + Related string + NoCopy bool + GoType interface{} + Translate bool + OnChange Methoder + Constraint Methoder + Inverse Methoder + Contexts FieldContexts + Default func(Environment) interface{} +} + +// DeclareField creates a html field for the given FieldsCollection with the given name. +func (jsonf JSONField) DeclareField(fc *FieldsCollection, name string) *Field { + fInfo := genericDeclareField(fc, &jsonf, name, fieldtype.JSON, new(types.JSONText)) + return fInfo +} + + diff --git a/src/models/fieldtype/fieldtype.go b/src/models/fieldtype/fieldtype.go index 6a7f516b..747453fa 100644 --- a/src/models/fieldtype/fieldtype.go +++ b/src/models/fieldtype/fieldtype.go @@ -6,6 +6,7 @@ package fieldtype import ( "reflect" + "github.com/hexya-erp/hexya/src/models/types" "github.com/hexya-erp/hexya/src/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. @@ -87,6 +89,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/src/models/types/json.go b/src/models/types/json.go new file mode 100644 index 00000000..a8e37db5 --- /dev/null +++ b/src/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 +}