spark-sql-migrations 0.0.4__tar.gz → 0.0.6__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.
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/PKG-INFO +56 -14
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/setup.py +62 -17
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/migration_templates/default_migration_template.sql +3 -0
- spark_sql_migrations-0.0.6/spark_sql_migrations/migrations_initial/first_01_intial_create_cat.sql +9 -0
- spark_sql_migrations-0.0.4/spark_sql_migrations/migrations_initial/first_03_intial_create_state_table_all.sql → spark_sql_migrations-0.0.6/spark_sql_migrations/migrations_initial/first_03_intial_create_state_table.sql +1 -2
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/spark_sql/spark_sql.py +112 -84
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/PKG-INFO +56 -14
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/SOURCES.txt +3 -3
- spark_sql_migrations-0.0.4/spark_sql_migrations/migrations_initial/first_01_intial_create_cat_dbr_only.sql +0 -4
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/LICENSE +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/MANIFEST.in +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/backend_shim.py +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/setup.cfg +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/__init__.py +0 -0
- /spark_sql_migrations-0.0.4/spark_sql_migrations/migrations_initial/first_02_intial_create_schema_all.sql → /spark_sql_migrations-0.0.6/spark_sql_migrations/migrations_initial/first_02_intial_create_schema.sql +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/py.typed +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/spark_sql/__init__.py +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/spark_utils.py +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/dependency_links.txt +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/entry_points.txt +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/namespace_packages.txt +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/requires.txt +0 -0
- {spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spark_sql_migrations
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Chained, idempotent SQL migrations for Spark and Databricks.
|
|
5
5
|
Home-page: https://github.com/mcjug2015/spark_sql_migrations
|
|
6
6
|
Author: Victor Semenov
|
|
@@ -79,28 +79,32 @@ Migrations come from two different owners, and they behave differently:
|
|
|
79
79
|
| | ships in the wheel | owned by you |
|
|
80
80
|
|---|---|---|
|
|
81
81
|
| **what** | catalog, schema, `_spark_migrations_version` table | your tables and columns |
|
|
82
|
-
| **where** | `spark_sql_migrations/migrations_initial/` |
|
|
83
|
-
| **
|
|
82
|
+
| **where** | `spark_sql_migrations/migrations_initial/` | the directory you pass as `--migrations-dir` |
|
|
83
|
+
| **ordered by** | filename | the `prev_revision_id` chain |
|
|
84
84
|
| **when applied** | until the version table exists | once, then recorded |
|
|
85
85
|
|
|
86
86
|
The bootstrap chain is applied on every run until `_spark_migrations_version` exists, and
|
|
87
87
|
skipped from then on. A run that fails partway through bootstrapping replays it, so it must
|
|
88
|
-
still be idempotent. Your own
|
|
88
|
+
still be idempotent. Your own migrations are applied once each and the head revision is
|
|
89
89
|
written to the version table.
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
Both kinds are rendered the same way, so both can gate a statement on `is_dbr` — the
|
|
92
|
+
bootstrap chain uses it for `CREATE CATALOG`, which only Unity Catalog has.
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
## Laying out your chain
|
|
95
|
+
|
|
96
|
+
`--migrations-dir` is the directory holding the chain — the `.sql` files sit in it directly:
|
|
94
97
|
|
|
95
98
|
```
|
|
96
99
|
your_project/
|
|
97
|
-
└── migrations/
|
|
98
|
-
├──
|
|
99
|
-
└──
|
|
100
|
+
└── migrations/ <- this is --migrations-dir
|
|
101
|
+
├── 260831_01_create_test_table_e5ce0039b32b.sql
|
|
102
|
+
└── 260831_02_create_metrics_table_07990e2a101e.sql
|
|
100
103
|
```
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
One chain covers both engines: a Databricks-only *statement* gates on `is_dbr` inside a
|
|
106
|
+
migration that otherwise runs everywhere — see below. SQL that only Databricks understands
|
|
107
|
+
must never sit ungated there, or your local runs will break.
|
|
104
108
|
|
|
105
109
|
## Running migrations
|
|
106
110
|
|
|
@@ -124,7 +128,7 @@ from spark_sql_migrations.spark_sql.spark_sql import main
|
|
|
124
128
|
|
|
125
129
|
|
|
126
130
|
def get_migrations_dir():
|
|
127
|
-
"""the
|
|
131
|
+
"""the directory holding this project's own migration chain."""
|
|
128
132
|
return os.path.dirname(__file__)
|
|
129
133
|
|
|
130
134
|
|
|
@@ -134,7 +138,7 @@ main(cat="spark_catalog", schema="default", migrations_dir=get_migrations_dir())
|
|
|
134
138
|
`main` builds its own session via `get_spark()` and, being library code, leaves logging to
|
|
135
139
|
you. To supply your own — a session already
|
|
136
140
|
configured by your job, say — call `run_migrations(spark, cat, schema, output_folder,
|
|
137
|
-
|
|
141
|
+
migrations_dir)` directly.
|
|
138
142
|
|
|
139
143
|
Every rendered statement is written to a timestamped folder under `migrations_out/`
|
|
140
144
|
before it is executed, so you can always read the exact SQL a run applied.
|
|
@@ -144,7 +148,7 @@ before it is executed, so you can always read the exact SQL a run applied.
|
|
|
144
148
|
Generate one rather than hand-rolling the header:
|
|
145
149
|
|
|
146
150
|
```bash
|
|
147
|
-
spark-sql-migrations create_new_migration --message "add batch id to metrics" --output-path path/to/migrations
|
|
151
|
+
spark-sql-migrations create_new_migration --message "add batch id to metrics" --output-path path/to/migrations
|
|
148
152
|
```
|
|
149
153
|
|
|
150
154
|
That writes `<yymmdd>_<slug>_<revision_id>.sql` from the template shipped in the package
|
|
@@ -183,6 +187,44 @@ begin
|
|
|
183
187
|
end;
|
|
184
188
|
```
|
|
185
189
|
|
|
190
|
+
### Databricks-only statements
|
|
191
|
+
|
|
192
|
+
`is_dbr` is in the render context, so a migration in the shared chain can carry SQL only
|
|
193
|
+
Databricks understands:
|
|
194
|
+
|
|
195
|
+
```sql
|
|
196
|
+
-- revision_id:e8614b76da6d;
|
|
197
|
+
-- prev_revision_id:57037b6c19b4;
|
|
198
|
+
{% if is_dbr %}
|
|
199
|
+
begin
|
|
200
|
+
grant READ FILES on external location `some-bucket` to `some_group`;
|
|
201
|
+
end;
|
|
202
|
+
{% endif %}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`create_new_migration` writes that conditional for you with `--add-is-dbr`, below the
|
|
206
|
+
headers so they stay readable:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
spark-sql-migrations create_new_migration --message "read files on the cms zip external location" --output-path path/to/migrations --add-is-dbr
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
This has to happen at render time, not in SQL. OSS Spark rejects `GRANT` and the rest of
|
|
213
|
+
its unsupported-native-command list while *parsing*, so a `declare exit handler` in the same
|
|
214
|
+
script never gets the chance to run — and the whole script fails to compile, not just that
|
|
215
|
+
statement. Jinja removes the text before `spark.sql()` ever sees it.
|
|
216
|
+
|
|
217
|
+
A migration left holding nothing but comments is recorded in the version table and not
|
|
218
|
+
executed, so the chain advances in step on both engines. That recording is forward-only: a
|
|
219
|
+
catalog migrated locally past a gated revision will not pick that statement up if it is
|
|
220
|
+
later migrated from Databricks. For disposable test schemas that is exactly what you want;
|
|
221
|
+
for a long-lived catalog, don't migrate it from both.
|
|
222
|
+
|
|
223
|
+
`_spark_migrations_version` holds one row and one column, `version_num`. There is no
|
|
224
|
+
in-place upgrade from the older two-chain shape, which carried a `migration_type` column and
|
|
225
|
+
a row per chain: a catalog bootstrapped by an earlier release has to be rebuilt from
|
|
226
|
+
scratch, which also clears the revision history that layout recorded.
|
|
227
|
+
|
|
186
228
|
## Local vs Databricks
|
|
187
229
|
|
|
188
230
|
`get_spark()` returns a `DatabricksSession` when `is_dbr()` is true and a Delta-configured
|
|
@@ -80,28 +80,32 @@ Migrations come from two different owners, and they behave differently:
|
|
|
80
80
|
| | ships in the wheel | owned by you |
|
|
81
81
|
|---|---|---|
|
|
82
82
|
| **what** | catalog, schema, `_spark_migrations_version` table | your tables and columns |
|
|
83
|
-
| **where** | `spark_sql_migrations/migrations_initial/` |
|
|
84
|
-
| **
|
|
83
|
+
| **where** | `spark_sql_migrations/migrations_initial/` | the directory you pass as `--migrations-dir` |
|
|
84
|
+
| **ordered by** | filename | the `prev_revision_id` chain |
|
|
85
85
|
| **when applied** | until the version table exists | once, then recorded |
|
|
86
86
|
|
|
87
87
|
The bootstrap chain is applied on every run until `_spark_migrations_version` exists, and
|
|
88
88
|
skipped from then on. A run that fails partway through bootstrapping replays it, so it must
|
|
89
|
-
still be idempotent. Your own
|
|
89
|
+
still be idempotent. Your own migrations are applied once each and the head revision is
|
|
90
90
|
written to the version table.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
Both kinds are rendered the same way, so both can gate a statement on `is_dbr` — the
|
|
93
|
+
bootstrap chain uses it for `CREATE CATALOG`, which only Unity Catalog has.
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
## Laying out your chain
|
|
96
|
+
|
|
97
|
+
`--migrations-dir` is the directory holding the chain — the `.sql` files sit in it directly:
|
|
95
98
|
|
|
96
99
|
```
|
|
97
100
|
your_project/
|
|
98
|
-
└── migrations/
|
|
99
|
-
├──
|
|
100
|
-
└──
|
|
101
|
+
└── migrations/ <- this is --migrations-dir
|
|
102
|
+
├── 260831_01_create_test_table_e5ce0039b32b.sql
|
|
103
|
+
└── 260831_02_create_metrics_table_07990e2a101e.sql
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
One chain covers both engines: a Databricks-only *statement* gates on `is_dbr` inside a
|
|
107
|
+
migration that otherwise runs everywhere — see below. SQL that only Databricks understands
|
|
108
|
+
must never sit ungated there, or your local runs will break.
|
|
105
109
|
|
|
106
110
|
## Running migrations
|
|
107
111
|
|
|
@@ -128,7 +132,7 @@ from spark_sql_migrations.spark_sql.spark_sql import main
|
|
|
128
132
|
|
|
129
133
|
|
|
130
134
|
def get_migrations_dir():
|
|
131
|
-
\"\"\"the
|
|
135
|
+
\"\"\"the directory holding this project's own migration chain.\"\"\"
|
|
132
136
|
return os.path.dirname(__file__)
|
|
133
137
|
|
|
134
138
|
|
|
@@ -138,7 +142,7 @@ main(cat="spark_catalog", schema="default", migrations_dir=get_migrations_dir())
|
|
|
138
142
|
`main` builds its own session via `get_spark()` and, being library code, leaves logging to
|
|
139
143
|
you. To supply your own — a session already
|
|
140
144
|
configured by your job, say — call `run_migrations(spark, cat, schema, output_folder,
|
|
141
|
-
|
|
145
|
+
migrations_dir)` directly.
|
|
142
146
|
|
|
143
147
|
Every rendered statement is written to a timestamped folder under `migrations_out/`
|
|
144
148
|
before it is executed, so you can always read the exact SQL a run applied.
|
|
@@ -150,7 +154,7 @@ Generate one rather than hand-rolling the header:
|
|
|
150
154
|
```bash
|
|
151
155
|
spark-sql-migrations create_new_migration \
|
|
152
156
|
--message "add batch id to metrics" \
|
|
153
|
-
--output-path path/to/migrations
|
|
157
|
+
--output-path path/to/migrations
|
|
154
158
|
```
|
|
155
159
|
|
|
156
160
|
That writes `<yymmdd>_<slug>_<revision_id>.sql` from the template shipped in the package
|
|
@@ -189,6 +193,47 @@ begin
|
|
|
189
193
|
end;
|
|
190
194
|
```
|
|
191
195
|
|
|
196
|
+
### Databricks-only statements
|
|
197
|
+
|
|
198
|
+
`is_dbr` is in the render context, so a migration in the shared chain can carry SQL only
|
|
199
|
+
Databricks understands:
|
|
200
|
+
|
|
201
|
+
```sql
|
|
202
|
+
-- revision_id:e8614b76da6d;
|
|
203
|
+
-- prev_revision_id:57037b6c19b4;
|
|
204
|
+
{% if is_dbr %}
|
|
205
|
+
begin
|
|
206
|
+
grant READ FILES on external location `some-bucket` to `some_group`;
|
|
207
|
+
end;
|
|
208
|
+
{% endif %}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`create_new_migration` writes that conditional for you with `--add-is-dbr`, below the
|
|
212
|
+
headers so they stay readable:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
spark-sql-migrations create_new_migration \
|
|
216
|
+
--message "read files on the cms zip external location" \
|
|
217
|
+
--output-path path/to/migrations \
|
|
218
|
+
--add-is-dbr
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This has to happen at render time, not in SQL. OSS Spark rejects `GRANT` and the rest of
|
|
222
|
+
its unsupported-native-command list while *parsing*, so a `declare exit handler` in the same
|
|
223
|
+
script never gets the chance to run — and the whole script fails to compile, not just that
|
|
224
|
+
statement. Jinja removes the text before `spark.sql()` ever sees it.
|
|
225
|
+
|
|
226
|
+
A migration left holding nothing but comments is recorded in the version table and not
|
|
227
|
+
executed, so the chain advances in step on both engines. That recording is forward-only: a
|
|
228
|
+
catalog migrated locally past a gated revision will not pick that statement up if it is
|
|
229
|
+
later migrated from Databricks. For disposable test schemas that is exactly what you want;
|
|
230
|
+
for a long-lived catalog, don't migrate it from both.
|
|
231
|
+
|
|
232
|
+
`_spark_migrations_version` holds one row and one column, `version_num`. There is no
|
|
233
|
+
in-place upgrade from the older two-chain shape, which carried a `migration_type` column and
|
|
234
|
+
a row per chain: a catalog bootstrapped by an earlier release has to be rebuilt from
|
|
235
|
+
scratch, which also clears the revision history that layout recorded.
|
|
236
|
+
|
|
192
237
|
## Local vs Databricks
|
|
193
238
|
|
|
194
239
|
`get_spark()` returns a `DatabricksSession` when `is_dbr()` is true and a Delta-configured
|
|
@@ -238,9 +283,9 @@ Apache License 2.0 — see [LICENSE](LICENSE).
|
|
|
238
283
|
'package_data': {
|
|
239
284
|
'spark_sql_migrations': (
|
|
240
285
|
'migration_templates/default_migration_template.sql',
|
|
241
|
-
'migrations_initial/
|
|
242
|
-
'migrations_initial/
|
|
243
|
-
'migrations_initial/
|
|
286
|
+
'migrations_initial/first_01_intial_create_cat.sql',
|
|
287
|
+
'migrations_initial/first_02_intial_create_schema.sql',
|
|
288
|
+
'migrations_initial/first_03_intial_create_state_table.sql',
|
|
244
289
|
'py.typed',
|
|
245
290
|
),
|
|
246
291
|
},
|
|
@@ -254,5 +299,5 @@ Apache License 2.0 — see [LICENSE](LICENSE).
|
|
|
254
299
|
},
|
|
255
300
|
'python_requires': '>=3.10',
|
|
256
301
|
'url': 'https://github.com/mcjug2015/spark_sql_migrations',
|
|
257
|
-
'version': '0.0.
|
|
302
|
+
'version': '0.0.6',
|
|
258
303
|
})
|
|
@@ -5,6 +5,9 @@ begin
|
|
|
5
5
|
-- would like to have applied to databricks db or local spark(for testing)
|
|
6
6
|
-- crutch scaffolding will use jinja to fill in the cat and schema that are sent to it
|
|
7
7
|
-- this is also taken care of in the gh build.
|
|
8
|
+
-- is_dbr is filled in too: statements only databricks understands go in a jinja
|
|
9
|
+
-- conditional on it, outside the begin/end, rather than in a second chain. pass
|
|
10
|
+
-- --add-is-dbr to create_new_migration to have that conditional written for you.
|
|
8
11
|
|
|
9
12
|
-- e.g:
|
|
10
13
|
-- create schema if not exists {{cat}}.{{schema}};
|
spark_sql_migrations-0.0.6/spark_sql_migrations/migrations_initial/first_01_intial_create_cat.sql
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- revision_id:;
|
|
2
|
+
-- a catalog is a unity catalog object: local spark has spark_catalog and no way
|
|
3
|
+
-- to make another, so this one is gated to databricks and renders to nothing
|
|
4
|
+
-- else, leaving the bootstrap chain the same three files everywhere.
|
|
5
|
+
{% if is_dbr %}
|
|
6
|
+
begin
|
|
7
|
+
create catalog if not exists {{cat}};
|
|
8
|
+
end;
|
|
9
|
+
{% endif %}
|
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
Two kinds of migration exist, and they are owned by different parties:
|
|
4
4
|
|
|
5
5
|
* the *initial* chain and the new-migration template ship inside this package.
|
|
6
|
-
They bootstrap the catalog, the schema and the version table,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* the
|
|
11
|
-
|
|
12
|
-
``
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
They bootstrap the catalog, the schema and the version table, carry no
|
|
7
|
+
revision of their own, and are applied in filename order on every run until
|
|
8
|
+
the version table exists -- so a run that failed partway through replays
|
|
9
|
+
them, and they must be idempotent.
|
|
10
|
+
* the client chain belongs to the consuming project. The caller passes the
|
|
11
|
+
directory holding it as ``migrations_dir``; the chain is walked from its root
|
|
12
|
+
via ``prev_revision_id`` and only the tail not yet recorded in the version
|
|
13
|
+
table is applied.
|
|
14
|
+
|
|
15
|
+
Which engine a migration targets is decided inside the migration, not by where it
|
|
16
|
+
sits: every one of them is rendered through jinja before it is executed, with
|
|
17
|
+
``is_dbr`` in the render context, so a chain that runs everywhere can still gate
|
|
18
|
+
the statements one engine would reject. A migration left holding no statements is
|
|
19
|
+
recorded and not executed, which is what lets a project keep one chain and no
|
|
20
|
+
second head.
|
|
15
21
|
"""
|
|
16
22
|
|
|
17
23
|
import argparse
|
|
@@ -23,24 +29,23 @@ import uuid
|
|
|
23
29
|
from dataclasses import dataclass
|
|
24
30
|
|
|
25
31
|
from jinja2 import Environment, FileSystemLoader, PackageLoader, select_autoescape
|
|
26
|
-
from pyspark.sql.functions import col
|
|
27
32
|
|
|
28
33
|
from spark_sql_migrations.spark_utils import get_spark, is_dbr
|
|
29
34
|
|
|
30
35
|
logger = logging.getLogger(__name__)
|
|
31
36
|
|
|
32
37
|
|
|
33
|
-
ALL_SPARK = "all_spark"
|
|
34
|
-
DBR_ONLY = "dbr_only"
|
|
35
|
-
|
|
36
38
|
VERSION_TABLE = "_spark_migrations_version"
|
|
37
39
|
|
|
38
40
|
PACKAGE_NAME = "spark_sql_migrations"
|
|
39
41
|
INITIAL_MIGRATIONS_PACKAGE_PATH = "migrations_initial"
|
|
42
|
+
# where a run's rendered client migrations land, under its output folder
|
|
43
|
+
CLIENT_OUTPUT_DIRNAME = "migrations"
|
|
40
44
|
TEMPLATES_PACKAGE_PATH = "migration_templates"
|
|
41
45
|
DEFAULT_TEMPLATE_NAME = "default_migration_template.sql"
|
|
42
46
|
|
|
43
47
|
_HEADER_RE = re.compile(r"^--\s*(revision_id|prev_revision_id)\s*:\s*(\w*)\s*;$")
|
|
48
|
+
_COMMENT_OR_BLANK_RE = re.compile(r"^\s*(--.*)?$")
|
|
44
49
|
|
|
45
50
|
|
|
46
51
|
@dataclass(frozen=True)
|
|
@@ -50,11 +55,6 @@ class Migration:
|
|
|
50
55
|
template_name: str
|
|
51
56
|
|
|
52
57
|
|
|
53
|
-
def get_migrations_dir(migrations_root, migration_type):
|
|
54
|
-
"""the client-owned directory holding one chain."""
|
|
55
|
-
return os.path.join(migrations_root, f"{migration_type}_migrations")
|
|
56
|
-
|
|
57
|
-
|
|
58
58
|
def get_default_template_path():
|
|
59
59
|
"""the template this package ships; callers may pass their own instead."""
|
|
60
60
|
return os.path.join(os.path.dirname(__file__), "..", TEMPLATES_PACKAGE_PATH, DEFAULT_TEMPLATE_NAME)
|
|
@@ -150,39 +150,56 @@ def get_migrations_list(migrations_dir):
|
|
|
150
150
|
return get_ordered_migration_objs(migrations)
|
|
151
151
|
|
|
152
152
|
|
|
153
|
-
def
|
|
154
|
-
"""the
|
|
155
|
-
|
|
156
|
-
the whole chain when the table is missing or holds no row for this type.
|
|
157
|
-
"""
|
|
158
|
-
ordered = get_migrations_list(migrations_dir)
|
|
159
|
-
|
|
153
|
+
def get_table_version(spark, cat: str, schema: str):
|
|
154
|
+
"""the revision VERSION_TABLE records, or None if it holds no row yet."""
|
|
160
155
|
version_table = f"{cat}.{schema}.{VERSION_TABLE}"
|
|
161
156
|
if not spark.catalog.tableExists(version_table):
|
|
162
|
-
return
|
|
157
|
+
return None
|
|
158
|
+
return spark.read.table(version_table).select("version_num").head()
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
160
|
+
|
|
161
|
+
def get_unapplied_migrations_list(spark, migrations_dir, cat: str, schema: str):
|
|
162
|
+
ordered = get_migrations_list(migrations_dir)
|
|
163
|
+
current = get_table_version(spark, cat, schema)
|
|
167
164
|
if not current:
|
|
168
165
|
return ordered
|
|
169
166
|
|
|
170
167
|
current_revision_id = current["version_num"]
|
|
171
168
|
|
|
169
|
+
if not ordered:
|
|
170
|
+
logger.warning(
|
|
171
|
+
f"{VERSION_TABLE} is at {current_revision_id}, but {migrations_dir} holds no migrations; "
|
|
172
|
+
f"treating the chain as retired;"
|
|
173
|
+
)
|
|
174
|
+
return ordered
|
|
175
|
+
|
|
172
176
|
for index, migration in enumerate(ordered):
|
|
173
177
|
if migration.revision_id == current_revision_id:
|
|
174
178
|
return ordered[index + 1 :]
|
|
175
179
|
|
|
176
|
-
raise ValueError(
|
|
177
|
-
f"{VERSION_TABLE} has {migration_type} at {current_revision_id}, "
|
|
178
|
-
f"which matches no migration in {migrations_dir};"
|
|
179
|
-
)
|
|
180
|
+
raise ValueError(f"{VERSION_TABLE} is at {current_revision_id}, which matches no migration in {migrations_dir};")
|
|
180
181
|
|
|
181
182
|
|
|
182
|
-
def
|
|
183
|
-
"""
|
|
184
|
-
|
|
183
|
+
def gate_on_is_dbr(migration_content):
|
|
184
|
+
"""wrap a migration's body in the jinja conditional, below its headers.
|
|
185
|
+
|
|
186
|
+
the revision headers stay outside it: they are read off the file as comments,
|
|
187
|
+
and the first line that is neither comment nor blank ends that read -- so an
|
|
188
|
+
opening tag above them would hide them. a body-less template gets an empty
|
|
189
|
+
gate at the end, which renders to nothing either way.
|
|
185
190
|
"""
|
|
191
|
+
lines = migration_content.splitlines()
|
|
192
|
+
body_start = len(lines)
|
|
193
|
+
for index, line in enumerate(lines):
|
|
194
|
+
if line.strip() and not line.startswith("--"):
|
|
195
|
+
body_start = index
|
|
196
|
+
break
|
|
197
|
+
return "\n".join(lines[:body_start] + ["{% if is_dbr %}"] + lines[body_start:] + ["{% endif %}"]) + "\n"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def create_new_migration(
|
|
201
|
+
message: str, template_path: str, output_path: str, truncate_slug_length: int = 40, add_is_dbr: bool = False
|
|
202
|
+
):
|
|
186
203
|
rev_id = uuid.uuid4().hex[-12:]
|
|
187
204
|
|
|
188
205
|
date_prefix = datetime.datetime.today().strftime("%y%m%d")
|
|
@@ -195,15 +212,24 @@ def create_new_migration(message: str, template_path: str, output_path: str, tru
|
|
|
195
212
|
|
|
196
213
|
migration_path = os.path.join(output_path, migration_filename)
|
|
197
214
|
|
|
215
|
+
try:
|
|
216
|
+
ordered_migrations = get_migrations_list(output_path)
|
|
217
|
+
last_migration_rev_id = ordered_migrations[-1].revision_id if len(ordered_migrations) > 0 else ""
|
|
218
|
+
except ValueError:
|
|
219
|
+
last_migration_rev_id = ""
|
|
220
|
+
|
|
198
221
|
default_migration_content = open(template_path).read()
|
|
199
222
|
default_migration_content = default_migration_content.replace("{{revision_id}}", rev_id)
|
|
223
|
+
default_migration_content = default_migration_content.replace("{{prev_revision_id}}", last_migration_rev_id)
|
|
224
|
+
if add_is_dbr:
|
|
225
|
+
default_migration_content = gate_on_is_dbr(default_migration_content)
|
|
200
226
|
|
|
201
227
|
with open(migration_path, "w") as migration_file:
|
|
202
228
|
migration_file.write(default_migration_content)
|
|
203
229
|
|
|
204
230
|
|
|
205
231
|
def apply_template(output_dir, template, cat: str, schema: str):
|
|
206
|
-
result_sql = template.render(cat=cat, schema=schema)
|
|
232
|
+
result_sql = template.render(cat=cat, schema=schema, is_dbr=is_dbr())
|
|
207
233
|
with open(os.path.join(output_dir, template.name.replace(".sql", "_primed.sql")), "w") as file_handle:
|
|
208
234
|
file_handle.write(result_sql)
|
|
209
235
|
return result_sql
|
|
@@ -220,83 +246,76 @@ def get_output_folder(output_parent_path):
|
|
|
220
246
|
return os.path.join(output_parent_path, folder_name)
|
|
221
247
|
|
|
222
248
|
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
def has_no_statements(rendered_sql):
|
|
250
|
+
for line in rendered_sql.splitlines():
|
|
251
|
+
if not _COMMENT_OR_BLANK_RE.match(line):
|
|
252
|
+
return False
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def render_and_apply(spark, output_folder, template, cat: str, schema: str):
|
|
257
|
+
"""render one migration and run it, unless this engine was gated out of it."""
|
|
258
|
+
result_sql = apply_template(output_folder, template, cat=cat, schema=schema)
|
|
259
|
+
if has_no_statements(result_sql):
|
|
260
|
+
logger.info(f"{template.name} rendered no statements; nothing to apply here;")
|
|
261
|
+
return
|
|
262
|
+
spark.sql(result_sql)
|
|
229
263
|
|
|
230
264
|
|
|
231
265
|
def migrate_initial(spark, output_folder, cat: str, schema: str):
|
|
232
|
-
"""apply the package's own bootstrap chain
|
|
266
|
+
"""apply the package's own bootstrap chain, in filename order rather than by revision."""
|
|
233
267
|
env = Environment(
|
|
234
268
|
loader=PackageLoader(package_name=PACKAGE_NAME, package_path=INITIAL_MIGRATIONS_PACKAGE_PATH),
|
|
235
269
|
autoescape=select_autoescape(),
|
|
236
270
|
)
|
|
237
|
-
all_templates = env.list_templates(
|
|
271
|
+
all_templates = env.list_templates(extensions=["sql"])
|
|
238
272
|
|
|
239
273
|
logger.info(f"found {len(all_templates)} migrations; first five are {all_templates[:5]};")
|
|
240
274
|
for template_name in all_templates:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
logger.info(f"invoked spark on {len(all_templates)} migrations;")
|
|
244
|
-
|
|
275
|
+
render_and_apply(spark, output_folder, env.get_template(template_name), cat=cat, schema=schema)
|
|
276
|
+
logger.info(f"rendered {len(all_templates)} initial migrations;")
|
|
245
277
|
|
|
246
|
-
def record_revision(spark, cat: str, schema: str, migration_type: str, revision_id: str):
|
|
247
|
-
"""point VERSION_TABLE's row for one migration type at revision_id.
|
|
248
278
|
|
|
249
|
-
|
|
250
|
-
after a crash between the two statements is harmless because migrations are
|
|
251
|
-
idempotent. the values are inlined rather than passed as `args` because
|
|
252
|
-
delta's DELETE does not bind named parameters.
|
|
253
|
-
"""
|
|
279
|
+
def record_revision(spark, cat: str, schema: str, revision_id: str):
|
|
254
280
|
version_table = f"{cat}.{schema}.{VERSION_TABLE}"
|
|
255
|
-
type_literal = migration_type.replace("'", "''")
|
|
256
281
|
revision_literal = revision_id.replace("'", "''")
|
|
257
|
-
spark.sql(f"delete from {version_table}
|
|
258
|
-
spark.sql(
|
|
259
|
-
f"insert into {version_table} (migration_type, version_num) " f"values ('{type_literal}', '{revision_literal}')"
|
|
260
|
-
)
|
|
282
|
+
spark.sql(f"delete from {version_table}")
|
|
283
|
+
spark.sql(f"insert into {version_table} (version_num) values ('{revision_literal}')")
|
|
261
284
|
|
|
262
285
|
|
|
263
|
-
def migrate_w_rev(spark, output_folder,
|
|
286
|
+
def migrate_w_rev(spark, output_folder, migrations_dir, cat: str, schema: str):
|
|
264
287
|
"""
|
|
265
|
-
apply the migrations of one client chain that VERSION_TABLE hasn't recorded yet.
|
|
266
288
|
TODO XXX permit applying up to a revision below the head revision
|
|
267
289
|
TODO XXX add backwards migrations, permit migrating backwards
|
|
268
290
|
"""
|
|
269
|
-
|
|
270
|
-
|
|
291
|
+
if not os.path.isdir(migrations_dir):
|
|
292
|
+
logger.info(f"no chain at {migrations_dir}; skipping it;")
|
|
293
|
+
return
|
|
294
|
+
unapplied = get_unapplied_migrations_list(spark, migrations_dir, cat=cat, schema=schema)
|
|
271
295
|
|
|
272
296
|
logger.info(
|
|
273
|
-
f"{
|
|
297
|
+
f"{len(unapplied)} unapplied migrations; "
|
|
274
298
|
f"first five are {[migration.template_name for migration in unapplied[:5]]};"
|
|
275
299
|
)
|
|
276
300
|
|
|
277
301
|
env = Environment(loader=FileSystemLoader(migrations_dir), autoescape=select_autoescape())
|
|
278
302
|
for migration in unapplied:
|
|
279
|
-
|
|
280
|
-
spark.
|
|
281
|
-
|
|
282
|
-
logger.info(f"applied {migration.template_name}; {migration_type} is now at {migration.revision_id};")
|
|
303
|
+
render_and_apply(spark, output_folder, env.get_template(migration.template_name), cat=cat, schema=schema)
|
|
304
|
+
record_revision(spark, cat, schema, migration.revision_id)
|
|
305
|
+
logger.info(f"{migration.template_name} done; now at {migration.revision_id};")
|
|
283
306
|
|
|
284
307
|
|
|
285
|
-
def run_migrations(spark, cat, schema, output_folder,
|
|
308
|
+
def run_migrations(spark, cat, schema, output_folder, migrations_dir):
|
|
286
309
|
initial_output_folder = os.path.join(output_folder, INITIAL_MIGRATIONS_PACKAGE_PATH)
|
|
287
|
-
|
|
288
|
-
all_spark_ouput_folder = os.path.join(output_folder, f"{ALL_SPARK}_migrations")
|
|
310
|
+
client_output_folder = os.path.join(output_folder, CLIENT_OUTPUT_DIRNAME)
|
|
289
311
|
os.makedirs(initial_output_folder, exist_ok=True)
|
|
290
|
-
os.makedirs(
|
|
291
|
-
os.makedirs(all_spark_ouput_folder, exist_ok=True)
|
|
312
|
+
os.makedirs(client_output_folder, exist_ok=True)
|
|
292
313
|
version_table = f"{cat}.{schema}.{VERSION_TABLE}"
|
|
293
314
|
if spark.catalog.tableExists(version_table):
|
|
294
315
|
logger.info(f"{version_table} already exists; skipping the initial migrations;")
|
|
295
316
|
else:
|
|
296
317
|
migrate_initial(spark, initial_output_folder, cat, schema)
|
|
297
|
-
|
|
298
|
-
migrate_w_rev(spark, dbr_ouput_folder, migrations_root, cat, schema, DBR_ONLY)
|
|
299
|
-
migrate_w_rev(spark, all_spark_ouput_folder, migrations_root, cat, schema, ALL_SPARK)
|
|
318
|
+
migrate_w_rev(spark, client_output_folder, migrations_dir, cat, schema)
|
|
300
319
|
|
|
301
320
|
|
|
302
321
|
def main(cat, schema, migrations_dir, output_parent_path=None):
|
|
@@ -309,8 +328,8 @@ def _cli_main(cat, schema, migrations_dir): # pragma: no cover
|
|
|
309
328
|
main(cat, schema, migrations_dir)
|
|
310
329
|
|
|
311
330
|
|
|
312
|
-
def _cli_create_new_migration(message, output_path, template_path): # pragma: no cover
|
|
313
|
-
create_new_migration(message, template_path or get_default_template_path(), output_path)
|
|
331
|
+
def _cli_create_new_migration(message, output_path, template_path, add_is_dbr): # pragma: no cover
|
|
332
|
+
create_new_migration(message, template_path or get_default_template_path(), output_path, add_is_dbr=add_is_dbr)
|
|
314
333
|
|
|
315
334
|
|
|
316
335
|
def build_parser(): # pragma: no cover
|
|
@@ -320,15 +339,24 @@ def build_parser(): # pragma: no cover
|
|
|
320
339
|
p_run = subparsers.add_parser("run", help="apply all pending migrations")
|
|
321
340
|
p_run.add_argument("--cat", default="spark_catalog")
|
|
322
341
|
p_run.add_argument("--schema", default="default")
|
|
323
|
-
p_run.add_argument(
|
|
324
|
-
"--migrations-dir", required=True, help="parent of all_spark_migrations/ and dbr_only_migrations/"
|
|
325
|
-
)
|
|
342
|
+
p_run.add_argument("--migrations-dir", required=True, help="the directory holding the migration chain")
|
|
326
343
|
p_run.set_defaults(func=_cli_main)
|
|
327
344
|
|
|
328
345
|
p_create = subparsers.add_parser("create_new_migration", help="create a new migration from the template")
|
|
329
346
|
p_create.add_argument("--message", required=True)
|
|
330
|
-
p_create.add_argument(
|
|
347
|
+
p_create.add_argument(
|
|
348
|
+
"--output-path",
|
|
349
|
+
required=True,
|
|
350
|
+
help="the chain directory to write the new migration into. if sql files are in there, the last sql's rev_id"
|
|
351
|
+
"will be set as the new migrations prev_rev_id.",
|
|
352
|
+
)
|
|
331
353
|
p_create.add_argument("--template-path", default=None, help="defaults to the template shipped in this package")
|
|
354
|
+
# the %% are argparse's: it interpolates help strings
|
|
355
|
+
p_create.add_argument(
|
|
356
|
+
"--add-is-dbr",
|
|
357
|
+
action="store_true",
|
|
358
|
+
help="wrap the new migration's body in {%% if is_dbr %%}, for statements only databricks understands",
|
|
359
|
+
)
|
|
332
360
|
p_create.set_defaults(func=_cli_create_new_migration)
|
|
333
361
|
|
|
334
362
|
return parser
|
{spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spark_sql_migrations
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Chained, idempotent SQL migrations for Spark and Databricks.
|
|
5
5
|
Home-page: https://github.com/mcjug2015/spark_sql_migrations
|
|
6
6
|
Author: Victor Semenov
|
|
@@ -79,28 +79,32 @@ Migrations come from two different owners, and they behave differently:
|
|
|
79
79
|
| | ships in the wheel | owned by you |
|
|
80
80
|
|---|---|---|
|
|
81
81
|
| **what** | catalog, schema, `_spark_migrations_version` table | your tables and columns |
|
|
82
|
-
| **where** | `spark_sql_migrations/migrations_initial/` |
|
|
83
|
-
| **
|
|
82
|
+
| **where** | `spark_sql_migrations/migrations_initial/` | the directory you pass as `--migrations-dir` |
|
|
83
|
+
| **ordered by** | filename | the `prev_revision_id` chain |
|
|
84
84
|
| **when applied** | until the version table exists | once, then recorded |
|
|
85
85
|
|
|
86
86
|
The bootstrap chain is applied on every run until `_spark_migrations_version` exists, and
|
|
87
87
|
skipped from then on. A run that fails partway through bootstrapping replays it, so it must
|
|
88
|
-
still be idempotent. Your own
|
|
88
|
+
still be idempotent. Your own migrations are applied once each and the head revision is
|
|
89
89
|
written to the version table.
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
Both kinds are rendered the same way, so both can gate a statement on `is_dbr` — the
|
|
92
|
+
bootstrap chain uses it for `CREATE CATALOG`, which only Unity Catalog has.
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
## Laying out your chain
|
|
95
|
+
|
|
96
|
+
`--migrations-dir` is the directory holding the chain — the `.sql` files sit in it directly:
|
|
94
97
|
|
|
95
98
|
```
|
|
96
99
|
your_project/
|
|
97
|
-
└── migrations/
|
|
98
|
-
├──
|
|
99
|
-
└──
|
|
100
|
+
└── migrations/ <- this is --migrations-dir
|
|
101
|
+
├── 260831_01_create_test_table_e5ce0039b32b.sql
|
|
102
|
+
└── 260831_02_create_metrics_table_07990e2a101e.sql
|
|
100
103
|
```
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
One chain covers both engines: a Databricks-only *statement* gates on `is_dbr` inside a
|
|
106
|
+
migration that otherwise runs everywhere — see below. SQL that only Databricks understands
|
|
107
|
+
must never sit ungated there, or your local runs will break.
|
|
104
108
|
|
|
105
109
|
## Running migrations
|
|
106
110
|
|
|
@@ -124,7 +128,7 @@ from spark_sql_migrations.spark_sql.spark_sql import main
|
|
|
124
128
|
|
|
125
129
|
|
|
126
130
|
def get_migrations_dir():
|
|
127
|
-
"""the
|
|
131
|
+
"""the directory holding this project's own migration chain."""
|
|
128
132
|
return os.path.dirname(__file__)
|
|
129
133
|
|
|
130
134
|
|
|
@@ -134,7 +138,7 @@ main(cat="spark_catalog", schema="default", migrations_dir=get_migrations_dir())
|
|
|
134
138
|
`main` builds its own session via `get_spark()` and, being library code, leaves logging to
|
|
135
139
|
you. To supply your own — a session already
|
|
136
140
|
configured by your job, say — call `run_migrations(spark, cat, schema, output_folder,
|
|
137
|
-
|
|
141
|
+
migrations_dir)` directly.
|
|
138
142
|
|
|
139
143
|
Every rendered statement is written to a timestamped folder under `migrations_out/`
|
|
140
144
|
before it is executed, so you can always read the exact SQL a run applied.
|
|
@@ -144,7 +148,7 @@ before it is executed, so you can always read the exact SQL a run applied.
|
|
|
144
148
|
Generate one rather than hand-rolling the header:
|
|
145
149
|
|
|
146
150
|
```bash
|
|
147
|
-
spark-sql-migrations create_new_migration --message "add batch id to metrics" --output-path path/to/migrations
|
|
151
|
+
spark-sql-migrations create_new_migration --message "add batch id to metrics" --output-path path/to/migrations
|
|
148
152
|
```
|
|
149
153
|
|
|
150
154
|
That writes `<yymmdd>_<slug>_<revision_id>.sql` from the template shipped in the package
|
|
@@ -183,6 +187,44 @@ begin
|
|
|
183
187
|
end;
|
|
184
188
|
```
|
|
185
189
|
|
|
190
|
+
### Databricks-only statements
|
|
191
|
+
|
|
192
|
+
`is_dbr` is in the render context, so a migration in the shared chain can carry SQL only
|
|
193
|
+
Databricks understands:
|
|
194
|
+
|
|
195
|
+
```sql
|
|
196
|
+
-- revision_id:e8614b76da6d;
|
|
197
|
+
-- prev_revision_id:57037b6c19b4;
|
|
198
|
+
{% if is_dbr %}
|
|
199
|
+
begin
|
|
200
|
+
grant READ FILES on external location `some-bucket` to `some_group`;
|
|
201
|
+
end;
|
|
202
|
+
{% endif %}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`create_new_migration` writes that conditional for you with `--add-is-dbr`, below the
|
|
206
|
+
headers so they stay readable:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
spark-sql-migrations create_new_migration --message "read files on the cms zip external location" --output-path path/to/migrations --add-is-dbr
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
This has to happen at render time, not in SQL. OSS Spark rejects `GRANT` and the rest of
|
|
213
|
+
its unsupported-native-command list while *parsing*, so a `declare exit handler` in the same
|
|
214
|
+
script never gets the chance to run — and the whole script fails to compile, not just that
|
|
215
|
+
statement. Jinja removes the text before `spark.sql()` ever sees it.
|
|
216
|
+
|
|
217
|
+
A migration left holding nothing but comments is recorded in the version table and not
|
|
218
|
+
executed, so the chain advances in step on both engines. That recording is forward-only: a
|
|
219
|
+
catalog migrated locally past a gated revision will not pick that statement up if it is
|
|
220
|
+
later migrated from Databricks. For disposable test schemas that is exactly what you want;
|
|
221
|
+
for a long-lived catalog, don't migrate it from both.
|
|
222
|
+
|
|
223
|
+
`_spark_migrations_version` holds one row and one column, `version_num`. There is no
|
|
224
|
+
in-place upgrade from the older two-chain shape, which carried a `migration_type` column and
|
|
225
|
+
a row per chain: a catalog bootstrapped by an earlier release has to be rebuilt from
|
|
226
|
+
scratch, which also clears the revision history that layout recorded.
|
|
227
|
+
|
|
186
228
|
## Local vs Databricks
|
|
187
229
|
|
|
188
230
|
`get_spark()` returns a `DatabricksSession` when `is_dbr()` is true and a Delta-configured
|
{spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/SOURCES.txt
RENAMED
|
@@ -13,8 +13,8 @@ spark_sql_migrations.egg-info/namespace_packages.txt
|
|
|
13
13
|
spark_sql_migrations.egg-info/requires.txt
|
|
14
14
|
spark_sql_migrations.egg-info/top_level.txt
|
|
15
15
|
spark_sql_migrations/migration_templates/default_migration_template.sql
|
|
16
|
-
spark_sql_migrations/migrations_initial/
|
|
17
|
-
spark_sql_migrations/migrations_initial/
|
|
18
|
-
spark_sql_migrations/migrations_initial/
|
|
16
|
+
spark_sql_migrations/migrations_initial/first_01_intial_create_cat.sql
|
|
17
|
+
spark_sql_migrations/migrations_initial/first_02_intial_create_schema.sql
|
|
18
|
+
spark_sql_migrations/migrations_initial/first_03_intial_create_state_table.sql
|
|
19
19
|
spark_sql_migrations/spark_sql/__init__.py
|
|
20
20
|
spark_sql_migrations/spark_sql/spark_sql.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/spark_sql/__init__.py
RENAMED
|
File without changes
|
{spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations/spark_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spark_sql_migrations-0.0.4 → spark_sql_migrations-0.0.6}/spark_sql_migrations.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|