@@ -231,11 +231,10 @@ class SingleStoreDBDDLCompiler(MySQLDDLCompiler):
231
231
"""SingleStoreDB SQLAlchemy DDL compiler."""
232
232
233
233
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."""
236
235
table_opts = []
237
236
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
239
238
opts = dict (
240
239
(k [len (self .dialect .name ) + 1 :].upper (), v )
241
240
for k , v in table .kwargs .items ()
@@ -252,10 +251,51 @@ def post_create_table(self, table: Any) -> str:
252
251
if table .comment is not None :
253
252
opts ['COMMENT' ] = table .comment
254
253
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
256
275
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 } ' )
259
299
260
300
if table_opts :
261
301
return ' ' + ' ' .join (table_opts )
@@ -273,21 +313,22 @@ def visit_create_table(self, create: Any, **kw: Any) -> str:
273
313
# Get dialect options for SingleStore
274
314
dialect_opts = create .element .dialect_options .get ('singlestoredb' , {})
275
315
316
+ # Collect all DDL elements to append
317
+ ddl_elements = []
318
+
276
319
# Handle shard key (single value only)
277
320
shard_key = dialect_opts .get ('shard_key' )
278
321
if shard_key is not None :
279
322
from sqlalchemy_singlestoredb .ddlelement import compile_shard_key
280
323
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 )
283
325
284
326
# Handle sort key (single value only)
285
327
sort_key = dialect_opts .get ('sort_key' )
286
328
if sort_key is not None :
287
329
from sqlalchemy_singlestoredb .ddlelement import compile_sort_key
288
330
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 )
291
332
292
333
# Handle vector keys (single value or list)
293
334
vector_key = dialect_opts .get ('vector_key' )
@@ -297,10 +338,7 @@ def visit_create_table(self, create: Any, **kw: Any) -> str:
297
338
vector_keys = vector_key if isinstance (vector_key , list ) else [vector_key ]
298
339
for vector_index in vector_keys :
299
340
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 )
304
342
305
343
# Handle multi-value indexes (single value or list)
306
344
multi_value_index = dialect_opts .get ('multi_value_index' )
@@ -312,20 +350,43 @@ def visit_create_table(self, create: Any, **kw: Any) -> str:
312
350
) else [multi_value_index ]
313
351
for mv_index in multi_value_indexes :
314
352
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 )
319
354
320
355
# Handle fulltext index (single value only - SingleStore limitation)
321
356
full_text_index = dialect_opts .get ('full_text_index' )
322
357
if full_text_index is not None :
323
358
from sqlalchemy_singlestoredb .ddlelement import compile_fulltext_index
324
359
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 } '
329
390
330
391
return create_table_sql
331
392
0 commit comments