Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fa05d57
Updated testing environment.
simukka Nov 6, 2025
351a6dc
Working connection.
simukka Nov 6, 2025
b1b982d
Updated fixture to use pgx. Simplified how we introspect the DSN to d…
simukka Dec 3, 2025
2d9462f
Wrote initial sqlcode migration for postgres.
simukka Dec 3, 2025
6643158
Add security definer role.
simukka Dec 3, 2025
fd8c76f
[wip] working through changes for EnsureUploaded to support postgresql.
simukka Dec 3, 2025
b6681a2
Working EnsureUpload!
simukka Dec 4, 2025
da5b115
[wip] update parser and scanner
simukka Dec 5, 2025
18309f0
Fixed issue with Preprocess. Passing pgsql tests.
simukka Dec 9, 2025
b651814
Updated GO workflow to test both drivers.
simukka Dec 9, 2025
160df17
Fixed typo in GH workflow.
simukka Dec 9, 2025
11659ed
Updated Dockerfile
simukka Dec 9, 2025
37fd588
Use build tags to exclude examples from bulid & test
simukka Dec 9, 2025
ca444bc
Exclude example test
simukka Dec 9, 2025
c483fb7
Fixed failing test.
simukka Dec 9, 2025
4b803bf
Moved Document structs to a separate file for better organization.
simukka Dec 11, 2025
f202e18
Updated go-mssql depedency to use microsoft fork. DropAndUpload now s…
simukka Dec 11, 2025
ad129b8
Initial unit tests for T-SQL syntax parsing.
simukka Dec 11, 2025
352ed9f
Refactored to use a Document interface.
simukka Dec 11, 2025
5e807d5
Renamed the existing Document struct to be specific for T-SQL.
simukka Dec 11, 2025
494dca9
Created initial PGSqlDocument for PostgreSQL.
simukka Dec 11, 2025
1f7b6b7
Updated unit test.
simukka Dec 11, 2025
af75628
Updated tests.
simukka Dec 11, 2025
6916d34
Simplified Document interface. Created Pragma struct.
simukka Dec 11, 2025
1a91556
[wip] pgsql document parsing
simukka Dec 11, 2025
fb56414
Simplify the interfaces for parsing a SQL document.
simukka Dec 16, 2025
5e29dc9
Refactored pgsql document to use node parser.
simukka Dec 16, 2025
6e114a9
[wip]
simukka Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
test: test_mssql test_pgsql


test_mssql:
docker compose --progress plain -f docker-compose.mssql.yml run test

test_pgsql:
docker compose --progress plain -f docker-compose.pgsql.yml run test
2 changes: 2 additions & 0 deletions dbintf.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sqlcode
import (
"context"
"database/sql"
"database/sql/driver"
)

type DB interface {
Expand All @@ -11,6 +12,7 @@ type DB interface {
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
Conn(ctx context.Context) (*sql.Conn, error)
BeginTx(ctx context.Context, txOptions *sql.TxOptions) (*sql.Tx, error)
Driver() driver.Driver
Copy link
Contributor Author

@simukka simukka Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a required interface for sql.DB and shouldn't be too much of a hassle to support in internal libraries.

}

var _ DB = &sql.DB{}
16 changes: 15 additions & 1 deletion dbops.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,25 @@ package sqlcode
import (
"context"
"database/sql"

mssql "github.com/denisenkom/go-mssqldb"
Copy link
Contributor Author

@simukka simukka Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Should we use the official mssql driver?

"github.com/jackc/pgx/v5/stdlib"
)

func Exists(ctx context.Context, dbc DB, schemasuffix string) (bool, error) {
var schemaID int
err := dbc.QueryRowContext(ctx, `select isnull(schema_id(@p1), 0)`, SchemaName(schemasuffix)).Scan(&schemaID)

driver := dbc.Driver()
var qs string

if _, ok := driver.(*mssql.Driver); ok {
qs = `select isnull(schema_id(@p1), 0)`
}
if _, ok := driver.(*stdlib.Driver); ok {
qs = `select coalesce((select oid from pg_namespace where nspname = $1),0)`
}

err := dbc.QueryRowContext(ctx, qs, SchemaName(schemasuffix)).Scan(&schemaID)
if err != nil {
return false, err
}
Expand Down
105 changes: 76 additions & 29 deletions deployable.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"time"

mssql "github.com/denisenkom/go-mssqldb"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/stdlib"
pgxstdlib "github.com/jackc/pgx/v5/stdlib"
"github.com/vippsas/sqlcode/sqlparser"
)

Expand Down Expand Up @@ -77,21 +80,22 @@ func impersonate(ctx context.Context, dbc DB, username string, f func(conn *sql.
// Upload will create and upload the schema; resulting in an error
// if the schema already exists
func (d *Deployable) Upload(ctx context.Context, dbc DB) error {
// First, impersonate a user with minimal privileges to get at least
// some level of sandboxing so that migration scripts can't do anything
// the caller didn't expect them to.
return impersonate(ctx, dbc, "sqlcode-deploy-sandbox-user", func(conn *sql.Conn) error {
driver := dbc.Driver()
qs := make(map[string][]interface{}, 1)

var uploadFunc = func(conn *sql.Conn) error {
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
return err
}

_, err = tx.ExecContext(ctx, `sqlcode.CreateCodeSchema`,
sql.Named("schemasuffix", d.SchemaSuffix),
)
if err != nil {
_ = tx.Rollback()
return err
for q, args := range qs {
_, err = tx.ExecContext(ctx, q, args...)

if err != nil {
_ = tx.Rollback()
return fmt.Errorf("failed to execute (%s) with arg(%s) in schema %s: %w", q, args, d.SchemaSuffix, err)
}
}

preprocessed, err := Preprocess(d.CodeBase, d.SchemaSuffix)
Expand Down Expand Up @@ -123,8 +127,36 @@ func (d *Deployable) Upload(ctx context.Context, dbc DB) error {

return nil

})
}

if _, ok := driver.(*mssql.Driver); ok {
// First, impersonate a user with minimal privileges to get at least
// some level of sandboxing so that migration scripts can't do anything
// the caller didn't expect them to.
qs["sqlcode.CreateCodeSchema"] = []interface {
}{
sql.Named("schemasuffix", d.SchemaSuffix),
}

return impersonate(ctx, dbc, "sqlcode-deploy-sandbox-user", uploadFunc)
}

if _, ok := driver.(*stdlib.Driver); ok {
qs[`set role "sqlcode-deploy-sandbox-user"`] = nil
qs[`call sqlcode.createcodeschema(@schemasuffix)`] = []interface{}{
pgx.NamedArgs{"schemasuffix": d.SchemaSuffix},
}
conn, err := dbc.Conn(ctx)
if err != nil {
return err
}
defer func() {
_ = conn.Close()
}()
return uploadFunc(conn)
}

return fmt.Errorf("failed to determine sql driver to upload schema: %s", d.SchemaSuffix)
}

// EnsureUploaded checks that the schema with the suffix already exists,
Expand All @@ -137,36 +169,51 @@ func (d *Deployable) EnsureUploaded(ctx context.Context, dbc DB) error {
return nil
}

driver := dbc.Driver()
lockResourceName := "sqlcode.EnsureUploaded/" + d.SchemaSuffix

var lockRetCode int
var lockQs string
var unlockQs string
var err error

// When a lock is opened with the Transaction lock owner,
// that lock is released when the transaction is committed or rolled back.
var lockRetCode int
err := dbc.QueryRowContext(ctx, `
declare @retcode int;
exec @retcode = sp_getapplock @Resource = @resource, @LockMode = 'Shared', @LockOwner = 'Session', @LockTimeout = @timeoutMs;
select @retcode;
`,
sql.Named("resource", lockResourceName),
sql.Named("timeoutMs", 20000),
).Scan(&lockRetCode)
if _, ok := driver.(*pgxstdlib.Driver); ok {
lockQs = `select sqlcode.get_applock(@resource, @timeout)`
unlockQs = `select sqlcode.release_applock(@resource)`

err = dbc.QueryRowContext(ctx, lockQs, pgx.NamedArgs{
"resource": lockResourceName,
"timeoutMs": 20000,
}).Scan(&lockRetCode)

defer func() {
dbc.ExecContext(ctx, unlockQs, pgx.NamedArgs{"resource": lockResourceName})
}()
}

if _, ok := driver.(*mssql.Driver); ok {
// TODO
Copy link
Contributor Author

@simukka simukka Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Add back the mssql sql to lock on upload.


defer func() {
// TODO: This returns an error if the lock is already released
_, _ = dbc.ExecContext(ctx, unlockQs,
sql.Named("Resource", lockResourceName),
sql.Named("LockOwner", "Session"),
)
}()
}

if err != nil {
return err
}
if lockRetCode < 0 {
return errors.New("was not able to get lock before timeout")
}

defer func() {
_, _ = dbc.ExecContext(ctx, `sp_releaseapplock`,
sql.Named("Resource", lockResourceName),
sql.Named("LockOwner", "Session"),
)
}()

exists, err := Exists(ctx, dbc, d.SchemaSuffix)
if err != nil {
return err
return fmt.Errorf("unable to determine if schema %s exists: %w", d.SchemaSuffix, err)
}

if exists {
Expand Down
25 changes: 25 additions & 0 deletions docker-compose.mssql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
mssql:
image: mcr.microsoft.com/mssql/server:latest
networks:
- mssql
environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: VippsPw1
healthcheck:
test: ["CMD", "/opt/mssql-tools18/bin/sqlcmd", "-C", "-Usa", "-PVippsPw1", "-Q", "select 1"]
interval: 1s
retries: 20
test:
build:
dockerfile: dockerfile.test
networks:
- mssql
environment:
SQLSERVER_DSN: sqlserver://mssql:1433?database=master&user id=sa&password=VippsPw1
SQLSERVER_DRIVER: sqlserver
depends_on:
mssql:
condition: service_healthy
networks:
mssql:
27 changes: 27 additions & 0 deletions docker-compose.pgsql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
services:
postgres:
image: postgres
networks:
- postgres
environment:
POSTGRES_PASSWORD: VippsPw1
POSTGRES_USER: sa
POSTGRES_DB: master
PGOPTIONS: "-c log_error_verbosity=verbose -c log_statement=all"
healthcheck:
test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
interval: 1s
retries: 20
test:
build:
dockerfile: dockerfile.test
networks:
- postgres
environment:
SQLSERVER_DSN: postgresql://sa:VippsPw1@postgres:5432/master?sslmode=disable
GODEBUG: "x509negativeserial=1"
depends_on:
postgres:
condition: service_healthy
networks:
postgres:
15 changes: 0 additions & 15 deletions docker-compose.test.yml

This file was deleted.

6 changes: 6 additions & 0 deletions dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM golang:1.25.1 AS builder
WORKDIR /sqlcode
ENV GODEBUG="x509negativeserial=1"
COPY . .
RUN go mod tidy
CMD ["go", "test", "-v", "$(go list ./... | grep -v './example')"]
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ require (
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
Expand All @@ -38,6 +48,7 @@ github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
Expand All @@ -51,6 +62,8 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -69,6 +82,7 @@ golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
Loading
Loading