Skip to content

Commit 7abea70

Browse files
authored
Merge pull request #14 from 1ma/doc-tips
Add Tips section to README.md
2 parents 2895f87 + 35135ed commit 7abea70

File tree

1 file changed

+134
-1
lines changed

1 file changed

+134
-1
lines changed

README.md

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,140 @@ $migration->reset();
241241
$migration->up();
242242
```
243243

244-
The Migration object controls the database version.
244+
The Migration object controls the database version.
245+
246+
247+
### Tips on writing SQL migrations
248+
249+
#### Rely on explicit transactions
250+
251+
```sql
252+
-- DO
253+
BEGIN;
254+
255+
ALTER TABLE 1;
256+
UPDATE 1;
257+
UPDATE 2;
258+
UPDATE 3;
259+
ALTER TABLE 2;
260+
261+
COMMIT;
262+
263+
264+
-- DON'T
265+
ALTER TABLE 1;
266+
UPDATE 1;
267+
UPDATE 2;
268+
UPDATE 3;
269+
ALTER TABLE 2;
270+
```
271+
272+
It is generally desirable to wrap migration scripts inside a `BEGIN; ... COMMIT;` block.
273+
This way, if _any_ of the inner statements fail, _none_ of them are committed and the
274+
database does not end up in an inconsistent state.
275+
276+
Mind that in case of a failure `byjg/migration` will always mark the migration as `partial`
277+
and warn you when you attempt to run it again. The difference is that with explicit
278+
transactions you know that the database cannot be in an inconsistent state after an
279+
unexpected failure.
280+
281+
#### On creating triggers and SQL functions
282+
283+
```sql
284+
-- DO
285+
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
286+
BEGIN
287+
-- Check that empname and salary are given
288+
IF NEW.empname IS NULL THEN
289+
RAISE EXCEPTION 'empname cannot be null'; -- it doesn't matter if these comments are blank or not
290+
END IF; --
291+
IF NEW.salary IS NULL THEN
292+
RAISE EXCEPTION '% cannot have null salary', NEW.empname; --
293+
END IF; --
294+
295+
-- Who works for us when they must pay for it?
296+
IF NEW.salary < 0 THEN
297+
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; --
298+
END IF; --
299+
300+
-- Remember who changed the payroll when
301+
NEW.last_date := current_timestamp; --
302+
NEW.last_user := current_user; --
303+
RETURN NEW; --
304+
END; --
305+
$emp_stamp$ LANGUAGE plpgsql;
306+
307+
308+
-- DON'T
309+
CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
310+
BEGIN
311+
-- Check that empname and salary are given
312+
IF NEW.empname IS NULL THEN
313+
RAISE EXCEPTION 'empname cannot be null';
314+
END IF;
315+
IF NEW.salary IS NULL THEN
316+
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
317+
END IF;
318+
319+
-- Who works for us when they must pay for it?
320+
IF NEW.salary < 0 THEN
321+
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
322+
END IF;
323+
324+
-- Remember who changed the payroll when
325+
NEW.last_date := current_timestamp;
326+
NEW.last_user := current_user;
327+
RETURN NEW;
328+
END;
329+
$emp_stamp$ LANGUAGE plpgsql;
330+
```
331+
332+
Since the `PDO` database abstraction layer cannot run batches of SQL statements,
333+
when `byjg/migration` reads a migration file it has to split up the whole contents of the SQL
334+
file at the semicolons, and run the statements one by one. However, there is one kind of
335+
statement that can have multiple semicolons in-between its body: functions.
336+
337+
In order to be able to parse functions correctly, `byjg/migration` 2.1.0 started splitting migration
338+
files at the `semicolon + EOL` sequence instead of just the semicolon. This way, if you append an empty
339+
comment after every inner semicolon of a function definition `byjg/migration` will be able to parse it.
340+
341+
Unfortunately, if you forget to add any of these comments the library will split the `CREATE FUNCTION` statement in
342+
multiple parts and the migration will fail.
343+
344+
#### Avoid the colon character (`:`)
345+
346+
```sql
347+
-- DO
348+
CREATE TABLE bookings (
349+
booking_id UUID PRIMARY KEY,
350+
booked_at TIMESTAMPTZ NOT NULL CHECK (CAST(booked_at AS DATE) <= check_in),
351+
check_in DATE NOT NULL
352+
);
353+
354+
355+
-- DON'T
356+
CREATE TABLE bookings (
357+
booking_id UUID PRIMARY KEY,
358+
booked_at TIMESTAMPTZ NOT NULL CHECK (booked_at::DATE <= check_in),
359+
check_in DATE NOT NULL
360+
);
361+
```
362+
363+
Since `PDO` uses the colon character to prefix named parameters in prepared statements, its use will trip it
364+
up in other contexts.
365+
366+
For instance, PostgreSQL statements can use `::` to cast values between types. On the other hand `PDO` will
367+
read this as an invalid named parameter in an invalid context and fail when it tries to run it.
368+
369+
The only way to fix this inconsistency is avoiding colons altogether (in this case, PostgreSQL also has an alternative
370+
syntax: `CAST(value AS type)`).
371+
372+
#### Use an SQL editor
373+
374+
Finally, writing manual SQL migrations can be tiresome, but it is significantly easier if
375+
you use an editor capable of understanding the SQL syntax, providing autocomplete,
376+
introspecting your current database schema and/or autoformatting your code.
377+
245378

246379
### Handle different migration inside one schema
247380

0 commit comments

Comments
 (0)