torque-postgresql 3.0.0 → 3.1.0
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.
- checksums.yaml +4 -4
- data/README.rdoc +17 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +61 -7
- data/lib/torque/postgresql/adapter/schema_dumper.rb +39 -7
- data/lib/torque/postgresql/adapter/schema_statements.rb +40 -0
- data/lib/torque/postgresql/adapter.rb +2 -2
- data/lib/torque/postgresql/associations/preloader/loader_query.rb +1 -1
- data/lib/torque/postgresql/base.rb +18 -24
- data/lib/torque/postgresql/config.rb +13 -0
- data/lib/torque/postgresql/migration/command_recorder.rb +8 -8
- data/lib/torque/postgresql/reflection/abstract_reflection.rb +15 -38
- data/lib/torque/postgresql/schema_cache.rb +6 -1
- data/lib/torque/postgresql/table_name.rb +41 -0
- data/lib/torque/postgresql/version.rb +1 -1
- data/lib/torque/postgresql.rb +2 -1
- data/spec/models/internal/user.rb +5 -0
- data/spec/schema.rb +11 -1
- data/spec/spec_helper.rb +3 -1
- data/spec/tests/belongs_to_many_spec.rb +47 -0
- data/spec/tests/enum_set_spec.rb +7 -6
- data/spec/tests/has_many_spec.rb +44 -0
- data/spec/tests/schema_spec.rb +92 -0
- data/spec/tests/table_inheritance_spec.rb +11 -15
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25fb390d67c43461cf2c03096990149d2dbcae04072e96d69896d60f8e368863
|
4
|
+
data.tar.gz: e2910f192602059a71dbf3cfca2cc412e2d34eb1faacc51620058ce408c1be9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c664a0e98b7cc78b5930d4e3273330caa49cb8a18b792892cc42dae96b2392ebc61b71043c758c5e0abc3c54b12f8ee567fc53a48ad9f6765726ed1e000e2770
|
7
|
+
data.tar.gz: cfdb00e3740a05f2cabc1ccb25070ce05f5a628faa84209a899d5a247c17465dba0cac0a09a305e0c19fb05fb456c8d87e05107c9f0acb66803226a86ae2083b
|
data/README.rdoc
CHANGED
@@ -128,6 +128,23 @@ reconfigured on the model, and then can be used during querying process.
|
|
128
128
|
|
129
129
|
{Learn more}[link:classes/Torque/PostgreSQL/AuxiliaryStatement.html]
|
130
130
|
|
131
|
+
* Multiple Schemas
|
132
|
+
|
133
|
+
Allows models and modules to have a schema associated with them, so that
|
134
|
+
developers can better organize their tables into schemas and build features in
|
135
|
+
a way that the database can better represent how they are separated.
|
136
|
+
|
137
|
+
create_schema "internal", force: :cascade
|
138
|
+
|
139
|
+
module Internal
|
140
|
+
class User < ActiveRecord::Base
|
141
|
+
self.schema = 'internal'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
Internal::User.all
|
146
|
+
|
147
|
+
{Learn more}[link:classes/Torque/PostgreSQL/Adapter/DatabaseStatements.html]
|
131
148
|
|
132
149
|
== Download and installation
|
133
150
|
|
@@ -12,6 +12,26 @@ module Torque
|
|
12
12
|
@_dump_mode = !!!@_dump_mode
|
13
13
|
end
|
14
14
|
|
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
|
+
|
15
35
|
# Check if a given type is valid.
|
16
36
|
def valid_type?(type)
|
17
37
|
super || extended_types.include?(type)
|
@@ -22,6 +42,17 @@ module Torque
|
|
22
42
|
EXTENDED_DATABASE_TYPES
|
23
43
|
end
|
24
44
|
|
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
|
+
|
25
56
|
# Returns true if type exists.
|
26
57
|
def type_exists?(name)
|
27
58
|
user_defined_types.key? name.to_s
|
@@ -124,18 +155,41 @@ module Torque
|
|
124
155
|
# Get the list of inherited tables associated with their parent tables
|
125
156
|
def inherited_tables
|
126
157
|
tables = query(<<-SQL, 'SCHEMA')
|
127
|
-
SELECT
|
128
|
-
|
158
|
+
SELECT inhrelid::regclass AS table_name,
|
159
|
+
inhparent::regclass AS inheritances
|
129
160
|
FROM pg_inherits
|
130
161
|
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
|
131
162
|
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
|
132
|
-
|
133
|
-
ORDER BY pg_inherits.inhrelid
|
163
|
+
ORDER BY inhrelid
|
134
164
|
SQL
|
135
165
|
|
136
|
-
tables.
|
137
|
-
[
|
138
|
-
end
|
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
|
139
193
|
end
|
140
194
|
|
141
195
|
# Get the list of columns, and their definition, but only from the
|
@@ -12,6 +12,11 @@ module Torque
|
|
12
12
|
stream
|
13
13
|
end
|
14
14
|
|
15
|
+
def extensions(stream) # :nodoc:
|
16
|
+
super
|
17
|
+
user_defined_schemas(stream)
|
18
|
+
end
|
19
|
+
|
15
20
|
# Translate +:enum_set+ into +:enum+
|
16
21
|
def schema_type(column)
|
17
22
|
column.type == :enum_set ? :enum : super
|
@@ -20,8 +25,23 @@ module Torque
|
|
20
25
|
private
|
21
26
|
|
22
27
|
def tables(stream) # :nodoc:
|
28
|
+
around_tables(stream) { dump_tables(stream) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def around_tables(stream)
|
32
|
+
functions(stream) if fx_functions_position == :beginning
|
33
|
+
|
34
|
+
yield
|
35
|
+
|
36
|
+
functions(stream) if fx_functions_position == :end
|
37
|
+
triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
|
38
|
+
end
|
39
|
+
|
40
|
+
def dump_tables(stream)
|
23
41
|
inherited_tables = @connection.inherited_tables
|
24
|
-
sorted_tables = @connection.tables
|
42
|
+
sorted_tables = (@connection.tables - @connection.views).sort_by do |table_name|
|
43
|
+
table_name.split(/(?:public)?\./).reverse
|
44
|
+
end
|
25
45
|
|
26
46
|
stream.puts " # These are the common tables"
|
27
47
|
(sorted_tables - inherited_tables.keys).each do |table_name|
|
@@ -38,7 +58,7 @@ module Torque
|
|
38
58
|
|
39
59
|
# Add the inherits setting
|
40
60
|
sub_stream.rewind
|
41
|
-
inherits.map!(
|
61
|
+
inherits.map! { |parent| parent.to_s.sub(/\Apublic\./, '') }
|
42
62
|
inherits = inherits.first if inherits.size === 1
|
43
63
|
inherits = ", inherits: #{inherits.inspect} do |t|"
|
44
64
|
table_dump = sub_stream.read.gsub(/ do \|t\|$/, inherits)
|
@@ -55,13 +75,25 @@ module Torque
|
|
55
75
|
foreign_keys(tbl, stream) unless ignored?(tbl)
|
56
76
|
end
|
57
77
|
end
|
78
|
+
end
|
79
|
+
|
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
|
58
84
|
|
59
|
-
|
60
|
-
|
85
|
+
# Dump user defined schemas
|
86
|
+
def user_defined_schemas(stream)
|
87
|
+
return if (list = (@connection.user_defined_schemas - ['public'])).empty?
|
61
88
|
|
62
|
-
#
|
63
|
-
|
64
|
-
|
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
|
+
|
94
|
+
def fx_functions_position
|
95
|
+
return unless defined?(::Fx::SchemaDumper::Function)
|
96
|
+
Fx.configuration.dump_functions_at_beginning_of_schema ? :beginning : :end
|
65
97
|
end
|
66
98
|
end
|
67
99
|
|
@@ -7,6 +7,21 @@ module Torque
|
|
7
7
|
|
8
8
|
TableDefinition = ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
|
9
9
|
|
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
|
+
|
10
25
|
# Drops a type.
|
11
26
|
def drop_type(name, options = {})
|
12
27
|
force = options.fetch(:force, '').upcase
|
@@ -64,12 +79,37 @@ module Torque
|
|
64
79
|
|
65
80
|
# Rewrite the method that creates tables to easily accept extra options
|
66
81
|
def create_table(table_name, **options, &block)
|
82
|
+
table_name = "#{options[:schema]}.#{table_name}" if options[:schema].present?
|
83
|
+
|
67
84
|
options[:id] = false if options[:inherits].present? &&
|
68
85
|
options[:primary_key].blank? && options[:id].blank?
|
69
86
|
|
70
87
|
super table_name, **options, &block
|
71
88
|
end
|
72
89
|
|
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
|
+
|
73
113
|
private
|
74
114
|
|
75
115
|
def quote_enum_values(name, values, options)
|
@@ -31,9 +31,9 @@ module Torque
|
|
31
31
|
)
|
32
32
|
end
|
33
33
|
|
34
|
-
# Add `inherits` to the list of extracted table options
|
34
|
+
# Add `inherits` and `schema` to the list of extracted table options
|
35
35
|
def extract_table_options!(options)
|
36
|
-
super.merge(options.extract!(:inherits))
|
36
|
+
super.merge(options.extract!(:inherits, :schema))
|
37
37
|
end
|
38
38
|
|
39
39
|
# Allow filtered bulk insert by adding the where clause. This method is
|
@@ -5,15 +5,27 @@ module Torque
|
|
5
5
|
module Base
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
+
##
|
9
|
+
# :singleton-method: schema
|
10
|
+
# :call-seq: schema
|
11
|
+
#
|
12
|
+
# The schema to which the table belongs to.
|
13
|
+
|
8
14
|
included do
|
9
15
|
mattr_accessor :belongs_to_many_required_by_default, instance_accessor: false
|
16
|
+
class_attribute :schema, instance_writer: false
|
10
17
|
end
|
11
18
|
|
12
19
|
module ClassMethods
|
13
20
|
delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
|
14
21
|
|
15
|
-
#
|
16
|
-
|
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
|
17
29
|
def inherited(subclass)
|
18
30
|
super
|
19
31
|
|
@@ -22,32 +34,14 @@ module Torque
|
|
22
34
|
|
23
35
|
record_class = ActiveRecord::Relation._record_class_attribute
|
24
36
|
|
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
|
-
|
36
37
|
# Define the dynamic attribute that returns the same information as
|
37
38
|
# the one provided by the auxiliary statement
|
38
39
|
subclass.dynamic_attribute(record_class) do
|
39
|
-
|
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?
|
49
42
|
|
50
|
-
|
43
|
+
query = klass.unscoped.where(subclass.primary_key => id)
|
44
|
+
query.pluck(klass.arel_table['tableoid'].cast('regclass')).first
|
51
45
|
end
|
52
46
|
end
|
53
47
|
|
@@ -39,6 +39,19 @@ module Torque
|
|
39
39
|
|
40
40
|
end
|
41
41
|
|
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
|
+
|
42
55
|
# Configure auxiliary statement features
|
43
56
|
config.nested(:auxiliary_statement) do |cte|
|
44
57
|
|
@@ -5,24 +5,24 @@ module Torque
|
|
5
5
|
module Migration
|
6
6
|
module CommandRecorder
|
7
7
|
|
8
|
-
# Records the rename operation for types
|
8
|
+
# Records the rename operation for types
|
9
9
|
def rename_type(*args, &block)
|
10
10
|
record(:rename_type, args, &block)
|
11
11
|
end
|
12
12
|
|
13
|
-
# Inverts the type
|
13
|
+
# Inverts the type rename operation
|
14
14
|
def invert_rename_type(args)
|
15
15
|
[:rename_type, args.reverse]
|
16
16
|
end
|
17
17
|
|
18
|
-
# Records the creation of
|
19
|
-
def
|
20
|
-
record(:
|
18
|
+
# Records the creation of a schema
|
19
|
+
def create_schema(*args, &block)
|
20
|
+
record(:create_schema, args, &block)
|
21
21
|
end
|
22
22
|
|
23
|
-
# Inverts the creation of
|
24
|
-
def
|
25
|
-
[:
|
23
|
+
# Inverts the creation of a schema
|
24
|
+
def invert_create_schema(args)
|
25
|
+
[:drop_schema, [args.first]]
|
26
26
|
end
|
27
27
|
|
28
28
|
end
|
@@ -40,34 +40,29 @@ module Torque
|
|
40
40
|
result
|
41
41
|
end
|
42
42
|
|
43
|
-
# Build the id constraint checking if both types are perfect matching
|
43
|
+
# Build the id constraint checking if both types are perfect matching.
|
44
|
+
# The klass attribute (left side) will always be a column attribute
|
44
45
|
def build_id_constraint(klass_attr, source_attr)
|
45
46
|
return klass_attr.eq(source_attr) unless connected_through_array?
|
46
47
|
|
47
48
|
# Klass and key are associated with the reflection Class
|
48
|
-
klass_type = klass.columns_hash[
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
49
|
+
klass_type = klass.columns_hash[join_keys.key.to_s]
|
50
|
+
|
51
|
+
# Apply an ANY operation which checks if the single value on the left
|
52
|
+
# side exists in the array on the right side
|
53
|
+
if source_attr.is_a?(AREL_ATTR)
|
54
|
+
any_value = [klass_attr, source_attr]
|
55
|
+
any_value.reverse! if klass_type.try(:array?)
|
56
|
+
return any_value.shift.eq(::Arel::Nodes::NamedFunction.new('ANY', any_value))
|
57
|
+
end
|
56
58
|
|
57
59
|
# If the left side is not an array, just use the IN condition
|
58
60
|
return klass_attr.in(source_attr) unless klass_type.try(:array)
|
59
61
|
|
60
|
-
#
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# Apply necessary transformations to values
|
66
|
-
klass_attr = cast_constraint_to_array(klass_type, klass_attr, should_cast)
|
67
|
-
source_attr = cast_constraint_to_array(source_type, source_attr, should_cast)
|
68
|
-
|
69
|
-
# Return the overlap condition
|
70
|
-
klass_attr.overlaps(source_attr)
|
62
|
+
# Build the overlap condition (array && array) ensuring that the right
|
63
|
+
# side has the same type as the left side
|
64
|
+
source_attr = ::Arel::Nodes.build_quoted(Array.wrap(source_attr))
|
65
|
+
klass_attr.overlaps(source_attr.cast(klass_type.sql_type_metadata.sql_type))
|
71
66
|
end
|
72
67
|
|
73
68
|
# TODO: Deprecate this method
|
@@ -83,24 +78,6 @@ module Torque
|
|
83
78
|
|
84
79
|
build_id_constraint(klass_attr, source_attr)
|
85
80
|
end
|
86
|
-
|
87
|
-
# Prepare a value for an array constraint overlap condition
|
88
|
-
def cast_constraint_to_array(type, value, should_cast)
|
89
|
-
base_ready = type.try(:array) && value.is_a?(AREL_ATTR)
|
90
|
-
return value if base_ready && (type.sql_type.eql?(ARR_NO_CAST) || !should_cast)
|
91
|
-
|
92
|
-
value = ::Arel::Nodes.build_quoted(Array.wrap(value)) unless base_ready
|
93
|
-
value = value.cast(ARR_CAST) if should_cast
|
94
|
-
value
|
95
|
-
end
|
96
|
-
|
97
|
-
# Check if it's possible to turn both attributes into an ANY condition
|
98
|
-
def arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
|
99
|
-
return unless !klass_type.try(:array) && source_type.try(:array) &&
|
100
|
-
klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR)
|
101
|
-
|
102
|
-
::Arel::Nodes::NamedFunction.new('ANY', [source_attr])
|
103
|
-
end
|
104
81
|
end
|
105
82
|
|
106
83
|
::ActiveRecord::Reflection::AbstractReflection.prepend(AbstractReflection)
|
@@ -109,7 +109,6 @@ module Torque
|
|
109
109
|
|
110
110
|
# Try to find a model based on a given table
|
111
111
|
def lookup_model(table_name, scoped_class = '')
|
112
|
-
# byebug if table_name == 'activities'
|
113
112
|
scoped_class = scoped_class.name if scoped_class.is_a?(Class)
|
114
113
|
return @data_sources_model_names[table_name] \
|
115
114
|
if @data_sources_model_names.key?(table_name)
|
@@ -118,6 +117,12 @@ module Torque
|
|
118
117
|
scopes = scoped_class.scan(/(?:::)?[A-Z][a-z]+/)
|
119
118
|
scopes.unshift('Object::')
|
120
119
|
|
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
|
+
|
121
126
|
# Consider the maximum namespaced possible model name
|
122
127
|
max_name = table_name.tr('_', '/').camelize.split(/(::)/)
|
123
128
|
max_name[-1] = max_name[-1].singularize
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Torque
|
4
|
+
module PostgreSQL
|
5
|
+
class TableName < Delegator
|
6
|
+
def initialize(klass, table_name)
|
7
|
+
@klass = klass
|
8
|
+
@table_name = table_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def schema
|
12
|
+
return @schema if defined?(@schema)
|
13
|
+
|
14
|
+
@schema = ([@klass] + @klass.module_parents[0..-2]).find do |klass|
|
15
|
+
next unless klass.respond_to?(:schema)
|
16
|
+
break klass.schema
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
schema.nil? ? @table_name : "#{schema}.#{@table_name}"
|
22
|
+
end
|
23
|
+
|
24
|
+
alias __getobj__ to_s
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
other.to_s =~ /("?#{schema | search_path_schemes.join('|')}"?\.)?"?#{@table_name}"?/
|
28
|
+
end
|
29
|
+
|
30
|
+
def __setobj__(value)
|
31
|
+
@table_name = value
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def search_path_schemes
|
37
|
+
klass.connection.schemas_search_path_sanitized
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/torque/postgresql.rb
CHANGED
@@ -20,12 +20,13 @@ require 'torque/postgresql/associations'
|
|
20
20
|
require 'torque/postgresql/attributes'
|
21
21
|
require 'torque/postgresql/autosave_association'
|
22
22
|
require 'torque/postgresql/auxiliary_statement'
|
23
|
-
require 'torque/postgresql/base'
|
24
23
|
require 'torque/postgresql/inheritance'
|
24
|
+
require 'torque/postgresql/base' # Needs to be after inheritance
|
25
25
|
require 'torque/postgresql/insert_all'
|
26
26
|
require 'torque/postgresql/migration'
|
27
27
|
require 'torque/postgresql/relation'
|
28
28
|
require 'torque/postgresql/reflection'
|
29
29
|
require 'torque/postgresql/schema_cache'
|
30
|
+
require 'torque/postgresql/table_name'
|
30
31
|
|
31
32
|
require 'torque/postgresql/railtie' if defined?(Rails)
|
data/spec/schema.rb
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
version =
|
13
|
+
version = 1
|
14
14
|
|
15
15
|
return if ActiveRecord::Migrator.current_version == version
|
16
16
|
ActiveRecord::Schema.define(version: version) do
|
@@ -20,6 +20,9 @@ ActiveRecord::Schema.define(version: version) do
|
|
20
20
|
enable_extension "pgcrypto"
|
21
21
|
enable_extension "plpgsql"
|
22
22
|
|
23
|
+
# Custom schemas used in this database.
|
24
|
+
create_schema "internal", force: :cascade
|
25
|
+
|
23
26
|
# Custom types defined in this database.
|
24
27
|
# Note that some types may not work with other database engines. Be careful if changing database.
|
25
28
|
create_enum "content_status", ["created", "draft", "published", "archived"]
|
@@ -117,6 +120,13 @@ ActiveRecord::Schema.define(version: version) do
|
|
117
120
|
t.datetime "updated_at", null: false
|
118
121
|
end
|
119
122
|
|
123
|
+
create_table "users", schema: "internal", force: :cascade do |t|
|
124
|
+
t.string "email"
|
125
|
+
t.datetime "created_at", null: false
|
126
|
+
t.datetime "updated_at", null: false
|
127
|
+
t.index ["email"], name: "index_internal_users_on_email", unique: true
|
128
|
+
end
|
129
|
+
|
120
130
|
create_table "activities", force: :cascade do |t|
|
121
131
|
t.integer "author_id"
|
122
132
|
t.string "title"
|
data/spec/spec_helper.rb
CHANGED
@@ -22,7 +22,7 @@ cleaner = ->() do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
load File.join('schema.rb')
|
25
|
-
Dir.glob(File.join('spec', '{models,factories,mocks}', '*.rb')) do |file|
|
25
|
+
Dir.glob(File.join('spec', '{models,factories,mocks}', '**', '*.rb')) do |file|
|
26
26
|
require file[5..-4]
|
27
27
|
end
|
28
28
|
|
@@ -39,6 +39,8 @@ RSpec.configure do |config|
|
|
39
39
|
|
40
40
|
# Handles acton before rspec initialize
|
41
41
|
config.before(:suite) do
|
42
|
+
Torque::PostgreSQL.config.schemas.whitelist << 'internal'
|
43
|
+
ActiveSupport::Deprecation.silenced = true
|
42
44
|
DatabaseCleaner.clean_with(:truncation)
|
43
45
|
end
|
44
46
|
|
@@ -393,4 +393,51 @@ RSpec.describe 'BelongsToMany' do
|
|
393
393
|
end
|
394
394
|
end
|
395
395
|
end
|
396
|
+
|
397
|
+
context 'using uuid' do
|
398
|
+
let(:connection) { ActiveRecord::Base.connection }
|
399
|
+
let(:game) { Class.new(ActiveRecord::Base) }
|
400
|
+
let(:player) { Class.new(ActiveRecord::Base) }
|
401
|
+
let(:other) { player.create }
|
402
|
+
|
403
|
+
# TODO: Set as a shred example
|
404
|
+
before do
|
405
|
+
connection.create_table(:players, id: :uuid) { |t| t.string :name }
|
406
|
+
connection.create_table(:games, id: :uuid) { |t| t.uuid :player_ids, array: true }
|
407
|
+
|
408
|
+
game.table_name = 'games'
|
409
|
+
player.table_name = 'players'
|
410
|
+
game.belongs_to_many :players, anonymous_class: player,
|
411
|
+
inverse_of: false, foreign_key: :player_ids
|
412
|
+
end
|
413
|
+
|
414
|
+
subject { game.create }
|
415
|
+
|
416
|
+
it 'loads associated records' do
|
417
|
+
subject.update(player_ids: [other.id])
|
418
|
+
expect(subject.players.to_sql).to be_eql(<<-SQL.squish)
|
419
|
+
SELECT "players".* FROM "players" WHERE "players"."id" IN ('#{other.id}')
|
420
|
+
SQL
|
421
|
+
|
422
|
+
expect(subject.players.load).to be_a(ActiveRecord::Associations::CollectionProxy)
|
423
|
+
expect(subject.players.to_a).to be_eql([other])
|
424
|
+
end
|
425
|
+
|
426
|
+
it 'can preload records' do
|
427
|
+
records = 5.times.map { player.create }
|
428
|
+
subject.players.concat(records)
|
429
|
+
|
430
|
+
entries = game.all.includes(:players).load
|
431
|
+
|
432
|
+
expect(entries.size).to be_eql(1)
|
433
|
+
expect(entries.first.players).to be_loaded
|
434
|
+
expect(entries.first.players.size).to be_eql(5)
|
435
|
+
end
|
436
|
+
|
437
|
+
it 'can joins records' do
|
438
|
+
query = game.all.joins(:players)
|
439
|
+
expect(query.to_sql).to match(/INNER JOIN "players"/)
|
440
|
+
expect { query.load }.not_to raise_error
|
441
|
+
end
|
442
|
+
end
|
396
443
|
end
|
data/spec/tests/enum_set_spec.rb
CHANGED
@@ -43,18 +43,19 @@ RSpec.describe 'Enum' do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
context 'on schema' do
|
46
|
+
let(:dump_result) do
|
47
|
+
ActiveRecord::SchemaDumper.dump(connection, (dump_result = StringIO.new))
|
48
|
+
dump_result.string
|
49
|
+
end
|
50
|
+
|
46
51
|
it 'can be used on tables' do
|
47
|
-
dump_io = StringIO.new
|
48
52
|
checker = /t\.enum +"conflicts", +array: true, +enum_type: "conflicts"/
|
49
|
-
|
50
|
-
expect(dump_io.string).to match checker
|
53
|
+
expect(dump_result).to match checker
|
51
54
|
end
|
52
55
|
|
53
56
|
xit 'can have a default value as an array of symbols' do
|
54
|
-
dump_io = StringIO.new
|
55
57
|
checker = /t\.enum +"types", +default: \[:A, :B\], +array: true, +enum_type: "types"/
|
56
|
-
|
57
|
-
expect(dump_io.string).to match checker
|
58
|
+
expect(dump_result).to match checker
|
58
59
|
end
|
59
60
|
end
|
60
61
|
|
data/spec/tests/has_many_spec.rb
CHANGED
@@ -411,4 +411,48 @@ RSpec.describe 'HasMany' do
|
|
411
411
|
expect { query.load }.not_to raise_error
|
412
412
|
end
|
413
413
|
end
|
414
|
+
|
415
|
+
context 'using uuid' do
|
416
|
+
let(:connection) { ActiveRecord::Base.connection }
|
417
|
+
let(:game) { Class.new(ActiveRecord::Base) }
|
418
|
+
let(:player) { Class.new(ActiveRecord::Base) }
|
419
|
+
|
420
|
+
# TODO: Set as a shred example
|
421
|
+
before do
|
422
|
+
connection.create_table(:players, id: :uuid) { |t| t.string :name }
|
423
|
+
connection.create_table(:games, id: :uuid) { |t| t.uuid :player_ids, array: true }
|
424
|
+
|
425
|
+
game.table_name = 'games'
|
426
|
+
player.table_name = 'players'
|
427
|
+
player.has_many :games, array: true, anonymous_class: game,
|
428
|
+
inverse_of: false, foreign_key: :player_ids
|
429
|
+
end
|
430
|
+
|
431
|
+
subject { player.create }
|
432
|
+
|
433
|
+
it 'loads associated records' do
|
434
|
+
expect(subject.games.to_sql).to match(Regexp.new(<<-SQL.squish))
|
435
|
+
SELECT "games"\\.\\* FROM "games"
|
436
|
+
WHERE \\(?"games"\\."player_ids" && ARRAY\\['#{subject.id}'\\]::uuid\\[\\]\\)?
|
437
|
+
SQL
|
438
|
+
|
439
|
+
expect(subject.games.load).to be_a(ActiveRecord::Associations::CollectionProxy)
|
440
|
+
expect(subject.games.to_a).to be_eql([])
|
441
|
+
end
|
442
|
+
|
443
|
+
it 'can preload records' do
|
444
|
+
5.times { game.create(player_ids: [subject.id]) }
|
445
|
+
entries = player.all.includes(:games).load
|
446
|
+
|
447
|
+
expect(entries.size).to be_eql(1)
|
448
|
+
expect(entries.first.games).to be_loaded
|
449
|
+
expect(entries.first.games.size).to be_eql(5)
|
450
|
+
end
|
451
|
+
|
452
|
+
it 'can joins records' do
|
453
|
+
query = player.all.joins(:games)
|
454
|
+
expect(query.to_sql).to match(/INNER JOIN "games"/)
|
455
|
+
expect { query.load }.not_to raise_error
|
456
|
+
end
|
457
|
+
end
|
414
458
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Schema' do
|
4
|
+
let(:connection) { ActiveRecord::Base.connection }
|
5
|
+
|
6
|
+
before do
|
7
|
+
connection.instance_variable_set(:@schmeas_blacklist, nil)
|
8
|
+
connection.instance_variable_set(:@schmeas_whitelist, nil)
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'on migration' do
|
12
|
+
it 'can check for existance' do
|
13
|
+
expect(connection.schema_exists?(:information_schema)).to be_falsey
|
14
|
+
expect(connection.schema_exists?(:information_schema, filtered: false)).to be_truthy
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'can be created' do
|
18
|
+
expect(connection.schema_exists?(:legacy, filtered: false)).to be_falsey
|
19
|
+
connection.create_schema(:legacy)
|
20
|
+
expect(connection.schema_exists?(:legacy, filtered: false)).to be_truthy
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'can be deleted' do
|
24
|
+
expect(connection.schema_exists?(:legacy, filtered: false)).to be_falsey
|
25
|
+
|
26
|
+
connection.create_schema(:legacy)
|
27
|
+
expect(connection.schema_exists?(:legacy, filtered: false)).to be_truthy
|
28
|
+
|
29
|
+
connection.drop_schema(:legacy)
|
30
|
+
expect(connection.schema_exists?(:legacy, filtered: false)).to be_falsey
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'works with whitelist' do
|
34
|
+
expect(connection.schema_exists?(:legacy)).to be_falsey
|
35
|
+
connection.create_schema(:legacy)
|
36
|
+
|
37
|
+
expect(connection.schema_exists?(:legacy)).to be_falsey
|
38
|
+
expect(connection.schema_exists?(:legacy, filtered: false)).to be_truthy
|
39
|
+
|
40
|
+
connection.schemas_whitelist.push('legacy')
|
41
|
+
expect(connection.schema_exists?(:legacy)).to be_truthy
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'on schema' do
|
46
|
+
let(:dump_result) do
|
47
|
+
ActiveRecord::SchemaDumper.dump(connection, (dump_result = StringIO.new))
|
48
|
+
dump_result.string
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'does not add when there is no extra schemas' do
|
52
|
+
connection.drop_schema(:internal, force: :cascade)
|
53
|
+
expect(dump_result).not_to match /Custom schemas defined in this database/
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'does not include tables from blacklisted schemas' do
|
57
|
+
connection.schemas_blacklist.push('internal')
|
58
|
+
expect(dump_result).not_to match /create_table \"users\",.*schema: +"internal"/
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with internal schema whitelisted' do
|
62
|
+
before { connection.schemas_whitelist.push('internal') }
|
63
|
+
|
64
|
+
it 'dumps the schemas' do
|
65
|
+
expect(dump_result).to match /create_schema \"internal\"/
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'shows the internal users table in the connection tables list' do
|
69
|
+
expect(connection.tables).to include('internal.users')
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'dumps tables on whitelisted schemas' do
|
73
|
+
expect(dump_result).to match /create_table \"users\",.*schema: +"internal"/
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'on relation' do
|
79
|
+
let(:model) { Internal::User }
|
80
|
+
|
81
|
+
it 'adds the schema to the query' do
|
82
|
+
expect(model.all.to_sql).to match(/FROM "internal"."users"/)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'can load the schema from the module' do
|
86
|
+
allow(Internal).to receive(:schema).and_return('internal')
|
87
|
+
allow(model).to receive(:schema).and_return(nil)
|
88
|
+
|
89
|
+
expect(model.all.to_sql).to match(/FROM "internal"."users"/)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -73,37 +73,33 @@ RSpec.describe 'TableInheritance' do
|
|
73
73
|
end
|
74
74
|
|
75
75
|
context 'on schema' do
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
let(:dump_result) do
|
77
|
+
ActiveRecord::SchemaDumper.dump(connection, (dump_result = StringIO.new))
|
78
|
+
dump_result.string
|
79
|
+
end
|
79
80
|
|
81
|
+
it 'dumps single inheritance with body' do
|
80
82
|
parts = '"activity_books"'
|
81
83
|
parts << ', id: false'
|
82
84
|
parts << ', force: :cascade'
|
83
|
-
parts << ', inherits:
|
84
|
-
expect(
|
85
|
+
parts << ', inherits: "activities"'
|
86
|
+
expect(dump_result).to match(/create_table #{parts} do /)
|
85
87
|
end
|
86
88
|
|
87
89
|
it 'dumps single inheritance without body' do
|
88
|
-
dump_io = StringIO.new
|
89
|
-
ActiveRecord::SchemaDumper.dump(connection, dump_io)
|
90
|
-
|
91
90
|
parts = '"activity_post_samples"'
|
92
91
|
parts << ', id: false'
|
93
92
|
parts << ', force: :cascade'
|
94
|
-
parts << ', inherits:
|
95
|
-
expect(
|
93
|
+
parts << ', inherits: "activity_posts"'
|
94
|
+
expect(dump_result).to match(/create_table #{parts}(?! do \|t\|)/)
|
96
95
|
end
|
97
96
|
|
98
97
|
it 'dumps multiple inheritance' do
|
99
|
-
dump_io = StringIO.new
|
100
|
-
ActiveRecord::SchemaDumper.dump(connection, dump_io)
|
101
|
-
|
102
98
|
parts = '"activity_posts"'
|
103
99
|
parts << ', id: false'
|
104
100
|
parts << ', force: :cascade'
|
105
|
-
parts << ', inherits: (\[
|
106
|
-
expect(
|
101
|
+
parts << ', inherits: (\["images", "activities"\]|\["activities", "images"\])'
|
102
|
+
expect(dump_result).to match(/create_table #{parts}/)
|
107
103
|
end
|
108
104
|
end
|
109
105
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: torque-postgresql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Carlos Silva
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-12-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -155,7 +155,7 @@ dependencies:
|
|
155
155
|
description: Add support to complex resources of PostgreSQL, like data types, array
|
156
156
|
associations, and auxiliary statements (CTE)
|
157
157
|
email:
|
158
|
-
-
|
158
|
+
- me@carlosfsilva.com
|
159
159
|
executables: []
|
160
160
|
extensions: []
|
161
161
|
extra_rdoc_files: []
|
@@ -232,6 +232,7 @@ files:
|
|
232
232
|
- lib/torque/postgresql/relation/inheritance.rb
|
233
233
|
- lib/torque/postgresql/relation/merger.rb
|
234
234
|
- lib/torque/postgresql/schema_cache.rb
|
235
|
+
- lib/torque/postgresql/table_name.rb
|
235
236
|
- lib/torque/postgresql/version.rb
|
236
237
|
- spec/en.yml
|
237
238
|
- spec/factories/authors.rb
|
@@ -254,6 +255,7 @@ files:
|
|
254
255
|
- spec/models/course.rb
|
255
256
|
- spec/models/geometry.rb
|
256
257
|
- spec/models/guest_comment.rb
|
258
|
+
- spec/models/internal/user.rb
|
257
259
|
- spec/models/item.rb
|
258
260
|
- spec/models/post.rb
|
259
261
|
- spec/models/question.rb
|
@@ -280,13 +282,18 @@ files:
|
|
280
282
|
- spec/tests/period_spec.rb
|
281
283
|
- spec/tests/quoting_spec.rb
|
282
284
|
- spec/tests/relation_spec.rb
|
285
|
+
- spec/tests/schema_spec.rb
|
283
286
|
- spec/tests/table_inheritance_spec.rb
|
284
287
|
homepage: https://github.com/crashtech/torque-postgresql
|
285
288
|
licenses:
|
286
289
|
- MIT
|
287
|
-
metadata:
|
290
|
+
metadata:
|
291
|
+
source_code_uri: https://github.com/crashtech/torque-postgresql
|
292
|
+
bug_tracker_uri: https://github.com/crashtech/torque-postgresql/issues
|
288
293
|
post_install_message:
|
289
|
-
rdoc_options:
|
294
|
+
rdoc_options:
|
295
|
+
- "--title"
|
296
|
+
- Torque PostgreSQL
|
290
297
|
require_paths:
|
291
298
|
- lib
|
292
299
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -326,6 +333,7 @@ test_files:
|
|
326
333
|
- spec/models/course.rb
|
327
334
|
- spec/models/geometry.rb
|
328
335
|
- spec/models/guest_comment.rb
|
336
|
+
- spec/models/internal/user.rb
|
329
337
|
- spec/models/item.rb
|
330
338
|
- spec/models/post.rb
|
331
339
|
- spec/models/question.rb
|
@@ -352,4 +360,5 @@ test_files:
|
|
352
360
|
- spec/tests/period_spec.rb
|
353
361
|
- spec/tests/quoting_spec.rb
|
354
362
|
- spec/tests/relation_spec.rb
|
363
|
+
- spec/tests/schema_spec.rb
|
355
364
|
- spec/tests/table_inheritance_spec.rb
|