postgres_ext 2.3.0 → 2.4.0.beta.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f8878d742f2c1d692cca006bbd3fe55dfb69e1d
4
- data.tar.gz: 64ae985d2dae1389ee05b0c843b2ad8d578d1ffc
3
+ metadata.gz: 45761b40db17573204e88b48c6eb91c135968fde
4
+ data.tar.gz: d774a33f235ac5f450df26f6f517575ba1360bed
5
5
  SHA512:
6
- metadata.gz: 1a8cd94d52fce6e7dbf5472899b1a1f45b9328fbf31e42a912c4ec4cf10880d2e398f0f954bf9ab49fea5e503c11a660f6ee89fbf690162e2bf3c96d64a338bc
7
- data.tar.gz: 66a9c9c002c548b0920657f243c6c23ea04a4468666b3799f5bc52ea76b25d0f47f2e84ec8071e2f818638f6af12e804223994e515456540b1eab9f38cbadcab
6
+ metadata.gz: c2b883ca3787352f2d6394d45116485565b4c0631b3f84e17405949cbd63049201c087860e470d88b3cad19e849f841b32d3537d5dfef29162d3e153bfb40538
7
+ data.tar.gz: 7f0224e503f2d0883aae7974b94de5366d612cba43ad4e243e1bf7d16bbffc5d3e2562eae66ad60e55af76915c5c7b1d45907d3652e607471f9e803d77994128
data/.travis.yml CHANGED
@@ -5,6 +5,7 @@ rvm:
5
5
  gemfile:
6
6
  - gemfiles/Gemfile.activerecord-4.0.x
7
7
  - gemfiles/Gemfile.activerecord-4.1.x
8
+ - gemfiles/Gemfile.activerecord-4.2.x
8
9
 
9
10
  env: DATABASE_URL=postgres://postgres@localhost/postgres_ext_test
10
11
 
data/Rakefile CHANGED
@@ -38,7 +38,7 @@ namespace :test do
38
38
  desc 'Test against all supported ActiveRecord versions'
39
39
  task :all do
40
40
  # Currently only supports Active Record v4.0
41
- %w(4.0.x 4.1.x).each do |version|
41
+ %w(4.0.x 4.1.x 4.2.x).each do |version|
42
42
  sh "BUNDLE_GEMFILE='gemfiles/Gemfile.activerecord-#{version}' bundle install --quiet"
43
43
  sh "BUNDLE_GEMFILE='gemfiles/Gemfile.activerecord-#{version}' bundle exec rake test"
44
44
  end
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path => '..'
4
+
5
+ gem "activerecord", "~> 4.2.0.beta2"
6
+
7
+ unless ENV['CI'] || RUBY_PLATFORM =~ /java/
8
+ gem 'byebug'
9
+ end
@@ -22,34 +22,9 @@ class CTEProxy
22
22
  name
23
23
  end
24
24
 
25
- def column_names
26
- @model.column_names
27
- end
28
-
29
- def columns_hash
30
- @model.columns_hash
31
- end
32
-
33
- def model_name
34
- @model.model_name
35
- end
36
-
37
- def primary_key
38
- @model.primary_key
39
- end
40
-
41
- def attribute_alias?(*args)
42
- @model.attribute_alias?(*args)
43
- end
44
-
45
- def aggregate_reflections(*args)
46
- @model.aggregate_reflections(*args)
47
- end
48
-
49
- def instantiate(record, column_types = {})
50
- @model.instantiate(record, column_types)
51
- end
52
-
25
+ delegate :column_names, :columns_hash, :model_name, :primary_key, :attribute_alias?,
26
+ :aggregate_reflections, :instantiate, :type_for_attribute, to: :@model
27
+
53
28
  private
54
29
 
55
30
  def reflections
@@ -8,7 +8,7 @@ module ActiveRecord
8
8
  case value
9
9
  when Array
10
10
  engine = attribute.relation.engine
11
- column = engine.connection.columns(attribute.relation.name).detect{ |col| col.name.to_s == attribute.name.to_s }
11
+ column = engine.connection.schema_cache.columns(attribute.relation.name).detect{ |col| col.name.to_s == attribute.name.to_s }
12
12
  if column && column.array
13
13
  attribute.eq(value)
14
14
  else
@@ -147,7 +147,6 @@ module ActiveRecord
147
147
  end
148
148
 
149
149
  def build_with(arel)
150
- visitor = arel.engine.connection.visitor
151
150
  with_statements = with_values.flat_map do |with_value|
152
151
  case with_value
153
152
  when String
@@ -156,13 +155,11 @@ module ActiveRecord
156
155
  with_value.map do |name, expression|
157
156
  case expression
158
157
  when String
159
- select = Arel::SqlLiteral.new "(#{expression})"
160
- when ActiveRecord::Relation
161
- select = Arel::SqlLiteral.new "(#{expression.to_sql})"
162
- when Arel::SelectManager
163
- select = Arel::SqlLiteral.new visitor.accept(expression)
158
+ select = Arel::Nodes::SqlLiteral.new "(#{expression})"
159
+ when ActiveRecord::Relation, Arel::SelectManager
160
+ select = Arel::Nodes::SqlLiteral.new "(#{expression.to_sql})"
164
161
  end
165
- as = Arel::Nodes::As.new Arel::SqlLiteral.new("\"#{name.to_s}\""), select
162
+ Arel::Nodes::As.new Arel::Nodes::SqlLiteral.new("\"#{name.to_s}\""), select
166
163
  end
167
164
  end
168
165
  end
@@ -1,9 +1,19 @@
1
- require 'arel/visitors/visitor'
1
+ require 'arel/visitors/postgresql'
2
+
2
3
  module Arel
3
4
  module Visitors
4
- class Visitor
5
- # We are adding our visitors to the main visitor for the time being until the right spot is found to monkey patch
5
+ class PostgreSQL
6
6
  private
7
+
8
+ def visit_Array o, a
9
+ column = a.relation.engine.connection.schema_cache.columns(a.relation.name).find { |col| col.name == a.name.to_s } if a
10
+ if column && column.respond_to?(:array) && column.array
11
+ quoted o, a
12
+ else
13
+ o.empty? ? 'NULL' : o.map { |x| visit x }.join(', ')
14
+ end
15
+ end
16
+
7
17
  def visit_Arel_Nodes_ContainedWithin o, a = nil
8
18
  "#{visit o.left, a} << #{visit o.right, o.left}"
9
19
  end
@@ -0,0 +1 @@
1
+ require 'postgres_ext/arel/4.1/visitors/postgresql'
@@ -0,0 +1,35 @@
1
+ require 'arel/predications'
2
+
3
+ module Arel
4
+ module Predications
5
+ def contained_within(other)
6
+ Nodes::ContainedWithin.new self, Nodes.build_quoted(other, self)
7
+ end
8
+
9
+ def contained_within_or_equals(other)
10
+ Nodes::ContainedWithinEquals.new self, Nodes.build_quoted(other, self)
11
+ end
12
+
13
+ def contains(other)
14
+ Nodes::Contains.new self, Nodes.build_quoted(other, self)
15
+ end
16
+
17
+ def contains_or_equals(other)
18
+ Nodes::ContainsEquals.new self, Nodes.build_quoted(other, self)
19
+ end
20
+
21
+ def overlap(other)
22
+ Nodes::Overlap.new self, Nodes.build_quoted(other, self)
23
+ end
24
+
25
+ def any(other)
26
+ any_tags_function = Arel::Nodes::NamedFunction.new('ANY', [self])
27
+ Arel::Nodes::Equality.new(Nodes.build_quoted(other, self), any_tags_function)
28
+ end
29
+
30
+ def all(other)
31
+ any_tags_function = Arel::Nodes::NamedFunction.new('ALL', [self])
32
+ Arel::Nodes::Equality.new(Nodes.build_quoted(other, self), any_tags_function)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ require 'arel/visitors/postgresql'
2
+
3
+ module Arel
4
+ module Visitors
5
+ class PostgreSQL
6
+ private
7
+
8
+ def visit_Arel_Nodes_ContainedWithin o, collector
9
+ infix_value o, collector, " << "
10
+ end
11
+
12
+ def visit_Arel_Nodes_ContainedWithinEquals o, collector
13
+ infix_value o, collector, " <<= "
14
+ end
15
+
16
+ def visit_Arel_Nodes_Contains o, collector
17
+ left_column = o.left.relation.engine.columns.find { |col| col.name == o.left.name.to_s }
18
+
19
+ if left_column && (left_column.type == :hstore || (left_column.respond_to?(:array) && left_column.array))
20
+ infix_value o, collector, " @> "
21
+ else
22
+ infix_value o, collector, " >> "
23
+ end
24
+ end
25
+
26
+ def visit_Arel_Nodes_ContainsEquals o, collector
27
+ infix_value o, collector, " >>= "
28
+ end
29
+
30
+ def visit_Arel_Nodes_Overlap o, collector
31
+ infix_value o, collector, " && "
32
+ end
33
+
34
+ def visit_IPAddr value, collector
35
+ collector << quote("#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1 @@
1
+ require 'postgres_ext/arel/4.2/visitors/postgresql'
@@ -1,3 +1,14 @@
1
+ ## TODO: Change to ~> 4.2.0 on gem release
2
+
3
+ gdep = Gem::Dependency.new('activerecord', '~> 4.2.0.beta2')
4
+ ar_version_cutoff = gdep.matching_specs.sort_by(&:version).last
5
+
1
6
  require 'postgres_ext/arel/nodes'
2
- require 'postgres_ext/arel/predications'
3
- require 'postgres_ext/arel/visitors'
7
+ if ar_version_cutoff
8
+ require 'postgres_ext/arel/4.2/predications'
9
+ require 'postgres_ext/arel/4.2/visitors'
10
+ else
11
+ require 'postgres_ext/arel/4.1/predications'
12
+ require 'postgres_ext/arel/4.1/visitors'
13
+ end
14
+
@@ -1,3 +1,3 @@
1
1
  module PostgresExt
2
- VERSION = '2.3.0'
2
+ VERSION = '2.4.0.beta.1'
3
3
  end
@@ -32,7 +32,7 @@ describe 'Array Column Predicates' do
32
32
 
33
33
  describe 'Array Overlap' do
34
34
  it 'converts Arel overlap statement' do
35
- arel_table.where(arel_table[:tags].overlap(['tag','tag 2'])).to_sql.must_match /&& '\{"tag","tag 2"\}'/
35
+ arel_table.where(arel_table[:tags].overlap(['tag','tag 2'])).to_sql.must_match /&& '\{"?tag"?,"tag 2"\}'/
36
36
  end
37
37
 
38
38
  it 'converts Arel overlap statement' do
@@ -61,7 +61,7 @@ describe 'Array Column Predicates' do
61
61
 
62
62
  describe 'Array Contains' do
63
63
  it 'converts Arel contains statement and escapes strings' do
64
- arel_table.where(arel_table[:tags].contains(['tag','tag 2'])).to_sql.must_match /@> '\{"tag","tag 2"\}'/
64
+ arel_table.where(arel_table[:tags].contains(['tag','tag 2'])).to_sql.must_match /@> '\{"?tag"?,"tag 2"\}'/
65
65
  end
66
66
 
67
67
  it 'converts Arel contains statement with numbers' do
@@ -1,7 +1,7 @@
1
1
  require 'test_helper'
2
2
 
3
3
  describe 'Array queries' do
4
- let(:equality_regex) { %r{\"people\"\.\"tags\" = '\{\"working\"\}'} }
4
+ let(:equality_regex) { %r{\"people\"\.\"tags\" = '\{"?working"?\}'} }
5
5
  let(:overlap_regex) { %r{\"people\"\.\"tag_ids\" && '\{1,2\}'} }
6
6
  let(:any_regex) { %r{2 = ANY\(\"people\"\.\"tag_ids\"\)} }
7
7
  let(:all_regex) { %r{2 = ALL\(\"people\"\.\"tag_ids\"\)} }
@@ -4,22 +4,22 @@ describe 'Common Table Expression queries' do
4
4
  describe '.with(common_table_expression_hash)' do
5
5
  it 'generates an expression with the CTE' do
6
6
  query = Person.with(lucky_number_seven: Person.where(lucky_number: 7)).joins('JOIN lucky_number_seven ON lucky_number_seven.id = people.id')
7
- query.to_sql.must_equal 'WITH "lucky_number_seven" AS (SELECT "people".* FROM "people" WHERE "people"."lucky_number" = 7) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id'
7
+ query.to_sql.must_match(/WITH "lucky_number_seven" AS \(SELECT "people".* FROM "people"(\s+)WHERE "people"."lucky_number" = 7\) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id/)
8
8
  end
9
9
 
10
10
  it 'generates an expression with multiple CTEs' do
11
11
  query = Person.with(lucky_number_seven: Person.where(lucky_number: 7), lucky_number_three: Person.where(lucky_number: 3)).joins('JOIN lucky_number_seven ON lucky_number_seven.id = people.id').joins('JOIN lucky_number_three ON lucky_number_three.id = people.id')
12
- query.to_sql.must_equal 'WITH "lucky_number_seven" AS (SELECT "people".* FROM "people" WHERE "people"."lucky_number" = 7), "lucky_number_three" AS (SELECT "people".* FROM "people" WHERE "people"."lucky_number" = 3) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id JOIN lucky_number_three ON lucky_number_three.id = people.id'
12
+ query.to_sql.must_match(/WITH "lucky_number_seven" AS \(SELECT "people".* FROM "people"(\s+)WHERE "people"."lucky_number" = 7\), "lucky_number_three" AS \(SELECT "people".* FROM "people"(\s+)WHERE "people"."lucky_number" = 3\) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id JOIN lucky_number_three ON lucky_number_three.id = people.id/)
13
13
  end
14
14
 
15
15
  it 'generates an expression with multiple with calls' do
16
16
  query = Person.with(lucky_number_seven: Person.where(lucky_number: 7)).with(lucky_number_three: Person.where(lucky_number: 3)).joins('JOIN lucky_number_seven ON lucky_number_seven.id = people.id').joins('JOIN lucky_number_three ON lucky_number_three.id = people.id')
17
- query.to_sql.must_equal 'WITH "lucky_number_seven" AS (SELECT "people".* FROM "people" WHERE "people"."lucky_number" = 7), "lucky_number_three" AS (SELECT "people".* FROM "people" WHERE "people"."lucky_number" = 3) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id JOIN lucky_number_three ON lucky_number_three.id = people.id'
17
+ query.to_sql.must_match(/WITH "lucky_number_seven" AS \(SELECT "people".* FROM "people"(\s+)WHERE "people"."lucky_number" = 7\), "lucky_number_three" AS \(SELECT "people".* FROM "people"(\s+)WHERE "people"."lucky_number" = 3\) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id JOIN lucky_number_three ON lucky_number_three.id = people.id/)
18
18
  end
19
19
 
20
20
  it 'generates an expression with recursive' do
21
21
  query = Person.with.recursive(lucky_number_seven: Person.where(lucky_number: 7)).joins('JOIN lucky_number_seven ON lucky_number_seven.id = people.id')
22
- query.to_sql.must_equal 'WITH RECURSIVE "lucky_number_seven" AS (SELECT "people".* FROM "people" WHERE "people"."lucky_number" = 7) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id'
22
+ query.to_sql.must_match(/WITH RECURSIVE "lucky_number_seven" AS \(SELECT "people".* FROM "people"(\s+)WHERE "people"."lucky_number" = 7\) SELECT "people".* FROM "people" JOIN lucky_number_seven ON lucky_number_seven.id = people.id/)
23
23
  end
24
24
 
25
25
  it 'accepts Arel::SelectMangers' do
@@ -34,7 +34,7 @@ describe 'Common Table Expression queries' do
34
34
  describe '.from_cte(common_table_expression_hash)' do
35
35
  it 'generates an expression with the CTE as the main table' do
36
36
  query = Person.from_cte('lucky_number_seven', Person.where(lucky_number: 7)).where(id: 5)
37
- query.to_sql.must_equal 'WITH "lucky_number_seven" AS (SELECT "people".* FROM "people" WHERE "people"."lucky_number" = 7) SELECT "lucky_number_seven".* FROM "lucky_number_seven" WHERE "lucky_number_seven"."id" = 5'
37
+ query.to_sql.must_match(/WITH "lucky_number_seven" AS \(SELECT "people".* FROM "people"(\s+)WHERE "people"."lucky_number" = 7\) SELECT "lucky_number_seven".* FROM "lucky_number_seven"(\s+)WHERE "lucky_number_seven"."id" = 5/)
38
38
  end
39
39
 
40
40
  it 'returns instances of the model' do
@@ -7,7 +7,7 @@ describe 'Contains queries' do
7
7
  let(:contains_array_regex) { %r{\"people\"\.\"tag_ids\" @> '\{1,2\}'} }
8
8
  let(:contains_hstore_regex) { %r{\"people\"\.\"data\" @> '\"nickname\"=>"Dan"'} }
9
9
  let(:contains_equals_regex) { %r{\"people\"\.\"ip\" >>= '127.0.0.1'} }
10
- let(:equality_regex) { %r{\"people\"\.\"tags\" = '\{\"working\"\}'} }
10
+ let(:equality_regex) { %r{\"people\"\.\"tags\" = '\{"?working"?\}'} }
11
11
 
12
12
  describe '.where.contained_within(:column, value)' do
13
13
  it 'generates the appropriate where clause' do
@@ -17,8 +17,8 @@ describe 'Ensure that we don\'t stomp on Active Record\'s queries' do
17
17
 
18
18
  describe '.where(joins: { column: [] })' do
19
19
  it 'generates IN clause for non array columns' do
20
- query = Person.joins(:hm_tags).where(tags: { id: ['working'] }).to_sql
21
- query.must_match /WHERE "tags"\."id" IN \('working'\)/
20
+ query = Person.joins(:hm_tags).where(tags: { id: [1,2,3] }).to_sql
21
+ query.must_match /WHERE "tags"\."id" IN \(1, 2, 3\)/
22
22
  end
23
23
  end
24
24
 
@@ -4,12 +4,12 @@ describe 'Window functions' do
4
4
  describe 'ranked' do
5
5
  it 'uses the order when no order is passed to ranked' do
6
6
  query = Person.order(lucky_number: :desc).ranked
7
- query.to_sql.must_equal 'SELECT "people".*, rank() OVER (ORDER BY "people"."lucky_number" DESC) FROM "people" ORDER BY "people"."lucky_number" DESC'
7
+ query.to_sql.must_match(/SELECT "people".*, rank\(\) OVER \(ORDER BY "people"."lucky_number" DESC\) FROM "people"(\s+)ORDER BY "people"."lucky_number" DESC/)
8
8
  end
9
9
 
10
10
  it 'uses the order when no order is passed to ranked, swapped calls' do
11
11
  query = Person.ranked.order(lucky_number: :desc)
12
- query.to_sql.must_equal 'SELECT "people".*, rank() OVER (ORDER BY "people"."lucky_number" DESC) FROM "people" ORDER BY "people"."lucky_number" DESC'
12
+ query.to_sql.must_match(/SELECT "people".*, rank\(\) OVER \(ORDER BY "people"."lucky_number" DESC\) FROM "people"(\s+)ORDER BY "people"."lucky_number" DESC/)
13
13
  end
14
14
 
15
15
  it 'uses the rank value when there is an order passed to it' do
@@ -29,7 +29,7 @@ describe 'Window functions' do
29
29
 
30
30
  it 'combines the order and rank' do
31
31
  query = Person.ranked(lucky_number: :desc).order(id: :asc)
32
- query.to_sql.must_equal 'SELECT "people".*, rank() OVER (ORDER BY "people"."lucky_number" DESC) FROM "people" ORDER BY "people"."id" ASC'
32
+ query.to_sql.must_match(/SELECT "people".*, rank\(\) OVER \(ORDER BY "people"."lucky_number" DESC\) FROM "people"(\s+)ORDER BY "people"."id" ASC/)
33
33
  end
34
34
 
35
35
  it 'executes the query with the rank' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postgres_ext
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.4.0.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan McClain
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-18 00:00:00.000000000 Z
11
+ date: 2014-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -168,6 +168,7 @@ files:
168
168
  - docs/querying.md
169
169
  - gemfiles/Gemfile.activerecord-4.0.x
170
170
  - gemfiles/Gemfile.activerecord-4.1.x
171
+ - gemfiles/Gemfile.activerecord-4.2.x
171
172
  - lib/postgres_ext.rb
172
173
  - lib/postgres_ext/active_record.rb
173
174
  - lib/postgres_ext/active_record/cte_proxy.rb
@@ -176,13 +177,15 @@ files:
176
177
  - lib/postgres_ext/active_record/relation/predicate_builder.rb
177
178
  - lib/postgres_ext/active_record/relation/query_methods.rb
178
179
  - lib/postgres_ext/arel.rb
180
+ - lib/postgres_ext/arel/4.1/predications.rb
181
+ - lib/postgres_ext/arel/4.1/visitors.rb
182
+ - lib/postgres_ext/arel/4.1/visitors/postgresql.rb
183
+ - lib/postgres_ext/arel/4.2/predications.rb
184
+ - lib/postgres_ext/arel/4.2/visitors.rb
185
+ - lib/postgres_ext/arel/4.2/visitors/postgresql.rb
179
186
  - lib/postgres_ext/arel/nodes.rb
180
187
  - lib/postgres_ext/arel/nodes/array_nodes.rb
181
188
  - lib/postgres_ext/arel/nodes/contained_within.rb
182
- - lib/postgres_ext/arel/predications.rb
183
- - lib/postgres_ext/arel/visitors.rb
184
- - lib/postgres_ext/arel/visitors/to_sql.rb
185
- - lib/postgres_ext/arel/visitors/visitor.rb
186
189
  - lib/postgres_ext/version.rb
187
190
  - postgres_ext.gemspec
188
191
  - test/arel/array_test.rb
@@ -208,9 +211,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
208
211
  version: '0'
209
212
  required_rubygems_version: !ruby/object:Gem::Requirement
210
213
  requirements:
211
- - - ">="
214
+ - - ">"
212
215
  - !ruby/object:Gem::Version
213
- version: '0'
216
+ version: 1.3.1
214
217
  requirements: []
215
218
  rubyforge_project:
216
219
  rubygems_version: 2.2.0
@@ -1,16 +0,0 @@
1
- require 'arel/visitors/to_sql'
2
-
3
- module Arel
4
- module Visitors
5
- class ToSql
6
- def visit_Array o, a
7
- column = a.relation.engine.connection.columns(a.relation.name).find { |col| col.name == a.name.to_s } if a
8
- if column && column.respond_to?(:array) && column.array
9
- quoted o, a
10
- else
11
- o.empty? ? 'NULL' : o.map { |x| visit x }.join(', ')
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,2 +0,0 @@
1
- require 'postgres_ext/arel/visitors/visitor'
2
- require 'postgres_ext/arel/visitors/to_sql'