activerecord-materialize-adapter 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.
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