Skip to content

Commit 5361e34

Browse files
committed
add support for bulk insert & connection pooling
1 parent 1b51297 commit 5361e34

File tree

9 files changed

+137
-20
lines changed

9 files changed

+137
-20
lines changed

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/gookit/gcli/v3 v3.0.1
88
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
99
github.com/jedib0t/go-pretty v4.3.0+incompatible
10+
github.com/lib/pq v1.10.9
1011
github.com/stretchr/testify v1.7.0
1112
github.com/thatmattlove/go-macaddr v0.0.7
1213
github.com/urfave/cli/v2 v2.3.0
@@ -25,12 +26,10 @@ require (
2526
github.com/gookit/color v1.5.0 // indirect
2627
github.com/gookit/goutil v0.4.3 // indirect
2728
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
28-
github.com/lib/pq v1.10.9 // indirect
2929
github.com/mattn/go-isatty v0.0.14 // indirect
3030
github.com/mattn/go-runewidth v0.0.13 // indirect
3131
github.com/mitchellh/mapstructure v1.4.3 // indirect
3232
github.com/oklog/ulid v1.3.1 // indirect
33-
github.com/pkg/errors v0.9.1 // indirect
3433
github.com/pmezard/go-difflib v1.0.0 // indirect
3534
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
3635
github.com/rivo/uniseg v0.2.0 // indirect

go.sum

-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
7979
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
8080
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
8181
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
82-
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
8382
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
8483
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8584
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

internal/util/util.go

+16
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,19 @@ func PathExists(n string) bool {
4343
func TimeSince(t time.Time) string {
4444
return durafmt.Parse(time.Since(t)).LimitFirstN(1).String()
4545
}
46+
47+
func SplitSlice[T any](slice []T, max int) [][]T {
48+
result := make([][]T, 0)
49+
50+
for i := 0; i < len(slice); i += max {
51+
end := i + max
52+
if end > len(slice) {
53+
end = len(slice)
54+
}
55+
part := slice[i:end]
56+
if part != nil {
57+
result = append(result, part)
58+
}
59+
}
60+
return result
61+
}

internal/util/util_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package util_test
22

33
import (
44
"os"
5+
"reflect"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -67,3 +68,39 @@ func Test_pathExists(t *testing.T) {
6768
assert.True(t, r)
6869
})
6970
}
71+
72+
func Test_SplitSlice(t *testing.T) {
73+
t.Run("equal parts", func(t *testing.T) {
74+
t.Parallel()
75+
original := []int{1, 2, 3, 4, 5, 6}
76+
max := 2
77+
expected := [][]int{
78+
{1, 2},
79+
{3, 4},
80+
{5, 6},
81+
}
82+
result := util.SplitSlice(original, max)
83+
assert.True(t, reflect.DeepEqual(expected, result))
84+
})
85+
t.Run("unequal parts", func(t *testing.T) {
86+
t.Parallel()
87+
original := []int{1, 2, 3, 4, 5, 6}
88+
max := 4
89+
expected := [][]int{
90+
{1, 2, 3, 4},
91+
{5, 6},
92+
}
93+
result := util.SplitSlice(original, max)
94+
assert.True(t, reflect.DeepEqual(expected, result))
95+
})
96+
97+
t.Run("empty", func(t *testing.T) {
98+
t.Parallel()
99+
original := []string{}
100+
max := 3
101+
expected := [][]string{}
102+
result := util.SplitSlice(original, max)
103+
assert.Equal(t, expected, result)
104+
})
105+
106+
}

main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func getArgs() []string {
1717

1818
func main() {
1919
args := getArgs()
20-
err := cmd.New(TABLE_VERSION).Run(args)
20+
err := cmd.New(Version).Run(args)
2121
if err != nil {
2222
fmt.Println(err)
2323
os.Exit(1)

oui/db.go

+59-6
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"database/sql"
55
"fmt"
66
"strconv"
7+
"strings"
78

89
"github.com/gookit/gcli/v3/progress"
910
"github.com/thatmattlove/go-macaddr"
11+
"github.com/thatmattlove/oui/v2/internal/util"
1012
)
1113

1214
type OUIDB struct {
@@ -81,6 +83,59 @@ func (ouidb *OUIDB) Insert(d *VendorDef) (res sql.Result, err error) {
8183
return
8284
}
8385

86+
func (ouidb *OUIDB) BulkInsert(defs []*VendorDef) (int64, error) {
87+
88+
var statement string
89+
var tmpl string
90+
var maxRecords int
91+
switch ouidb.dialect {
92+
case dialectSqlite:
93+
tmpl = "(?,?,?,?)"
94+
statement = "INSERT INTO %s(prefix, length, org, registry) values%s"
95+
maxRecords = maxVarsSqlite
96+
case dialectPsql:
97+
tmpl = "($%d,$%d,$%d,$%d)"
98+
statement = "INSERT INTO %s(prefix, length, org, registry) values%s ON CONFLICT (prefix, length, registry) DO UPDATE SET prefix = excluded.prefix, length = excluded.length, registry = excluded.registry"
99+
maxRecords = maxVarsPsql
100+
}
101+
102+
splitDefs := util.SplitSlice(defs, maxRecords/4)
103+
inserted := int64(0)
104+
105+
for _, split := range splitDefs {
106+
placeholders := make([]string, 0, len(split))
107+
args := make([]interface{}, 0, len(split)*4)
108+
i := 0
109+
for _, def := range split {
110+
def := def
111+
var placeholder string
112+
switch ouidb.dialect {
113+
case dialectSqlite:
114+
placeholder = tmpl
115+
case dialectPsql:
116+
placeholder = fmt.Sprintf(tmpl, i*4+1, i*4+2, i*4+3, i*4+4)
117+
}
118+
placeholders = append(placeholders, placeholder)
119+
args = append(args, def.Prefix, def.Length, def.Org, def.Registry)
120+
i++
121+
}
122+
q := fmt.Sprintf(statement, ouidb.Version, strings.Join(placeholders, ","))
123+
res, err := ouidb.Connection.Exec(q, args...)
124+
if err != nil {
125+
return inserted, err
126+
}
127+
rows, err := res.RowsAffected()
128+
if err != nil {
129+
return inserted, err
130+
}
131+
inserted += rows
132+
if err != nil {
133+
return inserted, err
134+
}
135+
}
136+
return inserted, nil
137+
}
138+
84139
func (ouidb *OUIDB) Populate() (records int64, err error) {
85140
err = ouidb.Clear()
86141
if err != nil {
@@ -98,12 +153,9 @@ func (ouidb *OUIDB) Populate() (records int64, err error) {
98153
if err != nil {
99154
return
100155
}
101-
for _, def := range defs {
102-
_, err = ouidb.Insert(def)
103-
if err != nil {
104-
return
105-
}
106-
records++
156+
records, err = ouidb.BulkInsert(defs)
157+
if err != nil {
158+
return
107159
}
108160
return
109161
}
@@ -184,6 +236,7 @@ func New(opts ...Option) (*OUIDB, error) {
184236
}
185237
} else if options.dialect == dialectPsql {
186238
q := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %v ( id SERIAL PRIMARY KEY, prefix VARCHAR(32) NOT NULL, length INT NOT NULL, org VARCHAR(256) NOT NULL, registry VARCHAR(32) NOT NULL, UNIQUE(prefix, length, registry) )", options.Version)
239+
options.Connection.SetMaxOpenConns(int(options.MaxConnections))
187240
_, err := options.Connection.Exec(q)
188241
if err != nil {
189242
return nil, err

oui/db_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func Test_New(t *testing.T) {
2020
t.Run("postgres", func(t *testing.T) {
2121
t.Parallel()
2222
password := os.Getenv("POSTGRES_PASSWORD")
23-
require.NotEqual(t, "", password)
23+
require.NotEqual(t, "", password, "missing POSTGRES_PASSWORD environment variable")
2424
cs := fmt.Sprintf("postgresql://oui:%s@localhost/oui?sslmode=disable", password)
2525
psql, err := oui.CreatePostgresOption(cs)
2626
require.NoError(t, err)

oui/options.go

+17-9
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import (
1212
)
1313

1414
type Options struct {
15-
Logger *LoggerType
16-
Progress *progress.Progress
17-
Version string
18-
Connection *sql.DB
19-
dialect int
15+
Logger *LoggerType
16+
Progress *progress.Progress
17+
Version string
18+
Connection *sql.DB
19+
dialect int
20+
MaxConnections uint
2021
}
2122

2223
type Option func(*Options)
@@ -45,12 +46,19 @@ func WithConnection(conn *sql.DB) Option {
4546
}
4647
}
4748

49+
func WithMaxConnections(max uint) Option {
50+
return func(opts *Options) {
51+
opts.MaxConnections = max
52+
}
53+
}
54+
4855
func getOptions(setters ...Option) *Options {
4956
options := &Options{
50-
Logger: nil,
51-
Progress: nil,
52-
Version: "default",
53-
Connection: nil,
57+
Logger: nil,
58+
Progress: nil,
59+
Version: "default",
60+
Connection: nil,
61+
MaxConnections: 0,
5462
}
5563
for _, setter := range setters {
5664
setter(options)

oui/types.go

+5
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ const (
2626
dialectSqlite int = iota
2727
dialectPsql
2828
)
29+
30+
const (
31+
maxVarsSqlite int = 999
32+
maxVarsPsql int = 65535
33+
)

0 commit comments

Comments
 (0)