From 203338eef2c93bcc5524558cc83a8914835957d7 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 18 Mar 2026 18:10:47 +0400 Subject: [PATCH 1/2] Add x-no-lock option to database drivers with real distributed locks Add x-no-lock URL parameter to postgres, pgx, pgx/v5, sqlserver, cockroachdb, and yugabytedb drivers following the existing MySQL pattern. When set to true, Lock()/Unlock() become no-ops, allowing driver initialization and Version() calls without acquiring advisory or table-based locks. Drivers with in-process-only atomic locks (sqlite, clickhouse, etc.) are not changed as they never block. --- database/cockroachdb/README.md | 1 + database/cockroachdb/cockroachdb.go | 18 ++++++++++++++++++ database/pgx/README.md | 1 + database/pgx/pgx.go | 18 ++++++++++++++++++ database/pgx/v5/README.md | 1 + database/pgx/v5/pgx.go | 18 ++++++++++++++++++ database/postgres/README.md | 1 + database/postgres/postgres.go | 18 ++++++++++++++++++ database/sqlserver/README.md | 1 + database/sqlserver/sqlserver.go | 18 ++++++++++++++++++ database/yugabytedb/README.md | 1 + database/yugabytedb/yugabytedb.go | 19 +++++++++++++++++++ 12 files changed, 115 insertions(+) diff --git a/database/cockroachdb/README.md b/database/cockroachdb/README.md index 7931c2791..b7f327bce 100644 --- a/database/cockroachdb/README.md +++ b/database/cockroachdb/README.md @@ -7,6 +7,7 @@ | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | | `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock | | `x-force-lock` | `ForceLock` | Force lock acquisition to fix faulty migrations which may not have released the schema lock (Boolean, default is `false`) | +| `x-no-lock` | `NoLock` | Set to `true` to skip lock table acquisition. Useful for read-only checks. Only run migrations from one host when this is enabled. | | `dbname` | `DatabaseName` | The name of the database to connect to | | `user` | | The user to sign in as | | `password` | | The user's password | diff --git a/database/cockroachdb/cockroachdb.go b/database/cockroachdb/cockroachdb.go index 7af1d2efd..d48f3cf7d 100644 --- a/database/cockroachdb/cockroachdb.go +++ b/database/cockroachdb/cockroachdb.go @@ -37,6 +37,7 @@ type Config struct { LockTable string ForceLock bool DatabaseName string + NoLock bool } type CockroachDb struct { @@ -127,11 +128,20 @@ func (c *CockroachDb) Open(url string) (database.Driver, error) { forceLock = false } + noLock := false + if s := purl.Query().Get("x-no-lock"); len(s) > 0 { + noLock, err = strconv.ParseBool(s) + if err != nil { + return nil, fmt.Errorf("unable to parse option x-no-lock: %w", err) + } + } + px, err := WithInstance(db, &Config{ DatabaseName: purl.Path, MigrationsTable: migrationsTable, LockTable: lockTable, ForceLock: forceLock, + NoLock: noLock, }) if err != nil { return nil, err @@ -148,6 +158,10 @@ func (c *CockroachDb) Close() error { // See: https://github.com/cockroachdb/cockroach/issues/13546 func (c *CockroachDb) Lock() error { return database.CasRestoreOnErr(&c.isLocked, false, true, database.ErrLocked, func() (err error) { + if c.config.NoLock { + return nil + } + return crdb.ExecuteTx(context.Background(), c.db, nil, func(tx *sql.Tx) (err error) { aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName) if err != nil { @@ -185,6 +199,10 @@ func (c *CockroachDb) Lock() error { // See: https://github.com/cockroachdb/cockroach/issues/13546 func (c *CockroachDb) Unlock() error { return database.CasRestoreOnErr(&c.isLocked, true, false, database.ErrNotLocked, func() (err error) { + if c.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName) if err != nil { return err diff --git a/database/pgx/README.md b/database/pgx/README.md index bec7c5c75..ef0621832 100644 --- a/database/pgx/README.md +++ b/database/pgx/README.md @@ -13,6 +13,7 @@ This package is for [pgx/v4](https://pkg.go.dev/github.com/jackc/pgx/v4). A back | `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) | | `x-lock-strategy` | `LockStrategy` | Strategy used for locking during migration (default: advisory) | | `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock (default: schema_lock) | +| `x-no-lock` | `NoLock` | Set to `true` to skip advisory lock/table lock calls. Useful for read-only checks or multi-master setups. Only run migrations from one host when this is enabled. | | `dbname` | `DatabaseName` | The name of the database to connect to | | `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | | `user` | | The user to sign in as | diff --git a/database/pgx/pgx.go b/database/pgx/pgx.go index e97f15e9f..33a15a43c 100644 --- a/database/pgx/pgx.go +++ b/database/pgx/pgx.go @@ -63,6 +63,7 @@ type Config struct { MigrationsTableQuoted bool MultiStatementEnabled bool MultiStatementMaxSize int + NoLock bool } type Postgres struct { @@ -219,6 +220,14 @@ func (p *Postgres) Open(url string) (database.Driver, error) { lockStrategy := purl.Query().Get("x-lock-strategy") lockTable := purl.Query().Get("x-lock-table") + noLock := false + if s := purl.Query().Get("x-no-lock"); len(s) > 0 { + noLock, err = strconv.ParseBool(s) + if err != nil { + return nil, fmt.Errorf("unable to parse option x-no-lock: %w", err) + } + } + px, err := WithInstance(db, &Config{ DatabaseName: purl.Path, MigrationsTable: migrationsTable, @@ -228,6 +237,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) { MultiStatementMaxSize: multiStatementMaxSize, LockStrategy: lockStrategy, LockTable: lockTable, + NoLock: noLock, }) if err != nil { @@ -248,6 +258,10 @@ func (p *Postgres) Close() error { func (p *Postgres) Lock() error { return database.CasRestoreOnErr(&p.isLocked, false, true, database.ErrLocked, func() error { + if p.config.NoLock { + return nil + } + switch p.config.LockStrategy { case LockStrategyAdvisory: return p.applyAdvisoryLock() @@ -261,6 +275,10 @@ func (p *Postgres) Lock() error { func (p *Postgres) Unlock() error { return database.CasRestoreOnErr(&p.isLocked, true, false, database.ErrNotLocked, func() error { + if p.config.NoLock { + return nil + } + switch p.config.LockStrategy { case LockStrategyAdvisory: return p.releaseAdvisoryLock() diff --git a/database/pgx/v5/README.md b/database/pgx/v5/README.md index 1c00710e3..06033e057 100644 --- a/database/pgx/v5/README.md +++ b/database/pgx/v5/README.md @@ -11,6 +11,7 @@ This package is for [pgx/v5](https://pkg.go.dev/github.com/jackc/pgx/v5). A back | `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds | | `x-multi-statement` | `MultiStatementEnabled` | Enable multi-statement execution (default: false) | | `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) | +| `x-no-lock` | `NoLock` | Set to `true` to skip `pg_advisory_lock`/`pg_advisory_unlock` calls. Useful for read-only checks or multi-master setups. Only run migrations from one host when this is enabled. | | `dbname` | `DatabaseName` | The name of the database to connect to | | `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | | `user` | | The user to sign in as | diff --git a/database/pgx/v5/pgx.go b/database/pgx/v5/pgx.go index ef5f05674..01c02fac1 100644 --- a/database/pgx/v5/pgx.go +++ b/database/pgx/v5/pgx.go @@ -51,6 +51,7 @@ type Config struct { MigrationsTableQuoted bool MultiStatementEnabled bool MultiStatementMaxSize int + NoLock bool } type Postgres struct { @@ -192,6 +193,14 @@ func (p *Postgres) Open(url string) (database.Driver, error) { } } + noLock := false + if s := purl.Query().Get("x-no-lock"); len(s) > 0 { + noLock, err = strconv.ParseBool(s) + if err != nil { + return nil, fmt.Errorf("unable to parse option x-no-lock: %w", err) + } + } + px, err := WithInstance(db, &Config{ DatabaseName: purl.Path, MigrationsTable: migrationsTable, @@ -199,6 +208,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) { StatementTimeout: time.Duration(statementTimeout) * time.Millisecond, MultiStatementEnabled: multiStatementEnabled, MultiStatementMaxSize: multiStatementMaxSize, + NoLock: noLock, }) if err != nil { @@ -220,6 +230,10 @@ func (p *Postgres) Close() error { // https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS func (p *Postgres) Lock() error { return database.CasRestoreOnErr(&p.isLocked, false, true, database.ErrLocked, func() error { + if p.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName) if err != nil { return err @@ -236,6 +250,10 @@ func (p *Postgres) Lock() error { func (p *Postgres) Unlock() error { return database.CasRestoreOnErr(&p.isLocked, true, false, database.ErrNotLocked, func() error { + if p.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName) if err != nil { return err diff --git a/database/postgres/README.md b/database/postgres/README.md index bc823f4e1..5f60a0792 100644 --- a/database/postgres/README.md +++ b/database/postgres/README.md @@ -9,6 +9,7 @@ | `x-statement-timeout` | `StatementTimeout` | Abort any statement that takes more than the specified number of milliseconds | | `x-multi-statement` | `MultiStatementEnabled` | Enable multi-statement execution (default: false) | | `x-multi-statement-max-size` | `MultiStatementMaxSize` | Maximum size of single statement in bytes (default: 10MB) | +| `x-no-lock` | `NoLock` | Set to `true` to skip `pg_advisory_lock`/`pg_advisory_unlock` calls. Useful for read-only checks or multi-master setups. Only run migrations from one host when this is enabled. | | `dbname` | `DatabaseName` | The name of the database to connect to | | `search_path` | | This variable specifies the order in which schemas are searched when an object is referenced by a simple name with no schema specified. | | `user` | | The user to sign in as | diff --git a/database/postgres/postgres.go b/database/postgres/postgres.go index ed96fe63e..b0344e2b6 100644 --- a/database/postgres/postgres.go +++ b/database/postgres/postgres.go @@ -51,6 +51,7 @@ type Config struct { migrationsTableName string StatementTimeout time.Duration MultiStatementMaxSize int + NoLock bool } type Postgres struct { @@ -200,6 +201,14 @@ func (p *Postgres) Open(url string) (database.Driver, error) { } } + noLock := false + if s := purl.Query().Get("x-no-lock"); len(s) > 0 { + noLock, err = strconv.ParseBool(s) + if err != nil { + return nil, fmt.Errorf("unable to parse option x-no-lock: %w", err) + } + } + px, err := WithInstance(db, &Config{ DatabaseName: purl.Path, MigrationsTable: migrationsTable, @@ -207,6 +216,7 @@ func (p *Postgres) Open(url string) (database.Driver, error) { StatementTimeout: time.Duration(statementTimeout) * time.Millisecond, MultiStatementEnabled: multiStatementEnabled, MultiStatementMaxSize: multiStatementMaxSize, + NoLock: noLock, }) if err != nil { @@ -232,6 +242,10 @@ func (p *Postgres) Close() error { // https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS func (p *Postgres) Lock() error { return database.CasRestoreOnErr(&p.isLocked, false, true, database.ErrLocked, func() error { + if p.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName) if err != nil { return err @@ -249,6 +263,10 @@ func (p *Postgres) Lock() error { func (p *Postgres) Unlock() error { return database.CasRestoreOnErr(&p.isLocked, true, false, database.ErrNotLocked, func() error { + if p.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(p.config.DatabaseName, p.config.migrationsSchemaName, p.config.migrationsTableName) if err != nil { return err diff --git a/database/sqlserver/README.md b/database/sqlserver/README.md index 8ecd87723..c29516314 100644 --- a/database/sqlserver/README.md +++ b/database/sqlserver/README.md @@ -6,6 +6,7 @@ | URL Query | WithInstance Config | Description | |------------|---------------------|-------------| | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | +| `x-no-lock` | `NoLock` | Set to `true` to skip `sp_getapplock`/`sp_releaseapplock` calls. Useful for read-only checks. Only run migrations from one host when this is enabled. | | `username` | | enter the SQL Server Authentication user id or the Windows Authentication user id in the DOMAIN\User format. On Windows, if user id is empty or missing Single-Sign-On is used. | | `password` | | The user's password. | | `host` | | The host to connect to. | diff --git a/database/sqlserver/sqlserver.go b/database/sqlserver/sqlserver.go index c77bde1f8..ae92258cc 100644 --- a/database/sqlserver/sqlserver.go +++ b/database/sqlserver/sqlserver.go @@ -44,6 +44,7 @@ type Config struct { MigrationsTable string DatabaseName string SchemaName string + NoLock bool } // SQL Server connection @@ -167,9 +168,18 @@ func (ss *SQLServer) Open(url string) (database.Driver, error) { migrationsTable := purl.Query().Get("x-migrations-table") + noLock := false + if s := purl.Query().Get("x-no-lock"); len(s) > 0 { + noLock, err = strconv.ParseBool(s) + if err != nil { + return nil, fmt.Errorf("unable to parse option x-no-lock: %w", err) + } + } + px, err := WithInstance(db, &Config{ DatabaseName: purl.Path, MigrationsTable: migrationsTable, + NoLock: noLock, }) if err != nil { @@ -192,6 +202,10 @@ func (ss *SQLServer) Close() error { // Lock creates an advisory local on the database to prevent multiple migrations from running at the same time. func (ss *SQLServer) Lock() error { return database.CasRestoreOnErr(&ss.isLocked, false, true, database.ErrLocked, func() error { + if ss.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(ss.config.DatabaseName, ss.config.SchemaName) if err != nil { return err @@ -222,6 +236,10 @@ func (ss *SQLServer) Lock() error { // Unlock froms the migration lock from the database func (ss *SQLServer) Unlock() error { return database.CasRestoreOnErr(&ss.isLocked, true, false, database.ErrNotLocked, func() error { + if ss.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(ss.config.DatabaseName, ss.config.SchemaName) if err != nil { return err diff --git a/database/yugabytedb/README.md b/database/yugabytedb/README.md index 0488feed8..ad459cb9b 100644 --- a/database/yugabytedb/README.md +++ b/database/yugabytedb/README.md @@ -7,6 +7,7 @@ | `x-migrations-table` | `MigrationsTable` | Name of the migrations table | | `x-lock-table` | `LockTable` | Name of the table which maintains the migration lock | | `x-force-lock` | `ForceLock` | Force lock acquisition to fix faulty migrations which may not have released the schema lock (Boolean, default is `false`) | +| `x-no-lock` | `NoLock` | Set to `true` to skip lock table acquisition. Useful for read-only checks. Only run migrations from one host when this is enabled. | | `x-max-retries` | `MaxRetries` | How many times retry queries on retryable errors (40001, 40P01, 08006, XX000). Default is 10 | | `x-max-retry-interval` | `MaxRetryInterval` | Interval between retries increases exponentially. This option specifies maximum duration between retries. Default is 15s | | `x-max-retry-elapsed-time` | `MaxRetryElapsedTime` | Total retries timeout. Default is 30s | diff --git a/database/yugabytedb/yugabytedb.go b/database/yugabytedb/yugabytedb.go index 6855bf767..b46e8bd0e 100644 --- a/database/yugabytedb/yugabytedb.go +++ b/database/yugabytedb/yugabytedb.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" "io" "net/url" "regexp" @@ -48,6 +49,7 @@ type Config struct { MaxRetryInterval time.Duration MaxRetryElapsedTime time.Duration MaxRetries int + NoLock bool } type YugabyteDB struct { @@ -168,6 +170,14 @@ func (c *YugabyteDB) Open(dbURL string) (database.Driver, error) { maxRetries = DefaultMaxRetries } + noLock := false + if s := purl.Query().Get("x-no-lock"); len(s) > 0 { + noLock, err = strconv.ParseBool(s) + if err != nil { + return nil, fmt.Errorf("unable to parse option x-no-lock: %w", err) + } + } + px, err := WithInstance(db, &Config{ DatabaseName: purl.Path, MigrationsTable: migrationsTable, @@ -176,6 +186,7 @@ func (c *YugabyteDB) Open(dbURL string) (database.Driver, error) { MaxRetryInterval: maxInterval, MaxRetryElapsedTime: maxElapsedTime, MaxRetries: maxRetries, + NoLock: noLock, }) if err != nil { return nil, err @@ -192,6 +203,10 @@ func (c *YugabyteDB) Close() error { // See: https://github.com/yugabyte/yugabyte-db/issues/3642 func (c *YugabyteDB) Lock() error { return database.CasRestoreOnErr(&c.isLocked, false, true, database.ErrLocked, func() (err error) { + if c.config.NoLock { + return nil + } + return c.doTxWithRetry(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable}, func(tx *sql.Tx) (err error) { aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName) if err != nil { @@ -229,6 +244,10 @@ func (c *YugabyteDB) Lock() error { // See: https://github.com/yugabyte/yugabyte-db/issues/3642 func (c *YugabyteDB) Unlock() error { return database.CasRestoreOnErr(&c.isLocked, true, false, database.ErrNotLocked, func() (err error) { + if c.config.NoLock { + return nil + } + aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName) if err != nil { return err From 26525b51e821a19ce4ca030aba7f1fdfa86d80c7 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 18 Mar 2026 18:11:11 +0400 Subject: [PATCH 2/2] Add x-no-lock tests for postgres, pgx, pgx/v5, sqlserver, cockroachdb, yugabytedb Add param validation unit test and integration lock/unlock test following the existing MySQL TestNoLockParamValidation/TestNoLockWorks pattern. --- database/cockroachdb/cockroachdb_test.go | 54 +++++++++++++++++++++++- database/pgx/pgx_test.go | 47 +++++++++++++++++++++ database/pgx/v5/pgx_test.go | 47 +++++++++++++++++++++ database/postgres/postgres_test.go | 48 +++++++++++++++++++++ database/sqlserver/sqlserver_test.go | 51 ++++++++++++++++++++++ database/yugabytedb/yugabytedb_test.go | 52 +++++++++++++++++++++++ 6 files changed, 298 insertions(+), 1 deletion(-) diff --git a/database/cockroachdb/cockroachdb_test.go b/database/cockroachdb/cockroachdb_test.go index 7b259ab3e..73e580e71 100644 --- a/database/cockroachdb/cockroachdb_test.go +++ b/database/cockroachdb/cockroachdb_test.go @@ -5,11 +5,14 @@ package cockroachdb import ( "context" "database/sql" + "errors" "fmt" - "github.com/golang-migrate/migrate/v4" "log" + "strconv" "strings" "testing" + + "github.com/golang-migrate/migrate/v4" ) import ( @@ -155,6 +158,55 @@ func TestMultiStatement(t *testing.T) { }) } +func TestNoLockParamValidation(t *testing.T) { + c := &CockroachDb{} + _, err := c.Open("cockroach://root@localhost/migrate?x-no-lock=not-a-bool") + if !errors.Is(err, strconv.ErrSyntax) { + t.Fatal("Expected syntax error when passing a non-bool as x-no-lock parameter") + } +} + +func TestNoLockWorks(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) { + createDB(t, ci) + + ip, port, err := ci.Port(defaultPort) + if err != nil { + t.Fatal(err) + } + + addr := fmt.Sprintf("cockroach://root@%v:%v/migrate?sslmode=disable", ip, port) + c := &CockroachDb{} + d, err := c.Open(addr) + if err != nil { + t.Fatal(err) + } + + lock := d.(*CockroachDb) + + c = &CockroachDb{} + d, err = c.Open(addr + "&x-no-lock=true") + if err != nil { + t.Fatal(err) + } + + noLock := d.(*CockroachDb) + + if err = lock.Lock(); err != nil { + t.Fatal(err) + } + if err = noLock.Lock(); err != nil { + t.Fatal(err) + } + if err = lock.Unlock(); err != nil { + t.Fatal(err) + } + if err = noLock.Unlock(); err != nil { + t.Fatal(err) + } + }) +} + func TestFilterCustomQuery(t *testing.T) { dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) { createDB(t, ci) diff --git a/database/pgx/pgx_test.go b/database/pgx/pgx_test.go index d86caeb36..4ac1f8074 100644 --- a/database/pgx/pgx_test.go +++ b/database/pgx/pgx_test.go @@ -784,3 +784,50 @@ func Test_computeLineFromPos(t *testing.T) { }) } } + +func TestNoLockParamValidation(t *testing.T) { + p := &Postgres{} + _, err := p.Open("pgx://postgres@localhost/postgres?x-no-lock=not-a-bool") + if !errors.Is(err, strconv.ErrSyntax) { + t.Fatal("Expected syntax error when passing a non-bool as x-no-lock parameter") + } +} + +func TestNoLockWorks(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + addr := pgConnectionString(ip, port) + p := &Postgres{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + + lock := d.(*Postgres) + + p = &Postgres{} + d, err = p.Open(pgConnectionString(ip, port, "x-no-lock=true")) + if err != nil { + t.Fatal(err) + } + + noLock := d.(*Postgres) + + if err = lock.Lock(); err != nil { + t.Fatal(err) + } + if err = noLock.Lock(); err != nil { + t.Fatal(err) + } + if err = lock.Unlock(); err != nil { + t.Fatal(err) + } + if err = noLock.Unlock(); err != nil { + t.Fatal(err) + } + }) +} diff --git a/database/pgx/v5/pgx_test.go b/database/pgx/v5/pgx_test.go index 9a8652768..42ad601cf 100644 --- a/database/pgx/v5/pgx_test.go +++ b/database/pgx/v5/pgx_test.go @@ -759,3 +759,50 @@ func Test_computeLineFromPos(t *testing.T) { }) } } + +func TestNoLockParamValidation(t *testing.T) { + p := &Postgres{} + _, err := p.Open("pgx5://postgres@localhost/postgres?x-no-lock=not-a-bool") + if !errors.Is(err, strconv.ErrSyntax) { + t.Fatal("Expected syntax error when passing a non-bool as x-no-lock parameter") + } +} + +func TestNoLockWorks(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + addr := pgConnectionString(ip, port) + p := &Postgres{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + + lock := d.(*Postgres) + + p = &Postgres{} + d, err = p.Open(pgConnectionString(ip, port, "x-no-lock=true")) + if err != nil { + t.Fatal(err) + } + + noLock := d.(*Postgres) + + if err = lock.Lock(); err != nil { + t.Fatal(err) + } + if err = noLock.Lock(); err != nil { + t.Fatal(err) + } + if err = lock.Unlock(); err != nil { + t.Fatal(err) + } + if err = noLock.Unlock(); err != nil { + t.Fatal(err) + } + }) +} diff --git a/database/postgres/postgres_test.go b/database/postgres/postgres_test.go index 3a49c50ab..b0c2a3edc 100644 --- a/database/postgres/postgres_test.go +++ b/database/postgres/postgres_test.go @@ -99,6 +99,7 @@ func Test(t *testing.T) { t.Run("testPostgresLock", testPostgresLock) t.Run("testWithInstanceConcurrent", testWithInstanceConcurrent) t.Run("testWithConnection", testWithConnection) + t.Run("testNoLockWorks", testNoLockWorks) t.Cleanup(func() { for _, spec := range specs { @@ -748,6 +749,53 @@ func testWithConnection(t *testing.T) { }) } +func TestNoLockParamValidation(t *testing.T) { + p := &Postgres{} + _, err := p.Open("postgres://postgres@localhost/postgres?x-no-lock=not-a-bool") + if !errors.Is(err, strconv.ErrSyntax) { + t.Fatal("Expected syntax error when passing a non-bool as x-no-lock parameter") + } +} + +func testNoLockWorks(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + addr := pgConnectionString(ip, port) + p := &Postgres{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + + lock := d.(*Postgres) + + p = &Postgres{} + d, err = p.Open(pgConnectionString(ip, port, "x-no-lock=true")) + if err != nil { + t.Fatal(err) + } + + noLock := d.(*Postgres) + + if err = lock.Lock(); err != nil { + t.Fatal(err) + } + if err = noLock.Lock(); err != nil { + t.Fatal(err) + } + if err = lock.Unlock(); err != nil { + t.Fatal(err) + } + if err = noLock.Unlock(); err != nil { + t.Fatal(err) + } + }) +} + func Test_computeLineFromPos(t *testing.T) { testcases := []struct { pos int diff --git a/database/sqlserver/sqlserver_test.go b/database/sqlserver/sqlserver_test.go index 402f4480f..6e9b861c1 100644 --- a/database/sqlserver/sqlserver_test.go +++ b/database/sqlserver/sqlserver_test.go @@ -4,9 +4,11 @@ import ( "context" "database/sql" sqldriver "database/sql/driver" + "errors" "fmt" "log" "runtime" + "strconv" "strings" "testing" "time" @@ -92,6 +94,7 @@ func Test(t *testing.T) { t.Run("testMsiTrue", testMsiTrue) t.Run("testOpenWithPasswordAndMSI", testOpenWithPasswordAndMSI) t.Run("testMsiFalse", testMsiFalse) + t.Run("testNoLockWorks", testNoLockWorks) t.Cleanup(func() { for _, spec := range specs { @@ -311,6 +314,54 @@ func testOpenWithPasswordAndMSI(t *testing.T) { }) } +func TestNoLockParamValidation(t *testing.T) { + p := &SQLServer{} + _, err := p.Open("sqlserver://sa:password@localhost?x-no-lock=not-a-bool") + if !errors.Is(err, strconv.ErrSyntax) { + t.Fatal("Expected syntax error when passing a non-bool as x-no-lock parameter") + } +} + +func testNoLockWorks(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + SkipIfUnsupportedArch(t, c) + ip, port, err := c.Port(defaultPort) + if err != nil { + t.Fatal(err) + } + + addr := msConnectionString(ip, port) + p := &SQLServer{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + + lock := d.(*SQLServer) + + p = &SQLServer{} + d, err = p.Open(addr + "&x-no-lock=true") + if err != nil { + t.Fatal(err) + } + + noLock := d.(*SQLServer) + + if err = lock.Lock(); err != nil { + t.Fatal(err) + } + if err = noLock.Lock(); err != nil { + t.Fatal(err) + } + if err = lock.Unlock(); err != nil { + t.Fatal(err) + } + if err = noLock.Unlock(); err != nil { + t.Fatal(err) + } + }) +} + func testMsiFalse(t *testing.T) { dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { SkipIfUnsupportedArch(t, c) diff --git a/database/yugabytedb/yugabytedb_test.go b/database/yugabytedb/yugabytedb_test.go index 05fb14fa7..ce84b1338 100644 --- a/database/yugabytedb/yugabytedb_test.go +++ b/database/yugabytedb/yugabytedb_test.go @@ -5,8 +5,10 @@ package yugabytedb import ( "context" "database/sql" + "errors" "fmt" "log" + "strconv" "strings" "testing" "time" @@ -95,6 +97,7 @@ func Test(t *testing.T) { t.Run("testMigrate", testMigrate) t.Run("testMultiStatement", testMultiStatement) t.Run("testFilterCustomQuery", testFilterCustomQuery) + t.Run("testNoLockWorks", testNoLockWorks) t.Cleanup(func() { for _, spec := range specs { @@ -179,6 +182,55 @@ func testMultiStatement(t *testing.T) { }) } +func TestNoLockParamValidation(t *testing.T) { + c := &YugabyteDB{} + _, err := c.Open("yugabyte://yugabyte@localhost/migrate?x-no-lock=not-a-bool") + if !errors.Is(err, strconv.ErrSyntax) { + t.Fatal("Expected syntax error when passing a non-bool as x-no-lock parameter") + } +} + +func testNoLockWorks(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) { + createDB(t, ci) + + ip, port, err := ci.Port(defaultPort) + if err != nil { + t.Fatal(err) + } + + addr := getConnectionString(ip, port) + c := &YugabyteDB{} + d, err := c.Open(addr) + if err != nil { + t.Fatal(err) + } + + lock := d.(*YugabyteDB) + + c = &YugabyteDB{} + d, err = c.Open(getConnectionString(ip, port, "x-no-lock=true")) + if err != nil { + t.Fatal(err) + } + + noLock := d.(*YugabyteDB) + + if err = lock.Lock(); err != nil { + t.Fatal(err) + } + if err = noLock.Lock(); err != nil { + t.Fatal(err) + } + if err = lock.Unlock(); err != nil { + t.Fatal(err) + } + if err = noLock.Unlock(); err != nil { + t.Fatal(err) + } + }) +} + func testFilterCustomQuery(t *testing.T) { dktesting.ParallelTest(t, specs, func(t *testing.T, ci dktest.ContainerInfo) { createDB(t, ci)