Skip to content

Commit

Permalink
support CREATE VIEW (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
jennifersp authored May 9, 2024
1 parent 48deac2 commit 8639d45
Show file tree
Hide file tree
Showing 16 changed files with 5,580 additions and 788 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/cockroachdb/apd/v2 v2.0.3-0.20200518165714-d020e156310a
github.com/cockroachdb/errors v1.7.5
github.com/dolthub/dolt/go v0.40.5-0.20240508210609-8dc778024bfb
github.com/dolthub/dolt/go v0.40.5-0.20240509172140-12335605bc80
github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240508122845-d204d27ab067
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2
github.com/dolthub/go-mysql-server v0.18.2-0.20240506205942-6f757d28ad30
github.com/dolthub/go-mysql-server v0.18.2-0.20240509164257-3278929b9379
github.com/dolthub/sqllogictest/go v0.0.0-20240118211725-a52e3f5697e3
github.com/dolthub/vitess v0.0.0-20240429213844-e8e1b4cd75c4
github.com/fatih/color v1.13.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dolthub/dolt/go v0.40.5-0.20240508210609-8dc778024bfb h1:TM0zbngh2Mf9Hzg2G5RSvX6oY9INlBhTAECMtfDNbag=
github.com/dolthub/dolt/go v0.40.5-0.20240508210609-8dc778024bfb/go.mod h1:L/+pp+co/kFHAzkJW59wZ8ZYPMll5y2OLACs4Ez82ic=
github.com/dolthub/dolt/go v0.40.5-0.20240509172140-12335605bc80 h1:zwtYRp1dF9MK3PJbmxdvA5CVt+syzlLmmPBjTj6pDxc=
github.com/dolthub/dolt/go v0.40.5-0.20240509172140-12335605bc80/go.mod h1:lagXAFpv7JSZe1dp/hi384bMqnyTrDUQPYG7JlJfEgc=
github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240508122845-d204d27ab067 h1:H7qIsM3fMIOdgIKbLsEI6NmC7nfHnmLFD0/UO6Ue+TU=
github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240508122845-d204d27ab067/go.mod h1:gHeHIDGU7em40EhFTliq62pExFcc1hxDTIZ9g5UqXYM=
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
Expand All @@ -224,8 +224,8 @@ github.com/dolthub/fslock v0.0.3 h1:iLMpUIvJKMKm92+N1fmHVdxJP5NdyDK5bK7z7Ba2s2U=
github.com/dolthub/fslock v0.0.3/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0=
github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e h1:kPsT4a47cw1+y/N5SSCkma7FhAPw7KeGmD6c9PBZW9Y=
github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e/go.mod h1:KPUcpx070QOfJK1gNe0zx4pA5sicIK1GMikIGLKC168=
github.com/dolthub/go-mysql-server v0.18.2-0.20240506205942-6f757d28ad30 h1:+xolBXi4KcBkyfKxw/ZSYbeVtpt3q0jArqKxJGVwFt8=
github.com/dolthub/go-mysql-server v0.18.2-0.20240506205942-6f757d28ad30/go.mod h1:T6EEu2iQoasR13Ovtp44yDn+rXQOBgh3BACPZMxSF/8=
github.com/dolthub/go-mysql-server v0.18.2-0.20240509164257-3278929b9379 h1:EEuMBsOD0lVImrWPcw+9SChorHmXMDu9VCsQOnrdFHs=
github.com/dolthub/go-mysql-server v0.18.2-0.20240509164257-3278929b9379/go.mod h1:T6EEu2iQoasR13Ovtp44yDn+rXQOBgh3BACPZMxSF/8=
github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI=
github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q=
github.com/dolthub/ishell v0.0.0-20221214210346-d7db0b066488 h1:0HHu0GWJH0N6a6keStrHhUAK5/o9LVfkh44pvsV4514=
Expand Down
30 changes: 24 additions & 6 deletions postgres/parser/parser/sql.y
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,9 @@ func (u *sqlSymUnion) storageType() tree.StorageType {
func (u *sqlSymUnion) detachPartition() tree.DetachPartition {
return u.val.(tree.DetachPartition)
}
func (u *sqlSymUnion) viewOption() tree.ViewOption {
return u.val.(tree.ViewOption)
}
func (u *sqlSymUnion) viewOptions() tree.ViewOptions {
return u.val.(tree.ViewOptions)
}
Expand Down Expand Up @@ -701,7 +704,7 @@ func (u *sqlSymUnion) aggregatesToDrop() []tree.AggregateToDrop {
%token <str> BOOLEAN BOTH BOX2D BUNDLE BY

%token <str> CACHE CHAIN CALL CALLED CANCEL CANCELQUERY CANONICAL CASCADE CASCADED CASE CAST CATEGORY CBRT
%token <str> CHANGEFEED CHAR CHARACTER CHARACTERISTICS CHECK CLASS CLOSE
%token <str> CHANGEFEED CHAR CHARACTER CHARACTERISTICS CHECK CHECK_OPTION CLASS CLOSE
%token <str> CLUSTER COALESCE COLLATABLE COLLATE COLLATION COLLATION_VERSION COLUMN COLUMNS COMBINEFUNC COMMENT COMMENTS
%token <str> COMMIT COMMITTED COMPACT COMPLETE COMPRESSION CONCAT CONCURRENTLY CONFIGURATION CONFIGURATIONS CONFIGURE
%token <str> CONFLICT CONNECT CONNECTION CONSTRAINT CONSTRAINTS CONTAINS CONTROLCHANGEFEED
Expand Down Expand Up @@ -771,7 +774,8 @@ func (u *sqlSymUnion) aggregatesToDrop() []tree.AggregateToDrop {
%token <str> RETRY RETURN RETURNING RETURNS REVISION_HISTORY REVOKE RIGHT
%token <str> ROLE ROLES ROUTINE ROUTINES ROLLBACK ROLLUP ROW ROWS RSHIFT RULE RUNNING

%token <str> SAFE SAVEPOINT SCATTER SCHEDULE SCHEDULES SCHEMA SCHEMAS SCRUB SEARCH SECOND SECURITY SEED SELECT SEND
%token <str> SAFE SAVEPOINT SCATTER SCHEDULE SCHEDULES SCHEMA SCHEMAS SCRUB SEARCH SECOND SECURITY
%token <str> SECURITY_BARRIER SECURITY_INVOKER SEED SELECT SEND
%token <str> SERIALFUNC SERIALIZABLE SERVER SESSION SESSIONS SESSION_USER SET SETOF SETTING SETTINGS SEQUENCE SEQUENCES SFUNC
%token <str> SHARE SHAREABLE SHOW SIMILAR SIMPLE SKIP SKIP_MISSING_FOREIGN_KEYS
%token <str> SKIP_MISSING_SEQUENCES SKIP_MISSING_SEQUENCE_OWNERS SKIP_MISSING_VIEWS SMALLINT SMALLSERIAL SNAPSHOT SOME
Expand Down Expand Up @@ -1136,6 +1140,7 @@ func (u *sqlSymUnion) aggregatesToDrop() []tree.AggregateToDrop {
%type <*tree.LanguageHandler> opt_language_handler

%type <tree.ViewOptions> view_options opt_with_view_options
%type <tree.ViewOption> view_option
%type <tree.ViewCheckOption> opt_with_check_option

%type <tree.Expr> opt_when
Expand Down Expand Up @@ -8546,15 +8551,25 @@ opt_with_view_options:
}

view_options:
name opt_var_value
view_option
{
$$.val = tree.ViewOptions{{Name: $1, Val: $2.expr()}}
$$.val = tree.ViewOptions{$1.viewOption()}
}
| view_options ',' name opt_var_value
| view_options ',' view_option
{
$$.val = append($1.viewOptions(), tree.ViewOption{Name: $3, Val: $4.expr()})
$$.val = append($1.viewOptions(), $3.viewOption())
}

view_option:
CHECK_OPTION { $$.val = tree.ViewOption{Name: $1, CheckOpt: "cascaded"} }
| CHECK_OPTION '=' SCONST { $$.val = tree.ViewOption{Name: $1, CheckOpt: $3} }
| SECURITY_BARRIER { $$.val = tree.ViewOption{Name: $1, Security: false} }
| SECURITY_BARRIER '=' TRUE { $$.val = tree.ViewOption{Name: $1, Security: true} }
| SECURITY_BARRIER '=' FALSE { $$.val = tree.ViewOption{Name: $1, Security: false} }
| SECURITY_INVOKER { $$.val = tree.ViewOption{Name: $1, Security: false} }
| SECURITY_INVOKER '=' TRUE { $$.val = tree.ViewOption{Name: $1, Security: true} }
| SECURITY_INVOKER '=' FALSE { $$.val = tree.ViewOption{Name: $1, Security: false} }

create_materialized_view_stmt:
CREATE MATERIALIZED VIEW view_name opt_column_list opt_using_method opt_with_storage_parameter_list opt_tablespace AS select_stmt opt_create_as_with_data
{
Expand Down Expand Up @@ -13883,6 +13898,7 @@ unreserved_keyword:
| BY
| CACHE
| CHAIN
| CHECK_OPTION
| CALL
| CALLED
| CANCEL
Expand Down Expand Up @@ -14206,6 +14222,8 @@ unreserved_keyword:
| SEARCH
| SECOND
| SECURITY
| SECURITY_BARRIER
| SECURITY_INVOKER
| SEED
| SEND
| SERIALFUNC
Expand Down
25 changes: 19 additions & 6 deletions postgres/parser/sem/tree/create_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

package tree

import "strings"

var _ Statement = &CreateView{}

// CreateView represents a CREATE VIEW statement.
Expand Down Expand Up @@ -81,8 +83,9 @@ const (
type ViewOptions []ViewOption

type ViewOption struct {
Name string
Val Expr
Name string
CheckOpt string
Security bool
}

func (node ViewOptions) Format(ctx *FmtCtx) {
Expand All @@ -91,10 +94,20 @@ func (node ViewOptions) Format(ctx *FmtCtx) {
if i != 0 {
ctx.WriteString(", ")
}
ctx.WriteString(opt.Name)
if opt.Val != nil {
ctx.WriteString(" = ")
ctx.FormatNode(opt.Val)
switch strings.ToLower(opt.Name) {
case "check_option":
ctx.WriteString(opt.Name)
if opt.CheckOpt != "" {
ctx.WriteString(" = ")
ctx.WriteString(opt.CheckOpt)
}
case "security_barrier", "security_invoker":
ctx.WriteString(opt.Name)
if opt.Security {
ctx.WriteString(" = TRUE")
} else {
ctx.WriteString(" = FALSE")
}
}
}
ctx.WriteString(" )")
Expand Down
79 changes: 76 additions & 3 deletions server/ast/create_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package ast

import (
"fmt"
"strings"

vitess "github.com/dolthub/vitess/go/vt/sqlparser"

Expand All @@ -27,7 +28,79 @@ func nodeCreateView(node *tree.CreateView) (*vitess.DDL, error) {
if node == nil {
return nil, nil
}
//TODO: AFAICT, CREATE VIEW uses a substring of the original string, which won't necessarily work for us.
// Although it takes a parsed definition, this definition isn't sent to integrators.
return nil, fmt.Errorf("CREATE VIEW is not yet supported")
if node.Persistence.IsTemporary() {
return nil, fmt.Errorf("CREATE TEMPORARY VIEW is not yet supported")
}
if node.IsRecursive {
return nil, fmt.Errorf("CREATE RECURSIVE VIEW is not yet supported")
}
var checkOption = tree.ViewCheckOptionUnspecified
var sqlSecurity string
if node.Options != nil {
for _, opt := range node.Options {
switch strings.ToLower(opt.Name) {
case "check_option":
switch strings.ToLower(opt.CheckOpt) {
case "local":
checkOption = tree.ViewCheckOptionLocal
case "cascaded":
checkOption = tree.ViewCheckOptionCascaded
default:
return nil, fmt.Errorf(`"ERROR: syntax error at or near "%s"`, opt.Name)
}
case "security_barrier":
return nil, fmt.Errorf("CREATE VIEW '%s' option is not yet supported", opt.Name)
case "security_invoker":
if opt.Security {
sqlSecurity = "invoker"
} else {
sqlSecurity = "definer"
}
default:
return nil, fmt.Errorf(`"ERROR: syntax error at or near "%s"`, opt.Name)
}
}
}

if checkOption != tree.ViewCheckOptionUnspecified && node.CheckOption != tree.ViewCheckOptionUnspecified {
return nil, fmt.Errorf(`ERROR: parameter "check_option" specified more than once`)
} else {
checkOption = node.CheckOption
}

vCheckOpt := vitess.ViewCheckOptionUnspecified
switch checkOption {
case tree.ViewCheckOptionCascaded:
vCheckOpt = vitess.ViewCheckOptionCascaded
case tree.ViewCheckOptionLocal:
vCheckOpt = vitess.ViewCheckOptionLocal
default:
}

tableName, err := nodeTableName(&node.Name)
if err != nil {
return nil, err
}
selectStmt, err := nodeSelectStatement(node.AsSource.Select)
if err != nil {
return nil, err
}
var cols = make(vitess.Columns, len(node.ColumnNames))
for i, col := range node.ColumnNames {
cols[i] = vitess.NewColIdent(col.String())
}

stmt := &vitess.DDL{
Action: vitess.CreateStr,
OrReplace: node.Replace,
ViewSpec: &vitess.ViewSpec{
ViewName: tableName,
ViewExpr: selectStmt,
Columns: cols,
Security: sqlSecurity,
CheckOption: vCheckOpt,
},
SubStatementStr: node.AsSource.Select.String(),
}
return stmt, nil
}
82 changes: 82 additions & 0 deletions server/ast/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2024 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 ast

import (
"fmt"

"github.com/dolthub/go-mysql-server/sql"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"

"github.com/dolthub/doltgresql/postgres/parser/parser"
)

var _ sql.Parser = &PostgresParser{}

// PostgresParser is a postgres syntax parser.
// This parser is used as parser in the engine for Doltgres.
type PostgresParser struct{}

// NewPostgresParser creates new PostgresParser.
func NewPostgresParser() *PostgresParser { return &PostgresParser{} }

// ParseSimple implements sql.Parser interface.
func (p *PostgresParser) ParseSimple(query string) (vitess.Statement, error) {
stmt, _, _, err := p.ParseWithOptions(query, ';', false, vitess.ParserOptions{})
return stmt, err
}

// Parse implements sql.Parser interface.
func (p *PostgresParser) Parse(_ *sql.Context, query string, multi bool) (vitess.Statement, string, string, error) {
return p.ParseWithOptions(query, ';', multi, vitess.ParserOptions{})
}

// ParseWithOptions implements sql.Parser interface.
func (p *PostgresParser) ParseWithOptions(query string, delimiter rune, _ bool, _ vitess.ParserOptions) (vitess.Statement, string, string, error) {
q := sql.RemoveSpaceAndDelimiter(query, delimiter)
stmts, err := parser.Parse(q)
if err != nil {
return nil, "", "", err
}
if len(stmts) > 1 {
return nil, "", "", fmt.Errorf("only a single statement at a time is currently supported")
}
if len(stmts) == 0 {
return nil, q, "", nil
}

vitessAST, err := Convert(stmts[0])
if err != nil {
return nil, "", "", err
}
if vitessAST == nil {
q = stmts[0].AST.String()
}

return vitessAST, q, "", nil
}

// ParseOneWithOptions implements sql.Parser interface.
func (p *PostgresParser) ParseOneWithOptions(query string, _ vitess.ParserOptions) (vitess.Statement, int, error) {
stmt, err := parser.ParseOne(query)
if err != nil {
return nil, 0, err
}
vitessAST, err := Convert(stmt)
if err != nil {
return nil, 0, err
}
return vitessAST, 0, nil
}
4 changes: 4 additions & 0 deletions server/ast/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ func nodeSelectExpr(node tree.SelectExpr) (vitess.SelectExpr, error) {
if err != nil {
return nil, err
}
// cast part is not part of column name, e.g. `id::INT2` should create column name as `id`.
if ce, ok := expr.(*tree.CastExpr); ok && node.As == "" {
node.As = tree.UnrestrictedName(tree.AsString(ce.Expr))
}
return &vitess.AliasedExpr{
Expr: vitessExpr,
As: vitess.NewColIdent(string(node.As)),
Expand Down
5 changes: 5 additions & 0 deletions server/ast/set_var.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func nodeSetVar(node *tree.SetVar) (vitess.Statement, error) {
if node == nil {
return nil, nil
}
// USE statement alias
if node.Name == "database" {
dbName := strings.TrimPrefix(strings.TrimSuffix(node.Values[0].String(), "'"), "'")
return &vitess.Use{DBName: vitess.NewTableIdent(dbName)}, nil
}
if !config.IsValidPostgresConfigParameter(node.Name) {
return nil, fmt.Errorf(`ERROR: syntax error at or near "%s"'`, node.Name)
}
Expand Down
10 changes: 5 additions & 5 deletions server/connection_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ func (h *ConnectionHandler) handleBind(message messages.Bind) error {
return err
}

boundPlan, fields, err := h.bindParams(message.SourcePreparedStatement, preparedData.Query.AST, bindVars)
boundPlan, fields, err := h.bindParams(preparedData.Query.String, preparedData.Query.AST, bindVars)
if err != nil {
return err
}
Expand Down Expand Up @@ -772,20 +772,20 @@ func (h *ConnectionHandler) handledPSQLCommands(statement string) (bool, error)
}
// Command: \dn
if statement == "select n.nspname as \"name\",\n pg_catalog.pg_get_userbyid(n.nspowner) as \"owner\"\nfrom pg_catalog.pg_namespace n\nwhere n.nspname !~ '^pg_' and n.nspname <> 'information_schema'\norder by 1;" {
return true, h.query(ConvertedQuery{String: "SELECT 'public' AS 'Name', 'pg_database_owner' AS 'Owner';"})
return true, h.query(ConvertedQuery{String: `SELECT 'public' AS 'Name', 'pg_database_owner' AS 'Owner';`})
}
// Command: \df
if statement == "select n.nspname as \"schema\",\n p.proname as \"name\",\n pg_catalog.pg_get_function_result(p.oid) as \"result data type\",\n pg_catalog.pg_get_function_arguments(p.oid) as \"argument data types\",\n case p.prokind\n when 'a' then 'agg'\n when 'w' then 'window'\n when 'p' then 'proc'\n else 'func'\n end as \"type\"\nfrom pg_catalog.pg_proc p\n left join pg_catalog.pg_namespace n on n.oid = p.pronamespace\nwhere pg_catalog.pg_function_is_visible(p.oid)\n and n.nspname <> 'pg_catalog'\n and n.nspname <> 'information_schema'\norder by 1, 2, 4;" {
return true, h.query(ConvertedQuery{String: "SELECT '' AS 'Schema', '' AS 'Name', '' AS 'Result data type', '' AS 'Argument data types', '' AS 'Type' FROM dual LIMIT 0;"})
return true, h.query(ConvertedQuery{String: `SELECT '' AS 'Schema', '' AS 'Name', '' AS 'Result data type', '' AS 'Argument data types', '' AS 'Type' FROM dual LIMIT 0;`})
}
// Command: \dv
if statement == "select n.nspname as \"schema\",\n c.relname as \"name\",\n case c.relkind when 'r' then 'table' when 'v' then 'view' when 'm' then 'materialized view' when 'i' then 'index' when 's' then 'sequence' when 't' then 'toast table' when 'f' then 'foreign table' when 'p' then 'partitioned table' when 'i' then 'partitioned index' end as \"type\",\n pg_catalog.pg_get_userbyid(c.relowner) as \"owner\"\nfrom pg_catalog.pg_class c\n left join pg_catalog.pg_namespace n on n.oid = c.relnamespace\nwhere c.relkind in ('v','')\n and n.nspname <> 'pg_catalog'\n and n.nspname !~ '^pg_toast'\n and n.nspname <> 'information_schema'\n and pg_catalog.pg_table_is_visible(c.oid)\norder by 1,2;" {
return true, h.query(ConvertedQuery{String: "SELECT 'public' AS 'Schema', TABLE_NAME AS 'Name', 'view' AS 'Type', 'postgres' AS 'Owner' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = database() AND TABLE_TYPE = 'VIEW' ORDER BY 2;"})
return true, h.query(ConvertedQuery{String: `SELECT 'public' AS 'Schema', TABLE_NAME AS 'Name', 'view' AS 'Type', 'postgres' AS 'Owner' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = database() AND TABLE_TYPE = 'VIEW' ORDER BY 2;`})
}
// Command: \du
if statement == "select r.rolname, r.rolsuper, r.rolinherit,\n r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\n r.rolconnlimit, r.rolvaliduntil,\n array(select b.rolname\n from pg_catalog.pg_auth_members m\n join pg_catalog.pg_roles b on (m.roleid = b.oid)\n where m.member = r.oid) as memberof\n, r.rolreplication\n, r.rolbypassrls\nfrom pg_catalog.pg_roles r\nwhere r.rolname !~ '^pg_'\norder by 1;" {
// We don't support users yet, so we'll just return nothing for now
return true, h.query(ConvertedQuery{String: "SELECT '' FROM dual LIMIT 0;"})
return true, h.query(ConvertedQuery{String: `SELECT '' FROM dual LIMIT 0;`})
}
return false, nil
}
Expand Down
Loading

0 comments on commit 8639d45

Please sign in to comment.