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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5b511ba76f20947fbccd7ebb7800746ca4ff7deaa7db27f02c0f9e0c2063fa5
4
- data.tar.gz: 4f280996410b7a415c14ce431879074d3ea14db4da9305358595738a8f31f674
3
+ metadata.gz: f5986c28a9014fcf2a656282a1c34d0303a2c6e37d10dbb5b1eb8591c5a1ebaf
4
+ data.tar.gz: 378c771dc40c70f3567727e4475b8383f28a723be48beff82dc5f5c25ea6e8f5
5
5
  SHA512:
6
- metadata.gz: df9d4acfa3ac509e4a9730a727befae3197cdd9abb9959c60d107191fb574898e1d75ca23eb7814ab5a6b80a81b0e7e5e2d2131954a47053726c0a1e3354e720
7
- data.tar.gz: 582f7bcacd1543c00d6efc7710762fe1b6b9a388c8e71c2fedaf006229d3ac7bce49510473e8ae3b97773314c6ead9445b5f38c8d6aeb678cfbc6c573adae763
6
+ metadata.gz: c09395f48662acf1c6ae08189f700229d03a23dbcfe170725f681094cb76357665dc18775c441eb7961f6afcdc841bf035e4fbe1da4d43e8bfe8d3e23f997af1
7
+ data.tar.gz: 52fe13eb1ed8ec5c43db442d85923e972b35c26e2be57939e75b8d0f0bb16817ab45404abd3db9ff640b7609e0d4491e199278691c9e07253be97f1bd298a5ed
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # jade-sql
2
2
 
3
+ [![CI](https://github.com/agustinrhcp/jade-sql/actions/workflows/ci.yml/badge.svg)](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`, `varchar`/`text`/`char` →
31
- `String`, `boolean` `Bool`, `jsonb`/`json` → `Decode.Value`, `date` →
32
- `Calendar.Date`, `timestamp` → `Clock.Instant`, `uuid` → `Uuid` (from
33
- `Sql.Uuid`). Unknown types fail loudly with the table+column name.
34
- `bytea` and `decimal`/`numeric` aren't wired into the type map yet
35
- `bytea` has a natural target in jade's `Bytes`, and a decimal can be
36
- modeled as a struct of `Int`s.
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
- parsed = parse_tables(sql)
70
+ bodies = scan_table_bodies(sql)
71
+ bodies = select_tables(bodies, tables) if tables
55
72
  pks = parse_pks(sql)
56
- parsed = parsed.map { |t| t.with(pk_columns: pks[t.name] || []) }
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
- def filter_tables(parsed, whitelist)
81
- missing = whitelist - parsed.map(&:name)
82
- raise "Unknown table(s): #{missing.join(', ')}" if missing.any?
83
-
84
- parsed.select { |t| whitelist.include?(t.name) }
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 parse_tables(sql)
88
- sql
89
- .scan(/CREATE TABLE (?:\w+\.)?(\w+)\s*\((.*?)\);/m)
90
- .map { |name, body| Table[name, parse_columns(body, name), []] }
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| [emit_strict_cols(t), emit_maybe_cols(t), emit_row(t), emit_table_fn(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
- exposed = tables
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
- .sort
138
- .join(", ")
163
+ names += projectored.map { |t| "#{t.name}_row" }
164
+ exposed = names.sort.join(", ")
139
165
 
140
- imports = ["import Sql exposing(Expr, Table, column, table)", *extra_imports_for(tables)]
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
@@ -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
@@ -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"
@@ -1,3 +1,3 @@
1
1
  module JadeSql
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
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.1.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-14 00:00:00.000000000 Z
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