mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-22 13:03:05 +08:00
Some checks failed
Tests / core system (push) Has been cancelled
Tests / plugins system (push) Has been cancelled
Licenses / run (push) Has been cancelled
Linting / run (push) Has been cancelled
Migration Tests / Tests (push) Has been cancelled
Publish Assets / publish-assets (push) Has been cancelled
Tests / core backend (push) Has been cancelled
Tests / plugins backend (push) Has been cancelled
Tests / core frontend (Chrome) (push) Has been cancelled
Tests / plugins frontend (push) Has been cancelled
Tests / themes frontend (push) Has been cancelled
Tests / themes system (push) Has been cancelled
Tests / core frontend (Firefox ESR) (push) Has been cancelled
Tests / core frontend (Firefox Evergreen) (push) Has been cancelled
Tests / chat system (push) Has been cancelled
Tests / merge (push) Has been cancelled
Allow table-level check constraints in the Intermediate DB In https://github.com/discourse/discourse/pull/34339, I’ve worked on distinguishing between `user_custom_fields` tied to `user_fields` and arbitrary `user_custom_fields` entries. To support this, I’ve made both `field_id` and `name` nullable, which requires a table-level constraint to ensure each entry has either a `field_id` (referencing `user_fields`) or an arbitrary `name` for the value. This first pass supports only named table-level `CHECK` constraints. ## Usage ```yaml user_custom_fields: columns: exclude: - "id" modify: - name: "name" nullable: true add: - name: "field_id" datatype: numeric - name: "is_multiselect_field" datatype: boolean indexes: # ... constraints: - name: "require_field_id_or_name" condition: "field_id IS NOT NULL OR name IS NOT NULL" - name: "disallow_both_field_id_and_name" type: check # default, only `check` supported for now condition: "NOT (field_id IS NOT NULL AND name IS NOT NULL)" ``` ```sql CREATE TABLE user_custom_fields ( created_at DATETIME, field_id NUMERIC, is_multiselect_field BOOLEAN, name TEXT, user_id NUMERIC NOT NULL, value TEXT, CONSTRAINT require_field_id_or_name CHECK (field_id IS NOT NULL OR name IS NOT NULL), CONSTRAINT disallow_both_field_id_and_name CHECK (NOT (field_id IS NOT NULL AND name IS NOT NULL)) ); ```
142 lines
3.9 KiB
Ruby
Vendored
142 lines
3.9 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module Migrations::Database::Schema
|
|
class Loader
|
|
def initialize(schema_config)
|
|
@schema_config = schema_config
|
|
@global = GlobalConfig.new(@schema_config)
|
|
end
|
|
|
|
def load_schema
|
|
@db = ActiveRecord::Base.lease_connection
|
|
|
|
schema = []
|
|
existing_table_names = @db.tables.to_set
|
|
|
|
@schema_config[:tables].sort.each do |table_name, config|
|
|
table_name = table_name.to_s
|
|
|
|
if config[:copy_of].present?
|
|
table_alias = table_name
|
|
table_name = config[:copy_of]
|
|
else
|
|
next if @global.excluded_table_name?(table_name)
|
|
end
|
|
|
|
if existing_table_names.include?(table_name)
|
|
schema << table(table_name, config, table_alias)
|
|
end
|
|
end
|
|
|
|
@db = nil
|
|
ActiveRecord::Base.release_connection
|
|
|
|
schema
|
|
end
|
|
|
|
private
|
|
|
|
def table(table_name, config, table_alias = nil)
|
|
primary_key_column_names =
|
|
config[:primary_key_column_names].presence || @db.primary_keys(table_name)
|
|
|
|
columns =
|
|
filtered_columns_of(table_name, config).map do |column|
|
|
Column.new(
|
|
name: name_for(column),
|
|
datatype: datatype_for(column),
|
|
nullable: nullable_for(column, config),
|
|
max_length: column.type == :text ? column.limit : nil,
|
|
is_primary_key: primary_key_column_names.include?(column.name),
|
|
)
|
|
end + added_columns(config, primary_key_column_names)
|
|
|
|
Table.new(
|
|
table_alias || table_name,
|
|
columns,
|
|
indexes(config),
|
|
primary_key_column_names,
|
|
constraints(config),
|
|
)
|
|
end
|
|
|
|
def filtered_columns_of(table_name, config)
|
|
columns_by_name = @db.columns(table_name).index_by(&:name)
|
|
columns_by_name.except!(*@global.excluded_column_names)
|
|
|
|
if (included_columns = config.dig(:columns, :include))
|
|
columns_by_name.slice!(*included_columns)
|
|
elsif (excluded_columns = config.dig(:columns, :exclude))
|
|
columns_by_name.except!(*excluded_columns)
|
|
end
|
|
|
|
columns_by_name.values
|
|
end
|
|
|
|
def added_columns(config, primary_key_column_names)
|
|
columns = config.dig(:columns, :add) || []
|
|
columns.map do |column|
|
|
datatype = column[:datatype].to_sym
|
|
Column.new(
|
|
name: column[:name],
|
|
datatype:,
|
|
nullable: column.fetch(:nullable, true),
|
|
max_length: datatype == :text ? column[:max_length] : nil,
|
|
is_primary_key: primary_key_column_names.include?(column[:name]),
|
|
)
|
|
end
|
|
end
|
|
|
|
def name_for(column)
|
|
@global.modified_name(column.name) || column.name
|
|
end
|
|
|
|
def datatype_for(column)
|
|
datatype = @global.modified_datatype(column.name) || column.type
|
|
|
|
case datatype
|
|
when :binary
|
|
:blob
|
|
when :string, :enum, :uuid
|
|
:text
|
|
when :jsonb
|
|
:json
|
|
when :boolean, :date, :datetime, :float, :inet, :integer, :numeric, :json, :text
|
|
datatype
|
|
else
|
|
raise "Unknown datatype: #{datatype}"
|
|
end
|
|
end
|
|
|
|
def nullable_for(column, config)
|
|
modified_column = config.dig(:columns, :modify)&.find { |col| col[:name] == column.name }
|
|
return modified_column[:nullable] if modified_column&.key?(:nullable)
|
|
|
|
global_nullable = @global.modified_nullable(column.name)
|
|
return global_nullable unless global_nullable.nil?
|
|
|
|
column.null || column.default.present?
|
|
end
|
|
|
|
def indexes(config)
|
|
config[:indexes]&.map do |index|
|
|
Index.new(
|
|
name: index[:name],
|
|
column_names: Array.wrap(index[:columns]),
|
|
unique: index.fetch(:unique, false),
|
|
condition: index[:condition],
|
|
)
|
|
end
|
|
end
|
|
|
|
def constraints(config)
|
|
config[:constraints]&.map do |constraint|
|
|
Constraint.new(
|
|
name: constraint[:name],
|
|
type: constraint.fetch(:type, :check).to_sym,
|
|
condition: constraint[:condition],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|