rococo 1.2.1__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {rococo-1.2.1 → rococo-1.3.0}/PKG-INFO +1 -1
  2. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/common/migration_runner.py +38 -42
  3. rococo-1.3.0/rococo/migrations/common/sql_migration_base.py +215 -0
  4. rococo-1.3.0/rococo/migrations/mysql/migration.py +40 -0
  5. rococo-1.3.0/rococo/migrations/postgres/migration.py +44 -0
  6. {rococo-1.2.1 → rococo-1.3.0}/rococo.egg-info/PKG-INFO +1 -1
  7. {rococo-1.2.1 → rococo-1.3.0}/rococo.egg-info/SOURCES.txt +3 -0
  8. {rococo-1.2.1 → rococo-1.3.0}/setup.py +1 -1
  9. rococo-1.3.0/tests/migration_cli_base_test.py +234 -0
  10. {rococo-1.2.1 → rococo-1.3.0}/tests/migration_runner_test.py +44 -0
  11. rococo-1.3.0/tests/sql_migration_base_test.py +330 -0
  12. rococo-1.2.1/rococo/migrations/mysql/migration.py +0 -112
  13. rococo-1.2.1/rococo/migrations/postgres/migration.py +0 -111
  14. {rococo-1.2.1 → rococo-1.3.0}/LICENSE +0 -0
  15. {rococo-1.2.1 → rococo-1.3.0}/README.md +0 -0
  16. {rococo-1.2.1 → rococo-1.3.0}/rococo/__init__.py +0 -0
  17. {rococo-1.2.1 → rococo-1.3.0}/rococo/auth/__init__.py +0 -0
  18. {rococo-1.2.1 → rococo-1.3.0}/rococo/auth/tokens.py +0 -0
  19. {rococo-1.2.1 → rococo-1.3.0}/rococo/config/__init__.py +0 -0
  20. {rococo-1.2.1 → rococo-1.3.0}/rococo/config/config.py +0 -0
  21. {rococo-1.2.1 → rococo-1.3.0}/rococo/data/__init__.py +0 -0
  22. {rococo-1.2.1 → rococo-1.3.0}/rococo/data/base.py +0 -0
  23. {rococo-1.2.1 → rococo-1.3.0}/rococo/data/dynamodb.py +0 -0
  24. {rococo-1.2.1 → rococo-1.3.0}/rococo/data/mongodb.py +0 -0
  25. {rococo-1.2.1 → rococo-1.3.0}/rococo/data/mysql.py +0 -0
  26. {rococo-1.2.1 → rococo-1.3.0}/rococo/data/postgresql.py +0 -0
  27. {rococo-1.2.1 → rococo-1.3.0}/rococo/data/surrealdb.py +0 -0
  28. {rococo-1.2.1 → rococo-1.3.0}/rococo/emailing/__init__.py +0 -0
  29. {rococo-1.2.1 → rococo-1.3.0}/rococo/emailing/base.py +0 -0
  30. {rococo-1.2.1 → rococo-1.3.0}/rococo/emailing/config.py +0 -0
  31. {rococo-1.2.1 → rococo-1.3.0}/rococo/emailing/enums.py +0 -0
  32. {rococo-1.2.1 → rococo-1.3.0}/rococo/emailing/factory.py +0 -0
  33. {rococo-1.2.1 → rococo-1.3.0}/rococo/emailing/mailjet.py +0 -0
  34. {rococo-1.2.1 → rococo-1.3.0}/rococo/emailing/ses.py +0 -0
  35. {rococo-1.2.1 → rococo-1.3.0}/rococo/faxing/__init__.py +0 -0
  36. {rococo-1.2.1 → rococo-1.3.0}/rococo/faxing/base.py +0 -0
  37. {rococo-1.2.1 → rococo-1.3.0}/rococo/faxing/config.py +0 -0
  38. {rococo-1.2.1 → rococo-1.3.0}/rococo/faxing/enums.py +0 -0
  39. {rococo-1.2.1 → rococo-1.3.0}/rococo/faxing/factory.py +0 -0
  40. {rococo-1.2.1 → rococo-1.3.0}/rococo/faxing/ifax.py +0 -0
  41. {rococo-1.2.1 → rococo-1.3.0}/rococo/messaging/__init__.py +0 -0
  42. {rococo-1.2.1 → rococo-1.3.0}/rococo/messaging/base.py +0 -0
  43. {rococo-1.2.1 → rococo-1.3.0}/rococo/messaging/rabbitmq.py +0 -0
  44. {rococo-1.2.1 → rococo-1.3.0}/rococo/messaging/sqs.py +0 -0
  45. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/__init__.py +0 -0
  46. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/common/__init__.py +0 -0
  47. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/common/cli_base.py +0 -0
  48. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/common/migration_base.py +0 -0
  49. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/common/migration_template.py +0 -0
  50. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/mongo/__init__.py +0 -0
  51. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/mongo/cli.py +0 -0
  52. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/mongo/migration.py +0 -0
  53. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/mysql/__init__.py +0 -0
  54. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/mysql/cli.py +0 -0
  55. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/postgres/__init__.py +0 -0
  56. {rococo-1.2.1 → rococo-1.3.0}/rococo/migrations/postgres/cli.py +0 -0
  57. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/__init__.py +0 -0
  58. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/email.py +0 -0
  59. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/login_method.py +0 -0
  60. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/organization.py +0 -0
  61. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/otp_method.py +0 -0
  62. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/person.py +0 -0
  63. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/person_organization_role.py +0 -0
  64. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/recovery_code.py +0 -0
  65. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/__init__.py +0 -0
  66. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/email.py +0 -0
  67. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/login_method.py +0 -0
  68. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/organization.py +0 -0
  69. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/otp_method.py +0 -0
  70. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/person.py +0 -0
  71. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/person_organization_role.py +0 -0
  72. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/surrealdb/surreal_versioned_model.py +0 -0
  73. {rococo-1.2.1 → rococo-1.3.0}/rococo/models/versioned_model.py +0 -0
  74. {rococo-1.2.1 → rococo-1.3.0}/rococo/plugins/__init__.py +0 -0
  75. {rococo-1.2.1 → rococo-1.3.0}/rococo/plugins/pooled_connection.py +0 -0
  76. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/__init__.py +0 -0
  77. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/base_repository.py +0 -0
  78. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/dynamodb/__init__.py +0 -0
  79. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/dynamodb/dynamodb_repository.py +0 -0
  80. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/mongodb/__init__.py +0 -0
  81. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/mongodb/mongodb_repository.py +0 -0
  82. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/mysql/__init__.py +0 -0
  83. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/mysql/mysql_repository.py +0 -0
  84. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/mysql/organization_repository.py +0 -0
  85. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/postgresql/__init__.py +0 -0
  86. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/postgresql/postgresql_repository.py +0 -0
  87. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/surrealdb/__init__.py +0 -0
  88. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/surrealdb/organization_repository.py +0 -0
  89. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/surrealdb/person_organization_role_repository.py +0 -0
  90. {rococo-1.2.1 → rococo-1.3.0}/rococo/repositories/surrealdb/surreal_db_repository.py +0 -0
  91. {rococo-1.2.1 → rococo-1.3.0}/rococo/sms/__init__.py +0 -0
  92. {rococo-1.2.1 → rococo-1.3.0}/rococo/sms/base.py +0 -0
  93. {rococo-1.2.1 → rococo-1.3.0}/rococo/sms/config.py +0 -0
  94. {rococo-1.2.1 → rococo-1.3.0}/rococo/sms/enums.py +0 -0
  95. {rococo-1.2.1 → rococo-1.3.0}/rococo/sms/factory.py +0 -0
  96. {rococo-1.2.1 → rococo-1.3.0}/rococo/sms/twilio.py +0 -0
  97. {rococo-1.2.1 → rococo-1.3.0}/rococo.egg-info/dependency_links.txt +0 -0
  98. {rococo-1.2.1 → rococo-1.3.0}/rococo.egg-info/entry_points.txt +0 -0
  99. {rococo-1.2.1 → rococo-1.3.0}/rococo.egg-info/requires.txt +0 -0
  100. {rococo-1.2.1 → rococo-1.3.0}/rococo.egg-info/top_level.txt +0 -0
  101. {rococo-1.2.1 → rococo-1.3.0}/setup.cfg +0 -0
  102. {rococo-1.2.1 → rococo-1.3.0}/tests/__init__.py +0 -0
  103. {rococo-1.2.1 → rococo-1.3.0}/tests/base_repository_test.py +0 -0
  104. {rococo-1.2.1 → rococo-1.3.0}/tests/config_test.py +0 -0
  105. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/__init__.py +0 -0
  106. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/conftest.py +0 -0
  107. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/test_dynamodb_integration.py +0 -0
  108. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/test_models.py +0 -0
  109. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/test_mongodb_integration.py +0 -0
  110. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/test_mysql_integration.py +0 -0
  111. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/test_postgres_integration.py +0 -0
  112. {rococo-1.2.1 → rococo-1.3.0}/tests/database-integration/test_surrealdb_integration.py +0 -0
  113. {rococo-1.2.1 → rococo-1.3.0}/tests/dynamodb_repository_test.py +0 -0
  114. {rococo-1.2.1 → rococo-1.3.0}/tests/model_test.py +0 -0
  115. {rococo-1.2.1 → rococo-1.3.0}/tests/mongodb_repository_test.py +0 -0
  116. {rococo-1.2.1 → rococo-1.3.0}/tests/mysql_repository_test.py +0 -0
  117. {rococo-1.2.1 → rococo-1.3.0}/tests/postgre_repository_test.py +0 -0
  118. {rococo-1.2.1 → rococo-1.3.0}/tests/surrealdb_repository_test.py +0 -0
  119. {rococo-1.2.1 → rococo-1.3.0}/tests/test_calculated_properties_fix.py +0 -0
  120. {rococo-1.2.1 → rococo-1.3.0}/tests/test_faxing.py +0 -0
  121. {rococo-1.2.1 → rococo-1.3.0}/tests/test_mailjet_addresses.py +0 -0
  122. {rococo-1.2.1 → rococo-1.3.0}/tests/test_non_versioned_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rococo
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: A Python library to help build things the way we want them built
5
5
  Home-page: https://github.com/EcorRouge/rococo
6
6
  Author: Jay Grieves
@@ -1,10 +1,8 @@
1
1
  import os
2
2
  import logging
3
+ import importlib
3
4
  from importlib import import_module
4
- # Assuming migration_template.py is in the same directory or accessible in python path
5
- # For a common module, it would typically be:
6
- # from rococo.migrations.common.migration_template import get_template
7
- from .migration_template import get_template # This relative import is fine if it's part of a package
5
+ from .migration_template import get_template
8
6
 
9
7
  class MigrationRunner:
10
8
  def __init__(self, migrations_dir, migration):
@@ -150,42 +148,40 @@ class MigrationRunner:
150
148
  print(f"Error creating migration file: {e}")
151
149
 
152
150
  def run_forward_migration_script(self, initial_db_version_for_run):
153
- # Renamed old_db_version to initial_db_version_for_run for clarity
154
- current_script_filename = self._get_forward_migration_script()
155
-
156
- if current_script_filename is None:
157
- latest_db_version_after_ops = self.get_db_version()
158
- if initial_db_version_for_run == latest_db_version_after_ops:
159
- # This means no scripts were found that could run from initial_db_version_for_run
160
- logging.info(f'No forward migration scripts found for current DB version: {initial_db_version_for_run}. Database is likely up to date.')
161
- else:
162
- # This means some migrations ran, and now no more are found.
163
- logging.info('All pending forward migrations complete!')
164
- logging.info(f'Latest DB version: {latest_db_version_after_ops}')
165
- return # End of migration run
166
-
167
- # Using os.path.basename to be platform-agnostic, though split('/') works on POSIX
168
- logging.info(f'Running forward migration: {os.path.basename(current_script_filename)}.py')
169
-
170
- try:
171
- # Dynamically import the migration module
172
- # Assumes migrations_dir is in sys.path (BaseCli handles this)
173
- module_name = current_script_filename
174
- migration_module = import_module(module_name) # Assumes migrations_dir is in sys.path
175
- migration_module.upgrade(self.migration)
176
- except Exception as e:
177
- logging.error(f"Error during forward migration {current_script_filename}.py: {e}", exc_info=True)
178
- # This print is for direct CLI feedback during a run
179
- print(f"Error applying migration {current_script_filename}.py. Halting.")
180
- # Potentially offer to show current DB version or suggest manual check
181
- print(f"Current DB version after error: {self.get_db_version()}")
182
- return # Stop further migrations
183
-
184
- # Recursively run next forward migration
185
- # The `initial_db_version_for_run` remains the version from the start of this `rf` command.
186
- # The next call to `_get_forward_migration_script` will use the *new* current DB version
187
- # (updated by the migration script that just ran).
188
- return self.run_forward_migration_script(initial_db_version_for_run)
151
+ # Iterates one migration at a time, re-resolving the next forward script
152
+ # against the DB version updated by the just-applied script. Iterative to
153
+ # avoid stack overflow on deep migration histories.
154
+ while True:
155
+ current_script_filename = self._get_forward_migration_script()
156
+
157
+ if current_script_filename is None:
158
+ latest_db_version_after_ops = self.get_db_version()
159
+ if initial_db_version_for_run == latest_db_version_after_ops:
160
+ logging.info(
161
+ f'No forward migration scripts found for current DB version: '
162
+ f'{initial_db_version_for_run}. Database is likely up to date.'
163
+ )
164
+ else:
165
+ logging.info('All pending forward migrations complete!')
166
+ logging.info(f'Latest DB version: {latest_db_version_after_ops}')
167
+ return
168
+
169
+ logging.info(
170
+ f'Running forward migration: {os.path.basename(current_script_filename)}.py'
171
+ )
172
+
173
+ try:
174
+ importlib.invalidate_caches()
175
+ migration_module = import_module(current_script_filename)
176
+ migration_module.upgrade(self.migration)
177
+ except Exception as e:
178
+ logging.error(
179
+ f"Error during forward migration {current_script_filename}.py: {e}",
180
+ exc_info=True,
181
+ )
182
+ print(f"Error applying migration {current_script_filename}.py. Halting.")
183
+ print(f"Current DB version after error: {self.get_db_version()}")
184
+ return
189
185
 
190
186
  def run_backward_migration_script(self):
191
187
  current_script_filename = self._get_backward_migration_script()
@@ -197,8 +193,8 @@ class MigrationRunner:
197
193
  logging.info(f'Running backward migration: {os.path.basename(current_script_filename)}.py')
198
194
 
199
195
  try:
200
- module_name = current_script_filename
201
- migration_module = import_module(module_name)
196
+ importlib.invalidate_caches()
197
+ migration_module = import_module(current_script_filename)
202
198
  migration_module.downgrade(self.migration)
203
199
  except Exception as e:
204
200
  logging.error(f"Error during backward migration {current_script_filename}.py: {e}", exc_info=True)
@@ -0,0 +1,215 @@
1
+ import logging
2
+ import re
3
+ from typing import Any, Iterable, Optional
4
+
5
+ from .migration_base import MigrationBase
6
+
7
+
8
+ class SQLMigrationBase(MigrationBase):
9
+ """Shared SQL migration helpers for relational adapters.
10
+
11
+ Concrete subclasses (e.g. PostgresMigration, MySQLMigration) provide the
12
+ dialect-specific SQL via class-level templates and queries. Identifiers
13
+ (table, column, index, constraint names) are validated against
14
+ ``_IDENT_RE`` before being interpolated into SQL. Values (database name,
15
+ version strings) are bound through the adapter's parameterized query
16
+ interface, never concatenated into SQL strings.
17
+ """
18
+
19
+ # Parameterized queries (use %s placeholders).
20
+ EXISTS_COLUMN_QUERY: str = NotImplemented # args: (database, table, column)
21
+ EXISTS_PK_QUERY: str = NotImplemented # args: (database, table)
22
+ UPDATE_VERSION_QUERY: str = NotImplemented # args: (version,)
23
+ INSERT_DB_VERSION_DATA_QUERY: str = NotImplemented # no args
24
+
25
+ # Identifier templates (use str.format with validated names).
26
+ DROP_PK_TEMPLATE: str = NotImplemented # {table}
27
+ ADD_PK_TEMPLATE: str = NotImplemented # {table}, {keys}
28
+ ADD_COLUMN_TEMPLATE: str = NotImplemented # {table}, {column}, {datatype}
29
+ DROP_COLUMN_TEMPLATE: str = NotImplemented # {table}, {column}
30
+ ADD_INDEX_TEMPLATE: str = NotImplemented # {table}, {index}, {column}
31
+ REMOVE_INDEX_TEMPLATE: str = NotImplemented # {table}, {index}
32
+ ALTER_INDEX_TEMPLATES: tuple = NotImplemented # tuple of templates run in order
33
+ ALTER_COLUMN_TEMPLATE: str = NotImplemented # {table}, {column}, {datatype}
34
+ RENAME_COLUMN_TEMPLATE: str = NotImplemented # {table}, {old}, {new}
35
+ RENAME_TABLE_TEMPLATE: str = NotImplemented # {old_table}, {new_table}
36
+ CREATE_TABLE_TEMPLATE: str = NotImplemented # {table}, {fields_with_type}
37
+ DROP_TABLE_IF_EXISTS_TEMPLATE: str = NotImplemented # {table}
38
+ DROP_TABLE_TEMPLATE: str = NotImplemented # {table}
39
+
40
+ _IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
41
+
42
+ def _validate_ident(self, name: str) -> str:
43
+ if not isinstance(name, str) or not self._IDENT_RE.match(name):
44
+ raise ValueError(f"invalid SQL identifier: {name!r}")
45
+ return name
46
+
47
+ def _validate_idents(self, names: Iterable[str]) -> None:
48
+ for n in names:
49
+ self._validate_ident(n)
50
+
51
+ def _validate_pk_keys(self, keys: str) -> str:
52
+ if not isinstance(keys, str) or not (keys.startswith("(") and keys.endswith(")")):
53
+ raise ValueError(
54
+ f"primary key keys must be wrapped in parentheses, got: {keys!r}"
55
+ )
56
+ inner = keys[1:-1]
57
+ for col in inner.split(","):
58
+ self._validate_ident(col.strip())
59
+ return keys
60
+
61
+ @staticmethod
62
+ def _extract_count(response: Any) -> int:
63
+ if not response:
64
+ return 0
65
+ row = response[0] if isinstance(response, list) else response
66
+ if not isinstance(row, dict) or not row:
67
+ return 0
68
+ value = next(iter(row.values()))
69
+ return int(value or 0)
70
+
71
+ def _does_column_exist(self, table_name, column_name, commit: bool = True) -> bool:
72
+ self._validate_idents([table_name, column_name])
73
+ rows = self.execute(
74
+ self.EXISTS_COLUMN_QUERY,
75
+ commit=commit,
76
+ args=(self.db_adapter._database, table_name, column_name),
77
+ )
78
+ return self._extract_count(rows) > 0
79
+
80
+ def _does_primary_key_constraint_exists(self, table_name, commit: bool = True) -> bool:
81
+ self._validate_ident(table_name)
82
+ rows = self.execute(
83
+ self.EXISTS_PK_QUERY,
84
+ commit=commit,
85
+ args=(self.db_adapter._database, table_name),
86
+ )
87
+ return self._extract_count(rows) > 0
88
+
89
+ def remove_primary_key(self, table_name, commit: bool = True):
90
+ self._validate_ident(table_name)
91
+ if self._does_primary_key_constraint_exists(table_name, commit=commit):
92
+ return self.execute(
93
+ self.DROP_PK_TEMPLATE.format(table=table_name), commit=commit
94
+ )
95
+ logging.info(
96
+ f"PRIMARY KEY constraint for table {table_name} does not exist. "
97
+ f"Skipping removal..."
98
+ )
99
+
100
+ def add_primary_key(self, table_name, keys: str, commit: bool = True):
101
+ self._validate_ident(table_name)
102
+ self._validate_pk_keys(keys)
103
+ if self._does_primary_key_constraint_exists(table_name, commit=commit):
104
+ self.execute(
105
+ self.DROP_PK_TEMPLATE.format(table=table_name), commit=commit
106
+ )
107
+ return self.execute(
108
+ self.ADD_PK_TEMPLATE.format(table=table_name, keys=keys), commit=commit
109
+ )
110
+
111
+ def add_column(self, table_name, column_name, datatype, commit: bool = True):
112
+ self._validate_idents([table_name, column_name])
113
+ if self._does_column_exist(table_name, column_name, commit=commit):
114
+ logging.info(
115
+ f"Column {column_name} for table {table_name} already exists. "
116
+ f"Skipping creation..."
117
+ )
118
+ return
119
+ query = self.ADD_COLUMN_TEMPLATE.format(
120
+ table=table_name, column=column_name, datatype=datatype
121
+ )
122
+ self.execute(query, commit=commit)
123
+
124
+ def drop_column(self, table_name, column_name, commit: bool = True):
125
+ self._validate_idents([table_name, column_name])
126
+ if not self._does_column_exist(table_name, column_name, commit=commit):
127
+ logging.info(
128
+ f"Column {column_name} for table {table_name} does not exist. "
129
+ f"Skipping deletion..."
130
+ )
131
+ return
132
+ query = self.DROP_COLUMN_TEMPLATE.format(
133
+ table=table_name, column=column_name
134
+ )
135
+ self.execute(query, commit=commit)
136
+
137
+ def alter_index(
138
+ self,
139
+ table_name,
140
+ new_index_name,
141
+ new_indexed_column,
142
+ old_index_name,
143
+ commit: bool = True,
144
+ ):
145
+ self._validate_idents(
146
+ [table_name, new_index_name, new_indexed_column, old_index_name]
147
+ )
148
+ for tmpl in self.ALTER_INDEX_TEMPLATES:
149
+ query = tmpl.format(
150
+ table=table_name,
151
+ new_index=new_index_name,
152
+ new_column=new_indexed_column,
153
+ old_index=old_index_name,
154
+ )
155
+ self.execute(query, commit=commit)
156
+
157
+ def add_index(self, table_name, index_name, indexed_column, commit: bool = True):
158
+ self._validate_idents([table_name, index_name, indexed_column])
159
+ query = self.ADD_INDEX_TEMPLATE.format(
160
+ table=table_name, index=index_name, column=indexed_column
161
+ )
162
+ self.execute(query, commit=commit)
163
+
164
+ def remove_index(self, table_name, index_name, commit: bool = True):
165
+ self._validate_idents([table_name, index_name])
166
+ query = self.REMOVE_INDEX_TEMPLATE.format(
167
+ table=table_name, index=index_name
168
+ )
169
+ self.execute(query, commit=commit)
170
+
171
+ def alter_column(self, table_name, column_name, datatype, commit: bool = True):
172
+ self._validate_idents([table_name, column_name])
173
+ query = self.ALTER_COLUMN_TEMPLATE.format(
174
+ table=table_name, column=column_name, datatype=datatype
175
+ )
176
+ self.execute(query, commit=commit)
177
+
178
+ def change_column_name(
179
+ self, table_name, old_column_name, new_column_name, commit: bool = True
180
+ ):
181
+ self._validate_idents([table_name, old_column_name, new_column_name])
182
+ query = self.RENAME_COLUMN_TEMPLATE.format(
183
+ table=table_name, old=old_column_name, new=new_column_name
184
+ )
185
+ self.execute(query, commit=commit)
186
+
187
+ def change_table_name(self, old_table, new_table, commit: bool = True):
188
+ self._validate_idents([old_table, new_table])
189
+ query = self.RENAME_TABLE_TEMPLATE.format(
190
+ old_table=old_table, new_table=new_table
191
+ )
192
+ self.execute(query, commit=commit)
193
+
194
+ def create_table(
195
+ self, table_name, fields_with_type, drop_if_exists: bool = True, commit: bool = True
196
+ ):
197
+ self._validate_ident(table_name)
198
+ if drop_if_exists:
199
+ drop_query = self.DROP_TABLE_IF_EXISTS_TEMPLATE.format(table=table_name)
200
+ self.execute(drop_query, commit=commit)
201
+ create_query = self.CREATE_TABLE_TEMPLATE.format(
202
+ table=table_name, fields_with_type=fields_with_type
203
+ )
204
+ self.execute(create_query, commit=commit)
205
+
206
+ def drop_table(self, table_name, commit: bool = True):
207
+ self._validate_ident(table_name)
208
+ query = self.DROP_TABLE_TEMPLATE.format(table=table_name)
209
+ self.execute(query, commit=commit)
210
+
211
+ def insert_db_version_data(self, commit: bool = True):
212
+ self.execute(self.INSERT_DB_VERSION_DATA_QUERY, commit=commit)
213
+
214
+ def update_version_table(self, version, commit: bool = True):
215
+ self.execute(self.UPDATE_VERSION_QUERY, commit=commit, args=(str(version),))
@@ -0,0 +1,40 @@
1
+ from ..common.sql_migration_base import SQLMigrationBase
2
+ from rococo.data.mysql import MySqlAdapter
3
+
4
+
5
+ class MySQLMigration(SQLMigrationBase):
6
+ EXISTS_COLUMN_QUERY = (
7
+ "SELECT COUNT(*) AS count "
8
+ "FROM information_schema.COLUMNS "
9
+ "WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s;"
10
+ )
11
+ EXISTS_PK_QUERY = (
12
+ "SELECT COUNT(*) AS count "
13
+ "FROM information_schema.TABLE_CONSTRAINTS "
14
+ "WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s "
15
+ "AND CONSTRAINT_TYPE = 'PRIMARY KEY';"
16
+ )
17
+ UPDATE_VERSION_QUERY = "UPDATE db_version SET version = %s;"
18
+ INSERT_DB_VERSION_DATA_QUERY = (
19
+ "INSERT INTO db_version (version) VALUES ('0000000000');"
20
+ )
21
+
22
+ DROP_PK_TEMPLATE = "ALTER TABLE {table} DROP PRIMARY KEY;"
23
+ ADD_PK_TEMPLATE = "ALTER TABLE {table} ADD PRIMARY KEY {keys};"
24
+ ADD_COLUMN_TEMPLATE = "ALTER TABLE {table} ADD {column} {datatype};"
25
+ DROP_COLUMN_TEMPLATE = "ALTER TABLE {table} DROP {column};"
26
+ ADD_INDEX_TEMPLATE = "ALTER TABLE {table} ADD INDEX {index} ({column});"
27
+ REMOVE_INDEX_TEMPLATE = "ALTER TABLE {table} DROP INDEX {index};"
28
+ ALTER_INDEX_TEMPLATES = (
29
+ "ALTER TABLE {table} ADD INDEX {new_index} ({new_column}), "
30
+ "DROP INDEX {old_index};",
31
+ )
32
+ ALTER_COLUMN_TEMPLATE = "ALTER TABLE {table} MODIFY COLUMN {column} {datatype};"
33
+ RENAME_COLUMN_TEMPLATE = "ALTER TABLE {table} RENAME COLUMN {old} TO {new};"
34
+ RENAME_TABLE_TEMPLATE = "ALTER TABLE {old_table} RENAME TO {new_table};"
35
+ CREATE_TABLE_TEMPLATE = "CREATE TABLE {table} ({fields_with_type});"
36
+ DROP_TABLE_IF_EXISTS_TEMPLATE = "DROP TABLE IF EXISTS {table};"
37
+ DROP_TABLE_TEMPLATE = "DROP TABLE {table};"
38
+
39
+ def __init__(self, db_adapter: MySqlAdapter):
40
+ super().__init__(db_adapter)
@@ -0,0 +1,44 @@
1
+ from ..common.sql_migration_base import SQLMigrationBase
2
+ from rococo.data.postgresql import PostgreSQLAdapter
3
+
4
+
5
+ class PostgresMigration(SQLMigrationBase):
6
+ EXISTS_COLUMN_QUERY = (
7
+ "SELECT COUNT(*) AS count "
8
+ "FROM information_schema.columns "
9
+ "WHERE table_catalog = %s AND table_name = %s AND column_name = %s;"
10
+ )
11
+ EXISTS_PK_QUERY = (
12
+ "SELECT COUNT(*) AS count "
13
+ "FROM information_schema.table_constraints "
14
+ "WHERE table_catalog = %s AND table_name = %s "
15
+ "AND constraint_type = 'PRIMARY KEY';"
16
+ )
17
+ UPDATE_VERSION_QUERY = "UPDATE db_version SET version = %s;"
18
+ INSERT_DB_VERSION_DATA_QUERY = (
19
+ "INSERT INTO db_version (version) VALUES ('0000000000');"
20
+ )
21
+
22
+ DROP_PK_TEMPLATE = (
23
+ "ALTER TABLE {table} DROP CONSTRAINT IF EXISTS {table}_pkey;"
24
+ )
25
+ ADD_PK_TEMPLATE = (
26
+ "ALTER TABLE {table} ADD CONSTRAINT {table}_pkey PRIMARY KEY {keys};"
27
+ )
28
+ ADD_COLUMN_TEMPLATE = "ALTER TABLE {table} ADD COLUMN {column} {datatype};"
29
+ DROP_COLUMN_TEMPLATE = "ALTER TABLE {table} DROP COLUMN {column};"
30
+ ADD_INDEX_TEMPLATE = "CREATE INDEX {index} ON {table} ({column});"
31
+ REMOVE_INDEX_TEMPLATE = "DROP INDEX IF EXISTS {index};"
32
+ ALTER_INDEX_TEMPLATES = (
33
+ "DROP INDEX IF EXISTS {old_index};",
34
+ "CREATE INDEX {new_index} ON {table} ({new_column});",
35
+ )
36
+ ALTER_COLUMN_TEMPLATE = "ALTER TABLE {table} ALTER COLUMN {column} TYPE {datatype};"
37
+ RENAME_COLUMN_TEMPLATE = "ALTER TABLE {table} RENAME COLUMN {old} TO {new};"
38
+ RENAME_TABLE_TEMPLATE = "ALTER TABLE {old_table} RENAME TO {new_table};"
39
+ CREATE_TABLE_TEMPLATE = "CREATE TABLE {table} ({fields_with_type});"
40
+ DROP_TABLE_IF_EXISTS_TEMPLATE = "DROP TABLE IF EXISTS {table} CASCADE;"
41
+ DROP_TABLE_TEMPLATE = "DROP TABLE {table} CASCADE;"
42
+
43
+ def __init__(self, db_adapter: PostgreSQLAdapter):
44
+ super().__init__(db_adapter)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rococo
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: A Python library to help build things the way we want them built
5
5
  Home-page: https://github.com/EcorRouge/rococo
6
6
  Author: Jay Grieves
@@ -42,6 +42,7 @@ rococo/migrations/common/cli_base.py
42
42
  rococo/migrations/common/migration_base.py
43
43
  rococo/migrations/common/migration_runner.py
44
44
  rococo/migrations/common/migration_template.py
45
+ rococo/migrations/common/sql_migration_base.py
45
46
  rococo/migrations/mongo/__init__.py
46
47
  rococo/migrations/mongo/cli.py
47
48
  rococo/migrations/mongo/migration.py
@@ -95,11 +96,13 @@ tests/__init__.py
95
96
  tests/base_repository_test.py
96
97
  tests/config_test.py
97
98
  tests/dynamodb_repository_test.py
99
+ tests/migration_cli_base_test.py
98
100
  tests/migration_runner_test.py
99
101
  tests/model_test.py
100
102
  tests/mongodb_repository_test.py
101
103
  tests/mysql_repository_test.py
102
104
  tests/postgre_repository_test.py
105
+ tests/sql_migration_base_test.py
103
106
  tests/surrealdb_repository_test.py
104
107
  tests/test_calculated_properties_fix.py
105
108
  tests/test_faxing.py
@@ -64,7 +64,7 @@ extras_require["all"] = [
64
64
 
65
65
  setup(
66
66
  name='rococo',
67
- version='1.2.1',
67
+ version='1.3.0',
68
68
  packages=find_packages(),
69
69
  url='https://github.com/EcorRouge/rococo',
70
70
  license='MIT',