jade-sql 0.7.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.
- checksums.yaml +4 -4
- data/README.md +16 -10
- data/docs/building.md +398 -96
- data/docs/running.md +37 -17
- data/exe/jade-sql +37 -9
- data/lib/jade-sql/bin/generate_schema.rb +655 -44
- data/lib/jade-sql/compiler/assignable.rb +146 -0
- data/lib/jade-sql/compiler/columns.rb +220 -0
- data/lib/jade-sql/compiler/errors.rb +82 -0
- data/lib/jade-sql/compiler/selectable.rb +76 -0
- data/lib/jade-sql/compiler.rb +34 -0
- data/lib/jade-sql/runtime.rb +15 -11
- data/lib/jade-sql/schema_drift.rb +115 -0
- data/lib/jade-sql/sql/expr.jd +65 -0
- data/lib/jade-sql/sql/json.jd +154 -0
- data/lib/jade-sql/sql/query.jd +284 -92
- data/lib/jade-sql/sql/write.jd +625 -0
- data/lib/jade-sql/sql.jd +328 -90
- data/lib/jade-sql/tasks.rake +41 -8
- data/lib/jade-sql/version.rb +1 -1
- data/lib/jade-sql.rb +8 -7
- metadata +16 -8
- data/lib/jade-sql/sql/mutation.jd +0 -464
data/docs/running.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
# Running queries and
|
|
1
|
+
# Running queries and writes
|
|
2
2
|
|
|
3
|
-
`Sql`
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
- `
|
|
48
|
-
- `
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
begin
|
|
76
|
+
case (command = ARGV.shift)
|
|
77
|
+
in 'schema'
|
|
78
|
+
run_schema(ARGV)
|
|
57
79
|
|
|
58
|
-
in 'version' | '-v' | '--version'
|
|
59
|
-
|
|
80
|
+
in 'version' | '-v' | '--version'
|
|
81
|
+
puts JadeSql::VERSION
|
|
60
82
|
|
|
61
|
-
in nil | 'help' | '-h' | '--help'
|
|
62
|
-
|
|
83
|
+
in nil | 'help' | '-h' | '--help'
|
|
84
|
+
puts HELP
|
|
63
85
|
|
|
64
|
-
else
|
|
65
|
-
|
|
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
|