11from sqlalchemy import exc
22from sqlalchemy .dialects .postgresql .base import PGDDLCompiler
3+ from collections .abc import Sequence
4+ from typing import Any , cast
5+
6+ from sqlalchemy import ColumnElement , exc
7+ from sqlalchemy .ext .compiler import compiles # type: ignore[import-untyped]
8+ from sqlalchemy .schema import CreateIndex , CreateTable , Index , Table
9+ from sqlalchemy .sql import coercions , expression , roles
10+ from sqlalchemy .sql .compiler import DDLCompiler # type: ignore[import-untyped]
11+ from sqlalchemy_cockroachdb .base import ( # type: ignore[import-untyped]
12+ CockroachDBDialect ,
13+ )
14+ from sqlalchemy_cockroachdb .ddl_compiler import ( # type: ignore[import-untyped]
15+ CockroachDDLCompiler ,
16+ )
317
418
519class CockroachDDLCompiler (PGDDLCompiler ):
@@ -14,3 +28,166 @@ def visit_computed_column(self, generated, **kw):
1428 return "AS (%s) STORED" % self .sql_compiler .process (
1529 generated .sqltext , include_table = False , literal_binds = True
1630 )
31+
32+
33+ # TODO: convert visitors to memeber functions on CockroachDDLCompiler (it's like
34+ # this now just because I wrote+tested it inside our codebase).
35+
36+
37+ @compiles (CreateTable , "cockroachdb" )
38+ def visit_create_table (
39+ element : CreateTable , compiler : CockroachDDLCompiler , ** kw : Any
40+ ) -> str :
41+ out = compiler .visit_create_table (element , ** kw )
42+
43+ assert isinstance (element .target , Table )
44+
45+ if len (element .target .indexes ) > 0 :
46+ indexes = [
47+ _codegen_index (i , compiler , include_schema = False , ** kw )
48+ for i in element .target .indexes
49+ ]
50+
51+ # TODO: Not compatible with anything that uses post_create_table, we
52+ # need to parse properly to find the `)` which matches `CREATE TABLE (`.
53+ out = out .rstrip ().rstrip (")" ).rstrip ()
54+ out += ",\n "
55+ out += ",\n \t " .join (indexes )
56+ out += "\n )"
57+
58+ # Record that we created these indexes so that we can double check it
59+ # later.
60+ for index in element .target .indexes :
61+ index .info ["_cockroachdb_index_created_by_create_table" ] = True
62+
63+ return out
64+
65+
66+ @compiles (CreateIndex , "cockroachdb" )
67+ def visit_create_index (element : Any , compiler : CockroachDDLCompiler , ** kw : Any ) -> str :
68+ index = element .target
69+ assert isinstance (index , Index )
70+ was_created = index .info .get ("_cockroachdb_index_created_by_create_table" , False )
71+ assert was_created
72+
73+ return "SELECT 'No-op: in cockroachdb we put index creation DDL inside the corresponding CREATE TABLE for improved performance.'"
74+
75+
76+ # Copy+paste of private function DDLCompiler._prepared_index_name
77+ def _prepared_index_name (
78+ index : Index , compiler : DDLCompiler , include_schema : bool = False
79+ ) -> str :
80+ if index .table is not None :
81+ effective_schema = compiler .preparer .schema_for_object (index .table )
82+ else :
83+ effective_schema = None
84+ if include_schema and effective_schema :
85+ schema_name = compiler .preparer .quote_schema (effective_schema )
86+ else :
87+ schema_name = None
88+
89+ index_name : str = cast (str , compiler .preparer .format_index (index ))
90+
91+ if schema_name :
92+ index_name = schema_name + "." + index_name
93+ return index_name
94+
95+
96+ IDX_USING = re .compile (r"^(?:btree|hash|gist|gin|[\w_]+)$" , re .I )
97+
98+
99+ # Heavily based on DDLCompiler.visit_create_index
100+ def _codegen_index (
101+ index : Index , compiler : DDLCompiler , include_schema : bool , ** kw : Any
102+ ) -> str :
103+ # I think this is only nullable before _set_parent is called. We shouldn't
104+ # need to emit DDL for any indexes in that state.
105+ assert index .table is not None
106+
107+ text = ""
108+
109+ # TODO: check this more carefully, I'm winging it here. Do all supported
110+ # postgres USINGs map to INVERTED? Why didn't we need this before these changes?
111+ using = index .dialect_options ["postgresql" ]["using" ]
112+ if using :
113+ assert using .lower () in ("gin" , "gist" )
114+ text += "INVERTED "
115+
116+ if index .unique :
117+ text += "UNIQUE "
118+ assert not using
119+
120+ # I don't think we strictly need an index name, but best to require one for
121+ # sqlalchemy compat with any other database.
122+ if index .name is None :
123+ raise exc .CompileError ("CREATE INDEX requires that the index have a name" )
124+
125+ text += "INDEX %s " % _prepared_index_name (
126+ index , compiler , include_schema = include_schema
127+ )
128+
129+ ops = index .dialect_options ["postgresql" ]["ops" ]
130+ text += "(%s)" % (
131+ ", " .join (
132+ [
133+ compiler .sql_compiler .process (
134+ (
135+ expr .self_group ()
136+ if not isinstance (expr , expression .ColumnClause )
137+ else expr
138+ ),
139+ include_table = False ,
140+ literal_binds = True ,
141+ )
142+ + (
143+ (" " + ops [expr .key ])
144+ if hasattr (expr , "key" ) and expr .key in ops
145+ else ""
146+ )
147+ for expr in cast (Sequence [ColumnElement [Any ]], index .expressions )
148+ ]
149+ )
150+ )
151+
152+ includeclause = index .dialect_options ["postgresql" ]["include" ]
153+ if includeclause :
154+ inclusions = [
155+ index .table .c [col ] if isinstance (col , str ) else col for col in includeclause
156+ ]
157+ text += " INCLUDE (%s)" % ", " .join (
158+ [compiler .preparer .quote (c .name ) for c in inclusions ]
159+ )
160+
161+ # TODO: I don't think crdb supports this feature?
162+ # nulls_not_distinct = index.dialect_options["postgresql"]["nulls_not_distinct"]
163+ # if nulls_not_distinct is True:
164+ # text += " NULLS NOT DISTINCT"
165+ # elif nulls_not_distinct is False:
166+ # text += " NULLS DISTINCT"
167+
168+ withclause = index .dialect_options ["postgresql" ]["with" ]
169+ if withclause :
170+ text += " WITH (%s)" % (
171+ ", " .join (
172+ [
173+ "%s = %s" % storage_parameter
174+ for storage_parameter in withclause .items ()
175+ ]
176+ )
177+ )
178+
179+ # TODO: I don't think crdb supports this feature?
180+ # tablespace_name = index.dialect_options["postgresql"]["tablespace"]
181+ # if tablespace_name:
182+ # text += " TABLESPACE %s" % compiler.preparer.quote(tablespace_name)
183+
184+ whereclause = index .dialect_options ["postgresql" ]["where" ]
185+ if whereclause is not None :
186+ whereclause = coercions .expect (roles .DDLExpressionRole , whereclause )
187+
188+ where_compiled = compiler .sql_compiler .process (
189+ whereclause , include_table = False , literal_binds = True
190+ )
191+ text += " WHERE " + where_compiled
192+
193+ return text
0 commit comments