Skip to content

Commit 0e266ab

Browse files
authored
migrations - add support for package imports (#12)
1 parent 1a7875c commit 0e266ab

File tree

3 files changed

+58
-28
lines changed

3 files changed

+58
-28
lines changed

README.md

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ This is a lightweight ORM written in Python and built on top of Cloud Spanner.
88

99
Make sure that Python 3.7 is the default version of python for your environment,
1010
then run:
11-
```pip install spanner-orm```
11+
`pip install spanner-orm`
1212

1313
### Connecting
14+
1415
To connect the Spanner ORM to an existing Spanner database:
15-
``` python
16+
17+
```python
1618
import spanner_orm
1719
spanner_orm.connect(instance_name, database_name)
1820
```
@@ -27,9 +29,11 @@ libraries seems to not work, and the thread code associated with using the Pingi
2729
also seems to not do what is intended (ping the pool every so often)
2830

2931
### Creating a model
32+
3033
In order to write to and read from a table on Spanner, you need to tell the ORM
3134
about the table by writing a model class, which looks something like this:
32-
``` python
35+
36+
```python
3337
import spanner_orm
3438

3539
class TestModel(spanner_orm.Model):
@@ -67,7 +71,7 @@ the corresponding table on the database through the ORM in one of two ways. If
6771
the database has not yet been created, we can create it and the table at the
6872
same time by:
6973

70-
``` python
74+
```python
7175
admin_api = spanner_orm.connect_admin(
7276
'instance_name',
7377
'database_name',
@@ -79,16 +83,16 @@ If the database already exists, we can execute a Migration where the upgrade
7983
method returns a CreateTable for the model you have just defined (see section
8084
on migrations)
8185

82-
8386
### Retrieve data from Spanner
87+
8488
All queries through Spanner take place in a
8589
[transaction](https://cloud.google.com/spanner/docs/transactions). The ORM
8690
usually expects a transaction to be present and provided, but if None is
8791
specified, a new transaction will be created for that request.
88-
The two main ways of retrieving data through the ORM are ```where()``` and
89-
```find()```/```find_multi()```:
92+
The two main ways of retrieving data through the ORM are `where()` and
93+
`find()`/`find_multi()`:
9094

91-
``` python
95+
```python
9296
# where() is invokes on a model class to retrieve models of that tyep. it takes a
9397
# transaction and then a sequence of conditions.
9498
# Most conditions that specify a Field, Index, Relationship, or Model can take
@@ -118,56 +122,63 @@ specific_object = finder(1)
118122
```
119123

120124
### Write data to Spanner
125+
121126
The simplest way to write data is to create a Model (or retrieve one and modify
122127
it) and then call save() on it:
123-
``` python
128+
129+
```python
124130
test_model = TestModel({'key': 'key', 'value': 1})
125131
test_model.save()
126132
```
133+
127134
Note that creating a model as per above will fail if there's already a row in
128135
the database where the primary key matches, as it uses a Spanner INSERT instead
129136
of an UPDATE, as the ORM thinks it's a new object, as it wasn't retrieved from
130137
Spanner.
131138

132-
For modifying multiple objects at the same time, the Model ```save_batch()``` method
139+
For modifying multiple objects at the same time, the Model `save_batch()` method
133140
can be used:
134-
``` python
141+
142+
```python
135143
models = []
136144
for i in range(10):
137145
key = 'test_{}'.format(i)
138146
models.append(TestModel({'key': key, 'value': value}))
139147
TestModel.save_batch(None, models)
140148
```
141149

142-
```spanner_orm.spanner_api().run_write()``` can be used for executing read-write
143-
transactions, or the ```transactional_write``` decorator can be used similarly
150+
`spanner_orm.spanner_api().run_write()` can be used for executing read-write
151+
transactions, or the `transactional_write` decorator can be used similarly
144152
to the read decorator mentioned above. Note that if a transaction fails due to
145153
data being modified after the read happened and before the transaction finished
146154
executing, the called method will be re-run until it succeeds or a certain
147-
number of failures happen. Make sure that there are no side effects that could
155+
number of failures happen. Make sure that there are no side effects that could
148156
cause issues if called multiple times. Exceptions thrown out of the called
149157
method will abort the transaction.
150158

151-
Other helper methods exist for more complex use cases (```create```, ```update```,
152-
```upsert```, and others), but you will have to do more work in order to use those
159+
Other helper methods exist for more complex use cases (`create`, `update`,
160+
`upsert`, and others), but you will have to do more work in order to use those
153161
correctly. See the documentation on those methods for more information.
154162

155163
## Migrations
164+
156165
### Creating migrations
157-
Running ```spanner-orm generate <migration name>``` will generate a new
166+
167+
Running `spanner-orm generate <migration name>` will generate a new
158168
migration file to be filled out in the directory specified (or 'migrations' by
159-
default). The ```upgrade``` function is executed when migrating, and the
160-
```downgrade``` function is executed when rolling back the migration. Each of
169+
default). The `upgrade` function is executed when migrating, and the
170+
`downgrade` function is executed when rolling back the migration. Each of
161171
these should return a single SchemaUpdate object (e.g., CreateTable, AddColumn,
162172
etc.), as Spanner cannot execute multiple schema updates atomically.
163173

164174
### Executing migrations
165-
Running ```spanner-orm migrate <Spanner instance> <Spanner database>``` will
175+
176+
Running `spanner-orm migrate <Spanner instance> <Spanner database>` will
166177
execute all the unmigrated migrations for that database in the correct order,
167178
using the application default credentials. If that won't work for your use case,
168-
```MigrationExecutor``` can be used instead:
179+
`MigrationExecutor` can be used instead:
169180

170-
``` python
181+
```python
171182
connection = spanner_orm.SpannerConnection(
172183
instance_name,
173184
database_name,
@@ -176,12 +187,22 @@ executor = spanner_orm.MigrationExecutor(connection)
176187
executor.migrate()
177188
```
178189

190+
- **Note:** If you need `MigrationExecutor` to import migration files under a
191+
particular package name (i.e. not as parent packages) then provide the package
192+
name and migration files will be imported using the full module name like so:
193+
194+
```python
195+
# Migration files will be imported as `project.migrations.migration_name`
196+
# Allows you to import other modules from `project` into your migration files
197+
executor = spanner_orm.MigrationExecutor(connection, pkg_name="project.migrations")
198+
```
199+
179200
Note that there is no protection against trying execute migrations concurrently
180201
multiple times, so try not to do that.
181202

182203
If a migration needs to be rolled back,
183-
```spanner-orm rollback <migration_name> <Spanner instance> <Spanner database>```
184-
or the corresponding ```MigrationExecutor``` method should be used.
204+
`spanner-orm rollback <migration_name> <Spanner instance> <Spanner database>`
205+
or the corresponding `MigrationExecutor` method should be used.
185206

186-
To see a list of all migrations found, run ```spanner-orm showmigrations <Spanner instance> <Spanner database>```.
207+
To see a list of all migrations found, run `spanner-orm showmigrations <Spanner instance> <Spanner database>`.
187208
Migrations that have already been applied migrations are marked by an `[X]`.

spanner_orm/admin/migration_executor.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ class MigrationExecutor:
3434
"""Handles execution of migrations."""
3535

3636
def __init__(
37-
self, connection: api.SpannerConnection, basedir: Optional[str] = None
37+
self,
38+
connection: api.SpannerConnection,
39+
basedir: Optional[str] = None,
40+
pkg_name: Optional[str] = None,
3841
):
39-
self._manager = migration_manager.MigrationManager(basedir)
42+
self._manager = migration_manager.MigrationManager(basedir, pkg_name)
4043
self._migration_status_map = None
4144
self._connection = connection
4245

spanner_orm/admin/migration_manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ class MigrationManager:
3131

3232
DEFAULT_DIRECTORY = "migrations"
3333

34-
def __init__(self, basedir: Optional[str] = None):
34+
def __init__(self, basedir: Optional[str] = None, pkg_name: Optional[str] = None):
3535
self.basedir = basedir or self.DEFAULT_DIRECTORY
3636
self._migrations = None
37+
self._pkg_name = pkg_name
3738

3839
if not os.path.exists(self.basedir):
3940
os.makedirs(self.basedir)
@@ -75,6 +76,11 @@ def _migration_from_file(self, filename: str) -> migration.Migration:
7576
"""Loads a single migration from the given filename in the base dir."""
7677
module_name = re.sub(r"\W", "_", filename)
7778
path = os.path.join(self.basedir, filename)
79+
80+
if self._pkg_name is not None:
81+
# Prepend package name to module name to import full module name
82+
module_name = "{}.{}".format(self._pkg_name, module_name)
83+
7884
spec = importlib.util.spec_from_file_location(module_name, path)
7985
module = importlib.util.module_from_spec(spec)
8086
spec.loader.exec_module(module)

0 commit comments

Comments
 (0)