jade-sql 0.6.0 → 0.8.0

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.
data/docs/running.md CHANGED
@@ -1,12 +1,17 @@
1
- # Running queries and mutations
1
+ # Running queries and writes
2
2
 
3
- `Sql` exposes `fetch_one` / `fetch_many` for reads and `execute` for
4
- writes all polymorphic over anything `Renderable` (Q, Mutation, …)
5
- plus `*_raw` siblings that take a `(String, List(Value))` pair for the
6
- escape hatch:
3
+ `Sql.Query` and `Sql.Write` each run what they build: `fetch_one` /
4
+ `fetch_many` for reads, `execute` for writes. They live in the builder
5
+ modules rather than in `Sql` because that is where the row type is known —
6
+ a `Select(Patient)` fetches a `Patient` and nothing else.
7
+
8
+ `Sql` keeps the `*_raw` siblings, which take a `(String, List(Value))` pair
9
+ and cannot know what they return:
7
10
 
8
11
  ```jade
9
- import Sql exposing (SqlError, execute, execute_raw, fetch_many, fetch_one)
12
+ import Sql exposing (SqlError, execute_raw)
13
+ import Sql.Query exposing (fetch_many, fetch_one)
14
+ import Sql.Write exposing (execute)
10
15
 
11
16
  # Affected count for INSERT/UPDATE/DELETE
12
17
  def reschedule(a: Appointment) -> Task(Int, SqlError)
@@ -29,12 +34,21 @@ def count_active -> Task(Int, SqlError)
29
34
  end
30
35
  ```
31
36
 
32
- `fetch_one` / `fetch_many` / `execute` accept anything that implements
33
- `Sql.Renderable` (Q, Mutation). Internally they call `render(r) |> *_raw`,
34
- where `render` is the interface method that resolves to each container's
35
- `to_sql`. For raw SQL, skip the builder and call `fetch_one_raw` /
36
- `fetch_many_raw` / `execute_raw` directly with a `(String, List(Value))`
37
- pair.
37
+ Each runner is typed against what its module builds, so the row type the
38
+ query was written to produce is the one it hands back:
39
+
40
+ ```jade
41
+ Sql.Query.fetch_one : Select(a) -> Task(a, SqlError)
42
+ Sql.Write.fetch_one : Write(ret, c) -> Task(ret, SqlError)
43
+ ```
44
+
45
+ A write only has a row type once `returning` gives it one, which is what
46
+ makes fetching from one meaningful.
47
+
48
+ For raw SQL, skip the builders: `fetch_one_raw` / `fetch_many_raw` /
49
+ `execute_raw` take a `(String, List(Value))` pair. Their result type is
50
+ unconstrained, which is honest — nothing about a hand-written string says
51
+ what it returns.
38
52
 
39
53
  Row decoding is automatic — the caller's type (`Patient`, `List(Patient)`)
40
54
  threads its `Decodable` instance into the polymorphic port. The runtime
@@ -44,8 +58,8 @@ at the boundary.
44
58
  `SqlError` variants:
45
59
  - `DbError(String)` — AR `StatementInvalid` message
46
60
  - `NotFound` — `fetch_one` with zero rows
47
- - `NotUnique` — `fetch_one` with more than one row
48
- - `Conflict(String)` — a write hit a unique index; the `String` is the
61
+ - `TooManyRows` — `fetch_one` with more than one row
62
+ - `UniqueViolation(String)` — a write hit a unique index; the `String` is the
49
63
  violated constraint name (e.g. `users_email_key`), so you can route it to a
50
64
  field error instead of string-matching a `DbError` message
51
65
 
@@ -74,9 +88,15 @@ Because the wrapped task keeps its own decoding, `transaction` is fully
74
88
  polymorphic in the result — `transaction(t) : Task(a, SqlError)` for any
75
89
  `t : Task(a, SqlError)`.
76
90
 
77
- Transactions don't nest yet (no savepoints): wrapping a `transaction`
78
- inside another issues a second `BEGIN` on the same connection. Needs
79
- opt-in via `require 'jade-sql/runtime'`.
91
+ Transactions nest. A `transaction` inside another becomes a savepoint of
92
+ it, so an inner `Err` that the caller recovers from rolls back only the
93
+ inner work, while an outer `Err` still rolls back everything — including
94
+ what a nested transaction committed. The same holds in the other
95
+ direction: a jade transaction inside an `ActiveRecord::Base.transaction`
96
+ block is a savepoint of that block, and rolling the block back discards
97
+ the jade work with it.
98
+
99
+ Needs opt-in via `require 'jade-sql/runtime'`.
80
100
 
81
101
  ## Testing without a DB
82
102
 
data/exe/jade-sql CHANGED
@@ -9,12 +9,14 @@ def run_schema(args)
9
9
  require 'fileutils'
10
10
  require 'jade-sql'
11
11
  require 'jade-sql/bin/generate_schema'
12
+ require 'jade-sql/schema_drift'
12
13
 
13
14
  opts = {
14
15
  input: 'db/structure.sql',
15
16
  output: 'app/jade/schema.jd',
16
17
  module_name: 'Schema',
17
18
  tables: nil,
19
+ check: false,
18
20
  }
19
21
 
20
22
  OptionParser.new do |o|
@@ -25,6 +27,7 @@ def run_schema(args)
25
27
  opts[:tables] = it.split(',').map(&:strip).reject(&:empty?)
26
28
  }
27
29
  o.on('-m', '--module NAME', 'module name (default: Schema)') { opts[:module_name] = it }
30
+ o.on('-c', '--check', 'report drift and exit non-zero, writing nothing') { opts[:check] = true }
28
31
  end.parse!(args)
29
32
 
30
33
  generated = JadeSql::SchemaGenerator.generate(
@@ -33,11 +36,28 @@ def run_schema(args)
33
36
  module_name: opts[:module_name],
34
37
  )
35
38
 
39
+ return check(generated, opts[:output]) if opts[:check]
40
+
36
41
  FileUtils.mkdir_p(File.dirname(opts[:output]))
37
42
  File.write(opts[:output], generated)
38
43
  puts "wrote #{opts[:output]}"
39
44
  end
40
45
 
46
+ # For CI: the database moved, the schema did not, and every type built on
47
+ # it is now describing a table that is not there.
48
+ def check(generated, output)
49
+ unless File.exist?(output)
50
+ warn "#{output} does not exist. Generate it with `jade-sql schema`."
51
+ exit 1
52
+ end
53
+
54
+ report = JadeSql::SchemaDrift.between(generated, File.read(output))
55
+ return puts("#{output} matches the database.") unless report.any?
56
+
57
+ warn report.to_s
58
+ exit 1
59
+ end
60
+
41
61
  HELP = <<~USAGE
42
62
  jade-sql #{JadeSql::VERSION} — type-safe SQL for Jade
43
63
 
@@ -45,23 +65,31 @@ HELP = <<~USAGE
45
65
 
46
66
  Commands:
47
67
  schema Generate schema.jd from a SQL structure dump
68
+ (`--check` reports drift instead of writing)
48
69
  version Print the version
49
70
  help Show this message
50
71
 
51
72
  Run `jade-sql schema --help` for schema options.
52
73
  USAGE
53
74
 
54
- case (command = ARGV.shift)
55
- in 'schema'
56
- run_schema(ARGV)
75
+ begin
76
+ case (command = ARGV.shift)
77
+ in 'schema'
78
+ run_schema(ARGV)
57
79
 
58
- in 'version' | '-v' | '--version'
59
- puts JadeSql::VERSION
80
+ in 'version' | '-v' | '--version'
81
+ puts JadeSql::VERSION
60
82
 
61
- in nil | 'help' | '-h' | '--help'
62
- puts HELP
83
+ in nil | 'help' | '-h' | '--help'
84
+ puts HELP
63
85
 
64
- else
65
- warn "jade-sql: unknown command '#{command}' (try `jade-sql help`)"
86
+ else
87
+ warn "jade-sql: unknown command '#{command}' (try `jade-sql help`)"
88
+ exit 1
89
+ end
90
+ rescue RuntimeError => e
91
+ # What the DDL holds that the generator cannot read. The message names it;
92
+ # a backtrace through the emitters would only bury that.
93
+ warn "jade-sql: #{e.message}"
66
94
  exit 1
67
95
  end