meerschaum 2.6.17__py3-none-any.whl → 2.7.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. meerschaum/actions/delete.py +65 -69
  2. meerschaum/actions/install.py +1 -2
  3. meerschaum/api/routes/_pipes.py +7 -8
  4. meerschaum/config/_default.py +1 -1
  5. meerschaum/config/_paths.py +2 -1
  6. meerschaum/config/_version.py +1 -1
  7. meerschaum/connectors/api/_pipes.py +18 -21
  8. meerschaum/connectors/sql/_instance.py +11 -12
  9. meerschaum/connectors/sql/_pipes.py +122 -78
  10. meerschaum/connectors/sql/_sql.py +43 -8
  11. meerschaum/connectors/valkey/_pipes.py +12 -1
  12. meerschaum/core/Pipe/__init__.py +23 -13
  13. meerschaum/core/Pipe/_attributes.py +25 -1
  14. meerschaum/core/Pipe/_dtypes.py +23 -16
  15. meerschaum/core/Pipe/_sync.py +59 -31
  16. meerschaum/core/Pipe/_verify.py +8 -7
  17. meerschaum/jobs/_Job.py +2 -0
  18. meerschaum/plugins/_Plugin.py +11 -14
  19. meerschaum/utils/daemon/Daemon.py +20 -13
  20. meerschaum/utils/dataframe.py +178 -16
  21. meerschaum/utils/dtypes/__init__.py +149 -14
  22. meerschaum/utils/dtypes/sql.py +41 -7
  23. meerschaum/utils/misc.py +8 -8
  24. meerschaum/utils/sql.py +174 -64
  25. meerschaum/utils/venv/_Venv.py +4 -4
  26. meerschaum/utils/venv/__init__.py +53 -20
  27. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/METADATA +1 -1
  28. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/RECORD +34 -34
  29. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/LICENSE +0 -0
  30. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/NOTICE +0 -0
  31. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/WHEEL +0 -0
  32. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/entry_points.txt +0 -0
  33. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/top_level.txt +0 -0
  34. {meerschaum-2.6.17.dist-info → meerschaum-2.7.0.dist-info}/zip-safe +0 -0
meerschaum/utils/misc.py CHANGED
@@ -177,14 +177,14 @@ def string_to_dict(
177
177
  keys = _keys[:-1]
178
178
  try:
179
179
  val = ast.literal_eval(_keys[-1])
180
- except Exception as e:
180
+ except Exception:
181
181
  val = str(_keys[-1])
182
182
 
183
183
  c = params_dict
184
184
  for _k in keys[:-1]:
185
185
  try:
186
186
  k = ast.literal_eval(_k)
187
- except Exception as e:
187
+ except Exception:
188
188
  k = str(_k)
189
189
  if k not in c:
190
190
  c[k] = {}
@@ -196,12 +196,12 @@ def string_to_dict(
196
196
 
197
197
 
198
198
  def parse_config_substitution(
199
- value: str,
200
- leading_key: str = 'MRSM',
201
- begin_key: str = '{',
202
- end_key: str = '}',
203
- delimeter: str = ':'
204
- ) -> List[Any]:
199
+ value: str,
200
+ leading_key: str = 'MRSM',
201
+ begin_key: str = '{',
202
+ end_key: str = '}',
203
+ delimeter: str = ':'
204
+ ) -> List[Any]:
205
205
  """
206
206
  Parse Meerschaum substitution syntax
207
207
  E.g. MRSM{value1:value2} => ['value1', 'value2']
meerschaum/utils/sql.py CHANGED
@@ -7,6 +7,7 @@ Flavor-specific SQL tools.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
10
11
  from datetime import datetime, timezone, timedelta
11
12
  import meerschaum as mrsm
12
13
  from meerschaum.utils.typing import Optional, Dict, Any, Union, List, Iterable, Tuple
@@ -50,10 +51,12 @@ update_queries = {
50
51
  {sets_subquery_none}
51
52
  FROM {target_table_name} AS t
52
53
  INNER JOIN (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) AS p
53
- ON {and_subquery_t}
54
+ ON
55
+ {and_subquery_t}
54
56
  WHERE
55
57
  {and_subquery_f}
56
- AND {date_bounds_subquery}
58
+ AND
59
+ {date_bounds_subquery}
57
60
  """,
58
61
  'timescaledb-upsert': """
59
62
  INSERT INTO {target_table_name} ({patch_cols_str})
@@ -82,9 +85,11 @@ update_queries = {
82
85
  'mysql': """
83
86
  UPDATE {target_table_name} AS f
84
87
  JOIN (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) AS p
85
- ON {and_subquery_f}
88
+ ON
89
+ {and_subquery_f}
86
90
  {sets_subquery_f}
87
- WHERE {date_bounds_subquery}
91
+ WHERE
92
+ {date_bounds_subquery}
88
93
  """,
89
94
  'mysql-upsert': """
90
95
  INSERT {ignore}INTO {target_table_name} ({patch_cols_str})
@@ -96,9 +101,11 @@ update_queries = {
96
101
  'mariadb': """
97
102
  UPDATE {target_table_name} AS f
98
103
  JOIN (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) AS p
99
- ON {and_subquery_f}
104
+ ON
105
+ {and_subquery_f}
100
106
  {sets_subquery_f}
101
- WHERE {date_bounds_subquery}
107
+ WHERE
108
+ {date_bounds_subquery}
102
109
  """,
103
110
  'mariadb-upsert': """
104
111
  INSERT {ignore}INTO {target_table_name} ({patch_cols_str})
@@ -108,34 +115,56 @@ update_queries = {
108
115
  {cols_equal_values}
109
116
  """,
110
117
  'mssql': """
118
+ {with_temp_date_bounds}
111
119
  MERGE {target_table_name} f
112
- USING (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) p
113
- ON {and_subquery_f}
114
- AND {date_bounds_subquery}
120
+ USING (SELECT {patch_cols_str} FROM {patch_table_name}) p
121
+ ON
122
+ {and_subquery_f}
123
+ AND
124
+ {date_bounds_subquery}
115
125
  WHEN MATCHED THEN
116
126
  UPDATE
117
127
  {sets_subquery_none};
118
128
  """,
119
- 'mssql-upsert': """
129
+ 'mssql-upsert': [
130
+ "{identity_insert_on}",
131
+ """
132
+ {with_temp_date_bounds}
120
133
  MERGE {target_table_name} f
121
- USING (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) p
122
- ON {and_subquery_f}
123
- AND {date_bounds_subquery}
124
- {when_matched_update_sets_subquery_none}
134
+ USING (SELECT {patch_cols_str} FROM {patch_table_name}) p
135
+ ON
136
+ {and_subquery_f}
137
+ AND
138
+ {date_bounds_subquery}{when_matched_update_sets_subquery_none}
125
139
  WHEN NOT MATCHED THEN
126
140
  INSERT ({patch_cols_str})
127
141
  VALUES ({patch_cols_prefixed_str});
128
- """,
142
+ """,
143
+ "{identity_insert_off}",
144
+ ],
129
145
  'oracle': """
130
146
  MERGE INTO {target_table_name} f
131
- USING (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) p
147
+ USING (SELECT {patch_cols_str} FROM {patch_table_name}) p
132
148
  ON (
133
149
  {and_subquery_f}
134
- AND {date_bounds_subquery}
150
+ AND
151
+ {date_bounds_subquery}
135
152
  )
136
- WHEN MATCHED THEN
137
- UPDATE
138
- {sets_subquery_none}
153
+ WHEN MATCHED THEN
154
+ UPDATE
155
+ {sets_subquery_none}
156
+ """,
157
+ 'oracle-upsert': """
158
+ MERGE INTO {target_table_name} f
159
+ USING (SELECT {patch_cols_str} FROM {patch_table_name}) p
160
+ ON (
161
+ {and_subquery_f}
162
+ AND
163
+ {date_bounds_subquery}
164
+ ){when_matched_update_sets_subquery_none}
165
+ WHEN NOT MATCHED THEN
166
+ INSERT ({patch_cols_str})
167
+ VALUES ({patch_cols_prefixed_str})
139
168
  """,
140
169
  'sqlite-upsert': """
141
170
  INSERT INTO {target_table_name} ({patch_cols_str})
@@ -323,7 +352,11 @@ columns_indices_queries = {
323
352
  CASE
324
353
  WHEN kc.type = 'PK' THEN 'PRIMARY KEY'
325
354
  ELSE 'INDEX'
326
- END AS [index_type]
355
+ END AS [index_type],
356
+ CASE
357
+ WHEN i.type = 1 THEN CAST(1 AS BIT)
358
+ ELSE CAST(0 AS BIT)
359
+ END AS [clustered]
327
360
  FROM
328
361
  sys.schemas s
329
362
  INNER JOIN sys.tables t
@@ -489,7 +522,8 @@ def dateadd_str(
489
522
  flavor: str = 'postgresql',
490
523
  datepart: str = 'day',
491
524
  number: Union[int, float] = 0,
492
- begin: Union[str, datetime, int] = 'now'
525
+ begin: Union[str, datetime, int] = 'now',
526
+ db_type: Optional[str] = None,
493
527
  ) -> str:
494
528
  """
495
529
  Generate a `DATEADD` clause depending on database flavor.
@@ -528,6 +562,10 @@ def dateadd_str(
528
562
  begin: Union[str, datetime], default `'now'`
529
563
  Base datetime to which to add dateparts.
530
564
 
565
+ db_type: Optional[str], default None
566
+ If provided, cast the datetime string as the type.
567
+ Otherwise, infer this from the input datetime value.
568
+
531
569
  Returns
532
570
  -------
533
571
  The appropriate `DATEADD` string for the corresponding database flavor.
@@ -539,7 +577,7 @@ def dateadd_str(
539
577
  ... begin = datetime(2022, 1, 1, 0, 0),
540
578
  ... number = 1,
541
579
  ... )
542
- "DATEADD(day, 1, CAST('2022-01-01 00:00:00' AS DATETIME))"
580
+ "DATEADD(day, 1, CAST('2022-01-01 00:00:00' AS DATETIME2))"
543
581
  >>> dateadd_str(
544
582
  ... flavor = 'postgresql',
545
583
  ... begin = datetime(2022, 1, 1, 0, 0),
@@ -582,7 +620,7 @@ def dateadd_str(
582
620
  )
583
621
 
584
622
  dt_is_utc = begin_time.tzinfo is not None if begin_time is not None else '+' in str(begin)
585
- db_type = get_db_type_from_pd_type(
623
+ db_type = db_type or get_db_type_from_pd_type(
586
624
  ('datetime64[ns, UTC]' if dt_is_utc else 'datetime64[ns]'),
587
625
  flavor=flavor,
588
626
  )
@@ -707,7 +745,7 @@ def get_distinct_col_count(
707
745
  result = connector.value(_meta_query, debug=debug)
708
746
  try:
709
747
  return int(result)
710
- except Exception as e:
748
+ except Exception:
711
749
  return None
712
750
 
713
751
 
@@ -717,12 +755,15 @@ def sql_item_name(item: str, flavor: str, schema: Optional[str] = None) -> str:
717
755
 
718
756
  Parameters
719
757
  ----------
720
- item: str :
758
+ item: str
721
759
  The database item (table, view, etc.) in need of quotes.
722
760
 
723
- flavor: str :
761
+ flavor: str
724
762
  The database flavor (`'postgresql'`, `'mssql'`, `'sqllite'`, etc.).
725
763
 
764
+ schema: Optional[str], default None
765
+ If provided, prefix the table name with the schema.
766
+
726
767
  Returns
727
768
  -------
728
769
  A `str` which contains the input `item` wrapped in the corresponding escape characters.
@@ -754,6 +795,8 @@ def sql_item_name(item: str, flavor: str, schema: Optional[str] = None) -> str:
754
795
  ### NOTE: SQLite does not support schemas.
755
796
  if flavor == 'sqlite':
756
797
  schema = None
798
+ elif flavor == 'mssql' and str(item).startswith('#'):
799
+ schema = None
757
800
 
758
801
  schema_prefix = (
759
802
  (wrappers[0] + schema + wrappers[1] + '.')
@@ -1056,7 +1099,7 @@ def get_sqlalchemy_table(
1056
1099
  connector.metadata,
1057
1100
  **table_kwargs
1058
1101
  )
1059
- except sqlalchemy.exc.NoSuchTableError as e:
1102
+ except sqlalchemy.exc.NoSuchTableError:
1060
1103
  warn(f"Table '{truncated_table_name}' does not exist in '{connector}'.")
1061
1104
  return None
1062
1105
  return tables[truncated_table_name]
@@ -1109,6 +1152,7 @@ def get_table_cols_types(
1109
1152
  -------
1110
1153
  A dictionary mapping column names to data types.
1111
1154
  """
1155
+ import textwrap
1112
1156
  from meerschaum.connectors import SQLConnector
1113
1157
  sqlalchemy = mrsm.attempt_import('sqlalchemy')
1114
1158
  flavor = flavor or getattr(connectable, 'flavor', None)
@@ -1134,7 +1178,7 @@ def get_table_cols_types(
1134
1178
  )
1135
1179
 
1136
1180
  cols_types_query = sqlalchemy.text(
1137
- columns_types_queries.get(
1181
+ textwrap.dedent(columns_types_queries.get(
1138
1182
  flavor,
1139
1183
  columns_types_queries['default']
1140
1184
  ).format(
@@ -1145,7 +1189,7 @@ def get_table_cols_types(
1145
1189
  table_upper=table_upper,
1146
1190
  table_upper_trunc=table_upper_trunc,
1147
1191
  db_prefix=db_prefix,
1148
- )
1192
+ )).lstrip().rstrip()
1149
1193
  )
1150
1194
 
1151
1195
  cols = ['database', 'schema', 'table', 'column', 'type']
@@ -1259,6 +1303,7 @@ def get_table_cols_indices(
1259
1303
  -------
1260
1304
  A dictionary mapping column names to a list of indices.
1261
1305
  """
1306
+ import textwrap
1262
1307
  from collections import defaultdict
1263
1308
  from meerschaum.connectors import SQLConnector
1264
1309
  sqlalchemy = mrsm.attempt_import('sqlalchemy')
@@ -1285,7 +1330,7 @@ def get_table_cols_indices(
1285
1330
  )
1286
1331
 
1287
1332
  cols_indices_query = sqlalchemy.text(
1288
- columns_indices_queries.get(
1333
+ textwrap.dedent(columns_indices_queries.get(
1289
1334
  flavor,
1290
1335
  columns_indices_queries['default']
1291
1336
  ).format(
@@ -1297,10 +1342,12 @@ def get_table_cols_indices(
1297
1342
  table_upper_trunc=table_upper_trunc,
1298
1343
  db_prefix=db_prefix,
1299
1344
  schema=schema,
1300
- )
1345
+ )).lstrip().rstrip()
1301
1346
  )
1302
1347
 
1303
1348
  cols = ['database', 'schema', 'table', 'column', 'index', 'index_type']
1349
+ if flavor == 'mssql':
1350
+ cols.append('clustered')
1304
1351
  result_cols_ix = dict(enumerate(cols))
1305
1352
 
1306
1353
  debug_kwargs = {'debug': debug} if isinstance(connectable, SQLConnector) else {}
@@ -1341,7 +1388,6 @@ def get_table_cols_indices(
1341
1388
  )
1342
1389
  )
1343
1390
  ]
1344
-
1345
1391
  ### NOTE: This may return incorrect columns if the schema is not explicitly stated.
1346
1392
  if cols_types_docs and not cols_types_docs_filtered:
1347
1393
  cols_types_docs_filtered = cols_types_docs
@@ -1357,12 +1403,13 @@ def get_table_cols_indices(
1357
1403
  else doc['column']
1358
1404
  )
1359
1405
  )
1360
- cols_indices[col].append(
1361
- {
1362
- 'name': doc.get('index', None),
1363
- 'type': doc.get('index_type', None),
1364
- }
1365
- )
1406
+ index_doc = {
1407
+ 'name': doc.get('index', None),
1408
+ 'type': doc.get('index_type', None)
1409
+ }
1410
+ if flavor == 'mssql':
1411
+ index_doc['clustered'] = doc.get('clustered', None)
1412
+ cols_indices[col].append(index_doc)
1366
1413
 
1367
1414
  return dict(cols_indices)
1368
1415
  except Exception as e:
@@ -1383,6 +1430,7 @@ def get_update_queries(
1383
1430
  datetime_col: Optional[str] = None,
1384
1431
  schema: Optional[str] = None,
1385
1432
  patch_schema: Optional[str] = None,
1433
+ identity_insert: bool = False,
1386
1434
  debug: bool = False,
1387
1435
  ) -> List[str]:
1388
1436
  """
@@ -1420,6 +1468,10 @@ def get_update_queries(
1420
1468
  If provided, use this schema when quoting the patch table.
1421
1469
  Defaults to `schema`.
1422
1470
 
1471
+ identity_insert: bool, default False
1472
+ If `True`, include `SET IDENTITY_INSERT` queries before and after the update queries.
1473
+ Only applies for MSSQL upserts.
1474
+
1423
1475
  debug: bool, default False
1424
1476
  Verbosity toggle.
1425
1477
 
@@ -1427,9 +1479,11 @@ def get_update_queries(
1427
1479
  -------
1428
1480
  A list of query strings to perform the update operation.
1429
1481
  """
1482
+ import textwrap
1430
1483
  from meerschaum.connectors import SQLConnector
1431
1484
  from meerschaum.utils.debug import dprint
1432
- from meerschaum.utils.dtypes.sql import DB_FLAVORS_CAST_DTYPES
1485
+ from meerschaum.utils.dtypes import are_dtypes_equal
1486
+ from meerschaum.utils.dtypes.sql import DB_FLAVORS_CAST_DTYPES, get_pd_type_from_db_type
1433
1487
  flavor = flavor or (connectable.flavor if isinstance(connectable, SQLConnector) else None)
1434
1488
  if not flavor:
1435
1489
  raise ValueError("Provide a flavor if using a SQLAlchemy session.")
@@ -1522,21 +1576,35 @@ def get_update_queries(
1522
1576
  def sets_subquery(l_prefix: str, r_prefix: str):
1523
1577
  if not value_cols:
1524
1578
  return ''
1579
+
1580
+ cast_func_cols = {
1581
+ c_name: (
1582
+ ('', '', '')
1583
+ if (
1584
+ flavor == 'oracle'
1585
+ and are_dtypes_equal(get_pd_type_from_db_type(c_type), 'bytes')
1586
+ )
1587
+ else (
1588
+ ('CAST(', f" AS {c_type.replace('_', ' ')}", ')')
1589
+ if flavor != 'sqlite'
1590
+ else ('', '', '')
1591
+ )
1592
+ )
1593
+ for c_name, c_type in value_cols
1594
+ }
1525
1595
  return 'SET ' + ',\n'.join([
1526
1596
  (
1527
1597
  l_prefix + sql_item_name(c_name, flavor, None)
1528
1598
  + ' = '
1529
- + ('CAST(' if flavor != 'sqlite' else '')
1530
- + r_prefix
1531
- + sql_item_name(c_name, flavor, None)
1532
- + (' AS ' if flavor != 'sqlite' else '')
1533
- + (c_type.replace('_', ' ') if flavor != 'sqlite' else '')
1534
- + (')' if flavor != 'sqlite' else '')
1599
+ + cast_func_cols[c_name][0]
1600
+ + r_prefix + sql_item_name(c_name, flavor, None)
1601
+ + cast_func_cols[c_name][1]
1602
+ + cast_func_cols[c_name][2]
1535
1603
  ) for c_name, c_type in value_cols
1536
1604
  ])
1537
1605
 
1538
1606
  def and_subquery(l_prefix: str, r_prefix: str):
1539
- return '\nAND\n'.join([
1607
+ return '\n AND\n '.join([
1540
1608
  (
1541
1609
  "COALESCE("
1542
1610
  + l_prefix
@@ -1544,7 +1612,7 @@ def get_update_queries(
1544
1612
  + ", "
1545
1613
  + get_null_replacement(c_type, flavor)
1546
1614
  + ")"
1547
- + ' = '
1615
+ + '\n =\n '
1548
1616
  + "COALESCE("
1549
1617
  + r_prefix
1550
1618
  + sql_item_name(c_name, flavor, None)
@@ -1554,22 +1622,39 @@ def get_update_queries(
1554
1622
  ) for c_name, c_type in join_cols_types
1555
1623
  ])
1556
1624
 
1625
+ skip_query_val = ""
1557
1626
  target_table_name = sql_item_name(target, flavor, schema)
1558
1627
  patch_table_name = sql_item_name(patch, flavor, patch_schema)
1559
1628
  dt_col_name = sql_item_name(datetime_col, flavor, None) if datetime_col else None
1629
+ date_bounds_table = patch_table_name if flavor != 'mssql' else '[date_bounds]'
1630
+ min_dt_col_name = f"MIN({dt_col_name})" if flavor != 'mssql' else '[Min_dt]'
1631
+ max_dt_col_name = f"MAX({dt_col_name})" if flavor != 'mssql' else '[Max_dt]'
1560
1632
  date_bounds_subquery = (
1561
- f"""
1562
- f.{dt_col_name} >= (SELECT MIN({dt_col_name}) FROM {patch_table_name})
1563
- AND f.{dt_col_name} <= (SELECT MAX({dt_col_name}) FROM {patch_table_name})
1564
- """
1633
+ f"""f.{dt_col_name} >= (SELECT {min_dt_col_name} FROM {date_bounds_table})
1634
+ AND
1635
+ f.{dt_col_name} <= (SELECT {max_dt_col_name} FROM {date_bounds_table})"""
1565
1636
  if datetime_col
1566
1637
  else "1 = 1"
1567
1638
  )
1639
+ with_temp_date_bounds = f"""WITH [date_bounds] AS (
1640
+ SELECT MIN({dt_col_name}) AS {min_dt_col_name}, MAX({dt_col_name}) AS {max_dt_col_name}
1641
+ FROM {patch_table_name}
1642
+ )""" if datetime_col else ""
1643
+ identity_insert_on = (
1644
+ f"SET IDENTITY_INSERT {target_table_name} ON"
1645
+ if identity_insert
1646
+ else skip_query_val
1647
+ )
1648
+ identity_insert_off = (
1649
+ f"SET IDENTITY_INSERT {target_table_name} OFF"
1650
+ if identity_insert
1651
+ else skip_query_val
1652
+ )
1568
1653
 
1569
1654
  ### NOTE: MSSQL upserts must exclude the update portion if only upserting indices.
1570
1655
  when_matched_update_sets_subquery_none = "" if not value_cols else (
1571
- "WHEN MATCHED THEN"
1572
- f" UPDATE {sets_subquery('', 'p.')}"
1656
+ "\n WHEN MATCHED THEN\n"
1657
+ f" UPDATE {sets_subquery('', 'p.')}"
1573
1658
  )
1574
1659
 
1575
1660
  cols_equal_values = '\n,'.join(
@@ -1585,8 +1670,8 @@ def get_update_queries(
1585
1670
  )
1586
1671
  ignore = "IGNORE " if not value_cols else ""
1587
1672
 
1588
- return [
1589
- base_query.format(
1673
+ formatted_queries = [
1674
+ textwrap.dedent(base_query.format(
1590
1675
  sets_subquery_none=sets_subquery('', 'p.'),
1591
1676
  sets_subquery_none_excluded=sets_subquery('', 'EXCLUDED.'),
1592
1677
  sets_subquery_f=sets_subquery('f.', 'p.'),
@@ -1604,10 +1689,16 @@ def get_update_queries(
1604
1689
  cols_equal_values=cols_equal_values,
1605
1690
  on_duplicate_key_update=on_duplicate_key_update,
1606
1691
  ignore=ignore,
1607
- )
1692
+ with_temp_date_bounds=with_temp_date_bounds,
1693
+ identity_insert_on=identity_insert_on,
1694
+ identity_insert_off=identity_insert_off,
1695
+ )).lstrip().rstrip()
1608
1696
  for base_query in base_queries
1609
1697
  ]
1610
1698
 
1699
+ ### NOTE: Allow for skipping some queries.
1700
+ return [query for query in formatted_queries if query]
1701
+
1611
1702
 
1612
1703
  def get_null_replacement(typ: str, flavor: str) -> str:
1613
1704
  """
@@ -1645,11 +1736,14 @@ def get_null_replacement(typ: str, flavor: str) -> str:
1645
1736
  )
1646
1737
  return f'CAST({val_to_cast} AS {bool_typ})'
1647
1738
  if 'time' in typ.lower() or 'date' in typ.lower():
1648
- return dateadd_str(flavor=flavor, begin='1900-01-01')
1739
+ db_type = typ if typ.isupper() else None
1740
+ return dateadd_str(flavor=flavor, begin='1900-01-01', db_type=db_type)
1649
1741
  if 'float' in typ.lower() or 'double' in typ.lower() or typ.lower() in ('decimal',):
1650
1742
  return '-987654321.0'
1651
1743
  if flavor == 'oracle' and typ.lower().split('(', maxsplit=1)[0] == 'char':
1652
1744
  return "'-987654321'"
1745
+ if flavor == 'oracle' and typ.lower() in ('blob', 'bytes'):
1746
+ return '00'
1653
1747
  if typ.lower() in ('uniqueidentifier', 'guid', 'uuid'):
1654
1748
  magic_val = 'DEADBEEF-ABBA-BABE-CAFE-DECAFC0FFEE5'
1655
1749
  if flavor == 'mssql':
@@ -1857,7 +1951,17 @@ def _get_create_table_query_from_dtypes(
1857
1951
 
1858
1952
  table_name = sql_item_name(new_table, schema=schema, flavor=flavor)
1859
1953
  primary_key_name = sql_item_name(primary_key, flavor) if primary_key else None
1954
+ primary_key_constraint_name = (
1955
+ sql_item_name(f'PK_{new_table}', flavor, None)
1956
+ if primary_key
1957
+ else None
1958
+ )
1860
1959
  datetime_column_name = sql_item_name(datetime_column, flavor) if datetime_column else None
1960
+ primary_key_clustered = (
1961
+ "CLUSTERED"
1962
+ if not datetime_column or datetime_column == primary_key
1963
+ else "NONCLUSTERED"
1964
+ )
1861
1965
  query = f"CREATE TABLE {table_name} ("
1862
1966
  if primary_key:
1863
1967
  col_db_type = cols_types[0][1]
@@ -1877,6 +1981,8 @@ def _get_create_table_query_from_dtypes(
1877
1981
  query += f"\n {col_name} {col_db_type} {auto_increment_str} PRIMARY KEY,"
1878
1982
  elif flavor == 'timescaledb' and datetime_column and datetime_column != primary_key:
1879
1983
  query += f"\n {col_name} {col_db_type}{auto_increment_str} NOT NULL,"
1984
+ elif flavor == 'mssql':
1985
+ query += f"\n {col_name} {col_db_type}{auto_increment_str} NOT NULL,"
1880
1986
  else:
1881
1987
  query += f"\n {col_name} {col_db_type} PRIMARY KEY{auto_increment_str} NOT NULL,"
1882
1988
 
@@ -1892,6 +1998,10 @@ def _get_create_table_query_from_dtypes(
1892
1998
  and datetime_column != primary_key
1893
1999
  ):
1894
2000
  query += f"\n PRIMARY KEY({datetime_column_name}, {primary_key_name}),"
2001
+
2002
+ if flavor == 'mssql' and primary_key:
2003
+ query += f"\n CONSTRAINT {primary_key_constraint_name} PRIMARY KEY {primary_key_clustered} ({primary_key_name}),"
2004
+
1895
2005
  query = query[:-1]
1896
2006
  query += "\n)"
1897
2007
 
@@ -1912,12 +2022,11 @@ def _get_create_table_query_from_cte(
1912
2022
  Create a new table from a CTE query.
1913
2023
  """
1914
2024
  import textwrap
1915
- from meerschaum.utils.dtypes.sql import AUTO_INCREMENT_COLUMN_FLAVORS
1916
2025
  create_cte = 'create_query'
1917
2026
  create_cte_name = sql_item_name(create_cte, flavor, None)
1918
2027
  new_table_name = sql_item_name(new_table, flavor, schema)
1919
2028
  primary_key_constraint_name = (
1920
- sql_item_name(f'pk_{new_table}', flavor, None)
2029
+ sql_item_name(f'PK_{new_table}', flavor, None)
1921
2030
  if primary_key
1922
2031
  else None
1923
2032
  )
@@ -1926,6 +2035,7 @@ def _get_create_table_query_from_cte(
1926
2035
  if primary_key
1927
2036
  else None
1928
2037
  )
2038
+ primary_key_clustered = "CLUSTERED" if not datetime_column else "NONCLUSTERED"
1929
2039
  datetime_column_name = (
1930
2040
  sql_item_name(datetime_column, flavor)
1931
2041
  if datetime_column
@@ -1933,7 +2043,7 @@ def _get_create_table_query_from_cte(
1933
2043
  )
1934
2044
  if flavor in ('mssql',):
1935
2045
  query = query.lstrip()
1936
- if 'with ' in query.lower():
2046
+ if query.lower().startswith('with '):
1937
2047
  final_select_ix = query.lower().rfind('select')
1938
2048
  create_table_query = (
1939
2049
  query[:final_select_ix].rstrip() + ',\n'
@@ -1951,7 +2061,7 @@ def _get_create_table_query_from_cte(
1951
2061
 
1952
2062
  alter_type_query = f"""
1953
2063
  ALTER TABLE {new_table_name}
1954
- ADD CONSTRAINT {primary_key_constraint_name} PRIMARY KEY ({primary_key_name})
2064
+ ADD CONSTRAINT {primary_key_constraint_name} PRIMARY KEY {primary_key_clustered} ({primary_key_name})
1955
2065
  """
1956
2066
  elif flavor in (None,):
1957
2067
  create_table_query = f"""
@@ -1999,11 +2109,11 @@ def _get_create_table_query_from_cte(
1999
2109
  ADD PRIMARY KEY ({primary_key_name})
2000
2110
  """
2001
2111
 
2002
- create_table_query = textwrap.dedent(create_table_query)
2112
+ create_table_query = textwrap.dedent(create_table_query).lstrip().rstrip()
2003
2113
  if not primary_key:
2004
2114
  return [create_table_query]
2005
2115
 
2006
- alter_type_query = textwrap.dedent(alter_type_query)
2116
+ alter_type_query = textwrap.dedent(alter_type_query).lstrip().rstrip()
2007
2117
 
2008
2118
  return [
2009
2119
  create_table_query,
@@ -34,10 +34,10 @@ class Venv:
34
34
  """
35
35
 
36
36
  def __init__(
37
- self,
38
- venv: Union[str, 'meerschaum.plugins.Plugin', None] = 'mrsm',
39
- debug: bool = False,
40
- ) -> None:
37
+ self,
38
+ venv: Union[str, 'meerschaum.plugins.Plugin', None] = 'mrsm',
39
+ debug: bool = False,
40
+ ) -> None:
41
41
  from meerschaum.utils.venv import activate_venv, deactivate_venv, active_venvs
42
42
  ### For some weird threading issue,
43
43
  ### we can't use `isinstance` here.