Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .spelling-wordlist.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
API
APIs
autocommit
BIGINT
Datatypes
DBC
Expand All @@ -10,6 +11,7 @@ DSN
DSNs
ENV
hostname
ie
ini
iODBC
jdbc
Expand Down Expand Up @@ -38,13 +40,16 @@ RDBMS
schemas
se
SMALLINT
sth
SQLBigInteger
SQLFloat
SQLInteger
SQLNull
SQLReal
SQLSmallInteger
SQLState
SQLStates
SQLTables
SQLVarchar
STMT
targeting
Expand Down
51 changes: 50 additions & 1 deletion docs/simple/populating.md
Original file line number Diff line number Diff line change
@@ -1 +1,50 @@
# Placeholder
# Populating Our Tables

We're going to manually insert two of the Play names into our database to make the process as clear as possible:

## Preparing Statements

When we are executing any statements that use any kind of user-input or are going to be repeated with different data or parameters, we should always use `prepare()?` and `execute()?`, instead of `direct_exec()?`. The reasons for this are SQL Injection vulnerabilities and performance.

A prepared statement allows you to create tokens in your SQL Statement that are replaced with data at runtime. For example:

```pony
sth.prepare("insert into play (name) values (?)")?
```

This creates a _parameter_ placeholder which we need to populate with the correct value to insert into the database.

Let's write a simple function to populate the play table:

```pony
fun populate_play_table(sth: ODBCStmt)? =>
var name: SQLVarchar = SQLVarchar(31)
sth
.> prepare("insert into play (name) values (?)")?
.> bind_parameter(name)?
```

When we bind parameters we have to bind them in order. If there were multiple parameters in our `prepare()?` statement then we would have to execute multiple `bind_parameter()?` statements, with the variables and types in the correct order. We will see this later when we populate more complex tables.

Now that we have our statement fully prepared, we can populate data and execute it:

```pony
name.write("Romeo and Juliet")
sth.execute()?

name.write("A Midsummer nights dream")
sth.execute()?
```

Please note that we are reusing the same statement handle (sth), and the same bound parameter (name). In doing so we do not have to do the SQL statement parsing, query setup, memory allocation for our buffers, and binding said newly allocated buffers.

Of course, in our example you'll need to add a call to this function on line 17 of our example:

```pony
let sth: ODBCStmt = dbh.stmt()?
create_tables(sth)?
populate_play_table(sth)?
else
```

Next up, let's query the data!
63 changes: 62 additions & 1 deletion docs/simple/queries.md
Original file line number Diff line number Diff line change
@@ -1 +1,62 @@
# Placeholder
# Simple Queries

Let's write a simple function to query the database for the id (I64) for a specific play in the play table.

## Preparing the Query

A reminder that our tables schema looks like this:

```sql
CREATE TEMPORARY TABLE play (
id BIGSERIAL,
name VARCHAR(30) UNIQUE NOT NULL
);
```

In order to fulfil our function, we will need to provide a SQLVarchar _in_, and a SQLBigInteger _out_.

```pony
fun play_id_from_name(sth: ODBCStmt, queryname: String): I64 ? -=>
var id: SQLBigInteger = SQLBigInteger
var name: SQLVarchar = SQLVarchar(31)
```

Like before, we need to bind our name _parameter_ to our query using `bind_parameter()?`. In addition, we need to bind a _column_ for every column that will be in the query's result set. We do this using the somewhat intuitive `bind_column()?` function:

```pony
sth
.> prepare("select id from play where name = ?")?
.> bind_parameter(name)?
.> bind_column(id)?
```

Then we can write our value to the name _parameter_, execute the query and fetch the (singular, due to name being UNIQUE) result back.

```pony
name.write(queryname)

sth.execute()?

if (sth.fetch()?) then
sth.finish()?
id.read()?
else
error // No row returned
end
```

NOTE: There is a trap here. You *must* check the return value of `fetch()?`. If you do not, you are going to end up with the previous value of `id`, not the one that would be returned. (Arguably in this case it doesn't matter as the value of `id` defaults to SQLNull so an `id.read()?` would fail regardless). But let's try to set a good example eh?

Let's add some example calls to our Main.create function to test this:

```pony
create_tables(sth)?
populate_play_table(sth)?
Debug.out(" R&J: " + play_id_from_name(sth, "Romeo and Juliet")?.string())
Debug.out("MSND: " + play_id_from_name(sth, "A Midsummer nights dream")?.string())
try
play_id_from_name(sth, "I don't exist")?
else
Debug.out("I don't exist doesn't exist™")
end
```
4 changes: 2 additions & 2 deletions docs/simple/sqltypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ As mentioned previously, all data is written in and out of the database via text

For every SQL Datatype we create we have to also support a Nullable version. Instead of doing this by defining duplicate types for everything, we chose instead to take the following approach:

## Reading NULLs
### Reading NULLs

```pony
var my_sql_integer: SQLInteger = SQLInteger
Expand All @@ -35,7 +35,7 @@ If your schema has marked a column as `NOT NULL`, then you can safely call `read

If however you try to `read()?` the value directly without testing for NULL and it is NULL, the function will error.

## Writing NULLs
### Writing NULLs

All pony SQL Types default to NULL, so all that needs be done is to create your object and not set a value.

Expand Down
76 changes: 75 additions & 1 deletion docs/simple/transactions.md
Original file line number Diff line number Diff line change
@@ -1 +1,75 @@
# Placeholder
# Transactions, Commits, and Rollbacks

We have gone out of our way so far to ensure that none of the changes we have made to our database have been persistent. We did that so that during this tutorial we can be sure that every time we tested our program we did so from a known state. It's time to rip that band-aid off and move to transactions.

## Database Transactions

A database transaction is a sequence of one or more operations—such as reading, writing, updating, or deleting data—performed on a database as a single, logical unit of work. Transactions ensure that either all operations are successfully completed (committed) or none are applied (rolled back), maintaining data integrity even in the event of system failures. In other words, all commands in a transaction are either applied in an atomic manner, or rejected.

ODBC by default commits every command you execute after you execute it. This is called "autocommit".

In order to implement transactions, we need to disable autocommit in Main.create:

```pony
try
let dbh: ODBCDbc = enh.dbc()?
dbh.set_autocommit(false)?
dbh.connect("psql-demo")?
```

Now remove all of the TEMPORARY keywords from the SQL create statements and run out example again multiple times. As we didn't commit the transaction, the tables we added and inserted data on are removed automatically from the database on disconnect.

## Testing for a table's existence

In order to ensure that we do not attempt to recreate the tables if the tables have been created by a previous run, we need a function to determine if a table exists.

We're going to assume in this example that if we only commit our database when all of our tables have been successfully created, we can treat all the tables as a unit and only test for one table.

There is an API call which allows us to search for tables. We can use this API call as a simple way to identify if a table exists:

```pony
fun check_table_exists(sth: ODBCStmt, tablename: String): Bool ? =>
sth.tables("", "", tablename, "TABLE")?
var rt: Bool = sth.fetch()?
sth.finish()?
rt
```

`tables()?`, aka SQLTables call behaves like a SQL statement, so we must ensure that the ODBCStmt that we pass it is pristine. We can either do that by creating a new one, or by ensuring that `finish()?` was called before it to reset it. You should not call `execute()?` after calling `tables()?` - it is implied. We did not bind any columns to this query because as we don't need the data. We just need to know if the data exists.

The function `fetch()?` returns `true` if there was a row of data (ie, our table exists), or `false` if there is no data.

Now we can modify our program to only create tables if the tables don't exist… and commit them if they are successful!

```pony
if (not check_table_exists(sth, "play")?) then
create_tables(sth)?
dbh.commit()?
end
```

## Testing if a table contains data

A simple SQL query will do this.

```pony
fun check_play_populated(sth: ODBCStmt): Bool ? =>
var cnt: SQLBigInteger = SQLBigInteger
sth
.> finish()?
.> bind_column(cnt)?
.> direct_exec("select count(*) from play")?
.> fetch()?
.> finish()?

(cnt.read()? > 0)
```

Now we can likewise gate the call to `populate_play_table()?` and execute a `commit()?` on success.

```pony
if (not check_play_populated(sth)?) then
populate_play_table(sth)?
dbh.commit()?
end
```