diff --git a/_examples/go.mod b/_examples/go.mod index e483d56..a927141 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -6,10 +6,10 @@ require github.com/goccy/go-zetasqlite v0.6.6 require ( github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/goccy/go-json v0.9.10 // indirect - github.com/goccy/go-zetasql v0.3.3 // indirect + github.com/goccy/go-json v0.10.0 // indirect + github.com/goccy/go-zetasql v0.5.5 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/mattn/go-sqlite3 v1.14.14 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect gonum.org/v1/gonum v0.11.0 // indirect ) diff --git a/_examples/go.sum b/_examples/go.sum index 4a6ad4a..9dc1652 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -2,13 +2,16 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc= github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-zetasql v0.3.3 h1:+Ar/GZ4k2vNgaljRPt5lpD8JSIiq0WSEG38Fbowj5fM= github.com/goccy/go-zetasql v0.3.3/go.mod h1:6W14CJVKh7crrSPyj6NPk4c49L2NWnxvyDLsRkOm4BI= +github.com/goccy/go-zetasql v0.5.5/go.mod h1:xvvooX2RG404vnbdFZuAM8bTFksYwVUlqeIUrUNuo40= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs= gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= diff --git a/compile-tests-and-run-with-delve.sh b/compile-tests-and-run-with-delve.sh new file mode 100755 index 0000000..19ad888 --- /dev/null +++ b/compile-tests-and-run-with-delve.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +pkill dlv + +#tmux split-window -h -t main:0 + +# Send the script execution command to the second pane (index 1) +tmux send-keys -t main:0.0 'go test -c && dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./go-zetasqlite.test' Enter + +# Attach to the tmux session +#tmux attach-session -t my_session + +PORT=2345 # The port to check +DELAY=1 # Number of seconds to wait between checks + +# Function to check if the port is being used +check_port() { + lsof -iTCP:$PORT -sTCP:LISTEN -t >/dev/null +} + +# Loop until the program is listening on the port +while ! check_port; do + echo "Waiting for the program to start listening on port $PORT..." + sleep $DELAY +done + +# Program is now listening +echo "The program is now listening on port $PORT." \ No newline at end of file diff --git a/compile-tests-and-run.sh b/compile-tests-and-run.sh new file mode 100755 index 0000000..3d839fe --- /dev/null +++ b/compile-tests-and-run.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +pkill dlv + +#tmux split-window -h -t main:0 + +# Send the script execution command to the second pane (index 1) +tmux send-keys -t main:0.0 ' go test -c 2>&1 | logsane-cli && ./go-zetasqlite.test | lnav -n' Enter + +# Attach to the tmux session +#tmux attach-session -t my_session + +#PORT=2345 # The port to check +#DELAY=1 # Number of seconds to wait between checks +# +## Function to check if the port is being used +#check_port() { +# lsof -iTCP:$PORT -sTCP:LISTEN -t >/dev/null +#} +# +## Loop until the program is listening on the port +#while ! check_port; do +# echo "Waiting for the program to start listening on port $PORT..." +# sleep $DELAY +#done +# +## Program is now listening +#echo "The program is now listening on port $PORT." \ No newline at end of file diff --git a/driver_test.go b/driver_test.go index 30d8030..d5351f5 100644 --- a/driver_test.go +++ b/driver_test.go @@ -10,6 +10,218 @@ import ( zetasqlite "github.com/goccy/go-zetasqlite" ) +func TestDriverAlter(t *testing.T) { + db, err := sql.Open("zetasqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) +)`); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`INSERT Singers (SingerId, FirstName, LastName) VALUES (1, 'John', 'Titor')`); err != nil { + t.Fatal(err) + } + row := db.QueryRow("SELECT SingerId, FirstName, LastName FROM Singers WHERE SingerId = @id", 1) + if row.Err() != nil { + t.Fatal(row.Err()) + } + var ( + singerID int64 + firstName string + lastName string + ) + if err := row.Scan(&singerID, &firstName, &lastName); err != nil { + t.Fatal(err) + } + if singerID != 1 || firstName != "John" || lastName != "Titor" { + t.Fatalf("failed to find row %v %v %v", singerID, firstName, lastName) + } + + if _, err := db.Exec(` +CREATE VIEW IF NOT EXISTS SingerNames AS SELECT FirstName || ' ' || LastName AS Name FROM Singers`); err != nil { + t.Fatal(err) + } + + viewRow := db.QueryRow("SELECT Name FROM SingerNames LIMIT 1") + if viewRow.Err() != nil { + t.Fatal(viewRow.Err()) + } + + var name string + + if err := viewRow.Scan(&name); err != nil { + t.Fatal(err) + } + if name != "John Titor" { + t.Fatalf("failed to find view row") + } + + // Test ALTER TABLE SET OPTIONS + if _, err := db.Exec(`ALTER TABLE Singers SET OPTIONS (description="Famous singers")`); err != nil { + t.Fatal(err) + } + + // Test ALTER TABLE ADD COLUMN + if _, err := db.Exec(`ALTER TABLE Singers ADD COLUMN Age INT64, ADD COLUMN IsSingle BOOL`); err != nil { + t.Fatal(err) + } + + // Verify the changes + row = db.QueryRow("SELECT SingerId, FirstName, LastName, Age, IsSingle FROM Singers WHERE SingerId = @id", 1) + if row.Err() != nil { + t.Fatal(row.Err()) + } + + var age sql.NullInt64 + var isSingle sql.NullBool + if err := row.Scan(&singerID, &firstName, &lastName, &age, &isSingle); err != nil { + t.Fatal(err) + } + if singerID != 1 || firstName != "John" || lastName != "Titor" || age.Valid || isSingle.Valid { + t.Fatalf("failed to find row after ALTER TABLE statements") + } + + if _, err := db.Exec(`INSERT Singers (SingerId, FirstName, LastName, Age, IsSingle) VALUES (1, 'John', 'Titor', 10, TRUE)`); err != nil { + t.Fatal(err) + } + row = db.QueryRow("SELECT SingerId, FirstName, LastName, Age, isSingle FROM Singers WHERE SingerId = @id AND isSingle IS NOT NULL", 1) + if row.Err() != nil { + t.Fatal(row.Err()) + } + if err := row.Scan(&singerID, &firstName, &lastName, &age, &isSingle); err != nil { + t.Fatal(err) + } + if singerID != 1 || firstName != "John" || lastName != "Titor" || age.Int64 != 10 || isSingle.Bool != true { + t.Fatalf("failed to find row %v %v %v %v %v", singerID, firstName, lastName, age, isSingle) + } + + if _, err := db.Exec(` + ALTER TABLE Artists + ADD COLUMN Age INT64, + ADD COLUMN Nationality STRING NOT NULL DEFAULT 'Unknown', + DROP COLUMN LastName, + RENAME COLUMN GivenName TO FirstName + RENAME TO Musicians + `); err != nil { + t.Fatal(err) + } + + // Verify the changes + row = db.QueryRow("SELECT SingerID, FirstName, Age, Nationality FROM Musicians WHERE SingerId = @id", 1) + if row.Err() != nil { + t.Fatal(row.Err()) + } + + //var nationality sql.NullString + //if err := row.Scan(&singerID, &firstName, &lastName, &age, &nationality); err != nil { + // t.Fatal(err) + //} + //if singerID != 1 || firstName != "John" || lastName != "Titor" || age.Valid || nationality.Valid { + // t.Fatalf("failed to find row after multi-action ALTER TABLE statement") + //} +} + +func TestDriverAlter2(t *testing.T) { + db, err := sql.Open("zetasqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) +)`); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`INSERT Singers (SingerId, FirstName, LastName) VALUES (1, 'John', 'Titor')`); err != nil { + t.Fatal(err) + } + row := db.QueryRow("SELECT SingerId, FirstName, LastName FROM Singers WHERE SingerId = @id", 1) + if row.Err() != nil { + t.Fatal(row.Err()) + } + var ( + singerID int64 + firstName string + lastName string + ) + if err := row.Scan(&singerID, &firstName, &lastName); err != nil { + t.Fatal(err) + } + if singerID != 1 || firstName != "John" || lastName != "Titor" { + t.Fatalf("failed to find row %v %v %v", singerID, firstName, lastName) + } + + if _, err := db.Exec(` +CREATE VIEW IF NOT EXISTS SingerNames AS SELECT FirstName || ' ' || LastName AS Name FROM Singers`); err != nil { + t.Fatal(err) + } + + viewRow := db.QueryRow("SELECT Name FROM SingerNames LIMIT 1") + if viewRow.Err() != nil { + t.Fatal(viewRow.Err()) + } + + var name string + + if err := viewRow.Scan(&name); err != nil { + t.Fatal(err) + } + if name != "John Titor" { + t.Fatalf("failed to find view row") + } + + // Test ALTER TABLE SET OPTIONS + //if _, err := db.Exec(`ALTER TABLE Singers SET OPTIONS (description="Famous singers")`); err != nil { + // t.Fatal(err) + //} + // + //// Test ALTER TABLE ADD COLUMN + //if _, err := db.Exec(`ALTER TABLE Singers ADD COLUMN Age INT64`); err != nil { + // t.Fatal(err) + //} + + //// Test ALTER TABLE RENAME COLUMN + //if _, err := db.Exec(`ALTER TABLE Singers RENAME COLUMN FirstName TO GivenName`); err != nil { + // t.Fatal(err) + //} + + // Test ALTER TABLE DROP COLUMN + //if _, err := db.Exec(`ALTER TABLE Singers DROP COLUMN SingerInfo`); err != nil { + // t.Fatal(err) + //} + + if _, err := db.Exec(` + ALTER TABLE Singers + ADD COLUMN Age INT64, + ADD COLUMN Nationality STRING + `); err != nil { + t.Fatal(err) + } + + // Verify the changes + row = db.QueryRow("SELECT SingerId, FirstName, Age, Nationality FROM Singers WHERE SingerId = @id", 1) + if row.Err() != nil { + t.Fatal(row.Err()) + } + + //var nationality sql.NullString + //if err := row.Scan(&singerID, &firstName, &lastName, &age, &nationality); err != nil { + // t.Fatal(err) + //} + //if singerID != 1 || firstName != "John" || lastName != "Titor" || age.Valid || nationality.Valid { + // t.Fatalf("failed to find row after multi-action ALTER TABLE statement") + //} +} + func TestDriver(t *testing.T) { db, err := sql.Open("zetasqlite", ":memory:") if err != nil { diff --git a/go.mod b/go.mod index 4052993..b08860a 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( cloud.google.com/go/bigquery v1.51.0 github.com/DataDog/go-hll v1.0.2 github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86 + github.com/gdexlab/go-render v1.0.1 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 golang.org/x/net v0.8.0 golang.org/x/text v0.8.0 diff --git a/go.sum b/go.sum index c089e18..bb381fe 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U= +github.com/gdexlab/go-render v1.0.1/go.mod h1:wRi5nW2qfjiGj4mPukH4UV0IknS1cHD4VgFTmJX5JzM= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= diff --git a/internal/analyzer.go b/internal/analyzer.go index fa0aac6..374a8ad 100644 --- a/internal/analyzer.go +++ b/internal/analyzer.go @@ -87,6 +87,8 @@ func newAnalyzerOptions() (*zetasql.AnalyzerOptions, error) { ast.DropStmt, ast.TruncateStmt, ast.CreateTableStmt, + ast.AlterTableStmt, // TODO: (mfudala) add more + ast.RenameStmt, ast.CreateTableAsSelectStmt, ast.CreateProcedureStmt, ast.CreateFunctionStmt, @@ -290,10 +292,32 @@ func (a *Analyzer) newStmtAction(ctx context.Context, query string, args []drive return a.newBeginStmtAction(ctx, query, args, node) case ast.CommitStmt: return a.newCommitStmtAction(ctx, query, args, node) + case ast.AlterTableStmt: + return a.alterTableStmtAction(ctx, query, args, node.(*ast.AlterTableStmtNode)) } return nil, fmt.Errorf("unsupported stmt %s", node.DebugString()) } +func (a *Analyzer) alterTableStmtAction(_ context.Context, query string, args []driver.NamedValue, node *ast.AlterTableStmtNode) (*AlterTableStmtAction, error) { + spec, err := newAlterSpec(a.namePath, node) + if err != nil { + return nil, err + } + params := getParamsFromNode(node) + queryArgs, err := getArgsFromParams(args, params) + if err != nil { + return nil, err + } + return &AlterTableStmtAction{ + query: query, + spec: spec, + node: node, + args: queryArgs, + rawArgs: args, + catalog: a.catalog, + }, nil +} + func (a *Analyzer) newCreateTableStmtAction(_ context.Context, query string, args []driver.NamedValue, node *ast.CreateTableStmtNode) (*CreateTableStmtAction, error) { spec := newTableSpec(a.namePath, node) params := getParamsFromNode(node) diff --git a/internal/catalog.go b/internal/catalog.go index c626dd1..81e6f20 100644 --- a/internal/catalog.go +++ b/internal/catalog.go @@ -438,6 +438,36 @@ func (c *Catalog) addTableSpec(spec *TableSpec) error { return nil } +func (c *Catalog) modifyTableSpec(spec *AlterTableSpec) error { + tableName := spec.TableName() + foundSpecToUpdate, exists := c.tableMap[tableName] + + if !exists { + return fmt.Errorf("table %s does not exist", tableName) + } + + formattedPath := formatPath(spec.NamePath) + + err := c.deleteTableSpecByName(formattedPath) + if err != nil { + return err + } + + addedColumns := make([]*ColumnSpec, len(foundSpecToUpdate.Columns)) + copy(addedColumns, foundSpecToUpdate.Columns) + addedColumns = append(addedColumns, spec.AddedColumns...) + + foundSpecToUpdate.Columns = addedColumns + foundSpecToUpdate.UpdatedAt = spec.UpdatedAt + + err = c.addTableSpec(foundSpecToUpdate) + if err != nil { + return err + } + + return nil +} + func (c *Catalog) addTableSpecRecursive(cat *types.SimpleCatalog, spec *TableSpec) error { if len(spec.NamePath) > 1 { subCatalogName := spec.NamePath[0] diff --git a/internal/spec.go b/internal/spec.go index fb1c831..4886385 100644 --- a/internal/spec.go +++ b/internal/spec.go @@ -114,6 +114,12 @@ type TableSpec struct { CreatedAt time.Time `json:"createdAt"` } +type AlterTableSpec struct { + NamePath []string `json:"namePath"` + AddedColumns []*ColumnSpec `json:"columns"` + UpdatedAt time.Time `json:"updatedAt"` +} + func (s *TableSpec) Column(name string) *ColumnSpec { for _, col := range s.Columns { if col.Name == name { @@ -123,6 +129,10 @@ func (s *TableSpec) Column(name string) *ColumnSpec { return nil } +func (s *AlterTableSpec) TableName() string { + return formatPath(s.NamePath) +} + func (s *TableSpec) TableName() string { return formatPath(s.NamePath) } @@ -513,6 +523,43 @@ func newPrimaryKey(key *ast.PrimaryKeyNode) []string { return key.ColumnNameList() } +func newAlterSpec(namePath *NamePath, stmt *ast.AlterTableStmtNode) (*AlterTableSpec, error) { + + list := stmt.AlterActionList() + var columnsList []*ast.ColumnDefinitionNode + + var err error + + for i := range list { + action := list[i] + if err != nil { + return nil, err + } + switch action.Kind() { + case ast.SetOptionsAction: + err = fmt.Errorf("SetOptionsAction not supported") + case ast.RenameColumnAction: + err = fmt.Errorf("RenameColumnAction not supported") + case ast.RenameStmt: + err = fmt.Errorf("RenameStmt not supported") + case ast.AddColumnAction: + addColumnAction := action.(*ast.AddColumnActionNode) + columnsList = append(columnsList, addColumnAction.ColumnDefinition()) + case ast.DropColumnAction: + err = fmt.Errorf("DropColumnAction not supported") + case ast.RenameToAction: + err = fmt.Errorf("RenameToAction not supported") + } + } + + now := time.Now() + return &AlterTableSpec{ + NamePath: namePath.mergePath(stmt.NamePath()), + AddedColumns: newColumnsFromDef(columnsList), + UpdatedAt: now, + }, nil +} + func newTableSpec(namePath *NamePath, stmt *ast.CreateTableStmtNode) *TableSpec { now := time.Now() return &TableSpec{ diff --git a/internal/stmt_action.go b/internal/stmt_action.go index 8dee05e..7b3bb86 100644 --- a/internal/stmt_action.go +++ b/internal/stmt_action.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/gdexlab/go-render/render" ast "github.com/goccy/go-zetasql/resolved_ast" ) @@ -17,6 +18,76 @@ type StmtAction interface { Args() []interface{} } +type AlterTableStmtAction struct { + query string + node *ast.AlterTableStmtNode + spec *AlterTableSpec + actions []ast.AlterActionNode + args []interface{} + rawArgs []driver.NamedValue + catalog *Catalog +} + +func (a *AlterTableStmtAction) Prepare(ctx context.Context, conn *Conn) (driver.Stmt, error) { + stmt, err := conn.PrepareContext(ctx, a.query) + if err != nil { + return nil, fmt.Errorf("failed to prepare %s: %w", a.query, err) + } + return newDMLStmt(stmt, []*ast.ParameterNode{}, ""), nil +} + +func debug(context string, any interface{}) { + fmt.Printf("DEBUG(%s): %s\n", context, render.Render(any)) +} + +func (a *AlterTableStmtAction) exec(ctx context.Context, conn *Conn) error { + debug("args", a.args) + debug("rawArgs", a.rawArgs) + + var statementsToExecute []string + + for _, column := range a.spec.AddedColumns { + statementsToExecute = append( + statementsToExecute, + fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN `%s` %s;", formatPath(a.spec.NamePath), column.Name, column.SQLiteSchema()), + ) + } + + fullQuery := strings.Join(statementsToExecute, "\n") + + if _, err := conn.ExecContext(ctx, fullQuery); err != nil { + return fmt.Errorf("failed to exec %s: %w", a.query, err) + } + + if err := a.catalog.modifyTableSpec(a.spec); err != nil { + return fmt.Errorf("failed to add new table spec: %w", err) + } + + return nil +} + +func (a *AlterTableStmtAction) ExecContext(ctx context.Context, conn *Conn) (driver.Result, error) { + if err := a.exec(ctx, conn); err != nil { + return nil, err + } + return &Result{conn: conn}, nil +} + +func (a *AlterTableStmtAction) QueryContext(ctx context.Context, conn *Conn) (*Rows, error) { + if err := a.exec(ctx, conn); err != nil { + return nil, err + } + return &Rows{conn: conn}, nil +} + +func (a *AlterTableStmtAction) Args() []interface{} { + return a.args +} + +func (a *AlterTableStmtAction) Cleanup(ctx context.Context, conn *Conn) error { + return nil +} + type CreateTableStmtAction struct { query string args []interface{}