File: sql_comments.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (203 lines) | stat: -rw-r--r-- 6,471 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# frozen-string-literal: true
#
# The sql_comments extension adds Dataset#comment to the datasets,
# allowing you to set SQL comments in the resulting query.  These
# comments are appended to the end of the SQL query:
#
#   ds = DB[:table].comment("Some Comment").all
#   # SELECT * FROM table -- Some Comment
#   #
#
# As you can see, this uses single line SQL comments (--) suffixed
# by a newline.  This plugin transforms all consecutive whitespace
# in the comment to a single string:
#
#   ds = DB[:table].comment("Some\r\nComment     Here").all
#   # SELECT * FROM table -- Some Comment Here
#   #
#
# The reason for the prefixing and suffixing by newlines is to
# work correctly when used in subqueries:
#
#   ds = DB[:table].comment("Some\r\nComment     Here")
#   ds.where(id: ds).all
#   # SELECT * FROM table WHERE (id IN (SELECT * FROM table -- Some Comment Here
#   # )) -- Some Comment Here
#   #
#
# In addition to working on SELECT queries, it also works when
# inserting, updating, and deleting.
#
# Due to the use of single line SQL comments and converting all
# whitespace to spaces, this should correctly handle even
# malicious input.  However, it would be unwise to rely on that,
# you should ensure that the argument given
# to Dataset#comment is not derived from user input.
#
# You can load this extension into specific datasets:
#
#   ds = DB[:table]
#   ds = ds.extension(:sql_comments)
#
# Or you can load it into all of a database's datasets, which
# is probably the desired behavior if you are using this extension:
#
#   DB.extension(:sql_comments)
#
# Loading the sql_comments extension into the database also adds 
# support for block-level comment support via Database#with_comments.
# You call #with_comments with a hash.  Queries inside the hash will
# include a comment based on the hash (assuming they are inside the
# same thread):
#
#   DB.with_comments(model: Album, action: :all) do
#     DB[:albums].all
#     # SELECT * FROM albums -- model:Album,action:all
#   end
#
# You can nest calls to #with_comments, which will combine the
# entries from both calls:
#
#   DB.with_comments(application: App, path: :scrubbed_path) do
#     DB.with_comments(model: Album, action: :all) do
#       ds = DB[:albums].all
#       # SELECT * FROM albums
#       # -- application:App,path:scrubbed_path,model:Album,action:all
#     end
#   end
#
# You can override comment entries specified in earlier blocks, or
# remove entries specified earlier using a nil value:
#
#   DB.with_comments(application: App, path: :scrubbed_path) do
#     DB.with_comments(application: Foo, path: nil) do
#       ds = DB[:albums].all
#       # SELECT * FROM albums # -- application:Foo
#     end
#   end
#
# You can combine block-level comments with dataset-specific
# comments:
#
#   DB.with_comments(model: Album, action: :all) do
#     DB[:table].comment("Some Comment").all
#     # SELECT * FROM albums -- model:Album,action:all -- Some Comment
#   end
#
# Note that Microsoft Access does not support inline comments,
# and attempting to use comments on it will result in SQL syntax
# errors.
#
# Related modules: Sequel::SQLComments, Sequel::Database::SQLComments

#
module Sequel
  module SQLComments
    # Return a modified copy of the dataset that will use the given comment.
    # To uncomment a commented dataset, pass nil as the argument.
    def comment(comment)
      clone(:comment=>(format_sql_comment(comment) if comment))
    end

    %w'select insert update delete'.each do |type|
      define_method(:"#{type}_sql") do |*a|
        sql = super(*a)
        if comment = _sql_comment
          # This assumes that the comment stored in the dataset has
          # already been formatted. If not, this could result in SQL
          # injection.
          #
          # Additionally, due to the use of an SQL comment, if any
          # SQL is appened to the query after the comment is added,
          # it will become part of the comment unless it is preceded
          # by a newline.
          if sql.frozen?
            sql += comment
            sql.freeze
          elsif @opts[:append_sql] || @opts[:placeholder_literalizer]
            sql << comment
          else
            sql += comment
          end
        end
        sql
      end
    end

    private

    # The comment to include in the SQL query, if any.
    def _sql_comment
      @opts[:comment]
    end

    # Format the comment.  For maximum compatibility, this uses a
    # single line SQL comment, and converts all consecutive whitespace
    # in the comment to a single space.
    def format_sql_comment(comment)
      " -- #{comment.to_s.gsub(/\s+/, ' ')}\n"
    end
  end

  module Database::SQLComments
    def self.extended(db)
      db.instance_variable_set(:@comment_hashes, {})
      db.extend_datasets DatasetSQLComments
    end

    # A map of threads to comment hashes, used for correctly setting
    # comments for all queries inside #with_comments blocks.
    attr_reader :comment_hashes

    # Store the comment hash and use it to create comments inside the block
    def with_comments(comment_hash)
      hashes = @comment_hashes
      t = Sequel.current
      new_hash = if hash = Sequel.synchronize{hashes[t]}
        hash.merge(comment_hash)
      else
        comment_hash.dup
      end
      yield Sequel.synchronize{hashes[t] = new_hash}
    ensure
      if hash
        Sequel.synchronize{hashes[t] = hash}
      else
        t && Sequel.synchronize{hashes.delete(t)}
      end
    end

    module DatasetSQLComments
      include Sequel::SQLComments

      private

      # Include comments added via Database#with_comments in the output SQL.
      def _sql_comment
        specific_comment = super
        return specific_comment if @opts[:append_sql]

        t = Sequel.current
        hashes = db.comment_hashes
        block_comment = if comment_hash = Sequel.synchronize{hashes[t]}
          comment_array = comment_hash.map{|k,v| "#{k}:#{v}" unless v.nil?}
          comment_array.compact!
          comment_array.join(",")
        end

        if block_comment
          if specific_comment
            format_sql_comment(block_comment + specific_comment)
          else
            format_sql_comment(block_comment)
          end
        else
          specific_comment
        end
      end
    end
  end

  Dataset.register_extension(:sql_comments, SQLComments)
  Database.register_extension(:sql_comments, Database::SQLComments)
end