torque-postgresql 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4fac19ef8680f477df0f79502331ddda06266658a54c8128321d68145a9f18b
4
- data.tar.gz: 0e93ec49f80d40ec9ce9b7fcc5ef0eff65a882cad79a63e5429657ad9c66691b
3
+ metadata.gz: 25fb390d67c43461cf2c03096990149d2dbcae04072e96d69896d60f8e368863
4
+ data.tar.gz: e2910f192602059a71dbf3cfca2cc412e2d34eb1faacc51620058ce408c1be9f
5
5
  SHA512:
6
- metadata.gz: 4a9abd492b544296c29e0949e1ad0ad4852a1ed0c48379551e5ffbbaad7018579cad994ac2a841231603bdddd8da4b448d7771a9086312289f818124d842d6a6
7
- data.tar.gz: 8019255d20eff471ed177078e1bd6042a0a9b95752d32578bdb8ce86afdf9cffbefa8ed181caab0143f0dbbf80cfc39c99fd005cfc2b28089687b26a635268ef
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 child.relname AS table_name,
128
- array_agg(parent.relname) AS inheritances
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
- GROUP BY child.relname, pg_inherits.inhrelid
133
- ORDER BY pg_inherits.inhrelid
163
+ ORDER BY inhrelid
134
164
  SQL
135
165
 
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
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.sort - @connection.views
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!(&:to_sym)
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
- # Scenic integration
60
- views(stream) if defined?(::Scenic)
85
+ # Dump user defined schemas
86
+ def user_defined_schemas(stream)
87
+ return if (list = (@connection.user_defined_schemas - ['public'])).empty?
61
88
 
62
- # FX integration
63
- functions(stream) if defined?(::Fx::SchemaDumper::Function)
64
- triggers(stream) if defined?(::Fx::SchemaDumper::Trigger)
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
@@ -6,7 +6,7 @@ module Torque
6
6
  module Preloader
7
7
  module LoaderQuery
8
8
  def foreign_column
9
- @foreign_column ||= scope.columns_hash[association_key_name]
9
+ @foreign_column ||= scope.columns_hash[association_key_name.to_s]
10
10
  end
11
11
 
12
12
  def load_records_for_keys(keys, &block)
@@ -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
- # 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
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
- 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?
49
42
 
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
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 name.
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 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)
21
21
  end
22
22
 
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]]
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[join_primary_key.to_s]
49
- # active_record and foreign_key are associated with the source Class
50
- source_type = active_record.columns_hash[join_foreign_key.to_s]
51
-
52
- # If both are attributes but the left side is not an array, and the
53
- # right side is, use the ANY operation
54
- any_operation = arel_array_to_any(klass_attr, source_attr, klass_type, source_type)
55
- return klass_attr.eq(any_operation) if any_operation
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
- # Decide if should apply a cast to ensure same type comparision
61
- should_cast = klass_type.type.eql?(:integer) && source_type.type.eql?(:integer)
62
- should_cast &= !klass_type.sql_type.eql?(source_type.sql_type)
63
- should_cast |= !(klass_attr.is_a?(AREL_ATTR) && source_attr.is_a?(AREL_ATTR))
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '3.0.0'
5
+ VERSION = '3.1.0'
6
6
  end
7
7
  end
@@ -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)
@@ -0,0 +1,5 @@
1
+ module Internal
2
+ class User < ActiveRecord::Base
3
+ self.schema = 'internal'
4
+ end
5
+ end
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 = 2
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
@@ -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
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
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
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
57
- expect(dump_io.string).to match checker
58
+ expect(dump_result).to match checker
58
59
  end
59
60
  end
60
61
 
@@ -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
- it 'dumps single inheritance with body' do
77
- dump_io = StringIO.new
78
- ActiveRecord::SchemaDumper.dump(connection, dump_io)
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: :activities'
84
- expect(dump_io.string).to match(/create_table #{parts} do /)
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: :activity_posts'
95
- expect(dump_io.string).to match(/create_table #{parts}(?! do \|t\|)/)
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: (\[:images, :activities\]|\[:activities, :images\])'
106
- expect(dump_io.string).to match(/create_table #{parts}/)
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.0.0
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-04-04 00:00:00.000000000 Z
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
- - carlinhus.fsilva@gmail.com
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