44"""
55
66import logging
7- from typing import Any , Optional , Union
7+ from typing import TYPE_CHECKING , Any , Optional , Union
88
99import sqlglot
1010from sqlglot import exp
1111from sqlglot .dialects .dialect import DialectType
1212from sqlglot .errors import ParseError as SQLGlotParseError
1313
14- from sqlspec .builder import Column , Delete , Insert , Merge , Select , Truncate , Update
14+ from sqlspec .builder import (
15+ AlterTable ,
16+ Column ,
17+ CommentOn ,
18+ CreateIndex ,
19+ CreateMaterializedView ,
20+ CreateSchema ,
21+ CreateTable ,
22+ CreateTableAsSelect ,
23+ CreateView ,
24+ Delete ,
25+ DropIndex ,
26+ DropSchema ,
27+ DropTable ,
28+ DropView ,
29+ Insert ,
30+ Merge ,
31+ RenameTable ,
32+ Select ,
33+ Truncate ,
34+ Update ,
35+ )
1536from sqlspec .exceptions import SQLBuilderError
1637
17- __all__ = ("Case" , "Column" , "Delete" , "Insert" , "Merge" , "SQLFactory" , "Select" , "Truncate" , "Update" , "sql" )
38+ if TYPE_CHECKING :
39+ from sqlspec .core .statement import SQL
40+
41+ __all__ = (
42+ "AlterTable" ,
43+ "Case" ,
44+ "Column" ,
45+ "CommentOn" ,
46+ "CreateIndex" ,
47+ "CreateMaterializedView" ,
48+ "CreateSchema" ,
49+ "CreateTable" ,
50+ "CreateTableAsSelect" ,
51+ "CreateView" ,
52+ "Delete" ,
53+ "DropIndex" ,
54+ "DropSchema" ,
55+ "DropTable" ,
56+ "DropView" ,
57+ "Insert" ,
58+ "Merge" ,
59+ "RenameTable" ,
60+ "SQLFactory" ,
61+ "Select" ,
62+ "Truncate" ,
63+ "Update" ,
64+ "sql" ,
65+ )
1866
1967logger = logging .getLogger ("sqlspec" )
2068
@@ -212,6 +260,174 @@ def merge(self, table_or_sql: Optional[str] = None, dialect: DialectType = None)
212260 return builder .into (table_or_sql )
213261 return builder
214262
263+ # ===================
264+ # DDL Statement Builders
265+ # ===================
266+
267+ def create_table (self , table_name : str , dialect : DialectType = None ) -> "CreateTable" :
268+ """Create a CREATE TABLE builder.
269+
270+ Args:
271+ table_name: Name of the table to create
272+ dialect: Optional SQL dialect
273+
274+ Returns:
275+ CreateTable builder instance
276+ """
277+ builder = CreateTable (table_name )
278+ builder .dialect = dialect or self .dialect
279+ return builder
280+
281+ def create_table_as_select (self , dialect : DialectType = None ) -> "CreateTableAsSelect" :
282+ """Create a CREATE TABLE AS SELECT builder.
283+
284+ Args:
285+ dialect: Optional SQL dialect
286+
287+ Returns:
288+ CreateTableAsSelect builder instance
289+ """
290+ builder = CreateTableAsSelect ()
291+ builder .dialect = dialect or self .dialect
292+ return builder
293+
294+ def create_view (self , dialect : DialectType = None ) -> "CreateView" :
295+ """Create a CREATE VIEW builder.
296+
297+ Args:
298+ dialect: Optional SQL dialect
299+
300+ Returns:
301+ CreateView builder instance
302+ """
303+ builder = CreateView ()
304+ builder .dialect = dialect or self .dialect
305+ return builder
306+
307+ def create_materialized_view (self , dialect : DialectType = None ) -> "CreateMaterializedView" :
308+ """Create a CREATE MATERIALIZED VIEW builder.
309+
310+ Args:
311+ dialect: Optional SQL dialect
312+
313+ Returns:
314+ CreateMaterializedView builder instance
315+ """
316+ builder = CreateMaterializedView ()
317+ builder .dialect = dialect or self .dialect
318+ return builder
319+
320+ def create_index (self , index_name : str , dialect : DialectType = None ) -> "CreateIndex" :
321+ """Create a CREATE INDEX builder.
322+
323+ Args:
324+ index_name: Name of the index to create
325+ dialect: Optional SQL dialect
326+
327+ Returns:
328+ CreateIndex builder instance
329+ """
330+ return CreateIndex (index_name , dialect = dialect or self .dialect )
331+
332+ def create_schema (self , dialect : DialectType = None ) -> "CreateSchema" :
333+ """Create a CREATE SCHEMA builder.
334+
335+ Args:
336+ dialect: Optional SQL dialect
337+
338+ Returns:
339+ CreateSchema builder instance
340+ """
341+ builder = CreateSchema ()
342+ builder .dialect = dialect or self .dialect
343+ return builder
344+
345+ def drop_table (self , table_name : str , dialect : DialectType = None ) -> "DropTable" :
346+ """Create a DROP TABLE builder.
347+
348+ Args:
349+ table_name: Name of the table to drop
350+ dialect: Optional SQL dialect
351+
352+ Returns:
353+ DropTable builder instance
354+ """
355+ return DropTable (table_name , dialect = dialect or self .dialect )
356+
357+ def drop_view (self , dialect : DialectType = None ) -> "DropView" :
358+ """Create a DROP VIEW builder.
359+
360+ Args:
361+ dialect: Optional SQL dialect
362+
363+ Returns:
364+ DropView builder instance
365+ """
366+ return DropView (dialect = dialect or self .dialect )
367+
368+ def drop_index (self , index_name : str , dialect : DialectType = None ) -> "DropIndex" :
369+ """Create a DROP INDEX builder.
370+
371+ Args:
372+ index_name: Name of the index to drop
373+ dialect: Optional SQL dialect
374+
375+ Returns:
376+ DropIndex builder instance
377+ """
378+ return DropIndex (index_name , dialect = dialect or self .dialect )
379+
380+ def drop_schema (self , dialect : DialectType = None ) -> "DropSchema" :
381+ """Create a DROP SCHEMA builder.
382+
383+ Args:
384+ dialect: Optional SQL dialect
385+
386+ Returns:
387+ DropSchema builder instance
388+ """
389+ return DropSchema (dialect = dialect or self .dialect )
390+
391+ def alter_table (self , table_name : str , dialect : DialectType = None ) -> "AlterTable" :
392+ """Create an ALTER TABLE builder.
393+
394+ Args:
395+ table_name: Name of the table to alter
396+ dialect: Optional SQL dialect
397+
398+ Returns:
399+ AlterTable builder instance
400+ """
401+ builder = AlterTable (table_name )
402+ builder .dialect = dialect or self .dialect
403+ return builder
404+
405+ def rename_table (self , dialect : DialectType = None ) -> "RenameTable" :
406+ """Create a RENAME TABLE builder.
407+
408+ Args:
409+ dialect: Optional SQL dialect
410+
411+ Returns:
412+ RenameTable builder instance
413+ """
414+ builder = RenameTable ()
415+ builder .dialect = dialect or self .dialect
416+ return builder
417+
418+ def comment_on (self , dialect : DialectType = None ) -> "CommentOn" :
419+ """Create a COMMENT ON builder.
420+
421+ Args:
422+ dialect: Optional SQL dialect
423+
424+ Returns:
425+ CommentOn builder instance
426+ """
427+ builder = CommentOn ()
428+ builder .dialect = dialect or self .dialect
429+ return builder
430+
215431 # ===================
216432 # SQL Analysis Helpers
217433 # ===================
@@ -363,39 +579,39 @@ def __getattr__(self, name: str) -> Column:
363579 # ===================
364580
365581 @staticmethod
366- def raw (sql_fragment : str ) -> exp .Expression :
367- """Create a raw SQL expression from a string fragment.
582+ def raw (sql_fragment : str , ** parameters : Any ) -> "Union[ exp.Expression, SQL]" :
583+ """Create a raw SQL expression from a string fragment with optional parameters .
368584
369585 This method makes it explicit that you are passing raw SQL that should
370586 be parsed and included directly in the query. Useful for complex expressions,
371587 database-specific functions, or when you need precise control over the SQL.
372588
373589 Args:
374590 sql_fragment: Raw SQL string to parse into an expression.
591+ **parameters: Named parameters for parameter binding.
375592
376593 Returns:
377- SQLGlot expression from the parsed SQL fragment.
594+ SQLGlot expression from the parsed SQL fragment (if no parameters).
595+ SQL statement object (if parameters provided).
378596
379597 Raises:
380598 SQLBuilderError: If the SQL fragment cannot be parsed.
381599
382600 Example:
383601 ```python
384- # Raw column expression with alias
385- query = sql.select(
386- sql.raw("user.id AS u_id"), "name"
387- ).from_("users")
602+ # Raw expression without parameters (current behavior)
603+ expr = sql.raw("COALESCE(name, 'Unknown')")
388604
389- # Raw function call
390- query = sql.select (
391- sql.raw("COALESCE (name, 'Unknown')")
392- ).from_("users")
605+ # Raw SQL with named parameters (new functionality)
606+ stmt = sql.raw (
607+ "LOWER (name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
608+ )
393609
394- # Raw complex expression
395- query = (
396- sql.select("*")
397- .from_("orders")
398- .where(sql.raw("DATE(created_at) = CURRENT_DATE"))
610+ # Raw complex expression with parameters
611+ expr = sql.raw (
612+ "price BETWEEN :min_price AND :max_price",
613+ min_price=100,
614+ max_price=500,
399615 )
400616
401617 # Raw window function
@@ -407,16 +623,23 @@ def raw(sql_fragment: str) -> exp.Expression:
407623 ).from_("employees")
408624 ```
409625 """
410- try :
411- parsed : Optional [exp .Expression ] = exp .maybe_parse (sql_fragment )
412- if parsed is not None :
413- return parsed
414- if sql_fragment .strip ().replace ("_" , "" ).replace ("." , "" ).isalnum ():
415- return exp .to_identifier (sql_fragment )
416- return exp .Literal .string (sql_fragment )
417- except Exception as e :
418- msg = f"Failed to parse raw SQL fragment '{ sql_fragment } ': { e } "
419- raise SQLBuilderError (msg ) from e
626+ if not parameters :
627+ # Original behavior - return pure expression
628+ try :
629+ parsed : Optional [exp .Expression ] = exp .maybe_parse (sql_fragment )
630+ if parsed is not None :
631+ return parsed
632+ if sql_fragment .strip ().replace ("_" , "" ).replace ("." , "" ).isalnum ():
633+ return exp .to_identifier (sql_fragment )
634+ return exp .Literal .string (sql_fragment )
635+ except Exception as e :
636+ msg = f"Failed to parse raw SQL fragment '{ sql_fragment } ': { e } "
637+ raise SQLBuilderError (msg ) from e
638+
639+ # New behavior - return SQL statement with parameters
640+ from sqlspec .core .statement import SQL
641+
642+ return SQL (sql_fragment , parameters )
420643
421644 # ===================
422645 # Aggregate Functions
0 commit comments