Class: AppQuery::Q

Inherits:
Object
  • Object
show all
Defined in:
lib/app_query.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(sql, name: nil) ⇒ Q

Returns a new instance of Q.



93
94
95
96
# File 'lib/app_query.rb', line 93

def initialize(sql, name: nil)
  @sql = sql
  @name = name
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



91
92
93
# File 'lib/app_query.rb', line 91

def name
  @name
end

#sqlObject (readonly)

Returns the value of attribute sql.



91
92
93
# File 'lib/app_query.rb', line 91

def sql
  @sql
end

Instance Method Details

#append_cte(cte) ⇒ Object

example:

AppQuery("select 1").append_cte("foo as(select 1)")


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
# File 'lib/app_query.rb', line 170

def append_cte(cte)
  # early raise when cte is not valid sql
  add_recursive, to_append = Tokenizer.tokenize(cte, state: :lex_append_cte).then do |tokens|
    [!recursive? && tokens.find { _1[:t] == "RECURSIVE" },
      tokens.reject { _1[:t] == "RECURSIVE" }]
  end

  if cte_names.none?
    self.class.new("WITH #{cte}\n#{self}")
  else
    nof_ctes = cte_names.size

    self.class.new(tokens.map do |token|
      nof_ctes -= 1 if token[:t] == "CTE_SELECT"

      if nof_ctes.zero?
        nof_ctes -= 1
        token[:v] + to_append.map { _1[:v] }.join
      elsif token[:t] == "WITH" && add_recursive
        token[:v] + add_recursive[:v]
      else
        token[:v]
      end
    end.join)
  end
end

#cte_namesObject



122
123
124
# File 'lib/app_query.rb', line 122

def cte_names
  tokens.filter { _1[:t] == "CTE_IDENTIFIER" }.map { _1[:v] }
end

#prepend_cte(cte) ⇒ Object

example:

AppQuery("select 1").prepend_cte("foo as(select 1)")


148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/app_query.rb', line 148

def prepend_cte(cte)
  # early raise when cte is not valid sql
  to_append = Tokenizer.tokenize(cte, state: :lex_prepend_cte).then do |tokens|
    recursive? ? tokens.reject { _1[:t] == "RECURSIVE" } : tokens
  end

  if cte_names.none?
    self.class.new("WITH #{cte}\n#{self}")
  else
    split_at_type = recursive? ? "RECURSIVE" : "WITH"
    self.class.new(tokens.map do |token|
      if token[:t] == split_at_type
        token[:v] + to_append.map { _1[:v] }.join
      else
        token[:v]
      end
    end.join)
  end
end

#recursive?Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/app_query.rb', line 142

def recursive?
  !!tokens.find { _1[:t] == "RECURSIVE" }
end

#replace_cte(cte) ⇒ Object

Replaces an existing cte. Raises ‘ArgumentError` when cte does not exist.



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/app_query.rb', line 199

def replace_cte(cte)
  add_recursive, to_append = Tokenizer.tokenize(cte, state: :lex_recursive_cte).then do |tokens|
    [!recursive? && tokens.find { _1[:t] == "RECURSIVE" },
      tokens.reject { _1[:t] == "RECURSIVE" }]
  end

  cte_name = to_append.find { _1[:t] == "CTE_IDENTIFIER" }&.[](:v)
  unless cte_names.include?(cte_name)
    raise ArgumentError, "Unknown cte #{cte_name.inspect}. Options: #{cte_names}."
  end
  cte_ix = cte_names.index(cte_name)

  return self unless cte_ix

  cte_found = false

  self.class.new(tokens.map do |token|
    if cte_found ||= token[:t] == "CTE_IDENTIFIER" && token[:v] == cte_name
      unless (cte_found = (token[:t] != "CTE_SELECT"))
        next to_append.map { _1[:v] }.join
      end

      next
    elsif token[:t] == "WITH" && add_recursive
      token[:v] + add_recursive[:v]
    else
      token[:v]
    end
  end.join)
end

#selectObject



138
139
140
# File 'lib/app_query.rb', line 138

def select
  tokens.find { _1[:t] == "SELECT" }&.[](:v)
end

#select_all(binds: [], select: nil, cast: false) ⇒ Object



98
99
100
101
102
103
104
# File 'lib/app_query.rb', line 98

def select_all(binds: [], select: nil, cast: false)
  with_select(select).then do |aq|
    ActiveRecord::Base.connection.select_all(aq.to_s, name, binds).then do |result|
      Result.from_ar_result(result, cast)
    end
  end
end

#select_one(binds: [], select: nil, cast: false) ⇒ Object



106
107
108
# File 'lib/app_query.rb', line 106

def select_one(binds: [], select: nil, cast: false)
  select_all(binds:, select:, cast:).first || {}
end

#select_value(binds: [], select: nil, cast: false) ⇒ Object



110
111
112
# File 'lib/app_query.rb', line 110

def select_value(binds: [], select: nil, cast: false)
  select_one(binds:, select:, cast:).values.first
end

#to_sObject



230
231
232
# File 'lib/app_query.rb', line 230

def to_s
  @sql
end

#tokenizerObject



118
119
120
# File 'lib/app_query.rb', line 118

def tokenizer
  @tokenizer ||= Tokenizer.new(to_s)
end

#tokensObject



114
115
116
# File 'lib/app_query.rb', line 114

def tokens
  @tokens ||= tokenizer.run
end

#with_select(sql) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/app_query.rb', line 126

def with_select(sql)
  return self unless sql
  if cte_names.include?("_")
    self.class.new(tokens.each_with_object([]) do |token, acc|
      v = (token[:t] == "SELECT") ? sql : token[:v]
      acc << v
    end.join, name: name)
  else
    append_cte("_ as (\n  #{select}\n)").with_select(sql)
  end
end