@@ -142,6 +142,29 @@ def values(self, *values: Any, **kwargs: Any) -> "Self":
142142 for i , value in enumerate (values ):
143143 if isinstance (value , exp .Expression ):
144144 value_placeholders .append (value )
145+ elif hasattr (value , "expression" ) and hasattr (value , "sql" ):
146+ # Handle SQL objects (from sql.raw with parameters)
147+ expression = getattr (value , "expression" , None )
148+ if expression is not None and isinstance (expression , exp .Expression ):
149+ # Merge parameters from SQL object into builder
150+ if hasattr (value , "parameters" ):
151+ sql_parameters = getattr (value , "parameters" , {})
152+ for param_name , param_value in sql_parameters .items ():
153+ self .add_parameter (param_value , name = param_name )
154+ value_placeholders .append (expression )
155+ else :
156+ # If expression is None, fall back to parsing the raw SQL
157+ sql_text = getattr (value , "sql" , "" )
158+ # Merge parameters even when parsing raw SQL
159+ if hasattr (value , "parameters" ):
160+ sql_parameters = getattr (value , "parameters" , {})
161+ for param_name , param_value in sql_parameters .items ():
162+ self .add_parameter (param_value , name = param_name )
163+ # Check if sql_text is callable (like Expression.sql method)
164+ if callable (sql_text ):
165+ sql_text = str (value )
166+ value_expr = exp .maybe_parse (sql_text ) or exp .convert (str (sql_text ))
167+ value_placeholders .append (value_expr )
145168 else :
146169 if self ._columns and i < len (self ._columns ):
147170 column_str = str (self ._columns [i ])
@@ -228,29 +251,171 @@ def values_from_dicts(self, data: "Sequence[Mapping[str, Any]]") -> "Self":
228251
229252 return self
230253
231- def on_conflict_do_nothing (self ) -> "Self" :
232- """Adds an ON CONFLICT DO NOTHING clause (PostgreSQL syntax).
254+ def on_conflict (self , * columns : str ) -> "ConflictBuilder" :
255+ """Adds an ON CONFLICT clause with specified columns.
256+
257+ Args:
258+ *columns: Column names that define the conflict. If no columns provided,
259+ creates an ON CONFLICT without specific columns (catches all conflicts).
260+
261+ Returns:
262+ A ConflictBuilder instance for chaining conflict resolution methods.
263+
264+ Example:
265+ ```python
266+ # ON CONFLICT (id) DO NOTHING
267+ sql.insert("users").values(id=1, name="John").on_conflict(
268+ "id"
269+ ).do_nothing()
270+
271+ # ON CONFLICT (email, username) DO UPDATE SET updated_at = NOW()
272+ sql.insert("users").values(...).on_conflict(
273+ "email", "username"
274+ ).do_update(updated_at=sql.raw("NOW()"))
275+
276+ # ON CONFLICT DO NOTHING (catches all conflicts)
277+ sql.insert("users").values(...).on_conflict().do_nothing()
278+ ```
279+ """
280+ return ConflictBuilder (self , columns )
281+
282+ def on_conflict_do_nothing (self , * columns : str ) -> "Insert" :
283+ """Adds an ON CONFLICT DO NOTHING clause (convenience method).
233284
234- This is used to ignore rows that would cause a conflict.
285+ Args:
286+ *columns: Column names that define the conflict. If no columns provided,
287+ creates an ON CONFLICT without specific columns.
235288
236289 Returns:
237290 The current builder instance for method chaining.
238291
239292 Note:
240- This is PostgreSQL-specific syntax. Different databases have different syntax.
241- For a more general solution, you might need dialect-specific handling.
293+ This is a convenience method. For more control, use on_conflict().do_nothing().
242294 """
243- insert_expr = self ._get_insert_expression ()
244- insert_expr .set ("on" , exp .OnConflict (this = None , expressions = []))
245- return self
295+ return self .on_conflict (* columns ).do_nothing ()
246296
247- def on_duplicate_key_update (self , ** _ : Any ) -> "Self " :
248- """Adds an ON DUPLICATE KEY UPDATE clause (MySQL syntax ).
297+ def on_duplicate_key_update (self , ** kwargs : Any ) -> "Insert " :
298+ """Adds conflict resolution using the ON CONFLICT syntax (cross-database compatible ).
249299
250300 Args:
251- **_ : Column-value pairs to update on duplicate key .
301+ **kwargs : Column-value pairs to update on conflict .
252302
253303 Returns:
254304 The current builder instance for method chaining.
305+
306+ Note:
307+ This method uses PostgreSQL-style ON CONFLICT syntax but SQLGlot will
308+ transpile it to the appropriate syntax for each database (MySQL's
309+ ON DUPLICATE KEY UPDATE, etc.).
255310 """
256- return self
311+ if not kwargs :
312+ return self
313+ return self .on_conflict ().do_update (** kwargs )
314+
315+
316+ class ConflictBuilder :
317+ """Builder for ON CONFLICT clauses in INSERT statements.
318+
319+ This builder provides a fluent interface for constructing conflict resolution
320+ clauses using PostgreSQL-style syntax, which SQLGlot can transpile to other dialects.
321+ """
322+
323+ __slots__ = ("_columns" , "_insert_builder" )
324+
325+ def __init__ (self , insert_builder : "Insert" , columns : tuple [str , ...]) -> None :
326+ """Initialize ConflictBuilder.
327+
328+ Args:
329+ insert_builder: The parent Insert builder
330+ columns: Column names that define the conflict
331+ """
332+ self ._insert_builder = insert_builder
333+ self ._columns = columns
334+
335+ def do_nothing (self ) -> "Insert" :
336+ """Add DO NOTHING conflict resolution.
337+
338+ Returns:
339+ The parent Insert builder for method chaining.
340+
341+ Example:
342+ ```python
343+ sql.insert("users").values(id=1, name="John").on_conflict(
344+ "id"
345+ ).do_nothing()
346+ ```
347+ """
348+ insert_expr = self ._insert_builder ._get_insert_expression ()
349+
350+ # Create ON CONFLICT with proper structure
351+ conflict_keys = [exp .to_identifier (col ) for col in self ._columns ] if self ._columns else None
352+ on_conflict = exp .OnConflict (conflict_keys = conflict_keys , action = exp .var ("DO NOTHING" ))
353+
354+ insert_expr .set ("conflict" , on_conflict )
355+ return self ._insert_builder
356+
357+ def do_update (self , ** kwargs : Any ) -> "Insert" :
358+ """Add DO UPDATE conflict resolution with SET clauses.
359+
360+ Args:
361+ **kwargs: Column-value pairs to update on conflict.
362+
363+ Returns:
364+ The parent Insert builder for method chaining.
365+
366+ Example:
367+ ```python
368+ sql.insert("users").values(id=1, name="John").on_conflict(
369+ "id"
370+ ).do_update(
371+ name="Updated Name", updated_at=sql.raw("NOW()")
372+ )
373+ ```
374+ """
375+ insert_expr = self ._insert_builder ._get_insert_expression ()
376+
377+ # Create SET expressions for the UPDATE
378+ set_expressions = []
379+ for col , val in kwargs .items ():
380+ if hasattr (val , "expression" ) and hasattr (val , "sql" ):
381+ # Handle SQL objects (from sql.raw with parameters)
382+ expression = getattr (val , "expression" , None )
383+ if expression is not None and isinstance (expression , exp .Expression ):
384+ # Merge parameters from SQL object into builder
385+ if hasattr (val , "parameters" ):
386+ sql_parameters = getattr (val , "parameters" , {})
387+ for param_name , param_value in sql_parameters .items ():
388+ self ._insert_builder .add_parameter (param_value , name = param_name )
389+ value_expr = expression
390+ else :
391+ # If expression is None, fall back to parsing the raw SQL
392+ sql_text = getattr (val , "sql" , "" )
393+ # Merge parameters even when parsing raw SQL
394+ if hasattr (val , "parameters" ):
395+ sql_parameters = getattr (val , "parameters" , {})
396+ for param_name , param_value in sql_parameters .items ():
397+ self ._insert_builder .add_parameter (param_value , name = param_name )
398+ # Check if sql_text is callable (like Expression.sql method)
399+ if callable (sql_text ):
400+ sql_text = str (val )
401+ value_expr = exp .maybe_parse (sql_text ) or exp .convert (str (sql_text ))
402+ elif isinstance (val , exp .Expression ):
403+ value_expr = val
404+ else :
405+ # Create parameter for regular values
406+ param_name = self ._insert_builder ._generate_unique_parameter_name (col )
407+ _ , param_name = self ._insert_builder .add_parameter (val , name = param_name )
408+ value_expr = exp .Placeholder (this = param_name )
409+
410+ set_expressions .append (exp .EQ (this = exp .column (col ), expression = value_expr ))
411+
412+ # Create ON CONFLICT with proper structure
413+ conflict_keys = [exp .to_identifier (col ) for col in self ._columns ] if self ._columns else None
414+ on_conflict = exp .OnConflict (
415+ conflict_keys = conflict_keys ,
416+ action = exp .var ("DO UPDATE" ),
417+ expressions = set_expressions if set_expressions else None ,
418+ )
419+
420+ insert_expr .set ("conflict" , on_conflict )
421+ return self ._insert_builder
0 commit comments