Skip to content

Commit 4839844

Browse files
authored
Merge pull request #277 from retran/feature/catalogdb-interface
refactor: extract CatalogDB interface for WASM portability
2 parents 0cad20d + 7dc3cc6 commit 4839844

11 files changed

Lines changed: 280 additions & 44 deletions

mdl/catalog/builder.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
package catalog
44

55
import (
6-
"database/sql"
76
"fmt"
87
"strings"
98
"time"
@@ -79,7 +78,7 @@ type Builder struct {
7978
snapshot *Snapshot
8079
progress ProgressFunc
8180
hierarchy *hierarchy
82-
tx *sql.Tx // Transaction for batched inserts
81+
tx CatalogTx // Transaction for batched inserts
8382
fullMode bool // If true, do full parsing (activities/widgets)
8483
sourceMode bool // If true, build source FTS table (implies full)
8584
describeFunc DescribeFunc

mdl/catalog/catalog.go

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ import (
88
"fmt"
99
"strings"
1010
"time"
11-
12-
_ "modernc.org/sqlite"
1311
)
1412

1513
// Catalog provides SQL querying over Mendix project metadata.
1614
type Catalog struct {
17-
db *sql.DB
15+
db CatalogDB
1816
projectID string
1917
projectName string
2018
mendixVersion string
@@ -58,17 +56,15 @@ type ProgressFunc func(table string, count int)
5856

5957
// New creates a new catalog with an in-memory SQLite database.
6058
func New() (*Catalog, error) {
61-
db, err := sql.Open("sqlite", ":memory:")
59+
db, err := NewSqliteCatalogDB()
6260
if err != nil {
6361
return nil, fmt.Errorf("failed to open in-memory database: %w", err)
6462
}
63+
return NewFromDB(db)
64+
}
6565

66-
// Enable foreign keys
67-
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
68-
db.Close()
69-
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
70-
}
71-
66+
// NewFromDB creates a catalog from an existing CatalogDB.
67+
func NewFromDB(db CatalogDB) (*Catalog, error) {
7268
c := &Catalog{
7369
db: db,
7470
projectID: "default",
@@ -193,8 +189,8 @@ func (c *Catalog) Query(sqlQuery string) (*QueryResult, error) {
193189
return result, rows.Err()
194190
}
195191

196-
// DB returns the underlying database connection for direct access.
197-
func (c *Catalog) DB() *sql.DB {
192+
// CatalogDB returns the underlying database abstraction.
193+
func (c *Catalog) CatalogDB() CatalogDB {
198194
return c.db
199195
}
200196

@@ -263,17 +259,11 @@ type CacheInfo struct {
263259

264260
// NewFromFile opens a catalog from a persisted SQLite file.
265261
func NewFromFile(path string) (*Catalog, error) {
266-
db, err := sql.Open("sqlite", path)
262+
db, err := NewSqliteCatalogDBFromFile(path)
267263
if err != nil {
268264
return nil, fmt.Errorf("failed to open catalog file: %w", err)
269265
}
270266

271-
// Enable foreign keys
272-
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
273-
db.Close()
274-
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
275-
}
276-
277267
c := &Catalog{
278268
db: db,
279269
projectID: "default",
@@ -286,7 +276,14 @@ func NewFromFile(path string) (*Catalog, error) {
286276

287277
// SaveToFile saves the catalog to a SQLite file.
288278
// This copies the in-memory database to a file for persistence.
279+
// Requires the underlying CatalogDB to be a *SqliteCatalogDB.
289280
func (c *Catalog) SaveToFile(path string) error {
281+
sdb, ok := c.db.(*SqliteCatalogDB)
282+
if !ok {
283+
return fmt.Errorf("SaveToFile requires a SQLite-backed catalog")
284+
}
285+
rawDB := sdb.RawDB()
286+
290287
// Open destination file database
291288
destDB, err := sql.Open("sqlite", path)
292289
if err != nil {
@@ -296,17 +293,17 @@ func (c *Catalog) SaveToFile(path string) error {
296293

297294
// Use SQLite backup API via VACUUM INTO (SQLite 3.27+)
298295
// Fall back to manual copy if not available
299-
_, err = c.db.Exec(fmt.Sprintf("VACUUM INTO '%s'", path))
296+
_, err = rawDB.Exec(fmt.Sprintf("VACUUM INTO '%s'", path))
300297
if err != nil {
301298
// Fall back: export and import
302-
return c.saveToFileManual(path)
299+
return c.saveToFileManual(path, rawDB)
303300
}
304301

305302
return nil
306303
}
307304

308305
// saveToFileManual saves by dumping and restoring (fallback method).
309-
func (c *Catalog) saveToFileManual(path string) error {
306+
func (c *Catalog) saveToFileManual(path string, rawDB *sql.DB) error {
310307
// Open destination
311308
destDB, err := sql.Open("sqlite", path)
312309
if err != nil {
@@ -315,7 +312,7 @@ func (c *Catalog) saveToFileManual(path string) error {
315312
defer destDB.Close()
316313

317314
// Get list of tables
318-
rows, err := c.db.Query("SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
315+
rows, err := rawDB.Query("SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
319316
if err != nil {
320317
return err
321318
}
@@ -339,7 +336,7 @@ func (c *Catalog) saveToFileManual(path string) error {
339336
}
340337

341338
// Copy data
342-
srcRows, err := c.db.Query(fmt.Sprintf("SELECT * FROM %s", t.name))
339+
srcRows, err := rawDB.Query(fmt.Sprintf("SELECT * FROM %s", t.name))
343340
if err != nil {
344341
return err
345342
}
@@ -377,7 +374,7 @@ func (c *Catalog) saveToFileManual(path string) error {
377374
}
378375

379376
// Copy views
380-
rows, err = c.db.Query("SELECT sql FROM sqlite_master WHERE type='view'")
377+
rows, err = rawDB.Query("SELECT sql FROM sqlite_master WHERE type='view'")
381378
if err != nil {
382379
return err
383380
}
@@ -394,7 +391,7 @@ func (c *Catalog) saveToFileManual(path string) error {
394391
rows.Close()
395392

396393
// Copy indexes
397-
rows, err = c.db.Query("SELECT sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL")
394+
rows, err = rawDB.Query("SELECT sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL")
398395
if err != nil {
399396
return err
400397
}

mdl/catalog/catalog_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func TestQueryWithData(t *testing.T) {
124124
defer cat.Close()
125125

126126
// Insert test data
127-
_, err = cat.DB().Exec(
127+
_, err = cat.CatalogDB().Exec(
128128
"INSERT INTO modules (Id, Name, QualifiedName, ModuleName) VALUES (?, ?, ?, ?)",
129129
"mod-1", "TestModule", "TestModule", "TestModule",
130130
)
@@ -280,7 +280,7 @@ func TestSaveAndLoadFromFile(t *testing.T) {
280280
}
281281

282282
cat.SetProject("proj-1", "TestApp", "10.0.0")
283-
_, err = cat.DB().Exec(
283+
_, err = cat.CatalogDB().Exec(
284284
"INSERT INTO modules (Id, Name, QualifiedName, ModuleName) VALUES (?, ?, ?, ?)",
285285
"mod-1", "MyModule", "MyModule", "MyModule",
286286
)
@@ -359,7 +359,7 @@ func TestRoleMappingsTable(t *testing.T) {
359359
}
360360

361361
for _, m := range mappings {
362-
_, err := cat.DB().Exec(
362+
_, err := cat.CatalogDB().Exec(
363363
"INSERT INTO role_mappings (UserRoleName, ModuleRoleName, ModuleName) VALUES (?, ?, ?)",
364364
m.userRole, m.moduleRole, m.module,
365365
)

mdl/catalog/catalogdb.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package catalog
4+
5+
import "database/sql"
6+
7+
// CatalogDB abstracts the database layer used by Catalog and LintContext.
8+
// The primary implementation wraps modernc.org/sqlite for native builds;
9+
// a js/wasm build will supply a host-backed implementation.
10+
type CatalogDB interface {
11+
Query(query string, args ...any) (*sql.Rows, error)
12+
QueryRow(query string, args ...any) *sql.Row
13+
Exec(query string, args ...any) (sql.Result, error)
14+
Begin() (CatalogTx, error)
15+
Close() error
16+
}
17+
18+
// CatalogTx abstracts a database transaction. Builder uses this for bulk
19+
// inserts with prepared statements.
20+
type CatalogTx interface {
21+
Prepare(query string) (*sql.Stmt, error)
22+
Exec(query string, args ...any) (sql.Result, error)
23+
QueryRow(query string, args ...any) *sql.Row
24+
Commit() error
25+
Rollback() error
26+
}

mdl/catalog/catalogdb_sqlite.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
//go:build !js
4+
5+
package catalog
6+
7+
import (
8+
"database/sql"
9+
10+
_ "modernc.org/sqlite"
11+
)
12+
13+
// SqliteCatalogDB wraps *sql.DB from modernc.org/sqlite.
14+
type SqliteCatalogDB struct {
15+
db *sql.DB
16+
}
17+
18+
// NewSqliteCatalogDB opens an in-memory SQLite database.
19+
func NewSqliteCatalogDB() (*SqliteCatalogDB, error) {
20+
db, err := sql.Open("sqlite", ":memory:")
21+
if err != nil {
22+
return nil, err
23+
}
24+
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
25+
db.Close()
26+
return nil, err
27+
}
28+
return &SqliteCatalogDB{db: db}, nil
29+
}
30+
31+
// NewSqliteCatalogDBFromFile opens a file-based SQLite database.
32+
func NewSqliteCatalogDBFromFile(path string) (*SqliteCatalogDB, error) {
33+
db, err := sql.Open("sqlite", path)
34+
if err != nil {
35+
return nil, err
36+
}
37+
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
38+
db.Close()
39+
return nil, err
40+
}
41+
return &SqliteCatalogDB{db: db}, nil
42+
}
43+
44+
func (s *SqliteCatalogDB) Query(query string, args ...any) (*sql.Rows, error) {
45+
return s.db.Query(query, args...)
46+
}
47+
48+
func (s *SqliteCatalogDB) QueryRow(query string, args ...any) *sql.Row {
49+
return s.db.QueryRow(query, args...)
50+
}
51+
52+
func (s *SqliteCatalogDB) Exec(query string, args ...any) (sql.Result, error) {
53+
return s.db.Exec(query, args...)
54+
}
55+
56+
func (s *SqliteCatalogDB) Begin() (CatalogTx, error) {
57+
return s.db.Begin()
58+
}
59+
60+
func (s *SqliteCatalogDB) Close() error {
61+
return s.db.Close()
62+
}
63+
64+
// RawDB returns the underlying *sql.DB. Used only for SQLite-specific
65+
// operations like VACUUM INTO in SaveToFile.
66+
func (s *SqliteCatalogDB) RawDB() *sql.DB {
67+
return s.db
68+
}
69+
70+
// WrapSqlDB wraps an existing *sql.DB as a CatalogDB.
71+
// Used by tests that create their own in-memory databases.
72+
func WrapSqlDB(db *sql.DB) *SqliteCatalogDB {
73+
return &SqliteCatalogDB{db: db}
74+
}

0 commit comments

Comments
 (0)