mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-01 01:10:02 +08:00
- 后端新增 IRIS 连接、查询、DDL、索引元数据和 DataGrid 编辑能力 - 接入 optional driver-agent、构建标签、revision 生成和变更检测流程 - 前端新增 IRIS 连接入口、方言映射、能力配置和图标展示 - 修复 IRIS 主键识别、事务开启错误处理和驱动连接关闭问题 - 补充后端、前端和构建脚本相关回归测试 Refs #408
286 lines
6.4 KiB
Markdown
286 lines
6.4 KiB
Markdown
# go-irisnative
|
||
|
||
A Golang driver for InterSystems IRIS that implements `database/sql`.
|
||
|
||
> Project status: **alpha**. API may change. Feedback and PRs welcome.
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
# replace the module path with the final repo path when published
|
||
go get github.com/caretdev/go-irisnative
|
||
```
|
||
|
||
Register the driver by importing it for side‑effects:
|
||
|
||
```go
|
||
import (
|
||
"database/sql"
|
||
_ "github.com/caretdev/go-irisnative" // registers driver as "iris"
|
||
)
|
||
```
|
||
|
||
## DSN formats
|
||
|
||
The driver accepts a URL-style DSN (recommended) or key=value pairs.
|
||
|
||
**URL style**
|
||
|
||
```
|
||
iris://user:password@host:1972/NAMESPACE?
|
||
```
|
||
|
||
* `host` — IRIS hostname or IP
|
||
* `1972` — superserver port (default)
|
||
* `Namespace` — IRIS namespace (e.g., `USER`)
|
||
|
||
---
|
||
|
||
## Quick start (database/sql)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"log"
|
||
"time"
|
||
|
||
_ "github.com/caretdev/go-irisnative"
|
||
)
|
||
|
||
func main() {
|
||
dsn := "iris://_SYSTEM:SYS@localhost:1972/USER"
|
||
db, err := sql.Open("iris", dsn)
|
||
if err != nil { log.Fatal(err) }
|
||
defer db.Close()
|
||
|
||
// Connection pool tuning
|
||
db.SetMaxOpenConns(10)
|
||
db.SetMaxIdleConns(5)
|
||
db.SetConnMaxLifetime(30 * time.Minute)
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
_, err = db.ExecContext(ctx, `DROP TABLE IF EXISTS demo_person`)
|
||
if err != nil { log.Fatal("drop table:", err) }
|
||
|
||
// 1) Create a table (id INT PRIMARY KEY, name VARCHAR(80))
|
||
_, err = db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS demo_person (
|
||
id INT PRIMARY KEY,
|
||
name VARCHAR(80) NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)`)
|
||
if err != nil { log.Fatal("create table:", err) }
|
||
|
||
// 2) Insert with placeholders
|
||
res, err := db.ExecContext(ctx, `INSERT INTO demo_person(id, name) VALUES(?, ?)`, 1, "Alice")
|
||
if err != nil { log.Fatal("insert:", err) }
|
||
if n, _ := res.RowsAffected(); n > 0 { fmt.Println("inserted:", n) }
|
||
|
||
// 3) Query rows
|
||
rows, err := db.QueryContext(ctx, `SELECT id, name, created_at FROM demo_person ORDER BY id`)
|
||
if err != nil { log.Fatal("query:", err) }
|
||
defer rows.Close()
|
||
|
||
for rows.Next() {
|
||
var (
|
||
id int
|
||
name string
|
||
createdAt time.Time
|
||
)
|
||
if err := rows.Scan(&id, &name, &createdAt); err != nil { log.Fatal(err) }
|
||
fmt.Printf("row: id=%d name=%s created_at=%s\n", id, name, createdAt.Format(time.RFC3339))
|
||
}
|
||
if err := rows.Err(); err != nil { log.Fatal(err) }
|
||
|
||
// 4) Prepared statement
|
||
stmt, err := db.PrepareContext(ctx, `UPDATE demo_person SET name=? WHERE id=?`)
|
||
if err != nil { log.Fatal("prepare:", err) }
|
||
defer stmt.Close()
|
||
if _, err := stmt.ExecContext(ctx, "Alice Updated", 1); err != nil { log.Fatal("update:", err) }
|
||
|
||
// 5) Transaction example
|
||
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
||
if err != nil { log.Fatal("begin tx:", err) }
|
||
if _, err := tx.ExecContext(ctx, `INSERT INTO demo_person(id, name) VALUES(?, ?)`, 2, "Bob"); err != nil {
|
||
tx.Rollback()
|
||
log.Fatal("tx insert:", err)
|
||
}
|
||
if err := tx.Commit(); err != nil { log.Fatal("commit:", err) }
|
||
}
|
||
```
|
||
|
||
### Query single value helper
|
||
|
||
```go
|
||
var count int
|
||
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM demo_person`).Scan(&count); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Println("count=", count)
|
||
```
|
||
|
||
---
|
||
|
||
## Using with `sqlx`
|
||
|
||
`sqlx` adds nice helpers over `database/sql` like struct scanning and named queries.
|
||
|
||
```bash
|
||
go get github.com/jmoiron/sqlx
|
||
```
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
"time"
|
||
|
||
_ "github.com/caretdev/go-irisnative" // driver
|
||
"github.com/jmoiron/sqlx"
|
||
)
|
||
|
||
type Person struct {
|
||
ID int `db:"id"`
|
||
Name string `db:"name"`
|
||
CreatedAt time.Time `db:"created_at"`
|
||
}
|
||
|
||
func create(ctx context.Context, db *sqlx.DB) {
|
||
drop(ctx, db)
|
||
_, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS demo_person (
|
||
id INT PRIMARY KEY,
|
||
name VARCHAR(80) NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)`)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
|
||
func drop(ctx context.Context, db *sqlx.DB) {
|
||
_, err := db.ExecContext(ctx, `DROP TABLE IF EXISTS demo_person`)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
}
|
||
|
||
func main() {
|
||
ctx := context.Background()
|
||
dsn := "iris://_SYSTEM:SYS@localhost:1972/USER"
|
||
db := sqlx.MustConnect("iris", dsn)
|
||
defer db.Close()
|
||
|
||
create(ctx, db)
|
||
defer drop(ctx, db)
|
||
|
||
// Struct-based insert with NamedExec
|
||
p := Person{ID: 3, Name: "Carol"}
|
||
_, err := db.NamedExecContext(ctx,
|
||
`INSERT INTO demo_person(id, name) VALUES(:id, :name)`, p,
|
||
)
|
||
if err != nil {
|
||
log.Fatal("named insert:", err)
|
||
}
|
||
|
||
// Select into slice of structs
|
||
var people []Person
|
||
if err := db.SelectContext(ctx, &people, `SELECT id, name, created_at FROM demo_person ORDER BY id`); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Printf("people: %#v\n", people)
|
||
|
||
// Get a single struct
|
||
var one Person
|
||
if err := db.GetContext(ctx, &one, `SELECT id, name, created_at FROM demo_person WHERE id=?`, people[0].ID); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Printf("one: %+v\n", one)
|
||
|
||
// Named query with IN (sqlx.In)
|
||
ids := []int{1, 2, 3}
|
||
q, args, err := sqlx.In(`SELECT id, name FROM demo_person WHERE id IN (?)`, ids)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
q = db.Rebind(q) // ensure driver-specific bindvars
|
||
rows, err := db.QueryxContext(ctx, q, args...)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer rows.Close()
|
||
for rows.Next() {
|
||
var id int
|
||
var name string
|
||
if err := rows.Scan(&id, &name); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Println(id, name)
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Placeholders & rebind
|
||
|
||
* The driver uses `?` positional placeholders.
|
||
* With `sqlx`, **always** call `db.Rebind(q)` after `sqlx.In(...)` to adapt placeholders.
|
||
|
||
---
|
||
|
||
## Context, timeouts & cancellations
|
||
|
||
All examples use `Context`. Set sensible timeouts to avoid runaway queries:
|
||
|
||
```go
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
```
|
||
|
||
---
|
||
|
||
## Error handling tips
|
||
|
||
* Check `rows.Err()` after iteration.
|
||
* Prefer `ExecContext`/`QueryContext` to ensure timeouts are respected.
|
||
* Wrap errors with operation context (e.g., `fmt.Errorf("create table: %w", err)`).
|
||
|
||
---
|
||
|
||
## Testing locally
|
||
|
||
1. Start IRIS and ensure SQL is enabled for your namespace (e.g., `USER`).
|
||
2. Create a SQL user with privileges to connect and create tables.
|
||
3. Verify connectivity using the DSN shown above.
|
||
|
||
---
|
||
|
||
## Compatibility
|
||
|
||
* Go: 1.21+
|
||
* InterSystems IRIS: 2025.1+
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT
|
||
|
||
---
|
||
|
||
## Contributing
|
||
|
||
* Run `go vet` and `go test ./...` before submitting PRs.
|
||
* Add tests for new behaviors.
|
||
* Document any DSN parameters you introduce.
|