Skip to content

Commit 50fb2b2

Browse files
authored
support SELECT ... INTO OUTFILE/DUMPFILE ... (#2317)
1 parent 8c7762d commit 50fb2b2

File tree

15 files changed

+590
-165
lines changed

15 files changed

+590
-165
lines changed

enginetest/enginetests.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"io"
2222
"net"
23+
"os"
2324
"reflect"
2425
"strings"
2526
"sync"
@@ -887,26 +888,265 @@ func TestSpatialInsertInto(t *testing.T, harness Harness) {
887888
}
888889
}
889890

891+
// setSecureFilePriv sets the secure_file_priv system variable to the current working directory.
892+
func setSecureFilePriv() error {
893+
wd, err := os.Getwd()
894+
if err != nil {
895+
wd = "./"
896+
}
897+
return sql.SystemVariables.AssignValues(map[string]interface{}{
898+
"secure_file_priv": wd,
899+
})
900+
}
901+
890902
func TestLoadData(t *testing.T, harness Harness) {
891903
harness.Setup(setup.MydbData)
904+
905+
require.NoError(t, setSecureFilePriv())
906+
TestQuery(t, harness, "select @@secure_file_priv != '';", []sql.Row{{true}}, nil, nil)
907+
892908
for _, script := range queries.LoadDataScripts {
893909
TestScript(t, harness, script)
894910
}
895911
}
896912

897913
func TestLoadDataErrors(t *testing.T, harness Harness) {
914+
require.NoError(t, setSecureFilePriv())
915+
TestQuery(t, harness, "select @@secure_file_priv != '';", []sql.Row{{true}}, nil, nil)
916+
898917
for _, script := range queries.LoadDataErrorScripts {
899918
TestScript(t, harness, script)
900919
}
901920
}
902921

903922
func TestLoadDataFailing(t *testing.T, harness Harness) {
904923
t.Skip()
924+
925+
require.NoError(t, setSecureFilePriv())
926+
TestQuery(t, harness, "select @@secure_file_priv != '';", []sql.Row{{true}}, nil, nil)
927+
905928
for _, script := range queries.LoadDataFailingScripts {
906929
TestScript(t, harness, script)
907930
}
908931
}
909932

933+
func TestSelectIntoFile(t *testing.T, harness Harness) {
934+
harness.Setup(setup.MydbData, setup.MytableData, setup.EmptytableData, setup.NiltableData)
935+
e := mustNewEngine(t, harness)
936+
defer e.Close()
937+
938+
ctx := NewContext(harness)
939+
err := CreateNewConnectionForServerEngine(ctx, e)
940+
require.NoError(t, err, nil)
941+
942+
require.NoError(t, setSecureFilePriv())
943+
TestQuery(t, harness, "select @@secure_file_priv != '';", []sql.Row{{true}}, nil, nil)
944+
945+
tests := []struct {
946+
file string
947+
query string
948+
exp string
949+
err *errors.Kind
950+
skip bool
951+
}{
952+
{
953+
file: "outfile.txt",
954+
query: "select * from mytable into outfile 'outfile.txt';",
955+
exp: "" +
956+
"1\tfirst row\n" +
957+
"2\tsecond row\n" +
958+
"3\tthird row\n",
959+
},
960+
{
961+
file: "dumpfile.txt",
962+
query: "select * from mytable limit 1 into dumpfile 'dumpfile.txt';",
963+
exp: "1first row",
964+
},
965+
{
966+
file: "outfile.txt",
967+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',';",
968+
exp: "" +
969+
"1,first row\n" +
970+
"2,second row\n" +
971+
"3,third row\n",
972+
},
973+
{
974+
file: "outfile.txt",
975+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by '$$';",
976+
exp: "" +
977+
"1$$first row\n" +
978+
"2$$second row\n" +
979+
"3$$third row\n",
980+
},
981+
{
982+
file: "outfile.txt",
983+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' optionally enclosed by '\"';",
984+
exp: "" +
985+
"1,\"first row\"\n" +
986+
"2,\"second row\"\n" +
987+
"3,\"third row\"\n",
988+
},
989+
{
990+
file: "outfile.txt",
991+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' optionally enclosed by '$$';",
992+
err: sql.ErrUnexpectedSeparator,
993+
},
994+
{
995+
file: "outfile.txt",
996+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' escaped by '$$';",
997+
err: sql.ErrUnexpectedSeparator,
998+
},
999+
{
1000+
file: "outfile.txt",
1001+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' enclosed by '\"';",
1002+
exp: "" +
1003+
"\"1\",\"first row\"\n" +
1004+
"\"2\",\"second row\"\n" +
1005+
"\"3\",\"third row\"\n",
1006+
},
1007+
{
1008+
file: "outfile.txt",
1009+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' lines terminated by ';';",
1010+
exp: "" +
1011+
"1,first row;" +
1012+
"2,second row;" +
1013+
"3,third row;",
1014+
},
1015+
{
1016+
file: "outfile.txt",
1017+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' lines terminated by 'r';",
1018+
exp: "" +
1019+
"1,fi\\rst \\rowr" +
1020+
"2,second \\rowr" +
1021+
"3,thi\\rd \\rowr",
1022+
},
1023+
{
1024+
file: "outfile.txt",
1025+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' lines starting by 'r';",
1026+
exp: "" +
1027+
"r1,first row\n" +
1028+
"r2,second row\n" +
1029+
"r3,third row\n",
1030+
},
1031+
{
1032+
file: "outfile.txt",
1033+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by '';",
1034+
exp: "" +
1035+
"1\tfirst row\n" +
1036+
"2\tsecond row\n" +
1037+
"3\tthird row\n",
1038+
},
1039+
{
1040+
file: "outfile.txt",
1041+
query: "select * from mytable into outfile 'outfile.txt' fields terminated by ',' lines terminated by '';",
1042+
exp: "" +
1043+
"1,first row" +
1044+
"2,second row" +
1045+
"3,third row",
1046+
},
1047+
{
1048+
file: "outfile.txt",
1049+
query: "select * from niltable into outfile 'outfile.txt';",
1050+
exp: "1\t\\N\t\\N\t\\N\n" +
1051+
"2\t2\t1\t\\N\n" +
1052+
"3\t\\N\t0\t\\N\n" +
1053+
"4\t4\t\\N\t4\n" +
1054+
"5\t\\N\t1\t5\n" +
1055+
"6\t6\t0\t6\n",
1056+
},
1057+
{
1058+
file: "outfile.txt",
1059+
query: "select * from niltable into outfile 'outfile.txt' fields terminated by ',' enclosed by '\"';",
1060+
exp: "\"1\",\\N,\\N,\\N\n" +
1061+
"\"2\",\"2\",\"1\",\\N\n" +
1062+
"\"3\",\\N,\"0\",\\N\n" +
1063+
"\"4\",\"4\",\\N,\"4\"\n" +
1064+
"\"5\",\\N,\"1\",\"5\"\n" +
1065+
"\"6\",\"6\",\"0\",\"6\"\n",
1066+
},
1067+
{
1068+
file: "outfile.txt",
1069+
query: "select * from niltable into outfile 'outfile.txt' fields terminated by ',' escaped by '$';",
1070+
exp: "1,$N,$N,$N\n" +
1071+
"2,2,1,$N\n" +
1072+
"3,$N,0,$N\n" +
1073+
"4,4,$N,4\n" +
1074+
"5,$N,1,5\n" +
1075+
"6,6,0,6\n",
1076+
},
1077+
{
1078+
file: "outfile.txt",
1079+
query: "select * from niltable into outfile 'outfile.txt' fields terminated by ',' escaped by '';",
1080+
exp: "1,NULL,NULL,NULL\n" +
1081+
"2,2,1,NULL\n" +
1082+
"3,NULL,0,NULL\n" +
1083+
"4,4,NULL,4\n" +
1084+
"5,NULL,1,5\n" +
1085+
"6,6,0,6\n",
1086+
},
1087+
{
1088+
file: "./subdir/outfile.txt",
1089+
query: "select * from mytable into outfile './subdir/outfile.txt';",
1090+
exp: "" +
1091+
"1\tfirst row\n" +
1092+
"2\tsecond row\n" +
1093+
"3\tthird row\n",
1094+
},
1095+
{
1096+
file: "../outfile.txt",
1097+
query: "select * from mytable into outfile '../outfile.txt';",
1098+
err: sql.ErrSecureFilePriv,
1099+
},
1100+
{
1101+
file: "outfile.txt",
1102+
query: "select * from mytable into outfile 'outfile.txt' charset binary;",
1103+
err: sql.ErrUnsupportedFeature,
1104+
},
1105+
}
1106+
1107+
subdir := "subdir"
1108+
if _, subErr := os.Stat(subdir); subErr == nil {
1109+
subErr = os.RemoveAll(subdir)
1110+
require.NoError(t, subErr)
1111+
}
1112+
err = os.Mkdir(subdir, 0777)
1113+
require.NoError(t, err)
1114+
defer os.RemoveAll(subdir)
1115+
1116+
for _, tt := range tests {
1117+
t.Run(tt.query, func(t *testing.T) {
1118+
if tt.skip {
1119+
t.Skip()
1120+
}
1121+
if tt.err != nil {
1122+
AssertErrWithCtx(t, e, harness, ctx, tt.query, tt.err)
1123+
return
1124+
}
1125+
// in case there are any residual files from previous runs
1126+
os.Remove(tt.file)
1127+
TestQueryWithContext(t, ctx, e, harness, tt.query, nil, nil, nil)
1128+
res, err := os.ReadFile(tt.file)
1129+
require.NoError(t, err)
1130+
require.Equal(t, tt.exp, string(res))
1131+
os.Remove(tt.file)
1132+
})
1133+
}
1134+
1135+
// remove tmp directory from previously failed runs
1136+
exists := "exists.txt"
1137+
if _, existsErr := os.Stat(exists); existsErr == nil {
1138+
err = os.Remove(exists)
1139+
require.NoError(t, err)
1140+
}
1141+
file, err := os.Create(exists)
1142+
require.NoError(t, err)
1143+
file.Close()
1144+
defer os.Remove(exists)
1145+
1146+
AssertErrWithCtx(t, e, harness, ctx, "SELECT * FROM mytable INTO OUTFILE './exists.txt'", sql.ErrFileExists)
1147+
AssertErrWithCtx(t, e, harness, ctx, "SELECT * FROM mytable LIMIT 1 INTO DUMPFILE './exists.txt'", sql.ErrFileExists)
1148+
}
1149+
9101150
func TestReplaceInto(t *testing.T, harness Harness) {
9111151
harness.Setup(setup.MydbData, setup.MytableData, setup.Mytable_del_idxData, setup.TypestableData)
9121152
for _, tt := range queries.ReplaceQueries {

enginetest/memory_engine_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,10 @@ func TestLoadDataFailing(t *testing.T) {
543543
enginetest.TestLoadDataFailing(t, enginetest.NewDefaultMemoryHarness())
544544
}
545545

546+
func TestSelectIntoFile(t *testing.T) {
547+
enginetest.TestSelectIntoFile(t, enginetest.NewDefaultMemoryHarness())
548+
}
549+
546550
func TestReplaceInto(t *testing.T) {
547551
enginetest.TestReplaceInto(t, enginetest.NewDefaultMemoryHarness())
548552
}

enginetest/queries/load_queries.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ var LoadDataErrorScripts = []ScriptTest{
266266
},
267267
Assertions: []ScriptTestAssertion{
268268
{
269-
Query: "LOAD DATA INFILE '/x/ytx' INTO TABLE loadtable",
269+
Query: "LOAD DATA INFILE './bad/doesnotexist.txt' INTO TABLE loadtable",
270270
ExpectedErr: sql.ErrLoadDataCannotOpen,
271271
},
272272
},
@@ -299,7 +299,7 @@ var LoadDataErrorScripts = []ScriptTest{
299299
Assertions: []ScriptTestAssertion{
300300
{
301301
Query: "LOAD DATA INFILE './testdata/test1.txt' INTO TABLE loadtable FIELDS ESCAPED BY 'xx' (pk)",
302-
ExpectedErr: sql.ErrLoadDataCharacterLength,
302+
ExpectedErr: sql.ErrUnexpectedSeparator,
303303
},
304304
},
305305
},
@@ -311,7 +311,7 @@ var LoadDataErrorScripts = []ScriptTest{
311311
Assertions: []ScriptTestAssertion{
312312
{
313313
Query: "LOAD DATA INFILE './testdata/test1.txt' INTO TABLE loadtable FIELDS ENCLOSED BY 'xx' (pk)",
314-
ExpectedErr: sql.ErrLoadDataCharacterLength,
314+
ExpectedErr: sql.ErrUnexpectedSeparator,
315315
},
316316
},
317317
},

enginetest/queries/script_queries.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,12 +1265,8 @@ CREATE TABLE tab3 (
12651265
ExpectedErr: sql.ErrMoreThanOneRow,
12661266
},
12671267
{
1268-
Query: `SELECT 1 INTO OUTFILE 'x.txt'`,
1269-
ExpectedErr: sql.ErrUnsupportedSyntax,
1270-
},
1271-
{
1272-
Query: `SELECT id INTO DUMPFILE 'dump.txt' FROM tab1 ORDER BY id DESC LIMIT 15`,
1273-
ExpectedErr: sql.ErrUnsupportedSyntax,
1268+
Query: `SELECT id INTO DUMPFILE 'baddump.out' FROM tab1 ORDER BY id DESC LIMIT 15`,
1269+
ExpectedErr: sql.ErrMoreThanOneRow,
12741270
},
12751271
{
12761272
Query: `select 1, 2, 3 into @my1, @my2`,

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e
77
github.com/dolthub/jsonpath v0.0.2-0.20240201003050-392940944c15
88
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81
9-
github.com/dolthub/vitess v0.0.0-20240207121055-c057d2347007
9+
github.com/dolthub/vitess v0.0.0-20240207220624-0c2d2128fb7b
1010
github.com/go-kit/kit v0.10.0
1111
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d
1212
github.com/gocraft/dbr/v2 v2.7.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ github.com/dolthub/jsonpath v0.0.2-0.20240201003050-392940944c15 h1:sfTETOpsrNJP
5858
github.com/dolthub/jsonpath v0.0.2-0.20240201003050-392940944c15/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI=
5959
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9XGFa6q5Ap4Z/OhNkAMBaK5YeuEzwJt+NZdhiE=
6060
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY=
61-
github.com/dolthub/vitess v0.0.0-20240207121055-c057d2347007 h1:MvFoe0FnHhxQLyp4Ldw0HRj1yu83YErbtbr7XxhaIFk=
62-
github.com/dolthub/vitess v0.0.0-20240207121055-c057d2347007/go.mod h1:IwjNXSQPymrja5pVqmfnYdcy7Uv7eNJNBPK/MEh9OOw=
61+
github.com/dolthub/vitess v0.0.0-20240207220624-0c2d2128fb7b h1:7Sxjtwd7Cm2ilQDFvyXPfCkNkJMK//2GBGb3aM9ht7k=
62+
github.com/dolthub/vitess v0.0.0-20240207220624-0c2d2128fb7b/go.mod h1:IwjNXSQPymrja5pVqmfnYdcy7Uv7eNJNBPK/MEh9OOw=
6363
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
6464
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
6565
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=

sql/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,15 @@ var (
719719
// ErrUnsupportedJoinFactorCount is returned for a query with more commutable join tables than we support
720720
ErrUnsupportedJoinFactorCount = errors.NewKind("unsupported join factor count: expected fewer than %d tables, found %d")
721721

722+
// ErrSecureFilePriv is returned when an outfile/dumpfile path is invalid or not under the secure-file-priv directory
723+
ErrSecureFilePriv = errors.NewKind("The MySQL server is running with the --secure-file-priv option so it cannot execute this statement")
724+
725+
// ErrFileExists is returned when a file already exists
726+
ErrFileExists = errors.NewKind("File '%s' already exists")
727+
728+
// ErrUnexpectedSeparator is returned when an invalid separator is used
729+
ErrUnexpectedSeparator = errors.NewKind("Field separator argument is not what is expected; check the manual")
730+
722731
// ErrNotMatchingSRID is returned for SRID values not matching
723732
ErrNotMatchingSRID = errors.NewKind("The SRID of the geometry is %v, but the SRID of the column is %v. Consider changing the SRID of the geometry or the SRID property of the column.")
724733

0 commit comments

Comments
 (0)