activerecord-materialize-adapter 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_record/connection_adapters/materialize/column.rb +0 -9
  3. data/lib/active_record/connection_adapters/materialize/database_statements.rb +22 -41
  4. data/lib/active_record/connection_adapters/materialize/oid.rb +0 -12
  5. data/lib/active_record/connection_adapters/materialize/oid/type_map_initializer.rb +5 -48
  6. data/lib/active_record/connection_adapters/materialize/quoting.rb +1 -7
  7. data/lib/active_record/connection_adapters/materialize/schema/source_statements.rb +66 -0
  8. data/lib/active_record/connection_adapters/materialize/schema/view_statements.rb +22 -0
  9. data/lib/active_record/connection_adapters/materialize/schema_creation.rb +18 -53
  10. data/lib/active_record/connection_adapters/materialize/schema_definitions.rb +3 -6
  11. data/lib/active_record/connection_adapters/materialize/schema_dumper.rb +82 -22
  12. data/lib/active_record/connection_adapters/materialize/schema_statements.rb +102 -354
  13. data/lib/active_record/connection_adapters/materialize/version.rb +1 -1
  14. data/lib/active_record/connection_adapters/materialize_adapter.rb +100 -253
  15. metadata +9 -15
  16. data/lib/active_record/connection_adapters/materialize/oid/cidr.rb +0 -50
  17. data/lib/active_record/connection_adapters/materialize/oid/legacy_point.rb +0 -44
  18. data/lib/active_record/connection_adapters/materialize/oid/money.rb +0 -41
  19. data/lib/active_record/connection_adapters/materialize/oid/point.rb +0 -64
  20. data/lib/active_record/connection_adapters/materialize/oid/range.rb +0 -96
  21. data/lib/active_record/connection_adapters/materialize/oid/uuid.rb +0 -25
  22. data/lib/active_record/connection_adapters/materialize/oid/vector.rb +0 -28
  23. data/lib/active_record/connection_adapters/materialize/oid/xml.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1003f4f98f3745e7b60ae1784af47747ad13dcaa0dbc2dfc8fb96489e6a90f4
4
- data.tar.gz: 07bfb83d66eea02e86e053a0be1ea88fb18a7a101a4ac89047889a098f2246d0
3
+ metadata.gz: 03b754e13f63726e0e11d14107d145449521404b78ccafb431d83de73dcf2360
4
+ data.tar.gz: bc53fd81cece00b418ea20ab4068bc5b063bee235f48bcd16f665d7be05ab4f7
5
5
  SHA512:
6
- metadata.gz: 61b3e6efea873654b7a8225aed6062c3d87f8b78be411801e3fa9b25ed4a826bcce5a441c95caadf6ec4eb9d8792708e0e1cea56589e5e206c49d82b6eea15ff
7
- data.tar.gz: 55bb91241a81f26ac8a3d2e662962c4195ba2517494fda6cd59549be030a3ee6d72137e7da933d463574cd314e88d66eeffd2445546b1cdf5f79cd4fafd3c42c
6
+ metadata.gz: 99041a07b198e3cd30000855d02698fd3e222040f2d25693060e407b9f068242d694785feecf2bb61187a72f30f02446f50e8da0fac2dbc7419ca9228b914523
7
+ data.tar.gz: dc272b7bbbd43a0ead177606b944b871f94fe8e85597c4e12263f1c987710b0bfd07d1030ea39581fcb7da92457edd6ea7db0cef3bf4700c3508b37945db0e61
@@ -6,15 +6,6 @@ module ActiveRecord
6
6
  class Column < ActiveRecord::ConnectionAdapters::Column # :nodoc:
7
7
  delegate :oid, :fmod, to: :sql_type_metadata
8
8
 
9
- def initialize(*, serial: nil, **)
10
- super
11
- @serial = serial
12
- end
13
-
14
- def serial?
15
- @serial
16
- end
17
-
18
9
  def array
19
10
  sql_type_metadata.sql_type.end_with?("[]")
20
11
  end
@@ -11,9 +11,9 @@ module ActiveRecord
11
11
  Materialize::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds))
12
12
  end
13
13
 
14
- # The internal Materialize identifier of the money data type.
14
+ # The internal PostgreSQL identifier of the money data type.
15
15
  MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
16
- # The internal Materialize identifier of the BYTEA data type.
16
+ # The internal PostgreSQL identifier of the BYTEA data type.
17
17
  BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
18
18
 
19
19
  # create a 2D array representing the result set
@@ -62,15 +62,17 @@ module ActiveRecord
62
62
  def query(sql, name = nil) #:nodoc:
63
63
  materialize_transactions
64
64
 
65
+ result = nil
65
66
  log(sql, name) do
66
67
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
67
- result_as_array @connection.async_exec(sql)
68
+ result = execute_async_and_raise(sql)
68
69
  end
69
70
  end
71
+ result_as_array result
70
72
  end
71
73
 
72
74
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
73
- :begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback, :with
75
+ :begin, :commit, :explain, :select, :set, :show, :rollback, :with
74
76
  ) # :nodoc:
75
77
  private_constant :READ_QUERY
76
78
 
@@ -89,20 +91,13 @@ module ActiveRecord
89
91
 
90
92
  materialize_transactions
91
93
 
94
+ result = nil
92
95
  log(sql, name) do
93
96
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
94
- @connection.async_exec(sql)
97
+ result = execute_async_and_raise(sql)
95
98
  end
96
99
  end
97
-
98
- # Known issue: PG::InternalError: ERROR: At least one input has no complete timestamps yet
99
- # https://github.com/MaterializeInc/materialize/issues/2917
100
- rescue ActiveRecord::StatementInvalid => error
101
- if error.message.include? "At least one input has no complete timestamps yet"
102
- raise ::Materialize::Errors::IncompleteInput, error.message
103
- else
104
- raise
105
- end
100
+ result
106
101
  end
107
102
 
108
103
  def exec_query(sql, name = "SQL", binds = [], prepare: false)
@@ -112,6 +107,7 @@ module ActiveRecord
112
107
  fields.each_with_index do |fname, i|
113
108
  ftype = result.ftype i
114
109
  fmod = result.fmod i
110
+
115
111
  types[fname] = get_oid_type(ftype, fmod, fname)
116
112
  end
117
113
  ActiveRecord::Result.new(fields, result.values, types)
@@ -130,32 +126,10 @@ module ActiveRecord
130
126
  pk = primary_key(table_ref) if table_ref
131
127
  end
132
128
 
133
- if pk = suppress_composite_primary_key(pk)
134
- sql = "#{sql} RETURNING #{quote_column_name(pk)}"
135
- end
136
-
137
129
  super
138
130
  end
139
131
  private :sql_for_insert
140
132
 
141
- def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
142
- if use_insert_returning? || pk == false
143
- super
144
- else
145
- result = exec_query(sql, name, binds)
146
- unless sequence_name
147
- table_ref = extract_table_ref_from_insert_sql(sql)
148
- if table_ref
149
- pk = primary_key(table_ref) if pk.nil?
150
- pk = suppress_composite_primary_key(pk)
151
- sequence_name = default_sequence_name(table_ref, pk)
152
- end
153
- return result unless sequence_name
154
- end
155
- last_insert_id_result(sequence_name)
156
- end
157
- end
158
-
159
133
  # Begins a transaction.
160
134
  def begin_db_transaction
161
135
  execute "BEGIN"
@@ -177,6 +151,18 @@ module ActiveRecord
177
151
  end
178
152
 
179
153
  private
154
+ # Known issue: PG::InternalError: ERROR: At least one input has no complete timestamps yet
155
+ # https://github.com/MaterializeInc/materialize/issues/2917
156
+ def execute_async_and_raise(sql)
157
+ @connection.async_exec(sql)
158
+ rescue PG::InternalError => error
159
+ if error.message.include? "At least one input has no complete timestamps yet"
160
+ raise ::Materialize::Errors::IncompleteInput, error.message
161
+ else
162
+ raise
163
+ end
164
+ end
165
+
180
166
  def execute_batch(statements, name = nil)
181
167
  execute(combine_multi_statements(statements))
182
168
  end
@@ -185,11 +171,6 @@ module ActiveRecord
185
171
  ["TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"]
186
172
  end
187
173
 
188
- # Returns the current ID of a table's sequence.
189
- def last_insert_id_result(sequence_name)
190
- exec_query("SELECT currval(#{quote(sequence_name)})", "SQL")
191
- end
192
-
193
174
  def suppress_composite_primary_key(pk)
194
175
  pk unless pk.is_a?(Array)
195
176
  end
@@ -5,24 +5,12 @@ require "active_record/connection_adapters/materialize/oid/array"
5
5
  require "active_record/connection_adapters/materialize/oid/bit"
6
6
  require "active_record/connection_adapters/materialize/oid/bit_varying"
7
7
  require "active_record/connection_adapters/materialize/oid/bytea"
8
- require "active_record/connection_adapters/materialize/oid/cidr"
9
8
  require "active_record/connection_adapters/materialize/oid/date"
10
9
  require "active_record/connection_adapters/materialize/oid/date_time"
11
10
  require "active_record/connection_adapters/materialize/oid/decimal"
12
11
  require "active_record/connection_adapters/materialize/oid/enum"
13
- require "active_record/connection_adapters/materialize/oid/hstore"
14
- require "active_record/connection_adapters/materialize/oid/inet"
15
12
  require "active_record/connection_adapters/materialize/oid/jsonb"
16
- require "active_record/connection_adapters/materialize/oid/money"
17
13
  require "active_record/connection_adapters/materialize/oid/oid"
18
- require "active_record/connection_adapters/materialize/oid/point"
19
- require "active_record/connection_adapters/materialize/oid/legacy_point"
20
- require "active_record/connection_adapters/materialize/oid/range"
21
- require "active_record/connection_adapters/materialize/oid/specialized_string"
22
- require "active_record/connection_adapters/materialize/oid/uuid"
23
- require "active_record/connection_adapters/materialize/oid/vector"
24
- require "active_record/connection_adapters/materialize/oid/xml"
25
-
26
14
  require "active_record/connection_adapters/materialize/oid/type_map_initializer"
27
15
 
28
16
  module ActiveRecord
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
  module ConnectionAdapters
7
7
  module Materialize
8
8
  module OID # :nodoc:
9
- # This class uses the data from Materialize pg_type table to build
9
+ # This class uses the data from Materialize mz_types table to build
10
10
  # the OID -> Type mapping.
11
11
  # - OID is an integer representing the type.
12
12
  # - Type is an OID::Type object.
@@ -18,65 +18,22 @@ module ActiveRecord
18
18
 
19
19
  def run(records)
20
20
  nodes = records.reject { |row| @store.key? row["oid"].to_i }
21
- mapped = nodes.extract! { |row| @store.key? row["typname"] }
22
- ranges = nodes.extract! { |row| row["typtype"] == "r" }
23
- enums = nodes.extract! { |row| row["typtype"] == "e" }
24
- domains = nodes.extract! { |row| row["typtype"] == "d" }
25
- arrays = nodes.extract! { |row| row["typinput"] == "array_in" }
26
- composites = nodes.extract! { |row| row["typelem"].to_i != 0 }
21
+ mapped = nodes.extract! { |row| @store.key? row["name"] }
27
22
 
28
- mapped.each { |row| register_mapped_type(row) }
29
- enums.each { |row| register_enum_type(row) }
30
- domains.each { |row| register_domain_type(row) }
31
- arrays.each { |row| register_array_type(row) }
32
- ranges.each { |row| register_range_type(row) }
33
- composites.each { |row| register_composite_type(row) }
23
+ mapped.each { |row| register_mapped_type(row) }
34
24
  end
35
25
 
36
26
  def query_conditions_for_initial_load
37
27
  known_type_names = @store.keys.map { |n| "'#{n}'" }
38
28
  known_type_types = %w('r' 'e' 'd')
39
29
  <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")]
40
- WHERE
41
- t.typname IN (%s)
42
- OR t.typtype IN (%s)
43
- OR t.typelem != 0
30
+ AND t.name IN (%s)
44
31
  SQL
45
32
  end
46
33
 
47
34
  private
48
35
  def register_mapped_type(row)
49
- alias_type row["oid"], row["typname"]
50
- end
51
-
52
- def register_enum_type(row)
53
- register row["oid"], OID::Enum.new
54
- end
55
-
56
- def register_array_type(row)
57
- register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype|
58
- OID::Array.new(subtype, row["typdelim"])
59
- end
60
- end
61
-
62
- def register_range_type(row)
63
- register_with_subtype(row["oid"], row["rngsubtype"].to_i) do |subtype|
64
- OID::Range.new(subtype, row["typname"].to_sym)
65
- end
66
- end
67
-
68
- def register_domain_type(row)
69
- if base_type = @store.lookup(row["typbasetype"].to_i)
70
- register row["oid"], base_type
71
- else
72
- warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
73
- end
74
- end
75
-
76
- def register_composite_type(row)
77
- if subtype = @store.lookup(row["typelem"].to_i)
78
- register row["oid"], OID::Vector.new(row["typdelim"], subtype)
79
- end
36
+ alias_type row["oid"], row["name"]
80
37
  end
81
38
 
82
39
  def register(oid, oid_type = nil, &block)
@@ -116,14 +116,8 @@ module ActiveRecord
116
116
  private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
117
117
 
118
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
119
  def _quote(value)
124
120
  case value
125
- when OID::Xml::Data
126
- "xml '#{quote_string(value.to_s)}'"
127
121
  when OID::Bit::Data
128
122
  if value.binary?
129
123
  "B'#{value}'"
@@ -152,7 +146,7 @@ module ActiveRecord
152
146
  # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc
153
147
  # for more information
154
148
  { value: value.to_s, format: 1 }
155
- when OID::Xml::Data, OID::Bit::Data
149
+ when OID::Bit::Data
156
150
  value.to_s
157
151
  when OID::Array::Data
158
152
  encode_array(value)
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module Schema
7
+ module SourceStatements
8
+
9
+ def sources
10
+ query_values(data_source_sql(type: "SOURCE"), "SCHEMA")
11
+ end
12
+
13
+ # Get source options from an existing source
14
+ def source_options(source_name)
15
+ name_ref, statement = query("SHOW CREATE SOURCE #{quote_table_name(source_name)}", "SCHEMA").first
16
+ database_name, schema_name, publication_name = name_ref.split(".")
17
+ materialized = !!/CREATE\sMATERIALIZED/.match(statement)
18
+ _, source_type, _ = /FROM\s(POSTGRES|KAFKA\sBROKER)\sCONNECTION/.match(statement).to_s.split " "
19
+ source_type = source_type.to_s.downcase.to_sym
20
+
21
+ {
22
+ database_name: database_name,
23
+ schema_name: schema_name,
24
+ source_name: source_name,
25
+ source_type: source_type,
26
+ publication: publication_name,
27
+ materialized: materialized || source_type == :postgres
28
+ }
29
+ end
30
+
31
+ # "host=postgresdb port=5432 user=postgres dbname=source_database"
32
+ def select_database_config(connection_params)
33
+ {
34
+ host: connection_params['host'],
35
+ port: connection_params['port'],
36
+ user: connection_params['username'],
37
+ dbname: connection_params['database'],
38
+ password: connection_params['password']
39
+ }.compact
40
+ end
41
+
42
+ def create_source(source_name, publication:, source_type: :postgres, materialized: true, connection_params: nil)
43
+ materialized_statement = materialized ? 'MATERIALIZED ' : ''
44
+ connection_string = select_database_config(connection_params).map { |k, v| [k, v].join("=") }.join(" ")
45
+ case source_type
46
+ when :postgres
47
+ execute <<-SQL.squish
48
+ CREATE #{materialized_statement}SOURCE #{quote_schema_name(source_name)} FROM POSTGRES
49
+ CONNECTION #{quote(connection_string)}
50
+ PUBLICATION #{quote(publication)}
51
+ SQL
52
+ else
53
+ raise "Source type #{source_type} is currently unsupported for Materialized sources", NotImplementedError
54
+ end
55
+ end
56
+
57
+ def drop_source(source_name)
58
+ end
59
+
60
+ def source_exists?(source_name)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Materialize
6
+ module Schema
7
+ module ViewStatements
8
+
9
+ # Returns an array of view names defined in the database.
10
+ def materialized_views
11
+ query_values(data_source_sql(type: "MATERIALIZED"), "SCHEMA")
12
+ end
13
+
14
+ def view_sql(view_name)
15
+ query("SHOW CREATE VIEW #{quote_table_name(view_name)}", "SCHEMA").first.last
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -5,71 +5,36 @@ module ActiveRecord
5
5
  module Materialize
6
6
  class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
7
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}"
8
+ def visit_TableDefinition(o)
9
+ create_sql = +"CREATE#{table_modifier_in_create(o)} TABLE "
10
+ create_sql << "IF NOT EXISTS " if o.if_not_exists
11
+ create_sql << "#{quote_table_name(o.name)} "
26
12
 
27
- options = column_options(column)
13
+ statements = o.columns.map { |c| accept c }
28
14
 
29
- if options[:collation]
30
- change_column_sql << " COLLATE \"#{options[:collation]}\""
15
+ if supports_indexes_in_create?
16
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
31
17
  end
32
18
 
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})"
19
+ if supports_foreign_keys?
20
+ statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
38
21
  end
39
22
 
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
23
+ create_sql << "(#{statements.join(', ')})" if statements.present?
24
+ add_table_options!(create_sql, table_options(o))
25
+ create_sql << " AS #{to_sql(o.as)}" if o.as
26
+ create_sql
54
27
  end
55
28
 
56
29
  def add_column_options!(sql, options)
57
- if options[:collation]
58
- sql << " COLLATE \"#{options[:collation]}\""
30
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
31
+ # must explicitly check for :null to allow change_column to work on migrations
32
+ if options[:null] == false
33
+ sql << " NOT NULL"
59
34
  end
60
- super
35
+ sql
61
36
  end
62
37
 
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
38
  end
74
39
  end
75
40
  end