activerecord-spanner-adapter 1.5.1 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
  3. data/.github/workflows/acceptance-tests-on-production.yaml +5 -3
  4. data/.github/workflows/ci.yaml +1 -1
  5. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +1 -1
  6. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +5 -3
  7. data/.github/workflows/nightly-unit-tests.yaml +1 -1
  8. data/.github/workflows/release-please-label.yml +1 -1
  9. data/.github/workflows/rubocop.yaml +1 -1
  10. data/.release-please-manifest.json +1 -1
  11. data/.rubocop.yml +1 -1
  12. data/CHANGELOG.md +14 -0
  13. data/Gemfile +7 -2
  14. data/README.md +10 -10
  15. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
  16. data/acceptance/cases/migration/change_schema_test.rb +19 -3
  17. data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
  18. data/acceptance/cases/models/interleave_test.rb +6 -0
  19. data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
  20. data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
  21. data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
  22. data/acceptance/models/table_with_sequence.rb +10 -0
  23. data/acceptance/schema/schema.rb +65 -19
  24. data/acceptance/test_helper.rb +1 -1
  25. data/activerecord-spanner-adapter.gemspec +1 -1
  26. data/benchmarks/application.rb +1 -1
  27. data/examples/snippets/bit-reversed-sequence/README.md +103 -0
  28. data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
  29. data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
  30. data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
  31. data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
  32. data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
  33. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
  34. data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
  35. data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
  36. data/examples/snippets/interleaved-tables/README.md +44 -53
  37. data/examples/snippets/interleaved-tables/Rakefile +2 -2
  38. data/examples/snippets/interleaved-tables/application.rb +2 -2
  39. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
  40. data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
  41. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  42. data/examples/snippets/interleaved-tables/models/album.rb +3 -7
  43. data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
  44. data/examples/snippets/interleaved-tables/models/track.rb +6 -7
  45. data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
  46. data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
  47. data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
  48. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
  49. data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
  50. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
  51. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
  52. data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
  53. data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
  54. data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
  55. data/examples/snippets/query-logs/README.md +43 -0
  56. data/examples/snippets/query-logs/Rakefile +13 -0
  57. data/examples/snippets/query-logs/application.rb +63 -0
  58. data/examples/snippets/query-logs/config/database.yml +8 -0
  59. data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
  60. data/examples/snippets/query-logs/db/schema.rb +31 -0
  61. data/examples/snippets/query-logs/db/seeds.rb +24 -0
  62. data/examples/snippets/query-logs/models/album.rb +9 -0
  63. data/examples/snippets/query-logs/models/singer.rb +9 -0
  64. data/examples/snippets/read-only-transactions/application.rb +1 -1
  65. data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
  66. data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
  67. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
  68. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -2
  69. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
  70. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +12 -2
  71. data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
  72. data/lib/activerecord_spanner_adapter/base.rb +64 -20
  73. data/lib/activerecord_spanner_adapter/foreign_key.rb +6 -2
  74. data/lib/activerecord_spanner_adapter/index/column.rb +3 -1
  75. data/lib/activerecord_spanner_adapter/index.rb +4 -2
  76. data/lib/activerecord_spanner_adapter/information_schema.rb +124 -72
  77. data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
  78. data/lib/activerecord_spanner_adapter/table/column.rb +7 -10
  79. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  80. data/lib/arel/visitors/spanner.rb +3 -1
  81. data/lib/spanner_client_ext.rb +6 -1
  82. metadata +34 -5
@@ -4,6 +4,7 @@
4
4
  # license that can be found in the LICENSE file or at
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
+ require "active_record/gem_version"
7
8
  require "activerecord_spanner_adapter/relation"
8
9
 
9
10
  module ActiveRecord
@@ -14,6 +15,8 @@ module ActiveRecord
14
15
  end
15
16
 
16
17
  class Base
18
+ VERSION_7_1 = Gem::Version.create "7.1.0"
19
+
17
20
  # Creates an object (or multiple objects) and saves it to the database. This method will use mutations instead
18
21
  # of DML if there is no active transaction, or if the active transaction has been created with the option
19
22
  # isolation: :buffered_mutations.
@@ -30,7 +33,9 @@ module ActiveRecord
30
33
  return super unless spanner_adapter?
31
34
  return super if active_transaction?
32
35
 
33
- transaction isolation: :buffered_mutations do
36
+ # Only use mutations to create new records if the primary key is generated client-side.
37
+ isolation = sequence_name ? nil : :buffered_mutations
38
+ transaction isolation: isolation do
34
39
  return super
35
40
  end
36
41
  end
@@ -43,35 +48,74 @@ module ActiveRecord
43
48
  spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
44
49
  end
45
50
 
46
- def self._insert_record values
47
- return super unless buffered_mutations? || (primary_key && values.is_a?(Hash))
51
+ def self._insert_record values, returning = []
52
+ if !(buffered_mutations? || (primary_key && values.is_a?(Hash))) || !spanner_adapter?
53
+ return super values if ActiveRecord.gem_version < VERSION_7_1
54
+ return super
55
+ end
48
56
 
49
- return _buffer_record values, :insert if buffered_mutations?
57
+ # Mutations cannot be used in combination with a sequence, as mutations do not support a THEN RETURN clause.
58
+ if buffered_mutations? && sequence_name
59
+ raise StatementInvalid, "Mutations cannot be used to create records that use a sequence " \
60
+ "to generate the primary key. #{self} uses #{sequence_name}."
61
+ end
50
62
 
51
- primary_key_value =
52
- if primary_key.is_a? Array
53
- _set_composite_primary_key_values primary_key, values
54
- else
55
- _set_single_primary_key_value primary_key, values
56
- end
63
+ return _buffer_record values, :insert, returning if buffered_mutations?
64
+
65
+ _insert_record_dml values, returning
66
+ end
67
+
68
+ def self._insert_record_dml values, returning
69
+ primary_key_value = _set_primary_key_value values
57
70
  if ActiveRecord::VERSION::MAJOR >= 7
58
71
  im = Arel::InsertManager.new arel_table
59
72
  im.insert(values.transform_keys { |name| arel_table[name] })
60
73
  else
61
74
  im = arel_table.compile_insert _substitute_values(values)
62
75
  end
63
- connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
76
+ result = connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
77
+
78
+ _convert_primary_key result, returning
79
+ end
80
+
81
+ def self._set_primary_key_value values
82
+ if primary_key.is_a? Array
83
+ _set_composite_primary_key_values primary_key, values
84
+ else
85
+ _set_single_primary_key_value primary_key, values
86
+ end
87
+ end
88
+
89
+ def self._convert_primary_key primary_key_value, returning
90
+ # Rails 7.1 and higher supports composite primary keys, and therefore require the provider to return an array
91
+ # instead of a single value in all cases. The order of the values should be equal to the order of the returning
92
+ # columns (or the primary key if no returning columns were specified).
93
+ return primary_key_value if ActiveRecord.gem_version < VERSION_7_1
94
+ return primary_key_value if primary_key_value.is_a?(Array) && primary_key_value.length == 1
95
+ return [primary_key_value] unless primary_key_value.is_a? Array
96
+
97
+ # Re-order the returned values according to the returning columns if it is not equal to the primary key of the
98
+ # table.
99
+ keys = returning || primary_key
100
+ return primary_key_value if keys == primary_key
101
+
102
+ primary_key_values_hash = Hash[primary_key.zip(primary_key_value)]
103
+ values = []
104
+ keys.each do |column|
105
+ values.append primary_key_values_hash[column]
106
+ end
107
+ values
64
108
  end
65
109
 
66
- def self._upsert_record values
67
- _buffer_record values, :insert_or_update
110
+ def self._upsert_record values, returning
111
+ _buffer_record values, :insert_or_update, returning
68
112
  end
69
113
 
70
114
  def self.insert_all _attributes, **_kwargs
71
115
  raise NotImplementedError, "Cloud Spanner does not support skip_duplicates. Use insert! or upsert instead."
72
116
  end
73
117
 
74
- def self.insert_all! attributes, **_kwargs
118
+ def self.insert_all! attributes, returning: nil, **_kwargs
75
119
  return super unless spanner_adapter?
76
120
  return super if active_transaction? && !buffered_mutations?
77
121
 
@@ -90,7 +134,7 @@ module ActiveRecord
90
134
  end
91
135
  end
92
136
 
93
- def self.upsert_all attributes, **_kwargs
137
+ def self.upsert_all attributes, returning: nil, unique_by: nil, **_kwargs
94
138
  return super unless spanner_adapter?
95
139
  if active_transaction? && !buffered_mutations?
96
140
  raise NotImplementedError, "Cloud Spanner does not support upsert using DML. " \
@@ -102,18 +146,18 @@ module ActiveRecord
102
146
  # The mutations will be sent as one batch when the transaction is committed.
103
147
  if active_transaction?
104
148
  attributes.each do |record|
105
- _upsert_record record
149
+ _upsert_record record, returning
106
150
  end
107
151
  else
108
152
  transaction isolation: :buffered_mutations do
109
153
  attributes.each do |record|
110
- _upsert_record record
154
+ _upsert_record record, returning
111
155
  end
112
156
  end
113
157
  end
114
158
  end
115
159
 
116
- def self._buffer_record values, method
160
+ def self._buffer_record values, method, returning
117
161
  primary_key_value =
118
162
  if primary_key.is_a? Array
119
163
  _set_composite_primary_key_values primary_key, values
@@ -132,10 +176,9 @@ module ActiveRecord
132
176
  mutation = Google::Cloud::Spanner::V1::Mutation.new(
133
177
  "#{method}": write
134
178
  )
135
-
136
179
  connection.current_spanner_transaction.buffer mutation
137
180
 
138
- primary_key_value
181
+ _convert_primary_key primary_key_value, returning
139
182
  end
140
183
 
141
184
  def self._set_composite_primary_key_values primary_key, values
@@ -174,6 +217,7 @@ module ActiveRecord
174
217
  def self._set_single_primary_key_value primary_key, values
175
218
  primary_key_value = values[primary_key] || values[primary_key.to_sym]
176
219
 
220
+ return primary_key_value if sequence_name
177
221
  return primary_key_value unless prefetch_primary_key?
178
222
 
179
223
  if primary_key_value.nil?
@@ -6,7 +6,7 @@
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
8
  class ForeignKey
9
- attr_accessor :table_name, :name, :columns, :ref_table, :ref_columns,
9
+ attr_accessor :table_schema, :table_name, :name, :columns, :ref_schema, :ref_table, :ref_columns,
10
10
  :on_delete, :on_update
11
11
 
12
12
  def initialize \
@@ -16,10 +16,14 @@ module ActiveRecordSpannerAdapter
16
16
  ref_table,
17
17
  ref_columns,
18
18
  on_delete: nil,
19
- on_update: nil
19
+ on_update: nil,
20
+ table_schema: "",
21
+ ref_schema: ""
22
+ @table_schema = table_schema
20
23
  @table_name = table_name
21
24
  @name = name
22
25
  @columns = Array(columns)
26
+ @ref_schema = ref_schema
23
27
  @ref_table = ref_table
24
28
  @ref_columns = Array(ref_columns)
25
29
  @on_delete = on_delete unless on_delete == "NO ACTION"
@@ -7,16 +7,18 @@
7
7
  module ActiveRecordSpannerAdapter
8
8
  class Index
9
9
  class Column
10
- attr_accessor :table_name, :index_name, :name, :order, :ordinal_position
10
+ attr_accessor :table_name, :schema_name, :index_name, :name, :order, :ordinal_position
11
11
 
12
12
  def initialize \
13
13
  table_name,
14
14
  index_name,
15
15
  name,
16
+ schema_name: "",
16
17
  order: nil,
17
18
  ordinal_position: nil
18
19
  @table_name = table_name.to_s
19
20
  @index_name = index_name.to_s
21
+ @schema_name = schema_name.to_s
20
22
  @name = name.to_s
21
23
  @order = order.to_s.upcase if order
22
24
  @ordinal_position = ordinal_position
@@ -8,7 +8,7 @@ require "activerecord_spanner_adapter/index/column"
8
8
 
9
9
  module ActiveRecordSpannerAdapter
10
10
  class Index
11
- attr_accessor :table, :name, :columns, :type, :unique, :null_filtered,
11
+ attr_accessor :schema, :table, :name, :columns, :type, :unique, :null_filtered,
12
12
  :interleave_in, :storing, :state
13
13
 
14
14
  def initialize \
@@ -20,7 +20,9 @@ module ActiveRecordSpannerAdapter
20
20
  null_filtered: false,
21
21
  interleave_in: nil,
22
22
  storing: nil,
23
- state: nil
23
+ state: nil,
24
+ schema: ""
25
+ @schema = schema.to_s
24
26
  @table = table.to_s
25
27
  @name = name.to_s
26
28
  @columns = Array(columns)
@@ -15,6 +15,8 @@ module ActiveRecordSpannerAdapter
15
15
  class InformationSchema
16
16
  include ActiveRecord::ConnectionAdapters::Quoting
17
17
 
18
+ IsRails71OrLater = ActiveRecord.gem_version >= Gem::Version.create("7.1.0")
19
+
18
20
  attr_reader :connection
19
21
 
20
22
  def initialize connection
@@ -22,7 +24,7 @@ module ActiveRecordSpannerAdapter
22
24
  @mutex = Mutex.new
23
25
  end
24
26
 
25
- def tables table_name: nil, schema_name: nil, view: nil
27
+ def tables table_name: nil, schema_name: "", view: nil
26
28
  sql = +"SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, PARENT_TABLE_NAME, ON_DELETE_ACTION"
27
29
  sql << " FROM INFORMATION_SCHEMA.TABLES"
28
30
  sql << " WHERE TABLE_SCHEMA=%<schema_name>s"
@@ -43,17 +45,17 @@ module ActiveRecordSpannerAdapter
43
45
  )
44
46
 
45
47
  if [:full, :columns].include? view
46
- table.columns = table_columns table.name
48
+ table.columns = table_columns table.name, schema_name: schema_name
47
49
  end
48
50
 
49
51
  if [:full, :indexes].include? view
50
- table.indexes = indexes table.name
52
+ table.indexes = indexes table.name, schema_name: table.schema_name
51
53
  end
52
54
  table
53
55
  end
54
56
  end
55
57
 
56
- def table table_name, schema_name: nil, view: nil
58
+ def table table_name, schema_name: "", view: nil
57
59
  tables(
58
60
  table_name: table_name,
59
61
  schema_name: schema_name,
@@ -61,52 +63,62 @@ module ActiveRecordSpannerAdapter
61
63
  ).first
62
64
  end
63
65
 
64
- def table_columns table_name, column_name: nil
66
+ def table_columns table_name, column_name: nil, schema_name: ""
67
+ primary_keys = table_primary_keys(table_name).map(&:name)
65
68
  sql = +"SELECT COLUMN_NAME, SPANNER_TYPE, IS_NULLABLE, GENERATION_EXPRESSION,"
66
69
  sql << " CAST(COLUMN_DEFAULT AS STRING) AS COLUMN_DEFAULT, ORDINAL_POSITION"
67
70
  sql << " FROM INFORMATION_SCHEMA.COLUMNS"
68
71
  sql << " WHERE TABLE_NAME=%<table_name>s"
72
+ sql << " AND TABLE_SCHEMA=%<schema_name>s"
69
73
  sql << " AND COLUMN_NAME=%<column_name>s" if column_name
70
74
  sql << " ORDER BY ORDINAL_POSITION ASC"
71
75
 
72
- column_options = column_options table_name, column_name
76
+ column_options = column_options table_name, column_name, schema_name: schema_name
73
77
  execute_query(
74
78
  sql,
75
79
  table_name: table_name,
76
- column_name: column_name
80
+ column_name: column_name,
81
+ schema_name: schema_name
77
82
  ).map do |row|
78
- type, limit = parse_type_and_limit row["SPANNER_TYPE"]
79
- column_name = row["COLUMN_NAME"]
80
- options = column_options[column_name]
83
+ _create_column table_name, row, primary_keys, column_options, schema_name: schema_name
84
+ end
85
+ end
81
86
 
82
- default = row["COLUMN_DEFAULT"]
83
- default_function = row["GENERATION_EXPRESSION"]
87
+ def _create_column table_name, row, primary_keys, column_options, schema_name: ""
88
+ type, limit = parse_type_and_limit row["SPANNER_TYPE"]
89
+ column_name = row["COLUMN_NAME"]
90
+ options = column_options[column_name]
91
+ primary_key = primary_keys.include? column_name
84
92
 
85
- if /\w+\(.*\)/.match?(default)
86
- default_function ||= default
87
- default = nil
88
- end
93
+ default = row["COLUMN_DEFAULT"]
94
+ default_function = row["GENERATION_EXPRESSION"]
89
95
 
90
- if default && type == "STRING"
91
- default = unquote_string default
92
- end
96
+ if default && default.length < 200 && /\w+\(.*\)/.match?(default)
97
+ default_function ||= default
98
+ default = nil
99
+ end
93
100
 
94
- Table::Column.new \
95
- table_name,
96
- column_name,
97
- type,
98
- limit: limit,
99
- allow_commit_timestamp: options["allow_commit_timestamp"],
100
- ordinal_position: row["ORDINAL_POSITION"],
101
- nullable: row["IS_NULLABLE"] == "YES",
102
- default: default,
103
- default_function: default_function,
104
- generated: row["GENERATION_EXPRESSION"].present?
101
+ if default && type == "STRING"
102
+ default = unquote_string default
105
103
  end
104
+
105
+ Table::Column.new \
106
+ table_name,
107
+ column_name,
108
+ type,
109
+ schema_name: schema_name,
110
+ limit: limit,
111
+ allow_commit_timestamp: options["allow_commit_timestamp"],
112
+ ordinal_position: row["ORDINAL_POSITION"],
113
+ nullable: row["IS_NULLABLE"] == "YES",
114
+ default: default,
115
+ default_function: default_function,
116
+ generated: row["GENERATION_EXPRESSION"].present?,
117
+ primary_key: primary_key
106
118
  end
107
119
 
108
- def table_column table_name, column_name
109
- table_columns(table_name, column_name: column_name).first
120
+ def table_column table_name, column_name, schema_name: ""
121
+ table_columns(table_name, column_name: column_name, schema_name: schema_name).first
110
122
  end
111
123
 
112
124
  # Returns the primary key columns of the given table. By default it will only return the columns that are not part
@@ -114,43 +126,49 @@ module ActiveRecordSpannerAdapter
114
126
  # ActiveRecord. The parent primary key columns are filtered out by default to allow interleaved tables to be
115
127
  # considered as tables with a single-column primary key by ActiveRecord. The actual primary key of the table will
116
128
  # include both the parent primary key columns and the 'own' primary key columns of a table.
117
- def table_primary_keys table_name, include_parent_keys = false
129
+ def table_primary_keys table_name, include_parent_keys = IsRails71OrLater, schema_name: ""
118
130
  sql = +"WITH TABLE_PK_COLS AS ( "
119
- sql << "SELECT C.TABLE_NAME, C.COLUMN_NAME, C.INDEX_NAME, C.COLUMN_ORDERING, C.ORDINAL_POSITION "
131
+ sql << "SELECT C.TABLE_CATALOG, C.TABLE_SCHEMA, C.TABLE_NAME, C.COLUMN_NAME, "
132
+ sql << "C.INDEX_NAME, C.COLUMN_ORDERING, C.ORDINAL_POSITION "
120
133
  sql << "FROM INFORMATION_SCHEMA.INDEX_COLUMNS C "
121
134
  sql << "WHERE C.INDEX_TYPE = 'PRIMARY_KEY' "
122
135
  sql << "AND TABLE_CATALOG = '' "
123
136
  sql << "AND TABLE_SCHEMA = '') "
124
137
  sql << "SELECT INDEX_NAME, COLUMN_NAME, COLUMN_ORDERING, ORDINAL_POSITION "
125
138
  sql << "FROM TABLE_PK_COLS "
126
- sql << "INNER JOIN INFORMATION_SCHEMA.TABLES T USING (TABLE_NAME) "
139
+ sql << "INNER JOIN INFORMATION_SCHEMA.TABLES T USING (TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME) "
127
140
  sql << "WHERE TABLE_NAME = %<table_name>s "
128
141
  sql << "AND TABLE_CATALOG = '' "
129
- sql << "AND TABLE_SCHEMA = '' "
142
+ sql << "AND TABLE_SCHEMA = %<schema_name>s "
130
143
  unless include_parent_keys
131
144
  sql << "AND (T.PARENT_TABLE_NAME IS NULL OR COLUMN_NAME NOT IN ( "
132
145
  sql << " SELECT COLUMN_NAME "
133
146
  sql << " FROM TABLE_PK_COLS "
134
- sql << " WHERE TABLE_NAME = T.PARENT_TABLE_NAME "
147
+ sql << " WHERE TABLE_CATALOG = T.TABLE_CATALOG "
148
+ sql << " AND TABLE_SCHEMA=T.TABLE_SCHEMA "
149
+ sql << " AND TABLE_NAME = T.PARENT_TABLE_NAME "
135
150
  sql << ")) "
136
151
  end
137
152
  sql << "ORDER BY ORDINAL_POSITION"
138
153
  execute_query(
139
154
  sql,
140
- table_name: table_name
155
+ table_name: table_name,
156
+ schema_name: schema_name
141
157
  ).map do |row|
142
158
  Index::Column.new \
143
159
  table_name,
144
160
  row["INDEX_NAME"],
145
161
  row["COLUMN_NAME"],
162
+ schema_name: schema_name,
146
163
  order: row["COLUMN_ORDERING"],
147
164
  ordinal_position: row["ORDINAL_POSITION"]
148
165
  end
149
166
  end
150
167
 
151
- def indexes table_name, index_name: nil, index_type: nil
168
+ def indexes table_name, schema_name: "", index_name: nil, index_type: nil
152
169
  table_indexes_columns = index_columns(
153
170
  table_name,
171
+ schema_name: schema_name,
154
172
  index_name: index_name
155
173
  )
156
174
 
@@ -158,7 +176,7 @@ module ActiveRecordSpannerAdapter
158
176
  sql << " FROM INFORMATION_SCHEMA.INDEXES"
159
177
  sql << " WHERE TABLE_NAME=%<table_name>s"
160
178
  sql << " AND TABLE_CATALOG = ''"
161
- sql << " AND TABLE_SCHEMA = ''"
179
+ sql << " AND TABLE_SCHEMA = %<schema_name>s"
162
180
  sql << " AND INDEX_NAME=%<index_name>s" if index_name
163
181
  sql << " AND INDEX_TYPE=%<index_type>s" if index_type
164
182
  sql << " AND SPANNER_IS_MANAGED=FALSE"
@@ -166,6 +184,7 @@ module ActiveRecordSpannerAdapter
166
184
  execute_query(
167
185
  sql,
168
186
  table_name: table_name,
187
+ schema_name: schema_name,
169
188
  index_name: index_name,
170
189
  index_type: index_type
171
190
  ).map do |row|
@@ -189,89 +208,120 @@ module ActiveRecordSpannerAdapter
189
208
  null_filtered: row["IS_NULL_FILTERED"],
190
209
  interleave_in: row["PARENT_TABLE_NAME"],
191
210
  storing: storing,
192
- state: row["INDEX_STATE"]
211
+ state: row["INDEX_STATE"],
212
+ schema: schema_name
193
213
  end
194
214
  end
195
215
 
196
- def index table_name, index_name
197
- indexes(table_name, index_name: index_name).first
216
+ def index table_name, index_name, schema_name: ""
217
+ indexes(table_name, index_name: index_name, schema_name: schema_name).first
198
218
  end
199
219
 
200
- def index_columns table_name, index_name: nil
220
+ def index_columns table_name, schema_name: "", index_name: nil
201
221
  sql = +"SELECT INDEX_NAME, COLUMN_NAME, COLUMN_ORDERING, ORDINAL_POSITION"
202
222
  sql << " FROM INFORMATION_SCHEMA.INDEX_COLUMNS"
203
223
  sql << " WHERE TABLE_NAME=%<table_name>s"
204
224
  sql << " AND TABLE_CATALOG = ''"
205
- sql << " AND TABLE_SCHEMA = ''"
225
+ sql << " AND TABLE_SCHEMA = %<schema_name>s"
206
226
  sql << " AND INDEX_NAME=%<index_name>s" if index_name
207
227
  sql << " ORDER BY ORDINAL_POSITION ASC"
208
228
 
209
229
  execute_query(
210
230
  sql,
211
- table_name: table_name, index_name: index_name
231
+ table_name: table_name, schema_name: schema_name, index_name: index_name
212
232
  ).map do |row|
213
233
  Index::Column.new \
214
234
  table_name,
215
235
  row["INDEX_NAME"],
216
236
  row["COLUMN_NAME"],
237
+ schema_name: schema_name,
217
238
  order: row["COLUMN_ORDERING"],
218
239
  ordinal_position: row["ORDINAL_POSITION"]
219
240
  end
220
241
  end
221
242
 
222
- def indexes_by_columns table_name, column_names
243
+ def indexes_by_columns table_name, column_names, schema_name: ""
223
244
  column_names = Array(column_names).map(&:to_s)
224
245
 
225
- indexes(table_name).select do |index|
246
+ indexes(table_name, schema_name: schema_name).select do |index|
226
247
  index.columns.any? { |c| column_names.include? c.name }
227
248
  end
228
249
  end
229
250
 
230
- def foreign_keys table_name
251
+ def foreign_keys from_table_name, from_schema_name: ""
231
252
  sql = <<~SQL
232
- SELECT cc.table_name AS to_table,
233
- cc.column_name AS primary_key,
234
- fk.column_name as column,
235
- fk.constraint_name AS name,
236
- rc.update_rule AS on_update,
237
- rc.delete_rule AS on_delete
238
- FROM information_schema.referential_constraints rc
239
- INNER JOIN information_schema.key_column_usage fk ON rc.constraint_name = fk.constraint_name
240
- INNER JOIN information_schema.constraint_column_usage cc ON rc.constraint_name = cc.constraint_name
241
- WHERE fk.table_name = %<table_name>s
242
- AND fk.constraint_schema = %<constraint_schema>s
253
+ SELECT CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, CONSTRAINT_NAME, UPDATE_RULE, DELETE_RULE,
254
+ FK_CATALOG, FK_SCHEMA, FK_TABLE,
255
+ PK_CATALOG, PK_SCHEMA, PK_TABLE,
256
+ ARRAY_AGG(FK_COLUMN) AS FK_COLUMNS, ARRAY_AGG(PK_COLUMN) AS PK_COLUMNS
257
+ FROM (SELECT CONSTRAINTS.CONSTRAINT_CATALOG,
258
+ CONSTRAINTS.CONSTRAINT_SCHEMA,
259
+ CONSTRAINTS.CONSTRAINT_NAME,
260
+ CONSTRAINTS.UPDATE_RULE,
261
+ CONSTRAINTS.DELETE_RULE,
262
+ CHILD.TABLE_CATALOG AS FK_CATALOG,
263
+ CHILD.TABLE_SCHEMA AS FK_SCHEMA,
264
+ CHILD.TABLE_NAME AS FK_TABLE,
265
+ CHILD.COLUMN_NAME AS FK_COLUMN,
266
+ PARENT.TABLE_CATALOG AS PK_CATALOG,
267
+ PARENT.TABLE_SCHEMA AS PK_SCHEMA,
268
+ PARENT.TABLE_NAME AS PK_TABLE,
269
+ PARENT.COLUMN_NAME AS PK_COLUMN
270
+ FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS CONSTRAINTS
271
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CHILD
272
+ ON CONSTRAINTS.CONSTRAINT_CATALOG = CHILD.CONSTRAINT_CATALOG
273
+ AND CONSTRAINTS.CONSTRAINT_SCHEMA = CHILD.CONSTRAINT_SCHEMA
274
+ AND CONSTRAINTS.CONSTRAINT_NAME = CHILD.CONSTRAINT_NAME
275
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE PARENT
276
+ ON CONSTRAINTS.UNIQUE_CONSTRAINT_CATALOG = PARENT.CONSTRAINT_CATALOG
277
+ AND CONSTRAINTS.UNIQUE_CONSTRAINT_SCHEMA = PARENT.CONSTRAINT_SCHEMA
278
+ AND CONSTRAINTS.UNIQUE_CONSTRAINT_NAME = PARENT.CONSTRAINT_NAME
279
+ AND PARENT.ORDINAL_POSITION = CHILD.POSITION_IN_UNIQUE_CONSTRAINT
280
+ ORDER BY CHILD.TABLE_CATALOG, CHILD.TABLE_SCHEMA, CHILD.TABLE_NAME, CHILD.POSITION_IN_UNIQUE_CONSTRAINT
281
+ ) FOREIGN_KEYS
282
+ WHERE FK_TABLE = %<table_name>s
283
+ AND FK_SCHEMA = %<constraint_schema>s
284
+ GROUP BY CONSTRAINT_CATALOG, CONSTRAINT_SCHEMA, CONSTRAINT_NAME, UPDATE_RULE, DELETE_RULE,
285
+ FK_CATALOG, FK_SCHEMA, FK_TABLE,
286
+ PK_CATALOG, PK_SCHEMA, PK_TABLE
243
287
  SQL
244
288
 
245
289
  rows = execute_query(
246
- sql, table_name: table_name, constraint_schema: ""
290
+ sql, table_name: from_table_name, constraint_schema: from_schema_name
247
291
  )
248
292
 
249
293
  rows.map do |row|
250
294
  ForeignKey.new(
251
- table_name,
252
- row["name"],
253
- row["column"],
254
- row["to_table"],
255
- row["primary_key"],
256
- on_delete: row["on_delete"],
257
- on_update: row["on_update"]
295
+ from_table_name,
296
+ row["CONSTRAINT_NAME"],
297
+ row["FK_COLUMNS"],
298
+ row["PK_TABLE"],
299
+ row["PK_COLUMNS"],
300
+ on_delete: row["DELETE_RULE"],
301
+ on_update: row["UPDATE_RULE"],
302
+ table_schema: from_schema_name,
303
+ ref_schema: row["PK_SCHEMA"]
258
304
  )
259
305
  end
260
306
  end
261
307
 
262
- def check_constraints table_name
308
+ def check_constraints table_name, schema_name: ""
263
309
  sql = <<~SQL.squish
264
310
  SELECT tc.TABLE_NAME,
265
311
  tc.CONSTRAINT_NAME,
266
312
  cc.CHECK_CLAUSE
267
313
  FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
268
- INNER JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc ON tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
314
+ INNER JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc
315
+ ON tc.CONSTRAINT_CATALOG = cc.CONSTRAINT_CATALOG
316
+ AND tc.CONSTRAINT_SCHEMA = cc.CONSTRAINT_SCHEMA
317
+ AND tc.CONSTRAINT_NAME = cc.CONSTRAINT_NAME
269
318
  WHERE tc.TABLE_NAME = %<table_name>s
319
+ AND tc.CONSTRAINT_SCHEMA = %<schema_name>s
270
320
  AND tc.CONSTRAINT_TYPE = 'CHECK'
271
321
  AND NOT (tc.CONSTRAINT_NAME LIKE 'CK_IS_NOT_NULL_%%' AND cc.CHECK_CLAUSE LIKE '%%IS NOT NULL')
272
322
  SQL
273
323
 
274
- rows = execute_query sql, table_name: table_name
324
+ rows = execute_query sql, table_name: table_name, schema_name: schema_name
275
325
 
276
326
  rows.map do |row|
277
327
  ActiveRecord::ConnectionAdapters::CheckConstraintDefinition.new(
@@ -354,16 +404,18 @@ module ActiveRecordSpannerAdapter
354
404
  [value[start...(start + length)].to_i(base)].pack "U"
355
405
  end
356
406
 
357
- def column_options table_name, column_name
407
+ def column_options table_name, column_name, schema_name: ""
358
408
  sql = +"SELECT COLUMN_NAME, OPTION_NAME, OPTION_TYPE, OPTION_VALUE"
359
409
  sql << " FROM INFORMATION_SCHEMA.COLUMN_OPTIONS"
360
410
  sql << " WHERE TABLE_NAME=%<table_name>s"
411
+ sql << " AND TABLE_SCHEMA=%<schema_name>s"
361
412
  sql << " AND COLUMN_NAME=%<column_name>s" if column_name
362
413
 
363
414
  column_options = Hash.new { |h, k| h[k] = {} }
364
415
  execute_query(
365
416
  sql,
366
417
  table_name: table_name,
418
+ schema_name: schema_name,
367
419
  column_name: column_name
368
420
  ).each_with_object(column_options) do |row, options|
369
421
  next unless row["OPTION_TYPE"] == "BOOL"
@@ -18,7 +18,7 @@ module ActiveRecord
18
18
  end
19
19
 
20
20
  def fetch_primary_and_parent_key
21
- return connection.schema_cache.primary_and_parent_keys table_name \
21
+ return connection.spanner_schema_cache.primary_and_parent_keys table_name \
22
22
  if ActiveRecord::Base != self && table_exists?
23
23
  end
24
24
 
@@ -7,22 +7,24 @@
7
7
  module ActiveRecordSpannerAdapter
8
8
  class Table
9
9
  class Column
10
- attr_accessor :table_name, :name, :type, :limit, :ordinal_position,
10
+ attr_accessor :schema_name, :table_name, :name, :type, :limit, :ordinal_position,
11
11
  :allow_commit_timestamp, :default, :default_function, :generated,
12
- :primary_key
13
- attr_writer :nullable
12
+ :primary_key, :nullable
14
13
 
15
14
  def initialize \
16
15
  table_name,
17
16
  name,
18
17
  type,
18
+ schema_name: "",
19
19
  limit: nil,
20
20
  ordinal_position: nil,
21
21
  nullable: true,
22
22
  allow_commit_timestamp: nil,
23
23
  default: nil,
24
24
  default_function: nil,
25
- generated: nil
25
+ generated: nil,
26
+ primary_key: false
27
+ @schema_name = schema_name.to_s
26
28
  @table_name = table_name.to_s
27
29
  @name = name.to_s
28
30
  @type = type
@@ -33,12 +35,7 @@ module ActiveRecordSpannerAdapter
33
35
  @default = default
34
36
  @default_function = default_function
35
37
  @generated = generated == true
36
- @primary_key = false
37
- end
38
-
39
- def nullable
40
- return false if primary_key
41
- @nullable
38
+ @primary_key = primary_key
42
39
  end
43
40
 
44
41
  def spanner_type
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "1.5.1".freeze
8
+ VERSION = "1.6.1".freeze
9
9
  end