Skip to content

Commit 306eeaf

Browse files
author
Carlos Silva
committed
Add support for multiple schemas
1 parent 59bff54 commit 306eeaf

17 files changed

+343
-69
lines changed

lib/torque/postgresql.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
require 'torque/postgresql/attributes'
2121
require 'torque/postgresql/autosave_association'
2222
require 'torque/postgresql/auxiliary_statement'
23-
require 'torque/postgresql/base'
2423
require 'torque/postgresql/inheritance'
24+
require 'torque/postgresql/base' # Needs to be after inheritance
2525
require 'torque/postgresql/insert_all'
2626
require 'torque/postgresql/migration'
2727
require 'torque/postgresql/relation'
2828
require 'torque/postgresql/reflection'
2929
require 'torque/postgresql/schema_cache'
30+
require 'torque/postgresql/table_name'
3031

3132
require 'torque/postgresql/railtie' if defined?(Rails)

lib/torque/postgresql/adapter.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ def version
3131
)
3232
end
3333

34-
# Add `inherits` to the list of extracted table options
34+
# Add `inherits` and `schema` to the list of extracted table options
3535
def extract_table_options!(options)
36-
super.merge(options.extract!(:inherits))
36+
super.merge(options.extract!(:inherits, :schema))
3737
end
3838

3939
# Allow filtered bulk insert by adding the where clause. This method is

lib/torque/postgresql/adapter/database_statements.rb

+61-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ def dump_mode!
1212
@_dump_mode = !!!@_dump_mode
1313
end
1414

15+
# List of schemas blocked by the application in the current connection
16+
def schemas_blacklist
17+
@schemas_blacklist ||= Torque::PostgreSQL.config.schemas.blacklist +
18+
(@config.dig(:schemas, 'blacklist') || [])
19+
end
20+
21+
# List of schemas used by the application in the current connection
22+
def schemas_whitelist
23+
@schemas_whitelist ||= Torque::PostgreSQL.config.schemas.whitelist +
24+
(@config.dig(:schemas, 'whitelist') || [])
25+
end
26+
27+
# A list of schemas on the search path sanitized
28+
def schemas_search_path_sanitized
29+
@schemas_search_path_sanitized ||= begin
30+
db_user = @config[:username] || ENV['USER'] || ENV['USERNAME']
31+
schema_search_path.split(',').map { |item| item.strip.sub('"$user"', db_user) }
32+
end
33+
end
34+
1535
# Check if a given type is valid.
1636
def valid_type?(type)
1737
super || extended_types.include?(type)
@@ -22,6 +42,17 @@ def extended_types
2242
EXTENDED_DATABASE_TYPES
2343
end
2444

45+
# Checks if a given schema exists in the database. If +filtered+ is
46+
# given as false, then it will check regardless of whitelist and
47+
# blacklist
48+
def schema_exists?(name, filtered: true)
49+
return user_defined_schemas.include?(name.to_s) if filtered
50+
51+
query_value(<<-SQL) == 1
52+
SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = '#{name}'
53+
SQL
54+
end
55+
2556
# Returns true if type exists.
2657
def type_exists?(name)
2758
user_defined_types.key? name.to_s
@@ -124,18 +155,41 @@ def user_defined_types(*categories)
124155
# Get the list of inherited tables associated with their parent tables
125156
def inherited_tables
126157
tables = query(<<-SQL, 'SCHEMA')
127-
SELECT child.relname AS table_name,
128-
array_agg(parent.relname) AS inheritances
158+
SELECT inhrelid::regclass AS table_name,
159+
inhparent::regclass AS inheritances
129160
FROM pg_inherits
130161
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
131162
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
132-
GROUP BY child.relname, pg_inherits.inhrelid
133-
ORDER BY pg_inherits.inhrelid
163+
ORDER BY inhrelid
134164
SQL
135165

136-
tables.map do |(table, refs)|
137-
[table, PG::TextDecoder::Array.new.decode(refs)]
138-
end.to_h
166+
tables.each_with_object({}) do |(child, parent), result|
167+
(result[child] ||= []) << parent
168+
end
169+
end
170+
171+
# Get the list of schemas that were created by the user
172+
def user_defined_schemas
173+
query_values(user_defined_schemas_sql, 'SCHEMA')
174+
end
175+
176+
# Build the query for allowed schemas
177+
def user_defined_schemas_sql
178+
conditions = []
179+
conditions << <<-SQL if schemas_blacklist.any?
180+
nspname NOT LIKE ANY (ARRAY['#{schemas_blacklist.join("', '")}'])
181+
SQL
182+
183+
conditions << <<-SQL if schemas_whitelist.any?
184+
nspname LIKE ANY (ARRAY['#{schemas_whitelist.join("', '")}'])
185+
SQL
186+
187+
<<-SQL.squish
188+
SELECT nspname
189+
FROM pg_catalog.pg_namespace
190+
WHERE 1=1 AND #{conditions.join(' AND ')}
191+
ORDER BY oid
192+
SQL
139193
end
140194

141195
# Get the list of columns, and their definition, but only from the

lib/torque/postgresql/adapter/schema_dumper.rb

+23-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ def dump(stream) # :nodoc:
1212
stream
1313
end
1414

15+
def extensions(stream) # :nodoc:
16+
super
17+
user_defined_schemas(stream)
18+
end
19+
1520
# Translate +:enum_set+ into +:enum+
1621
def schema_type(column)
1722
column.type == :enum_set ? :enum : super
@@ -34,7 +39,9 @@ def around_tables(stream)
3439

3540
def dump_tables(stream)
3641
inherited_tables = @connection.inherited_tables
37-
sorted_tables = @connection.tables.sort - @connection.views
42+
sorted_tables = (@connection.tables - @connection.views).sort_by do |table_name|
43+
table_name.split(/(?:public)?\./).reverse
44+
end
3845

3946
stream.puts " # These are the common tables"
4047
(sorted_tables - inherited_tables.keys).each do |table_name|
@@ -51,7 +58,7 @@ def dump_tables(stream)
5158

5259
# Add the inherits setting
5360
sub_stream.rewind
54-
inherits.map!(&:to_sym)
61+
inherits.map! { |parent| parent.to_s.sub(/\Apublic\./, '') }
5562
inherits = inherits.first if inherits.size === 1
5663
inherits = ", inherits: #{inherits.inspect} do |t|"
5764
table_dump = sub_stream.read.gsub(/ do \|t\|$/, inherits)
@@ -70,6 +77,20 @@ def dump_tables(stream)
7077
end
7178
end
7279

80+
# Make sure to remove the schema from the table name
81+
def remove_prefix_and_suffix(table)
82+
super(table.sub(/\A[a-z0-9_]*\./, ''))
83+
end
84+
85+
# Dump user defined schemas
86+
def user_defined_schemas(stream)
87+
return if (list = (@connection.user_defined_schemas - ['public'])).empty?
88+
89+
stream.puts " # Custom schemas defined in this database."
90+
list.each { |name| stream.puts " create_schema \"#{name}\", force: :cascade" }
91+
stream.puts
92+
end
93+
7394
def fx_functions_position
7495
return unless defined?(::Fx::SchemaDumper::Function)
7596
Fx.configuration.dump_functions_at_beginning_of_schema ? :beginning : :end

lib/torque/postgresql/adapter/schema_statements.rb

+40
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ module SchemaStatements
77

88
TableDefinition = ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
99

10+
# Create a new schema
11+
def create_schema(name, options = {})
12+
drop_schema(name, options) if options[:force]
13+
14+
check = 'IF NOT EXISTS' if options.fetch(:check, true)
15+
execute("CREATE SCHEMA #{check} #{quote_schema_name(name.to_s)}")
16+
end
17+
18+
# Drop an existing schema
19+
def drop_schema(name, options = {})
20+
force = options.fetch(:force, '').upcase
21+
check = 'IF EXISTS' if options.fetch(:check, true)
22+
execute("DROP SCHEMA #{check} #{quote_schema_name(name.to_s)} #{force}")
23+
end
24+
1025
# Drops a type.
1126
def drop_type(name, options = {})
1227
force = options.fetch(:force, '').upcase
@@ -64,12 +79,37 @@ def enum_values(name)
6479

6580
# Rewrite the method that creates tables to easily accept extra options
6681
def create_table(table_name, **options, &block)
82+
table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
83+
6784
options[:id] = false if options[:inherits].present? &&
6885
options[:primary_key].blank? && options[:id].blank?
6986

7087
super table_name, **options, &block
7188
end
7289

90+
# Add the schema option when extracting table options
91+
def table_options(table_name)
92+
parts = table_name.split('.').reverse
93+
return super unless parts.size == 2 && parts[1] != 'public'
94+
95+
(super || {}).merge(schema: parts[1])
96+
end
97+
98+
# When dumping the schema we need to add all schemas, not only those
99+
# active for the current +schema_search_path+
100+
def quoted_scope(name = nil, type: nil)
101+
return super unless name.nil?
102+
103+
super.merge(schema: "ANY ('{#{user_defined_schemas.join(',')}}')")
104+
end
105+
106+
# Fix the query to include the schema on tables names when dumping
107+
def data_source_sql(name = nil, type: nil)
108+
return super unless name.nil?
109+
110+
super.sub('SELECT c.relname FROM', "SELECT n.nspname || '.' || c.relname FROM")
111+
end
112+
73113
private
74114

75115
def quote_enum_values(name, values, options)

lib/torque/postgresql/base.rb

+18-24
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,27 @@ module PostgreSQL
55
module Base
66
extend ActiveSupport::Concern
77

8+
##
9+
# :singleton-method: schema
10+
# :call-seq: schema
11+
#
12+
# The schema to which the table belongs to.
13+
814
included do
915
mattr_accessor :belongs_to_many_required_by_default, instance_accessor: false
16+
class_attribute :schema, instance_writer: false
1017
end
1118

1219
module ClassMethods
1320
delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
1421

15-
# Wenever it's inherited, add a new list of auxiliary statements
16-
# It also adds an auxiliary statement to load inherited records' relname
22+
# Make sure that table name is an instance of TableName class
23+
def reset_table_name
24+
self.table_name = TableName.new(self, super)
25+
end
26+
27+
# Whenever the base model is inherited, add a list of auxiliary
28+
# statements like the one that loads inherited records' relname
1729
def inherited(subclass)
1830
super
1931

@@ -22,32 +34,14 @@ def inherited(subclass)
2234

2335
record_class = ActiveRecord::Relation._record_class_attribute
2436

25-
# Define helper methods to return the class of the given records
26-
subclass.auxiliary_statement record_class do |cte|
27-
pg_class = ::Arel::Table.new('pg_class')
28-
arel_query = ::Arel::SelectManager.new(pg_class)
29-
arel_query.project(pg_class['oid'], pg_class['relname'].as(record_class.to_s))
30-
31-
cte.query 'pg_class', arel_query.to_sql
32-
cte.attributes col(record_class) => record_class
33-
cte.join tableoid: :oid
34-
end
35-
3637
# Define the dynamic attribute that returns the same information as
3738
# the one provided by the auxiliary statement
3839
subclass.dynamic_attribute(record_class) do
39-
next self.class.table_name unless self.class.physically_inheritances?
40-
41-
pg_class = ::Arel::Table.new('pg_class')
42-
source = ::Arel::Table.new(subclass.table_name, as: 'source')
43-
quoted_id = ::Arel::Nodes::Quoted.new(id)
44-
45-
query = ::Arel::SelectManager.new(pg_class)
46-
query.join(source).on(pg_class['oid'].eq(source['tableoid']))
47-
query.where(source[subclass.primary_key].eq(quoted_id))
48-
query.project(pg_class['relname'])
40+
klass = self.class
41+
next klass.table_name unless klass.physically_inheritances?
4942

50-
self.class.connection.select_value(query)
43+
query = klass.unscoped.where(subclass.primary_key => id)
44+
query.pluck(klass.arel_table['tableoid'].cast('regclass')).first
5145
end
5246
end
5347

lib/torque/postgresql/config.rb

+13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ def config.irregular_models=(hash)
3939

4040
end
4141

42+
# Configure multiple schemas
43+
config.nested(:schemas) do |schemas|
44+
45+
# Defines a list of LIKE-based schemas to not consider for a multiple
46+
# schema database
47+
schemas.blacklist = %w[information_schema pg_%]
48+
49+
# Defines a list of LIKE-based schemas to consider for a multiple schema
50+
# database
51+
schemas.whitelist = %w[public]
52+
53+
end
54+
4255
# Configure auxiliary statement features
4356
config.nested(:auxiliary_statement) do |cte|
4457

lib/torque/postgresql/migration/command_recorder.rb

+8-8
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@ module PostgreSQL
55
module Migration
66
module CommandRecorder
77

8-
# Records the rename operation for types.
8+
# Records the rename operation for types
99
def rename_type(*args, &block)
1010
record(:rename_type, args, &block)
1111
end
1212

13-
# Inverts the type name.
13+
# Inverts the type rename operation
1414
def invert_rename_type(args)
1515
[:rename_type, args.reverse]
1616
end
1717

18-
# Records the creation of the enum to be reverted.
19-
def create_enum(*args, &block)
20-
record(:create_enum, args, &block)
18+
# Records the creation of a schema
19+
def create_schema(*args, &block)
20+
record(:create_schema, args, &block)
2121
end
2222

23-
# Inverts the creation of the enum.
24-
def invert_create_enum(args)
25-
[:drop_type, [args.first]]
23+
# Inverts the creation of a schema
24+
def invert_create_schema(args)
25+
[:drop_schema, [args.first]]
2626
end
2727

2828
end

lib/torque/postgresql/schema_cache.rb

+6-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ def associations(table_name)
109109

110110
# Try to find a model based on a given table
111111
def lookup_model(table_name, scoped_class = '')
112-
# byebug if table_name == 'activities'
113112
scoped_class = scoped_class.name if scoped_class.is_a?(Class)
114113
return @data_sources_model_names[table_name] \
115114
if @data_sources_model_names.key?(table_name)
@@ -118,6 +117,12 @@ def lookup_model(table_name, scoped_class = '')
118117
scopes = scoped_class.scan(/(?:::)?[A-Z][a-z]+/)
119118
scopes.unshift('Object::')
120119

120+
# Check if the table name comes with a schema
121+
if table_name.include?('.')
122+
schema, table_name = table_name.split('.')
123+
scopes.insert(1, schema.camelize) if schema != 'public'
124+
end
125+
121126
# Consider the maximum namespaced possible model name
122127
max_name = table_name.tr('_', '/').camelize.split(/(::)/)
123128
max_name[-1] = max_name[-1].singularize

0 commit comments

Comments
 (0)