jade-sql 0.1.0 → 0.2.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 +2 -0
- data/docs/building.md +35 -7
- data/lib/jade-sql/bin/generate_schema.rb +75 -20
- data/lib/jade-sql/runtime.rb +21 -0
- data/lib/jade-sql/sql/decimal.jd +193 -0
- data/lib/jade-sql/sql/query.jd +18 -0
- data/lib/jade-sql/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5986c28a9014fcf2a656282a1c34d0303a2c6e37d10dbb5b1eb8591c5a1ebaf
|
|
4
|
+
data.tar.gz: 378c771dc40c70f3567727e4475b8383f28a723be48beff82dc5f5c25ea6e8f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c09395f48662acf1c6ae08189f700229d03a23dbcfe170725f681094cb76357665dc18775c441eb7961f6afcdc841bf035e4fbe1da4d43e8bfe8d3e23f997af1
|
|
7
|
+
data.tar.gz: 52fe13eb1ed8ec5c43db442d85923e972b35c26e2be57939e75b8d0f0bb16817ab45404abd3db9ff640b7609e0d4491e199278691c9e07253be97f1bd298a5ed
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# jade-sql
|
|
2
2
|
|
|
3
|
+
[](https://github.com/agustinrhcp/jade-sql/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
Type-safe SQL for Jade. Builds queries and mutations from a generated
|
|
4
6
|
schema, renders them to `(String, List(Value))`, and runs them against
|
|
5
7
|
ActiveRecord via a Task port that auto-decodes rows into typed Jade
|
data/docs/building.md
CHANGED
|
@@ -27,13 +27,23 @@ bundle exec rake jade:schema \
|
|
|
27
27
|
OUTPUT=app/jade/schema/billing.jd
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Type map: `bigint`/`integer`/`smallint` → `Int`, `
|
|
31
|
-
`
|
|
32
|
-
`
|
|
33
|
-
`
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
Type map: `bigint`/`integer`/`smallint` → `Int`, `numeric`/`decimal` →
|
|
31
|
+
`Decimal` (from `Sql.Decimal`), `double precision`/`real` → `Float`,
|
|
32
|
+
`varchar`/`text`/`char` → `String`, `boolean` → `Bool`, `jsonb`/`json` →
|
|
33
|
+
`Decode.Value`, `date` → `Calendar.Date`, `timestamp` → `Clock.Instant`,
|
|
34
|
+
`uuid` → `Uuid` (from `Sql.Uuid`). Unknown types fail loudly with the
|
|
35
|
+
table+column name.
|
|
36
|
+
|
|
37
|
+
`numeric`/`decimal` map to `Sql.Decimal` — an exact base-10 value
|
|
38
|
+
(`mantissa * 10^exponent`), never `Float`, so no precision is lost. Genuine
|
|
39
|
+
floating-point columns (`double precision`/`real`) map to `Float`. `bytea`
|
|
40
|
+
isn't mapped yet, though jade's `Bytes` is the natural target.
|
|
41
|
+
|
|
42
|
+
`Sql.Decimal` implements `Numeric`, so `+`/`-`/`*` work and stay exact.
|
|
43
|
+
Mirroring `BigDecimal`, `/` can't represent a repeating quotient, so it
|
|
44
|
+
rounds half-up to a default scale (use `div(a, b, scale)` for explicit
|
|
45
|
+
control, `round(d, scale)` to round); division by zero raises. `to_i`
|
|
46
|
+
truncates toward zero and `to_float` converts (lossily, on purpose).
|
|
37
47
|
|
|
38
48
|
For each table, the generator emits:
|
|
39
49
|
|
|
@@ -51,6 +61,24 @@ Strict cols mirror NOT NULL constraints; the maybe version wraps every
|
|
|
51
61
|
field in `Maybe` for left-join projections. The default alias is the
|
|
52
62
|
table name; override per-call with `aliased` (see joins below).
|
|
53
63
|
|
|
64
|
+
A column whose name is a Jade keyword (e.g. `type`) gets a trailing
|
|
65
|
+
underscore in the struct field (`type_`) while the SQL column reference
|
|
66
|
+
keeps the real name. For a table with such a column the generator also
|
|
67
|
+
emits a `<table>_row` projector that aliases every column to its field name
|
|
68
|
+
(`SELECT … AS type_`), so reads round-trip without hand-written SQL:
|
|
69
|
+
|
|
70
|
+
```jade
|
|
71
|
+
def entries -> Q(Selector(JournalEntriesRow))
|
|
72
|
+
c <- from(journal_entries)
|
|
73
|
+
journal_entries_row(c)
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
In hand-written selects, `field_as(e, "name")` sets a column's output name
|
|
78
|
+
when it differs from the SQL — needed for renamed columns and computed
|
|
79
|
+
projections (decode keys by field name, so `field_as(count_all, "visits")`
|
|
80
|
+
makes a `COUNT(*)` land in a `visits` field).
|
|
81
|
+
|
|
54
82
|
## Build queries
|
|
55
83
|
|
|
56
84
|
```jade
|
|
@@ -23,10 +23,18 @@ module JadeSql
|
|
|
23
23
|
/\Adate\[\]/ => "List(Calendar.Date)",
|
|
24
24
|
/\Atimestamp\[\]/ => "List(Clock.Instant)",
|
|
25
25
|
/\Auuid\[\]/ => "List(Uuid)",
|
|
26
|
+
/\Anumeric\[\]/ => "List(Decimal)",
|
|
27
|
+
/\Adecimal\[\]/ => "List(Decimal)",
|
|
28
|
+
/\Adouble precision\[\]/ => "List(Float)",
|
|
29
|
+
/\Areal\[\]/ => "List(Float)",
|
|
26
30
|
|
|
27
31
|
/\Abigint\b/ => "Int",
|
|
28
32
|
/\Ainteger\b/ => "Int",
|
|
29
33
|
/\Asmallint\b/ => "Int",
|
|
34
|
+
/\Anumeric\b/ => "Decimal",
|
|
35
|
+
/\Adecimal\b/ => "Decimal",
|
|
36
|
+
/\Adouble precision\b/ => "Float",
|
|
37
|
+
/\Areal\b/ => "Float",
|
|
30
38
|
/\Acharacter varying\b/ => "String",
|
|
31
39
|
/\Avarchar\b/ => "String",
|
|
32
40
|
/\Acharacter\b/ => "String",
|
|
@@ -45,16 +53,24 @@ module JadeSql
|
|
|
45
53
|
"Clock.Instant" => "import Clock",
|
|
46
54
|
"Decode.Value" => "import Decode",
|
|
47
55
|
"Uuid" => "import Sql.Uuid exposing(Uuid)",
|
|
56
|
+
"Decimal" => "import Sql.Decimal exposing(Decimal)",
|
|
48
57
|
}.freeze
|
|
49
58
|
|
|
59
|
+
# Column names that collide with Jade keywords get a trailing underscore
|
|
60
|
+
# in the struct field; the SQL column reference keeps the real name.
|
|
61
|
+
RESERVED = %w[
|
|
62
|
+
type def module import exposing struct interface implements uses
|
|
63
|
+
case in if then else end with
|
|
64
|
+
].freeze
|
|
65
|
+
|
|
50
66
|
Table = Data.define(:name, :columns, :pk_columns)
|
|
51
67
|
Column = Data.define(:name, :jade_type, :nullable)
|
|
52
68
|
|
|
53
69
|
def generate(sql, tables: nil, module_name: 'Schema')
|
|
54
|
-
|
|
70
|
+
bodies = scan_table_bodies(sql)
|
|
71
|
+
bodies = select_tables(bodies, tables) if tables
|
|
55
72
|
pks = parse_pks(sql)
|
|
56
|
-
parsed =
|
|
57
|
-
parsed = filter_tables(parsed, tables) if tables
|
|
73
|
+
parsed = bodies.map { |name, body| Table[name, parse_columns(body, name), pks[name] || []] }
|
|
58
74
|
format(emit(parsed, module_name))
|
|
59
75
|
end
|
|
60
76
|
|
|
@@ -77,17 +93,18 @@ module JadeSql
|
|
|
77
93
|
|
|
78
94
|
private
|
|
79
95
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
# Returns [[name, body], ...] without parsing columns, so the whitelist
|
|
97
|
+
# can be applied before type-mapping — an unsupported type in a table the
|
|
98
|
+
# caller didn't ask for shouldn't abort the whole run.
|
|
99
|
+
def scan_table_bodies(sql)
|
|
100
|
+
sql.scan(/CREATE TABLE (?:\w+\.)?(\w+)\s*\((.*?)\);/m)
|
|
85
101
|
end
|
|
86
102
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
def select_tables(bodies, whitelist)
|
|
104
|
+
missing = whitelist - bodies.map(&:first)
|
|
105
|
+
raise "Unknown table(s): #{missing.join(', ')}" if missing.any?
|
|
106
|
+
|
|
107
|
+
bodies.select { |name, _| whitelist.include?(name) }
|
|
91
108
|
end
|
|
92
109
|
|
|
93
110
|
def parse_columns(body, table_name)
|
|
@@ -127,17 +144,30 @@ module JadeSql
|
|
|
127
144
|
def emit(tables, module_name)
|
|
128
145
|
[
|
|
129
146
|
emit_header(tables, module_name),
|
|
130
|
-
*tables.flat_map { |t|
|
|
147
|
+
*tables.flat_map { |t|
|
|
148
|
+
parts = [emit_strict_cols(t), emit_maybe_cols(t), emit_row(t), emit_table_fn(t)]
|
|
149
|
+
reserved_cols?(t) ? parts + [emit_row_projector(t)] : parts
|
|
150
|
+
},
|
|
131
151
|
].join("\n\n") + "\n"
|
|
132
152
|
end
|
|
133
153
|
|
|
154
|
+
def reserved_cols?(t)
|
|
155
|
+
t.columns.any? { |c| RESERVED.include?(c.name) }
|
|
156
|
+
end
|
|
157
|
+
|
|
134
158
|
def emit_header(tables, module_name)
|
|
135
|
-
|
|
159
|
+
projectored = tables.select { |t| reserved_cols?(t) }
|
|
160
|
+
|
|
161
|
+
names = tables
|
|
136
162
|
.flat_map { |t| ["#{camel(t.name)}Cols", "Maybe#{camel(t.name)}Cols", "#{camel(t.name)}Row(..)", t.name] }
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
names += projectored.map { |t| "#{t.name}_row" }
|
|
164
|
+
exposed = names.sort.join(", ")
|
|
139
165
|
|
|
140
|
-
|
|
166
|
+
sql_import = projectored.any? ?
|
|
167
|
+
"import Sql exposing(Expr, Selector, Table, column, table)" :
|
|
168
|
+
"import Sql exposing(Expr, Table, column, table)"
|
|
169
|
+
query_import = projectored.any? ? ["import Sql.Query exposing(Q, field_as, select)"] : []
|
|
170
|
+
imports = [sql_import, *query_import, *extra_imports_for(tables)]
|
|
141
171
|
|
|
142
172
|
<<~JADE.strip
|
|
143
173
|
module #{module_name} exposing(#{exposed})
|
|
@@ -158,7 +188,7 @@ module JadeSql
|
|
|
158
188
|
|
|
159
189
|
def emit_strict_cols(t)
|
|
160
190
|
fields = t.columns
|
|
161
|
-
.map { |c| " #{c.name}: Expr(#{c.nullable ? "Maybe(#{c.jade_type})" : c.jade_type})" }
|
|
191
|
+
.map { |c| " #{field_name(c.name)}: Expr(#{c.nullable ? "Maybe(#{c.jade_type})" : c.jade_type})" }
|
|
162
192
|
.join(",\n")
|
|
163
193
|
|
|
164
194
|
"struct #{camel(t.name)}Cols = {\n#{fields}\n}"
|
|
@@ -166,7 +196,7 @@ module JadeSql
|
|
|
166
196
|
|
|
167
197
|
def emit_maybe_cols(t)
|
|
168
198
|
fields = t.columns
|
|
169
|
-
.map { |c| " #{c.name}: Expr(Maybe(#{c.jade_type}))" }
|
|
199
|
+
.map { |c| " #{field_name(c.name)}: Expr(Maybe(#{c.jade_type}))" }
|
|
170
200
|
.join(",\n")
|
|
171
201
|
|
|
172
202
|
"struct Maybe#{camel(t.name)}Cols = {\n#{fields}\n}"
|
|
@@ -174,12 +204,16 @@ module JadeSql
|
|
|
174
204
|
|
|
175
205
|
def emit_row(t)
|
|
176
206
|
fields = t.columns
|
|
177
|
-
.map { |c| " #{c.name}: #{c.nullable ? "Maybe(#{c.jade_type})" : c.jade_type}" }
|
|
207
|
+
.map { |c| " #{field_name(c.name)}: #{c.nullable ? "Maybe(#{c.jade_type})" : c.jade_type}" }
|
|
178
208
|
.join(",\n")
|
|
179
209
|
|
|
180
210
|
"struct #{camel(t.name)}Row = {\n#{fields}\n}"
|
|
181
211
|
end
|
|
182
212
|
|
|
213
|
+
def field_name(name)
|
|
214
|
+
RESERVED.include?(name) ? "#{name}_" : name
|
|
215
|
+
end
|
|
216
|
+
|
|
183
217
|
def emit_table_fn(t)
|
|
184
218
|
strict_fields = t.columns.map { |c| "column(a, #{c.name.inspect})" }.join(", ")
|
|
185
219
|
maybe_fields = t.columns.map { |c| "column(a, #{c.name.inspect})" }.join(", ")
|
|
@@ -198,6 +232,27 @@ module JadeSql
|
|
|
198
232
|
JADE
|
|
199
233
|
end
|
|
200
234
|
|
|
235
|
+
# A row projector that aliases every column to its (possibly renamed)
|
|
236
|
+
# field name, so a reserved-word column like `type` round-trips through
|
|
237
|
+
# decode: `SELECT alias.type AS type_`. Emitted only for tables that have
|
|
238
|
+
# a renamed column. Composes in a bind-chain:
|
|
239
|
+
# c <- from(t)
|
|
240
|
+
# t_row(c) |> where(...)
|
|
241
|
+
def emit_row_projector(t)
|
|
242
|
+
klass = camel(t.name)
|
|
243
|
+
holes = t.columns.map { "_" }.join(", ")
|
|
244
|
+
projections = t.columns
|
|
245
|
+
.map { |c| " |> field_as(c.#{field_name(c.name)}, #{field_name(c.name).inspect})" }
|
|
246
|
+
.join("\n")
|
|
247
|
+
|
|
248
|
+
<<~JADE.strip
|
|
249
|
+
def #{t.name}_row(c: #{klass}Cols) -> Q(Selector(#{klass}Row))
|
|
250
|
+
select(#{klass}Row(#{holes}))
|
|
251
|
+
#{projections}
|
|
252
|
+
end
|
|
253
|
+
JADE
|
|
254
|
+
end
|
|
255
|
+
|
|
201
256
|
def camel(snake)
|
|
202
257
|
snake.split('_').map(&:capitalize).join
|
|
203
258
|
end
|
data/lib/jade-sql/runtime.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'date'
|
|
2
|
+
require 'bigdecimal'
|
|
2
3
|
require 'jade/tasks'
|
|
3
4
|
|
|
4
5
|
# Opt-in runtime: requires ActiveRecord. The Jade-side Sql.Run module
|
|
@@ -73,6 +74,10 @@ module JadeSql
|
|
|
73
74
|
# when AR's exec_query path doesn't run the OID typecast. Parse them
|
|
74
75
|
# back to Ruby Arrays so `Decode.list(...)` works the same as for any
|
|
75
76
|
# other List(a) column.
|
|
77
|
+
#
|
|
78
|
+
# numeric/decimal columns come back as ::BigDecimal; the schema generator
|
|
79
|
+
# maps them to Sql.Decimal, whose decoder reads the exact
|
|
80
|
+
# "<mantissa>e<exponent>" wire form. Float would lose precision, so don't.
|
|
76
81
|
def self.coerce_row(row)
|
|
77
82
|
row.transform_values { |v| coerce_value(v) }
|
|
78
83
|
end
|
|
@@ -81,12 +86,28 @@ module JadeSql
|
|
|
81
86
|
case v
|
|
82
87
|
when ::Date then v.iso8601
|
|
83
88
|
when ::Time, ::DateTime then v.iso8601
|
|
89
|
+
when ::BigDecimal then decimal_wire(v)
|
|
84
90
|
when ::String
|
|
85
91
|
pg_array_literal?(v) ? parse_pg_array(v) : v
|
|
86
92
|
else v
|
|
87
93
|
end
|
|
88
94
|
end
|
|
89
95
|
|
|
96
|
+
# ::BigDecimal -> "<mantissa>e<exponent>" with value = mantissa * 10^exp,
|
|
97
|
+
# exactly (BigDecimal#split gives sign, significant digits, and a base-10
|
|
98
|
+
# exponent). Matches the wire form Sql.Decimal's decoder parses.
|
|
99
|
+
#
|
|
100
|
+
# 'NaN'/'Infinity'::numeric are legal Postgres values that Sql.Decimal
|
|
101
|
+
# can't represent; fail loudly rather than silently decode them as 0.
|
|
102
|
+
def self.decimal_wire(v)
|
|
103
|
+
raise ArgumentError, "non-finite numeric: #{v}" unless v.finite?
|
|
104
|
+
|
|
105
|
+
sign, digits, _base, exp = v.split
|
|
106
|
+
mantissa = sign * digits.to_i
|
|
107
|
+
exponent = exp - digits.length
|
|
108
|
+
"#{mantissa}e#{exponent}"
|
|
109
|
+
end
|
|
110
|
+
|
|
90
111
|
# PG arrays render as `{}`, `{a,b,c}`, `{"a,b","c"}`, with NULL as
|
|
91
112
|
# bare `NULL`. Quoted elements escape `"` and `\` with backslashes.
|
|
92
113
|
# The JSON-object guard rejects `{"key":...}` shapes — they share the
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
module Sql.Decimal exposing (
|
|
2
|
+
Decimal,
|
|
3
|
+
decimal,
|
|
4
|
+
div,
|
|
5
|
+
exponent,
|
|
6
|
+
mantissa,
|
|
7
|
+
parse,
|
|
8
|
+
round,
|
|
9
|
+
to_float,
|
|
10
|
+
to_i,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
import Decode exposing (Decodable, Decoder)
|
|
14
|
+
import Encode exposing (Encodable)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# An exact base-10 decimal: value = mantissa * 10 ^ exponent. Lossless for
|
|
18
|
+
# Postgres numeric/decimal — no Float rounding. The wire form is
|
|
19
|
+
# "<mantissa>e<exponent>" (e.g. "175e-3"), which Postgres casts to numeric
|
|
20
|
+
# exactly and which decodes back without parsing a decimal point.
|
|
21
|
+
type Decimal = Decimal(Int, Int)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def decimal(m: Int, e: Int) -> Decimal
|
|
25
|
+
Decimal(m, e)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mantissa(d: Decimal) -> Int
|
|
30
|
+
Decimal(m, _) = d
|
|
31
|
+
|
|
32
|
+
m
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def exponent(d: Decimal) -> Int
|
|
37
|
+
Decimal(_, e) = d
|
|
38
|
+
|
|
39
|
+
e
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def pow10(n: Int) -> Int
|
|
44
|
+
n <= 0 ? 1 : 10 * pow10(n - 1)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def abs(x: Int) -> Int
|
|
49
|
+
x < 0 ? 0 - x : x
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Integer division, rounding ties half away from zero — i.e. Java's HALF_UP
|
|
54
|
+
# and Ruby BigDecimal's default mode (2.5 -> 3, -2.5 -> -3). Rounds the
|
|
55
|
+
# magnitude, then re-applies the sign. A zero `den` raises through Int `/`.
|
|
56
|
+
def round_div(num: Int, den: Int) -> Int
|
|
57
|
+
negative = not ((num < 0) == (den < 0))
|
|
58
|
+
n = abs(num)
|
|
59
|
+
d = abs(den)
|
|
60
|
+
q = n / d
|
|
61
|
+
r = n - q * d
|
|
62
|
+
rounded = (2 * r) >= d ? q + 1 : q
|
|
63
|
+
|
|
64
|
+
negative ? 0 - rounded : rounded
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def trunc_div(num: Int, den: Int) -> Int
|
|
69
|
+
q = abs(num) / abs(den)
|
|
70
|
+
|
|
71
|
+
num < 0 ? 0 - q : q
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# `+` `-` `*` are exact. `+`/`-` line the operands up on the smaller exponent
|
|
76
|
+
# first; `*` adds exponents.
|
|
77
|
+
def add(a: Decimal, b: Decimal) -> Decimal
|
|
78
|
+
Decimal(ma, ea) = a
|
|
79
|
+
Decimal(mb, eb) = b
|
|
80
|
+
e = ea < eb ? ea : eb
|
|
81
|
+
|
|
82
|
+
Decimal((ma * pow10(ea - e)) + (mb * pow10(eb - e)), e)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def sub(a: Decimal, b: Decimal) -> Decimal
|
|
87
|
+
Decimal(ma, ea) = a
|
|
88
|
+
Decimal(mb, eb) = b
|
|
89
|
+
e = ea < eb ? ea : eb
|
|
90
|
+
|
|
91
|
+
Decimal((ma * pow10(ea - e)) - (mb * pow10(eb - e)), e)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def mul(a: Decimal, b: Decimal) -> Decimal
|
|
96
|
+
Decimal(ma, ea) = a
|
|
97
|
+
Decimal(mb, eb) = b
|
|
98
|
+
|
|
99
|
+
Decimal(ma * mb, ea + eb)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# a / b rounded half-up to `scale` decimal places — like Java's
|
|
104
|
+
# BigDecimal.divide(divisor, scale, HALF_UP). Division by zero raises.
|
|
105
|
+
def div(a: Decimal, b: Decimal, scale: Int) -> Decimal
|
|
106
|
+
Decimal(ma, ea) = a
|
|
107
|
+
Decimal(mb, eb) = b
|
|
108
|
+
k = (ea - eb) + scale
|
|
109
|
+
num = k >= 0 ? ma * pow10(k) : ma
|
|
110
|
+
den = k >= 0 ? mb : mb * pow10(0 - k)
|
|
111
|
+
|
|
112
|
+
Decimal(round_div(num, den), 0 - scale)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# The Numeric `/`: like Ruby's BigDecimal `/`, it can't be exact for a
|
|
117
|
+
# repeating quotient (1/3), so it rounds half-up to a generous default scale.
|
|
118
|
+
def divide(a: Decimal, b: Decimal) -> Decimal
|
|
119
|
+
div(a, b, 32)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
implements Numeric(Decimal) with
|
|
124
|
+
(+): add,
|
|
125
|
+
(-): sub,
|
|
126
|
+
(*): mul,
|
|
127
|
+
(/): divide
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Round to `scale` decimal places, half-up. Fewer places than the value
|
|
132
|
+
# already has are left untouched.
|
|
133
|
+
def round(d: Decimal, scale: Int) -> Decimal
|
|
134
|
+
Decimal(m, e) = d
|
|
135
|
+
shift = e + scale
|
|
136
|
+
|
|
137
|
+
shift >= 0 ? d : Decimal(round_div(m, pow10(0 - shift)), 0 - scale)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Truncates toward zero (drops the fractional part).
|
|
142
|
+
def to_i(d: Decimal) -> Int
|
|
143
|
+
Decimal(m, e) = d
|
|
144
|
+
|
|
145
|
+
e >= 0 ? m * pow10(e) : trunc_div(m, pow10(0 - e))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def to_float(d: Decimal) -> Float
|
|
150
|
+
Decimal(m, e) = d
|
|
151
|
+
|
|
152
|
+
e >= 0
|
|
153
|
+
? Basics.to_float(m * pow10(e))
|
|
154
|
+
: Basics.to_float(m) / Basics.to_float(pow10(0 - e))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def to_wire(d: Decimal) -> String
|
|
159
|
+
Decimal(m, e) = d
|
|
160
|
+
|
|
161
|
+
String.from_int(m) ++ "e" ++ String.from_int(e)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse(s: String) -> Maybe(Decimal)
|
|
166
|
+
case String.split(s, "e")
|
|
167
|
+
in [m, e]
|
|
168
|
+
String.to_int(m)
|
|
169
|
+
|> Maybe.and_then((mi) -> {
|
|
170
|
+
String.to_int(e) |> Maybe.map((ei) -> { Decimal(mi, ei) })
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
else Nothing
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def decimal_decoder(s: String) -> Decoder(Decimal)
|
|
179
|
+
case parse(s)
|
|
180
|
+
in Just(d) then Decode.succeed(d)
|
|
181
|
+
in Nothing then Decode.fail("invalid decimal: " ++ s)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
implements Decodable(Decimal) with
|
|
187
|
+
decoder: -> { Decode.string |> Decode.and_then(decimal_decoder) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
implements Encodable(Decimal) with
|
|
192
|
+
encoder: (d) -> { Encode.string(to_wire(d)) }
|
|
193
|
+
end
|
data/lib/jade-sql/sql/query.jd
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Sql.Query exposing (
|
|
2
2
|
Q,
|
|
3
3
|
field,
|
|
4
|
+
field_as,
|
|
4
5
|
from,
|
|
5
6
|
group,
|
|
6
7
|
join,
|
|
@@ -235,6 +236,23 @@ def field(qs: Q(Selector(a -> b)), e: Expr(a)) -> Q(Selector(b))
|
|
|
235
236
|
end
|
|
236
237
|
|
|
237
238
|
|
|
239
|
+
def field_as(qs: Q(Selector(a -> b)), e: Expr(a), name: String) -> Q(Selector(b))
|
|
240
|
+
Q(
|
|
241
|
+
qs.tables,
|
|
242
|
+
qs.joins,
|
|
243
|
+
qs.wheres,
|
|
244
|
+
qs.groups,
|
|
245
|
+
qs.orders,
|
|
246
|
+
qs.limit_,
|
|
247
|
+
qs.offset_,
|
|
248
|
+
Selector(
|
|
249
|
+
qs.result.columns_sql ++ [e.sql ++ " AS " ++ name],
|
|
250
|
+
qs.result.params ++ e.params,
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
|
|
238
256
|
def join_keyword(k: JoinKind) -> String
|
|
239
257
|
case k
|
|
240
258
|
in InnerJ then "INNER JOIN"
|
data/lib/jade-sql/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jade-sql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- agustin
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-06-
|
|
10
|
+
date: 2026-06-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: jade-lang
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- lib/jade-sql/bin/generate_schema.rb
|
|
43
43
|
- lib/jade-sql/runtime.rb
|
|
44
44
|
- lib/jade-sql/sql.jd
|
|
45
|
+
- lib/jade-sql/sql/decimal.jd
|
|
45
46
|
- lib/jade-sql/sql/loader.jd
|
|
46
47
|
- lib/jade-sql/sql/mutation.jd
|
|
47
48
|
- lib/jade-sql/sql/query.jd
|