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.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ Databases are the ugly step-child of documentation. Explaining what each
2
+ column and table is actually *for* suffers from the "[documentation vicious
3
+ circle](http://www.hezmatt.org/~mpalmer/blog/2015/02/15/the-documentation-vicious-circle.html)"
4
+ -- nobody writes documentation, because nobody reads it, because it never
5
+ exists, therefore there's no point even looking for it. Unlike source code,
6
+ which allows comments to exist alongside the live-edited code, there's
7
+ typically *no way* to keep documentation at the "point of use" of an SQL
8
+ database. Sure, you can write docs in your migrations, or a wiki somewhere,
9
+ but when you're banging away at your SQL command line, who wants to go
10
+ rummaging around in a wiki?
11
+
12
+ As with all things, PostgreSQL to the rescue! The non-standard [`COMMENT`
13
+ command](http://www.postgresql.org/docs/current/interactive/sql-comment.html)
14
+ allows you to attach an arbitrary chunk of text to pretty much any object in
15
+ the database. Want to document a collation? `COMMENT ON COLLATION
16
+ <object_name> IS 'something something dark side'` and you're done!
17
+
18
+ If you're a lover of [Sequel](http://sequel.jeremyevans.net/), though, the
19
+ last thing you want to be doing is hand-writing SQL. Blech. That's old
20
+ school. You want to have your comments Right There in the migrations.
21
+ That's what this gem is all about.
22
+
23
+
24
+ # Usage
25
+
26
+ First, you need to enable the plugin:
27
+
28
+ Sequel::Database.extension :pg_comment
29
+
30
+ Then, you can attach a comment to anything you can create with [Sequel
31
+ schema
32
+ modifications](http://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html)
33
+ by adding a `:comment` option, like so:
34
+
35
+ create_table :comments, :comment => "Foo to you too" do
36
+ primary_key :id, :comment => "Auto-incrementing primary key"
37
+ String :data, :null => false, :comment => "Markdown"
38
+ foreign_key :user_id, :users, :comment => "The user who owns the comment"
39
+
40
+ index :user_id, :comment => "Find those user comments faster!"
41
+ end
42
+
43
+ For some object types, though, there's no syntactic sugar in Sequel, so
44
+ you've got to create them by hand. Never fear, though! You can comment on
45
+ any object using Ruby code, like so:
46
+
47
+ comment_on :collation, :my_collation, "Collate ALL THE THINGS!"
48
+
49
+ This is useful also for tables, where you might not want to insert a lengthy
50
+ comment in the top of your `create_table` block.
51
+
52
+ The `table.column` syntax that PgSQL requires for `COMMENT ON COLUMN` can be
53
+ simulated in the usual Sequel fashion, of using two underscores in the
54
+ symbol to separate the table name from the column name, like so:
55
+
56
+ comment_on :column, :foo__bar_id, %{
57
+ This is my column. There are many like it, but this one is mine.
58
+ }
59
+ # => COMMENT ON COLUMN "foo"."bar_id" IS 'This is my column. (etc)'
60
+
61
+ **NOTE**: The object you wish to comment on *must already exist* before you
62
+ call `comment_on`. The following example **WILL NOT WORK**:
63
+
64
+ comment_on :table, :foo, "This is an awesomely foo table"
65
+ create_table :foo, do
66
+ # ...
67
+ end
68
+
69
+ You have to put the `comment_on` *after* the `create_table`, like this:
70
+
71
+ create_table :foo, do
72
+ # ...
73
+ end
74
+ comment_on :table, :foo, "This is an awesomely foo table"
75
+
76
+
77
+ ## Comment string tidy-up
78
+
79
+ On the whole, `sequel-pg-comment` makes no judgment on what you put in your
80
+ comments. Plain text, markdown, XML, or morse code -- it's all the same.
81
+
82
+ There is *one* manipulation that is done to multi-line comments, though, to
83
+ make it a bit easier to write lengthy treatises on the whichness of the why,
84
+ and that is to strip out leading whitespace. The rules are very simple:
85
+
86
+ 1. Empty lines at the beginning and end of the comment are removed; and
87
+
88
+ 1. Whatever whitespace is present before the *first* non-empty line of the
89
+ comment, will be stripped from the beginning of *every* line.
90
+
91
+ That means you can use a heredoc for your multi-line comments, and they'll
92
+ still look neat and tidy without having to play `gsub` tricks:
93
+
94
+ create_table :foo do
95
+ String :data, :comment => <<-EOF
96
+ This is a very lengthy comment. It goes for many lines
97
+ and has a great deal to say on any number of subjects. I
98
+ could have used lorem ipsum here, but I prefer to do things
99
+ the old-fashioned way. If you've read all of this example,
100
+ you probably stay to read the whole of the credits at the
101
+ cinema. Good for you! I do too. Wave next time, you
102
+ anti-social loner.
103
+ EOF
104
+ end
105
+
106
+ One caveat: it's common to use the `%( ... )` quoting style for lengthy
107
+ strings. That's fine, but make sure to put the first line of docs on its
108
+ own line, and not directly after the `%(`. For example, this will not work
109
+ so well:
110
+
111
+ # This WILL NOT trim leading whitespace from each line
112
+ comment_on :table, :foo, %(This is a long comment.
113
+ However, due to the way that pg-comment trims whitespace,
114
+ these lines will have leading indents, because the first line
115
+ didn't.
116
+ )
117
+
118
+ Instead, you'll want to do this:
119
+
120
+ # This WILL trim leading whitespace from each line
121
+ comment_on :table, :foo, %(
122
+ This, too, is a long comment. Because the first non-empty
123
+ line had leading spaces, all of these other lines will have
124
+ their leaving spaces stripped too.
125
+ )
126
+
127
+ As always, inconsistent use of tabs and spaces will end in disaster. So
128
+ don't do that. Remember: tabs are for indenting, spaces are for formatting.
129
+
130
+
131
+ ## Quoting and escaping
132
+
133
+ In normal circumstances, if you follow some fairly simple rules and don't
134
+ need to put comments on a few gnarly types, you should never have to do any
135
+ SQL-specific escaping or quoting of the values you pass to the methods in
136
+ this extension. We work very hard to properly escape as much as we can.
137
+
138
+ The rules for quoting are:
139
+
140
+ 1. If an object name is passed as a symbol, it will be escaped. For certain
141
+ types (`COLUMN`, `CONSTRAINT`, `RULE`, and `TRIGGER`), we split on the first
142
+ double underscore (ie `__`) and the part before the double underscore is the
143
+ table name, and the rest is the object name. Each part is quoted
144
+ separately.
145
+
146
+ 2. If an object name is passed as a string, **NO QUOTING IS PERFORMED**. It
147
+ is assumed, in that instance, that you've already done all the quoting you
148
+ need yourself.
149
+
150
+ 3. Comment strings should always be passed as strings.
151
+
152
+ 4. Object types can be specified as either strings or symbols, with space-
153
+ or understore-separated words, in any mix of case, and they will be
154
+ correctly handled.
155
+
156
+ (Of course, the *real* rule for object naming is: *stick to alphanumerics and
157
+ underscores, fer cryin' out loud!*)
158
+
159
+ The relative complexity of these rules is due to the fact that there are a
160
+ few PostgreSQL object types which stubbornly resist attempts to
161
+ automatically escape their names in a safe manner. The most visible
162
+ offender is `FUNCTION`, which is both relatively commonly used, and has a
163
+ *particularly* complicated object name specification. For those types, you
164
+ have to do some quoting yourself, and pass the object name as a string.
165
+
166
+ I do apologise for the complexity and lack of *absolute* safety in all this
167
+ (although if someone can SQL inject your migrations, you're having a
168
+ *really* bad day), but I had absolutely no luck in producing an interface
169
+ that wasn't a complete nightmare to use, an implementation that wasn't a
170
+ complex mess, and that stuck fairly closely to the common idioms present in
171
+ Sequel proper. If you happen to have ideas on how to make this better, [I'm
172
+ all ears](mailto:theshed+sequel-pg-comment@hezmatt.org).
173
+
174
+
175
+ # Getting your comments back
176
+
177
+ It's great that this gem can help you to document your database, but that's
178
+ not much use if nobody can read them again. Within Sequel itself, you can
179
+ retrieve comments quite easily:
180
+
181
+ DB.comment_for(:foo) # => "Something something dark side"
182
+
183
+ For columns, you can either use the double underscore notation:
184
+
185
+ DB.comment_for(:foo__column) # => "Awwwwww yeah"
186
+
187
+ Or you can do the same thing from the dataset itself:
188
+
189
+ DB[:foo].comment_for(:column) # => "Awwwwww yeah"
190
+
191
+ There's currently no support for retrieving a database comment from a Sequel
192
+ model; pull requests implementing such a feature would be warmly welcomed.
193
+
194
+ The real value in database comments, though, comes when you use an
195
+ entity-relationship diagram tool like
196
+ [SchemaSpy](http://schemaspy.sourceforge.net/), which draws all sorts of
197
+ pretty pictures and lays out all of the schema information.
198
+
199
+ If you quickly need to get some docs when you're in `psql`, you can get it
200
+ out of the "additional detail" informational commands, like `\dt+ <table
201
+ name>`.
202
+
203
+
204
+ # Contributing
205
+
206
+ Bug reports should be sent to the [Github issue
207
+ tracker](https://github.com/mpalmer/sequel-pg-comment/issues), or
208
+ [e-mailed](mailto:theshed+sequel-pg-comment@hezmatt.org). Patches can be sent as a
209
+ Github pull request, or [e-mailed](mailto:theshed+sequel-pg-comment@hezmatt.org).
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ exec(*(["bundle", "exec", $PROGRAM_NAME] + ARGV)) if ENV['BUNDLE_GEMFILE'].nil?
2
+
3
+ Bundler.setup(:default, :development)
4
+
5
+ task :default => :test
6
+
7
+ begin
8
+ Bundler.setup(:default, :development)
9
+ rescue Bundler::BundlerError => e
10
+ $stderr.puts e.message
11
+ $stderr.puts "Run `bundle install` to install missing gems"
12
+ exit e.status_code
13
+ end
14
+
15
+ Bundler::GemHelper.install_tasks
16
+
17
+ task :release do
18
+ sh "git release"
19
+ end
20
+
21
+ require 'yard'
22
+
23
+ YARD::Rake::YardocTask.new :doc do |yardoc|
24
+ yardoc.files = %w{lib/**/*.rb - README.md}
25
+ end
26
+
27
+ desc "Run guard"
28
+ task :guard do
29
+ require 'guard'
30
+ ::Guard.start(:clear => true)
31
+ while ::Guard.running do
32
+ sleep 0.5
33
+ end
34
+ end
35
+
36
+ require 'rspec/core/rake_task'
37
+ RSpec::Core::RakeTask.new :test do |t|
38
+ t.pattern = "spec/**/*_spec.rb"
39
+ end
@@ -0,0 +1,73 @@
1
+ module Sequel::Extension; end #:nodoc:
2
+
3
+ # PostgreSQL-specific extension to set and retrieve comments on
4
+ # all database objects.
5
+ #
6
+ module Sequel::Extension::PgComment
7
+ # Strip indenting whitespace from a comment string.
8
+ #
9
+ # Two rules are applied by this method:
10
+ #
11
+ # 1. Empty lines (that is, lines which have *no* characters in them, not
12
+ # even whitespace) are removed from the beginning and end of the
13
+ # string.
14
+ #
15
+ # 2. Whatever whitespace characters may be present before the first line
16
+ # of text will be removed from the beginning of each subsequent line
17
+ # of the comment, if it exists.
18
+ #
19
+ # This means you can use heredocs and multi-line strings without having
20
+ # to play your own silly-buggers in order to not make the comments look
21
+ # like arse. If you like heredocs, you're encouraged to use this style:
22
+ #
23
+ # comment_for :table, :foo, <<-EOF
24
+ # This is my comment.
25
+ #
26
+ # And here's another line.
27
+ # EOF
28
+ #
29
+ # On the other hand, if percent-quotes are more your cup of tea, you can
30
+ # go at them like this:
31
+ #
32
+ # comment_for :table, :foo, %(
33
+ # This is my comment.
34
+ #
35
+ # And here's another line.
36
+ # )
37
+ #
38
+ # Be sure to not start the first line of your comment on the same line as
39
+ # the quote character, though, because that will mean the first line of
40
+ # the comment *doesn't* have any leading whitespace, and so whitespace
41
+ # won't be trimmed from the rest of the comment. So, don't do this:
42
+ #
43
+ # comment_for :table, :foo, %(This won't work at all well.
44
+ # Since the first line of text doesn't have leading whitespace,
45
+ # these lines won't have it stripped either, and everything will
46
+ # look weird.
47
+ # )
48
+ #
49
+ # @param comment [String] The comment to mangle for whitespace.
50
+ #
51
+ # @return [String] The normalised comment, with whitespace removed.
52
+ #
53
+ def self.normalise_comment(comment)
54
+ comment.tap do |s|
55
+ s.gsub!(/\A\n+/, '')
56
+ s.gsub!(/\n+\Z/, '')
57
+
58
+ (s =~ /\A(\s+)/ and lede = $1) or lede = ''
59
+ s.gsub!(/^#{lede}/, '')
60
+ end
61
+ end
62
+ end
63
+
64
+ require_relative 'pg_comment/sql_generator'
65
+ require_relative 'pg_comment/database_methods'
66
+ require_relative 'pg_comment/dataset_methods'
67
+ require_relative 'pg_comment/create_table_generator_methods'
68
+ require_relative 'pg_comment/alter_table_generator_methods'
69
+
70
+ Sequel::Database.register_extension(:pg_comment) do |db|
71
+ db.extend Sequel::Extension::PgComment::DatabaseMethods
72
+ db.extend_datasets Sequel::Extension::PgComment::DatasetMethods
73
+ end
@@ -0,0 +1,126 @@
1
+ #:nodoc:
2
+ # Enhancements to the standard schema modification methods in a
3
+ # block-form `alter_table` method, to support setting comments via the
4
+ # `:comment` option.
5
+ #
6
+ # Note that not every schema modification method is enhanced in this module;
7
+ # some modifications are implemneted in terms of more fundamental methods,
8
+ # and so do not require their own method here. For example, `add_foreign_key`
9
+ # with a single column is handled by `add_column`, and so doesn't require its
10
+ # own implementation. Rest assured that all schema modification methods
11
+ # *should* accept a `:comment` option, and set a comment in the database. If
12
+ # you find one that doesn't, please file a bug.
13
+ #
14
+ module Sequel::Extension::PgComment::AlterTableGeneratorMethods
15
+ attr_reader :comments
16
+
17
+ include Sequel::Extension::PgComment
18
+
19
+ # Enhanced version of the `add_column` schema modification method,
20
+ # which supports setting a comment on the column.
21
+ #
22
+ # @option [String] :comment The comment to set on the column
23
+ # that is being added.
24
+ #
25
+ def add_column(*args)
26
+ super
27
+
28
+ if args.last.is_a?(Hash) && args.last[:comment]
29
+ comments << SqlGenerator.create(:column, args.first, args.last[:comment])
30
+ end
31
+ end
32
+
33
+ # Enhanced version of the `add_composite_primary_key` schema modification
34
+ # method, which supports setting a comment on the index.
35
+ #
36
+ # @option [String] :comment The comment to set on the index that is being
37
+ # added.
38
+ #
39
+ def add_composite_primary_key(columns, opts)
40
+ super
41
+
42
+ if opts[:comment]
43
+ comments << PrefixSqlGenerator.new(:index, :_pkey, opts[:comment])
44
+ end
45
+ end
46
+
47
+ # Enhanced version of the `add_composite_foreign_key` schema modification
48
+ # method, which supports setting a comment on the constraint.
49
+ #
50
+ # @option [String] :comment The comment to set on the constraint that is being
51
+ # added.
52
+ #
53
+ def add_composite_foreign_key(columns, table, opts)
54
+ super
55
+
56
+ if opts[:comment]
57
+ comments << if opts[:name]
58
+ SqlGenerator.create(:constraint, opts[:name], opts[:comment])
59
+ else
60
+ PrefixSqlGenerator.new(
61
+ :constraint,
62
+ "_#{columns.first}_fkey".to_sym,
63
+ opts[:comment]
64
+ )
65
+ end
66
+ end
67
+ end
68
+
69
+ # Enhanced version of the `add_index` schema modification method, which
70
+ # supports setting a comment on the index.
71
+ #
72
+ # @option [String] :comment The comment to set on the index that is being
73
+ # added.
74
+ #
75
+ def add_index(columns, opts = OPTS)
76
+ if opts[:comment]
77
+ comments << if opts[:name]
78
+ SqlGenerator.create(:index, opts[:name], opts[:comment])
79
+ else
80
+ PrefixSqlGenerator.new(
81
+ :index,
82
+ "_#{[columns].flatten.map(&:to_s).join("_")}_index".to_sym,
83
+ opts[:comment]
84
+ )
85
+ end
86
+ end
87
+ end
88
+
89
+ # Enhanced version of the `add_constraint` schema modification method,
90
+ # which supports setting a comment on the constraint.
91
+ #
92
+ # @option [String] :comment The comment to set on the constraint that is
93
+ # being added.
94
+ #
95
+ def add_constraint(name, *args, &block)
96
+ super
97
+
98
+ opts = args.last.is_a?(Hash) ? args.last : {}
99
+
100
+ if opts[:comment]
101
+ comments << SqlGenerator.create(:constraint, name, opts[:comment])
102
+ end
103
+ end
104
+
105
+ # Enhanced version of the `add_unique_constraint` schema modification
106
+ # method, which supports setting a comment on the index.
107
+ #
108
+ # @option [String] :comment The comment to set on the index that is being
109
+ # added.
110
+ #
111
+ def add_unique_constraint(columns, opts = OPTS)
112
+ super
113
+
114
+ if opts[:comment]
115
+ comments << if opts[:name]
116
+ SqlGenerator.create(:index, opts[:name], opts[:comment])
117
+ else
118
+ PrefixSqlGenerator.new(
119
+ :index,
120
+ "_#{[columns].flatten.map(&:to_s).join("_")}_key".to_sym,
121
+ opts[:comment]
122
+ )
123
+ end
124
+ end
125
+ end
126
+ end