master
go 185 lines 4.29 KB
Raw
1 // Package db2 provides IBM DB2 database access helpers for the ibm.d framework.
2 // SPDX-License-Identifier: GPL-3.0-or-later
3
4 //go:build cgo
5
6 package db2
7
8 import (
9 "context"
10 "database/sql"
11 "errors"
12 "fmt"
13 "time"
14
15 _ "github.com/netdata/netdata/go/plugins/plugin/ibm.d/pkg/dbdriver"
16 )
17
18 // Config represents the connection options required to talk to IBM DB2.
19 type Config struct {
20 DSN string
21 Timeout time.Duration
22 MaxOpenConns int
23 ConnMaxLife time.Duration
24 }
25
26 // Client wraps the SQL connection and exposes typed query helpers used by the collector.
27 type Client struct {
28 cfg Config
29 db *sql.DB
30 }
31
32 // NewClient creates a new DB2 client with the supplied configuration.
33 func NewClient(cfg Config) *Client {
34 return &Client{cfg: cfg}
35 }
36
37 // Connect ensures the underlying connection is ready.
38 func (c *Client) Connect(ctx context.Context) error {
39 if c.db != nil {
40 return nil
41 }
42
43 if c.cfg.DSN == "" {
44 return errors.New("db2 protocol: DSN is required")
45 }
46
47 db, err := sql.Open("odbcbridge", c.cfg.DSN)
48 if err != nil {
49 return fmt.Errorf("db2 protocol: opening connection failed: %w", err)
50 }
51
52 if c.cfg.MaxOpenConns > 0 {
53 db.SetMaxOpenConns(c.cfg.MaxOpenConns)
54 }
55 if c.cfg.ConnMaxLife > 0 {
56 db.SetConnMaxLifetime(c.cfg.ConnMaxLife)
57 }
58
59 pingCtx, cancel := context.WithTimeout(ctx, c.effectiveTimeout())
60 defer cancel()
61
62 if err := db.PingContext(pingCtx); err != nil {
63 _ = db.Close()
64 return fmt.Errorf("db2 protocol: ping failed: %w", err)
65 }
66
67 c.db = db
68 return nil
69 }
70
71 // Ping verifies connectivity on an existing connection.
72 func (c *Client) Ping(ctx context.Context) error {
73 if c.db == nil {
74 return errors.New("db2 protocol: ping called before connect")
75 }
76 pingCtx, cancel := context.WithTimeout(ctx, c.effectiveTimeout())
77 defer cancel()
78 return c.db.PingContext(pingCtx)
79 }
80
81 // Close terminates the connection.
82 func (c *Client) Close() error {
83 if c.db == nil {
84 return nil
85 }
86 err := c.db.Close()
87 c.db = nil
88 return err
89 }
90
91 // DoQuery executes a query and streams rows to the handler.
92 func (c *Client) DoQuery(ctx context.Context, query string, fn func(column, value string, lineEnd bool)) error {
93 rows, cancel, err := c.QueryRows(ctx, query)
94 if err != nil {
95 return err
96 }
97 defer cancel()
98 defer rows.Close()
99
100 return c.readRows(rows, fn)
101 }
102
103 // DoQueryRow executes a query expected to return a single row.
104 func (c *Client) DoQueryRow(ctx context.Context, query string, fn func(column, value string)) error {
105 rows, cancel, err := c.QueryRows(ctx, query)
106 if err != nil {
107 return err
108 }
109 defer cancel()
110 defer rows.Close()
111
112 columns, err := rows.Columns()
113 if err != nil {
114 return fmt.Errorf("db2 protocol: fetching columns failed: %w", err)
115 }
116
117 scan := make([]sql.RawBytes, len(columns))
118 pointers := make([]any, len(columns))
119 for i := range scan {
120 pointers[i] = &scan[i]
121 }
122
123 if rows.Next() {
124 if err := rows.Scan(pointers...); err != nil {
125 return fmt.Errorf("db2 protocol: scanning row failed: %w", err)
126 }
127 for idx, col := range columns {
128 fn(col, string(scan[idx]))
129 }
130 }
131
132 return rows.Err()
133 }
134
135 // QueryRows runs the query and returns the raw rows.
136 func (c *Client) QueryRows(ctx context.Context, query string) (*sql.Rows, context.CancelFunc, error) {
137 if err := c.Connect(ctx); err != nil {
138 return nil, nil, err
139 }
140
141 queryCtx, cancel := context.WithTimeout(ctx, c.effectiveTimeout())
142 rows, err := c.db.QueryContext(queryCtx, query)
143 if err != nil {
144 cancel()
145 return nil, nil, fmt.Errorf("db2 protocol: query failed: %w", err)
146 }
147
148 return rows, cancel, nil
149 }
150
151 func (c *Client) readRows(rows *sql.Rows, fn func(column, value string, lineEnd bool)) error {
152 columns, err := rows.Columns()
153 if err != nil {
154 return fmt.Errorf("db2 protocol: reading columns failed: %w", err)
155 }
156
157 scan := make([]sql.RawBytes, len(columns))
158 pointers := make([]any, len(columns))
159 for i := range scan {
160 pointers[i] = &scan[i]
161 }
162
163 for rows.Next() {
164 if err := rows.Scan(pointers...); err != nil {
165 return fmt.Errorf("db2 protocol: scanning row failed: %w", err)
166 }
167 for idx, col := range columns {
168 fn(col, string(scan[idx]), idx == len(columns)-1)
169 }
170 }
171
172 return rows.Err()
173 }
174
175 func (c *Client) effectiveTimeout() time.Duration {
176 if c.cfg.Timeout > 0 {
177 return c.cfg.Timeout
178 }
179 return 5 * time.Second
180 }
181
182 // DB exposes the underlying handle for operations that still rely on *sql.DB.
183 func (c *Client) DB() *sql.DB {
184 return c.db
185 }