From 1082170f86236defc3256093ff3ab2db68139592 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 24 Feb 2025 23:00:24 +0100 Subject: [PATCH 1/3] Added Scan/Value drivers for sql support. --- sql.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++ sql_test.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 sql.go create mode 100644 sql_test.go diff --git a/sql.go b/sql.go new file mode 100644 index 0000000..bd9343e --- /dev/null +++ b/sql.go @@ -0,0 +1,52 @@ +// +// Copyright 2018-2025 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package semver + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements the sql.Scanner interface +func (v *Version) Scan(value interface{}) error { + raw, ok := value.(string) + if !ok { + return fmt.Errorf("incompatible type %T for Version", value) + } + + v.raw = raw + v.bytes = []byte(v.raw) + if err := parse(v); err != nil { + return err + } + return nil +} + +// Value implements the driver.Valuer interface +func (v *Version) Value() (driver.Value, error) { + return v.raw, nil +} + +// Scan implements the sql.Scanner interface +func (v *RelaxedVersion) Scan(value interface{}) error { + raw, ok := value.(string) + if !ok { + return fmt.Errorf("incompatible type %T for Version", value) + } + + res := ParseRelaxed(raw) + *v = *res + return nil +} + +// Value implements the driver.Valuer interface +func (v *RelaxedVersion) Value() (driver.Value, error) { + if v.version != nil { + return v.version.raw, nil + } + return string(v.customversion), nil +} diff --git a/sql_test.go b/sql_test.go new file mode 100644 index 0000000..c089e3e --- /dev/null +++ b/sql_test.go @@ -0,0 +1,59 @@ +// +// Copyright 2018-2025 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +package semver + +import ( + "database/sql/driver" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSQLDriverInterfaces(t *testing.T) { + + t.Run("Version", func(t *testing.T) { + // Test Version Scan/Value + v := &Version{} + if _, ok := interface{}(v).(driver.Valuer); !ok { + t.Error("Version does not implement driver.Valuer") + } + if _, ok := interface{}(v).(driver.Valuer); !ok { + t.Error("Version does not implement driver.Valuer") + } + require.Error(t, v.Scan(1)) + require.Error(t, v.Scan(nil)) + require.NoError(t, v.Scan("1.2.3-rc.1+build.2")) + require.Equal(t, "1.2.3-rc.1+build.2", v.String()) + d, err := v.Value() + require.NoError(t, err) + require.Equal(t, "1.2.3-rc.1+build.2", d) + }) + + t.Run("RelaxedVersion", func(t *testing.T) { + // Test RelaxedVersion Scan/Value + rv := &RelaxedVersion{} + if _, ok := interface{}(rv).(driver.Valuer); !ok { + t.Error("RelaxedVersion does not implement driver.Valuer") + } + if _, ok := interface{}(rv).(driver.Valuer); !ok { + t.Error("RelaxedVersion does not implement driver.Valuer") + } + require.Error(t, rv.Scan(1)) + require.Error(t, rv.Scan(nil)) + require.NoError(t, rv.Scan("4.5.6-rc.1+build.2")) + require.Equal(t, "4.5.6-rc.1+build.2", rv.String()) + rd, err := rv.Value() + require.NoError(t, err) + require.Equal(t, "4.5.6-rc.1+build.2", rd) + + require.NoError(t, rv.Scan("1-2.2-3.3")) + require.Equal(t, "1-2.2-3.3", rv.String()) + rd2, err := rv.Value() + require.NoError(t, err) + require.Equal(t, "1-2.2-3.3", rd2) + }) +} From e3f555eacecc87e891789d83fed92c0c422e48ba Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 24 Feb 2025 23:04:07 +0100 Subject: [PATCH 2/3] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e3fb934..592e518 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ The `Version` and `RelaxedVersion` provides optimized `MarshalBinary`/`Unmarshal The `Version` and `RelaxedVersion` have the YAML un/marshaler implemented so they can be YAML decoded/encoded with the excellent `gopkg.in/yaml.v3` library. +## SQL support + +The `Version` and `RelaxedVersion` types provides the `sql.Scanner` and `driver.Valuer` interfaces. Those objects could be directly used in SQL queries, their value will be mapped into a string field. + ## Lexicographic sortable strings that keeps semantic versioning order The `Version` and `RelaxedVersion` objects provides the `SortableString()` method that returns a string with a peculiar property: the alphanumeric sorting of two `Version.SortableString()` matches the semantic versioning ordering of the underling `Version` objects. In other words, given two `Version` object `a` and `b`: From 1f2e312ed64a0f62f0f6a2e890ec5518a7317d5a Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 24 Feb 2025 23:18:24 +0100 Subject: [PATCH 3/3] Code coverage back to 100% --- sql_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sql_test.go b/sql_test.go index c089e3e..eeeec57 100644 --- a/sql_test.go +++ b/sql_test.go @@ -26,6 +26,7 @@ func TestSQLDriverInterfaces(t *testing.T) { } require.Error(t, v.Scan(1)) require.Error(t, v.Scan(nil)) + require.Error(t, v.Scan("123asdf")) require.NoError(t, v.Scan("1.2.3-rc.1+build.2")) require.Equal(t, "1.2.3-rc.1+build.2", v.String()) d, err := v.Value() @@ -45,15 +46,19 @@ func TestSQLDriverInterfaces(t *testing.T) { require.Error(t, rv.Scan(1)) require.Error(t, rv.Scan(nil)) require.NoError(t, rv.Scan("4.5.6-rc.1+build.2")) + require.Empty(t, rv.customversion) + require.NotNil(t, rv.version) require.Equal(t, "4.5.6-rc.1+build.2", rv.String()) rd, err := rv.Value() require.NoError(t, err) require.Equal(t, "4.5.6-rc.1+build.2", rd) - require.NoError(t, rv.Scan("1-2.2-3.3")) - require.Equal(t, "1-2.2-3.3", rv.String()) + require.NoError(t, rv.Scan("a1-2.2-3.3")) + require.NotEmpty(t, rv.customversion) + require.Nil(t, rv.version) + require.Equal(t, "a1-2.2-3.3", rv.String()) rd2, err := rv.Value() require.NoError(t, err) - require.Equal(t, "1-2.2-3.3", rd2) + require.Equal(t, "a1-2.2-3.3", rd2) }) }