912 lines
33 KiB
Go
912 lines
33 KiB
Go
// Copyright 2026 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package _go
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// localRemoteUrl returns a file:// URL for a fresh temp directory suitable for use as a Dolt
|
|
// remote. Each call produces a unique directory, so two calls with the same |name| never collide.
|
|
// We avoid using [testing.T.TempDir] here, because on Windows, NBS chunk files can still be
|
|
// memory-mapped and locked for a brief window after a server's controller reports itself stopped,
|
|
// which interfers with the cleanup.
|
|
func localRemoteUrl(t *testing.T, name string) string {
|
|
t.Helper()
|
|
safeName := strings.ReplaceAll(t.Name(), "/", "_")
|
|
dir, err := os.MkdirTemp(os.TempDir(), safeName+"_"+name)
|
|
require.NoError(t, err)
|
|
return "file://" + filepath.ToSlash(dir)
|
|
}
|
|
|
|
// runRemoteTest runs |script| inside a dedicated, freshly-started on-disk Doltgres server and
|
|
// tears the server down when the subtest completes.
|
|
//
|
|
// A new server is started for every call so that each push/pull/clone test begins with a clean
|
|
// database state. The server runs against a temporary on-disk directory (via
|
|
// CreateServerLocalWithPort) because pushing to, pulling from, or cloning a file:// remote
|
|
// requires a real NBS chunk store -- in-memory storage cannot be synced to a file:// URL.
|
|
//
|
|
// dolt_push/dolt_pull/dolt_fetch/dolt_clone/dolt_remote are all invoked with SELECT rather than
|
|
// CALL: Doltgres only allows built-in Dolt procedures to be invoked as functions (see
|
|
// ErrDoltProcedureSelectOnly in server/functions/dolt_procedures.go).
|
|
func runRemoteTest(t *testing.T, script ScriptTest) {
|
|
t.Helper()
|
|
t.Run(script.Name, func(t *testing.T) {
|
|
t.Helper()
|
|
port, err := sql.GetEmptyPort()
|
|
require.NoError(t, err)
|
|
ctx, conn, controller := CreateServerLocalWithPort(t, "postgres", port)
|
|
defer func() {
|
|
conn.Close(ctx)
|
|
controller.Stop()
|
|
require.NoError(t, controller.WaitForStop())
|
|
}()
|
|
runScript(t, ctx, script, conn, true)
|
|
})
|
|
}
|
|
|
|
// TestDoltRemote exercises the file-system remote push/pull/fetch/clone workflow end to end.
|
|
func TestDoltRemote(t *testing.T) {
|
|
remoteUrl := localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "push and clone preserve table data and commit history",
|
|
SetUpScript: []string{
|
|
"create table items (id int primary key, label text not null);",
|
|
"insert into items values (1, 'apple'), (2, 'banana');",
|
|
"select dolt_commit('-Am', 'first commit: add apple and banana');",
|
|
"insert into items values (3, 'cherry');",
|
|
"select dolt_commit('-Am', 'second commit: add cherry');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_items');", remoteUrl),
|
|
"USE cloned_items",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select id, label from items order by id;",
|
|
Expected: []sql.Row{{1, "apple"}, {2, "banana"}, {3, "cherry"}},
|
|
},
|
|
{
|
|
// Initialize + CREATE DATABASE + 2 user commits.
|
|
Query: "select count(*) from dolt.commits;",
|
|
Expected: []sql.Row{{int64(4)}},
|
|
},
|
|
{
|
|
Query: "select message from dolt.commits order by date;",
|
|
Expected: []sql.Row{{"Initialize data repository"}, {"CREATE DATABASE"}, {"first commit: add apple and banana"}, {"second commit: add cherry"}},
|
|
},
|
|
{
|
|
// dolt_clone automatically configures "origin" to point back at the source URL.
|
|
Query: "select name from dolt.remotes;",
|
|
Expected: []sql.Row{{"origin"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "standalone sequence state is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create sequence counter start 1 increment 5;",
|
|
"select nextval('counter');", // advances to 1
|
|
"select nextval('counter');", // advances to 6
|
|
"select nextval('counter');", // advances to 11
|
|
"select dolt_commit('-Am', 'seed counter');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_seq');", remoteUrl),
|
|
"drop sequence counter", // delete the sequence in the first database
|
|
"USE cloned_seq",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select sequence_name, sequence_catalog from information_schema.sequences where sequence_schema = 'public';",
|
|
Expected: []sql.Row{{"counter", "cloned_seq"}},
|
|
},
|
|
{
|
|
// Must continue from where it left off (after 11, next is 16), not reset to 1.
|
|
Query: "select nextval('counter');",
|
|
Expected: []sql.Row{{int64(16)}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "serial column's owned sequence state is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create table widgets (id serial primary key, name text);",
|
|
"insert into widgets (name) values ('a'), ('b'), ('c');",
|
|
"select dolt_commit('-Am', 'seed widgets');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_widgets');", remoteUrl),
|
|
"drop table widgets",
|
|
"USE cloned_widgets",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select id, name from widgets order by id;",
|
|
Expected: []sql.Row{{1, "a"}, {2, "b"}, {3, "c"}},
|
|
},
|
|
{
|
|
// Auto-generated id must continue from 4, proving the owned sequence's state was pushed.
|
|
Query: "insert into widgets (name) values ('d') returning id;",
|
|
Expected: []sql.Row{{4}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "identity column's owned sequence state is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create table gadgets (id bigint generated by default as identity primary key, name text);",
|
|
"insert into gadgets (name) values ('a'), ('b'), ('c');",
|
|
"select dolt_add('.');",
|
|
"select dolt_commit('-m', 'seed gadgets');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_gadgets');", remoteUrl),
|
|
"drop table gadgets",
|
|
"USE cloned_gadgets",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select id, name from gadgets order by id;",
|
|
Expected: []sql.Row{{1, "a"}, {2, "b"}, {3, "c"}},
|
|
},
|
|
{
|
|
Query: "insert into gadgets (name) values ('d') returning id;",
|
|
Expected: []sql.Row{{4}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "sequence in a non-public schema is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create schema myschema;",
|
|
"create sequence myschema.seq2 start 100 increment 10;",
|
|
"select nextval('myschema.seq2');", // advances to 100
|
|
"select dolt_add('.');",
|
|
"select dolt_commit('-m', 'seed myschema.seq2');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_schema');", remoteUrl),
|
|
"USE cloned_schema",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select sequence_schema, sequence_name from information_schema.sequences where sequence_name = 'seq2';",
|
|
Expected: []sql.Row{{"myschema", "seq2"}},
|
|
},
|
|
{
|
|
// Must continue from 100 (after one call), not reset to the start value.
|
|
Query: "select nextval('myschema.seq2');",
|
|
Expected: []sql.Row{{int64(110)}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "incremental push and pull keep sequence and table state in sync",
|
|
SetUpScript: []string{
|
|
"create sequence counter start 1 increment 5;",
|
|
"create table orders (id int primary key default nextval('counter'), item text);",
|
|
"select dolt_commit('-Am', 'initial schema');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'replica');", remoteUrl),
|
|
// Advance the sequence and add data on the source side only.
|
|
"USE postgres",
|
|
"insert into orders (item) values ('widget');", // consumes nextval -> 1
|
|
"select dolt_commit('-Am', 'first order');",
|
|
"select dolt_push('origin', 'main');",
|
|
// Pull the change into the replica.
|
|
"USE replica",
|
|
"select dolt_pull('origin');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select id, item from orders order by id;",
|
|
Expected: []sql.Row{{1, "widget"}},
|
|
},
|
|
{
|
|
Query: "select nextval('counter');",
|
|
Expected: []sql.Row{{int64(6)}},
|
|
},
|
|
},
|
|
})
|
|
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "dolt_remotes reflects add and remove",
|
|
SetUpScript: []string{
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", localRemoteUrl(t, "remote_a")),
|
|
fmt.Sprintf("select dolt_remote('add', 'other', '%s');", localRemoteUrl(t, "remote_b")),
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select name from dolt_remotes order by name;",
|
|
Expected: []sql.Row{{"origin"}, {"other"}},
|
|
},
|
|
{
|
|
Query: "select dolt_remote('remove', 'other');",
|
|
Expected: []sql.Row{{"{0}"}},
|
|
},
|
|
{
|
|
Query: "select name from dolt_remotes;",
|
|
Expected: []sql.Row{{"origin"}},
|
|
},
|
|
{
|
|
Query: "select dolt_remote('remove', 'other');",
|
|
ExpectedErr: "unknown remote",
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "fetch updates remote-tracking branches without touching the working branch; pull merges",
|
|
SetUpScript: []string{
|
|
"create table events (id int primary key, name text);",
|
|
"insert into events values (1, 'first');",
|
|
"select dolt_commit('-Am', 'first event');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'subscriber');", remoteUrl),
|
|
"USE postgres",
|
|
"insert into events values (2, 'second');",
|
|
"select dolt_commit('-Am', 'second event');",
|
|
"select dolt_push('origin', 'main');",
|
|
"USE subscriber",
|
|
"select dolt_fetch('origin', 'main');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// fetch alone must not update the working branch.
|
|
Query: "select id, name from events order by id;",
|
|
Expected: []sql.Row{{1, "first"}},
|
|
},
|
|
{
|
|
Query: "select name from dolt_remote_branches;",
|
|
Expected: []sql.Row{{"remotes/origin/main"}},
|
|
},
|
|
{
|
|
Query: "select dolt_pull('origin');",
|
|
Expected: []sql.Row{{`{1,0,"merge successful"}`}},
|
|
},
|
|
{
|
|
// pull must fast-forward the working branch to match the remote.
|
|
Query: "select id, name from events order by id;",
|
|
Expected: []sql.Row{{1, "first"}, {2, "second"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "pull fails when the working set has uncommitted changes",
|
|
SetUpScript: []string{
|
|
"create table logs (id int primary key, msg text);",
|
|
"insert into logs values (1, 'a');",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'dirty_clone');", remoteUrl),
|
|
"USE postgres",
|
|
"insert into logs values (2, 'b');",
|
|
"select dolt_commit('-Am', 'second');",
|
|
"select dolt_push('origin', 'main');",
|
|
"USE dirty_clone",
|
|
// Leave an uncommitted change on the clone before pulling.
|
|
"insert into logs values (3, 'uncommitted');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select dolt_pull('origin');",
|
|
ExpectedErr: "cannot merge with uncommitted changes",
|
|
},
|
|
},
|
|
})
|
|
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "push without a configured remote returns an error",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key);",
|
|
"select dolt_add('.');",
|
|
"select dolt_commit('-m', 'seed');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select dolt_push('origin', 'main');",
|
|
ExpectedErr: "remote 'origin' not found",
|
|
},
|
|
},
|
|
})
|
|
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "pull without a configured remote returns an error",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key);",
|
|
"select dolt_add('.');",
|
|
"select dolt_commit('-m', 'seed');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select dolt_pull('origin');",
|
|
ExpectedErr: "remote 'origin' not found",
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "custom enum type is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create type mood as enum ('sad', 'ok', 'happy');",
|
|
"create table moods (id int primary key, m mood);",
|
|
"insert into moods values (1, 'happy'), (2, 'sad');",
|
|
"select dolt_commit('-Am', 'seed moods');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_moods');", remoteUrl),
|
|
"drop table moods;",
|
|
"drop type mood;",
|
|
"USE cloned_moods",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select id, m::text from moods order by id;",
|
|
Expected: []sql.Row{{1, "happy"}, {2, "sad"}},
|
|
},
|
|
{
|
|
// The enum type definition itself (not just data using it) must have transferred.
|
|
Query: "insert into moods values (3, 'ok') returning m::text;",
|
|
Expected: []sql.Row{{"ok"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "user-defined function is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create function double_it(x int) returns int as $$ begin return x * 2; end; $$ language plpgsql;",
|
|
"select dolt_commit('-Am', 'seed function');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_func');", remoteUrl),
|
|
"drop function double_it();",
|
|
"USE cloned_func",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select double_it(21);",
|
|
Expected: []sql.Row{{42}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "pushing and cloning a non-default branch carries its own sequence and table state",
|
|
SetUpScript: []string{
|
|
"create sequence counter start 1 increment 1;",
|
|
"create table gadgets (id int primary key default nextval('counter'), name text);",
|
|
"insert into gadgets (name) values ('a'), ('b'), ('c');", // consumes 1, 2, 3
|
|
"select dolt_commit('-Am', 'seed on main');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
"select dolt_checkout('-b', 'feature');",
|
|
"insert into gadgets (name) values ('feature-only');", // consumes 4
|
|
"select dolt_commit('-Am', 'feature work');",
|
|
"select dolt_push('origin', 'feature');",
|
|
fmt.Sprintf("select dolt_clone('--branch', 'feature', '%s', 'cloned_feature');", remoteUrl),
|
|
"drop table gadgets;",
|
|
"drop sequence counter;",
|
|
"USE cloned_feature",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select name from gadgets order by id;",
|
|
Expected: []sql.Row{{"a"}, {"b"}, {"c"}, {"feature-only"}},
|
|
},
|
|
{
|
|
// The owned sequence must reflect the feature branch's state (after 4), not main's.
|
|
Query: "insert into gadgets (name) values ('next') returning id;",
|
|
Expected: []sql.Row{{5}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "non-fast-forward push is rejected, then succeeds with --force",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key, note text);",
|
|
"insert into t values (1, 'source');",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'clone1');", remoteUrl),
|
|
// Diverge history on both sides so neither is a fast-forward of the other.
|
|
"insert into t values (2, 'source-diverged');",
|
|
"select dolt_commit('-Am', 'source diverges');",
|
|
"select dolt_push('origin', 'main');",
|
|
"USE clone1",
|
|
"insert into t values (3, 'clone-diverged');",
|
|
"select dolt_commit('-Am', 'clone diverges');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select dolt_push('origin', 'main');",
|
|
ExpectedErr: "failed to push some refs",
|
|
},
|
|
{
|
|
Query: "select dolt_push('-f', 'origin', 'main');",
|
|
SkipResultsCheck: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "pushing a branch delete removes it from the remote",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key);",
|
|
"insert into t values (1);",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
"select dolt_checkout('-b', 'doomed');",
|
|
"insert into t values (2);",
|
|
"select dolt_commit('-Am', 'doomed work');",
|
|
"select dolt_push('origin', 'doomed');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'observer');", remoteUrl),
|
|
"USE observer",
|
|
"select dolt_fetch('origin', 'doomed');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select name from dolt_remote_branches order by name;",
|
|
Expected: []sql.Row{{"remotes/origin/doomed"}, {"remotes/origin/main"}},
|
|
},
|
|
{
|
|
Query: "USE postgres",
|
|
SkipResultsCheck: true,
|
|
},
|
|
{
|
|
// A ":branch" refspec deletes the branch on the remote, just like the CLI's `dolt push origin :doomed`.
|
|
Query: "select dolt_push('origin', ':doomed');",
|
|
SkipResultsCheck: true,
|
|
},
|
|
{
|
|
Query: "USE observer",
|
|
SkipResultsCheck: true,
|
|
},
|
|
{
|
|
Query: "select dolt_fetch('--prune', 'origin');",
|
|
SkipResultsCheck: true,
|
|
},
|
|
{
|
|
Query: "select name from dolt_remote_branches order by name;",
|
|
Expected: []sql.Row{{"remotes/origin/main"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "tags are pushed and fetched along with commits",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key);",
|
|
"insert into t values (1);",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
"select dolt_tag('v1.0', '-m', 'first release');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
"select dolt_push('origin', 'v1.0');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_tags');", remoteUrl),
|
|
"USE cloned_tags",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select tag_name from dolt_tags;",
|
|
Expected: []sql.Row{{"v1.0"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "fetch supports an explicit refspec into a custom tracking ref",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key);",
|
|
"insert into t values (1);",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'observer');", remoteUrl),
|
|
"USE observer",
|
|
"select dolt_fetch('origin', 'refs/heads/main:refs/remotes/custom/main');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select name from dolt_remote_branches where name = 'remotes/custom/main';",
|
|
Expected: []sql.Row{{"remotes/custom/main"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "push --all pushes every local branch",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key);",
|
|
"insert into t values (1);",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
"select dolt_checkout('-b', 'branch_a');",
|
|
"select dolt_checkout('main');",
|
|
"select dolt_checkout('-b', 'branch_b');",
|
|
"select dolt_checkout('main');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('--all', 'origin');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_all');", remoteUrl),
|
|
"USE cloned_all",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// Clone only checks out a local branch for the default branch; the others land as
|
|
// remote-tracking refs, proving --all pushed them all to the remote.
|
|
Query: "select name from dolt_remote_branches order by name;",
|
|
Expected: []sql.Row{{"remotes/origin/branch_a"}, {"remotes/origin/branch_b"}, {"remotes/origin/main"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "adding a remote with a name that already exists returns an error",
|
|
SetUpScript: []string{
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", localRemoteUrl(t, "remote_a")),
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", localRemoteUrl(t, "remote_b")),
|
|
ExpectedErr: "remote already exists",
|
|
},
|
|
},
|
|
})
|
|
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "adding a remote with an invalid name returns an error",
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: fmt.Sprintf("select dolt_remote('add', 'bad name', '%s');", localRemoteUrl(t, "remote")),
|
|
ExpectedErr: "remote name invalid",
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "fetching an invalid ref spec returns an error",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key);",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select dolt_fetch('origin', 'garbage');",
|
|
ExpectedErr: "invalid ref spec",
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "pull auto-merges non-conflicting divergent history",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key, v text);",
|
|
"insert into t values (1, 'a'), (2, 'b');",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'clone1');", remoteUrl),
|
|
"USE postgres",
|
|
"insert into t values (3, 'source-row');",
|
|
"select dolt_commit('-Am', 'source adds row 3');",
|
|
"select dolt_push('origin', 'main');",
|
|
"USE clone1",
|
|
"insert into t values (4, 'clone-row');",
|
|
"select dolt_commit('-Am', 'clone adds row 4');",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// Divergent but non-conflicting: no fast-forward, no conflicts, and it merges automatically.
|
|
Query: "select dolt_pull('origin');",
|
|
Expected: []sql.Row{{`{0,0,"merge successful"}`}},
|
|
},
|
|
{
|
|
Query: "select id, v from t order by id;",
|
|
Expected: []sql.Row{{1, "a"}, {2, "b"}, {3, "source-row"}, {4, "clone-row"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "pull surfaces real merge conflicts, resolvable via dolt_conflicts_resolve",
|
|
SetUpScript: []string{
|
|
"create table t (id int primary key, v text);",
|
|
"insert into t values (1, 'a'), (2, 'b');",
|
|
"select dolt_commit('-Am', 'seed');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'clone1');", remoteUrl),
|
|
"USE postgres",
|
|
"update t set v = 'source-edit' where id = 1;",
|
|
"select dolt_commit('-Am', 'source edits row 1');",
|
|
"select dolt_push('origin', 'main');",
|
|
"USE clone1",
|
|
"update t set v = 'clone-edit' where id = 1;",
|
|
"select dolt_commit('-Am', 'clone edits row 1');",
|
|
// Conflicts abort the implicit transaction under autocommit unless explicitly allowed.
|
|
"set dolt_allow_commit_conflicts to 1;",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select dolt_pull('origin');",
|
|
Expected: []sql.Row{{`{0,1,"merge has unresolved conflicts or constraint violations"}`}},
|
|
},
|
|
{
|
|
Query: "select base_v, our_v, their_v from dolt_conflicts_t;",
|
|
Expected: []sql.Row{{"a", "clone-edit", "source-edit"}},
|
|
},
|
|
{
|
|
Query: "select dolt_conflicts_resolve('--ours', 't');",
|
|
SkipResultsCheck: true,
|
|
},
|
|
{
|
|
Query: "select count(*) from dolt_conflicts;",
|
|
Expected: []sql.Row{{int64(0)}},
|
|
},
|
|
{
|
|
// --ours keeps the clone's own edit.
|
|
Query: "select id, v from t order by id;",
|
|
Expected: []sql.Row{{1, "clone-edit"}, {2, "b"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "domain type and its check constraint are preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create domain pos_int as integer check (value > 0);",
|
|
"create table measurements (id int primary key, val pos_int);",
|
|
"insert into measurements values (1, 42);",
|
|
"select dolt_commit('-Am', 'seed measurements');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_domain');", remoteUrl),
|
|
"drop table measurements;",
|
|
"drop domain pos_int;",
|
|
"USE cloned_domain",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// typtype = 'd' for domain types.
|
|
Query: "select typname, typtype from pg_type where typname = 'pos_int';",
|
|
Expected: []sql.Row{{"pos_int", "d"}},
|
|
},
|
|
{
|
|
Query: "select val from measurements where id = 1;",
|
|
Expected: []sql.Row{{42}},
|
|
},
|
|
{
|
|
// The domain's check constraint must still be enforced after cloning.
|
|
Query: "insert into measurements values (2, -1);",
|
|
ExpectedErr: "pos_int_check",
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "user-defined composite type is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
`create type point_t as (x float8, y float8);`,
|
|
`create function distance(p point_t) returns float8 as $$ begin return sqrt((p).x * (p).x + (p).y * (p).y); end; $$ language plpgsql;`,
|
|
"select dolt_commit('-Am', 'seed composite type');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_composite');", remoteUrl),
|
|
"drop function distance(point_t);",
|
|
"drop type point_t;",
|
|
"USE cloned_composite",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// typtype = 'c' for composite types.
|
|
Query: "select typname, typtype from pg_type where typname = 'point_t';",
|
|
Expected: []sql.Row{{"point_t", "c"}},
|
|
},
|
|
{
|
|
Query: "select distance(row(3.0, 4.0)::point_t);",
|
|
Expected: []sql.Row{{float64(5.0)}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "stored procedure is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create table job_log (id int primary key, status text);",
|
|
"insert into job_log values (1, 'pending');",
|
|
`create procedure mark_done(job_id int) as $$ begin update job_log set status = 'done' where id = job_id; end; $$ language plpgsql;`,
|
|
"select dolt_commit('-Am', 'seed procedure');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_proc');", remoteUrl),
|
|
"drop procedure mark_done(int);",
|
|
"drop table job_log;",
|
|
"USE cloned_proc",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "call mark_done(1);",
|
|
SkipResultsCheck: true,
|
|
},
|
|
{
|
|
Query: "select status from job_log where id = 1;",
|
|
Expected: []sql.Row{{"done"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "trigger is preserved and fires after clone",
|
|
SetUpScript: []string{
|
|
"create table readings (id int primary key, val int);",
|
|
`create function clamp_val() returns trigger as $$ begin if NEW.val > 100 then NEW.val := 100; end if; return NEW; end; $$ language plpgsql;`,
|
|
"create trigger clamp_trigger before insert on readings for each row execute function clamp_val();",
|
|
"select dolt_commit('-Am', 'seed trigger');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_trigger');", remoteUrl),
|
|
"drop trigger clamp_trigger on readings;",
|
|
"drop function clamp_val();",
|
|
"drop table readings;",
|
|
"USE cloned_trigger",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// Inserting val=200 must be clamped to 100 by the trigger, proving it still fires post-clone.
|
|
Query: "insert into readings values (1, 200);",
|
|
SkipResultsCheck: true,
|
|
},
|
|
{
|
|
Query: "select val from readings where id = 1;",
|
|
Expected: []sql.Row{{100}},
|
|
},
|
|
},
|
|
})
|
|
|
|
// TODO: Extension loading in Windows CI environments don't work currently
|
|
if !(runtime.GOOS == "windows" && os.Getenv("CI") != "") {
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "extension is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
`create extension "uuid-ossp";`,
|
|
"select dolt_commit('-Am', 'seed extension');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_ext');", remoteUrl),
|
|
"USE cloned_ext",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// pg_extension is an unimplemented stub; calling the extension function is the best available proof.
|
|
// uuid_generate_v4() returns a 36-character UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
|
|
Query: "select length(uuid_generate_v4()::text) = 36;",
|
|
Expected: []sql.Row{{"t"}},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "named schema contents are preserved across push and clone",
|
|
SetUpScript: []string{
|
|
"create schema inventory;",
|
|
"create table inventory.products (id int primary key, name text);",
|
|
"insert into inventory.products values (1, 'widget'), (2, 'gadget');",
|
|
"select dolt_commit('-Am', 'add inventory schema');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_schema_contents');", remoteUrl),
|
|
"drop table inventory.products;",
|
|
"drop schema inventory;",
|
|
"USE cloned_schema_contents",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
Query: "select schema_name from information_schema.schemata where schema_name = 'inventory';",
|
|
Expected: []sql.Row{{"inventory"}},
|
|
},
|
|
{
|
|
Query: "select id, name from inventory.products order by id;",
|
|
Expected: []sql.Row{{1, "widget"}, {2, "gadget"}},
|
|
},
|
|
},
|
|
})
|
|
|
|
remoteUrl = localRemoteUrl(t, "remote")
|
|
runRemoteTest(t, ScriptTest{
|
|
Name: "custom cast is preserved across push and clone",
|
|
SetUpScript: []string{
|
|
// CREATE TABLE implicitly creates a composite row type of the same name.
|
|
`CREATE TABLE cast_src (v text);`,
|
|
`CREATE TABLE cast_dst (v text, tag text);`,
|
|
`CREATE FUNCTION cast_src_to_dst(src cast_src) RETURNS cast_dst AS $$ SELECT ROW((src).v, 'casted')::cast_dst $$ LANGUAGE SQL;`,
|
|
`CREATE FUNCTION cast_verify(dst cast_dst) RETURNS text AS $$ SELECT (dst).v || ':' || (dst).tag $$ LANGUAGE SQL;`,
|
|
`CREATE CAST (cast_src AS cast_dst) WITH FUNCTION cast_src_to_dst(cast_src);`,
|
|
"select dolt_commit('-Am', 'seed cast');",
|
|
fmt.Sprintf("select dolt_remote('add', 'origin', '%s');", remoteUrl),
|
|
"select dolt_push('origin', 'main');",
|
|
fmt.Sprintf("select dolt_clone('%s', 'cloned_cast');", remoteUrl),
|
|
"drop cast (cast_src as cast_dst);",
|
|
"drop function cast_src_to_dst(cast_src);",
|
|
"drop function cast_verify(cast_dst);",
|
|
"drop table cast_src;",
|
|
"drop table cast_dst;",
|
|
"USE cloned_cast",
|
|
},
|
|
Assertions: []ScriptTestAssertion{
|
|
{
|
|
// context 'e' = explicit, method 'f' = function.
|
|
Query: `SELECT c.castcontext::text, c.castmethod::text
|
|
FROM pg_cast c
|
|
JOIN pg_type src ON src.oid = c.castsource
|
|
JOIN pg_type dst ON dst.oid = c.casttarget
|
|
WHERE src.typname = 'cast_src' AND dst.typname = 'cast_dst';`,
|
|
Expected: []sql.Row{{"e", "f"}},
|
|
},
|
|
{
|
|
Query: `SELECT cast_verify((ROW('hello')::cast_src)::cast_dst);`,
|
|
Expected: []sql.Row{{"hello:casted"}},
|
|
},
|
|
},
|
|
})
|
|
}
|