declare_schema 0.6.2 → 0.8.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,21 +5,21 @@ module DeclareSchema
5
5
  class ForeignKeyDefinition
6
6
  include Comparable
7
7
 
8
- attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
8
+ attr_reader :constraint_name, :model, :foreign_key, :foreign_key_name, :options, :on_delete_cascade
9
9
 
10
10
  def initialize(model, foreign_key, options = {})
11
11
  @model = model
12
- @foreign_key = foreign_key.presence
12
+ @foreign_key = foreign_key.to_s.presence
13
13
  @options = options
14
14
 
15
15
  @child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
16
- @parent_table_name = options[:parent_table]
17
- @foreign_key_name = options[:foreign_key] || self.foreign_key
18
- @index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
19
- @constraint_name = options[:constraint_name] || @index_name || ''
20
- @on_delete_cascade = options[:dependent] == :delete
16
+ @parent_table_name = options[:parent_table]&.to_s
17
+ @foreign_key_name = options[:foreign_key]&.to_s || @foreign_key
18
+ @index_name = options[:index_name]&.to_s || model.connection.index_name(model.table_name, column: @foreign_key_name)
21
19
 
22
20
  # Empty constraint lets mysql generate the name
21
+ @constraint_name = options[:constraint_name]&.to_s || @index_name&.to_s || ''
22
+ @on_delete_cascade = options[:dependent] == :delete
23
23
  end
24
24
 
25
25
  class << self
@@ -28,11 +28,12 @@ module DeclareSchema
28
28
  constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
29
29
 
30
30
  constraints.map do |fkc|
31
- options = {}
32
31
  name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
33
- options[:constraint_name] = name
34
- options[:parent_table] = parent_table
35
- options[:foreign_key] = foreign_key
32
+ options = {
33
+ constraint_name: name,
34
+ parent_table: parent_table,
35
+ foreign_key: foreign_key
36
+ }
36
37
  options[:dependent] = :delete if fkc['ON DELETE CASCADE']
37
38
 
38
39
  new(model, foreign_key, options)
@@ -40,21 +41,37 @@ module DeclareSchema
40
41
  end
41
42
  end
42
43
 
44
+ # returns the parent class as a Class object
45
+ # or nil if no :class_name option given
46
+ def parent_class
47
+ if (class_name = options[:class_name])
48
+ if class_name.is_a?(Class)
49
+ class_name
50
+ else
51
+ class_name.to_s.constantize
52
+ end
53
+ end
54
+ end
55
+
43
56
  def parent_table_name
44
57
  @parent_table_name ||=
45
- if (klass = options[:class_name])
46
- klass = klass.to_s.constantize unless klass.is_a?(Class)
47
- klass.try(:table_name)
48
- end || foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
58
+ parent_class&.try(:table_name) ||
59
+ foreign_key.sub(/_id\z/, '').camelize.constantize.table_name
49
60
  end
50
61
 
51
- attr_writer :parent_table_name
62
+ def to_add_statement
63
+ "add_foreign_key(#{@child_table.inspect}, #{parent_table_name.inspect}, " +
64
+ "column: #{@foreign_key_name.inspect}, name: #{@constraint_name.inspect})"
65
+ end
52
66
 
53
- def to_add_statement(_new_table_name = nil, _existing_primary_key = nil)
54
- statement = "ALTER TABLE #{@child_table} ADD CONSTRAINT #{@constraint_name} FOREIGN KEY #{@index_name}(#{@foreign_key_name}) REFERENCES #{parent_table_name}(id) #{'ON DELETE CASCADE' if on_delete_cascade}"
55
- "execute #{statement.inspect}"
67
+ def <=>(rhs)
68
+ key <=> rhs.send(:key)
56
69
  end
57
70
 
71
+ alias eql? ==
72
+
73
+ private
74
+
58
75
  def key
59
76
  @key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
60
77
  end
@@ -62,12 +79,6 @@ module DeclareSchema
62
79
  def hash
63
80
  key.hash
64
81
  end
65
-
66
- def <=>(rhs)
67
- key <=> rhs.key
68
- end
69
-
70
- alias eql? ==
71
82
  end
72
83
  end
73
84
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.6.2"
4
+ VERSION = "0.8.0.pre.1"
5
5
  end
@@ -96,7 +96,7 @@ module DeclareSchema
96
96
  end
97
97
  end
98
98
  end
99
- rescue ::DeclareSchema::Model::FieldSpec::UnknownSqlTypeError => ex
99
+ rescue ::DeclareSchema::UnknownTypeError => ex
100
100
  say "Invalid field type: #{ex}"
101
101
  end
102
102
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_record'
4
+ require 'active_record/connection_adapters/abstract_adapter'
4
5
 
5
6
  module Generators
6
7
  module DeclareSchema
@@ -116,20 +117,6 @@ module Generators
116
117
  ActiveRecord::Base.connection
117
118
  end
118
119
 
119
- def fix_native_types(types)
120
- case connection.class.name
121
- when /mysql/i
122
- types[:integer][:limit] ||= 11
123
- types[:text][:limit] ||= 0xffff
124
- types[:binary][:limit] ||= 0xffff
125
- end
126
- types
127
- end
128
-
129
- def native_types
130
- @native_types ||= fix_native_types(connection.native_database_types)
131
- end
132
-
133
120
  def before_generating_migration(&block)
134
121
  block or raise ArgumentError, 'A block is required when setting the before_generating_migration callback'
135
122
  @before_generating_migration_callback = block
@@ -299,7 +286,7 @@ module Generators
299
286
  "drop_table :#{t}"
300
287
  end * "\n"
301
288
  undo_drops = to_drop.map do |t|
302
- revert_table(t)
289
+ add_table_back(t)
303
290
  end * "\n\n"
304
291
 
305
292
  creates = to_create.map do |t|
@@ -341,11 +328,11 @@ module Generators
341
328
  end
342
329
 
343
330
  def create_table(model)
344
- longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
331
+ longest_field_name = model.field_specs.values.map { |f| f.type.to_s.length }.max
345
332
  disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
346
333
  table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
347
334
  field_definitions = [
348
- disable_auto_increment ? "t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil,
335
+ ("t.integer :id, limit: 8, auto_increment: false, primary_key: true" if disable_auto_increment),
349
336
  *(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
350
337
  ].compact
351
338
 
@@ -386,13 +373,13 @@ module Generators
386
373
  end
387
374
 
388
375
  def create_constraints(model)
389
- model.constraint_specs.map { |fk| fk.to_add_statement(model.table_name) }
376
+ model.constraint_specs.map { |fk| fk.to_add_statement }
390
377
  end
391
378
 
392
379
  def create_field(field_spec, field_name_width)
393
- options = fk_field_options(field_spec.model, field_spec.name).merge(field_spec.sql_options)
394
- args = [field_spec.name.inspect] + format_options(options, field_spec.sql_type)
395
- format("t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
380
+ options = field_spec.sql_options.merge(fk_field_options(field_spec.model, field_spec.name))
381
+ args = [field_spec.name.inspect] + format_options(options.compact)
382
+ format("t.%-*s %s", field_name_width, field_spec.type, args.join(', '))
396
383
  end
397
384
 
398
385
  def change_table(model, current_table_name)
@@ -429,8 +416,8 @@ module Generators
429
416
  adds = to_add.map do |c|
430
417
  args =
431
418
  if (spec = model.field_specs[c])
432
- options = fk_field_options(model, c).merge(spec.sql_options)
433
- [":#{spec.sql_type}", *format_options(options, spec.sql_type)]
419
+ options = spec.sql_options.merge(fk_field_options(model, c))
420
+ [":#{spec.type}", *format_options(options.compact)]
434
421
  else
435
422
  [":integer"]
436
423
  end
@@ -444,34 +431,28 @@ module Generators
444
431
  "remove_column :#{new_table_name}, :#{c}"
445
432
  end
446
433
  undo_removes = to_remove.map do |c|
447
- revert_column(current_table_name, c)
434
+ add_column_back(model, current_table_name, c)
448
435
  end
449
436
 
450
437
  old_names = to_rename.invert
451
438
  changes = []
452
439
  undo_changes = []
453
- to_change.each do |c|
454
- col_name = old_names[c] || c
455
- col = db_columns[col_name]
456
- spec = model.field_specs[c]
457
- if spec.different_to?(current_table_name, col) # TODO: TECH-4814 DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
458
- change_spec = fk_field_options(model, c)
459
- change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
460
- ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
461
- (spec.limit || col.limit)
462
- change_spec[:precision] = spec.precision unless spec.precision.nil?
463
- change_spec[:scale] = spec.scale unless spec.scale.nil?
464
- change_spec[:null] = spec.null unless spec.null && col.null
465
- change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
466
- change_spec[:charset] = spec.charset unless spec.charset.nil?
467
- change_spec[:collation] = spec.collation unless spec.collation.nil?
468
-
469
- changes << "change_column :#{new_table_name}, :#{c}, " +
470
- ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
471
- back = change_column_back(current_table_name, col_name)
472
- undo_changes << back unless back.blank?
440
+ to_change.each do |col_name_to_change|
441
+ orig_col_name = old_names[col_name_to_change] || col_name_to_change
442
+ column = db_columns[orig_col_name] or raise "failed to find column info for #{orig_col_name.inspect}"
443
+ spec = model.field_specs[col_name_to_change] or raise "failed to find field spec for #{col_name_to_change.inspect}"
444
+ spec_attrs = spec.schema_attributes(column)
445
+ column_declaration = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
446
+ col_attrs = column_declaration.schema_attributes
447
+ normalized_schema_attrs = spec_attrs.merge(fk_field_options(model, col_name_to_change))
448
+
449
+ if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(normalized_schema_attrs, col_attrs)
450
+ type = normalized_schema_attrs.delete(:type) or raise "no :type found in #{normalized_schema_attrs.inspect}"
451
+ changes << ["change_column #{new_table_name.to_sym.inspect}", col_name_to_change.to_sym.inspect,
452
+ type.to_sym.inspect, *format_options(normalized_schema_attrs)].join(", ")
453
+ undo_changes << change_column_back(model, current_table_name, orig_col_name)
473
454
  end
474
- end.compact
455
+ end
475
456
 
476
457
  index_changes, undo_index_changes = change_indexes(model, current_table_name, to_remove)
477
458
  fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
@@ -485,26 +466,26 @@ module Generators
485
466
  [[], []]
486
467
  end
487
468
 
488
- [(renames + adds + removes + changes) * "\n",
469
+ [(renames + adds + removes + changes) * "\n",
489
470
  (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
490
- index_changes * "\n",
491
- undo_index_changes * "\n",
492
- fk_changes * "\n",
493
- undo_fk_changes * "\n",
494
- table_options_changes * "\n",
495
- undo_table_options_changes * "\n"]
471
+ index_changes * "\n",
472
+ undo_index_changes * "\n",
473
+ fk_changes * "\n",
474
+ undo_fk_changes * "\n",
475
+ table_options_changes * "\n",
476
+ undo_table_options_changes * "\n"]
496
477
  end
497
478
 
498
479
  def change_indexes(model, old_table_name, to_remove)
499
- return [[], []] if Migrator.disable_constraints
480
+ Migrator.disable_constraints and return [[], []]
500
481
 
501
482
  new_table_name = model.table_name
502
483
  existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
503
484
  model_indexes_with_equivalents = model.index_definitions_with_primary_key
504
485
  model_indexes = model_indexes_with_equivalents.map do |i|
505
486
  if i.explicit_name.nil?
506
- if ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
507
- i.with_name(ex.name)
487
+ if (existing = existing_indexes.find { |e| i != e && e.equivalent?(i) })
488
+ i.with_name(existing.name)
508
489
  end
509
490
  end || i
510
491
  end
@@ -514,15 +495,13 @@ module Generators
514
495
  end
515
496
  model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
516
497
 
517
- add_indexes_init = model_indexes - existing_indexes
518
- drop_indexes_init = existing_indexes - model_indexes
519
498
  undo_add_indexes = []
520
- undo_drop_indexes = []
521
- add_indexes = add_indexes_init.map do |i|
499
+ add_indexes = (model_indexes - existing_indexes).map do |i|
522
500
  undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
523
501
  i.to_add_statement(new_table_name, existing_has_primary_key)
524
502
  end
525
- drop_indexes = drop_indexes_init.map do |i|
503
+ undo_drop_indexes = []
504
+ drop_indexes = (existing_indexes - model_indexes).map do |i|
526
505
  undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
527
506
  drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
528
507
  end.compact
@@ -538,51 +517,41 @@ module Generators
538
517
  end
539
518
 
540
519
  def change_foreign_key_constraints(model, old_table_name)
541
- ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise 'SQLite does not support foreign keys'
542
- return [[], []] if Migrator.disable_indexing
520
+ ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
521
+ Migrator.disable_indexing and return [[], []]
543
522
 
544
523
  new_table_name = model.table_name
545
524
  existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
546
525
  model_fks = model.constraint_specs
547
- add_fks = model_fks - existing_fks
548
- drop_fks = existing_fks - model_fks
549
- undo_add_fks = []
550
- undo_drop_fks = []
551
526
 
552
- add_fks.map! do |fk|
527
+ undo_add_fks = []
528
+ add_fks = (model_fks - existing_fks).map do |fk|
553
529
  # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
554
- undo_add_fks << remove_foreign_key(old_table_name, fk.options[:constraint_name])
530
+ undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
555
531
  fk.to_add_statement
556
- end.compact
532
+ end
557
533
 
558
- drop_fks.map! do |fk|
534
+ undo_drop_fks = []
535
+ drop_fks = (existing_fks - model_fks).map do |fk|
559
536
  undo_drop_fks << fk.to_add_statement
560
- remove_foreign_key(new_table_name, fk.options[:constraint_name])
537
+ remove_foreign_key(new_table_name, fk.constraint_name)
561
538
  end
562
539
 
563
540
  [drop_fks + add_fks, undo_add_fks + undo_drop_fks]
564
541
  end
565
542
 
566
543
  def remove_foreign_key(old_table_name, fk_name)
567
- "remove_foreign_key('#{old_table_name}', name: '#{fk_name}')"
544
+ "remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
568
545
  end
569
546
 
570
- def format_options(options, type, changing: false)
547
+ def format_options(options)
571
548
  options.map do |k, v|
572
- if !changing && ((k == :limit && type == :decimal) || (k == :null && v == true))
573
- next
574
- end
575
-
576
- if !::DeclareSchema::Model::FieldSpec.mysql_text_limits? && k == :limit && type == :text
577
- next
578
- end
579
-
580
549
  if k.is_a?(Symbol)
581
550
  "#{k}: #{v.inspect}"
582
551
  else
583
552
  "#{k.inspect} => #{v.inspect}"
584
553
  end
585
- end.compact
554
+ end
586
555
  end
587
556
 
588
557
  def fk_field_options(model, field_name)
@@ -620,7 +589,33 @@ module Generators
620
589
  end
621
590
  end
622
591
 
623
- # TODO: TECH-4814 remove all methods from here through end of file
592
+ def with_previous_model_table_name(model, table_name)
593
+ model_table_name, model.table_name = model.table_name, table_name
594
+ yield
595
+ ensure
596
+ model.table_name = model_table_name
597
+ end
598
+
599
+ def add_column_back(model, current_table_name, col_name)
600
+ with_previous_model_table_name(model, current_table_name) do
601
+ column = model.columns_hash[col_name] or raise "no columns_hash entry found for #{col_name} in #{model.inspect}"
602
+ col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
603
+ schema_attributes = col_spec.schema_attributes
604
+ type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
605
+ ["add_column :#{current_table_name}, :#{col_name}, #{type.inspect}", *format_options(schema_attributes)].join(', ')
606
+ end
607
+ end
608
+
609
+ def change_column_back(model, current_table_name, col_name)
610
+ with_previous_model_table_name(model, current_table_name) do
611
+ column = model.columns_hash[col_name] or raise "no columns_hash entry found for #{col_name} in #{model.inspect}"
612
+ col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
613
+ schema_attributes = col_spec.schema_attributes
614
+ type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
615
+ ["change_column #{current_table_name.to_sym.inspect}", col_name.to_sym.inspect, type.to_sym.inspect, *format_options(schema_attributes)].join(', ')
616
+ end
617
+ end
618
+
624
619
  def default_collation_from_charset(charset)
625
620
  case charset
626
621
  when "utf8"
@@ -630,64 +625,47 @@ module Generators
630
625
  end
631
626
  end
632
627
 
633
- def revert_table(table)
634
- res = StringIO.new
635
- schema_dumper_klass = case Rails::VERSION::MAJOR
636
- when 4
637
- ActiveRecord::SchemaDumper
638
- else
639
- ActiveRecord::ConnectionAdapters::SchemaDumper
640
- end
641
- schema_dumper_klass.send(:new, ActiveRecord::Base.connection).send(:table, table, res)
628
+ SchemaDumper = case Rails::VERSION::MAJOR
629
+ when 4
630
+ ActiveRecord::SchemaDumper
631
+ else
632
+ ActiveRecord::ConnectionAdapters::SchemaDumper
633
+ end
634
+
635
+ def add_table_back(table)
636
+ dumped_schema_stream = StringIO.new
637
+ SchemaDumper.send(:new, ActiveRecord::Base.connection).send(:table, table, dumped_schema_stream)
642
638
 
643
- result = res.string.strip.gsub("\n ", "\n")
639
+ dumped_schema = dumped_schema_stream.string.strip.gsub!("\n ", "\n")
644
640
  if connection.class.name.match?(/mysql/i)
645
- if !result['options: ']
646
- result = result.sub('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
647
- "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
648
- end
649
- default_charset = result[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{result.inspect}"
650
- default_collation = result[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
651
- raise "unable to find collation in #{result.inspect} or charset #{default_charset.inspect}"
652
- result = result.split("\n").map do |line|
653
- if line['t.text'] || line['t.string']
654
- if !line['charset: ']
655
- if line['collation: ']
656
- line = line.sub('collation: ', "charset: #{default_charset.inspect}, collation: ")
657
- else
658
- line += ", charset: #{default_charset.inspect}"
659
- end
660
- end
661
- line['collation: '] or line += ", collation: #{default_collation.inspect}"
662
- end
663
- line
664
- end.join("\n")
641
+ fix_mysql_charset_and_collation(dumped_schema)
642
+ else
643
+ dumped_schema
665
644
  end
666
- result
667
645
  end
668
646
 
669
- def column_options_from_reverted_table(table, column)
670
- revert = revert_table(table)
671
- if (md = revert.match(/\s*t\.column\s+"#{column}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
672
- # Ugly migration
673
- _, type, options = *md
674
- elsif (md = revert.match(/\s*t\.([a-z_]+)\s+"#{column}"(?:,\s+(.*?)$)?/m))
675
- # Sexy migration
676
- _, string_type, options = *md
677
- type = ":#{string_type}"
647
+ # TODO: rewrite this method to use charset and collation variables rather than manipulating strings. -Colin
648
+ def fix_mysql_charset_and_collation(dumped_schema)
649
+ if !dumped_schema['options: ']
650
+ dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
651
+ "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
678
652
  end
679
- type or raise "unable to find column options for #{table}.#{column} in #{revert.inspect}"
680
- [type, options]
681
- end
682
-
683
- def change_column_back(table, column)
684
- type, options = column_options_from_reverted_table(table, column)
685
- ["change_column :#{table}, :#{column}, #{type}", options&.strip].compact.join(', ')
686
- end
687
-
688
- def revert_column(table, column)
689
- type, options = column_options_from_reverted_table(table, column)
690
- ["add_column :#{table}, :#{column}, #{type}", options&.strip].compact.join(', ')
653
+ default_charset = dumped_schema[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{dumped_schema.inspect}"
654
+ default_collation = dumped_schema[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
655
+ raise "unable to find collation in #{dumped_schema.inspect} or charset #{default_charset.inspect}"
656
+ dumped_schema.split("\n").map do |line|
657
+ if line['t.text'] || line['t.string']
658
+ if !line['charset: ']
659
+ if line['collation: ']
660
+ line.sub!('collation: ', "charset: #{default_charset.inspect}, collation: ")
661
+ else
662
+ line << ", charset: #{default_charset.inspect}"
663
+ end
664
+ end
665
+ line['collation: '] or line << ", collation: #{default_collation.inspect}"
666
+ end
667
+ line
668
+ end.join("\n")
691
669
  end
692
670
  end
693
671
  end