Skip to content

Commit 9132bbd

Browse files
Hacky WIP implementation of moving index creation inside CREATE TABLE
1 parent bc87688 commit 9132bbd

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

sqlalchemy_cockroachdb/ddl_compiler.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
from sqlalchemy import exc
22
from 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

519
class 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

Comments
 (0)