dynamic_migrations 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +13 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +71 -0
  6. data/lib/dynamic_migrations/expected_boolean_error.rb +4 -0
  7. data/lib/dynamic_migrations/expected_integer_error.rb +4 -0
  8. data/lib/dynamic_migrations/expected_string_error.rb +4 -0
  9. data/lib/dynamic_migrations/expected_symbol_error.rb +4 -0
  10. data/lib/dynamic_migrations/invalid_source_error.rb +7 -0
  11. data/lib/dynamic_migrations/module_included_into_unexpected_target_error.rb +4 -0
  12. data/lib/dynamic_migrations/postgres/connections.rb +42 -0
  13. data/lib/dynamic_migrations/postgres/data_types.rb +273 -0
  14. data/lib/dynamic_migrations/postgres/server/database/configured_schemas.rb +55 -0
  15. data/lib/dynamic_migrations/postgres/server/database/connection.rb +39 -0
  16. data/lib/dynamic_migrations/postgres/server/database/differences.rb +292 -0
  17. data/lib/dynamic_migrations/postgres/server/database/keys_and_unique_constraints_loader.rb +149 -0
  18. data/lib/dynamic_migrations/postgres/server/database/loaded_schemas.rb +55 -0
  19. data/lib/dynamic_migrations/postgres/server/database/loaded_schemas_builder.rb +86 -0
  20. data/lib/dynamic_migrations/postgres/server/database/schema/table/column.rb +84 -0
  21. data/lib/dynamic_migrations/postgres/server/database/schema/table/columns.rb +58 -0
  22. data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraint.rb +132 -0
  23. data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraints.rb +62 -0
  24. data/lib/dynamic_migrations/postgres/server/database/schema/table/index.rb +144 -0
  25. data/lib/dynamic_migrations/postgres/server/database/schema/table/indexes.rb +63 -0
  26. data/lib/dynamic_migrations/postgres/server/database/schema/table/primary_key.rb +83 -0
  27. data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraint.rb +101 -0
  28. data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraints.rb +59 -0
  29. data/lib/dynamic_migrations/postgres/server/database/schema/table/validation.rb +90 -0
  30. data/lib/dynamic_migrations/postgres/server/database/schema/table/validations.rb +59 -0
  31. data/lib/dynamic_migrations/postgres/server/database/schema/table.rb +73 -0
  32. data/lib/dynamic_migrations/postgres/server/database/schema.rb +72 -0
  33. data/lib/dynamic_migrations/postgres/server/database/source.rb +37 -0
  34. data/lib/dynamic_migrations/postgres/server/database/structure_loader.rb +242 -0
  35. data/lib/dynamic_migrations/postgres/server/database/validations_loader.rb +81 -0
  36. data/lib/dynamic_migrations/postgres/server/database.rb +54 -0
  37. data/lib/dynamic_migrations/postgres/server.rb +33 -0
  38. data/lib/dynamic_migrations/postgres.rb +8 -0
  39. data/lib/dynamic_migrations/version.rb +5 -0
  40. data/lib/dynamic_migrations.rb +44 -0
  41. metadata +113 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ class Schema
8
+ class Table < Source
9
+ # This module has all the tables methods for working with unique_constraints
10
+ module UniqueConstraints
11
+ class UniqueConstraintDoesNotExistError < StandardError
12
+ end
13
+
14
+ class UniqueConstraintAlreadyExistsError < StandardError
15
+ end
16
+
17
+ # returns the unique_constraint object for the provided unique_constraint name, and raises an
18
+ # error if the unique_constraint does not exist
19
+ def unique_constraint unique_constraint_name
20
+ raise ExpectedSymbolError, unique_constraint_name unless unique_constraint_name.is_a? Symbol
21
+ raise UniqueConstraintDoesNotExistError unless has_unique_constraint? unique_constraint_name
22
+ @unique_constraints[unique_constraint_name]
23
+ end
24
+
25
+ # returns true if this table has a unique_constraint with the provided name, otherwise false
26
+ def has_unique_constraint? unique_constraint_name
27
+ raise ExpectedSymbolError, unique_constraint_name unless unique_constraint_name.is_a? Symbol
28
+ @unique_constraints.key? unique_constraint_name
29
+ end
30
+
31
+ # returns an array of this tables unique_constraints
32
+ def unique_constraints
33
+ @unique_constraints.values
34
+ end
35
+
36
+ def unique_constraints_hash
37
+ @unique_constraints
38
+ end
39
+
40
+ # adds a new unique_constraint to this table, and returns it
41
+ def add_unique_constraint unique_constraint_name, column_names, **unique_constraint_options
42
+ if has_unique_constraint? unique_constraint_name
43
+ raise(UniqueConstraintAlreadyExistsError, "unique_constraint #{unique_constraint_name} already exists")
44
+ end
45
+ columns = column_names.map { |column_name| column column_name }
46
+ included_target = self
47
+ if included_target.is_a? Table
48
+ @unique_constraints[unique_constraint_name] = UniqueConstraint.new source, included_target, columns, unique_constraint_name, **unique_constraint_options
49
+ else
50
+ raise ModuleIncludedIntoUnexpectedTargetError, included_target
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ class Schema
8
+ class Table
9
+ # This class represents a postgres table validation
10
+ class Validation < Source
11
+ class ExpectedTableError < StandardError
12
+ end
13
+
14
+ class ExpectedArrayOfColumnsError < StandardError
15
+ end
16
+
17
+ class DuplicateColumnError < StandardError
18
+ end
19
+
20
+ attr_reader :table
21
+ attr_reader :validation_name
22
+ attr_reader :check_clause
23
+ attr_reader :deferrable
24
+ attr_reader :initially_deferred
25
+
26
+ # initialize a new object to represent a validation in a postgres table
27
+ def initialize source, table, columns, validation_name, check_clause, deferrable: false, initially_deferred: false
28
+ super source
29
+ raise ExpectedTableError, table unless table.is_a? Table
30
+ @table = table
31
+
32
+ # assert that the provided columns is an array
33
+ unless columns.is_a?(Array) && columns.count > 0
34
+ raise ExpectedArrayOfColumnsError
35
+ end
36
+
37
+ @columns = {}
38
+ columns.each do |column|
39
+ add_column column
40
+ end
41
+
42
+ raise ExpectedSymbolError, validation_name unless validation_name.is_a? Symbol
43
+ @validation_name = validation_name
44
+
45
+ raise ExpectedStringError, check_clause unless check_clause.is_a? String
46
+ @check_clause = check_clause
47
+
48
+ raise ExpectedBooleanError, deferrable unless [true, false].include?(deferrable)
49
+ @deferrable = deferrable
50
+
51
+ raise ExpectedBooleanError, initially_deferred unless [true, false].include?(initially_deferred)
52
+ @initially_deferred = initially_deferred
53
+ end
54
+
55
+ # return an array of this validations columns
56
+ def columns
57
+ @columns.values
58
+ end
59
+
60
+ def column_names
61
+ @columns.keys
62
+ end
63
+
64
+ private
65
+
66
+ # used internally to set the columns from this objects initialize method
67
+ def add_column column
68
+ # assert that the provided dsl name is an array of Columns
69
+ unless column.is_a? Column
70
+ raise ExpectedArrayOfColumnsError
71
+ end
72
+
73
+ # assert that the provided column exists within this validations table
74
+ unless @table.has_column? column.column_name
75
+ raise ExpectedArrayOfColumnsError, "One or more columns do not exist in this validations table"
76
+ end
77
+
78
+ if @columns.key? column.column_name
79
+ raise(DuplicateColumnError, "Column #{column.column_name} already exists")
80
+ end
81
+
82
+ @columns[column.column_name] = column
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ class Schema
8
+ class Table < Source
9
+ # This module has all the tables methods for working with validations
10
+ module Validations
11
+ class ValidationDoesNotExistError < StandardError
12
+ end
13
+
14
+ class ValidationAlreadyExistsError < StandardError
15
+ end
16
+
17
+ # returns the validation object for the provided validation name, and raises an
18
+ # error if the validation does not exist
19
+ def validation validation_name
20
+ raise ExpectedSymbolError, validation_name unless validation_name.is_a? Symbol
21
+ raise ValidationDoesNotExistError unless has_validation? validation_name
22
+ @validations[validation_name]
23
+ end
24
+
25
+ # returns true if this table has a validation with the provided name, otherwise false
26
+ def has_validation? validation_name
27
+ raise ExpectedSymbolError, validation_name unless validation_name.is_a? Symbol
28
+ @validations.key? validation_name
29
+ end
30
+
31
+ # returns an array of this tables validations
32
+ def validations
33
+ @validations.values
34
+ end
35
+
36
+ def validations_hash
37
+ @validations
38
+ end
39
+
40
+ # adds a new validation to this table, and returns it
41
+ def add_validation validation_name, column_names, check_clause, **validation_options
42
+ if has_validation? validation_name
43
+ raise(ValidationAlreadyExistsError, "Validation #{validation_name} already exists")
44
+ end
45
+ columns = column_names.map { |column_name| column column_name }
46
+ included_target = self
47
+ if included_target.is_a? Table
48
+ @validations[validation_name] = Validation.new source, included_target, columns, validation_name, check_clause, **validation_options
49
+ else
50
+ raise ModuleIncludedIntoUnexpectedTargetError, included_target
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ class Schema
8
+ # This class represents a postgres table.
9
+ class Table < Source
10
+ class ExpectedSchemaError < StandardError
11
+ end
12
+
13
+ class PrimaryKeyDoesNotExistError < StandardError
14
+ end
15
+
16
+ include Columns
17
+ include Validations
18
+ include Indexes
19
+ include ForeignKeyConstraints
20
+ include UniqueConstraints
21
+
22
+ attr_reader :schema
23
+ attr_reader :table_name
24
+ attr_reader :description
25
+
26
+ # initialize a new object to represent a postgres table
27
+ def initialize source, schema, table_name, description = nil
28
+ super source
29
+ raise ExpectedSchemaError, schema unless schema.is_a? Schema
30
+ raise ExpectedSymbolError, table_name unless table_name.is_a? Symbol
31
+ unless description.nil?
32
+ raise ExpectedStringError, description unless description.is_a? String
33
+ @description = description
34
+ end
35
+ @schema = schema
36
+ @table_name = table_name
37
+ @columns = {}
38
+ @validations = {}
39
+ @indexes = {}
40
+ @foreign_key_constraints = {}
41
+ @unique_constraints = {}
42
+ end
43
+
44
+ # returns true if this table has a description, otehrwise false
45
+ def has_description?
46
+ !@description.nil?
47
+ end
48
+
49
+ # add a primary key to this table
50
+ def add_primary_key primary_key_name, column_names, **primary_key_options
51
+ raise PrimaryKeyAlreadyExistsError if @primary_key
52
+ columns = column_names.map { |column_name| column column_name }
53
+ @primary_key = PrimaryKey.new source, self, columns, primary_key_name, **primary_key_options
54
+ end
55
+
56
+ # returns true if this table has a primary key, otherwise false
57
+ def has_primary_key?
58
+ !@primary_key.nil?
59
+ end
60
+
61
+ # returns a primary key if one exists, else raises an error
62
+ def primary_key
63
+ unless @primary_key
64
+ raise PrimaryKeyDoesNotExistError
65
+ end
66
+ @primary_key
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ # This class represents a postgres schema. A schema is essentially a namespace within
8
+ # a postgres database. Each schema is a collection of tables, functions and other
9
+ # database objects.
10
+ class Schema < Source
11
+ class ExpectedDatabaseError < StandardError
12
+ end
13
+
14
+ class TableAlreadyExistsError < StandardError
15
+ end
16
+
17
+ class TableDoesNotExistError < StandardError
18
+ end
19
+
20
+ attr_reader :database
21
+ attr_reader :schema_name
22
+
23
+ # initialize a new object to represent a postgres schema
24
+ def initialize source, database, schema_name
25
+ super source
26
+ raise ExpectedDatabaseError, database unless database.is_a? Database
27
+ raise ExpectedSymbolError, schema_name unless schema_name.is_a? Symbol
28
+ @database = database
29
+ @schema_name = schema_name
30
+ @tables = {}
31
+ end
32
+
33
+ # create and add a new table from a provided table name
34
+ def add_table table_name, description = nil
35
+ raise ExpectedSymbolError, table_name unless table_name.is_a? Symbol
36
+ if has_table? table_name
37
+ raise(TableAlreadyExistsError, "Table #{table_name} already exists")
38
+ end
39
+ included_target = self
40
+ if included_target.is_a? Schema
41
+ @tables[table_name] = Table.new source, included_target, table_name, description
42
+ else
43
+ raise ModuleIncludedIntoUnexpectedTargetError, included_target
44
+ end
45
+ end
46
+
47
+ # return a table by its name, raises an error if the table does not exist
48
+ def table table_name
49
+ raise ExpectedSymbolError, table_name unless table_name.is_a? Symbol
50
+ raise TableDoesNotExistError unless has_table? table_name
51
+ @tables[table_name]
52
+ end
53
+
54
+ # returns true/false representing if a table with the provided name exists
55
+ def has_table? table_name
56
+ raise ExpectedSymbolError, table_name unless table_name.is_a? Symbol
57
+ @tables.key? table_name
58
+ end
59
+
60
+ # returns an array of all tables in the schema
61
+ def tables
62
+ @tables.values
63
+ end
64
+
65
+ def tables_hash
66
+ @tables
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ class Source
8
+ attr_reader :source
9
+
10
+ # initialize a new object to represent a postgres schema
11
+ def initialize source
12
+ unless source == :configuration || source == :database
13
+ raise InvalidSourceError, source
14
+ end
15
+ @source = source
16
+ end
17
+
18
+ def from_configuration?
19
+ @source == :configuration
20
+ end
21
+
22
+ def from_database?
23
+ @source == :database
24
+ end
25
+
26
+ def assert_is_a_symbol! value
27
+ if value.is_a? Symbol
28
+ true
29
+ else
30
+ raise ExpectedSymbolError, "expected Symbol but got #{value}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicMigrations
4
+ module Postgres
5
+ class Server
6
+ class Database
7
+ module StructureLoader
8
+ def create_database_structure_cache
9
+ connection.exec(<<~SQL)
10
+ CREATE MATERIALIZED VIEW public.dynamic_migrations_structure_cache as
11
+ SELECT
12
+ -- Name of the schema containing the table
13
+ schemata.schema_name,
14
+ -- Name of the table
15
+ tables.table_name,
16
+ -- The comment which has been added to the table (if any)
17
+ table_description.description as table_description,
18
+ -- Name of the column
19
+ columns.column_name,
20
+ -- The comment which has been added to the column (if any)
21
+ column_description.description as column_description,
22
+ -- Default expression of the column
23
+ columns.column_default,
24
+ -- YES if the column is possibly nullable, NO if
25
+ -- it is known not nullable
26
+ columns.is_nullable,
27
+ -- Data type of the column, if it is a built-in type,
28
+ -- or ARRAY if it is some array (in that case, see the
29
+ -- view element_types), else USER-DEFINED (in that case,
30
+ -- the type is identified in udt_name and associated
31
+ -- columns). If the column is based on a domain, this
32
+ -- column refers to the type underlying the domain (and
33
+ -- the domain is identified in domain_name and associated
34
+ -- columns).
35
+ columns.data_type,
36
+ -- If data_type identifies a character or bit string type,
37
+ -- the declared maximum length; null for all other data
38
+ -- types or if no maximum length was declared.
39
+ columns.character_maximum_length,
40
+ -- If data_type identifies a character type, the maximum
41
+ -- possible length in octets (bytes) of a datum; null for
42
+ -- all other data types. The maximum octet length depends
43
+ -- on the declared character maximum length (see above)
44
+ -- and the server encoding.
45
+ columns.character_octet_length,
46
+ -- If data_type identifies a numeric type, this column
47
+ -- contains the (declared or implicit) precision of the type
48
+ -- for this column. The precision indicates the number of
49
+ -- significant digits. It can be expressed in decimal (base 10)
50
+ -- or binary (base 2) terms, as specified in the column
51
+ -- numeric_precision_radix. For all other data types, this
52
+ -- column is null.
53
+ columns.numeric_precision,
54
+ -- If data_type identifies a numeric type, this column indicates
55
+ -- in which base the values in the columns numeric_precision and
56
+ -- numeric_scale are expressed. The value is either 2 or 10. For
57
+ -- all other data types, this column is null.
58
+ columns.numeric_precision_radix,
59
+ -- If data_type identifies an exact numeric type, this column
60
+ -- contains the (declared or implicit) scale of the type for this
61
+ -- column. The scale indicates the number of significant digits to
62
+ -- the right of the decimal point. It can be expressed in decimal
63
+ -- (base 10) or binary (base 2) terms, as specified in the column
64
+ -- numeric_precision_radix. For all other data types, this column
65
+ -- is null.
66
+ columns.numeric_scale,
67
+ -- If data_type identifies a date, time, timestamp, or interval
68
+ -- type, this column contains the (declared or implicit) fractional
69
+ -- seconds precision of the type for this column, that is, the
70
+ -- number of decimal digits maintained following the decimal point
71
+ -- in the seconds value. For all other data types, this column is
72
+ -- null.
73
+ columns.datetime_precision,
74
+ -- If data_type identifies an interval type, this column contains
75
+ -- the specification which fields the intervals include for this
76
+ -- column, e.g., YEAR TO MONTH, DAY TO SECOND, etc. If no field
77
+ -- restrictions were specified (that is, the interval accepts all
78
+ -- fields), and for all other data types, this field is null.
79
+ columns.interval_type,
80
+ -- Name of the schema that the column data type (the underlying
81
+ --type of the domain, if applicable) is defined in
82
+ columns.udt_schema,
83
+ -- Name of the column data type (the underlying type of the domain,
84
+ -- if applicable)
85
+ columns.udt_name,
86
+ -- YES if the column is updatable, NO if not (Columns in base tables
87
+ -- are always updatable, columns in views not necessarily)
88
+ columns.is_updatable
89
+ FROM information_schema.schemata
90
+ LEFT JOIN information_schema.tables ON schemata.schema_name = tables.table_schema AND left(tables.table_name, 3) != 'pg_'
91
+ LEFT JOIN information_schema.columns ON tables.table_name = columns.table_name
92
+ -- required for the column and table description/comment joins
93
+ LEFT JOIN pg_catalog.pg_statio_all_tables ON pg_statio_all_tables.schemaname = schemata.schema_name AND pg_statio_all_tables.relname = tables.table_name
94
+ -- required for the table description/comment
95
+ LEFT JOIN pg_catalog.pg_description table_description ON table_description.objoid = pg_statio_all_tables.relid AND table_description.objsubid = 0
96
+ -- required for the column description/comment
97
+ LEFT JOIN pg_catalog.pg_description column_description ON column_description.objoid = pg_statio_all_tables.relid AND column_description.objsubid = columns.ordinal_position
98
+ WHERE schemata.schema_name != 'information_schema'
99
+ AND schemata.schema_name != 'postgis'
100
+ AND left(schemata.schema_name, 3) != 'pg_'
101
+ -- order by the schema and table names alphabetically, then by the column position in the table
102
+ ORDER BY schemata.schema_name, tables.table_schema, columns.ordinal_position
103
+ SQL
104
+ connection.exec(<<~SQL)
105
+ COMMENT ON MATERIALIZED VIEW public.dynamic_migrations_structure_cache IS 'A cached representation of the database structure. This is used by the dynamic migrations library and is created automatically and updated automatically after migrations have run.';
106
+ SQL
107
+ end
108
+
109
+ # fetch all columns from the database and build and return a
110
+ # useful hash representing the structure of your database
111
+ def fetch_structure
112
+ begin
113
+ rows = connection.exec_params(<<~SQL)
114
+ SELECT * FROM public.dynamic_migrations_structure_cache
115
+ SQL
116
+ rescue PG::UndefinedTable
117
+ create_database_structure_cache
118
+ rows = connection.exec_params(<<~SQL)
119
+ SELECT * FROM public.dynamic_migrations_structure_cache
120
+ SQL
121
+ end
122
+
123
+ schemas = {}
124
+ rows.each do |row|
125
+ schema_name = row["schema_name"].to_sym
126
+ schema = schemas[schema_name] ||= {
127
+ tables: {}
128
+ }
129
+
130
+ unless row["table_name"].nil?
131
+ table_name = row["table_name"].to_sym
132
+ table = schema[:tables][table_name] ||= {
133
+ description: row["table_description"],
134
+ columns: {}
135
+ }
136
+
137
+ unless row["column_name"].nil?
138
+ column_name = row["column_name"].to_sym
139
+ column = table[:columns][column_name] ||= {}
140
+
141
+ column[:data_type] = row["data_type"].to_sym
142
+ column[:null] = row["is_nullable"] == "YES"
143
+ column[:default] = row["column_default"]
144
+ column[:description] = row["column_description"]
145
+ column[:character_maximum_length] = row["character_maximum_length"].nil? ? nil : row["character_maximum_length"].to_i
146
+ column[:character_octet_length] = row["character_octet_length"].nil? ? nil : row["character_octet_length"].to_i
147
+ column[:numeric_precision] = row["numeric_precision"].nil? ? nil : row["numeric_precision"].to_i
148
+ column[:numeric_precision_radix] = row["numeric_precision_radix"].nil? ? nil : row["numeric_precision_radix"].to_i
149
+ column[:numeric_scale] = row["numeric_scale"].nil? ? nil : row["numeric_scale"].to_i
150
+ column[:datetime_precision] = row["datetime_precision"].nil? ? nil : row["datetime_precision"].to_i
151
+ column[:interval_type] = row["interval_type"].nil? ? nil : row["interval_type"].to_sym
152
+ column[:udt_schema] = row["udt_schema"].to_sym
153
+ column[:udt_name] = row["udt_name"].to_sym
154
+ column[:updatable] = row["is_updatable"] == "YES"
155
+ end
156
+ end
157
+ end
158
+ schemas
159
+ end
160
+
161
+ # recursively process the database and build all the schemas,
162
+ # tables and columns
163
+ def recursively_build_schemas_from_database
164
+ fetch_structure.each do |schema_name, schema_definition|
165
+ schema = add_loaded_schema schema_name
166
+
167
+ schema_definition[:tables].each do |table_name, table_definition|
168
+ table = schema.add_table table_name, table_definition[:description]
169
+
170
+ table_definition[:columns].each do |column_name, column_definition|
171
+ # we only need these for arrays and user-defined types
172
+ # (user-defined is usually ENUMS)
173
+ if [:ARRAY, :"USER-DEFINED"].include? column_definition[:data_type]
174
+ udt_schema = column_definition[:udt_schema]
175
+ udt_name = column_definition[:udt_name]
176
+ else
177
+ udt_schema = nil
178
+ udt_name = nil
179
+ end
180
+
181
+ table.add_column column_name, column_definition[:data_type],
182
+ null: column_definition[:null],
183
+ default: column_definition[:default],
184
+ description: column_definition[:description],
185
+ character_maximum_length: column_definition[:character_maximum_length],
186
+ character_octet_length: column_definition[:character_octet_length],
187
+ numeric_precision: column_definition[:numeric_precision],
188
+ numeric_precision_radix: column_definition[:numeric_precision_radix],
189
+ numeric_scale: column_definition[:numeric_scale],
190
+ datetime_precision: column_definition[:datetime_precision],
191
+ udt_schema: udt_schema,
192
+ udt_name: udt_name,
193
+ updatable: column_definition[:updatable]
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ # returns a list of the schema names in this database
200
+ def fetch_schema_names
201
+ rows = connection.exec(<<-SQL)
202
+ SELECT schema_name
203
+ FROM information_schema.schemata;
204
+ SQL
205
+ schema_names = rows.map { |row| row["schema_name"] }
206
+ schema_names.reject! { |schema_name| schema_name == "information_schema" }
207
+ schema_names.reject! { |schema_name| schema_name == "public" }
208
+ schema_names.reject! { |schema_name| schema_name.start_with? "pg_" }
209
+ schema_names.sort
210
+ end
211
+
212
+ # returns a list of the table names in the provided schema
213
+ def fetch_table_names schema_name
214
+ rows = connection.exec_params(<<-SQL, [schema_name.to_s])
215
+ SELECT table_name FROM information_schema.tables
216
+ WHERE table_schema = $1
217
+ SQL
218
+ table_names = rows.map { |row| row["table_name"] }
219
+ table_names.reject! { |table_name| table_name.start_with? "pg_" }
220
+ table_names.sort
221
+ end
222
+
223
+ # returns a list of columns definitions for the provided table
224
+ def fetch_columns schema_name, table_name
225
+ rows = connection.exec_params(<<-SQL, [schema_name.to_s, table_name.to_s])
226
+ SELECT column_name, is_nullable, data_type, character_octet_length, column_default, numeric_precision, numeric_precision_radix, numeric_scale, udt_schema, udt_name
227
+ FROM information_schema.columns
228
+ WHERE table_schema = $1
229
+ AND table_name = $2;
230
+ SQL
231
+ rows.map do |row|
232
+ {
233
+ column_name: row["column_name"].to_sym,
234
+ type: row["data_type"].to_sym
235
+ }
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end