Skip to content

Commit

Permalink
docs: add samples for Spanner-specific features (#492)
Browse files Browse the repository at this point in the history
* docs: add samples for Spanner-specific features

* docs: more samples

* docs: add more samples

* test: add tests for samples

* chore: fix linting error

* docs: document samples

* docs: link to README
  • Loading branch information
olavloite authored Nov 8, 2024
1 parent 93579c8 commit a6ed382
Show file tree
Hide file tree
Showing 14 changed files with 915 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/test_suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ jobs:
- name: Run mockserver tests
run: nox -s mockserver

samples:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install nox
run: python -m pip install nox
- name: Run samples
run: nox -s _all_samples
working-directory: samples

compliance_tests_13:
runs-on: ubuntu-latest

Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ Next install the package from the package ``setup.py`` file:

During setup the dialect will be registered with entry points.

Samples
-------------

The `samples directory <https://github.com/googleapis/python-spanner-sqlalchemy/blob/-/samples/README.md>`__
contains multiple examples for how to configure and use common Spanner features.


A Minimal App
-------------

Expand Down
30 changes: 30 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Spanner SQLAlchemy Samples

This folder contains samples for how to use common Spanner features with SQLAlchemy. The samples use
a shared [data model](model.py) and can be executed as a standalone application. The samples
automatically start the [Spanner Emulator](https://cloud.google.com/spanner/docs/emulator) in a
Docker container when they are executed. You must therefore have Docker installed on your system to
run a sample.

You can run a sample with `nox`:

```shell
nox -s hello_world
```

Change `hello_world` to run any of the other sample names. The runnable samples all end with
`_sample.py`. Omit the `_sample.py` part of the file name to run the sample.



| Sample name | Description |
|-----------------------|-----------------------------------------------------------------------------|
| bit_reversed_sequence | Use a bit-reversed sequence for primary key generation. |
| date_and_timestamp | Map Spanner DATE and TIMESTAMP columns to SQLAlchemy. |
| default_column_value | Create and use a Spanner DEFAULT column constraint in SQLAlchemy. |
| generated_column | Create and use a Spanner generated column in SQLAlchemy. |
| hello_world | Shows how to connect to Spanner with SQLAlchemy and execute a simple query. |
| insert_data | Insert multiple rows to Spanner with SQLAlchemy. |
| interleaved_table | Create and use an interleaved table (INTERLEAVE IN PARENT) with SQLAlchemy. |
| transaction | Execute a read/write transaction on Spanner with SQLAlchemy. |

70 changes: 70 additions & 0 deletions samples/bit_reversed_sequence_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2024 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime
import uuid

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from sample_helper import run_sample
from model import Singer, Concert, Venue, TicketSale


# Shows how to use a bit-reversed sequence for primary key generation.
#
# The TicketSale model uses a bit-reversed sequence for automatic primary key
# generation:
#
# id: Mapped[int] = mapped_column(
# BigInteger,
# Sequence("ticket_sale_id"),
# server_default=TextClause("GET_NEXT_SEQUENCE_VALUE(SEQUENCE ticket_sale_id)"),
# primary_key=True,
# )
#
# This leads to the following table definition:
#
# CREATE TABLE ticket_sales (
# id INT64 NOT NULL DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE ticket_sale_id)),
# ...
# ) PRIMARY KEY (id)
def bit_reversed_sequence_sample():
engine = create_engine(
"spanner:///projects/sample-project/"
"instances/sample-instance/"
"databases/sample-database",
echo=True,
)
with Session(engine) as session:
singer = Singer(id=str(uuid.uuid4()), first_name="John", last_name="Doe")
venue = Venue(code="CH", name="Concert Hall", active=True)
concert = Concert(
venue=venue,
start_time=datetime.datetime(2024, 11, 7, 19, 30, 0),
singer=singer,
title="John Doe - Live in Concert Hall",
)
# TicketSale automatically generates a primary key value using a
# bit-reversed sequence. We therefore do not need to specify a primary
# key value when we create an instance of TicketSale.
ticket_sale = TicketSale(
concert=concert, customer_name="Alice Doe", seats=["A010", "A011", "A012"]
)
session.add_all([singer, venue, concert, ticket_sale])
session.commit()


if __name__ == "__main__":
run_sample(bit_reversed_sequence_sample)
64 changes: 64 additions & 0 deletions samples/date_and_timestamp_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2024 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime
import uuid

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from sample_helper import run_sample
from model import Singer, Concert, Venue


# Shows how to map and use the DATE and TIMESTAMP data types in Spanner.
def date_and_timestamp_sample():
engine = create_engine(
"spanner:///projects/sample-project/"
"instances/sample-instance/"
"databases/sample-database",
echo=True,
)
with Session(engine) as session:
# Singer has a property birthdate, which is mapped to a DATE column.
# Use the datetime.date type for this.
singer = Singer(
id=str(uuid.uuid4()),
first_name="John",
last_name="Doe",
birthdate=datetime.date(1979, 10, 14),
)
venue = Venue(code="CH", name="Concert Hall", active=True)
# Concert has a property `start_time`, which is mapped to a TIMESTAMP
# column. Use the datetime.datetime type for this.
concert = Concert(
venue=venue,
start_time=datetime.datetime(2024, 11, 7, 19, 30, 0),
singer=singer,
title="John Doe - Live in Concert Hall",
)
session.add_all([singer, venue, concert])
session.commit()

# Use AUTOCOMMIT for sessions that only read. This is more
# efficient than using a read/write transaction to only read.
session.connection(execution_options={"isolation_level": "AUTOCOMMIT"})
print(
f"{singer.full_name}, born on {singer.birthdate}, has planned "
f"a concert that starts on {concert.start_time} in {venue.name}."
)


if __name__ == "__main__":
run_sample(date_and_timestamp_sample)
61 changes: 61 additions & 0 deletions samples/default_column_value_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2024 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime
import uuid

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from sample_helper import run_sample
from model import Singer, Album, Track


# Shows how to use a default column with SQLAlchemy and Spanner.
def default_column_value_sample():
engine = create_engine(
"spanner:///projects/sample-project/"
"instances/sample-instance/"
"databases/sample-database",
echo=True,
)
with Session(engine) as session:
# The Track model has a `recorded_at` property that is set to
# CURRENT_TIMESTAMP if no other value is supplied.
singer = Singer(id=str(uuid.uuid4()), first_name="John", last_name="Doe")
album = Album(id=str(uuid.uuid4()), title="My album", singer=singer)

# This track will use the default CURRENT_TIMESTAMP for the recorded_at
# property.
track1 = Track(
id=str(uuid.uuid4()),
track_number=1,
title="My track 1",
album=album,
)
track2 = Track(
id=str(uuid.uuid4()),
track_number=2,
title="My track 2",
recorded_at=datetime.datetime(2024, 11, 7, 10, 0, 0),
album=album,
)
session.add_all([singer, album, track1, track2])
session.commit()
print(f"Track 1 was recorded at: " f"{track1.recorded_at}")
print(f"Track 2 was recorded at: " f"{track2.recorded_at}")


if __name__ == "__main__":
run_sample(default_column_value_sample)
50 changes: 50 additions & 0 deletions samples/generated_column_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2024 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import uuid

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from sample_helper import run_sample
from model import Singer


# Shows how to use a generated column with SQLAlchemy and Spanner.
def generated_column_sample():
engine = create_engine(
"spanner:///projects/sample-project/"
"instances/sample-instance/"
"databases/sample-database",
echo=True,
)
with Session(engine) as session:
# The Singer model has a `full_name` property that is generated by the
# database.
singer = Singer(id=str(uuid.uuid4()), first_name="John", last_name="Doe")
session.add(singer)
session.commit()
print(
f"The database generated a full name for the singer: " f"{singer.full_name}"
)

# Updating the first name or last name of the singer will also update
# the generated full name property.
singer.last_name = "Jones"
session.commit()
print(f"Updated full name for singer: " f"{singer.full_name}")


if __name__ == "__main__":
run_sample(generated_column_sample)
31 changes: 31 additions & 0 deletions samples/hello_world_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2024 Google LLC All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from sqlalchemy import create_engine, select, text
from sample_helper import run_sample


def quickstart():
engine = create_engine(
"spanner:///projects/sample-project/"
"instances/sample-instance/"
"databases/sample-database"
)
with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as connection:
results = connection.execute(select(text("'Hello World!'"))).fetchall()
print("\nMessage from Spanner: ", results[0][0], "\n")


if __name__ == "__main__":
run_sample(quickstart)
Loading

0 comments on commit a6ed382

Please sign in to comment.