Skip to content

Commit 43ccdf5

Browse files
ajitpratap0Ajit Pratap Singhclaude
authored
feat(integrations): add OpenTelemetry and GORM sub-modules (#451 #452)
* docs: add Q2 2026 roadmap design spec from multi-persona audit Full-project audit using 5 parallel analytical personas (Performance, SQL Compatibility, API/DX, Competitive, Community). Synthesizes into prioritized P0–P3 roadmap covering: HN launch, query fingerprinting, linter expansion to 30 rules, DML transforms, C binding hardening, live DB schema introspection, SQL transpilation, CONNECT BY, OTel, GORM integration, and advisor expansion. Corresponding GitHub issues: #442#460 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add Q2 2026 implementation plans for all roadmap items 12 implementation plans covering all prioritized GitHub issues: P0 (Critical): - 2026-03-29-sentry-fixes.md (#434, #437) — fix Sentry noise filters - 2026-03-29-openssf-scorecard.md (#443) — security scorecard badge P1 (High Priority): - 2026-03-29-query-fingerprinting.md (#444) — SQL normalization + SHA-256 fingerprints - 2026-03-29-linter-expansion.md (#445) — L011-L030 safety/performance/naming rules - 2026-03-29-dml-transforms.md (#446) — SET clause and RETURNING transforms - 2026-03-29-cbinding-hardening.md (#447) — C binding coverage + stress tests - 2026-03-29-advisor-expansion.md (#453) — OPT-009 through OPT-020 advisor rules P2 (Medium Priority): - 2026-03-29-sql-parser-additions.md (#450, #454, #455, #456) — DDL formatter, CONNECT BY, SAMPLE, PIVOT/UNPIVOT - 2026-03-29-schema-introspection.md (#448) — live DB schema introspection (Postgres, MySQL, SQLite) - 2026-03-29-integrations.md (#451, #452) — OpenTelemetry + GORM sub-modules - 2026-03-29-sql-transpilation.md (#449) — SQL dialect transpilation API P3 (Low Priority): - 2026-03-29-p3-items.md (#458, #459, #460) — CLI watch registration, pool stats, JSON functions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(integrations): add OpenTelemetry and GORM sub-modules (#451 #452) - integrations/opentelemetry: InstrumentedParse() wraps gosqlx.Parse() with OTel spans (db.system, db.statement.type, db.sql.tables, db.sql.columns); errors recorded on span with status Error; 3 tests pass with race detector - integrations/gorm: Plugin struct implementing gorm.Plugin; afterStatement callback normalizes GORM SQL (backtick→double-quote, ?→$N) before parsing; Stats()/Reset() API; nil Statement guard; 5 tests pass with race detector - .github/workflows/integrations.yml: separate jobs for each sub-module - CHANGELOG.md: document new sub-modules under [Unreleased] Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(integrations): bump OTel SDK v1.26→v1.42 to resolve CVE-2026-24051 Updates go.opentelemetry.io/otel/sdk from v1.26.0 to v1.42.0 to fix HIGH severity PATH hijacking vulnerability (GHSA-9h8m-3fm2-qjrq). Also bumps transitive deps: logr v1.4.3, sys v0.41.0. * fix(integrations): address PR #465 review — CI Go version, GORM history limit, error callback - CI workflow: bump go-version from 1.23 to 1.26 to match project requirements - GORM plugin: add maxHistory field (default 1000) to bound query record growth - GORM plugin: add OnParseError callback for visibility into parse failures - GORM plugin: add NewPluginWithOptions constructor with PluginOptions struct - Tests: add coverage for MaxHistory trimming, OnParseError callback, defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Ajit Pratap Singh <ajitpratapsingh@Ajits-Mac-mini-2655.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 88599d5 commit 43ccdf5

File tree

10 files changed

+637
-0
lines changed

10 files changed

+637
-0
lines changed

.github/workflows/integrations.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Integrations
2+
3+
on:
4+
push:
5+
paths:
6+
- 'integrations/**'
7+
- '.github/workflows/integrations.yml'
8+
pull_request:
9+
paths:
10+
- 'integrations/**'
11+
- '.github/workflows/integrations.yml'
12+
13+
jobs:
14+
opentelemetry:
15+
name: OpenTelemetry Integration
16+
runs-on: ubuntu-latest
17+
defaults:
18+
run:
19+
working-directory: integrations/opentelemetry
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: actions/setup-go@v5
23+
with:
24+
go-version: '1.26'
25+
- run: go mod tidy
26+
- run: go test -race -timeout 60s ./...
27+
28+
gorm:
29+
name: GORM Integration
30+
runs-on: ubuntu-latest
31+
defaults:
32+
run:
33+
working-directory: integrations/gorm
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: actions/setup-go@v5
37+
with:
38+
go-version: '1.26'
39+
- run: go mod tidy
40+
- run: go test -race -timeout 60s ./...

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- `gosqlx.Transpile()` top-level convenience wrapper
1616
- `gosqlx transpile --from <dialect> --to <dialect>` CLI subcommand
1717
- **MariaDB dialect** (`--dialect mariadb`): New SQL dialect extending MySQL with support for SEQUENCE DDL (`CREATE/DROP/ALTER SEQUENCE` with full option set), temporal tables (`FOR SYSTEM_TIME`, `WITH SYSTEM VERSIONING`, `PERIOD FOR`), and `CONNECT BY` hierarchical queries with `PRIOR`, `START WITH`, and `NOCYCLE`
18+
- `integrations/opentelemetry/` sub-module: `InstrumentedParse()` wraps `gosqlx.Parse()` with OpenTelemetry spans including `db.system`, `db.statement.type`, `db.sql.tables`, `db.sql.columns` attributes
19+
- `integrations/gorm/` sub-module: GORM plugin that records executed query metadata (tables, columns, statement type) via GoSQLX parsing with GORM SQL normalization (backtick identifiers, `?` placeholders); exposes `Stats()` and `Reset()` APIs
20+
- CI workflow for integration sub-modules (`.github/workflows/integrations.yml`)
1821

1922
## [1.13.0] - 2026-03-20
2023

integrations/gorm/go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module github.com/ajitpratap0/GoSQLX/integrations/gorm
2+
3+
go 1.26.1
4+
5+
require (
6+
github.com/ajitpratap0/GoSQLX v1.13.0
7+
gorm.io/driver/sqlite v1.5.6
8+
gorm.io/gorm v1.25.10
9+
)
10+
11+
require (
12+
github.com/jinzhu/inflection v1.0.0 // indirect
13+
github.com/jinzhu/now v1.1.5 // indirect
14+
github.com/mattn/go-sqlite3 v1.14.22 // indirect
15+
)
16+
17+
replace github.com/ajitpratap0/GoSQLX => ../../

integrations/gorm/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
2+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
3+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
4+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
5+
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
6+
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
7+
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
8+
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
9+
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
10+
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

integrations/gorm/plugin.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Package gosqlxgorm provides a GORM plugin that parses each executed query
2+
// with GoSQLX and records extracted metadata (tables, columns, statement type).
3+
package gosqlxgorm
4+
5+
import (
6+
"regexp"
7+
"strconv"
8+
"strings"
9+
"sync"
10+
11+
"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
12+
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
13+
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
14+
"gorm.io/gorm"
15+
)
16+
17+
// reQuestionMark replaces GORM's ? positional placeholders.
18+
var reQuestionMark = regexp.MustCompile(`\?`)
19+
20+
// QueryRecord holds metadata about a single recorded GORM query.
21+
type QueryRecord struct {
22+
SQL string
23+
Tables []string
24+
Columns []string
25+
Type string // SELECT, INSERT, UPDATE, DELETE, ...
26+
ParseOK bool
27+
}
28+
29+
// PluginStats is the aggregate of all queries observed since initialization.
30+
type PluginStats struct {
31+
TotalQueries int
32+
ParseErrors int
33+
Queries []QueryRecord
34+
}
35+
36+
// defaultMaxHistory is the default maximum number of query records kept.
37+
const defaultMaxHistory = 1000
38+
39+
// PluginOptions configures the GORM plugin behavior.
40+
type PluginOptions struct {
41+
// MaxHistory limits the number of query records kept. Zero uses the default (1000).
42+
MaxHistory int
43+
// OnParseError is called when GoSQLX fails to parse a query. Optional.
44+
OnParseError func(sql string, err error)
45+
}
46+
47+
// Plugin is a GORM plugin that parses each executed query with GoSQLX
48+
// and records extracted metadata (tables, columns, statement type).
49+
type Plugin struct {
50+
mu sync.Mutex
51+
queries []QueryRecord
52+
maxHistory int
53+
onParseError func(sql string, err error)
54+
}
55+
56+
// NewPlugin returns a new GoSQLX GORM plugin with default options.
57+
func NewPlugin() *Plugin {
58+
return &Plugin{maxHistory: defaultMaxHistory}
59+
}
60+
61+
// NewPluginWithOptions returns a new GoSQLX GORM plugin configured with opts.
62+
func NewPluginWithOptions(opts PluginOptions) *Plugin {
63+
mh := opts.MaxHistory
64+
if mh <= 0 {
65+
mh = defaultMaxHistory
66+
}
67+
return &Plugin{
68+
maxHistory: mh,
69+
onParseError: opts.OnParseError,
70+
}
71+
}
72+
73+
// Name implements gorm.Plugin.
74+
func (p *Plugin) Name() string { return "gosqlx" }
75+
76+
// Initialize implements gorm.Plugin by registering after-callbacks.
77+
func (p *Plugin) Initialize(db *gorm.DB) error {
78+
db.Callback().Query().After("gorm:query").Register("gosqlx:after_query", p.afterStatement)
79+
db.Callback().Create().After("gorm:create").Register("gosqlx:after_create", p.afterStatement)
80+
db.Callback().Update().After("gorm:update").Register("gosqlx:after_update", p.afterStatement)
81+
db.Callback().Delete().After("gorm:delete").Register("gosqlx:after_delete", p.afterStatement)
82+
db.Callback().Raw().After("gorm:raw").Register("gosqlx:after_raw", p.afterStatement)
83+
return nil
84+
}
85+
86+
func (p *Plugin) afterStatement(db *gorm.DB) {
87+
// Guard against nil Statement — this can happen during initialization callbacks.
88+
if db.Statement == nil {
89+
return
90+
}
91+
sql := db.Statement.SQL.String()
92+
if sql == "" {
93+
return
94+
}
95+
96+
rec := QueryRecord{SQL: sql}
97+
98+
// Normalize GORM-generated SQL for GoSQLX compatibility:
99+
// 1. Replace backtick-quoted identifiers with double-quoted identifiers
100+
// (GORM SQLite/MySQL driver uses backticks; GoSQLX standard mode uses double-quotes).
101+
// 2. Replace ? positional placeholders with $N (PostgreSQL style).
102+
normalized := normalizeSQLForParsing(sql)
103+
104+
// Try PostgreSQL dialect (handles double-quoted identifiers and $N placeholders),
105+
// then fall back to standard SQL parsing.
106+
tree, err := gosqlx.ParseWithDialect(normalized, keywords.DialectPostgreSQL)
107+
if err != nil {
108+
tree, err = gosqlx.Parse(normalized)
109+
}
110+
if err != nil {
111+
rec.ParseOK = false
112+
if p.onParseError != nil {
113+
p.onParseError(sql, err)
114+
}
115+
} else {
116+
rec.ParseOK = true
117+
rec.Tables = gosqlx.ExtractTables(tree)
118+
rec.Columns = gosqlx.ExtractColumns(tree)
119+
if tree != nil && len(tree.Statements) > 0 {
120+
rec.Type = stmtTypeName(tree.Statements[0])
121+
}
122+
}
123+
124+
p.mu.Lock()
125+
p.queries = append(p.queries, rec)
126+
if len(p.queries) > p.maxHistory {
127+
// Trim oldest entries to stay within the limit.
128+
excess := len(p.queries) - p.maxHistory
129+
copy(p.queries, p.queries[excess:])
130+
p.queries = p.queries[:p.maxHistory]
131+
}
132+
p.mu.Unlock()
133+
}
134+
135+
// normalizeSQLForParsing converts GORM-generated SQL into a form that GoSQLX
136+
// can parse: backtick identifiers become double-quoted, and ? placeholders
137+
// become $N placeholders.
138+
func normalizeSQLForParsing(sql string) string {
139+
// Replace backtick-quoted identifiers with double-quoted identifiers.
140+
sql = strings.ReplaceAll(sql, "`", "\"")
141+
// Replace ? positional placeholders with $N.
142+
n := 0
143+
sql = reQuestionMark.ReplaceAllStringFunc(sql, func(string) string {
144+
n++
145+
return "$" + strconv.Itoa(n)
146+
})
147+
return sql
148+
}
149+
150+
// Stats returns a snapshot of all recorded queries.
151+
func (p *Plugin) Stats() PluginStats {
152+
p.mu.Lock()
153+
defer p.mu.Unlock()
154+
var errCount int
155+
for _, q := range p.queries {
156+
if !q.ParseOK {
157+
errCount++
158+
}
159+
}
160+
qs := make([]QueryRecord, len(p.queries))
161+
copy(qs, p.queries)
162+
return PluginStats{
163+
TotalQueries: len(p.queries),
164+
ParseErrors: errCount,
165+
Queries: qs,
166+
}
167+
}
168+
169+
// Reset clears all recorded queries.
170+
func (p *Plugin) Reset() {
171+
p.mu.Lock()
172+
p.queries = p.queries[:0]
173+
p.mu.Unlock()
174+
}
175+
176+
// stmtTypeName returns a human-readable SQL statement type name.
177+
func stmtTypeName(stmt ast.Statement) string {
178+
switch stmt.(type) {
179+
case *ast.SelectStatement:
180+
return "SELECT"
181+
case *ast.InsertStatement:
182+
return "INSERT"
183+
case *ast.UpdateStatement:
184+
return "UPDATE"
185+
case *ast.DeleteStatement:
186+
return "DELETE"
187+
default:
188+
return "OTHER"
189+
}
190+
}

0 commit comments

Comments
 (0)