torque-postgresql 2.1.1 → 2.2.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
  SHA256:
3
- metadata.gz: 1f5815c2fe0a3682db3ccb907a29aa171382bfa6d70ffcb58ef7e7bdbb840f65
4
- data.tar.gz: 32ae2c431e089c2c83d6c73be202ae86e79c129f1a79a08ca596ff87ffb231f6
3
+ metadata.gz: fcb69118c9cca1afe5ef21a9ed9b4cf98e9036108b739707a1e7f65058aaaf84
4
+ data.tar.gz: d7ca85705f802c0d15c0aabca99a9ec17b8403e60e912724b609d8b93cd7bd86
5
5
  SHA512:
6
- metadata.gz: 78c8d75e7b3534570caa48fe0a607417431813b04d842fb60b292d252d7ab0cb73ac16df488ba71242238f160ad746fe21ea162a988da0e11809c962e988b2d7
7
- data.tar.gz: a9450b4271ddf00be01ef4fb1a45365970310887d0179c457928f8ab36d0be9378e9738befe3797380ace0cbb66181f973f4d966cd39cd3fff7eceef7afb8082
6
+ metadata.gz: 0a48ed549030276f9a3e4c845454eb89cf97b8d0bf3d00cd5431abd85a7317f6031e4f997e86ae3657012ca9d4578598d612a14c531247575f1d0496bc629f6e
7
+ data.tar.gz: 50a1817ef37c0c8d98a71b92500a73b420f5dc27cf9d19afc6aeb8492e3d4582b69ee9ada91b281699987cc4bb33a9d1df584219073d432299ebad53a4a29513
@@ -42,6 +42,8 @@ module Torque
42
42
  m.register_type 'interval', OID::Interval.new
43
43
  m.register_type 'line', OID::Line.new
44
44
  m.register_type 'segment', OID::Segment.new
45
+
46
+ m.alias_type 'regclass', 'varchar'
45
47
  end
46
48
 
47
49
  # :nodoc:
@@ -123,7 +125,7 @@ module Torque
123
125
  SQL
124
126
 
125
127
  tables.map do |(table, refs)|
126
- [table, Coder.decode(refs)]
128
+ [table, PG::TextDecoder::Array.new.decode(refs)]
127
129
  end.to_h
128
130
  end
129
131
 
@@ -147,20 +149,6 @@ module Torque
147
149
  SQL
148
150
  end
149
151
 
150
- # Extracts the value from a PostgreSQL column default definition.
151
- def extract_value_from_default(default)
152
- case default
153
- # Array elements
154
- when /\AARRAY\[(.*)\]\z/
155
- # TODO: Improve this since it's not the most safe approach
156
- eval(default.gsub(/ARRAY|::\w+(\[\])?/, ''))
157
- else
158
- super
159
- end
160
- rescue SyntaxError
161
- # If somethin goes wrong with the eval, just return nil
162
- end
163
-
164
152
  end
165
153
  end
166
154
  end
@@ -8,9 +8,6 @@ module Torque
8
8
 
9
9
  attr_reader :name, :klass, :set_klass, :enum_klass
10
10
 
11
- # Delegate all Hash-like methods to the enum class
12
- delegate *(Array.public_instance_methods - Object.public_methods), to: :@klass
13
-
14
11
  def self.create(row, type_map)
15
12
  name = row['typname']
16
13
  oid = row['oid'].to_i
@@ -20,7 +20,7 @@ module Torque
20
20
  end
21
21
 
22
22
  def quote_default_expression(value, column)
23
- if value.class <= Array
23
+ if column.options.try(:[], :array) && value.class <= Array
24
24
  quote(value) + '::' + column.sql_type
25
25
  else
26
26
  super
@@ -70,7 +70,11 @@ module Torque
70
70
 
71
71
  # Returns all values that an enum type can have.
72
72
  def enum_values(name)
73
- select_values("SELECT unnest(enum_range(NULL::#{name}))", 'SCHEMA')
73
+ select_values(<<-SQL.squish, 'SCHEMA')
74
+ SELECT enumlabel FROM pg_enum
75
+ WHERE enumtypid = #{quote(name)}::regtype::oid
76
+ ORDER BY enumsortorder
77
+ SQL
74
78
  end
75
79
 
76
80
  # Rewrite the method that creates tables to easily accept extra options
@@ -15,7 +15,14 @@ module Torque
15
15
  include DatabaseStatements
16
16
  include SchemaStatements
17
17
 
18
- INJECT_WHERE_REGEX = /(DO UPDATE SET.*excluded\.[^ ]+) RETURNING/.freeze
18
+ # :nodoc:
19
+ class DeduplicatableArray < ::Array
20
+ def deduplicate
21
+ map { |value| -value }
22
+ end
23
+
24
+ alias :-@ :deduplicate
25
+ end
19
26
 
20
27
  # Get the current PostgreSQL version as a Gem Version.
21
28
  def version
@@ -29,19 +36,28 @@ module Torque
29
36
  super.merge(options.extract!(:inherits))
30
37
  end
31
38
 
32
- # Allow filtered bulk insert by adding the where clause. This method is only used by
33
- # +InsertAll+, so it somewhat safe to override it
39
+ # Allow filtered bulk insert by adding the where clause. This method is
40
+ # only used by +InsertAll+, so it somewhat safe to override it
34
41
  def build_insert_sql(insert)
35
42
  super.tap do |sql|
36
43
  if insert.update_duplicates? && insert.where_condition?
37
44
  if insert.returning
38
- sql.gsub!(INJECT_WHERE_REGEX, "\\1 WHERE #{insert.where} RETURNING")
45
+ sql.sub!(' RETURNING ', " WHERE #{insert.where} RETURNING ")
39
46
  else
40
47
  sql << " WHERE #{insert.where}"
41
48
  end
42
49
  end
43
50
  end
44
51
  end
52
+
53
+ # Extend the extract default value to support array
54
+ def extract_value_from_default(default)
55
+ return super unless Torque::PostgreSQL.config.use_extended_defaults
56
+ return super unless default&.match(/ARRAY\[(.*?)\](?:::"?([\w. ]+)"?(?:\[\])+)?$/)
57
+
58
+ arr = $1.split(/(?!\B\[[^\]]*), ?(?![^\[]*\]\B)/)
59
+ DeduplicatableArray.new(arr.map(&method(:extract_value_from_default)))
60
+ end
45
61
  end
46
62
 
47
63
  ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend Adapter
@@ -113,7 +113,7 @@ module Torque
113
113
  end
114
114
 
115
115
  def insert_record(record, *)
116
- super.tap do |saved|
116
+ (record.persisted? || super).tap do |saved|
117
117
  ids_rewriter(record.read_attribute(klass_attr), :<<) if saved
118
118
  end
119
119
  end
@@ -41,8 +41,8 @@ module Torque
41
41
  end
42
42
  end
43
43
 
44
- if PostgreSQL::AR610
45
- # This is how Rails 6.1 now load the records
44
+ if PostgreSQL::AR604
45
+ # This is how Rails 6.0.4 and 6.1 now load the records
46
46
  def load_records
47
47
  return super unless connected_through_array?
48
48
 
@@ -452,12 +452,8 @@ module Torque
452
452
  def instance_current_on?
453
453
  attr_value = threshold.present? ? method_names[:real] : attribute
454
454
  default_value = default.inspect
455
- [
456
- "return #{default_value} if #{attr_value}.nil?",
457
- "return #{default_value} if #{attr_value}.min.try(:infinite?)",
458
- "return #{default_value} if #{attr_value}.max.try(:infinite?)",
459
- "#{attr_value}.min < value && #{attr_value}.max > value",
460
- ].join("\n")
455
+
456
+ "#{attr_value}.nil? ? #{default_value} : #{attr_value}.include?(value)"
461
457
  end
462
458
 
463
459
  def instance_start
@@ -29,7 +29,7 @@ module Torque
29
29
  def include_on(klass, method_name = nil)
30
30
  method_name ||= Torque::PostgreSQL.config.enum.base_method
31
31
  Builder.include_on(klass, method_name, Builder::Enum) do |builder|
32
- defined_enums[builder.attribute.to_s] = builder.subtype
32
+ defined_enums[builder.attribute.to_s] = builder.subtype.klass
33
33
  end
34
34
  end
35
35
 
@@ -40,7 +40,7 @@ module Torque
40
40
 
41
41
  pg_class = ::Arel::Table.new('pg_class')
42
42
  source = ::Arel::Table.new(subclass.table_name, as: 'source')
43
- quoted_id = ::Arel::Nodes::Quoted.new(self.class.connection.quote(id))
43
+ quoted_id = ::Arel::Nodes::Quoted.new(id)
44
44
 
45
45
  query = ::Arel::SelectManager.new(pg_class)
46
46
  query.join(source).on(pg_class['oid'].eq(source['tableoid']))
@@ -5,6 +5,7 @@ module Torque
5
5
  include ActiveSupport::Configurable
6
6
 
7
7
  # Stores a version check for compatibility purposes
8
+ AR604 = (ActiveRecord.gem_version >= Gem::Version.new('6.0.4'))
8
9
  AR610 = (ActiveRecord.gem_version >= Gem::Version.new('6.1.0'))
9
10
 
10
11
  # Use the same logger as the Active Record one
@@ -25,6 +26,11 @@ module Torque
25
26
  # same configuration is set to true
26
27
  config.eager_load = false
27
28
 
29
+ # This allows default values to have extended values like arrays and casted
30
+ # values. Extended defaults are still experimental, so enable and test it
31
+ # before using it in prod
32
+ config.use_extended_defaults = false
33
+
28
34
  # Set a list of irregular model name when associated with table names
29
35
  config.irregular_models = {}
30
36
  def config.irregular_models=(hash)
@@ -146,7 +146,7 @@ module Torque
146
146
  auto_cast = _auto_cast_attribute.to_s
147
147
  record_class = _record_class_attribute.to_s
148
148
  return super unless attributes.key?(record_class) &&
149
- attributes.delete(auto_cast) && attributes[record_class] != table_name
149
+ attributes.delete(auto_cast) && attributes[record_class] != table_name
150
150
 
151
151
  klass = casted_dependents[attributes[record_class]]
152
152
  raise_unable_to_cast(attributes[record_class]) if klass.nil?
@@ -56,8 +56,10 @@ module Torque
56
56
  end
57
57
 
58
58
  ::ActiveRecord::Reflection.const_set(:BelongsToManyReflection, BelongsToManyReflection)
59
- ::ActiveRecord::Reflection::AssociationReflection::VALID_AUTOMATIC_INVERSE_MACROS
60
- .push(:belongs_to_many)
59
+
60
+ reflection_class = ::ActiveRecord::Reflection::AssociationReflection
61
+ reflection_class::VALID_AUTOMATIC_INVERSE_MACROS.push(:belongs_to_many) \
62
+ if reflection_class.const_defined?('VALID_AUTOMATIC_INVERSE_MACROS')
61
63
  end
62
64
  end
63
65
  end
@@ -5,6 +5,8 @@ module Torque
5
5
  module Relation
6
6
  module Inheritance
7
7
 
8
+ # REGCLASS = ::Arel.sql('tableoid').cast('regclass')
9
+
8
10
  # :nodoc:
9
11
  def cast_records_value; get_value(:cast_records); end
10
12
  # :nodoc:
@@ -44,14 +46,8 @@ module Torque
44
46
 
45
47
  # Like #cast_records, but modifies relation in place
46
48
  def cast_records!(*types, **options)
47
- record_class = self.class._record_class_attribute
48
-
49
- with!(record_class)
50
- if options[:filter]
51
- table = record_class.to_s.camelize.underscore
52
- where!(table => { record_class => types.map(&:table_name) })
53
- end
54
-
49
+ where!(regclass.cast(:varchar).in(types.map(&:table_name))) if options[:filter]
50
+ self.select_extra_values += [regclass.as(_record_class_attribute.to_s)]
55
51
  self.cast_records_value = (types.present? ? types : model.casted_dependents.values)
56
52
  self
57
53
  end
@@ -109,13 +105,12 @@ module Torque
109
105
  end
110
106
 
111
107
  def build_auto_caster_marker(arel, types)
112
- types = types.map(&:table_name)
113
- type_attribute = self.class._record_class_attribute.to_s
114
- auto_cast_attribute = self.class._auto_cast_attribute.to_s
108
+ attribute = regclass.cast(:varchar).in(types.map(&:table_name))
109
+ attribute.as(self.class._auto_cast_attribute.to_s)
110
+ end
115
111
 
116
- table = ::Arel::Table.new(type_attribute.camelize.underscore)
117
- column = table[type_attribute].in(types)
118
- ::Arel.sql(column.to_sql).as(auto_cast_attribute)
112
+ def regclass
113
+ arel_table['tableoid'].cast(:regclass)
119
114
  end
120
115
 
121
116
  end
@@ -117,7 +117,7 @@ module Torque
117
117
  # because the type mapper may add new methods to the model. This happens
118
118
  # for the given model Klass and its inheritances
119
119
  module Initializer
120
- def initialize(klass, *)
120
+ def initialize(klass, *, **)
121
121
  super
122
122
 
123
123
  klass.superclass.send(:relation) if klass.define_attribute_methods &&
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '2.1.1'
5
+ VERSION = '2.2.1'
6
6
  end
7
7
  end
@@ -23,7 +23,6 @@ require 'torque/postgresql/auxiliary_statement'
23
23
  require 'torque/postgresql/base'
24
24
  require 'torque/postgresql/inheritance'
25
25
  require 'torque/postgresql/insert_all'
26
- require 'torque/postgresql/coder'
27
26
  require 'torque/postgresql/migration'
28
27
  require 'torque/postgresql/relation'
29
28
  require 'torque/postgresql/reflection'
data/lib/torque/range.rb CHANGED
@@ -17,6 +17,4 @@ module Torque
17
17
  end
18
18
  alias_method :|, :union
19
19
  end
20
-
21
- ::Range.include(Range)
22
20
  end
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :item do
3
+ name { Faker::Lorem.sentence }
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class Item < ActiveRecord::Base
2
+ belongs_to_many :tags
3
+ end
@@ -0,0 +1,3 @@
1
+ class Question < ActiveRecord::Base
2
+ self.implicit_order_column = 'created_at'
3
+ end
@@ -0,0 +1,2 @@
1
+ class QuestionSelect < Question
2
+ end
data/spec/schema.rb CHANGED
@@ -11,13 +11,14 @@
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
13
  begin
14
- version = 63
14
+ version = 73
15
15
 
16
16
  raise SystemExit if ActiveRecord::Migrator.current_version == version
17
17
  ActiveRecord::Schema.define(version: version) do
18
18
  self.verbose = false
19
19
 
20
20
  # These are extensions that must be enabled in order to support this database
21
+ enable_extension "pgcrypto"
21
22
  enable_extension "plpgsql"
22
23
 
23
24
  # These are user-defined types used on this database
@@ -102,6 +103,13 @@ begin
102
103
  t.index ["author_id"], name: "index_posts_on_author_id", using: :btree
103
104
  end
104
105
 
106
+ create_table "items", force: :cascade do |t|
107
+ t.string "name"
108
+ t.bigint "tag_ids", array: true, default: "{1}"
109
+ t.datetime "created_at", null: false
110
+ t.datetime "updated_at", null: false
111
+ end
112
+
105
113
  create_table "users", force: :cascade do |t|
106
114
  t.string "name", null: false
107
115
  t.enum "role", subtype: :roles, default: :visitor
@@ -118,6 +126,12 @@ begin
118
126
  t.datetime "updated_at", null: false
119
127
  end
120
128
 
129
+ create_table "questions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
130
+ t.string "title"
131
+ t.datetime "created_at", null: false
132
+ t.datetime "updated_at", null: false
133
+ end
134
+
121
135
  create_table "activity_books", force: :cascade, inherits: :activities do |t|
122
136
  t.text "description"
123
137
  t.string "url"
@@ -132,6 +146,10 @@ begin
132
146
 
133
147
  create_table "activity_post_samples", force: :cascade, inherits: :activity_posts
134
148
 
149
+ create_table "question_selects", force: :cascade, inherits: :questions do |t|
150
+ t.string "options", array: true
151
+ end
152
+
135
153
  # create_table "activity_blanks", force: :cascade, inherits: :activities
136
154
 
137
155
  # create_table "activity_images", force: :cascade, inherits: [:activities, :images]
@@ -48,6 +48,36 @@ RSpec.describe 'Arel' do
48
48
  end
49
49
  end
50
50
 
51
+ context 'on default value' do
52
+ let(:connection) { ActiveRecord::Base.connection }
53
+
54
+ before(:context) { Torque::PostgreSQL.config.use_extended_defaults = true }
55
+ after(:context) { Torque::PostgreSQL.config.use_extended_defaults = false }
56
+ after { Author.reset_column_information }
57
+
58
+ it 'does not break jsonb' do
59
+ expect { connection.add_column(:authors, :profile, :jsonb, default: []) }.not_to raise_error
60
+ expect(Author.columns_hash['profile'].default).to eq('[]')
61
+ end
62
+
63
+ it 'works properly when column is an array' do
64
+ expect { connection.add_column(:authors, :tag_ids, :bigint, array: true, default: []) }.not_to raise_error
65
+ expect(Author.columns_hash['tag_ids'].default).to eq([])
66
+ end
67
+
68
+ it 'works with an array with enum values' do
69
+ value = ['visitor', 'assistant']
70
+ expect { connection.add_column(:authors, :roles, :roles, array: true, default: value) }.not_to raise_error
71
+ expect(Author.columns_hash['roles'].default).to eq(value)
72
+ end
73
+
74
+ it 'works with multi dimentional array' do
75
+ value = [['1', '2'], ['3', '4']]
76
+ expect { connection.add_column(:authors, :tag_ids, :string, array: true, default: value) }.not_to raise_error
77
+ expect(Author.columns_hash['tag_ids'].default).to eq(value)
78
+ end
79
+ end
80
+
51
81
  context 'on cast' do
52
82
  it 'provides an array method' do
53
83
  sample1 = ::Arel.array(1, 2, 3, 4)
@@ -147,6 +147,22 @@ RSpec.describe 'BelongsToMany' do
147
147
  expect(record.tags.count).to be_eql(5)
148
148
  end
149
149
 
150
+ it 'does not trigger after commit on the associated record' do
151
+ called = false
152
+
153
+ tag = FactoryBot.create(:tag)
154
+ Tag.after_commit { called = true }
155
+
156
+ expect(called).to be_falsey
157
+
158
+ subject.tags << tag
159
+
160
+ expect(subject.tag_ids).to be_eql([tag.id])
161
+ expect(called).to be_falsey
162
+
163
+ Tag.reset_callbacks(:commit)
164
+ end
165
+
150
166
  it 'can build an associated record' do
151
167
  record = subject.tags.build(name: 'Test')
152
168
  expect(record).to be_a(other)
@@ -330,17 +346,11 @@ RSpec.describe 'BelongsToMany' do
330
346
  end
331
347
 
332
348
  context 'When the attribute has a default value' do
333
- after(:all) { Video.reset_column_information }
334
- let(:sql) { %{ALTER TABLE "videos" ALTER COLUMN "tag_ids" SET DEFAULT '{}'::bigint[]} }
335
-
336
- before do
337
- Video.connection.execute(sql)
338
- Video.reset_column_information
339
- end
349
+ subject { FactoryBot.create(:item) }
340
350
 
341
351
  it 'will always return the column default value' do
342
352
  expect(subject.tag_ids).to be_a(Array)
343
- expect(subject.tag_ids).to be_empty
353
+ expect(subject.tag_ids).to be_eql([1])
344
354
  end
345
355
 
346
356
  it 'will keep the value as an array even when the association is cleared' do
@@ -349,12 +359,12 @@ RSpec.describe 'BelongsToMany' do
349
359
 
350
360
  subject.reload
351
361
  expect(subject.tag_ids).to be_a(Array)
352
- expect(subject.tag_ids).not_to be_empty
362
+ expect(subject.tag_ids).not_to be_eql([1, *records.map(&:id)])
353
363
 
354
364
  subject.tags.clear
355
365
  subject.reload
356
366
  expect(subject.tag_ids).to be_a(Array)
357
- expect(subject.tag_ids).to be_empty
367
+ expect(subject.tag_ids).to be_eql([1])
358
368
  end
359
369
  end
360
370
 
@@ -266,6 +266,15 @@ RSpec.describe 'Period' do
266
266
 
267
267
  instance.period = 4.hour.from_now.utc..6.hour.from_now.utc
268
268
  expect(instance).not_to be_current_period
269
+
270
+ instance.period = [nil, 4.hours.ago.utc]
271
+ expect(instance).not_to be_current_period
272
+
273
+ instance.period = [4.hours.from_now.utc, nil]
274
+ expect(instance).not_to be_current_period
275
+
276
+ instance.period = [nil, nil]
277
+ expect(instance).to be_current_period
269
278
  end
270
279
 
271
280
  it 'checks fro current based on a value' do
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- RSpec.describe 'Range' do
3
+ RSpec.xdescribe 'Range' do
4
4
  let(:sample) { (5..15) }
5
5
 
6
6
  it 'has intersection' do
@@ -292,14 +292,12 @@ RSpec.describe 'TableInheritance' do
292
292
  end
293
293
 
294
294
  it 'adds all statements to load all the necessary records' do
295
- result = 'WITH "record_class" AS (SELECT "pg_class"."oid", "pg_class"."relname" AS _record_class FROM "pg_class")'
296
- result << ' SELECT "activities".*, "record_class"."_record_class", "i_0"."description"'
295
+ result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class, "i_0"."description"'
297
296
  result << ', COALESCE("i_0"."url", "i_1"."url", "i_2"."url") AS url, "i_0"."activated" AS activity_books__activated'
298
297
  result << ', "i_1"."activated" AS activity_posts__activated, "i_2"."activated" AS activity_post_samples__activated'
299
298
  result << ', COALESCE("i_1"."file", "i_2"."file") AS file, COALESCE("i_1"."post_id", "i_2"."post_id") AS post_id'
300
- result << ", \"record_class\".\"_record_class\" IN ('activity_books', 'activity_posts', 'activity_post_samples') AS _auto_cast"
299
+ result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books', 'activity_posts', 'activity_post_samples') AS _auto_cast"
301
300
  result << ' FROM "activities"'
302
- result << ' INNER JOIN "record_class" ON "record_class"."oid" = "activities"."tableoid"'
303
301
  result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
304
302
  result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
305
303
  result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
@@ -307,33 +305,27 @@ RSpec.describe 'TableInheritance' do
307
305
  end
308
306
 
309
307
  it 'can be have simplefied joins' do
310
- result = 'WITH "record_class" AS (SELECT "pg_class"."oid", "pg_class"."relname" AS _record_class FROM "pg_class")'
311
- result << ' SELECT "activities".*, "record_class"."_record_class"'
308
+ result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class'
312
309
  result << ', "i_0"."description", "i_0"."url", "i_0"."activated"'
313
- result << ", \"record_class\".\"_record_class\" IN ('activity_books') AS _auto_cast"
310
+ result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books') AS _auto_cast"
314
311
  result << ' FROM "activities"'
315
- result << ' INNER JOIN "record_class" ON "record_class"."oid" = "activities"."tableoid"'
316
312
  result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
317
313
  expect(base.cast_records(child).all.to_sql).to eql(result)
318
314
  end
319
315
 
320
316
  it 'can be filtered by record type' do
321
- result = 'WITH "record_class" AS (SELECT "pg_class"."oid", "pg_class"."relname" AS _record_class FROM "pg_class")'
322
- result << ' SELECT "activities".*, "record_class"."_record_class"'
317
+ result = 'SELECT "activities".*, "activities"."tableoid"::regclass AS _record_class'
323
318
  result << ', "i_0"."description", "i_0"."url", "i_0"."activated"'
324
- result << ", \"record_class\".\"_record_class\" IN ('activity_books') AS _auto_cast"
319
+ result << ", \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books') AS _auto_cast"
325
320
  result << ' FROM "activities"'
326
- result << ' INNER JOIN "record_class" ON "record_class"."oid" = "activities"."tableoid"'
327
321
  result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
328
- result << " WHERE \"record_class\".\"_record_class\" = 'activity_books'"
322
+ result << " WHERE \"activities\".\"tableoid\"::regclass::varchar IN ('activity_books')"
329
323
  expect(base.cast_records(child, filter: true).all.to_sql).to eql(result)
330
324
  end
331
325
 
332
326
  it 'works with count and does not add extra columns' do
333
- result = 'WITH "record_class" AS (SELECT "pg_class"."oid", "pg_class"."relname" AS _record_class FROM "pg_class")'
334
- result << ' SELECT COUNT(*)'
327
+ result = 'SELECT COUNT(*)'
335
328
  result << ' FROM "activities"'
336
- result << ' INNER JOIN "record_class" ON "record_class"."oid" = "activities"."tableoid"'
337
329
  result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
338
330
  result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
339
331
  result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
@@ -342,10 +334,8 @@ RSpec.describe 'TableInheritance' do
342
334
  end
343
335
 
344
336
  it 'works with sum and does not add extra columns' do
345
- result = 'WITH "record_class" AS (SELECT "pg_class"."oid", "pg_class"."relname" AS _record_class FROM "pg_class")'
346
- result << ' SELECT SUM("activities"."id")'
337
+ result = 'SELECT SUM("activities"."id")'
347
338
  result << ' FROM "activities"'
348
- result << ' INNER JOIN "record_class" ON "record_class"."oid" = "activities"."tableoid"'
349
339
  result << ' LEFT OUTER JOIN "activity_books" "i_0" ON "activities"."id" = "i_0"."id"'
350
340
  result << ' LEFT OUTER JOIN "activity_posts" "i_1" ON "activities"."id" = "i_1"."id"'
351
341
  result << ' LEFT OUTER JOIN "activity_post_samples" "i_2" ON "activities"."id" = "i_2"."id"'
@@ -387,10 +377,10 @@ RSpec.describe 'TableInheritance' do
387
377
  base.create(title: 'Activity test')
388
378
  child.create(title: 'Activity book')
389
379
  other.create(name: 'An author name')
380
+ base.instance_variable_set(:@casted_dependents, nil)
390
381
  end
391
382
 
392
383
  it 'does not affect normal records' do
393
- base.instance_variable_set(:@casted_dependents, {})
394
384
  expect(base.first.cast_record).to be_a(base)
395
385
  expect(child.first.cast_record).to be_a(child)
396
386
  expect(other.first.cast_record).to be_a(other)
@@ -408,9 +398,28 @@ RSpec.describe 'TableInheritance' do
408
398
  end
409
399
 
410
400
  it 'does trigger record casting when accessed through inheritance' do
411
- base.instance_variable_set(:@casted_dependents, nil)
412
401
  expect(base.second.cast_record).to eql(child.first)
413
402
  end
403
+
404
+ context 'using uuid' do
405
+ let(:base) { Question }
406
+ let(:child) { QuestionSelect }
407
+
408
+ before :each do
409
+ base.create(title: 'Simple question')
410
+ child.create(title: 'Select question')
411
+ base.instance_variable_set(:@casted_dependents, nil)
412
+ end
413
+
414
+ it 'does not affect normal records' do
415
+ expect(base.first.cast_record).to be_a(base)
416
+ expect(child.first.cast_record).to be_a(child)
417
+ end
418
+
419
+ it 'does trigger record casting when accessed through inheritance' do
420
+ expect(base.second.cast_record).to eql(child.first)
421
+ end
422
+ end
414
423
  end
415
424
  end
416
425
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: torque-postgresql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-04 00:00:00.000000000 Z
11
+ date: 2021-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -215,7 +215,6 @@ files:
215
215
  - lib/torque/postgresql/auxiliary_statement.rb
216
216
  - lib/torque/postgresql/auxiliary_statement/settings.rb
217
217
  - lib/torque/postgresql/base.rb
218
- - lib/torque/postgresql/coder.rb
219
218
  - lib/torque/postgresql/collector.rb
220
219
  - lib/torque/postgresql/config.rb
221
220
  - lib/torque/postgresql/geometry_builder.rb
@@ -243,6 +242,7 @@ files:
243
242
  - spec/en.yml
244
243
  - spec/factories/authors.rb
245
244
  - spec/factories/comments.rb
245
+ - spec/factories/item.rb
246
246
  - spec/factories/posts.rb
247
247
  - spec/factories/tags.rb
248
248
  - spec/factories/texts.rb
@@ -260,7 +260,10 @@ files:
260
260
  - spec/models/course.rb
261
261
  - spec/models/geometry.rb
262
262
  - spec/models/guest_comment.rb
263
+ - spec/models/item.rb
263
264
  - spec/models/post.rb
265
+ - spec/models/question.rb
266
+ - spec/models/question_select.rb
264
267
  - spec/models/tag.rb
265
268
  - spec/models/text.rb
266
269
  - spec/models/time_keeper.rb
@@ -271,7 +274,6 @@ files:
271
274
  - spec/tests/arel_spec.rb
272
275
  - spec/tests/auxiliary_statement_spec.rb
273
276
  - spec/tests/belongs_to_many_spec.rb
274
- - spec/tests/coder_spec.rb
275
277
  - spec/tests/collector_spec.rb
276
278
  - spec/tests/distinct_on_spec.rb
277
279
  - spec/tests/enum_set_spec.rb
@@ -305,7 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
305
307
  - !ruby/object:Gem::Version
306
308
  version: 1.8.11
307
309
  requirements: []
308
- rubygems_version: 3.1.4
310
+ rubygems_version: 3.2.14
309
311
  signing_key:
310
312
  specification_version: 4
311
313
  summary: ActiveRecord extension to access PostgreSQL advanced resources
@@ -326,6 +328,9 @@ test_files:
326
328
  - spec/models/tag.rb
327
329
  - spec/models/time_keeper.rb
328
330
  - spec/models/video.rb
331
+ - spec/models/item.rb
332
+ - spec/models/question.rb
333
+ - spec/models/question_select.rb
329
334
  - spec/factories/authors.rb
330
335
  - spec/factories/comments.rb
331
336
  - spec/factories/posts.rb
@@ -333,13 +338,11 @@ test_files:
333
338
  - spec/factories/texts.rb
334
339
  - spec/factories/users.rb
335
340
  - spec/factories/videos.rb
341
+ - spec/factories/item.rb
336
342
  - spec/tests/geometric_builder_spec.rb
337
- - spec/tests/range_spec.rb
338
- - spec/tests/arel_spec.rb
339
- - spec/tests/insert_all_spec.rb
340
343
  - spec/tests/enum_spec.rb
341
- - spec/tests/period_spec.rb
342
- - spec/tests/coder_spec.rb
344
+ - spec/tests/range_spec.rb
345
+ - spec/tests/table_inheritance_spec.rb
343
346
  - spec/tests/collector_spec.rb
344
347
  - spec/tests/distinct_on_spec.rb
345
348
  - spec/tests/interval_spec.rb
@@ -347,10 +350,12 @@ test_files:
347
350
  - spec/tests/quoting_spec.rb
348
351
  - spec/tests/relation_spec.rb
349
352
  - spec/tests/auxiliary_statement_spec.rb
353
+ - spec/tests/belongs_to_many_spec.rb
350
354
  - spec/tests/enum_set_spec.rb
351
355
  - spec/tests/has_many_spec.rb
352
- - spec/tests/belongs_to_many_spec.rb
353
- - spec/tests/table_inheritance_spec.rb
356
+ - spec/tests/insert_all_spec.rb
357
+ - spec/tests/arel_spec.rb
358
+ - spec/tests/period_spec.rb
354
359
  - spec/mocks/cache_query.rb
355
360
  - spec/mocks/create_table.rb
356
361
  - spec/en.yml
@@ -1,133 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Torque
4
- module PostgreSQL
5
- module Coder
6
-
7
- # This class represents an Record to be encoded, instead of a literal Array
8
- Record = Class.new(Array)
9
-
10
- class << self
11
-
12
- NEED_QUOTE_FOR = /[\\"(){}, \t\n\r\v\f]/m
13
- DELIMITER = ','
14
-
15
- # This method replace the +read_array+ method from PG gem
16
- # See https://github.com/ged/ruby-pg/blob/master/ext/pg_text_decoder.c#L177
17
- # for more information
18
- def decode(value)
19
- # TODO: Use StringScanner
20
- # See http://ruby-doc.org/stdlib-1.9.3/libdoc/strscan/rdoc/StringScanner.html
21
- _decode(::StringIO.new(value))
22
- end
23
-
24
- # This method replace the ++ method from PG gem
25
- # See https://github.com/ged/ruby-pg/blob/master/ext/pg_text_encoder.c#L398
26
- # for more information
27
- def encode(value)
28
- _encode(value)
29
- end
30
-
31
- private
32
-
33
- def _decode(stream)
34
- quoted = 0
35
- escaped = false
36
- result = []
37
- part = String.new
38
-
39
- # Always start getting the non-collection character, the second char
40
- stream.getc if stream.pos == 0
41
-
42
- # Check for an empty list
43
- return result if %w[} )].include?(stream.getc)
44
-
45
- # If it's not an empty list, return one position before iterating
46
- stream.pos -= 1
47
- stream.each_char do |c|
48
-
49
- case
50
- when quoted < 1
51
- case
52
- when c == DELIMITER, c == '}', c == ')'
53
-
54
- unless escaped
55
- # Non-quoted empty string or NULL as extense
56
- part = nil if quoted == 0 && ( part.length == 0 || part == 'NULL' )
57
- result << part
58
- end
59
-
60
- return result unless c == DELIMITER
61
-
62
- escaped = false
63
- quoted = 0
64
- part = String.new
65
-
66
- when c == '"'
67
- quoted = 1
68
- when c == '{', c == '('
69
- result << _decode(stream)
70
- escaped = true
71
- else
72
- part << c
73
- end
74
- when escaped
75
- escaped = false
76
- part << c
77
- when c == '\\'
78
- escaped = true
79
- when c == '"'
80
- if stream.getc == '"'
81
- part << c
82
- else
83
- stream.pos -= 1
84
- quoted = -1
85
- end
86
- else
87
- if ( c == '"' || c == "'" ) && stream.getc != c
88
- stream.pos -= 1
89
- quoted = -1
90
- else
91
- part << c
92
- end
93
- end
94
-
95
- end
96
- end
97
-
98
- def _encode(list)
99
- is_record = list.is_a?(Record)
100
- list.map! do |part|
101
- case part
102
- when NilClass
103
- is_record ? '' : 'NULL'
104
- when Array
105
- _encode(part)
106
- else
107
- _quote(part.to_s)
108
- end
109
- end
110
-
111
- result = is_record ? '(%s)' : '{%s}'
112
- result % list.join(DELIMITER)
113
- end
114
-
115
- def _quote(string)
116
- len = string.length
117
-
118
- # Fast results
119
- return '""' if len == 0
120
- return '"NULL"' if len == 4 && string == 'NULL'
121
-
122
- # Check if the string don't need quotes
123
- return string unless string =~ NEED_QUOTE_FOR
124
-
125
- # Use the original string escape function
126
- PG::Connection.escape_string(string).inspect
127
- end
128
-
129
- end
130
-
131
- end
132
- end
133
- end
@@ -1,367 +0,0 @@
1
- require 'spec_helper'
2
-
3
- RSpec.describe 'Complex coder', type: :helper do
4
- let(:coder) { Torque::PostgreSQL::Coder }
5
-
6
- context 'on decode' do
7
-
8
- context 'one dimensional arrays' do
9
- it 'returns an empty array' do
10
- expect(coder.decode(%[{}])).to eql []
11
- end
12
-
13
- it 'returns an array of strings' do
14
- expect(coder.decode(%[{1,2,3}])).to eql ['1','2','3']
15
- end
16
-
17
- it 'returns an array of strings, with nils replacing NULL characters' do
18
- expect(coder.decode(%[{1,,NULL}])).to eql ['1',nil,nil]
19
- end
20
-
21
- it 'returns an array with the word NULL' do
22
- expect(coder.decode(%[{1,"NULL",3}])).to eql ['1','NULL','3']
23
- end
24
-
25
- it 'returns an array of strings when containing commas in a quoted string' do
26
- expect(coder.decode(%[{1,"2,3",4}])).to eql ['1','2,3','4']
27
- end
28
-
29
- it 'returns an array of strings when containing an escaped quote' do
30
- expect(coder.decode(%[{1,"2\\",3",4}])).to eql ['1','2",3','4']
31
- end
32
-
33
- it 'returns an array of strings when containing an escaped backslash' do
34
- expect(coder.decode(%[{1,"2\\\\",3,4}])).to eql ['1','2\\','3','4']
35
- expect(coder.decode(%[{1,"2\\\\\\",3",4}])).to eql ['1','2\\",3','4']
36
- end
37
-
38
- it 'returns an array containing empty strings' do
39
- expect(coder.decode(%[{1,"",3,""}])).to eql ['1', '', '3', '']
40
- end
41
-
42
- it 'returns an array containing unicode strings' do
43
- expect(coder.decode(%[{"Paragraph 399(b)(i) – “valid leave” – meaning"}])).to eq(['Paragraph 399(b)(i) – “valid leave” – meaning'])
44
- end
45
- end
46
-
47
- context 'two dimensional arrays' do
48
- it 'returns an empty array' do
49
- expect(coder.decode(%[{{}}])).to eql [[]]
50
- expect(coder.decode(%[{{},{}}])).to eql [[],[]]
51
- end
52
-
53
- it 'returns an array of strings with a sub array' do
54
- expect(coder.decode(%[{1,{2,3},4}])).to eql ['1',['2','3'],'4']
55
- end
56
-
57
- it 'returns an array of strings with a sub array' do
58
- expect(coder.decode(%[{1,{"2,3"},4}])).to eql ['1',['2,3'],'4']
59
- end
60
-
61
- it 'returns an array of strings with a sub array and a quoted }' do
62
- expect(coder.decode(%[{1,{"2,}3",,NULL},4}])).to eql ['1',['2,}3',nil,nil],'4']
63
- end
64
-
65
- it 'returns an array of strings with a sub array and a quoted {' do
66
- expect(coder.decode(%[{1,{"2,{3"},4}])).to eql ['1',['2,{3'],'4']
67
- end
68
-
69
- it 'returns an array of strings with a sub array and a quoted { and escaped quote' do
70
- expect(coder.decode(%[{1,{"2\\",{3"},4}])).to eql ['1',['2",{3'],'4']
71
- end
72
-
73
- it 'returns an array of strings with a sub array with empty strings' do
74
- expect(coder.decode(%[{1,{""},4,{""}}])).to eql ['1',[''],'4',['']]
75
- end
76
- end
77
-
78
- context 'three dimensional arrays' do
79
- it 'returns an empty array' do
80
- expect(coder.decode(%[{{{}}}])).to eql [[[]]]
81
- expect(coder.decode(%[{{{},{}},{{},{}}}])).to eql [[[],[]],[[],[]]]
82
- end
83
-
84
- it 'returns an array of strings with sub arrays' do
85
- expect(coder.decode(%[{1,{2,{3,4}},{NULL,,6},7}])).to eql ['1',['2',['3','4']],[nil,nil,'6'],'7']
86
- end
87
- end
88
-
89
- context 'record syntax' do
90
- it 'returns an empty array' do
91
- expect(coder.decode(%[()])).to eql []
92
- end
93
-
94
- it 'returns an array of strings' do
95
- expect(coder.decode(%[(1,2,3)])).to eql ['1','2','3']
96
- end
97
-
98
- it 'returns an array of strings, with nils replacing NULL characters' do
99
- expect(coder.decode(%[(1,,NULL)])).to eql ['1',nil,nil]
100
- end
101
-
102
- it 'returns an array with the word NULL' do
103
- expect(coder.decode(%[(1,"NULL",3)])).to eql ['1','NULL','3']
104
- end
105
-
106
- it 'returns an array of strings when containing commas in a quoted string' do
107
- expect(coder.decode(%[(1,"2,3",4)])).to eql ['1','2,3','4']
108
- end
109
-
110
- it 'returns an array of strings when containing an escaped quote' do
111
- expect(coder.decode(%[(1,"2\\",3",4)])).to eql ['1','2",3','4']
112
- end
113
-
114
- it 'returns an array of strings when containing an escaped backslash' do
115
- expect(coder.decode(%[(1,"2\\\\",3,4)])).to eql ['1','2\\','3','4']
116
- expect(coder.decode(%[(1,"2\\\\\\",3",4)])).to eql ['1','2\\",3','4']
117
- end
118
-
119
- it 'returns an array containing empty strings' do
120
- expect(coder.decode(%[(1,"",3,"")])).to eql ['1', '', '3', '']
121
- end
122
-
123
- it 'returns an array containing unicode strings' do
124
- expect(coder.decode(%[("Paragraph 399(b)(i) – “valid leave” – meaning")])).to eq(['Paragraph 399(b)(i) – “valid leave” – meaning'])
125
- end
126
- end
127
-
128
- context 'array of records' do
129
- it 'returns an empty array' do
130
- expect(coder.decode(%[{()}])).to eql [[]]
131
- expect(coder.decode(%[{(),()}])).to eql [[],[]]
132
- end
133
-
134
- it 'returns an array of strings with a sub array' do
135
- expect(coder.decode(%[{1,(2,3),4}])).to eql ['1',['2','3'],'4']
136
- end
137
-
138
- it 'returns an array of strings with a sub array' do
139
- expect(coder.decode(%[{1,("2,3"),4}])).to eql ['1',['2,3'],'4']
140
- end
141
-
142
- it 'returns an array of strings with a sub array and a quoted }' do
143
- expect(coder.decode(%[{1,("2,}3",,NULL),4}])).to eql ['1',['2,}3',nil,nil],'4']
144
- end
145
-
146
- it 'returns an array of strings with a sub array and a quoted {' do
147
- expect(coder.decode(%[{1,("2,{3"),4}])).to eql ['1',['2,{3'],'4']
148
- end
149
-
150
- it 'returns an array of strings with a sub array and a quoted { and escaped quote' do
151
- expect(coder.decode(%[{1,("2\\",{3"),4}])).to eql ['1',['2",{3'],'4']
152
- end
153
-
154
- it 'returns an array of strings with a sub array with empty strings' do
155
- expect(coder.decode(%[{1,(""),4,("")}])).to eql ['1',[''],'4',['']]
156
- end
157
- end
158
-
159
- context 'mix of record and array' do
160
- it 'returns an empty array' do
161
- expect(coder.decode(%[({()})])).to eql [[[]]]
162
- expect(coder.decode(%[{({},{}),{(),{}}}])).to eql [[[],[]],[[],[]]]
163
- end
164
-
165
- it 'returns an array of strings with sub arrays' do
166
- expect(coder.decode(%[{1,(2,{3,4}),(NULL,,6),7}])).to eql ['1',['2',['3','4']],[nil,nil,'6'],'7']
167
- end
168
- end
169
-
170
- context 'record complex sample' do
171
- it 'may have double double quotes translate to single double quotes' do
172
- expect(coder.decode(%[("Test with double "" quoutes")])).to eql ['Test with double " quoutes']
173
- end
174
-
175
- it 'double double quotes may occur any number of times' do
176
- expect(coder.decode(%[("Only one ""","Now "" two "".",""",""{""}","""""")])).to eql ['Only one "', 'Now " two ".', '","{"}', '""']
177
- end
178
-
179
- it 'may have any kind of value' do
180
- expect(coder.decode(%[(String,123456,false,true,"2016-01-01 12:00:00",{1,2,3})])).to eql ['String', '123456', 'false', 'true', '2016-01-01 12:00:00', ['1', '2', '3']]
181
- end
182
- end
183
-
184
- end
185
-
186
- context 'on encode' do
187
- let(:record) { Torque::PostgreSQL::Coder::Record }
188
-
189
- context 'one dimensional arrays' do
190
- it 'receives an empty array' do
191
- expect(coder.encode([])).to eql %[{}]
192
- end
193
-
194
- it 'receives an array of strings' do
195
- expect(coder.encode(['1','2','3'])).to eql %[{1,2,3}]
196
- end
197
-
198
- it 'receives an array of strings, with nils replacing NULL characters' do
199
- expect(coder.encode(['1',nil,nil])).to eql %[{1,NULL,NULL}]
200
- end
201
-
202
- it 'receives an array with the word NULL' do
203
- expect(coder.encode(['1','NULL','3'])).to eql %[{1,"NULL",3}]
204
- end
205
-
206
- it 'receives an array of strings when containing commas in a quoted string' do
207
- expect(coder.encode(['1','2,3','4'])).to eql %[{1,"2,3",4}]
208
- end
209
-
210
- it 'receives an array of strings when containing an escaped quote' do
211
- expect(coder.encode(['1','2",3','4'])).to eql %[{1,"2\\",3",4}]
212
- end
213
-
214
- it 'receives an array of strings when containing an escaped backslash' do
215
- expect(coder.encode(['1','2\\','3','4'])).to eql %[{1,"2\\\\",3,4}]
216
- expect(coder.encode(['1','2\\",3','4'])).to eql %[{1,"2\\\\\\",3",4}]
217
- end
218
-
219
- it 'receives an array containing empty strings' do
220
- expect(coder.encode(['1', '', '3', ''])).to eql %[{1,"",3,""}]
221
- end
222
-
223
- it 'receives an array containing unicode strings' do
224
- expect(coder.encode(['Paragraph 399(b)(i) – “valid leave” – meaning'])).to eql %[{"Paragraph 399(b)(i) – “valid leave” – meaning"}]
225
- end
226
- end
227
-
228
- context 'two dimensional arrays' do
229
- it 'receives an empty array' do
230
- expect(coder.encode([[]])).to eql %[{{}}]
231
- expect(coder.encode([[],[]])).to eql %[{{},{}}]
232
- end
233
-
234
- it 'receives an array of strings with a sub array' do
235
- expect(coder.encode(['1',['2','3'],'4'])).to eql %[{1,{2,3},4}]
236
- end
237
-
238
- it 'receives an array of strings with a sub array' do
239
- expect(coder.encode(['1',['2,3'],'4'])).to eql %[{1,{"2,3"},4}]
240
- end
241
-
242
- it 'receives an array of strings with a sub array and a quoted }' do
243
- expect(coder.encode(['1',['2,}3',nil,nil],'4'])).to eql %[{1,{"2,}3",NULL,NULL},4}]
244
- end
245
-
246
- it 'receives an array of strings with a sub array and a quoted {' do
247
- expect(coder.encode(['1',['2,{3'],'4'])).to eql %[{1,{"2,{3"},4}]
248
- end
249
-
250
- it 'receives an array of strings with a sub array and a quoted { and escaped quote' do
251
- expect(coder.encode(['1',['2",{3'],'4'])).to eql %[{1,{"2\\",{3"},4}]
252
- end
253
-
254
- it 'receives an array of strings with a sub array with empty strings' do
255
- expect(coder.encode(['1',[''],'4',['']])).to eql %[{1,{""},4,{""}}]
256
- end
257
- end
258
-
259
- context 'three dimensional arrays' do
260
- it 'receives an empty array' do
261
- expect(coder.encode([[[]]])).to eql %[{{{}}}]
262
- expect(coder.encode([[[],[]],[[],[]]])).to eql %[{{{},{}},{{},{}}}]
263
- end
264
-
265
- it 'receives an array of strings with sub arrays' do
266
- expect(coder.encode(['1',['2',['3','4']],[nil,nil,'6'],'7'])).to eql %[{1,{2,{3,4}},{NULL,NULL,6},7}]
267
- end
268
- end
269
-
270
- context 'record syntax' do
271
- it 'receives an empty array' do
272
- expect(coder.encode( record.new )).to eql %[()]
273
- end
274
-
275
- it 'receives an array of strings' do
276
- expect(coder.encode( record.new(['1','2','3']) )).to eql %[(1,2,3)]
277
- end
278
-
279
- it 'receives an array of strings, with nils replacing NULL characters' do
280
- expect(coder.encode( record.new(['1',nil,nil]) )).to eql %[(1,,)]
281
- end
282
-
283
- it 'receives an array with the word NULL' do
284
- expect(coder.encode( record.new(['1','NULL','3']) )).to eql %[(1,"NULL",3)]
285
- end
286
-
287
- it 'receives an array of strings when containing commas in a quoted string' do
288
- expect(coder.encode( record.new(['1','2,3','4']) )).to eql %[(1,"2,3",4)]
289
- end
290
-
291
- it 'receives an array of strings when containing an escaped quote' do
292
- expect(coder.encode( record.new(['1','2",3','4']) )).to eql %[(1,"2\\",3",4)]
293
- end
294
-
295
- it 'receives an array of strings when containing an escaped backslash' do
296
- expect(coder.encode( record.new(['1','2\\','3','4']) )).to eql %[(1,"2\\\\",3,4)]
297
- expect(coder.encode( record.new(['1','2\\",3','4']) )).to eql %[(1,"2\\\\\\",3",4)]
298
- end
299
-
300
- it 'receives an array containing empty strings' do
301
- expect(coder.encode( record.new(['1', '', '3', '']) )).to eql %[(1,"",3,"")]
302
- end
303
-
304
- it 'receives an array containing unicode strings' do
305
- expect(coder.encode( record.new(['Paragraph 399(b)(i) – “valid leave” – meaning']) )).to eql %[("Paragraph 399(b)(i) – “valid leave” – meaning")]
306
- end
307
- end
308
-
309
- context 'array of records' do
310
- it 'receives an empty array' do
311
- expect(coder.encode([record.new])).to eql %[{()}]
312
- expect(coder.encode([record.new,record.new])).to eql %[{(),()}]
313
- end
314
-
315
- it 'receives an array of strings with a sub array' do
316
- expect(coder.encode(['1',record.new(['2','3']),'4'])).to eql %[{1,(2,3),4}]
317
- end
318
-
319
- it 'receives an array of strings with a sub array' do
320
- expect(coder.encode(['1',record.new(['2,3']),'4'])).to eql %[{1,("2,3"),4}]
321
- end
322
-
323
- it 'receives an array of strings with a sub array and a quoted }' do
324
- expect(coder.encode(['1',record.new(['2,}3',nil,nil]),'4'])).to eql %[{1,("2,}3",,),4}]
325
- end
326
-
327
- it 'receives an array of strings with a sub array and a quoted {' do
328
- expect(coder.encode(['1',record.new(['2,{3']),'4'])).to eql %[{1,("2,{3"),4}]
329
- end
330
-
331
- it 'receives an array of strings with a sub array and a quoted { and escaped quote' do
332
- expect(coder.encode(['1',record.new(['2",{3']),'4'])).to eql %[{1,("2\\",{3"),4}]
333
- end
334
-
335
- it 'receives an array of strings with a sub array with empty strings' do
336
- expect(coder.encode(['1',record.new(['']),'4',record.new([''])])).to eql %[{1,(""),4,("")}]
337
- end
338
- end
339
-
340
- context 'mix of record and array' do
341
- it 'receives an empty array' do
342
- expect(coder.encode( record.new([[record.new,nil]]) )).to eql %[({(),NULL})]
343
- expect(coder.encode( [record.new([[], []]),[record.new,[]]] )).to eql %[{({},{}),{(),{}}}]
344
- end
345
-
346
- it 'receives an array of strings with sub arrays' do
347
- expect(coder.encode(['1',record.new(['2',['3','4']]),record.new([nil,nil,'6']),'7'])).to eql %[{1,(2,{3,4}),(,,6),7}]
348
- end
349
- end
350
-
351
- context 'record complex sample' do
352
- it 'may have double double quotes translate to single double quotes' do
353
- expect(coder.encode( record.new(['Test with double " quoutes']) )).to eql %[("Test with double \\" quoutes")]
354
- end
355
-
356
- it 'double double quotes may occur any number of times' do
357
- expect(coder.encode( record.new(['Only one "', 'Now " two ".', '","{"}', '""']) )).to eql %[("Only one \\"","Now \\" two \\".","\\",\\"{\\"}","\\"\\"")]
358
- end
359
-
360
- it 'may have any kind of value' do
361
- expect(coder.encode( record.new(['String', '123456', 'false', 'true', '2016-01-01 12:00:00', ['1', '2', '3']]) )).to eql %[(String,123456,false,true,"2016-01-01 12:00:00",{1,2,3})]
362
- end
363
- end
364
-
365
- end
366
-
367
- end