Skip to content

Commit 156a1f0

Browse files
committed
Support computed columns in older CockroachDB versions
- Add a DDL compiler for CockroachDB so the computed column syntax without `GENERATED ALWAYS AS` is used. - Enable computed column tests. - Populate `autoincrement` property, since it's required by the computed column tests.
1 parent 8c9e668 commit 156a1f0

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-4
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from sqlalchemy import exc
2+
from sqlalchemy.dialects.postgresql.base import PGDDLCompiler
3+
4+
5+
class CockroachDDLCompiler(PGDDLCompiler):
6+
def visit_computed_column(self, generated):
7+
if generated.persisted is False:
8+
raise exc.CompileError(
9+
"CockroachDB computed columns do not support 'virtual' "
10+
"persistence; set the 'persisted' flag to None or True for "
11+
"CockroachDB support."
12+
)
13+
14+
print('sqltext', generated.sqltext)
15+
return "AS (%s) STORED" % self.sql_compiler.process(
16+
generated.sqltext, include_table=False, literal_binds=True
17+
)

cockroachdb/sqlalchemy/dialect.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import sqlalchemy.types as sqltypes
1313

1414
from .stmt_compiler import CockroachCompiler, CockroachIdentifierPreparer
15+
from .ddl_compiler import CockroachDDLCompiler
1516

1617
# Map type names (as returned by information_schema) to sqlalchemy type
1718
# objects.
@@ -97,6 +98,7 @@ class CockroachDBDialect(PGDialect_psycopg2):
9798
supports_sequences = False
9899
statement_compiler = CockroachCompiler
99100
preparer = CockroachIdentifierPreparer
101+
ddl_compiler = CockroachDDLCompiler
100102

101103
def __init__(self, *args, **kwargs):
102104
if kwargs.get("use_native_hstore", False):
@@ -160,11 +162,22 @@ def get_columns(self, conn, table_name, schema=None, **kw):
160162
# Oh well. Hoping 1.1 won't be around for long.
161163
rows = conn.execute('SHOW COLUMNS FROM "%s"."%s"' %
162164
(schema or self.default_schema_name, table_name))
165+
elif not self._is_v191plus:
166+
# v2.x does not have is_generated or generation_expression
167+
rows = conn.execute(
168+
'SELECT column_name, data_type, is_nullable::bool, column_default, '
169+
'numeric_precision, numeric_scale, character_maximum_length, '
170+
'NULL AS is_generated, NULL AS generation_expression '
171+
'FROM information_schema.columns '
172+
'WHERE table_schema = %s AND table_name = %s AND NOT is_hidden::bool',
173+
(schema or self.default_schema_name, table_name),
174+
)
163175
else:
164-
# v2.0 or later. Information schema is usable.
176+
# v19.1 or later. Information schema columns are all usable.
165177
rows = conn.execute(
166178
'SELECT column_name, data_type, is_nullable::bool, column_default, '
167-
'numeric_precision, numeric_scale, character_maximum_length '
179+
'numeric_precision, numeric_scale, character_maximum_length, '
180+
'is_generated::bool, generation_expression '
168181
'FROM information_schema.columns '
169182
'WHERE table_schema = %s AND table_name = %s AND NOT is_hidden::bool',
170183
(schema or self.default_schema_name, table_name),
@@ -198,12 +211,47 @@ def get_columns(self, conn, table_name, schema=None, **kw):
198211
typ = type_class(length=row.character_maximum_length)
199212
else:
200213
typ = type_class
201-
res.append(dict(
214+
if row.is_generated:
215+
# Currently, all computed columns are persisted.
216+
computed = dict(sqltext=row.generation_expression, persisted=True)
217+
default = None
218+
else:
219+
computed = None
220+
# Check if a sequence is being used and adjust the default value.
221+
autoincrement = False
222+
if default is not None:
223+
nextval_match = re.search(r"""(nextval\(')([^']+)('.*$)""", default)
224+
unique_rowid_match = re.search(r"""unique_rowid\(""", default)
225+
if nextval_match is not None or unique_rowid_match is not None:
226+
print('affinity', type_class)
227+
if issubclass(type_class, sqltypes.Integer):
228+
autoincrement = True
229+
# the default is related to a Sequence
230+
sch = schema
231+
if nextval_match is not None \
232+
and "." not in nextval_match.group(2) \
233+
and sch is not None:
234+
# unconditionally quote the schema name. this could
235+
# later be enhanced to obey quoting rules /
236+
# "quote schema"
237+
default = (
238+
nextval_match.group(1)
239+
+ ('"%s"' % sch)
240+
+ "."
241+
+ nextval_match.group(2)
242+
+ nextval_match.group(3)
243+
)
244+
245+
column_info = dict(
202246
name=name,
203247
type=typ,
204248
nullable=nullable,
205249
default=default,
206-
))
250+
autoincrement=autoincrement,
251+
)
252+
if computed is not None:
253+
column_info["computed"] = computed
254+
res.append(column_info)
207255
return res
208256

209257
def get_indexes(self, conn, table_name, schema=None, **kw):

cockroachdb/sqlalchemy/test_requirements.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ class Requirements(SuiteRequirements):
7272
update_from = exclusions.open()
7373
mod_operator_as_percent_sign = exclusions.open()
7474
foreign_key_constraint_reflection = exclusions.open()
75+
computed_columns = \
76+
exclusions.skip_if(lambda config: not config.db.dialect._is_v191plus,
77+
"versions before 19.1 do not support reflection on computed columns")
78+
computed_columns_stored = \
79+
exclusions.skip_if(lambda config: not config.db.dialect._is_v191plus,
80+
"versions before 19.1 do not support reflection on computed columns")
81+
computed_columns_default_persisted = \
82+
exclusions.skip_if(lambda config: not config.db.dialect._is_v191plus,
83+
"versions before 19.1 do not support reflection on computed columns")
84+
computed_columns_reflect_persisted = \
85+
exclusions.skip_if(lambda config: not config.db.dialect._is_v191plus,
86+
"versions before 19.1 do not support reflection on computed columns")
87+
computed_columns_virtual = exclusions.closed()
7588
ctes = exclusions.skip_if(lambda config: not config.db.dialect._is_v201plus,
7689
"versions before 20.x do not fully support CTEs.")
7790
ctes_with_update_delete = \

0 commit comments

Comments
 (0)