Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-44 implement driver.Valuer and sql.Scanner for Amount and Currency #99

Merged
merged 6 commits into from
Apr 26, 2024

Conversation

davidalpert
Copy link
Contributor

@davidalpert davidalpert commented Nov 29, 2021

Implements the driver.Valuer and sql.Scanner interfaces for money.Currency.

Implements the sql.Scanner interface for money.Money such that it expects to read a single string value of the format "amount|currency_code"

type MyCustomThing struct {
    Amount  *money.Money `json:"amount" db:"amount"`
}

This allows the client to save a money.Money object in a single string field like this "amount|currency_code" or in two discrete where "amount" and "currency" are stored separately and recomposed for scanning like this:

    SELECT
         amount || "|" || currency as amount

If the pipe (|) delimeter causes problems for your use case you can customize it by setting money.DBMoneyValueSeparator to a different value; this package-level var is used when converting a money.Money object to a string Value and also when scanning a string Value back into a money.Money instance.

    money.DBMoneyValueSeparator = "@"

I have tested this in my own project where the amount is stored as an integer field and the currency code is stored as a text field and they two are joined together in a hand-written SELECT query before passing it to money.Scan()

CREATE TABLE MyCustomThings
(
    id          INTEGER NOT NULL PRIMARY KEY autoincrement,
    amount      INTEGER NOT NULL,
    currency    TEXT  NOT NULL,
);

In my project I am using sqlx so I am fairly certain that this implementation will support any ORM or db wrapper which makes use of the sql.Scanner interface.

@davidalpert davidalpert marked this pull request as ready for review November 29, 2021 05:39
@davidalpert davidalpert mentioned this pull request Nov 29, 2021
@totemcaf
Copy link
Contributor

Money value was simplified, please adjust this code for the new type format. See that "val" was removed.

Copy link
Contributor

@totemcaf totemcaf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work

db.go Outdated
// let's support string and int64
switch src.(type) {
case string:
if i, err := strconv.ParseInt(src.(string), 10, 64); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My doubt here is the string representation of an amount should have the decimal separator or not.
Introducing the decimal separator in the string representation of an amount has the benefit of being explicit on it, but has the drawback of "," is a valid decimal separator for some currencies.

Copy link
Contributor Author

@davidalpert davidalpert Aug 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is easy to change.

we might use a pipe (|) or an at (@) or a colon (:) or semi-colon (;)

or even could implement this value separator as a package level variable (for example money.DBValueSeparator) with a default value and allow the client to override it as needed.

do you have a preference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored this out and made it configurable, updated the default to a pipe (|)

db.go Outdated

// Value implements driver.Valuer to serialize an Amount into sql/driver compatible type
func (a Amount) Value() (driver.Value, error) {
return a.val, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"val" was removed from Amount

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: in the last change, Amount was converted to an alias of "int64".
type Amount = int64
For this method to work you will need to change it to a new type:
type Amount int64
(Removing the "=" sign)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will simplify the db.go file as the driver.Valuer and sql.Scanner interfaces are already implemented for the base int64 type.

I am planning to rebase my branch on top of the recent changes and handle the Amount updates that way.

@davidalpert
Copy link
Contributor Author

than you for the feedback; I will look to get this PR updated shortly.

@davidalpert
Copy link
Contributor Author

this PR has been rebased on top of Rhymond:master and conflicts resolved.

as well the database value separator has been factored out to to a package level variable to allow clients to customize it and the default value separator changed from , to |

This allows any Golang ORM which supports the sql.Scanner to
serialize (via sql.Driver) and deserialize (via sql.Scanner) a
money.Currency instance.

money.Amount is now a type alias to int64 which is already supported
by sql.Scanner as one of the core built-in data types
Money's Value() function enables compatible
sql drivers to serialize a money.Money instance
to a single comma-delimited string value of
"amount,currency_code"

Money's Scan() function assumes that it receives
a single column where the src value is a comma-
delimited string in the format
"amount,currency_code"

While the storage format is up to the client when
the amount and currency are stored separately
a compatible scanner value can be created in
SQLite like this:

    SELECT amount || "," || currency as 'amount'

It is left to the client to decide to use Money's
Valuer implementation with a db annotation on
a property of type Money or else to store the
Amount and Currency values as two separate
columns and return them as a single joined
string field.
strings.Split(src,,) will return
a slice with length 2 even if
one of the strings is empty
clients can set money.DBMoneyValueSeparator to determine which
separator (e.g. "," "|" ";" ":" "AS" etc) to use when creating a single
driver.Value object to represent a money.Money instance as a single
string database field.

this allows the money package to support string values such as

10@USD
20;CAD
30|IRD
40 in GBP

etc
@Rhymond Rhymond merged commit 89bfd6b into Rhymond:master Apr 26, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants