activerecord-materialize-adapter 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/lib/active_record/connection_adapters/materialize/column.rb +30 -0
  4. data/lib/active_record/connection_adapters/materialize/database_statements.rb +199 -0
  5. data/lib/active_record/connection_adapters/materialize/explain_pretty_printer.rb +44 -0
  6. data/lib/active_record/connection_adapters/materialize/oid/array.rb +91 -0
  7. data/lib/active_record/connection_adapters/materialize/oid/bit.rb +53 -0
  8. data/lib/active_record/connection_adapters/materialize/oid/bit_varying.rb +15 -0
  9. data/lib/active_record/connection_adapters/materialize/oid/bytea.rb +17 -0
  10. data/lib/active_record/connection_adapters/materialize/oid/cidr.rb +50 -0
  11. data/lib/active_record/connection_adapters/materialize/oid/date.rb +23 -0
  12. data/lib/active_record/connection_adapters/materialize/oid/date_time.rb +23 -0
  13. data/lib/active_record/connection_adapters/materialize/oid/decimal.rb +15 -0
  14. data/lib/active_record/connection_adapters/materialize/oid/enum.rb +20 -0
  15. data/lib/active_record/connection_adapters/materialize/oid/hstore.rb +70 -0
  16. data/lib/active_record/connection_adapters/materialize/oid/inet.rb +15 -0
  17. data/lib/active_record/connection_adapters/materialize/oid/jsonb.rb +15 -0
  18. data/lib/active_record/connection_adapters/materialize/oid/legacy_point.rb +44 -0
  19. data/lib/active_record/connection_adapters/materialize/oid/money.rb +41 -0
  20. data/lib/active_record/connection_adapters/materialize/oid/oid.rb +15 -0
  21. data/lib/active_record/connection_adapters/materialize/oid/point.rb +64 -0
  22. data/lib/active_record/connection_adapters/materialize/oid/range.rb +96 -0
  23. data/lib/active_record/connection_adapters/materialize/oid/specialized_string.rb +18 -0
  24. data/lib/active_record/connection_adapters/materialize/oid/type_map_initializer.rb +112 -0
  25. data/lib/active_record/connection_adapters/materialize/oid/uuid.rb +25 -0
  26. data/lib/active_record/connection_adapters/materialize/oid/vector.rb +28 -0
  27. data/lib/active_record/connection_adapters/materialize/oid/xml.rb +30 -0
  28. data/lib/active_record/connection_adapters/materialize/oid.rb +35 -0
  29. data/lib/active_record/connection_adapters/materialize/quoting.rb +205 -0
  30. data/lib/active_record/connection_adapters/materialize/referential_integrity.rb +43 -0
  31. data/lib/active_record/connection_adapters/materialize/schema_creation.rb +76 -0
  32. data/lib/active_record/connection_adapters/materialize/schema_definitions.rb +222 -0
  33. data/lib/active_record/connection_adapters/materialize/schema_dumper.rb +49 -0
  34. data/lib/active_record/connection_adapters/materialize/schema_statements.rb +742 -0
  35. data/lib/active_record/connection_adapters/materialize/type_metadata.rb +36 -0
  36. data/lib/active_record/connection_adapters/materialize/utils.rb +80 -0
  37. data/lib/active_record/connection_adapters/materialize/version.rb +9 -0
  38. data/lib/active_record/connection_adapters/materialize_adapter.rb +952 -0
  39. data/lib/active_record/tasks/materialize_database_tasks.rb +130 -0
  40. data/lib/activerecord-materialize-adapter.rb +3 -0
  41. data/lib/materialize/errors/database_error.rb +10 -0
  42. data/lib/materialize/errors/incomplete_input.rb +10 -0
  43. metadata +170 -0
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module Quoting
7
+ # Escapes binary strings for bytea input to the database.
8
+ def escape_bytea(value)
9
+ @connection.escape_bytea(value) if value
10
+ end
11
+
12
+ # Unescapes bytea output from a database to the binary string it represents.
13
+ # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
14
+ # on escaped binary output from database drive.
15
+ def unescape_bytea(value)
16
+ @connection.unescape_bytea(value) if value
17
+ end
18
+
19
+ # Quotes strings for use in SQL input.
20
+ def quote_string(s) #:nodoc:
21
+ @connection.escape(s)
22
+ end
23
+
24
+ # Checks the following cases:
25
+ #
26
+ # - table_name
27
+ # - "table.name"
28
+ # - schema_name.table_name
29
+ # - schema_name."table.name"
30
+ # - "schema.name".table_name
31
+ # - "schema.name"."table.name"
32
+ def quote_table_name(name) # :nodoc:
33
+ self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
34
+ end
35
+
36
+ # Quotes schema names for use in SQL queries.
37
+ def quote_schema_name(name)
38
+ PG::Connection.quote_ident(name)
39
+ end
40
+
41
+ def quote_table_name_for_assignment(table, attr)
42
+ quote_column_name(attr)
43
+ end
44
+
45
+ # Quotes column names for use in SQL queries.
46
+ def quote_column_name(name) # :nodoc:
47
+ self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
48
+ end
49
+
50
+ # Quote date/time values for use in SQL input.
51
+ def quoted_date(value) #:nodoc:
52
+ if value.year <= 0
53
+ bce_year = format("%04d", -value.year + 1)
54
+ super.sub(/^-?\d+/, bce_year) + " BC"
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ def quoted_binary(value) # :nodoc:
61
+ "'#{escape_bytea(value.to_s)}'"
62
+ end
63
+
64
+ def quote_default_expression(value, column) # :nodoc:
65
+ if value.is_a?(Proc)
66
+ value.call
67
+ elsif column.type == :uuid && value.is_a?(String) && /\(\)/.match?(value)
68
+ value # Does not quote function default values for UUID columns
69
+ elsif column.respond_to?(:array?)
70
+ value = type_cast_from_column(column, value)
71
+ quote(value)
72
+ else
73
+ super
74
+ end
75
+ end
76
+
77
+ def lookup_cast_type_from_column(column) # :nodoc:
78
+ type_map.lookup(column.oid, column.fmod, column.sql_type)
79
+ end
80
+
81
+ def column_name_matcher
82
+ COLUMN_NAME
83
+ end
84
+
85
+ def column_name_with_order_matcher
86
+ COLUMN_NAME_WITH_ORDER
87
+ end
88
+
89
+ COLUMN_NAME = /
90
+ \A
91
+ (
92
+ (?:
93
+ # "table_name"."column_name"::type_name | function(one or no argument)::type_name
94
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
95
+ )
96
+ (?:\s+AS\s+(?:\w+|"\w+"))?
97
+ )
98
+ (?:\s*,\s*\g<1>)*
99
+ \z
100
+ /ix
101
+
102
+ COLUMN_NAME_WITH_ORDER = /
103
+ \A
104
+ (
105
+ (?:
106
+ # "table_name"."column_name"::type_name | function(one or no argument)::type_name
107
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
108
+ )
109
+ (?:\s+ASC|\s+DESC)?
110
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
111
+ )
112
+ (?:\s*,\s*\g<1>)*
113
+ \z
114
+ /ix
115
+
116
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
117
+
118
+ private
119
+ def lookup_cast_type(sql_type)
120
+ super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
121
+ end
122
+
123
+ def _quote(value)
124
+ case value
125
+ when OID::Xml::Data
126
+ "xml '#{quote_string(value.to_s)}'"
127
+ when OID::Bit::Data
128
+ if value.binary?
129
+ "B'#{value}'"
130
+ elsif value.hex?
131
+ "X'#{value}'"
132
+ end
133
+ when Numeric
134
+ if value.finite?
135
+ super
136
+ else
137
+ "'#{value}'"
138
+ end
139
+ when OID::Array::Data
140
+ _quote(encode_array(value))
141
+ when Range
142
+ _quote(encode_range(value))
143
+ else
144
+ super
145
+ end
146
+ end
147
+
148
+ def _type_cast(value)
149
+ case value
150
+ when Type::Binary::Data
151
+ # Return a bind param hash with format as binary.
152
+ # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc
153
+ # for more information
154
+ { value: value.to_s, format: 1 }
155
+ when OID::Xml::Data, OID::Bit::Data
156
+ value.to_s
157
+ when OID::Array::Data
158
+ encode_array(value)
159
+ when Range
160
+ encode_range(value)
161
+ else
162
+ super
163
+ end
164
+ end
165
+
166
+ def encode_array(array_data)
167
+ encoder = array_data.encoder
168
+ values = type_cast_array(array_data.values)
169
+
170
+ result = encoder.encode(values)
171
+ if encoding = determine_encoding_of_strings_in_array(values)
172
+ result.force_encoding(encoding)
173
+ end
174
+ result
175
+ end
176
+
177
+ def encode_range(range)
178
+ "[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}"
179
+ end
180
+
181
+ def determine_encoding_of_strings_in_array(value)
182
+ case value
183
+ when ::Array then determine_encoding_of_strings_in_array(value.first)
184
+ when ::String then value.encoding
185
+ end
186
+ end
187
+
188
+ def type_cast_array(values)
189
+ case values
190
+ when ::Array then values.map { |item| type_cast_array(item) }
191
+ else _type_cast(values)
192
+ end
193
+ end
194
+
195
+ def type_cast_range_value(value)
196
+ infinity?(value) ? "" : type_cast(value)
197
+ end
198
+
199
+ def infinity?(value)
200
+ value.respond_to?(:infinite?) && value.infinite?
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module ReferentialIntegrity # :nodoc:
7
+ def disable_referential_integrity # :nodoc:
8
+ original_exception = nil
9
+
10
+ begin
11
+ transaction(requires_new: true) do
12
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
13
+ end
14
+ rescue ActiveRecord::ActiveRecordError => e
15
+ original_exception = e
16
+ end
17
+
18
+ begin
19
+ yield
20
+ rescue ActiveRecord::InvalidForeignKey => e
21
+ warn <<-WARNING
22
+ WARNING: Rails was not able to disable referential integrity.
23
+
24
+ This is most likely caused due to missing permissions.
25
+ Rails needs superuser privileges to disable referential integrity.
26
+
27
+ cause: #{original_exception.try(:message)}
28
+
29
+ WARNING
30
+ raise e
31
+ end
32
+
33
+ begin
34
+ transaction(requires_new: true) do
35
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
36
+ end
37
+ rescue ActiveRecord::ActiveRecordError
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
7
+ private
8
+ def visit_AlterTable(o)
9
+ super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
10
+ end
11
+
12
+ def visit_AddForeignKey(o)
13
+ super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? }
14
+ end
15
+
16
+ def visit_ValidateConstraint(name)
17
+ "VALIDATE CONSTRAINT #{quote_column_name(name)}"
18
+ end
19
+
20
+ def visit_ChangeColumnDefinition(o)
21
+ column = o.column
22
+ column.sql_type = type_to_sql(column.type, **column.options)
23
+ quoted_column_name = quote_column_name(o.name)
24
+
25
+ change_column_sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{column.sql_type}"
26
+
27
+ options = column_options(column)
28
+
29
+ if options[:collation]
30
+ change_column_sql << " COLLATE \"#{options[:collation]}\""
31
+ end
32
+
33
+ if options[:using]
34
+ change_column_sql << " USING #{options[:using]}"
35
+ elsif options[:cast_as]
36
+ cast_as_type = type_to_sql(options[:cast_as], **options)
37
+ change_column_sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
38
+ end
39
+
40
+ if options.key?(:default)
41
+ if options[:default].nil?
42
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT"
43
+ else
44
+ quoted_default = quote_default_expression(options[:default], column)
45
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"
46
+ end
47
+ end
48
+
49
+ if options.key?(:null)
50
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} #{options[:null] ? 'DROP' : 'SET'} NOT NULL"
51
+ end
52
+
53
+ change_column_sql
54
+ end
55
+
56
+ def add_column_options!(sql, options)
57
+ if options[:collation]
58
+ sql << " COLLATE \"#{options[:collation]}\""
59
+ end
60
+ super
61
+ end
62
+
63
+ # Returns any SQL string to go between CREATE and TABLE. May be nil.
64
+ def table_modifier_in_create(o)
65
+ # A table cannot be both TEMPORARY and UNLOGGED, since all TEMPORARY
66
+ # tables are already UNLOGGED.
67
+ if o.temporary
68
+ " TEMPORARY"
69
+ elsif o.unlogged
70
+ " UNLOGGED"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module ColumnMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ # Defines the primary key field.
10
+ # Use of the native Materialize UUID type is supported, and can be used
11
+ # by defining your tables as such:
12
+ #
13
+ # create_table :stuffs, id: :uuid do |t|
14
+ # t.string :content
15
+ # t.timestamps
16
+ # end
17
+ #
18
+ # By default, this will use the <tt>gen_random_uuid()</tt> function from the
19
+ # +pgcrypto+ extension. As that extension is only available in
20
+ # Materialize 9.4+, for earlier versions an explicit default can be set
21
+ # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead:
22
+ #
23
+ # create_table :stuffs, id: false do |t|
24
+ # t.primary_key :id, :uuid, default: "uuid_generate_v4()"
25
+ # t.uuid :foo_id
26
+ # t.timestamps
27
+ # end
28
+ #
29
+ # To enable the appropriate extension, which is a requirement, use
30
+ # the +enable_extension+ method in your migrations.
31
+ #
32
+ # To use a UUID primary key without any of the extensions, set the
33
+ # +:default+ option to +nil+:
34
+ #
35
+ # create_table :stuffs, id: false do |t|
36
+ # t.primary_key :id, :uuid, default: nil
37
+ # t.uuid :foo_id
38
+ # t.timestamps
39
+ # end
40
+ #
41
+ # You may also pass a custom stored procedure that returns a UUID or use a
42
+ # different UUID generation function from another library.
43
+ #
44
+ # Note that setting the UUID primary key default value to +nil+ will
45
+ # require you to assure that you always provide a UUID value before saving
46
+ # a record (as primary keys cannot be +nil+). This might be done via the
47
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
48
+ def primary_key(name, type = :primary_key, **options)
49
+ if type == :uuid
50
+ options[:default] = options.fetch(:default, "gen_random_uuid()")
51
+ end
52
+
53
+ super
54
+ end
55
+
56
+ ##
57
+ # :method: bigserial
58
+ # :call-seq: bigserial(*names, **options)
59
+
60
+ ##
61
+ # :method: bit
62
+ # :call-seq: bit(*names, **options)
63
+
64
+ ##
65
+ # :method: bit_varying
66
+ # :call-seq: bit_varying(*names, **options)
67
+
68
+ ##
69
+ # :method: cidr
70
+ # :call-seq: cidr(*names, **options)
71
+
72
+ ##
73
+ # :method: citext
74
+ # :call-seq: citext(*names, **options)
75
+
76
+ ##
77
+ # :method: daterange
78
+ # :call-seq: daterange(*names, **options)
79
+
80
+ ##
81
+ # :method: hstore
82
+ # :call-seq: hstore(*names, **options)
83
+
84
+ ##
85
+ # :method: inet
86
+ # :call-seq: inet(*names, **options)
87
+
88
+ ##
89
+ # :method: interval
90
+ # :call-seq: interval(*names, **options)
91
+
92
+ ##
93
+ # :method: int4range
94
+ # :call-seq: int4range(*names, **options)
95
+
96
+ ##
97
+ # :method: int8range
98
+ # :call-seq: int8range(*names, **options)
99
+
100
+ ##
101
+ # :method: jsonb
102
+ # :call-seq: jsonb(*names, **options)
103
+
104
+ ##
105
+ # :method: ltree
106
+ # :call-seq: ltree(*names, **options)
107
+
108
+ ##
109
+ # :method: macaddr
110
+ # :call-seq: macaddr(*names, **options)
111
+
112
+ ##
113
+ # :method: money
114
+ # :call-seq: money(*names, **options)
115
+
116
+ ##
117
+ # :method: numrange
118
+ # :call-seq: numrange(*names, **options)
119
+
120
+ ##
121
+ # :method: oid
122
+ # :call-seq: oid(*names, **options)
123
+
124
+ ##
125
+ # :method: point
126
+ # :call-seq: point(*names, **options)
127
+
128
+ ##
129
+ # :method: line
130
+ # :call-seq: line(*names, **options)
131
+
132
+ ##
133
+ # :method: lseg
134
+ # :call-seq: lseg(*names, **options)
135
+
136
+ ##
137
+ # :method: box
138
+ # :call-seq: box(*names, **options)
139
+
140
+ ##
141
+ # :method: path
142
+ # :call-seq: path(*names, **options)
143
+
144
+ ##
145
+ # :method: polygon
146
+ # :call-seq: polygon(*names, **options)
147
+
148
+ ##
149
+ # :method: circle
150
+ # :call-seq: circle(*names, **options)
151
+
152
+ ##
153
+ # :method: serial
154
+ # :call-seq: serial(*names, **options)
155
+
156
+ ##
157
+ # :method: tsrange
158
+ # :call-seq: tsrange(*names, **options)
159
+
160
+ ##
161
+ # :method: tstzrange
162
+ # :call-seq: tstzrange(*names, **options)
163
+
164
+ ##
165
+ # :method: tsvector
166
+ # :call-seq: tsvector(*names, **options)
167
+
168
+ ##
169
+ # :method: uuid
170
+ # :call-seq: uuid(*names, **options)
171
+
172
+ ##
173
+ # :method: xml
174
+ # :call-seq: xml(*names, **options)
175
+
176
+ included do
177
+ define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
178
+ :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
179
+ :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
180
+ :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml
181
+ end
182
+ end
183
+
184
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
185
+ include ColumnMethods
186
+
187
+ attr_reader :unlogged
188
+
189
+ def initialize(*, **)
190
+ super
191
+ @unlogged = ActiveRecord::ConnectionAdapters::MaterializeAdapter.create_unlogged_tables
192
+ end
193
+
194
+ private
195
+ def integer_like_primary_key_type(type, options)
196
+ if type == :bigint || options[:limit] == 8
197
+ :bigserial
198
+ else
199
+ :serial
200
+ end
201
+ end
202
+ end
203
+
204
+ class Table < ActiveRecord::ConnectionAdapters::Table
205
+ include ColumnMethods
206
+ end
207
+
208
+ class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
209
+ attr_reader :constraint_validations
210
+
211
+ def initialize(td)
212
+ super
213
+ @constraint_validations = []
214
+ end
215
+
216
+ def validate_constraint(name)
217
+ @constraint_validations << name
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ class SchemaDumper < ActiveRecord::ConnectionAdapters::SchemaDumper # :nodoc:
7
+ private
8
+ def extensions(stream)
9
+ extensions = @connection.extensions
10
+ if extensions.any?
11
+ stream.puts " # These are extensions that must be enabled in order to support this database"
12
+ extensions.sort.each do |extension|
13
+ stream.puts " enable_extension #{extension.inspect}"
14
+ end
15
+ stream.puts
16
+ end
17
+ end
18
+
19
+ def prepare_column_options(column)
20
+ spec = super
21
+ spec[:array] = "true" if column.array?
22
+ spec
23
+ end
24
+
25
+ def default_primary_key?(column)
26
+ schema_type(column) == :bigserial
27
+ end
28
+
29
+ def explicit_primary_key_default?(column)
30
+ column.type == :uuid || (column.type == :integer && !column.serial?)
31
+ end
32
+
33
+ def schema_type(column)
34
+ return super unless column.serial?
35
+
36
+ if column.bigint?
37
+ :bigserial
38
+ else
39
+ :serial
40
+ end
41
+ end
42
+
43
+ def schema_expression(column)
44
+ super unless column.serial?
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end