activerecord-cockroachdb-adapter 0.2.3 → 5.2.2

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 (27) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.gitmodules +0 -3
  4. data/CONTRIBUTING.md +25 -53
  5. data/Gemfile +58 -6
  6. data/README.md +293 -2
  7. data/Rakefile +17 -5
  8. data/activerecord-cockroachdb-adapter.gemspec +3 -6
  9. data/build/Dockerfile +1 -1
  10. data/build/teamcity-test.sh +17 -37
  11. data/docker.sh +1 -1
  12. data/lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb +27 -0
  13. data/lib/active_record/connection_adapters/cockroachdb/attribute_methods.rb +28 -0
  14. data/lib/active_record/connection_adapters/cockroachdb/column.rb +94 -0
  15. data/lib/active_record/connection_adapters/cockroachdb/column_methods.rb +53 -0
  16. data/lib/active_record/connection_adapters/cockroachdb/database_statements.rb +102 -0
  17. data/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb +121 -0
  18. data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +37 -0
  19. data/lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb +23 -38
  20. data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +123 -40
  21. data/lib/active_record/connection_adapters/cockroachdb/setup.rb +19 -0
  22. data/lib/active_record/connection_adapters/cockroachdb/spatial_column_info.rb +44 -0
  23. data/lib/active_record/connection_adapters/cockroachdb/table_definition.rb +56 -0
  24. data/lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb +14 -16
  25. data/lib/active_record/connection_adapters/cockroachdb/type.rb +14 -0
  26. data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +218 -123
  27. metadata +18 -42
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CockroachDB
6
+ module OID
7
+ class Spatial < Type::Value
8
+ # sql_type is a string that comes from the database definition
9
+ # examples:
10
+ # "geometry(Point,4326)"
11
+ # "geography(Point,4326)"
12
+ # "geometry(Polygon,4326) NOT NULL"
13
+ # "geometry(Geography,4326)"
14
+ def initialize(oid, sql_type)
15
+ @sql_type = sql_type
16
+ @geo_type, @srid, @has_z, @has_m = self.class.parse_sql_type(sql_type)
17
+ end
18
+
19
+ # sql_type: geometry, geometry(Point), geometry(Point,4326), ...
20
+ #
21
+ # returns [geo_type, srid, has_z, has_m]
22
+ # geo_type: geography, geometry, point, line_string, polygon, ...
23
+ # srid: 1234
24
+ # has_z: false
25
+ # has_m: false
26
+ def self.parse_sql_type(sql_type)
27
+ geo_type = nil
28
+ srid = 0
29
+ has_z = false
30
+ has_m = false
31
+
32
+ if sql_type =~ /(geography|geometry)\((.*)\)$/i
33
+ # geometry(Point)
34
+ # geometry(Point,4326)
35
+ params = Regexp.last_match(2).split(',')
36
+ if params.first =~ /([a-z]+[^zm])(z?)(m?)/i
37
+ has_z = Regexp.last_match(2).length > 0
38
+ has_m = Regexp.last_match(3).length > 0
39
+ geo_type = Regexp.last_match(1)
40
+ end
41
+ srid = Regexp.last_match(1).to_i if params.last =~ /(\d+)/
42
+ else
43
+ geo_type = sql_type
44
+ end
45
+ [geo_type, srid, has_z, has_m]
46
+ end
47
+
48
+ def spatial_factory
49
+ @spatial_factory ||=
50
+ RGeo::ActiveRecord::SpatialFactoryStore.instance.factory(
51
+ factory_attrs
52
+ )
53
+ end
54
+
55
+ def geographic?
56
+ @sql_type =~ /geography/
57
+ end
58
+
59
+ def spatial?
60
+ true
61
+ end
62
+
63
+ def type
64
+ geographic? ? :geography : :geometry
65
+ end
66
+
67
+ # support setting an RGeo object or a WKT string
68
+ def serialize(value)
69
+ return if value.nil?
70
+
71
+ geo_value = cast_value(value)
72
+
73
+ # TODO: - only valid types should be allowed
74
+ # e.g. linestring is not valid for point column
75
+ # raise "maybe should raise" unless RGeo::Feature::Geometry.check_type(geo_value)
76
+
77
+ RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true)
78
+ .generate(geo_value)
79
+ end
80
+
81
+ private
82
+
83
+ def cast_value(value)
84
+ return if value.nil?
85
+
86
+ value.is_a?(String) ? parse_wkt(value) : value
87
+ end
88
+
89
+ # convert WKT string into RGeo object
90
+ def parse_wkt(string)
91
+ wkt_parser(string).parse(string)
92
+ rescue RGeo::Error::ParseError
93
+ nil
94
+ end
95
+
96
+ def binary_string?(string)
97
+ string[0] == "\x00" || string[0] == "\x01" || string[0, 4] =~ /[0-9a-fA-F]{4}/
98
+ end
99
+
100
+ def wkt_parser(string)
101
+ if binary_string?(string)
102
+ RGeo::WKRep::WKBParser.new(spatial_factory, support_ewkb: true, default_srid: @srid)
103
+ else
104
+ RGeo::WKRep::WKTParser.new(spatial_factory, support_ewkt: true, default_srid: @srid)
105
+ end
106
+ end
107
+
108
+ def factory_attrs
109
+ {
110
+ geo_type: @geo_type.underscore,
111
+ has_m: @has_m,
112
+ has_z: @has_z,
113
+ srid: @srid,
114
+ sql_type: type.to_s
115
+ }
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,37 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ module Quoting
5
+ private
6
+
7
+ # CockroachDB does not allow inserting integer values into string
8
+ # columns, but ActiveRecord expects this to work. CockroachDB will
9
+ # however allow inserting string values into integer columns. It will
10
+ # try to parse string values and convert them to integers so they can be
11
+ # inserted in integer columns.
12
+ #
13
+ # We take advantage of this behavior here by forcing numeric values to
14
+ # always be strings. Then, we won't have to make any additional changes
15
+ # to ActiveRecord to support inserting integer values into string
16
+ # columns.
17
+ #
18
+ # For spatial types, data is stored as Well-known Binary (WKB) strings
19
+ # (https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary)
20
+ # but when creating objects, using RGeo features is more convenient than
21
+ # converting to WKB, so this does it automatically.
22
+ def _quote(value)
23
+ case value
24
+ when Numeric
25
+ "'#{quote_string(value.to_s)}'"
26
+ when RGeo::Feature::Geometry
27
+ "'#{RGeo::WKRep::WKBGenerator.new(hex_format: true, type_format: :ewkb, emit_ewkb_srid: true).generate(value)}'"
28
+ when RGeo::Cartesian::BoundingBox
29
+ "'#{value.min_x},#{value.min_y},#{value.max_x},#{value.max_y}'::box"
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,51 +1,36 @@
1
1
  # frozen_string_literal: true
2
- # NOTE(joey): This is cradled from connection_adapters/postgresql/referential_integrity.rb
3
- # It is commonly used for setting up fixtures during tests.
2
+
3
+ # The PostgresSQL Adapter's ReferentialIntegrity module can disable and
4
+ # re-enable foreign key constraints by disabling all table triggers. Since
5
+ # triggers are not available in CockroachDB, we have to remove foreign keys and
6
+ # re-add them via the ActiveRecord API.
7
+ #
8
+ # This module is commonly used to load test fixture data without having to worry
9
+ # about the order in which that data is loaded.
4
10
  module ActiveRecord
5
11
  module ConnectionAdapters
6
12
  module CockroachDB
7
- module ReferentialIntegrity # :nodoc:
8
- def disable_referential_integrity # :nodoc:
9
- original_exception = nil
10
- fkeys = nil
13
+ module ReferentialIntegrity
14
+ def disable_referential_integrity
15
+ foreign_keys = tables.map { |table| foreign_keys(table) }.flatten
11
16
 
12
- begin
13
- transaction do
14
- tables.each do |table_name|
15
- fkeys = foreign_keys(table_name)
16
- fkeys.each do |fkey|
17
- remove_foreign_key table_name, name: fkey.options[:name]
18
- end
19
- end
20
- end
21
- rescue ActiveRecord::ActiveRecordError => e
22
- original_exception = e
17
+ foreign_keys.each do |foreign_key|
18
+ remove_foreign_key(foreign_key.from_table, name: foreign_key.options[:name])
23
19
  end
24
20
 
25
- begin
26
- yield
27
- rescue ActiveRecord::InvalidForeignKey => e
28
- warn <<-WARNING
29
- WARNING: Rails was not able to disable referential integrity.
30
-
31
- Please go to https://github.com/cockroachdb/activerecord-cockroachdb-adapter
32
- and report this issue.
33
-
34
- cause: #{original_exception.try(:message)}
35
-
36
- WARNING
37
- raise e
38
- end
21
+ yield
39
22
 
40
- begin
41
- transaction do
42
- if !fkeys.nil?
43
- fkeys.each do |fkey|
44
- add_foreign_key fkey.from_table, fkey.to_table, fkey.options
45
- end
23
+ foreign_keys.each do |foreign_key|
24
+ begin
25
+ add_foreign_key(foreign_key.from_table, foreign_key.to_table, foreign_key.options)
26
+ rescue ActiveRecord::StatementInvalid => error
27
+ if error.cause.class == PG::DuplicateObject
28
+ # This error is safe to ignore because the yielded caller
29
+ # already re-added the foreign key constraint.
30
+ else
31
+ raise error
46
32
  end
47
33
  end
48
- rescue ActiveRecord::ActiveRecordError
49
34
  end
50
35
  end
51
36
  end
@@ -1,56 +1,139 @@
1
- require 'active_record/connection_adapters/postgresql/schema_statements'
2
-
3
1
  module ActiveRecord
4
2
  module ConnectionAdapters
5
3
  module CockroachDB
6
4
  module SchemaStatements
7
5
  include ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements
8
- # NOTE(joey): This was ripped from PostgresSQL::SchemaStatements, with a
9
- # slight modification to change setval(string, int, bool) to just
10
- # setval(string, int) for CockroachDB compatbility.
11
- # See https://github.com/cockroachdb/cockroach/issues/19723
12
- #
13
- # Resets the sequence of a table's primary key to the maximum value.
14
- def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
15
- unless pk && sequence
16
- default_pk, default_sequence = pk_and_sequence_for(table)
17
6
 
18
- pk ||= default_pk
19
- sequence ||= default_sequence
7
+ def add_index(table_name, column_name, options = {})
8
+ super
9
+ rescue ActiveRecord::StatementInvalid => error
10
+ if debugging? && error.cause.class == PG::FeatureNotSupported
11
+ warn "#{error}\n\nThis error will be ignored and the index will not be created.\n\n"
12
+ else
13
+ raise error
14
+ end
15
+ end
16
+
17
+ # ActiveRecord allows for tables to exist without primary keys.
18
+ # Databases like PostgreSQL support this behavior, but CockroachDB does
19
+ # not. If a table is created without a primary key, CockroachDB will add
20
+ # a rowid column to serve as its primary key. This breaks a lot of
21
+ # ActiveRecord's assumptions so we'll treat tables with rowid primary
22
+ # keys as if they didn't have primary keys at all.
23
+ # https://www.cockroachlabs.com/docs/v19.2/create-table.html#create-a-table
24
+ # https://api.rubyonrails.org/v5.2.4/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_table
25
+ def primary_key(table_name)
26
+ pk = super
27
+
28
+ if pk == CockroachDBAdapter::DEFAULT_PRIMARY_KEY
29
+ nil
30
+ else
31
+ pk
32
+ end
33
+ end
34
+
35
+ # CockroachDB uses unique_rowid() for primary keys, not sequences. It's
36
+ # possible to force a table to use sequences, but since it's not the
37
+ # default behavior we'll always return nil for default_sequence_name.
38
+ def default_sequence_name(table_name, pk = "id")
39
+ nil
40
+ end
41
+
42
+ def columns(table_name)
43
+ # Limit, precision, and scale are all handled by the superclass.
44
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment|
45
+ oid = oid.to_i
46
+ fmod = fmod.to_i
47
+ type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
48
+ cast_type = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
49
+ default_value = extract_value_from_default(default)
50
+
51
+ default_function = extract_default_function(default_value, default)
52
+ new_column(table_name, column_name, default_value, cast_type, type_metadata, !notnull,
53
+ default_function, collation, comment)
20
54
  end
55
+ end
21
56
 
22
- if @logger && pk && !sequence
23
- @logger.warn "#{table} has primary key #{pk} with no default sequence."
57
+ def new_column(table_name, column_name, default, cast_type, sql_type_metadata = nil,
58
+ null = true, default_function = nil, collation = nil, comment = nil)
59
+ # JDBC gets true/false in Rails 4, where other platforms get 't'/'f' strings.
60
+ if null.is_a?(String)
61
+ null = (null == "t")
24
62
  end
25
63
 
26
- if pk && sequence
27
- quoted_sequence = quote_table_name(sequence)
28
- max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
29
- if max_pk.nil?
30
- if postgresql_version >= 100000
31
- minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
32
- else
33
- minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA")
64
+ column_info = spatial_column_info(table_name).get(column_name, sql_type_metadata.sql_type)
65
+
66
+ PostgreSQLColumn.new(
67
+ column_name,
68
+ default,
69
+ sql_type_metadata,
70
+ null,
71
+ table_name,
72
+ default_function,
73
+ collation,
74
+ comment,
75
+ cast_type,
76
+ column_info
77
+ )
78
+ end
79
+
80
+ # CockroachDB will use INT8 if the SQL type is INTEGER, so we make it use
81
+ # INT4 explicitly when needed.
82
+ #
83
+ # For spatial columns, include the limit to properly format the column name
84
+ # since type alone is not enough to format the column.
85
+ # Ex. type_to_sql(:geography, limit: "Point,4326")
86
+ # => "geography(Point,4326)"
87
+ #
88
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
89
+ sql = \
90
+ case type.to_s
91
+ when "integer"
92
+ case limit
93
+ when nil; "int"
94
+ when 1, 2; "int2"
95
+ when 3, 4; "int4"
96
+ when 5..8; "int8"
97
+ else super
34
98
  end
99
+ when "geometry", "geography"
100
+ "#{type}(#{limit})"
101
+ else
102
+ super
35
103
  end
36
- if max_pk
37
- # NOTE(joey): This is done to replace the call:
38
- #
39
- # SELECT setval(..., max_pk, false)
40
- #
41
- # with
42
- #
43
- # SELECT setval(..., max_pk-1)
44
- #
45
- # These two statements are semantically equivilant, but
46
- # setval(string, int, bool) is not supported by CockroachDB.
47
- #
48
- # FIXME(joey): This is incorrect if the sequence is not 1
49
- # incremented. We would need to pull out the custom increment value.
50
- max_pk - 1
51
- end
52
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue})", "SCHEMA")
104
+ # The call to super might have appeneded [] already.
105
+ if array && type != :primary_key && !sql.end_with?("[]")
106
+ sql = "#{sql}[]"
53
107
  end
108
+ sql
109
+ end
110
+
111
+ # override
112
+ def native_database_types
113
+ # Add spatial types
114
+ super.merge(
115
+ geography: { name: "geography" },
116
+ geometry: { name: "geometry" },
117
+ geometry_collection: { name: "geometry_collection" },
118
+ line_string: { name: "line_string" },
119
+ multi_line_string: { name: "multi_line_string" },
120
+ multi_point: { name: "multi_point" },
121
+ multi_polygon: { name: "multi_polygon" },
122
+ spatial: { name: "geometry" },
123
+ st_point: { name: "st_point" },
124
+ st_polygon: { name: "st_polygon" }
125
+ )
126
+ end
127
+
128
+ # override
129
+ def create_table_definition(*args, **kwargs)
130
+ CockroachDB::TableDefinition.new(*args, **kwargs)
131
+ end
132
+
133
+ # memoize hash of column infos for tables
134
+ def spatial_column_info(table_name)
135
+ @spatial_column_info ||= {}
136
+ @spatial_column_info[table_name.to_sym] ||= SpatialColumnInfo.new(self, table_name.to_s)
54
137
  end
55
138
  end
56
139
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord # :nodoc:
4
+ module ConnectionAdapters # :nodoc:
5
+ module CockroachDB # :nodoc:
6
+ def self.initial_setup
7
+ ::ActiveRecord::SchemaDumper.ignore_tables |= %w[
8
+ geography_columns
9
+ geometry_columns
10
+ layer
11
+ raster_columns
12
+ raster_overviews
13
+ spatial_ref_sys
14
+ topology
15
+ ]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ class SpatialColumnInfo
5
+ def initialize(adapter, table_name)
6
+ @adapter = adapter
7
+ @table_name = table_name
8
+ end
9
+
10
+ def all
11
+ info = @adapter.query(
12
+ "SELECT f_geometry_column,coord_dimension,srid,type FROM geometry_columns WHERE f_table_name='#{@table_name}'"
13
+ )
14
+ result = {}
15
+ info.each do |row|
16
+ name = row[0]
17
+ type = row[3]
18
+ dimension = row[1].to_i
19
+ has_m = !!(type =~ /m$/i)
20
+ type.sub!(/m$/, '')
21
+ has_z = dimension > 3 || dimension == 3 && !has_m
22
+ result[name] = {
23
+ dimension: dimension,
24
+ has_m: has_m,
25
+ has_z: has_z,
26
+ name: name,
27
+ srid: row[2].to_i,
28
+ type: type
29
+ }
30
+ end
31
+ result
32
+ end
33
+
34
+ # do not query the database for non-spatial columns/tables
35
+ def get(column_name, type)
36
+ return unless CockroachDBAdapter.spatial_column_options(type.to_sym)
37
+
38
+ @spatial_column_info ||= all
39
+ @spatial_column_info[column_name]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end