Skip to content
代码片段 群组 项目
提交 cfa49b45 编辑于 作者: dfrazao-gitlab's avatar dfrazao-gitlab
浏览文件

Convert the schema validation framework into a gem

- Move the schema validation code from the rails app
  to the gem

Relates to: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125597
Changelog: added
上级 083434ce
No related branches found
No related tags found
无相关合并请求
显示
1024 个添加1 个删除
...@@ -23,6 +23,8 @@ PATH ...@@ -23,6 +23,8 @@ PATH
remote: gems/gitlab-schema-validation remote: gems/gitlab-schema-validation
specs: specs:
gitlab-schema-validation (0.1.0) gitlab-schema-validation (0.1.0)
diffy
pg_query
PATH PATH
remote: gems/gitlab-utils remote: gems/gitlab-utils
......
...@@ -3,3 +3,9 @@ inherit_from: ...@@ -3,3 +3,9 @@ inherit_from:
AllCops: AllCops:
NewCops: enable NewCops: enable
Gitlab/RSpec/AvoidSetup:
Enabled: false
RSpec/MultipleMemoizedHelpers:
Max: 25
...@@ -2,6 +2,8 @@ PATH ...@@ -2,6 +2,8 @@ PATH
remote: . remote: .
specs: specs:
gitlab-schema-validation (0.1.0) gitlab-schema-validation (0.1.0)
diffy
pg_query
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
...@@ -21,24 +23,32 @@ GEM ...@@ -21,24 +23,32 @@ GEM
concurrent-ruby (1.2.2) concurrent-ruby (1.2.2)
debug_inspector (1.1.0) debug_inspector (1.1.0)
diff-lcs (1.5.0) diff-lcs (1.5.0)
diffy (3.4.2)
gitlab-styles (10.1.0) gitlab-styles (10.1.0)
rubocop (~> 1.50.2) rubocop (~> 1.50.2)
rubocop-graphql (~> 0.18) rubocop-graphql (~> 0.18)
rubocop-performance (~> 1.15) rubocop-performance (~> 1.15)
rubocop-rails (~> 2.17) rubocop-rails (~> 2.17)
rubocop-rspec (~> 2.22) rubocop-rspec (~> 2.22)
google-protobuf (3.23.3)
i18n (1.14.1) i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
json (2.6.3) json (2.6.3)
method_source (1.0.0)
minitest (5.18.1) minitest (5.18.1)
parallel (1.23.0) parallel (1.23.0)
parser (3.2.2.3) parser (3.2.2.3)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg_query (4.2.1)
google-protobuf (>= 3.22.3)
proc_to_ast (0.1.0) proc_to_ast (0.1.0)
coderay coderay
parser parser
unparser unparser
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
racc (1.7.1) racc (1.7.1)
rack (3.0.8) rack (3.0.8)
rainbow (3.1.1) rainbow (3.1.1)
...@@ -116,6 +126,7 @@ PLATFORMS ...@@ -116,6 +126,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
gitlab-schema-validation! gitlab-schema-validation!
gitlab-styles (~> 10.1.0) gitlab-styles (~> 10.1.0)
pry
rspec (~> 3.0) rspec (~> 3.0)
rspec-benchmark (~> 0.6.0) rspec-benchmark (~> 0.6.0)
rspec-parameterized (~> 1.0) rspec-parameterized (~> 1.0)
......
...@@ -19,7 +19,11 @@ Gem::Specification.new do |spec| ...@@ -19,7 +19,11 @@ Gem::Specification.new do |spec|
spec.files = Dir['lib/**/*.rb'] spec.files = Dir['lib/**/*.rb']
spec.require_paths = ["lib"] spec.require_paths = ["lib"]
spec.add_runtime_dependency "diffy"
spec.add_runtime_dependency "pg_query"
spec.add_development_dependency "gitlab-styles", "~> 10.1.0" spec.add_development_dependency "gitlab-styles", "~> 10.1.0"
spec.add_development_dependency "pry"
spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "rspec-benchmark", "~> 0.6.0" spec.add_development_dependency "rspec-benchmark", "~> 0.6.0"
spec.add_development_dependency "rspec-parameterized", "~> 1.0" spec.add_development_dependency "rspec-parameterized", "~> 1.0"
......
# frozen_string_literal: true # frozen_string_literal: true
require_relative "validation/version" require 'pg_query'
require 'diffy'
require_relative 'validation/version'
require_relative 'validation/inconsistency'
require_relative 'validation/pg_types'
require_relative 'validation/validators/base'
require_relative 'validation/validators/different_definition_indexes'
require_relative 'validation/validators/extra_indexes'
require_relative 'validation/validators/missing_indexes'
require_relative 'validation/validators/extra_table_columns'
require_relative 'validation/validators/missing_table_columns'
require_relative 'validation/validators/different_definition_foreign_keys'
require_relative 'validation/validators/extra_foreign_keys'
require_relative 'validation/validators/missing_foreign_keys'
require_relative 'validation/validators/different_definition_tables'
require_relative 'validation/validators/extra_tables'
require_relative 'validation/validators/missing_tables'
require_relative 'validation/validators/different_definition_triggers'
require_relative 'validation/validators/extra_triggers'
require_relative 'validation/validators/missing_triggers'
require_relative 'validation/sources/structure_sql'
require_relative 'validation/sources/database'
require_relative 'validation/schema_objects/base'
require_relative 'validation/schema_objects/column'
require_relative 'validation/schema_objects/index'
require_relative 'validation/schema_objects/table'
require_relative 'validation/schema_objects/trigger'
require_relative 'validation/schema_objects/foreign_key'
require_relative 'validation/adapters/column_database_adapter'
require_relative 'validation/adapters/column_structure_sql_adapter'
require_relative 'validation/adapters/foreign_key_database_adapter'
require_relative 'validation/adapters/foreign_key_structure_sql_adapter'
module Gitlab module Gitlab
module Schema module Schema
module Validation module Validation
class Runner
def initialize(structure_sql, database, validators:)
@structure_sql = structure_sql
@database = database
@validators = validators
end
def execute
validators.flat_map { |c| c.new(structure_sql, database).execute }
end
private
attr_reader :structure_sql, :database, :validators
end
end end
end end
end end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module Adapters
class ColumnDatabaseAdapter
def initialize(query_result)
@query_result = query_result
end
def name
@name ||= query_result['column_name']
end
def table_name
query_result['table_name']
end
def data_type
query_result['data_type']
end
def default
return unless query_result['column_default']
return if name == 'id' || query_result['column_default'].include?('nextval')
"DEFAULT #{query_result['column_default']}"
end
def nullable
'NOT NULL' if query_result['not_null']
end
def partition_key?
query_result['partition_key']
end
private
attr_reader :query_result
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module Adapters
UndefinedPGType = Class.new(StandardError)
class ColumnStructureSqlAdapter
NOT_NULL_CONSTR = :CONSTR_NOTNULL
DEFAULT_CONSTR = :CONSTR_DEFAULT
MAPPINGS = {
't' => 'true',
'f' => 'false'
}.freeze
attr_reader :table_name
def initialize(table_name, pg_query_stmt, partitioning_stmt)
@table_name = table_name
@pg_query_stmt = pg_query_stmt
@partitioning_stmt = partitioning_stmt
end
def name
@name ||= pg_query_stmt.colname
end
def data_type
type(pg_query_stmt.type_name)
end
def default
return if name == 'id'
value = parse_node(constraints.find { |node| node.constraint.contype == DEFAULT_CONSTR })
return unless value
"DEFAULT #{value}"
end
def nullable
'NOT NULL' if constraints.any? { |node| node.constraint.contype == NOT_NULL_CONSTR }
end
def partition_key?
partition_keys.include?(name)
end
private
attr_reader :pg_query_stmt, :partitioning_stmt
def constraints
@constraints ||= pg_query_stmt.constraints
end
# Returns the node type
#
# pg_type:: type alias, used internally by postgres, +int4+, +int8+, +bool+, +varchar+
# type:: type name, like +integer+, +bigint+, +boolean+, +character varying+.
# array_ext:: adds the +[]+ extension for array types.
# precision_ext:: adds the precision, if have any, like +(255)+, +(6)+.
#
# @info +timestamp+ and +timestamptz+ have a particular case when precision is defined.
# In this case, the order of the statement needs to be re-arranged from
# timestamp without time zone(6) to timestamp(6) without a time zone.
def type(node)
pg_type = parse_node(node.names.last)
type = PgTypes::TYPES.fetch(pg_type).dup
array_ext = '[]' if node.array_bounds.any?
precision_ext = "(#{node.typmods.map { |typmod| parse_node(typmod) }.join(',')})" if node.typmods.any?
if %w[timestamp timestamptz].include?(pg_type)
type.gsub!('timestamp', ['timestamp', precision_ext].compact.join)
precision_ext = nil
end
[type, precision_ext, array_ext].compact.join
rescue KeyError => e
raise UndefinedPGType, e.message
end
# Parses PGQuery nodes recursively
#
# :constraint:: nodes that groups column default info
# :partition_elem:: node that store partition key info
# :func_cal:: nodes that stores functions, like +now()+
# :a_const:: nodes that stores constant values, like +t+, +f+, +0.0.0.0+, +255+, +1.0+
# :type_cast:: nodes that stores casting values, like +'name'::text+, +'0.0.0.0'::inet+
# else:: extract node values in the last iteration of the recursion, like +int4+, +1.0+, +now+, +255+
#
# @note boolean types types are mapped from +t+, +f+ to +true+, +false+
def parse_node(node)
return unless node
case node.node
when :constraint
parse_node(node.constraint.raw_expr)
when :partition_elem
node.partition_elem.name
when :func_call
"#{parse_node(node.func_call.funcname.first)}()"
when :a_const
parse_a_const(node.a_const)
when :type_cast
value = parse_node(node.type_cast.arg)
type = type(node.type_cast.type_name)
separator = MAPPINGS.key?(value) ? '' : "::#{type}"
[MAPPINGS.fetch(value, "'#{value}'"), separator].compact.join
else
get_value_from_key(node, key: node.node)
end
end
def parse_a_const(a_const)
return unless a_const
type = a_const.val
get_value_from_key(a_const, key: type)
end
def get_value_from_key(node, key:)
node.to_h[key].values.last
end
def partition_keys
return [] unless partitioning_stmt
@partition_keys ||= partitioning_stmt.part_params.map { |key_stmt| parse_node(key_stmt) }
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module Adapters
class ForeignKeyDatabaseAdapter
def initialize(query_result)
@query_result = query_result
end
def name
"#{query_result['schema']}.#{query_result['foreign_key_name']}"
end
def table_name
query_result['table_name']
end
def statement
query_result['foreign_key_definition']
end
private
attr_reader :query_result
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module Adapters
class ForeignKeyStructureSqlAdapter
STATEMENT_REGEX = /\bREFERENCES\s\K\S+\K\s\(/
EXTRACT_REGEX = /\bFOREIGN KEY.*/
def initialize(parsed_stmt)
@parsed_stmt = parsed_stmt
end
def name
"#{schema_name}.#{foreign_key_name}"
end
def table_name
parsed_stmt.relation.relname
end
# PgQuery parses FK statements with an extra space in the referenced table column.
# This extra space needs to be removed.
#
# @example REFERENCES ci_pipelines (id) => REFERENCES ci_pipelines(id)
def statement
deparse_stmt[EXTRACT_REGEX].gsub(STATEMENT_REGEX, '(')
end
private
attr_reader :parsed_stmt
def schema_name
parsed_stmt.relation.schemaname
end
def foreign_key_name
parsed_stmt.cmds.first.alter_table_cmd.def.constraint.conname
end
def deparse_stmt
PgQuery.deparse_stmt(parsed_stmt)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
class Inconsistency
def initialize(validator_class, structure_sql_object, database_object)
@validator_class = validator_class
@structure_sql_object = structure_sql_object
@database_object = database_object
end
def error_message
format(validator_class::ERROR_MESSAGE, object_name)
end
def type
validator_class.name
end
def object_type
object_type = structure_sql_object&.class&.name || database_object&.class&.name
object_type&.gsub('Gitlab::Schema::Validation::SchemaObjects::', '')
end
def table_name
structure_sql_object&.table_name || database_object&.table_name
end
def object_name
structure_sql_object&.name || database_object&.name
end
def diff
Diffy::Diff.new(structure_sql_statement, database_statement)
end
def display
<<~MSG
#{'-' * 54}
#{error_message}
Diff:
#{diff.to_s(:color)}
#{'-' * 54}
MSG
end
def structure_sql_statement
return unless structure_sql_object
"#{structure_sql_object.statement}\n"
end
def database_statement
return unless database_object
"#{database_object.statement}\n"
end
private
attr_reader :validator_class, :structure_sql_object, :database_object
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
class PgTypes
TYPES = {
'bool' => 'boolean',
'bytea' => 'bytea',
'char' => '"char"',
'int8' => 'bigint',
'int2' => 'smallint',
'int4' => 'integer',
'regproc' => 'regproc',
'text' => 'text',
'oid' => 'oid',
'tid' => 'tid',
'xid' => 'xid',
'cid' => 'cid',
'json' => 'json',
'xml' => 'xml',
'pg_node_tree' => 'pg_node_tree',
'pg_ndistinct' => 'pg_ndistinct',
'pg_dependencies' => 'pg_dependencies',
'pg_mcv_list' => 'pg_mcv_list',
'xid8' => 'xid8',
'path' => 'path',
'polygon' => 'polygon',
'float4' => 'real',
'float8' => 'double precision',
'circle' => 'circle',
'money' => 'money',
'macaddr' => 'macaddr',
'inet' => 'inet',
'cidr' => 'cidr',
'macaddr8' => 'macaddr8',
'aclitem' => 'aclitem',
'bpchar' => 'character',
'varchar' => 'character varying',
'date' => 'date',
'time' => 'time without time zone',
'timestamp' => 'timestamp without time zone',
'timestamptz' => 'timestamp with time zone',
'interval' => 'interval',
'timetz' => 'time with time zone',
'bit' => 'bit',
'varbit' => 'bit varying',
'numeric' => 'numeric',
'refcursor' => 'refcursor',
'regprocedure' => 'regprocedure',
'regoper' => 'regoper',
'regoperator' => 'regoperator',
'regclass' => 'regclass',
'regcollation' => 'regcollation',
'regtype' => 'regtype',
'regrole' => 'regrole',
'regnamespace' => 'regnamespace',
'uuid' => 'uuid',
'pg_lsn' => 'pg_lsn',
'tsvector' => 'tsvector',
'gtsvector' => 'gtsvector',
'tsquery' => 'tsquery',
'regconfig' => 'regconfig',
'regdictionary' => 'regdictionary',
'jsonb' => 'jsonb',
'jsonpath' => 'jsonpath',
'txid_snapshot' => 'txid_snapshot',
'pg_snapshot' => 'pg_snapshot'
}.freeze
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module SchemaObjects
class Base
def initialize(parsed_stmt)
@parsed_stmt = parsed_stmt
end
def name
raise NoMethodError, "subclasses of #{self.class.name} must implement #{__method__}"
end
def table_name
parsed_stmt.relation.relname
end
def statement
@statement ||= PgQuery.deparse_stmt(parsed_stmt)
end
private
attr_reader :parsed_stmt
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module SchemaObjects
class Column
def initialize(adapter)
@adapter = adapter
end
attr_reader :adapter
def name
adapter.name
end
def table_name
adapter.table_name
end
def partition_key?
adapter.partition_key?
end
def statement
[adapter.name, adapter.data_type, adapter.default, adapter.nullable].compact.join(' ')
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module SchemaObjects
class ForeignKey
def initialize(adapter)
@adapter = adapter
end
# Foreign key name should include the schema, as the same name could be used across different schemas
#
# @example public.foreign_key_name
def name
@name ||= adapter.name
end
def table_name
@table_name ||= adapter.table_name
end
def statement
@statement ||= adapter.statement
end
private
attr_reader :adapter
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module SchemaObjects
class Index < Base
def name
parsed_stmt.idxname
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module SchemaObjects
class Table
def initialize(name, columns)
@name = name
@columns = columns
end
attr_reader :name, :columns
def table_name
name
end
def statement
format('CREATE TABLE %s (%s)', name, columns_statement)
end
def fetch_column_by_name(column_name)
columns.find { |column| column.name == column_name }
end
def column_exists?(column_name)
column = fetch_column_by_name(column_name)
return false if column.nil?
true
end
private
def columns_statement
columns.reject(&:partition_key?).map(&:statement).join(', ')
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module SchemaObjects
class Trigger < Base
def name
parsed_stmt.trigname
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module Sources
class Database
STATIC_PARTITIONS_SCHEMA = 'gitlab_partitions_static'
def initialize(connection)
@connection = connection
end
def fetch_index_by_name(index_name)
index_map[index_name]
end
def fetch_trigger_by_name(trigger_name)
trigger_map[trigger_name]
end
def fetch_foreign_key_by_name(foreign_key_name)
foreign_key_map[foreign_key_name]
end
def fetch_table_by_name(table_name)
table_map[table_name]
end
def index_exists?(index_name)
index = index_map[index_name]
return false if index.nil?
true
end
def trigger_exists?(trigger_name)
trigger = trigger_map[trigger_name]
return false if trigger.nil?
true
end
def foreign_key_exists?(foreign_key_name)
foreign_key = fetch_foreign_key_by_name(foreign_key_name)
return false if foreign_key.nil?
true
end
def table_exists?(table_name)
table = fetch_table_by_name(table_name)
return false if table.nil?
true
end
def indexes
index_map.values
end
def triggers
trigger_map.values
end
def foreign_keys
foreign_key_map.values
end
def tables
table_map.values
end
private
attr_reader :connection
def schemas
@schemas ||= [STATIC_PARTITIONS_SCHEMA, connection.current_schema]
end
def trigger_map
@trigger_map ||=
fetch_triggers.transform_values! do |trigger_stmt|
SchemaObjects::Trigger.new(PgQuery.parse(trigger_stmt).tree.stmts.first.stmt.create_trig_stmt)
end
end
def fetch_triggers
# rubocop:disable Rails/SquishedSQLHeredocs
sql = <<~SQL
SELECT triggers.tgname, pg_get_triggerdef(triggers.oid)
FROM pg_catalog.pg_trigger triggers
INNER JOIN pg_catalog.pg_class rel ON triggers.tgrelid = rel.oid
INNER JOIN pg_catalog.pg_namespace nsp ON nsp.oid = rel.relnamespace
WHERE triggers.tgisinternal IS FALSE
AND nsp.nspname IN ($1, $2)
SQL
# rubocop:enable Rails/SquishedSQLHeredocs
connection.select_rows(sql, nil, schemas).to_h
end
def table_map
@table_map ||= fetch_tables.transform_values! do |stmt|
columns = stmt.map { |column| SchemaObjects::Column.new(Adapters::ColumnDatabaseAdapter.new(column)) }
SchemaObjects::Table.new(stmt.first['table_name'], columns)
end
end
def fetch_tables
# rubocop:disable Rails/SquishedSQLHeredocs
sql = <<~SQL
SELECT
table_information.relname AS table_name,
col_information.attname AS column_name,
col_information.attnotnull AS not_null,
col_information.attnum = ANY(pg_partitioned_table.partattrs) as partition_key,
format_type(col_information.atttypid, col_information.atttypmod) AS data_type,
pg_get_expr(col_default_information.adbin, col_default_information.adrelid) AS column_default
FROM pg_attribute AS col_information
JOIN pg_class AS table_information ON col_information.attrelid = table_information.oid
JOIN pg_namespace AS schema_information ON table_information.relnamespace = schema_information.oid
LEFT JOIN pg_partitioned_table ON pg_partitioned_table.partrelid = table_information.oid
LEFT JOIN pg_attrdef AS col_default_information ON col_information.attrelid = col_default_information.adrelid
AND col_information.attnum = col_default_information.adnum
WHERE NOT col_information.attisdropped
AND col_information.attnum > 0
AND table_information.relkind IN ('r', 'p')
AND schema_information.nspname IN ($1, $2)
SQL
# rubocop:enable Rails/SquishedSQLHeredocs
connection.exec_query(sql, nil, schemas).group_by { |row| row['table_name'] }
end
def fetch_indexes
# rubocop:disable Rails/SquishedSQLHeredocs
sql = <<~SQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE indexname NOT LIKE '%_pkey' AND schemaname IN ($1, $2);
SQL
# rubocop:enable Rails/SquishedSQLHeredocs
connection.select_rows(sql, nil, schemas).to_h
end
def index_map
@index_map ||=
fetch_indexes.transform_values! do |index_stmt|
SchemaObjects::Index.new(PgQuery.parse(index_stmt).tree.stmts.first.stmt.index_stmt)
end
end
def foreign_key_map
@foreign_key_map ||= fetch_fks.each_with_object({}) do |stmt, result|
adapter = Adapters::ForeignKeyDatabaseAdapter.new(stmt)
result[adapter.name] = SchemaObjects::ForeignKey.new(adapter)
end
end
def fetch_fks
# rubocop:disable Rails/SquishedSQLHeredocs
sql = <<~SQL
SELECT
pg_namespace.nspname::text AS schema,
pg_class.relname::text AS table_name,
pg_constraint.conname AS foreign_key_name,
pg_get_constraintdef(pg_constraint.oid) AS foreign_key_definition
FROM pg_constraint
INNER JOIN pg_class ON pg_constraint.conrelid = pg_class.oid
INNER JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid
WHERE contype = 'f'
AND pg_namespace.nspname = $1
AND pg_constraint.conparentid = 0
SQL
# rubocop:enable Rails/SquishedSQLHeredocs
connection.exec_query(sql, nil, [connection.current_schema])
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module Sources
class StructureSql
DEFAULT_SCHEMA = 'public'
def initialize(structure_file_path, schema_name = DEFAULT_SCHEMA)
@structure_file_path = structure_file_path
@schema_name = schema_name
end
def index_exists?(index_name)
index = indexes.find { |index| index.name == index_name }
return false if index.nil?
true
end
def trigger_exists?(trigger_name)
trigger = triggers.find { |trigger| trigger.name == trigger_name }
return false if trigger.nil?
true
end
def foreign_key_exists?(foreign_key_name)
foreign_key = foreign_keys.find { |fk| fk.name == foreign_key_name }
return false if foreign_key.nil?
true
end
def table_exists?(table_name)
table = fetch_table_by_name(table_name)
return false if table.nil?
true
end
def fetch_table_by_name(table_name)
tables.find { |table| table.name == table_name }
end
def indexes
@indexes ||= map_with_default_schema(index_statements, SchemaObjects::Index)
end
def triggers
@triggers ||= map_with_default_schema(trigger_statements, SchemaObjects::Trigger)
end
def foreign_keys
@foreign_keys ||= foreign_key_statements.map do |stmt|
stmt.relation.schemaname = schema_name if stmt.relation.schemaname == ''
SchemaObjects::ForeignKey.new(Adapters::ForeignKeyStructureSqlAdapter.new(stmt))
end
end
def tables
@tables ||= table_statements.map do |stmt|
table_name = stmt.relation.relname
partition_stmt = stmt.partspec
columns = stmt.table_elts.select { |n| n.node == :column_def }.map do |column|
adapter = Adapters::ColumnStructureSqlAdapter.new(table_name, column.column_def, partition_stmt)
SchemaObjects::Column.new(adapter)
end
SchemaObjects::Table.new(table_name, columns)
end
end
private
attr_reader :structure_file_path, :schema_name
def index_statements
statements.filter_map { |s| s.stmt.index_stmt }
end
def trigger_statements
statements.filter_map { |s| s.stmt.create_trig_stmt }
end
def table_statements
statements.filter_map { |s| s.stmt.create_stmt }
end
def foreign_key_statements
constraint_statements(:CONSTR_FOREIGN)
end
# Filter constraint statement nodes
#
# @param constraint_type [Symbol] node type. One of CONSTR_PRIMARY, CONSTR_CHECK, CONSTR_EXCLUSION,
# CONSTR_UNIQUE or CONSTR_FOREIGN.
def constraint_statements(constraint_type)
alter_table_statements(:AT_AddConstraint).filter do |stmt|
stmt.cmds.first.alter_table_cmd.def.constraint.contype == constraint_type
end
end
# Filter alter table statement nodes
#
# @param subtype [Symbol] node subtype +AT_AttachPartition+, +AT_ColumnDefault+ or +AT_AddConstraint+
def alter_table_statements(subtype)
statements.filter_map do |statement|
node = statement.stmt.alter_table_stmt
next unless node
node if node.cmds.first.alter_table_cmd.subtype == subtype
end
end
def statements
@statements ||= parsed_structure_file.tree.stmts
end
def parsed_structure_file
PgQuery.parse(File.read(structure_file_path))
end
def map_with_default_schema(statements, validation_class)
statements.map do |statement|
statement.relation.schemaname = schema_name if statement.relation.schemaname == ''
validation_class.new(statement)
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Schema
module Validation
module Validators
class Base
ERROR_MESSAGE = 'A schema inconsistency has been found'
def initialize(structure_sql, database)
@structure_sql = structure_sql
@database = database
end
def execute
raise NoMethodError, "subclasses of #{self.class.name} must implement #{__method__}"
end
private
attr_reader :structure_sql, :database
def build_inconsistency(validator_class, structure_sql_object, database_object)
Inconsistency.new(validator_class, structure_sql_object, database_object)
end
end
end
end
end
end
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册