herringbone 0.2.0 → 0.3.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 +56 -4
- data/lib/herringbone/active_record.rb +44 -12
- data/lib/herringbone/bloom_filter.rb +112 -12
- data/lib/herringbone/byte_values.rb +29 -0
- data/lib/herringbone/codecs/lz4.rb +66 -3
- data/lib/herringbone/codecs/snappy.rb +132 -3
- data/lib/herringbone/compression.rb +48 -0
- data/lib/herringbone/encodings/delta.rb +70 -13
- data/lib/herringbone/encodings/plain.rb +22 -0
- data/lib/herringbone/encodings/rle.rb +53 -1
- data/lib/herringbone/format.rb +150 -0
- data/lib/herringbone/inspector.rb +477 -81
- data/lib/herringbone/io_buffer_support.rb +3 -1
- data/lib/herringbone/reader/column_chunk_reader.rb +98 -6
- data/lib/herringbone/reader/column_cursor.rb +46 -2
- data/lib/herringbone/reader/numo.rb +365 -0
- data/lib/herringbone/reader/page_stream.rb +280 -4
- data/lib/herringbone/reader/scan.rb +103 -9
- data/lib/herringbone/reader.rb +308 -17
- data/lib/herringbone/schema.rb +306 -15
- data/lib/herringbone/thrift.rb +155 -4
- data/lib/herringbone/types.rb +176 -41
- data/lib/herringbone/version.rb +2 -1
- data/lib/herringbone/visualizer.rb +51 -11
- data/lib/herringbone/writer.rb +308 -31
- data/lib/herringbone/xxhash.rb +108 -2
- data/lib/herringbone.rb +28 -0
- metadata +3 -2
data/lib/herringbone/xxhash.rb
CHANGED
|
@@ -15,13 +15,21 @@ module Herringbone
|
|
|
15
15
|
# arithmetic is written once with the small code generators below, whose output is inlined into
|
|
16
16
|
# the hashing methods: no method calls or allocations in the hot paths.
|
|
17
17
|
module XXHash
|
|
18
|
+
# 64-bit mask
|
|
18
19
|
M = 0xFFFF_FFFF_FFFF_FFFF
|
|
20
|
+
# XXH64 PRIME64_1
|
|
19
21
|
P1 = 11_400_714_785_074_694_791
|
|
22
|
+
# XXH64 PRIME64_2
|
|
20
23
|
P2 = 14_029_467_366_897_019_727
|
|
24
|
+
# XXH64 PRIME64_3
|
|
21
25
|
P3 = 1_609_587_929_392_839_161
|
|
26
|
+
# XXH64 PRIME64_4
|
|
22
27
|
P4 = 9_650_029_242_287_828_579
|
|
28
|
+
# XXH64 PRIME64_5
|
|
23
29
|
P5 = 2_870_177_450_012_600_261
|
|
30
|
+
# 32-bit mask
|
|
24
31
|
M32 = 0xFFFF_FFFF
|
|
32
|
+
# unpack format splitting the input into little-endian 32-bit words
|
|
25
33
|
WORDS = "V*" # frozen, unlike a literal in the generated code
|
|
26
34
|
|
|
27
35
|
# Gem providing the native implementation
|
|
@@ -35,8 +43,14 @@ module Herringbone
|
|
|
35
43
|
# Shifts are written as multiplications and divisions by powers of two, which YARV has
|
|
36
44
|
# specialized instructions for (<< and >> on Integers are method calls).
|
|
37
45
|
#
|
|
38
|
-
# (hi:lo) = (hi:lo) * c mod 2
|
|
46
|
+
# (hi:lo) = (hi:lo) * c mod 2^64. Products are at most 48 bits wide: the low halves are
|
|
39
47
|
# multiplied by 16-bit pieces of c, and only the low 32 bits of the cross terms are kept.
|
|
48
|
+
#
|
|
49
|
+
# @param hi [String] variable holding the high 32 bits
|
|
50
|
+
# @param lo [String] variable holding the low 32 bits
|
|
51
|
+
# @param c [Integer] unsigned 64-bit constant multiplier
|
|
52
|
+
# @param hi_zero [Boolean] whether +hi+ is known to be 0, which drops its cross terms
|
|
53
|
+
# @return [String] Ruby source (uses t0 and t1 as scratch)
|
|
40
54
|
def mul(hi, lo, c, hi_zero: false)
|
|
41
55
|
ch = c >> 32
|
|
42
56
|
cl = c & M32
|
|
@@ -51,6 +65,11 @@ module Herringbone
|
|
|
51
65
|
end
|
|
52
66
|
|
|
53
67
|
# Rotate (hi:lo) left by r bits (0 < r < 32)
|
|
68
|
+
#
|
|
69
|
+
# @param hi [String] variable holding the high 32 bits
|
|
70
|
+
# @param lo [String] variable holding the low 32 bits
|
|
71
|
+
# @param r [Integer] rotation in bits, 1..31
|
|
72
|
+
# @return [String] Ruby source (uses t0 as scratch)
|
|
54
73
|
def rotl(hi, lo, r)
|
|
55
74
|
mask = (1 << (32 - r)) - 1
|
|
56
75
|
<<~RUBY
|
|
@@ -61,6 +80,12 @@ module Herringbone
|
|
|
61
80
|
end
|
|
62
81
|
|
|
63
82
|
# (hi:lo) += (a_hi:a_lo), where the addend is two expressions (constants or variables)
|
|
83
|
+
#
|
|
84
|
+
# @param hi [String] variable holding the high 32 bits
|
|
85
|
+
# @param lo [String] variable holding the low 32 bits
|
|
86
|
+
# @param a_hi [String, Integer] expression for the addend's high 32 bits
|
|
87
|
+
# @param a_lo [String, Integer] expression for the addend's low 32 bits
|
|
88
|
+
# @return [String] Ruby source
|
|
64
89
|
def add(hi, lo, a_hi, a_lo)
|
|
65
90
|
<<~RUBY
|
|
66
91
|
#{lo} += #{a_lo}
|
|
@@ -69,43 +94,82 @@ module Herringbone
|
|
|
69
94
|
RUBY
|
|
70
95
|
end
|
|
71
96
|
|
|
97
|
+
# (hi:lo) += c, mod 2^64
|
|
98
|
+
#
|
|
99
|
+
# @param hi [String] variable holding the high 32 bits
|
|
100
|
+
# @param lo [String] variable holding the low 32 bits
|
|
101
|
+
# @param c [Integer] unsigned 64-bit constant addend
|
|
102
|
+
# @return [String] Ruby source
|
|
72
103
|
def add_const(hi, lo, c)
|
|
73
104
|
add(hi, lo, c >> 32, c & M32)
|
|
74
105
|
end
|
|
75
106
|
|
|
76
107
|
# Assigns the 64-bit constant c to (hi:lo)
|
|
108
|
+
#
|
|
109
|
+
# @param hi [String] variable for the high 32 bits
|
|
110
|
+
# @param lo [String] variable for the low 32 bits
|
|
111
|
+
# @param c [Integer] unsigned 64-bit constant
|
|
112
|
+
# @return [String] Ruby source
|
|
77
113
|
def set(hi, lo, c)
|
|
78
114
|
"#{hi} = #{c >> 32}\n#{lo} = #{c & M32}\n"
|
|
79
115
|
end
|
|
80
116
|
|
|
81
117
|
# XXH64 round with a zero accumulator: (hi:lo) = rotl(lane * P2, 31) * P1
|
|
118
|
+
#
|
|
119
|
+
# @param hi [String] variable holding the lane's high 32 bits, replaced by the result's
|
|
120
|
+
# @param lo [String] variable holding the lane's low 32 bits, replaced by the result's
|
|
121
|
+
# @return [String] Ruby source
|
|
82
122
|
def round0(hi, lo)
|
|
83
123
|
mul(hi, lo, P2) + rotl(hi, lo, 31) + mul(hi, lo, P1)
|
|
84
124
|
end
|
|
85
125
|
|
|
86
126
|
# Stripe round: acc = rotl(acc + lane * P2, 31) * P1, with the lane in (xh:xl)
|
|
127
|
+
#
|
|
128
|
+
# @param hi [String] variable holding the accumulator's high 32 bits
|
|
129
|
+
# @param lo [String] variable holding the accumulator's low 32 bits
|
|
130
|
+
# @param xh [String] variable holding the lane's high 32 bits (clobbered)
|
|
131
|
+
# @param xl [String] variable holding the lane's low 32 bits (clobbered)
|
|
132
|
+
# @return [String] Ruby source
|
|
87
133
|
def round(hi, lo, xh, xl)
|
|
88
134
|
mul(xh, xl, P2) + add(hi, lo, xh, xl) + rotl(hi, lo, 31) + mul(hi, lo, P1)
|
|
89
135
|
end
|
|
90
136
|
|
|
91
137
|
# h ^= rotl(v * P2, 31) * P1; h = h * P1 + P4, with v in (vh:vl) (left unchanged)
|
|
138
|
+
#
|
|
139
|
+
# @param hi [String] variable holding h's high 32 bits
|
|
140
|
+
# @param lo [String] variable holding h's low 32 bits
|
|
141
|
+
# @param vh [String] variable holding the accumulator v's high 32 bits
|
|
142
|
+
# @param vl [String] variable holding the accumulator v's low 32 bits
|
|
143
|
+
# @return [String] Ruby source (uses xh and xl as scratch)
|
|
92
144
|
def merge_round(hi, lo, vh, vl)
|
|
93
145
|
"xh = #{vh}\nxl = #{vl}\n" + round0("xh", "xl") +
|
|
94
146
|
"#{hi} ^= xh\n#{lo} ^= xl\n" + mul(hi, lo, P1) + add_const(hi, lo, P4)
|
|
95
147
|
end
|
|
96
148
|
|
|
97
149
|
# Consumes an 8-byte lane in (xh:xl)
|
|
150
|
+
#
|
|
151
|
+
# @param hi [String] variable holding the hash's high 32 bits
|
|
152
|
+
# @param lo [String] variable holding the hash's low 32 bits
|
|
153
|
+
# @return [String] Ruby source
|
|
98
154
|
def lane8(hi, lo)
|
|
99
155
|
round0("xh", "xl") + "#{hi} ^= xh\n#{lo} ^= xl\n" + rotl(hi, lo, 27) + mul(hi, lo, P1) + add_const(hi, lo, P4)
|
|
100
156
|
end
|
|
101
157
|
|
|
102
158
|
# Consumes a 4-byte word in xl
|
|
159
|
+
#
|
|
160
|
+
# @param hi [String] variable holding the hash's high 32 bits
|
|
161
|
+
# @param lo [String] variable holding the hash's low 32 bits
|
|
162
|
+
# @return [String] Ruby source (sets xh)
|
|
103
163
|
def lane4(hi, lo)
|
|
104
164
|
"xh = 0\n" + mul("xh", "xl", P1, hi_zero: true) + "#{hi} ^= xh\n#{lo} ^= xl\n" +
|
|
105
165
|
rotl(hi, lo, 23) + mul(hi, lo, P2) + add_const(hi, lo, P3)
|
|
106
166
|
end
|
|
107
167
|
|
|
108
|
-
# Consumes one byte in xl (as it is below 2
|
|
168
|
+
# Consumes one byte in xl (as it is below 2^16, byte * P5 needs no splitting)
|
|
169
|
+
#
|
|
170
|
+
# @param hi [String] variable holding the hash's high 32 bits
|
|
171
|
+
# @param lo [String] variable holding the hash's low 32 bits
|
|
172
|
+
# @return [String] Ruby source
|
|
109
173
|
def lane1(hi, lo)
|
|
110
174
|
<<~RUBY + rotl(hi, lo, 11) + mul(hi, lo, P1)
|
|
111
175
|
t0 = xl * #{P5 & M32}
|
|
@@ -115,12 +179,20 @@ module Herringbone
|
|
|
115
179
|
end
|
|
116
180
|
|
|
117
181
|
# Final mix; evaluates to the hash as one Integer
|
|
182
|
+
#
|
|
183
|
+
# @param hi [String] variable holding the hash's high 32 bits
|
|
184
|
+
# @param lo [String] variable holding the hash's low 32 bits
|
|
185
|
+
# @return [String] Ruby source whose last expression is the 64-bit hash
|
|
118
186
|
def avalanche(hi, lo)
|
|
119
187
|
"#{lo} ^= #{hi} / 2\n" + mul(hi, lo, P2) +
|
|
120
188
|
"#{lo} ^= (#{hi} & 0x1FFFFFFF) * 8 | #{lo} / 536870912\n#{hi} ^= #{hi} / 536870912\n" +
|
|
121
189
|
mul(hi, lo, P3) + "#{lo} ^= #{hi}\n(#{hi} << 32) | #{lo}\n"
|
|
122
190
|
end
|
|
123
191
|
|
|
192
|
+
# Source of the pure-Ruby hashing methods, evaluated into XXHash: +ruby_xxh64(bytes)+,
|
|
193
|
+
# +ruby_xxh64_lane(xh, xl)+ (8 bytes as two 32-bit halves) and +ruby_xxh64_u32(xl)+ (4 bytes)
|
|
194
|
+
#
|
|
195
|
+
# @return [String] Ruby source defining the three singleton methods
|
|
124
196
|
def source
|
|
125
197
|
g = self
|
|
126
198
|
<<~RUBY
|
|
@@ -219,6 +291,9 @@ module Herringbone
|
|
|
219
291
|
|
|
220
292
|
class << self
|
|
221
293
|
# XXH64 of a String's bytes, as an unsigned 64-bit Integer
|
|
294
|
+
#
|
|
295
|
+
# @param bytes [String] data to hash (its encoding is ignored)
|
|
296
|
+
# @return [Integer] the hash, 0...2^64
|
|
222
297
|
def xxh64(bytes)
|
|
223
298
|
native = @native
|
|
224
299
|
native = resolve_backend if native.nil?
|
|
@@ -227,6 +302,9 @@ module Herringbone
|
|
|
227
302
|
|
|
228
303
|
# XXH64 of 8 bytes given as a little-endian 64-bit Integer (an INT64 or DOUBLE's PLAIN
|
|
229
304
|
# encoding), signed or unsigned: only its low 64 bits are used
|
|
305
|
+
#
|
|
306
|
+
# @param lane [Integer] value whose low 64 bits are hashed
|
|
307
|
+
# @return [Integer] the hash, 0...2^64
|
|
230
308
|
def xxh64_u64(lane)
|
|
231
309
|
native = @native
|
|
232
310
|
native = resolve_backend if native.nil?
|
|
@@ -235,6 +313,9 @@ module Herringbone
|
|
|
235
313
|
end
|
|
236
314
|
|
|
237
315
|
# XXH64 of 4 bytes given as a little-endian 32-bit Integer (INT32, FLOAT), signed or unsigned
|
|
316
|
+
#
|
|
317
|
+
# @param word [Integer] value whose low 32 bits are hashed
|
|
318
|
+
# @return [Integer] the hash, 0...2^64
|
|
238
319
|
def xxh64_u32(word)
|
|
239
320
|
native = @native
|
|
240
321
|
native = resolve_backend if native.nil?
|
|
@@ -243,6 +324,9 @@ module Herringbone
|
|
|
243
324
|
end
|
|
244
325
|
|
|
245
326
|
# Hashes of many 64-bit Integers (low 64 bits of each)
|
|
327
|
+
#
|
|
328
|
+
# @param lanes [Array<Integer>] values to hash, signed or unsigned
|
|
329
|
+
# @return [Array<Integer>] the hashes, in the same order
|
|
246
330
|
def xxh64_u64_all(lanes)
|
|
247
331
|
native = @native
|
|
248
332
|
native = resolve_backend if native.nil?
|
|
@@ -255,6 +339,9 @@ module Herringbone
|
|
|
255
339
|
end
|
|
256
340
|
|
|
257
341
|
# Hashes of many 32-bit Integers (low 32 bits of each)
|
|
342
|
+
#
|
|
343
|
+
# @param words [Array<Integer>] values to hash, signed or unsigned
|
|
344
|
+
# @return [Array<Integer>] the hashes, in the same order
|
|
258
345
|
def xxh64_u32_all(words)
|
|
259
346
|
native = @native
|
|
260
347
|
native = resolve_backend if native.nil?
|
|
@@ -267,6 +354,9 @@ module Herringbone
|
|
|
267
354
|
end
|
|
268
355
|
|
|
269
356
|
# Hashes of many Strings
|
|
357
|
+
#
|
|
358
|
+
# @param strings [Array<String>] values whose bytes are hashed
|
|
359
|
+
# @return [Array<Integer>] the hashes, in the same order
|
|
270
360
|
def xxh64_all(strings)
|
|
271
361
|
native = @native
|
|
272
362
|
native = resolve_backend if native.nil?
|
|
@@ -274,6 +364,8 @@ module Herringbone
|
|
|
274
364
|
end
|
|
275
365
|
|
|
276
366
|
# :native when the xxhash gem is used, :ruby otherwise
|
|
367
|
+
#
|
|
368
|
+
# @return [Symbol] +:native+ or +:ruby+
|
|
277
369
|
def backend
|
|
278
370
|
native = @native
|
|
279
371
|
native = resolve_backend if native.nil?
|
|
@@ -282,6 +374,11 @@ module Herringbone
|
|
|
282
374
|
|
|
283
375
|
# :ruby forces pure Ruby, :native requires the xxhash gem (UnsupportedError if it cannot be
|
|
284
376
|
# loaded), nil goes back to the default: native when available
|
|
377
|
+
#
|
|
378
|
+
# @param name [Symbol, nil] +:ruby+, +:native+ or nil
|
|
379
|
+
# @return [void]
|
|
380
|
+
# @raise [UnsupportedError] for +:native+ when the gem cannot be loaded
|
|
381
|
+
# @raise [ArgumentError] for any other name
|
|
285
382
|
def backend=(name)
|
|
286
383
|
@native = case name
|
|
287
384
|
when :ruby then false
|
|
@@ -293,16 +390,25 @@ module Herringbone
|
|
|
293
390
|
end
|
|
294
391
|
|
|
295
392
|
# Whether the native xxhash gem can be loaded (whatever the selected backend)
|
|
393
|
+
#
|
|
394
|
+
# @return [Boolean] true when the gem is loadable
|
|
296
395
|
def native_available?
|
|
297
396
|
!!native_library
|
|
298
397
|
end
|
|
299
398
|
|
|
300
399
|
private
|
|
301
400
|
|
|
401
|
+
# Picks the default backend on first use: native when the gem loads, pure Ruby otherwise
|
|
402
|
+
#
|
|
403
|
+
# @return [Module, false] the native module, or false for pure Ruby
|
|
302
404
|
def resolve_backend
|
|
303
405
|
@native = native_library || false
|
|
304
406
|
end
|
|
305
407
|
|
|
408
|
+
# Requires the xxhash gem once and memoizes the result (a failed require is not retried)
|
|
409
|
+
#
|
|
410
|
+
# @return [Module, false] the gem's module to call +xxh64(data, seed)+ on, or false when it
|
|
411
|
+
# cannot be loaded
|
|
306
412
|
def native_library
|
|
307
413
|
if @native_lib.nil?
|
|
308
414
|
@native_lib = begin
|
data/lib/herringbone.rb
CHANGED
|
@@ -6,11 +6,14 @@ require_relative "herringbone/version"
|
|
|
6
6
|
|
|
7
7
|
# Pure-Ruby reader and writer for Apache Parquet files
|
|
8
8
|
module Herringbone
|
|
9
|
+
# Base class of every error Herringbone raises on purpose
|
|
9
10
|
class Error < StandardError; end
|
|
10
11
|
# The file is not valid Parquet (bad metadata, corrupt pages...)
|
|
11
12
|
class FormatError < Error; end
|
|
12
13
|
# A value cannot be written to its column
|
|
13
14
|
class EncodeError < Error; end
|
|
15
|
+
# The file (or the writer configuration) uses a Parquet feature Herringbone does not implement,
|
|
16
|
+
# such as encryption or a codec whose library is unavailable
|
|
14
17
|
class UnsupportedError < Error; end
|
|
15
18
|
end
|
|
16
19
|
|
|
@@ -44,6 +47,29 @@ module Herringbone
|
|
|
44
47
|
# Other options go to Writer.
|
|
45
48
|
#
|
|
46
49
|
# File.open("orders.parquet", "wb") { |f| Herringbone.write(f, Order.where(created_at: 1.year.ago..)) }
|
|
50
|
+
#
|
|
51
|
+
# @param io [IO, #write] destination; written sequentially, never closed
|
|
52
|
+
# @param records [Enumerable<Hash, Array, Object>, Class, #find_each] rows (Hashes, Arrays in schema
|
|
53
|
+
# order, or objects responding to #attributes or #to_h), or an ActiveRecord model or relation
|
|
54
|
+
# @param schema [Schema, nil] schema to write with; derived from +records+ when nil
|
|
55
|
+
# @param options [Hash{Symbol => Object}] passed to Writer.new
|
|
56
|
+
# @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
|
|
57
|
+
# @option options [Integer] :row_group_bytes (16MB) approximate buffered size that triggers a row group
|
|
58
|
+
# @option options [Integer, nil] :row_group_rows (nil) also flush a row group after this many rows
|
|
59
|
+
# @option options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
60
|
+
# @option options [Integer] :page_rows (20_000) maximum rows per data page
|
|
61
|
+
# @option options [Integer] :data_page_version (1) 1 or 2
|
|
62
|
+
# @option options [Boolean, Array<String>] :dictionary (true) dictionary-encode all eligible columns,
|
|
63
|
+
# none, or only the listed dotted column paths
|
|
64
|
+
# @option options [Hash{String => Symbol}] :encodings ({}) dotted column path => value encoding
|
|
65
|
+
# for non-dictionary pages
|
|
66
|
+
# @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
|
|
67
|
+
# @option options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
68
|
+
# columns to write split block bloom filters for, see Writer
|
|
69
|
+
# @return [Integer] number of rows written
|
|
70
|
+
# @raise [ArgumentError] when the schema has to be inferred and +records+ is empty or holds Array
|
|
71
|
+
# rows, or an option is invalid
|
|
72
|
+
# @raise [EncodeError] when a row does not fit the schema
|
|
47
73
|
def write(io, records, schema: nil, **options)
|
|
48
74
|
model = if records.respond_to?(:klass) then records.klass
|
|
49
75
|
elsif records.respond_to?(:columns) && records.respond_to?(:find_each) then records
|
|
@@ -61,6 +87,8 @@ module Herringbone
|
|
|
61
87
|
|
|
62
88
|
# Compression codecs this process can read and write, e.g. [:none, :snappy, :gzip, :lz4, :lz4_hadoop, :zstd].
|
|
63
89
|
# :zstd and :brotli are listed when the zstd-ruby / brotli gems can be loaded.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Symbol>] codec names accepted by the writer's +compression:+ option
|
|
64
92
|
def codecs
|
|
65
93
|
Compression::NAMES.values.select do |name|
|
|
66
94
|
Compression.ensure_available!(name)
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: herringbone
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|
|
@@ -52,6 +52,7 @@ files:
|
|
|
52
52
|
- lib/herringbone/reader.rb
|
|
53
53
|
- lib/herringbone/reader/column_chunk_reader.rb
|
|
54
54
|
- lib/herringbone/reader/column_cursor.rb
|
|
55
|
+
- lib/herringbone/reader/numo.rb
|
|
55
56
|
- lib/herringbone/reader/page_stream.rb
|
|
56
57
|
- lib/herringbone/reader/scan.rb
|
|
57
58
|
- lib/herringbone/schema.rb
|