sequel-pg-comment 1.0.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.
@@ -0,0 +1,159 @@
1
+ #:nodoc:
2
+ # Enhancements to the standard schema modification methods in a
3
+ # block-form `create_table` method, to support setting comments via the
4
+ # `:comment` option.
5
+ #
6
+ module Sequel::Extension::PgComment::CreateTableGeneratorMethods
7
+ # An array of all the comments that this generator has seen fit to
8
+ # create.
9
+ #
10
+ # @return [Array<SqlGenerator>]
11
+ #
12
+ attr_reader :comments
13
+
14
+ include Sequel::Extension::PgComment
15
+
16
+ # Enhanced version of the `column` table definition method, which
17
+ # supports setting a comment on the column.
18
+ #
19
+ # @option [String] :comment The comment to set on the column that is
20
+ # being defined.
21
+ #
22
+ def column(*args)
23
+ super
24
+
25
+ if args.last.is_a?(Hash) && args.last[:comment]
26
+ comments << SqlGenerator.create(:column, args.first, args.last[:comment])
27
+ end
28
+ end
29
+
30
+ # Enhanced version of the `primary_key` table definition method, which
31
+ # supports setting a comment on either the column or constraint.
32
+ #
33
+ # If the primary key is composite (`name` is an array), then the comment
34
+ # will be placed on the index. Otherwise, the comment will be set
35
+ # on the column itself.
36
+ #
37
+ # @option [String] :comment The comment to set on the column or
38
+ # index that is being defined.
39
+ #
40
+ def primary_key(name, *args)
41
+ if args.last.is_a?(Hash) && args.last[:comment] and !name.is_a? Array
42
+ # The composite primary key case will be handled by the
43
+ # `composite_primary_key` method, so we don't have to deal with it
44
+ # here.
45
+ comments << SqlGenerator.create(:column, name, args.last[:comment])
46
+ end
47
+
48
+ super
49
+ end
50
+
51
+ # Enhanced version of the `composite_primary_key` table definition method,
52
+ # which supports setting a comment on the primary key index.
53
+ #
54
+ # @option [String] :comment The comment to set on the primary key index.
55
+ #
56
+ def composite_primary_key(columns, *args)
57
+ if args.last.is_a?(Hash) and args.last[:comment]
58
+ if args.last[:name]
59
+ comments << SqlGenerator.create(
60
+ :index,
61
+ args.last[:name],
62
+ args.last[:comment]
63
+ )
64
+ else
65
+ comments << PrefixSqlGenerator.new(:index, :_pkey, args.last[:comment])
66
+ end
67
+ end
68
+
69
+ super
70
+ end
71
+
72
+ # Enhanced version of the `composite_foreign_key` table definition method,
73
+ # which supports setting a comment on the FK constraint.
74
+ #
75
+ # @option [String] :comment The comment to set on the foreign key constraint.
76
+ #
77
+ def composite_foreign_key(columns, opts)
78
+ if opts.is_a?(Hash) and opts[:comment] and opts[:table]
79
+ if opts[:name]
80
+ comments << SqlGenerator.create(
81
+ :constraint,
82
+ opts[:name],
83
+ opts[:comment]
84
+ )
85
+ else
86
+ comments << SqlGenerator.create(
87
+ :constraint,
88
+ "#{opts[:table]}_#{columns.first}_fkey".to_sym,
89
+ opts[:comment]
90
+ )
91
+ end
92
+ end
93
+
94
+ super
95
+ end
96
+
97
+ # Enhanced version of the `index` table definition method,
98
+ # which supports setting a comment on the index.
99
+ #
100
+ # @option [String] :comment The comment to set on the index that is being
101
+ # defined.
102
+ #
103
+ def index(columns, opts = OPTS)
104
+ if opts[:comment]
105
+ if opts[:name]
106
+ comments << SqlGenerator.create(:index, opts[:name], opts[:comment])
107
+ else
108
+ comments << PrefixSqlGenerator.new(
109
+ :index,
110
+ ("_" + [columns].flatten.map(&:to_s).join('_') + "_index").to_sym,
111
+ opts[:comment]
112
+ )
113
+ end
114
+ end
115
+
116
+ super
117
+ end
118
+
119
+ # Enhanced version of the `unique` table definition method,
120
+ # which supports setting a comment on the unique index.
121
+ #
122
+ # @option [String] :comment The comment to set on the index that will be
123
+ # defined.
124
+ #
125
+ def unique(columns, opts = OPTS)
126
+ if opts[:comment]
127
+ if opts[:name]
128
+ comments << SqlGenerator.create(:index, opts[:name], opts[:comment])
129
+ else
130
+ comments << PrefixSqlGenerator.new(
131
+ :index,
132
+ ("_" + [columns].flatten.map(&:to_s).join('_') + "_key").to_sym,
133
+ opts[:comment]
134
+ )
135
+ end
136
+ end
137
+
138
+ super
139
+ end
140
+
141
+ # Enhanced version of the `constraint` table definition method,
142
+ # which supports setting a comment on the constraint.
143
+ #
144
+ # @option [String] :comment The comment to set on the constraint that is
145
+ # being defined.
146
+ #
147
+ def constraint(name, *args, &block)
148
+ opts = name.is_a?(Hash) ? name : (args.last.is_a?(Hash) ? args.last : {})
149
+
150
+ if opts[:comment]
151
+ if name
152
+ comments << SqlGenerator.create(:constraint, name, opts[:comment])
153
+ else
154
+ raise RuntimeError,
155
+ "Setting comments on unnamed or check constraints is not supported"
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,175 @@
1
+ # Support for setting and retrieving comments on all object types
2
+ # in a PostgreSQL database.
3
+ #
4
+ module Sequel::Extension::PgComment::DatabaseMethods
5
+ # Set the comment for a database object.
6
+ #
7
+ # @param type [#to_s] The type of object that you wish to comment on.
8
+ # This can either be a string or symbol. Any object type that PgSQL
9
+ # knows about should be fair game. The current list of object types
10
+ # that this plugin knows about (and hence will accept) is listed in
11
+ # the {Sequel::Extension::PgComment::OBJECT_TYPES} array.
12
+ #
13
+ # @param id [#to_s] The name of the object that you wish to comment on.
14
+ # For most types of object, this should be the literal name of the
15
+ # object. However, for columns in a table or view, you should separate
16
+ # the table/view name from the column name with a double underscore
17
+ # (ie `__`). This is the standard Sequel convention for such things.
18
+ #
19
+ # @param comment [String] The comment you wish to set for the database
20
+ # object.
21
+ #
22
+ # @see {Sequel::Extension::PgComment.normalise_comment} for details on
23
+ # how the comment string is interpreted.
24
+ #
25
+ def comment_on(type, id, comment)
26
+ gen = begin
27
+ Sequel::Extension::PgComment::SqlGenerator.create(type, id, comment)
28
+ rescue ArgumentError
29
+ raise ArgumentError,
30
+ "Invalid object type: #{type.inspect}"
31
+ end
32
+
33
+ execute(gen.generate)
34
+ end
35
+
36
+ # Retrieve the comment for a database object.
37
+ #
38
+ # @param object [#to_s] The name of the database object to retrieve. For
39
+ # most objects, this should be the literal name of the object.
40
+ # However, for columns on tables and views, the name of the table/view
41
+ # should be a separated from the name of the column by a double
42
+ # underscore (ie `__`).
43
+ #
44
+ # @return [String, NilClass] The comment on the object, or `nil` if no
45
+ # comment has been defined for the object.
46
+ #
47
+ def comment_for(object)
48
+ object = object.to_s
49
+
50
+ if object.index("__")
51
+ tbl, col = object.split("__", 2)
52
+
53
+ execute("SELECT col_description(c.oid, a.attnum) " +
54
+ "FROM pg_class c JOIN pg_attribute a " +
55
+ "ON (c.oid=a.attrelid) " +
56
+ "WHERE c.relname=#{literal(tbl)} " +
57
+ "AND a.attname=#{literal(col)}"
58
+ )
59
+ else
60
+ execute("SELECT obj_description(#{literal(object.to_s)}::regclass, 'pg_class')")
61
+ end
62
+ end
63
+
64
+ # An enhanced form of the standard `create_table` method, which supports
65
+ # setting a comment in the `create_table` call when the `:comment` option
66
+ # is provided.
67
+ #
68
+ # @option [String] :comment The comment to set on the newly-created table.
69
+ #
70
+ # @see [Sequel::Database#create_table](http://sequel.jeremyevans.net/rdoc/classes/Sequel/Database.html#method-i-create_table)
71
+ #
72
+ def create_table(*args)
73
+ super
74
+
75
+ if args.last.is_a?(Hash) && args.last[:comment]
76
+ comment_on(:table, args.first, args.last[:comment])
77
+ end
78
+ end
79
+
80
+ #:nodoc:
81
+ # Enhanced version to support setting comments on objects created in a
82
+ # block-form `create_table` statement.
83
+ #
84
+ def create_table_generator(&block)
85
+ super do
86
+ extend Sequel::Extension::PgComment::CreateTableGeneratorMethods
87
+ @comments = []
88
+ instance_eval(&block) if block
89
+ end
90
+ end
91
+
92
+ #:nodoc:
93
+ # Enhanced version to support setting comments on objects created in a
94
+ # block-form `create_table` statement.
95
+ #
96
+ # If you're wondering why we override the
97
+ # create_table_indexes_from_generator method, rather than
98
+ # create_table_from_generator, it's because the indexes method runs last,
99
+ # and we can only create our comments after the objects we're commenting
100
+ # on have been created. We *could* set some comments in
101
+ # create_table_from_generator, and then set index comments in
102
+ # create_table_indexes_from_generator, but why override two methods when
103
+ # you can just override one to get the same net result?
104
+ #
105
+ def create_table_indexes_from_generator(name, generator, options)
106
+ super
107
+
108
+ generator.comments.each do |sql_gen|
109
+ if sql_gen.respond_to? :table_name
110
+ sql_gen.table_name = name
111
+ end
112
+
113
+ execute(sql_gen.generate)
114
+ end
115
+ end
116
+
117
+ #:nodoc:
118
+ # Enhanced version to support setting comments on objects created in a
119
+ # block-form `alter_table` statement.
120
+ #
121
+ def alter_table_generator(&block)
122
+ super do
123
+ extend Sequel::Extension::PgComment::AlterTableGeneratorMethods
124
+ @comments = []
125
+ instance_eval(&block) if block
126
+ end
127
+ end
128
+
129
+ #:nodoc:
130
+ # Enhanced version to support setting comments on objects created in a
131
+ # block-form `alter_table` statement.
132
+ #
133
+ def apply_alter_table_generator(name, generator)
134
+ super
135
+
136
+ generator.comments.each do |sql_gen|
137
+ if sql_gen.respond_to?(:table_name=)
138
+ sql_gen.table_name = name
139
+ end
140
+
141
+ execute(sql_gen.generate)
142
+ end
143
+ end
144
+
145
+ # An enhanced form of the standard `create_view` method, which supports
146
+ # setting a comment in the `create_view` call when the `:comment` option
147
+ # is provided.
148
+ #
149
+ # @option [String] :comment The comment to set on the newly-created view.
150
+ #
151
+ # @see [Sequel::Database#create_view](http://sequel.jeremyevans.net/rdoc/classes/Sequel/Database.html#method-i-create_view)
152
+ #
153
+ def create_view(*args)
154
+ super
155
+
156
+ if args.last.is_a?(Hash) && args.last[:comment]
157
+ comment_on(:view, args.first, args.last[:comment])
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ # Quote an object name, handling the double underscore convention
164
+ # for separating a column name from its containing object.
165
+ #
166
+ def quote_comment_identifier(id)
167
+ id = id.to_s
168
+ if id.index("__")
169
+ tbl, col = id.split("__", 2)
170
+ quote_identifier(tbl) + "." + quote_identifier(col)
171
+ else
172
+ quote_identifier(id)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,22 @@
1
+ # Support for retrieving column comments from a PostgreSQL database
2
+ # via a dataset. For example:
3
+ #
4
+ # DB[:foo_tbl].comment_for(:some_column)
5
+ #
6
+ # Will retrieve the comment for `foo_tbl.some_column`, if such a
7
+ # column exists.
8
+ #
9
+ module Sequel::Extension::PgComment::DatasetMethods
10
+ # Retrieve the comment for the column named `col` in the "primary" table
11
+ # for this dataset.
12
+ #
13
+ # @param col [#to_s] The name of the column for which to retrieve the
14
+ # comment.
15
+ #
16
+ # @return [String, NilClass] The comment defined for the column, or
17
+ # `nil` if there is no defined comment.
18
+ #
19
+ def comment_for(col)
20
+ db.comment_for("#{first_source_table}__#{col}")
21
+ end
22
+ end
@@ -0,0 +1,257 @@
1
+ module Sequel::Extension::PgComment
2
+ #:nodoc:
3
+ # Generate SQL to set a comment.
4
+ #
5
+ class SqlGenerator
6
+ # The PostgreSQL object types which this class knows how to generate
7
+ # comment SQL for.
8
+ #
9
+ OBJECT_TYPES = %w{AGGREGATE
10
+ CAST
11
+ COLLATION
12
+ CONVERSION
13
+ DATABASE
14
+ DOMAIN
15
+ EXTENSION
16
+ EVENT\ TRIGGER
17
+ FOREIGN\ DATA\ WRAPPER
18
+ FOREIGN\ TABLE
19
+ FUNCTION
20
+ INDEX
21
+ LARGE\ OBJECT
22
+ MATERIALIZED\ VIEW
23
+ OPERATOR
24
+ OPERATOR\ CLASS
25
+ OPERATOR\ FAMILY
26
+ PROCEDURAL\ LANGUAGE
27
+ LANGUAGE
28
+ ROLE
29
+ SCHEMA
30
+ SEQUENCE
31
+ SERVER
32
+ TABLE
33
+ TABLESPACE
34
+ TEXT\ SEARCH\ CONFIGURATION
35
+ TEXT\ SEARCH\ DICTIONARY
36
+ TEXT\ SEARCH\ PARSER
37
+ TEXT\ SEARCH\ TEMPLATE
38
+ TYPE
39
+ VIEW
40
+ }
41
+
42
+ # Find the correct class for a given object type, and instantiate a
43
+ # new one of them.
44
+ #
45
+ # @param object_type [String, Symbol] The type of object we're going
46
+ # to comment on. Strings and symbols are both fine, and any case
47
+ # is fine, too. Any underscores get turned into spaces. Apart from
48
+ # that, it needs to be the exact name that PostgreSQL uses for the given
49
+ # type.
50
+ #
51
+ # @param object_name [String, Symbol] The name of the database object to
52
+ # set the comment on. A string is considered "already quoted", and hence
53
+ # is not escaped any further. A symbol is run through the usual Sequel
54
+ # identifier escaping code before being unleashed on the world.
55
+ #
56
+ # @param comment [String] The comment to set.
57
+ #
58
+ # @return [SqlGenerator] Some sort of `SqlGenerator` object, or a subclass.
59
+ #
60
+ # @raise [ArgumentError] if you passed in an `object_type` that we don't
61
+ # know about.
62
+ #
63
+ def self.create(object_type, object_name, comment)
64
+ generators.each do |gclass|
65
+ if gclass.handles?(object_type)
66
+ return gclass.new(object_type, object_name, comment)
67
+ end
68
+ end
69
+
70
+ raise ArgumentError,
71
+ "Unrecognised object type #{object_type.inspect}"
72
+ end
73
+
74
+ # Return whether or not this class supports the specified object type.
75
+ #
76
+ # @param object_type [String, Symbol] @see {.create}
77
+ #
78
+ # @return [TrueClass, FalseClass] whether or not this class can handle
79
+ # the object type you passed.
80
+ #
81
+ def self.handles?(object_type)
82
+ self.const_get(:OBJECT_TYPES).include?(object_type.to_s.upcase.gsub('_', ' '))
83
+ end
84
+
85
+ private
86
+
87
+ # Return all known `SqlGenerator` classes.
88
+ #
89
+ def self.generators
90
+ @generators ||= ObjectSpace.each_object(Class).select do |klass|
91
+ klass.ancestors.include?(self)
92
+ end
93
+ end
94
+
95
+ # We just need this so we can quote things.
96
+ def self.mock_db
97
+ @mock_db ||= Sequel.connect("mock://postgres")
98
+ end
99
+
100
+ public
101
+
102
+ # The canonicalised string (that is, all-uppercase, with words
103
+ # separated by spaces) for the object type of this SQL generator.
104
+ #
105
+ attr_reader :object_type
106
+
107
+ # The raw (might-be-a-symbol, might-be-a-string) object name that
108
+ # was passed to us originally.
109
+ #
110
+ attr_reader :object_name
111
+
112
+ # The comment.
113
+ attr_reader :comment
114
+
115
+ # Spawn a new SqlGenerator.
116
+ #
117
+ # @see {.create}
118
+ #
119
+ def initialize(object_type, object_name, comment)
120
+ @object_type = object_type.to_s.upcase.gsub('_', ' ')
121
+ @object_name = object_name
122
+ @comment = comment
123
+ end
124
+
125
+ # SQL to set a comment on the object of our affection.
126
+ #
127
+ # @return [String] The SQL needed to set the comment.
128
+ #
129
+ def generate
130
+ quoted_object_name = case object_name
131
+ when Symbol
132
+ literal object_name
133
+ else
134
+ object_name
135
+ end
136
+
137
+ "COMMENT ON #{object_type} #{quoted_object_name} IS #{literal comment.to_s}"
138
+ end
139
+
140
+ private
141
+
142
+ # Quote the provided database object (a symbol) or string value
143
+ # (a string).
144
+ #
145
+ def literal(s)
146
+ self.class.mock_db.literal(s)
147
+ end
148
+ end
149
+
150
+ #:nodoc:
151
+ # A specialised generator for object types that live "inside" a
152
+ # table. Specifically, those types are columns, constraints,
153
+ # rules, and triggers.
154
+ #
155
+ # They get their own subclass because these object types can be
156
+ # manipulated inside a `create_table` or `alter_table` block, and at the
157
+ # time the block is evaluated, the code doesn't know the name of the
158
+ # table in which they are contained. So, we just stuff what we *do* know
159
+ # into these generators, and then when all's done, we can go to each of
160
+ # these generators, say "this is your table name", and then ask for the
161
+ # generated SQL.
162
+ #
163
+ class TableObjectSqlGenerator < SqlGenerator
164
+ # The few object types that this class handles.
165
+ OBJECT_TYPES = %w{COLUMN CONSTRAINT RULE TRIGGER}
166
+
167
+ # The name of the object which contains the object which is the direct
168
+ # target of this SQL generator. Basically, it's the table name.
169
+ attr_accessor :table_name
170
+
171
+ # Overridden constructor to deal with the double-underscore-separated
172
+ # names that we all know and love.
173
+ #
174
+ # @see {SqlGenerator#initialize}
175
+ #
176
+ def initialize(object_type, object_name, comment)
177
+ super
178
+
179
+ if object_name.is_a?(Symbol) and object_name.to_s.index("__")
180
+ @table_name, @object_name = object_name.to_s.split("__", 2).map(&:to_sym)
181
+ end
182
+ end
183
+
184
+ # Generate special SQL.
185
+ #
186
+ # @see {SqlGenerator#generate}
187
+ #
188
+ def generate
189
+ if table_name.nil?
190
+ raise ArgumentError,
191
+ "Cannot generate SQL for #{object_type} #{object_name} " +
192
+ "without a table_name"
193
+ end
194
+
195
+ qualified_object_name = case object_type
196
+ when "COLUMN"
197
+ "#{maybe_escape table_name}.#{maybe_escape object_name}"
198
+ when "CONSTRAINT", "RULE", "TRIGGER"
199
+ "#{maybe_escape object_name} ON #{maybe_escape table_name}"
200
+ end
201
+
202
+ "COMMENT ON #{object_type} #{qualified_object_name} IS #{literal comment}"
203
+ end
204
+
205
+ private
206
+
207
+ # Handle with the vagaries of having both strings and symbols as
208
+ # possible names -- we escape symbols, but leave strings to their own
209
+ # devices.
210
+ #
211
+ def maybe_escape(s)
212
+ Symbol === s ? literal(s) : s
213
+ end
214
+ end
215
+
216
+ #:nodoc:
217
+ # This is an annoying corner-case generator -- it doesn't handle any
218
+ # types by default, but it will handle any *other* type where the name of
219
+ # a table needs to be prefixed by a name. The only known use case for
220
+ # this at present is "implicit" (that is, automatically generated by the
221
+ # database) constraints and indexes that get prefixed by the table name,
222
+ # and which are generated at a time when the calling code doesn't know the
223
+ # name of the table that it is generating SQL for.
224
+ #
225
+ class PrefixSqlGenerator < SqlGenerator
226
+ # This class doesn't handle any object types directly, and must be
227
+ # instantiated directly when needed
228
+ OBJECT_TYPES = %w{}
229
+
230
+ # The name of the table which should be prefixed to the object name
231
+ # that was specified when this instance was created.
232
+ #
233
+ attr_accessor :table_name
234
+
235
+ # Generate super-dooper special SQL.
236
+ #
237
+ # @see {SqlGenerator#generate}
238
+ #
239
+ def generate
240
+ if table_name.nil?
241
+ raise ArgumentError,
242
+ "Cannot generate SQL for #{object_type} #{object_name} " +
243
+ "without a table_name"
244
+ end
245
+
246
+ prefixed_object_name = "#{table_name}#{object_name}"
247
+
248
+ if Symbol === table_name || Symbol === object_name
249
+ prefixed_object_name = prefixed_object_name.to_sym
250
+ end
251
+
252
+ g = SqlGenerator.create(object_type, prefixed_object_name, comment)
253
+ g.table_name = table_name if g.respond_to? :table_name
254
+ g.generate
255
+ end
256
+ end
257
+ end