Skip to content

Commit 8afcfbf

Browse files
authored
Merge pull request #56 from PostHog/fix/strip-on-conflict-ducklake
Strip ON CONFLICT clause in DuckLake mode
2 parents c0fff9f + a237389 commit 8afcfbf

File tree

3 files changed

+60
-4
lines changed

3 files changed

+60
-4
lines changed

transpiler/transform/onconflict.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,21 @@ import (
1313
// Note: DuckDB's ON CONFLICT support has evolved. As of DuckDB 0.9+,
1414
// it supports ON CONFLICT DO NOTHING and ON CONFLICT DO UPDATE.
1515
// This transform handles cases where the syntax might differ.
16-
type OnConflictTransform struct{}
16+
//
17+
// In DuckLake mode, PRIMARY KEY and UNIQUE constraints are stripped,
18+
// so ON CONFLICT clauses will fail with "columns not referenced by constraint".
19+
// We strip ON CONFLICT entirely in DuckLake mode since there are no constraints
20+
// to conflict with.
21+
type OnConflictTransform struct {
22+
DuckLakeMode bool
23+
}
1724

1825
func NewOnConflictTransform() *OnConflictTransform {
19-
return &OnConflictTransform{}
26+
return &OnConflictTransform{DuckLakeMode: false}
27+
}
28+
29+
func NewOnConflictTransformWithConfig(duckLakeMode bool) *OnConflictTransform {
30+
return &OnConflictTransform{DuckLakeMode: duckLakeMode}
2031
}
2132

2233
func (t *OnConflictTransform) Name() string {
@@ -46,6 +57,15 @@ func (t *OnConflictTransform) transformInsert(insert *pg_query.InsertStmt) bool
4657
return false
4758
}
4859

60+
// In DuckLake mode, we strip PRIMARY KEY and UNIQUE constraints,
61+
// so ON CONFLICT clauses will fail with "columns not referenced by constraint".
62+
// Strip the ON CONFLICT clause entirely since there are no constraints to conflict with.
63+
// The data will be inserted normally. Fivetran's DELETE + INSERT pattern handles updates.
64+
if t.DuckLakeMode {
65+
insert.OnConflictClause = nil
66+
return true
67+
}
68+
4969
// DuckDB now supports ON CONFLICT syntax similar to PostgreSQL
5070
// However, there are some differences:
5171
//

transpiler/transpiler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ func New(cfg Config) *Transpiler {
4848
// 8. SET/SHOW command handling
4949
t.transforms = append(t.transforms, transform.NewSetShowTransform())
5050

51-
// 9. ON CONFLICT handling
52-
t.transforms = append(t.transforms, transform.NewOnConflictTransform())
51+
// 9. ON CONFLICT handling (strips ON CONFLICT in DuckLake mode since constraints don't exist)
52+
t.transforms = append(t.transforms, transform.NewOnConflictTransformWithConfig(cfg.DuckLakeMode))
5353

5454
// DDL transforms only when DuckLake mode is enabled
5555
if cfg.DuckLakeMode {

transpiler/transpiler_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,42 @@ func TestTranspile_OnConflict(t *testing.T) {
740740
}
741741
}
742742

743+
func TestTranspile_OnConflict_DuckLakeMode(t *testing.T) {
744+
// In DuckLake mode, ON CONFLICT is stripped because PRIMARY KEY/UNIQUE constraints don't exist
745+
tests := []struct {
746+
name string
747+
input string
748+
}{
749+
{
750+
name: "ON CONFLICT DO NOTHING stripped",
751+
input: "INSERT INTO users (id, name) VALUES (1, 'test') ON CONFLICT (id) DO NOTHING",
752+
},
753+
{
754+
name: "ON CONFLICT DO UPDATE stripped",
755+
input: "INSERT INTO users (id, name) VALUES (1, 'test') ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name",
756+
},
757+
}
758+
759+
tr := New(Config{DuckLakeMode: true})
760+
761+
for _, tt := range tests {
762+
t.Run(tt.name, func(t *testing.T) {
763+
result, err := tr.Transpile(tt.input)
764+
if err != nil {
765+
t.Fatalf("Transpile(%q) error: %v", tt.input, err)
766+
}
767+
// ON CONFLICT should be stripped in DuckLake mode
768+
if strings.Contains(strings.ToUpper(result.SQL), "ON CONFLICT") {
769+
t.Errorf("Transpile(%q) = %q, should NOT contain ON CONFLICT in DuckLake mode", tt.input, result.SQL)
770+
}
771+
// But the INSERT should still work
772+
if !strings.Contains(strings.ToUpper(result.SQL), "INSERT INTO") {
773+
t.Errorf("Transpile(%q) = %q, should still contain INSERT INTO", tt.input, result.SQL)
774+
}
775+
})
776+
}
777+
}
778+
743779
func TestTranspile_JSONOperators(t *testing.T) {
744780
// DuckDB supports -> and ->> operators
745781
tests := []struct {

0 commit comments

Comments
 (0)