limber-timber 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daniel Tashjian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.3
2
+ Name: limber-timber
3
+ Version: 0.0.1
4
+ Summary: Database Migrations Made Easy
5
+ License: MIT
6
+ Author: Daniel Tashjian
7
+ Author-email: thewopple@gmail.com
8
+ Requires-Python: >=3.10
9
+ Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Framework :: Pydantic :: 2
11
+ Classifier: Framework :: Pytest
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Dist: devtools (==0.12.2)
20
+ Requires-Dist: google-cloud-bigquery (==3.34.0)
21
+ Requires-Dist: pydantic (==2.11.4)
22
+ Requires-Dist: pyyaml (==6.0.2)
23
+ Project-URL: Homepage, https://github.com/Wopple/limber-timber
24
+ Project-URL: Repository, https://github.com/Wopple/limber-timber
25
+ Description-Content-Type: text/markdown
26
+
27
+ Limber Timber
28
+ =============
29
+
30
+ ***Database Migrations Made Easy***
31
+
32
+ ## Overview
33
+
34
+ This project is not ready for production use, consider it v0.0.0.
35
+
36
+ I am writing the migration system I always wanted but does not exist (yet).
37
+
38
+ ## Notable Feature Goals
39
+
40
+ - Migrations specified in data, not SQL
41
+ - Down migrations automatically inferred from up migrations
42
+ - Yes, down migrations for drop table and drop column are automatically inferred
43
+ - Separation of database and metadata
44
+ - In-memory support
45
+ - Database adoption
46
+ - No checksums
47
+ - Manifest instead of numbered or timestamped migration filenames
48
+ - JSON schema for migration files
49
+ - Error recovery for backends that do not support DDL transactions
50
+
51
+ ## Not Goals
52
+
53
+ - ORM
54
+ - Parsing SQL
55
+ - Specifying all kinds of migrations in pure data (e.g. DML migrations will use SQL)
56
+ - Preserving lost data
57
+
58
+ ## Rationale
59
+
60
+ - It is cumbersome to iterate on migrations without robust down migrations
61
+ - Automatically inferred down migrations reduces developer burden
62
+ - Writing migrations in data is cleaner and not specific to a database
63
+ - Never parsing SQL reduces the complexity of the codebase
64
+ - A lightweight open source library makes it each to add missing features
65
+ - JSON schema allows IDEs to be configured for migration file validation and auto-completion
66
+ - Separation of database and metadata allows for more flexible metadata storage options
67
+ - In-memory database and metadata allow for application unit testing
68
+ - No checksums allows for modifying migration files without breaking the migrations
69
+ - Manifest files cause git merge conflicts when parallel development has collisions
70
+
71
+ ## Roadmap
72
+
73
+ These are listed in rough priority order if you are interested in contributing.
74
+
75
+ - ✅ CLI
76
+ - ➡️ Publish to PyPI
77
+ - ➡️ Github Actions
78
+ - ➡️ Unit Tests
79
+ - ➡️ Release
80
+ - ✅ In-memory Database
81
+ - ✅ In-memory Metadata
82
+ - ➡️ Big Query Database
83
+ - ✅ Create Table
84
+ - ✅ Drop Table
85
+ - ✅ Rename Table
86
+ - ✅ Set Table Partition Expiration
87
+ - ✅ Set Table Clustering
88
+ - ✅ Add Column
89
+ - ✅ Drop Column
90
+ - ✅ Rename Column
91
+ - ➡️ Alter Column
92
+ - ➡️ Create View
93
+ - ➡️ Create Materialized View
94
+ - ➡️ Create Snapshot Table
95
+ - ➡️ Create Table Clone
96
+ - ➡️ Labels
97
+ - ➡️ Options
98
+ - ✅ Big Query Metadata
99
+ - ➡️ Database Adoption
100
+ - ➡️ JSON Schema
101
+ - ✅ Database Specific Validation
102
+ - ➡️ Templating
103
+ - ➡️ Expand Grouped Operations
104
+ - ➡️ Grouped Operation Application
105
+ - ➡️ Minimize Scan Output
106
+ - ➡️ Arbitrary DML SQL Migrations
107
+ - ➡️ File System Metadata
108
+ - ➡️ SQLite Database
109
+ - ➡️ SQLite Metadata
110
+ - ➡️ Postgres Database
111
+ - ➡️ Postgres Metadata
112
+ - ➡️ MySQL Database
113
+ - ➡️ MySQL Metadata
114
+
115
+ ## Usage
116
+
117
+ ### Create Migrations
118
+
119
+ 1. Create your target manifest
120
+
121
+ > Note: All migration files can use any of these extensions:
122
+ > - `.json`
123
+ > - `.yaml`
124
+ > - `.yml`
125
+
126
+ Create a target directory with a manifest file named `manifest.yaml`.
127
+
128
+ ```yaml
129
+ # target_dir/manifest.yaml
130
+ version: 1
131
+ operation_files:
132
+ - path/to/create_user_table.yaml
133
+ - path/to/enrich_user_name.json
134
+ ```
135
+
136
+ 2. Create your target migration operations
137
+
138
+ > Tip: Using a subdirectory for the operations files makes it easy to configure your IDE to apply the correct JSON schema.
139
+
140
+ Create the files listed in your manifest.
141
+
142
+ ```yaml
143
+ # target_dir/path/to/create_user_table.yaml
144
+ version: 1
145
+ operations:
146
+ - kind: create_table
147
+ data:
148
+ table:
149
+ name:
150
+ database: your_project
151
+ schema_name: your_dataset
152
+ table_name: users
153
+ columns:
154
+ - name: id
155
+ data_type: INT64
156
+ - name: name
157
+ data_type: STRING
158
+ ```
159
+
160
+ ```yaml
161
+ # target_dir/path/to/enrich_user_name.yaml
162
+ version: 1
163
+ operations:
164
+ - kind: rename_column
165
+ data:
166
+ table_name:
167
+ database: your_project
168
+ schema_name: your_dataset
169
+ table_name: users
170
+ from_name: name
171
+ to_name: firstname
172
+ - kind: add_column
173
+ data:
174
+ table_name:
175
+ database: your_project
176
+ schema_name: your_dataset
177
+ table_name: users
178
+ column:
179
+ name: lastname
180
+ data_type: STRING
181
+ ```
182
+
183
+ 3. Check what migrations will run
184
+
185
+ ```shell
186
+ poetry run liti migrate \
187
+ -t target_dir \
188
+ --db bigquery \
189
+ --meta bigquery \
190
+ --meta-table-name your_project.your_dataset._migrations
191
+ ```
192
+
193
+ 4. Run the migrations
194
+
195
+ ```shell
196
+ poetry run liti migrate -w \
197
+ -t target_dir \
198
+ --db bigquery \
199
+ --meta bigquery \
200
+ --meta-table-name your_project.your_dataset._migrations
201
+ ```
202
+
203
+ ### Scan Database
204
+
205
+ You can also scan a schema / table which will print out the operations file that generates that schema / table.
206
+
207
+ ```shell
208
+ # scan a schema
209
+ poetry run liti scan \
210
+ --db bigquery \
211
+ --scan-database your_project \
212
+ --scan-schema your_dataset
213
+ ```
214
+
215
+ ```shell
216
+ # scan a table
217
+ poetry run liti scan \
218
+ --db bigquery \
219
+ --scan-database your_project \
220
+ --scan-schema your_dataset \
221
+ --scan-table your_table
222
+ ```
223
+
224
+ ## Learn
225
+
226
+ Being completely new to this project, you will have no idea where to start. Here. This is where you start. This is a
227
+ crash course on what Limber Timber is and how its put together.
228
+
229
+ ### The Big Picture
230
+
231
+ Limber Timber uses the `Operation` to describe changes to a database. These operations are pure data. They can be
232
+ serialized to JSON or YAML, and can be deserialized from the same. Developers write JSON or YAML files to describe the
233
+ migrations for their application.
234
+
235
+ The `Operation` can be enhanced to become an `OperationOps`. This type brings behavior to the data. It allows you to:
236
+ - check if the operation has been applied to the database
237
+ - useful for recovery from a failure between applying an operation and writing it to the metadata
238
+ - apply the operation, i.e. the "up" migration
239
+ - produce the inverse `Operation` that will perform the "down" migration
240
+
241
+ Down migrations are inferred from the up migrations, so developers only ever have to write the up migrations.
242
+
243
+ ### Migration Files
244
+
245
+ Migration files start with a manifest file. The manifest points to the operation files in the order they should be
246
+ applied. Each operation file contains a list of operations in the order they should be applied. In this way,
247
+ ```
248
+ # file1
249
+ [op1, op2]
250
+
251
+ # file2
252
+ [op3]
253
+ ```
254
+ is exactly the same as:
255
+ ```
256
+ # file1
257
+ [op1]
258
+
259
+ # file2
260
+ [op2, op3]
261
+ ```
262
+
263
+ The migrational unit is the `Operation`, not the file. Grouping operations into files can help for organization, but
264
+ having a single file with all operations or many files each with one operation are both valid. There are no checksums
265
+ and no need to specially name your files. You can also organize your migrations with sub-directories, just specify the
266
+ paths in the manifest.
267
+
268
+ One major benefit to this system is if parallel developers add operations, one will merge first, and then the other will
269
+ get a merge conflict. This is much better than having migrations applied out of order (or breaking) after the fact. You
270
+ learn right away about the conflict, and the developer is prompted to resolve it. This benefit assumes all developers
271
+ are using the same style for adding new migrations: either adding a new file to the manifest, or adding a new operation
272
+ to the most recent file.
273
+
274
+ ### Python Modules
275
+
276
+ `liti.core.model`
277
+
278
+ This module stores all the data models. The models are versioned, though currently there is only the one version. The
279
+ hierarchy is roughly:
280
+
281
+ > `operation.ops` > `operation.data` > `schema` > `datatype`
282
+
283
+ `liti.core.model.v1.operation.data`
284
+
285
+ These are the pure data operations. They are (de)serialized between the operation files and metadata.
286
+
287
+ `liti.core.model.v1.operation.ops`
288
+
289
+ These are the wrappers that enhance operations with behavior. There is a 1:1 relationship.
290
+
291
+ `liti.core.model.v1.datatype`
292
+
293
+ These are descriptions of column types.
294
+
295
+ `liti.core.model.v1.schema`
296
+
297
+ These are descriptions of tables and related constructs.
298
+
299
+ `liti.core.backend`
300
+
301
+ Both the database and the metadata can support different backends. You can even use different backends together. The
302
+ backends deal in both the `liti` model and backend specific types adapting between the two.
303
+
304
+ `liti.core.client`
305
+
306
+ These are clients used by the backends. They solely deal in backend specific types with no dependencies on the `liti`
307
+ model.
308
+
309
+ `liti.core.base`
310
+
311
+ This module has base classes for applying default values and validating the data. They are implemented using the
312
+ Observer / Observable pattern so different backends can define their own behavior.
313
+
314
+ `liti.core.runner`
315
+
316
+ This module is for the runners associated with the various ways `liti` can be run. Main code will instantiate a runner
317
+ and run it.
318
+
@@ -0,0 +1,29 @@
1
+ liti/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ liti/cli.py,sha256=ri3S3Os79mc3AhxUiIhYfyl3KqVtb-f9F1pbEDS7xxQ,6312
3
+ liti/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ liti/core/backend/__init__.py,sha256=Stp4fd7VAH8lGa1IpmdWM_s7YtTbB2DgtBct5Z0EmpU,30
5
+ liti/core/backend/base.py,sha256=51XaUSbfUKco08pG1rJh9U8QInLlgxdCyPRZBGdJ43w,4337
6
+ liti/core/backend/bigquery.py,sha256=d-gLx7M3ltWC-HWYJmo_9kG504r22QcmIinVqCQ361E,28133
7
+ liti/core/backend/memory.py,sha256=g3uZjGlWKrXcrlDZEGHrgig836XQv4QL-U14p5mAEjI,4024
8
+ liti/core/base.py,sha256=0IjpSQ1O6A8goOs0fDp4scaKZ9kVpGJKf7vANdHn08w,3101
9
+ liti/core/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ liti/core/client/bigquery.py,sha256=zOcoxAX8kWJAlWN43WDxBA0RLQAD8guFiXMtzzHSVj0,2859
11
+ liti/core/function.py,sha256=eR2-R7ANH3rcmeZgGdxDfDNn403Z_bZxgjE4GpDIqgM,1911
12
+ liti/core/logger.py,sha256=3KaHMDFEaPtBb1pm8Z6wi1_g3z6awoEB0lHp1jpc3OQ,1362
13
+ liti/core/model/__init__.py,sha256=tAvk5TAXrOBe-LtXI2ACrHtf2ToHb0-bKtp3tv6NtcQ,112
14
+ liti/core/model/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ liti/core/model/v1/datatype.py,sha256=af3SDwpvccSE_b2QSlYu4csLvZmia9k4PXP50WYbhpQ,5744
16
+ liti/core/model/v1/operation/__init__.py,sha256=7Hsc7sa55_9CNh88f3cYVQ5ajNz5QxlY2g4bVDxjqmc,32
17
+ liti/core/model/v1/operation/data/__init__.py,sha256=2cHyXVT2al6OvovrGdG8sldHJyrFChHbAhOPdy1c8ig,34
18
+ liti/core/model/v1/operation/data/base.py,sha256=g1pSpPzaDYsCg2I-nyG-u8JsWkjnsTAbGzaMEVIi04A,559
19
+ liti/core/model/v1/operation/data/table.py,sha256=LOOxBLQCTQI3Bg89xGRA5FAm2K6dmjwspo9NTgnE4JQ,2262
20
+ liti/core/model/v1/operation/ops/__init__.py,sha256=g5_8xq7H8DFL_3y_rWQEh-RrpiXMjwqfHattbQIqkCc,105
21
+ liti/core/model/v1/operation/ops/base.py,sha256=SDEl1bLapwQJOp8wRGph33ZjctoybASA8XI603xGqd0,1606
22
+ liti/core/model/v1/operation/ops/table.py,sha256=64lgnBDpYzq7vPXQg_lcdLQTax8gVzUi7pLJzgvrb9w,10108
23
+ liti/core/model/v1/schema.py,sha256=y6lxthDFJBv6BWAeYj999DR6M01y1z2FuqL4ebkMjcg,8385
24
+ liti/core/runner.py,sha256=71Ph4rPQWjQ3j6a3z28b6H01XIENRex3VjX2npWuHzU,4806
25
+ limber_timber-0.0.1.dist-info/LICENSE,sha256=jC5DbKlIWpMy35HUKaP-kNmm2I6d06vuu6g6G_S__zs,1072
26
+ limber_timber-0.0.1.dist-info/METADATA,sha256=vtN6dpiXCcjihl_C9YFD-j2I--vEE3hIOj26HLk1_VE,9493
27
+ limber_timber-0.0.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
28
+ limber_timber-0.0.1.dist-info/entry_points.txt,sha256=U0B88Tw-W_qi-xkHTbyb6kqQGpS30-6Cagneq8p6-jc,38
29
+ limber_timber-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ liti=liti.cli:main
3
+
liti/__init__.py ADDED
File without changes
liti/cli.py ADDED
@@ -0,0 +1,158 @@
1
+ import logging
2
+ from argparse import ArgumentParser, BooleanOptionalAction, Namespace
3
+
4
+ from google.cloud import bigquery
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from liti.core.backend.base import DbBackend, MetaBackend
8
+ from liti.core.backend.bigquery import BigQueryDbBackend, BigQueryMetaBackend
9
+ from liti.core.backend.memory import MemoryDbBackend, MemoryMetaBackend
10
+ from liti.core.client.bigquery import BqClient
11
+ from liti.core.model.v1.schema import DatabaseName, Identifier, SchemaName, TableName
12
+ from liti.core.runner import MigrateRunner, ScanRunner
13
+
14
+
15
+ class Clients(BaseModel):
16
+ """ Used to ensure we have a single instance of any client that is needed
17
+
18
+ This becomes useful for example when it comes to having transactions that span the DB and metadata.
19
+ """
20
+ model_config = ConfigDict(arbitrary_types_allowed=True)
21
+
22
+ big_query: BqClient | None = None
23
+
24
+
25
+ def parse_all_arguments() -> Namespace:
26
+ parser = ArgumentParser(prog='liti')
27
+ parser.add_argument('command', help='action to perform')
28
+ parser.add_argument('-t', '--target', help='directory with migration files')
29
+ parser.add_argument('-w', '--wet', action=BooleanOptionalAction, default=False, help='[False] should perform migration side effects')
30
+ parser.add_argument('-d', '--down', action=BooleanOptionalAction, default=False, help='[False] should allow performing down migrations')
31
+ parser.add_argument('-v', '--verbose', action=BooleanOptionalAction, default=False, help='[False] should log in a wet run')
32
+ parser.add_argument('--db', default='memory', help='[memory] type of database backend (e.g. memory, bigquery)')
33
+ parser.add_argument('--meta', default='memory', help='[memory] type of metadata backend (e.g. memory, bigquery)')
34
+ parser.add_argument('--meta-table-name', help='fully qualified table name for a metadata table')
35
+ parser.add_argument('--scan-database', help='database to scan')
36
+ parser.add_argument('--scan-schema', help='schema to scan')
37
+ parser.add_argument('--scan-table', help='table to scan')
38
+ parser.add_argument('--gcp-project', help='project to use for GCP backends')
39
+ return parser.parse_args()
40
+
41
+
42
+ def parse_migrate_arguments() -> Namespace:
43
+ parser = ArgumentParser(prog='liti')
44
+ parser.add_argument('command', help='action to perform')
45
+ parser.add_argument('-t', '--target', required=True, help='directory with migration files')
46
+ parser.add_argument('-w', '--wet', action=BooleanOptionalAction, default=False, help='[False] should perform migration side effects')
47
+ parser.add_argument('-d', '--down', action=BooleanOptionalAction, default=False, help='[False] should allow performing down migrations')
48
+ parser.add_argument('-v', '--verbose', action=BooleanOptionalAction, default=False, help='[False] should log in a wet run')
49
+ parser.add_argument('--db', default='memory', help='[memory] type of database backend (e.g. memory, bigquery)')
50
+ parser.add_argument('--meta', default='memory', help='[memory] type of metadata backend (e.g. memory, bigquery)')
51
+ parser.add_argument('--meta-table-name', help='fully qualified table name for a metadata table')
52
+ parser.add_argument('--gcp-project', help='project to use for GCP backends')
53
+ return parser.parse_args()
54
+
55
+
56
+ def parse_scan_arguments() -> Namespace:
57
+ parser = ArgumentParser(prog='liti')
58
+ parser.add_argument('command', help='action to perform')
59
+ parser.add_argument('--db', required=True, help='type of database backend (e.g. bigquery)')
60
+ parser.add_argument('--scan-database', required=True, help='database to scan')
61
+ parser.add_argument('--scan-schema', required=True, help='schema to scan')
62
+ parser.add_argument('--scan-table', help='table to scan, scan whole schema if not provided')
63
+ parser.add_argument('--gcp-project', help='project to use for GCP backends')
64
+ return parser.parse_args()
65
+
66
+
67
+ def build_clients(args: Namespace) -> Clients:
68
+ client_ids = []
69
+
70
+ if 'db' in args:
71
+ client_ids.append(args.db)
72
+
73
+ if 'meta' in args:
74
+ client_ids.append(args.meta)
75
+
76
+ if 'bigquery' in client_ids:
77
+ if 'gcp_project' in args:
78
+ gcp_project = args.gcp_project
79
+ elif 'scan_database' in args:
80
+ gcp_project = args.scan_database
81
+ else:
82
+ raise ValueError('Unable to determine the GCP project to use for the client')
83
+
84
+ big_query_client = BqClient(bigquery.Client(project=gcp_project))
85
+ else:
86
+ big_query_client = None
87
+
88
+ return Clients(
89
+ big_query=big_query_client,
90
+ )
91
+
92
+
93
+ def build_db_backend(args: Namespace, clients: Clients) -> DbBackend:
94
+ if args.db == 'memory':
95
+ return MemoryDbBackend()
96
+ elif args.db == 'memory':
97
+ return BigQueryDbBackend(clients.big_query)
98
+ else:
99
+ raise ValueError(f'Invalid database backend: {args.db}')
100
+
101
+
102
+ def build_meta_backend(args: Namespace, clients: Clients) -> MetaBackend:
103
+ if args.db == 'memory':
104
+ return MemoryMetaBackend()
105
+ elif args.db == 'memory':
106
+ return BigQueryMetaBackend(clients.big_query, TableName(args.meta_table_name))
107
+ else:
108
+ raise ValueError(f'Invalid metadata backend: {args.db}')
109
+
110
+
111
+ def migrate():
112
+ args = parse_migrate_arguments()
113
+ silent = args.wet and not args.verbose
114
+
115
+ if silent:
116
+ logging.basicConfig(level=logging.ERROR)
117
+ else:
118
+ logging.basicConfig(level=logging.INFO)
119
+
120
+ clients = build_clients(args)
121
+ db_backend = build_db_backend(args, clients)
122
+ meta_backend = build_meta_backend(args, clients)
123
+
124
+ runner = MigrateRunner(
125
+ db_backend=db_backend,
126
+ meta_backend=meta_backend,
127
+ target=args.target,
128
+ )
129
+
130
+ runner.run(
131
+ wet_run=args.wet,
132
+ allow_down=args.down,
133
+ silent=silent,
134
+ )
135
+
136
+
137
+ def scan():
138
+ args = parse_scan_arguments()
139
+ clients = build_clients(args)
140
+ db_backend = build_db_backend(args, clients)
141
+ runner = ScanRunner(db_backend)
142
+
143
+ runner.run(
144
+ database=DatabaseName(args.scan_database),
145
+ schema=SchemaName(args.scan_schema),
146
+ table=Identifier(args.scan_table) if args.scan_table else None,
147
+ )
148
+
149
+
150
+ def main():
151
+ args = parse_all_arguments()
152
+
153
+ if args.command == 'migrate':
154
+ migrate()
155
+ elif args.command == 'scan':
156
+ scan()
157
+ else:
158
+ raise ValueError(f'Invalid command: {args.command}')
liti/core/__init__.py ADDED
File without changes
@@ -0,0 +1 @@
1
+ # TODO: support more backends
@@ -0,0 +1,125 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from liti.core.base import Defaulter, Validator
4
+ from liti.core.model.v1.operation.data.base import Operation
5
+ from liti.core.model.v1.operation.data.table import CreateTable
6
+ from liti.core.model.v1.schema import Column, ColumnName, DatabaseName, Identifier, RoundingModeLiteral, \
7
+ SchemaName, Table, \
8
+ TableName
9
+
10
+
11
+ class DbBackend(ABC, Defaulter, Validator):
12
+ """ DB backends make changes to and read the state of the database """
13
+
14
+ @abstractmethod
15
+ def scan_schema(self, database: DatabaseName, schema: SchemaName) -> list[Operation]:
16
+ raise NotImplementedError('not supported')
17
+
18
+ @abstractmethod
19
+ def scan_table(self, name: TableName) -> CreateTable | None:
20
+ raise NotImplementedError('not supported')
21
+
22
+ @abstractmethod
23
+ def has_table(self, name: TableName) -> bool:
24
+ raise NotImplementedError('not supported')
25
+
26
+ @abstractmethod
27
+ def get_table(self, name: TableName) -> Table | None:
28
+ raise NotImplementedError('not supported')
29
+
30
+ @abstractmethod
31
+ def create_table(self, table: Table):
32
+ raise NotImplementedError('not supported')
33
+
34
+ @abstractmethod
35
+ def drop_table(self, name: TableName):
36
+ raise NotImplementedError('not supported')
37
+
38
+ @abstractmethod
39
+ def rename_table(self, from_name: TableName, to_name: Identifier):
40
+ raise NotImplementedError('not supported')
41
+
42
+ def set_clustering(self, table_name: TableName, columns: list[ColumnName] | None):
43
+ raise NotImplementedError('not supported')
44
+
45
+ @abstractmethod
46
+ def set_description(self, table_name: TableName, description: str | None):
47
+ raise NotImplementedError('not supported')
48
+
49
+ @abstractmethod
50
+ def set_labels(self, table_name: TableName, labels: dict[str, str] | None):
51
+ raise NotImplementedError('not supported')
52
+
53
+ @abstractmethod
54
+ def set_tags(self, table_name: TableName, tags: dict[str, str] | None):
55
+ raise NotImplementedError('not supported')
56
+
57
+ @abstractmethod
58
+ def set_default_rounding_mode(self, table_name: TableName, rounding_mode: RoundingModeLiteral):
59
+ raise NotImplementedError('not supported')
60
+
61
+ @abstractmethod
62
+ def add_column(self, table_name: TableName, column: Column):
63
+ raise NotImplementedError('not supported')
64
+
65
+ @abstractmethod
66
+ def drop_column(self, table_name: TableName, column_name: ColumnName):
67
+ raise NotImplementedError('not supported')
68
+
69
+ @abstractmethod
70
+ def rename_column(self, table_name: TableName, from_name: ColumnName, to_name: ColumnName):
71
+ raise NotImplementedError('not supported')
72
+
73
+ @abstractmethod
74
+ def set_column_nullable(self, table_name: TableName, column_name: ColumnName, nullable: bool):
75
+ raise NotImplementedError('not supported')
76
+
77
+ @abstractmethod
78
+ def set_column_description(self, table_name: TableName, column_name: ColumnName, description: str | None):
79
+ raise NotImplementedError('not supported')
80
+
81
+ @abstractmethod
82
+ def set_column_rounding_mode(self, table_name: TableName, column_name: ColumnName, rounding_mode: RoundingModeLiteral):
83
+ raise NotImplementedError('not supported')
84
+
85
+
86
+ class MetaBackend(ABC):
87
+ """ Meta backends manage the state of what migrations have been applied """
88
+
89
+ def initialize(self):
90
+ pass
91
+
92
+ @abstractmethod
93
+ def get_applied_operations(self) -> list[Operation]:
94
+ pass
95
+
96
+ @abstractmethod
97
+ def apply_operation(self, operation: Operation):
98
+ """ Add the operation to the metadata """
99
+ pass
100
+
101
+ @abstractmethod
102
+ def unapply_operation(self, operation: Operation):
103
+ """ Remove the operation from the metadata
104
+
105
+ The operation must be the most recent one.
106
+ """
107
+ pass
108
+
109
+ def get_previous_operations(self) -> list[Operation]:
110
+ return self.get_applied_operations()[:-1]
111
+
112
+ def get_migration_plan(self, target: list[Operation]) -> dict[str, list[Operation]]:
113
+ applied = self.get_applied_operations()
114
+ common_operations = 0
115
+
116
+ for applied_op, target_op in zip(applied, target):
117
+ if applied_op == target_op:
118
+ common_operations += 1
119
+ else:
120
+ break
121
+
122
+ return {
123
+ 'down': list(reversed(applied[common_operations:])),
124
+ 'up': target[common_operations:],
125
+ }