Skip to content

Commit f980b41

Browse files
kesmit13claude
andcommitted
feat: Add Python boolean support for SingleStore table options
Enhanced SingleStore table options to accept Python booleans for improved API usability: - Add boolean support for AUTOSTATS_ENABLED (True/False -> TRUE/FALSE) - Add boolean support for AUTOSTATS_SAMPLING (True/False -> ON/OFF) - Add boolean False support for AUTOSTATS_CARDINALITY_MODE and AUTOSTATS_HISTOGRAM_MODE (False -> OFF) - Maintain full backward compatibility with existing string-based values - Add comprehensive validation with descriptive error messages for invalid values - Add extensive test coverage for all boolean conversion scenarios This enhancement makes the API more Pythonic by allowing native Python boolean values instead of requiring string literals for boolean-like table options, while maintaining backward compatibility. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent a39f19c commit f980b41

File tree

2 files changed

+471
-22
lines changed

2 files changed

+471
-22
lines changed

sqlalchemy_singlestoredb/base.py

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,10 @@ class SingleStoreDBDDLCompiler(MySQLDDLCompiler):
231231
"""SingleStoreDB SQLAlchemy DDL compiler."""
232232

233233
def post_create_table(self, table: Any) -> str:
234-
"""Build table-level CREATE options, filter SingleStore options."""
235-
# Get all table kwargs and filter out SingleStore-specific options
234+
"""Build table-level CREATE options, including SingleStore-specific options."""
236235
table_opts = []
237236

238-
# Get opts the same way MySQL does, but filter out our options
237+
# Get opts the same way MySQL does, but filter out our DDL element options
239238
opts = dict(
240239
(k[len(self.dialect.name) + 1:].upper(), v)
241240
for k, v in table.kwargs.items()
@@ -252,10 +251,51 @@ def post_create_table(self, table: Any) -> str:
252251
if table.comment is not None:
253252
opts['COMMENT'] = table.comment
254253

255-
# Process remaining MySQL-compatible options
254+
# Handle SingleStore-specific table options with proper formatting
255+
singlestore_opts = {
256+
'AUTOSTATS_ENABLED': ['TRUE', 'FALSE'],
257+
'AUTOSTATS_CARDINALITY_MODE': ['INCREMENTAL', 'PERIODIC', 'OFF'],
258+
'AUTOSTATS_HISTOGRAM_MODE': ['CREATE', 'UPDATE', 'OFF'],
259+
'AUTOSTATS_SAMPLING': ['ON', 'OFF'],
260+
'COMPRESSION': ['SPARSE'],
261+
}
262+
263+
# Boolean conversion mappings for SingleStore options
264+
# For options that accept OFF, False maps to OFF
265+
# For AUTOSTATS_ENABLED, False maps to FALSE (specific to that option)
266+
# For AUTOSTATS_SAMPLING, True maps to ON (specific to that option)
267+
boolean_mappings = {
268+
'AUTOSTATS_ENABLED': {True: 'TRUE', False: 'FALSE'},
269+
'AUTOSTATS_CARDINALITY_MODE': {False: 'OFF'}, # Only False->OFF
270+
'AUTOSTATS_HISTOGRAM_MODE': {False: 'OFF'}, # Only False->OFF
271+
'AUTOSTATS_SAMPLING': {True: 'ON', False: 'OFF'},
272+
}
273+
274+
# Process remaining options
256275
for opt, arg in opts.items():
257-
if hasattr(arg, '__str__'):
258-
table_opts.append(f'{opt}={arg}')
276+
# Handle boolean values for specific SingleStore options
277+
if opt in boolean_mappings and isinstance(arg, bool):
278+
if arg in boolean_mappings[opt]:
279+
arg_str = boolean_mappings[opt][arg]
280+
else:
281+
# Boolean value not supported for this option, convert to string
282+
arg_str = str(arg)
283+
else:
284+
arg_str = str(arg)
285+
286+
# Handle SingleStore-specific options with validation
287+
if opt in singlestore_opts:
288+
if arg_str.upper() in singlestore_opts[opt]:
289+
table_opts.append(f'{opt} = {arg_str.upper()}')
290+
else:
291+
valid_values = ', '.join(singlestore_opts[opt])
292+
raise ValueError(
293+
f'Invalid value "{arg_str}" for {opt}. '
294+
f'Valid values are: {valid_values}',
295+
)
296+
else:
297+
# Standard table options
298+
table_opts.append(f'{opt}={arg_str}')
259299

260300
if table_opts:
261301
return ' ' + ' '.join(table_opts)
@@ -273,21 +313,22 @@ def visit_create_table(self, create: Any, **kw: Any) -> str:
273313
# Get dialect options for SingleStore
274314
dialect_opts = create.element.dialect_options.get('singlestoredb', {})
275315

316+
# Collect all DDL elements to append
317+
ddl_elements = []
318+
276319
# Handle shard key (single value only)
277320
shard_key = dialect_opts.get('shard_key')
278321
if shard_key is not None:
279322
from sqlalchemy_singlestoredb.ddlelement import compile_shard_key
280323
shard_key_sql = compile_shard_key(shard_key, self)
281-
# Append the SHARD KEY definition to the original SQL
282-
create_table_sql = f'{create_table_sql.rstrip()[:-2]},\n\t{shard_key_sql}\n)'
324+
ddl_elements.append(shard_key_sql)
283325

284326
# Handle sort key (single value only)
285327
sort_key = dialect_opts.get('sort_key')
286328
if sort_key is not None:
287329
from sqlalchemy_singlestoredb.ddlelement import compile_sort_key
288330
sort_key_sql = compile_sort_key(sort_key, self)
289-
# Append the SORT KEY definition to the original SQL
290-
create_table_sql = f'{create_table_sql.rstrip()[:-2]},\n\t{sort_key_sql}\n)'
331+
ddl_elements.append(sort_key_sql)
291332

292333
# Handle vector keys (single value or list)
293334
vector_key = dialect_opts.get('vector_key')
@@ -297,10 +338,7 @@ def visit_create_table(self, create: Any, **kw: Any) -> str:
297338
vector_keys = vector_key if isinstance(vector_key, list) else [vector_key]
298339
for vector_index in vector_keys:
299340
vector_index_sql = compile_vector_key(vector_index, self)
300-
# Append the VECTOR INDEX definition to the original SQL
301-
create_table_sql = (
302-
f'{create_table_sql.rstrip()[:-2]},\n\t{vector_index_sql}\n)'
303-
)
341+
ddl_elements.append(vector_index_sql)
304342

305343
# Handle multi-value indexes (single value or list)
306344
multi_value_index = dialect_opts.get('multi_value_index')
@@ -312,20 +350,43 @@ def visit_create_table(self, create: Any, **kw: Any) -> str:
312350
) else [multi_value_index]
313351
for mv_index in multi_value_indexes:
314352
mv_index_sql = compile_multi_value_index(mv_index, self)
315-
# Append the MULTI VALUE INDEX definition to the original SQL
316-
create_table_sql = (
317-
f'{create_table_sql.rstrip()[:-2]},\n\t{mv_index_sql}\n)'
318-
)
353+
ddl_elements.append(mv_index_sql)
319354

320355
# Handle fulltext index (single value only - SingleStore limitation)
321356
full_text_index = dialect_opts.get('full_text_index')
322357
if full_text_index is not None:
323358
from sqlalchemy_singlestoredb.ddlelement import compile_fulltext_index
324359
ft_index_sql = compile_fulltext_index(full_text_index, self)
325-
# Append the FULLTEXT INDEX definition to the original SQL
326-
create_table_sql = (
327-
f'{create_table_sql.rstrip()[:-2]},\n\t{ft_index_sql}\n)'
328-
)
360+
ddl_elements.append(ft_index_sql)
361+
362+
# If we have DDL elements to add, modify the SQL
363+
if ddl_elements:
364+
# We need to handle the case where table options might be present
365+
sql_stripped = create_table_sql.rstrip()
366+
367+
# Look for table options (they come after the closing parenthesis)
368+
# Pattern: ") TABLE_OPTION1=value1 TABLE_OPTION2=value2"
369+
closing_paren_pos = sql_stripped.rfind(')')
370+
371+
if closing_paren_pos != -1:
372+
# Split into table definition and table options parts
373+
table_def_part = sql_stripped[:closing_paren_pos] # Before ')'
374+
table_options_part = sql_stripped[closing_paren_pos + 1:] # After ')'
375+
376+
# Add DDL elements inside the table definition
377+
# Remove trailing newline from table definition and add comma
378+
table_def_clean = table_def_part.rstrip()
379+
formatted_ddl_elements = ',\n\t'.join(ddl_elements)
380+
381+
# Reconstruct with proper formatting
382+
create_table_sql = (
383+
f'{table_def_clean},\n\t{formatted_ddl_elements}\n)'
384+
f'{table_options_part}'
385+
)
386+
else:
387+
# This shouldn't happen with normal CREATE TABLE, but handle it
388+
ddl_part = ',\n\t' + ',\n\t'.join(ddl_elements)
389+
create_table_sql = f'{sql_stripped}{ddl_part}'
329390

330391
return create_table_sql
331392

0 commit comments

Comments
 (0)