Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
91 changes: 91 additions & 0 deletions writer/sql_insert_placeholder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package writer

import (
"bytes"
"errors"
"testing"

"cloud.google.com/go/spanner"
databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
"github.com/apstndb/spanvalue/gcvctor"
"github.com/google/go-cmp/cmp"
)

func TestSQLInsertWriterWithTablePlaceholder_scaffold(t *testing.T) {
t.Parallel()

var out bytes.Buffer
w := mustNewSQLInsertWriter(t, &out, "",
WithSQLTablePlaceholder("TABLE_NAME"),
)
if err := w.WriteValues([]string{"id"}, []spanner.GenericColumnValue{gcvctor.Int64Value(1)}); err != nil {
t.Fatalf("WriteValues() error = %v", err)
}

want := "INSERT INTO TABLE_NAME (`id`) VALUES (1);\n"
if diff := cmp.Diff(want, out.String()); diff != "" {
t.Fatalf("SQL output mismatch (-want +got):\n%s", diff)
}
}

func TestSQLInsertWriterWithTablePlaceholder_postgresColumnsQuoted(t *testing.T) {
t.Parallel()

var out bytes.Buffer
w := mustNewSQLInsertWriter(t, &out, "",
WithSQLTablePlaceholder("/* unresolved table */"),
WithSQLDialect(databasepb.DatabaseDialect_POSTGRESQL),
)
if err := w.WriteValues([]string{"id"}, []spanner.GenericColumnValue{gcvctor.Int64Value(42)}); err != nil {
t.Fatalf("WriteValues() error = %v", err)
}

want := "INSERT INTO /* unresolved table */ (\"id\") VALUES (42);\n"
if diff := cmp.Diff(want, out.String()); diff != "" {
t.Fatalf("SQL output mismatch (-want +got):\n%s", diff)
}
}

func TestSQLInsertWriterWithTablePlaceholder_emptyTokenRejected(t *testing.T) {
t.Parallel()

_, err := NewSQLInsertWriter(&bytes.Buffer{}, "users", WithSQLTablePlaceholder(""))
if !errors.Is(err, ErrEmptyTablePlaceholder) {
t.Fatalf("NewSQLInsertWriter() error = %v, want ErrEmptyTablePlaceholder", err)
}
}

func TestSQLInsertWriterEmptyTableWithoutPlaceholder(t *testing.T) {
t.Parallel()
t.Skip("WIP: #147 will reject empty table at construction; placeholder path should remain valid")

var out bytes.Buffer
_, err := NewSQLInsertWriter(&out, "")
if err != nil {
t.Fatalf("NewSQLInsertWriter() error = %v, want nil until #147 lands", err)
}
err = mustNewSQLInsertWriter(t, &out, "").WriteValues([]string{"id"}, []spanner.GenericColumnValue{gcvctor.Int64Value(1)})
if !errors.Is(err, ErrEmptyTableName) {
t.Fatalf("WriteValues() error = %v, want ErrEmptyTableName", err)
}
}

func TestSQLInsertWriterTablePlaceholder_batchedGoldenSketch(t *testing.T) {
t.Parallel()
t.Skip("WIP #146: batched INSERT with placeholders not golden-tested yet")

var out bytes.Buffer
w := mustNewSQLInsertWriter(t, &out, "",
WithSQLTablePlaceholder("TABLE_NAME"),
WithSQLBatchSize(2),
)
for _, id := range []int64{1, 2} {
if err := w.WriteValues([]string{"id"}, []spanner.GenericColumnValue{gcvctor.Int64Value(id)}); err != nil {
t.Fatalf("WriteValues() error = %v", err)
}
}
want := "INSERT INTO TABLE_NAME (`id`) VALUES\n (1),\n (2);\n"
if diff := cmp.Diff(want, out.String()); diff != "" {
t.Fatalf("SQL output mismatch (-want +got):\n%s", diff)
}
}
33 changes: 32 additions & 1 deletion writer/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ var (
// ErrTableNameChangedMidBatch reports that the SQL INSERT table name was mutated while
// a multi-row INSERT batch was open.
ErrTableNameChangedMidBatch = errors.New("table name changed mid-batch")
// ErrEmptyTablePlaceholder reports that [WithSQLTablePlaceholder] received an empty token.
ErrEmptyTablePlaceholder = errors.New("empty table placeholder token")
)

// Writer writes Spanner rows to an output stream.
Expand Down Expand Up @@ -200,6 +202,31 @@ func (o sqlBatchSizeOption) applySQLInsertOption(w *SQLInsertWriter) error {
return nil
}

type sqlTablePlaceholderOption struct {
token string
}

// WithSQLTablePlaceholder sets a verbatim INSERT INTO target token when the real table
// name is unknown at export time (WIP #146).
//
// The token is emitted without dialect identifier quoting. This is intentionally
// distinct from an accidentally empty table name (see #147). When a placeholder is
// configured, [NewSQLInsertWriter]'s table argument may be empty.
//
// Open design questions: delimiter conventions, text/template support, and whether
// placeholders belong on [SQLInsertWriter] or on future INSERT fragment helpers (#79).
func WithSQLTablePlaceholder(token string) SQLInsertOption {
return sqlTablePlaceholderOption{token: token}
}

func (o sqlTablePlaceholderOption) applySQLInsertOption(w *SQLInsertWriter) error {
if o.token == "" {
return ErrEmptyTablePlaceholder
}
w.tablePlaceholder = o.token
return nil
}

// SQLInsertOption configures a SQLInsertWriter created by [NewSQLInsertWriter].
type SQLInsertOption interface {
applySQLInsertOption(*SQLInsertWriter) error
Expand Down Expand Up @@ -968,6 +995,7 @@ type SQLInsertWriter struct {
sqlDialect databasepb.DatabaseDialect
batchSize int
batchPending int
tablePlaceholder string

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When tablePlaceholder is configured, the actual table name used in the generated INSERT statements will be the placeholder, not table. However, TableName() currently still returns w.table (which may be empty). To ensure consistency with its documented contract ('TableName returns the qualified table name used in INSERT statements'), TableName() should be updated to return w.tablePlaceholder if it is set.

schema columnSchema
quotedColumnNames string
quotedTable string
Expand Down Expand Up @@ -1162,7 +1190,7 @@ func (w *SQLInsertWriter) writeGCVs(values []spanner.GenericColumnValue, quotedC
if w.out == nil {
return ErrNilOutputWriter
}
if w.table == "" {
if w.table == "" && w.tablePlaceholder == "" {
return ErrEmptyTableName
}
formattedValues, err := spanvalue.FormatRowColumns(w.insertFormatter(), w.schema.names, values)
Expand Down Expand Up @@ -1283,6 +1311,9 @@ func (w *SQLInsertWriter) initOrValidateQuotedColumns(columnNames []string) (str
}

func (w *SQLInsertWriter) quotedQualifiedTable() (string, error) {
if w.tablePlaceholder != "" {
return w.tablePlaceholder, nil
}
if w.quotedTable != "" && w.quotedTableInput == w.table {
return w.quotedTable, nil
}
Expand Down
Loading