diff --git a/builder.go b/builder.go
index 20a39e2..561bb7e 100644
--- a/builder.go
+++ b/builder.go
@@ -21,7 +21,11 @@ func (s *StatementBuilder) InsertRecord(record interface{}, optTableName ...stri
return InsertBuilder{InsertBuilder: insert, err: wrapErr(err)}
}
if len(cols) == 0 {
- return InsertBuilder{InsertBuilder: insert, err: wrapErr(fmt.Errorf("Map returned no columns for %T; for an all-default INSERT use sq.Expr(\"INSERT INTO %s DEFAULT VALUES\")", record, tableName))}
+ hint := `SQL.InsertDefaults("
")`
+ if tableName != "" {
+ hint = fmt.Sprintf("SQL.InsertDefaults(%q)", tableName)
+ }
+ return InsertBuilder{InsertBuilder: insert, err: wrapErr(fmt.Errorf("Map returned no columns for %T; for an all-default INSERT use %s", record, hint))}
}
return InsertBuilder{InsertBuilder: insert.Into(tableName).Columns(cols...).Values(vals...)}
@@ -64,7 +68,7 @@ func (s StatementBuilder) InsertRecords(recordsSlice interface{}, optTableName .
return InsertBuilder{InsertBuilder: insert, err: wrapErr(err)}
}
if len(cols) == 0 {
- return InsertBuilder{InsertBuilder: insert, err: wrapErr(fmt.Errorf("Map returned no columns for record %d (%T); for an all-default INSERT use sq.Expr", i, record))}
+ return InsertBuilder{InsertBuilder: insert, err: wrapErr(fmt.Errorf("Map returned no columns for record %d (%T); for an all-default INSERT use SQL.InsertDefaults (single-row only)", i, record))}
}
if i == 0 {
@@ -84,6 +88,44 @@ func (s StatementBuilder) InsertRecords(recordsSlice interface{}, optTableName .
return InsertBuilder{InsertBuilder: insert.Into(tableName)}
}
+// InsertDefaults builds INSERT INTO DEFAULT VALUES; table must be non-empty.
+func (s StatementBuilder) InsertDefaults(table string) DefaultValuesBuilder {
+ if table == "" {
+ // raw error; Querier.wrapErr applies the pgkit: prefix at use time.
+ return DefaultValuesBuilder{err: fmt.Errorf("insert statements must specify a table")}
+ }
+ return DefaultValuesBuilder{table: table}
+}
+
+// DefaultValuesBuilder is the sq.Sqlizer returned by InsertDefaults.
+type DefaultValuesBuilder struct {
+ table string
+ suffix string
+ err error
+}
+
+func (b DefaultValuesBuilder) ToSql() (string, []any, error) {
+ if b.err != nil {
+ return "", nil, b.err
+ }
+ // Defensive: a zero-value DefaultValuesBuilder bypasses the InsertDefaults
+ // constructor's table check, so re-validate here.
+ if b.table == "" {
+ return "", nil, fmt.Errorf("insert statements must specify a table")
+ }
+ return "INSERT INTO " + b.table + " DEFAULT VALUES" + b.suffix, nil, nil
+}
+
+// Suffix appends literal SQL; no placeholder rewriting, use sq.Expr for that.
+func (b DefaultValuesBuilder) Suffix(sql string) DefaultValuesBuilder {
+ if b.err != nil || sql == "" {
+ return b
+ }
+ return DefaultValuesBuilder{table: b.table, suffix: b.suffix + " " + sql}
+}
+
+func (b DefaultValuesBuilder) Err() error { return b.err }
+
func (s StatementBuilder) UpdateRecord(record interface{}, whereExpr sq.Eq, optTableName ...string) UpdateBuilder {
return s.UpdateRecordColumns(record, whereExpr, nil, optTableName...)
}
diff --git a/builder_test.go b/builder_test.go
index d72df6e..0b5de8b 100644
--- a/builder_test.go
+++ b/builder_test.go
@@ -45,22 +45,90 @@ func TestInsertRecords_UniformShape(t *testing.T) {
require.NoError(t, b.Err())
}
-func TestInsertRecord_EmptyColumnsRejected(t *testing.T) {
- // All fields tagged ,omitzero (or ,omitempty) and all zero leaves
- // Map with no columns. Squirrel would emit invalid INSERT INTO t
- // VALUES (); fail fast at build time and point at sq.Expr as the
- // escape for the all-default INSERT case. Tracked in goware/pgkit#51.
+func TestInsertDefaults_PlainSQL(t *testing.T) {
+ sb := &pgkit.StatementBuilder{StatementBuilderType: sq.StatementBuilder.PlaceholderFormat(sq.Dollar)}
+ b := sb.InsertDefaults("items")
+ require.NoError(t, b.Err())
+ sql, args, err := b.ToSql()
+ require.NoError(t, err)
+ assert.Equal(t, "INSERT INTO items DEFAULT VALUES", sql)
+ assert.Empty(t, args)
+}
+
+func TestInsertDefaults_WithReturning(t *testing.T) {
+ sb := &pgkit.StatementBuilder{StatementBuilderType: sq.StatementBuilder.PlaceholderFormat(sq.Dollar)}
+ b := sb.InsertDefaults("items").Suffix(`RETURNING "id"`)
+ require.NoError(t, b.Err())
+ sql, _, err := b.ToSql()
+ require.NoError(t, err)
+ assert.Equal(t, `INSERT INTO items DEFAULT VALUES RETURNING "id"`, sql)
+}
+
+func TestInsertDefaults_MultipleSuffix(t *testing.T) {
+ sb := &pgkit.StatementBuilder{StatementBuilderType: sq.StatementBuilder.PlaceholderFormat(sq.Dollar)}
+ b := sb.InsertDefaults("items").
+ Suffix("ON CONFLICT (id) DO NOTHING").
+ Suffix(`RETURNING "id"`)
+ sql, _, err := b.ToSql()
+ require.NoError(t, err)
+ assert.Equal(t, `INSERT INTO items DEFAULT VALUES ON CONFLICT (id) DO NOTHING RETURNING "id"`, sql)
+}
+
+func TestInsertDefaults_OnConflictDoUpdateExcluded(t *testing.T) {
+ // EXCLUDED-based upsert is the realistic conflict shape: literal SQL,
+ // no placeholders. Proves Suffix covers the common case without sq.Expr.
+ sb := &pgkit.StatementBuilder{StatementBuilderType: sq.StatementBuilder.PlaceholderFormat(sq.Dollar)}
+ b := sb.InsertDefaults("items").Suffix("ON CONFLICT (id) DO UPDATE SET updated_at = EXCLUDED.updated_at RETURNING id")
+ sql, _, err := b.ToSql()
+ require.NoError(t, err)
+ assert.Equal(t, "INSERT INTO items DEFAULT VALUES ON CONFLICT (id) DO UPDATE SET updated_at = EXCLUDED.updated_at RETURNING id", sql)
+}
+
+func TestInsertDefaults_EmptyTableErrors(t *testing.T) {
+ // Build-time failure (not exec-time) + raw error (Querier owns the
+ // pgkit: prefix; double-wrapping would surface "pgkit: pgkit: ...").
+ sb := &pgkit.StatementBuilder{StatementBuilderType: sq.StatementBuilder.PlaceholderFormat(sq.Dollar)}
+ b := sb.InsertDefaults("")
+ require.Error(t, b.Err())
+ assert.Contains(t, b.Err().Error(), "table")
+ _, _, err := b.ToSql()
+ require.Error(t, err)
+ assert.NotContains(t, err.Error(), "pgkit: pgkit:")
+}
+
+func TestInsertDefaults_ZeroValueErrors(t *testing.T) {
+ // Direct zero-value construction bypasses the InsertDefaults factory's
+ // table check; ToSql must still error rather than emit invalid SQL.
+ var b pgkit.DefaultValuesBuilder
+ _, _, err := b.ToSql()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "table")
+}
+
+func TestInsertDefaults_EmptySuffixIsNoop(t *testing.T) {
+ // Conditional-suffix callers (b.Suffix(returningClause) with returningClause
+ // occasionally "") shouldn't get a trailing space that breaks SQL-string
+ // snapshot tests.
+ sb := &pgkit.StatementBuilder{StatementBuilderType: sq.StatementBuilder.PlaceholderFormat(sq.Dollar)}
+ b := sb.InsertDefaults("items").Suffix("")
+ sql, _, err := b.ToSql()
+ require.NoError(t, err)
+ assert.Equal(t, "INSERT INTO items DEFAULT VALUES", sql)
+}
+
+func TestInsertRecord_AllDefaultsErrorHintsAtInsertDefaults(t *testing.T) {
type Item struct {
Tags []string `db:"tags,omitzero"`
}
sb := &pgkit.StatementBuilder{StatementBuilderType: sq.StatementBuilder.PlaceholderFormat(sq.Dollar)}
b := sb.InsertRecord(&Item{}, "items")
require.Error(t, b.Err())
- assert.Contains(t, b.Err().Error(), "no columns")
- assert.Contains(t, b.Err().Error(), "sq.Expr")
+ assert.Contains(t, b.Err().Error(), `SQL.InsertDefaults("items")`)
}
func TestInsertRecords_EmptyColumnsRejected(t *testing.T) {
+ // Multi-row INSERT ... DEFAULT VALUES is not valid PG; the batch path
+ // rejects all-default records and points at the single-row InsertRecord.
type Item struct {
Tags []string `db:"tags,omitzero"`
}
diff --git a/tests/pgkit_test.go b/tests/pgkit_test.go
index c6c8530..517b646 100644
--- a/tests/pgkit_test.go
+++ b/tests/pgkit_test.go
@@ -1165,3 +1165,22 @@ func connectToDb(conf pgkit.Config) (*pgkit.DB, error) {
}
return dbClient, err
}
+
+func TestInsertDefaultsRoundTrip(t *testing.T) {
+ // Hits PG (not just SQL string snapshots) to catch syntax issues
+ // the builder might emit but squirrel can't validate.
+ truncateTable(t, "default_only")
+
+ var id int64
+ err := DB.Query.QueryRow(
+ context.Background(),
+ DB.SQL.InsertDefaults("default_only").Suffix(`RETURNING "id"`),
+ ).Scan(&id)
+ require.NoError(t, err)
+ require.NotZero(t, id)
+
+ rows, err := DB.Conn.Query(context.Background(), "SELECT id FROM default_only WHERE id = $1", id)
+ require.NoError(t, err)
+ defer rows.Close()
+ require.True(t, rows.Next(), "row should exist")
+}
diff --git a/tests/testdata/pgkit_test_db.sql b/tests/testdata/pgkit_test_db.sql
index 406d655..a4cba0c 100644
--- a/tests/testdata/pgkit_test_db.sql
+++ b/tests/testdata/pgkit_test_db.sql
@@ -44,3 +44,9 @@ CREATE TABLE stats (
big_num NUMERIC(78,0) NOT NULL, -- representing a big.Int runtime type
rating NUMERIC(78,0) NULL -- representing a nullable big.Int runtime type
);
+
+-- every column DB-defaulted, so DEFAULT VALUES is the only insert path.
+CREATE TABLE default_only (
+ id BIGSERIAL PRIMARY KEY,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);