activerecord-sqlserver-adapter-odbc-extended 8.0.10

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.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/.github/issue_template.md +22 -0
  3. data/.github/workflows/ci.yml +32 -0
  4. data/.gitignore +9 -0
  5. data/.rubocop.yml +69 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/Dockerfile.ci +14 -0
  9. data/Gemfile +26 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +104 -0
  12. data/RUNNING_UNIT_TESTS.md +38 -0
  13. data/Rakefile +45 -0
  14. data/VERSION +1 -0
  15. data/activerecord-sqlserver-adapter-odbc-extended.gemspec +34 -0
  16. data/compose.ci.yaml +15 -0
  17. data/lib/active_record/connection_adapters/extended_sqlserver_adapter.rb +204 -0
  18. data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +41 -0
  19. data/lib/active_record/connection_adapters/sqlserver/odbc_database_statements.rb +234 -0
  20. data/lib/active_record/connection_adapters/sqlserver/type/binary_ext.rb +25 -0
  21. data/lib/activerecord-sqlserver-adapter-odbc-extended.rb +12 -0
  22. data/test/appveyor/dbsetup.ps1 +27 -0
  23. data/test/appveyor/dbsetup.sql +11 -0
  24. data/test/cases/active_schema_test_sqlserver.rb +127 -0
  25. data/test/cases/adapter_test_sqlserver.rb +648 -0
  26. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  27. data/test/cases/change_column_null_test_sqlserver.rb +44 -0
  28. data/test/cases/coerced_tests.rb +2796 -0
  29. data/test/cases/column_test_sqlserver.rb +848 -0
  30. data/test/cases/connection_test_sqlserver.rb +138 -0
  31. data/test/cases/dbconsole.rb +19 -0
  32. data/test/cases/disconnected_test_sqlserver.rb +42 -0
  33. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  34. data/test/cases/enum_test_sqlserver.rb +49 -0
  35. data/test/cases/execute_procedure_test_sqlserver.rb +57 -0
  36. data/test/cases/fetch_test_sqlserver.rb +88 -0
  37. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +72 -0
  38. data/test/cases/helper_sqlserver.rb +61 -0
  39. data/test/cases/migration_test_sqlserver.rb +144 -0
  40. data/test/cases/order_test_sqlserver.rb +153 -0
  41. data/test/cases/pessimistic_locking_test_sqlserver.rb +102 -0
  42. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  43. data/test/cases/rake_test_sqlserver.rb +198 -0
  44. data/test/cases/schema_dumper_test_sqlserver.rb +296 -0
  45. data/test/cases/schema_test_sqlserver.rb +111 -0
  46. data/test/cases/trigger_test_sqlserver.rb +51 -0
  47. data/test/cases/utils_test_sqlserver.rb +129 -0
  48. data/test/cases/uuid_test_sqlserver.rb +54 -0
  49. data/test/cases/view_test_sqlserver.rb +58 -0
  50. data/test/config.yml +38 -0
  51. data/test/debug.rb +16 -0
  52. data/test/fixtures/1px.gif +0 -0
  53. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  54. data/test/migrations/create_clients_and_change_column_null.rb +25 -0
  55. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  56. data/test/models/sqlserver/alien.rb +5 -0
  57. data/test/models/sqlserver/booking.rb +5 -0
  58. data/test/models/sqlserver/composite_pk.rb +9 -0
  59. data/test/models/sqlserver/customers_view.rb +5 -0
  60. data/test/models/sqlserver/datatype.rb +5 -0
  61. data/test/models/sqlserver/datatype_migration.rb +10 -0
  62. data/test/models/sqlserver/dollar_table_name.rb +5 -0
  63. data/test/models/sqlserver/edge_schema.rb +13 -0
  64. data/test/models/sqlserver/fk_has_fk.rb +5 -0
  65. data/test/models/sqlserver/fk_has_pk.rb +5 -0
  66. data/test/models/sqlserver/natural_pk_data.rb +6 -0
  67. data/test/models/sqlserver/natural_pk_int_data.rb +5 -0
  68. data/test/models/sqlserver/no_pk_data.rb +5 -0
  69. data/test/models/sqlserver/object_default.rb +5 -0
  70. data/test/models/sqlserver/quoted_table.rb +9 -0
  71. data/test/models/sqlserver/quoted_view_1.rb +5 -0
  72. data/test/models/sqlserver/quoted_view_2.rb +5 -0
  73. data/test/models/sqlserver/sst_memory.rb +5 -0
  74. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  75. data/test/models/sqlserver/string_default.rb +5 -0
  76. data/test/models/sqlserver/string_defaults_big_view.rb +5 -0
  77. data/test/models/sqlserver/string_defaults_view.rb +5 -0
  78. data/test/models/sqlserver/table_with_spaces.rb +5 -0
  79. data/test/models/sqlserver/tinyint_pk.rb +5 -0
  80. data/test/models/sqlserver/trigger.rb +17 -0
  81. data/test/models/sqlserver/trigger_history.rb +5 -0
  82. data/test/models/sqlserver/upper.rb +5 -0
  83. data/test/models/sqlserver/uppered.rb +5 -0
  84. data/test/models/sqlserver/uuid.rb +5 -0
  85. data/test/schema/datatypes/2012.sql +56 -0
  86. data/test/schema/enable-in-memory-oltp.sql +81 -0
  87. data/test/schema/sqlserver_specific_schema.rb +363 -0
  88. data/test/support/coerceable_test_sqlserver.rb +55 -0
  89. data/test/support/connection_reflection.rb +32 -0
  90. data/test/support/core_ext/query_cache.rb +38 -0
  91. data/test/support/load_schema_sqlserver.rb +29 -0
  92. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  93. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  94. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  95. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  96. data/test/support/minitest_sqlserver.rb +3 -0
  97. data/test/support/paths_sqlserver.rb +50 -0
  98. data/test/support/query_assertions.rb +49 -0
  99. data/test/support/rake_helpers.rb +46 -0
  100. data/test/support/table_definition_sqlserver.rb +24 -0
  101. data/test/support/test_in_memory_oltp.rb +17 -0
  102. metadata +240 -0
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+ require "stringio"
5
+
6
+ class SchemaDumperTestSQLServer < ActiveRecord::TestCase
7
+ before { all_tables }
8
+
9
+ let(:all_tables) { ActiveRecord::Base.lease_connection.tables }
10
+ let(:schema) { @generated_schema }
11
+
12
+ it "sst_datatypes" do
13
+ generate_schema_for_table "sst_datatypes"
14
+
15
+ assert_line :bigint, type: "bigint", default: 42
16
+ assert_line :int, type: "integer", default: 42
17
+ assert_line :smallint, type: "integer", limit: 2, default: 42
18
+ assert_line :tinyint, type: "integer", limit: 1, default: 42
19
+ assert_line :bit, type: "boolean", default: true
20
+ assert_line :decimal_9_2, type: "decimal", precision: 9, scale: 2, default: 12345.01
21
+ assert_line :numeric_18_0, type: "decimal", precision: 18, default: 191
22
+ assert_line :numeric_36_2, type: "decimal", precision: 36, scale: 2, default: 12345678901234567890.01
23
+ assert_line :money, type: "money", precision: 19, scale: 4, default: 4.2
24
+ assert_line :smallmoney, type: "smallmoney", precision: 10, scale: 4, default: 4.2
25
+ # Approximate Numerics
26
+ assert_line :float, type: "float", default: 123.00000001
27
+ assert_line :real, type: "real", default: 123.45
28
+ # Date and Time
29
+ assert_line :date, type: "date", limit: nil, precision: nil, scale: nil, default: "01-01-1900"
30
+ assert_line :datetime, type: "datetime", limit: nil, precision: nil, scale: nil, default: "01-01-1753 00:00:00.123"
31
+ if connection_dblib_73?
32
+ assert_line :datetime2_7, type: "datetime", precision: 7, default: "12-31-9999 23:59:59.9999999"
33
+ assert_line :datetime2_3, type: "datetime", precision: 3
34
+ assert_line :datetime2_1, type: "datetime", precision: 1
35
+ end
36
+ assert_line :smalldatetime, type: "smalldatetime", default: "01-01-1901 15:45:00.0"
37
+ if connection_tds_73
38
+ assert_line :time_7, type: "time", precision: 7, default: "04:20:00.2883215"
39
+ assert_line :time_2, type: "time", precision: 2
40
+ assert_line :time_default, type: "time", precision: 7, default: "15:03:42.0621978"
41
+ end
42
+ # Character Strings
43
+ assert_line :char_10, type: "char", limit: 10, default: "1234567890"
44
+ assert_line :varchar_50, type: "varchar", limit: 50, default: "test varchar_50"
45
+ assert_line :varchar_max, type: "varchar_max", default: "test varchar_max"
46
+ assert_line :text, type: "text_basic", default: "test text"
47
+ # Unicode Character Strings
48
+ assert_line :nchar_10, type: "nchar", limit: 10, default: "12345678åå"
49
+ assert_line :nvarchar_50, type: "string", limit: 50, default: "test nvarchar_50 åå"
50
+ assert_line :nvarchar_max, type: "text", default: "test nvarchar_max åå"
51
+ assert_line :ntext, type: "ntext", default: "test ntext åå"
52
+ # Binary Strings
53
+ assert_line :binary_49, type: "binary_basic", limit: 49
54
+ assert_line :varbinary_49, type: "varbinary", limit: 49
55
+ assert_line :varbinary_max, type: "binary"
56
+ # Other Data Types
57
+ assert_line :uniqueidentifier, type: "uuid", default: -> { "newid()" }
58
+ assert_line :timestamp, type: "ss_timestamp"
59
+ end
60
+
61
+ it "sst_datatypes_migration" do
62
+ columns = SSTestDatatypeMigration.columns_hash
63
+ generate_schema_for_table "sst_datatypes_migration"
64
+
65
+ # Simple Rails conventions
66
+ _(columns["integer_col"].sql_type).must_equal "int(4)"
67
+ _(columns["bigint_col"].sql_type).must_equal "bigint(8)"
68
+ _(columns["boolean_col"].sql_type).must_equal "bit"
69
+ _(columns["decimal_col"].sql_type).must_equal "decimal(18,0)"
70
+ _(columns["float_col"].sql_type).must_equal "float"
71
+ _(columns["string_col"].sql_type).must_equal "nvarchar(4000)"
72
+ _(columns["text_col"].sql_type).must_equal "nvarchar(max)"
73
+ _(columns["datetime_nil_precision_col"].sql_type).must_equal "datetime"
74
+ _(columns["datetime_col"].sql_type).must_equal "datetime2(6)"
75
+ _(columns["timestamp_col"].sql_type).must_equal "datetime2(6)"
76
+ _(columns["time_col"].sql_type).must_equal "time(7)"
77
+ _(columns["date_col"].sql_type).must_equal "date"
78
+ _(columns["binary_col"].sql_type).must_equal "varbinary(max)"
79
+
80
+ assert_line :integer_col, type: "integer"
81
+ assert_line :bigint_col, type: "bigint"
82
+ assert_line :boolean_col, type: "boolean"
83
+ assert_line :decimal_col, type: "decimal", precision: 18
84
+ assert_line :float_col, type: "float"
85
+ assert_line :string_col, type: "string"
86
+ assert_line :text_col, type: "text"
87
+ assert_line :datetime_nil_precision_col, type: "datetime", precision: nil
88
+ assert_line :datetime_col, type: "datetime"
89
+ assert_line :datetime_col, type: "datetime"
90
+ assert_line :timestamp_col, type: "datetime"
91
+ assert_line :time_col, type: "time", precision: 7
92
+ assert_line :date_col, type: "date"
93
+ assert_line :binary_col, type: "binary"
94
+
95
+ # Our type methods.
96
+ _(columns["real_col"].sql_type).must_equal "real"
97
+ _(columns["money_col"].sql_type).must_equal "money"
98
+ _(columns["smalldatetime_col"].sql_type).must_equal "smalldatetime"
99
+ _(columns["datetime2_col"].sql_type).must_equal "datetime2(7)"
100
+ _(columns["datetimeoffset"].sql_type).must_equal "datetimeoffset(7)"
101
+ _(columns["smallmoney_col"].sql_type).must_equal "smallmoney"
102
+ _(columns["char_col"].sql_type).must_equal "char(1)"
103
+ _(columns["varchar_col"].sql_type).must_equal "varchar(8000)"
104
+ _(columns["text_basic_col"].sql_type).must_equal "text"
105
+ _(columns["nchar_col"].sql_type).must_equal "nchar(1)"
106
+ _(columns["ntext_col"].sql_type).must_equal "ntext"
107
+ _(columns["binary_basic_col"].sql_type).must_equal "binary(1)"
108
+ _(columns["binary_basic_16_col"].sql_type).must_equal "binary(16)"
109
+ _(columns["varbinary_col"].sql_type).must_equal "varbinary(8000)"
110
+ _(columns["uuid_col"].sql_type).must_equal "uniqueidentifier"
111
+ _(columns["sstimestamp_col"].sql_type).must_equal "timestamp"
112
+ _(columns["json_col"].sql_type).must_equal "nvarchar(max)"
113
+
114
+ assert_line :real_col, type: "real"
115
+ assert_line :money_col, type: "money", precision: 19, scale: 4
116
+ assert_line :smalldatetime_col, type: "smalldatetime"
117
+ assert_line :datetime2_col, type: "datetime", precision: 7
118
+ assert_line :datetimeoffset, type: "datetimeoffset", precision: 7
119
+ assert_line :smallmoney_col, type: "smallmoney", precision: 10, scale: 4
120
+ assert_line :char_col, type: "char", limit: 1
121
+ assert_line :varchar_col, type: "varchar"
122
+ assert_line :text_basic_col, type: "text_basic"
123
+ assert_line :nchar_col, type: "nchar", limit: 1
124
+ assert_line :ntext_col, type: "ntext"
125
+ assert_line :binary_basic_col, type: "binary_basic", limit: 1
126
+ assert_line :binary_basic_16_col, type: "binary_basic", limit: 16
127
+ assert_line :varbinary_col, type: "varbinary"
128
+ assert_line :uuid_col, type: "uuid"
129
+ assert_line :sstimestamp_col, type: "ss_timestamp", null: false
130
+ assert_line :json_col, type: "text"
131
+ end
132
+
133
+ it "dump column collation" do
134
+ generate_schema_for_table('sst_string_collation')
135
+
136
+ assert_line :string_without_collation, type: "string"
137
+ assert_line :string_default_collation, type: "varchar"
138
+ assert_line :string_with_collation, type: "varchar", collation: "SQL_Latin1_General_CP1_CS_AS"
139
+ assert_line :varchar_with_collation, type: "varchar", collation: "SQL_Latin1_General_CP1_CS_AS"
140
+ end
141
+
142
+ # Special Cases
143
+
144
+ it "honor nonstandard primary keys" do
145
+ generate_schema_for_table("movies") do |output|
146
+ match = output.match(%r{create_table "movies"(.*)do})
147
+ assert_not_nil(match, "non-standard primary key table not found")
148
+ assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved"
149
+ end
150
+ end
151
+
152
+ it "no id with model driven primary key" do
153
+ output = generate_schema_for_table "sst_no_pk_data"
154
+
155
+ _(output).must_match %r{create_table "sst_no_pk_data".*id:\sfalse.*do}
156
+ assert_line :name, type: "string"
157
+ end
158
+
159
+ it "dumps field with unique key constraints only once" do
160
+ output = generate_schema_for_table "unique_key_dumped_table"
161
+
162
+ _(output.scan('t.integer "unique_field"').length).must_equal(1)
163
+ end
164
+
165
+ it "schemas are dumped and tables names only include non-default schema" do
166
+ stream = StringIO.new
167
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection_pool, stream)
168
+ generated_schema = stream.string
169
+
170
+ # Only generate non-default schemas. Default schema is 'dbo'.
171
+ assert_not_includes generated_schema, 'create_schema "dbo"'
172
+ assert_not_includes generated_schema, 'create_schema "db_owner"'
173
+ assert_not_includes generated_schema, 'create_schema "INFORMATION_SCHEMA"'
174
+ assert_not_includes generated_schema, 'create_schema "sys"'
175
+ assert_not_includes generated_schema, 'create_schema "guest"'
176
+ assert_includes generated_schema, 'create_schema "test"'
177
+ assert_includes generated_schema, 'create_schema "test2"'
178
+
179
+ # Only non-default schemas should be included in table names. Default schema is 'dbo'.
180
+ assert_includes generated_schema, 'create_table "accounts"'
181
+ assert_includes generated_schema, 'create_table "test.aliens"'
182
+ assert_includes generated_schema, 'create_table "test2.sst_schema_test_multiple_schema"'
183
+ end
184
+
185
+ private
186
+
187
+ def generate_schema_for_table(*table_names)
188
+ previous_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
189
+ ActiveRecord::SchemaDumper.ignore_tables = all_tables - table_names
190
+
191
+ stream = StringIO.new
192
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection_pool, stream)
193
+
194
+ @generated_schema = stream.string
195
+ yield @generated_schema if block_given?
196
+ @schema_lines = Hash.new
197
+ type_matcher = /\A\s+t\.\w+\s+"(.*?)"[,\n]/
198
+ @generated_schema.each_line do |line|
199
+ next unless line =~ type_matcher
200
+
201
+ @schema_lines[Regexp.last_match[1]] = SchemaLine.new(line)
202
+ end
203
+ @generated_schema
204
+ ensure
205
+ ActiveRecord::SchemaDumper.ignore_tables = previous_ignore_tables
206
+ end
207
+
208
+ def line(column_name)
209
+ @schema_lines[column_name.to_s]
210
+ end
211
+
212
+ def assert_line(column_name, expected_options = {})
213
+ line = line(column_name)
214
+ assert line, "Could not find line with column name: #{column_name.inspect} in schema:\n#{schema}"
215
+
216
+ # Check that the expected and actual option keys.
217
+ expected_options_keys = expected_options.keys
218
+ expected_options_keys.delete(:type)
219
+ _(expected_options_keys.sort).must_equal (line.options.keys.sort), "For column '#{column_name}' expected schema options and actual schema options do not match."
220
+
221
+ # Check the expected and actual option values.
222
+ expected_options.each do |key, expected|
223
+ actual = key == :type ? line.send(:type_method) : line.send(key)
224
+
225
+ message = "#{key.to_s.titleize} of #{expected.inspect} not found in:\n#{line}"
226
+
227
+ if expected.nil?
228
+ _(actual).must_be_nil message
229
+ elsif expected.is_a?(Array)
230
+ _(actual).must_include expected, message
231
+ elsif expected.is_a?(Float)
232
+ _(actual).must_be_close_to expected, 0.001
233
+ elsif expected.is_a?(Proc)
234
+ _(actual.call).must_equal(expected.call)
235
+ else
236
+ _(actual).must_equal expected, message
237
+ end
238
+ end
239
+ end
240
+
241
+ class SchemaLine
242
+ LINE_PARSER = %r{t\.(\w+)\s+"(.*?)"[,\s+](.*)}
243
+
244
+ attr_reader :line,
245
+ :type_method,
246
+ :col_name,
247
+ :options
248
+
249
+ def self.option(method_name)
250
+ define_method(method_name) do
251
+ if options.key?(method_name.to_sym)
252
+ options[method_name.to_sym]
253
+ else
254
+ throw "Schema line does include the '#{method_name}' option!"
255
+ end
256
+ end
257
+ end
258
+
259
+ def initialize(line)
260
+ @line = line
261
+ @type_method, @col_name, @options = parse_line
262
+ end
263
+
264
+ option :limit
265
+ option :precision
266
+ option :scale
267
+ option :default
268
+ option :collation
269
+ option :null
270
+
271
+ def to_s
272
+ line.squish
273
+ end
274
+
275
+ def inspect
276
+ "#<SchemaLine col_name=#{col_name.inspect}, options=#{options.inspect}>"
277
+ end
278
+
279
+ private
280
+
281
+ def parse_line
282
+ _all, type_method, col_name, options = @line.match(LINE_PARSER).to_a
283
+ options = parse_options(options)
284
+
285
+ [type_method, col_name, options]
286
+ end
287
+
288
+ def parse_options(opts)
289
+ if opts.present?
290
+ eval "{#{opts}}"
291
+ else
292
+ {}
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+
5
+ class SchemaTestSQLServer < ActiveRecord::TestCase
6
+ describe "When table is dbo schema" do
7
+ it "find primary key for tables with odd schema" do
8
+ _(connection.primary_key("sst_natural_pk_data")).must_equal "legacy_id"
9
+ end
10
+ end
11
+
12
+ describe "When table is in non-dbo schema" do
13
+ it "work with table exists" do
14
+ assert connection.data_source_exists?("test.sst_schema_natural_id")
15
+ assert connection.data_source_exists?("[test].[sst_schema_natural_id]")
16
+ end
17
+
18
+ it "find primary key for tables with odd schema" do
19
+ _(connection.primary_key("test.sst_schema_natural_id")).must_equal "legacy_id"
20
+ end
21
+
22
+ it "have only one identity column" do
23
+ columns = connection.columns("test.sst_schema_identity")
24
+
25
+ assert_equal 2, columns.size
26
+ assert_equal 1, columns.select { |c| c.is_identity? }.size
27
+ end
28
+
29
+ it "read only column properties for table in specific schema" do
30
+ test_columns = connection.columns("test.sst_schema_columns")
31
+ dbo_columns = connection.columns("dbo.sst_schema_columns")
32
+ columns = connection.columns("sst_schema_columns") # This returns table from dbo schema
33
+
34
+ assert_equal 7, test_columns.size
35
+ assert_equal 2, dbo_columns.size
36
+ assert_equal 2, columns.size
37
+ assert_equal 1, test_columns.select { |c| c.is_identity? }.size
38
+ assert_equal 1, dbo_columns.select { |c| c.is_identity? }.size
39
+ assert_equal 1, columns.select { |c| c.is_identity? }.size
40
+ end
41
+
42
+ it "return correct varchar and nvarchar column limit length when table is in non-dbo schema" do
43
+ columns = connection.columns("test.sst_schema_columns")
44
+
45
+ assert_equal 255, columns.find { |c| c.name == "name" }.limit
46
+ assert_equal 1000, columns.find { |c| c.name == "description" }.limit
47
+ assert_equal 255, columns.find { |c| c.name == "n_name" }.limit
48
+ assert_equal 1000, columns.find { |c| c.name == "n_description" }.limit
49
+ end
50
+ end
51
+
52
+ describe "parsing table name from raw SQL" do
53
+ describe 'SELECT statements' do
54
+ it do
55
+ assert_equal "[sst_schema_columns]", connection.send(:get_raw_table_name, "SELECT [sst_schema_columns].[id] FROM [sst_schema_columns]")
56
+ end
57
+
58
+ it do
59
+ assert_equal "sst_schema_columns", connection.send(:get_raw_table_name, "SELECT [sst_schema_columns].[id] FROM sst_schema_columns")
60
+ end
61
+
62
+ it do
63
+ assert_equal "[WITH - SPACES]", connection.send(:get_raw_table_name, "SELECT id FROM [WITH - SPACES]")
64
+ end
65
+
66
+ it do
67
+ assert_equal "[WITH - SPACES$DOLLAR]", connection.send(:get_raw_table_name, "SELECT id FROM [WITH - SPACES$DOLLAR]")
68
+ end
69
+
70
+ it do
71
+ assert_nil connection.send(:get_raw_table_name, nil)
72
+ end
73
+ end
74
+
75
+ describe 'INSERT statements' do
76
+ it do
77
+ assert_equal "[dashboards]", connection.send(:get_raw_table_name, "INSERT INTO [dashboards] DEFAULT VALUES; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident")
78
+ end
79
+
80
+ it do
81
+ assert_equal "lock_without_defaults", connection.send(:get_raw_table_name, "INSERT INTO lock_without_defaults(title) VALUES('title1')")
82
+ end
83
+
84
+ it do
85
+ assert_equal "json_data_type", connection.send(:get_raw_table_name, "insert into json_data_type (payload) VALUES ('null')")
86
+ end
87
+
88
+ it do
89
+ assert_equal "[auto_increments]", connection.send(:get_raw_table_name, "INSERT INTO [auto_increments] OUTPUT INSERTED.[id] DEFAULT VALUES")
90
+ end
91
+
92
+ it do
93
+ assert_equal "[WITH - SPACES]", connection.send(:get_raw_table_name, "EXEC sp_executesql N'INSERT INTO [WITH - SPACES] ([external_id]) OUTPUT INSERTED.[id] VALUES (@0)', N'@0 bigint', @0 = 10")
94
+ end
95
+
96
+ it do
97
+ assert_equal "[test].[aliens]", connection.send(:get_raw_table_name, "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([name]) OUTPUT INSERTED.[id] VALUES (@0)', N'@0 varchar(255)', @0 = 'Trisolarans'")
98
+ end
99
+
100
+ it do
101
+ assert_equal "[with].[select notation]", connection.send(:get_raw_table_name, "INSERT INTO [with].[select notation] SELECT * FROM [table_name]")
102
+ end
103
+ end
104
+
105
+ describe 'CREATE VIEW statements' do
106
+ it do
107
+ assert_equal "test_table_as", connection.send(:get_raw_table_name, "CREATE VIEW test_views ( test_table_a_id, test_table_b_id ) AS SELECT test_table_as.id as test_table_a_id, test_table_bs.id as test_table_b_id FROM (test_table_as with(nolock) LEFT JOIN test_table_bs with(nolock) ON (test_table_as.id = test_table_bs.test_table_a_id))")
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+
5
+ class SQLServerTriggerTest < ActiveRecord::TestCase
6
+ after { exclude_output_inserted_table_names.clear }
7
+
8
+ let(:exclude_output_inserted_table_names) do
9
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.exclude_output_inserted_table_names
10
+ end
11
+
12
+ it "can insert into a table with output inserted - with a true setting for table name" do
13
+ exclude_output_inserted_table_names["sst_table_with_trigger"] = true
14
+ assert SSTestTriggerHistory.all.empty?
15
+ obj = SSTestTrigger.create! event_name: "test trigger"
16
+ _(["Fixnum", "Integer"]).must_include obj.id.class.name
17
+ _(obj.event_name).must_equal "test trigger"
18
+ _(obj.id).must_be :present?
19
+ _(obj.id.to_s).must_equal SSTestTriggerHistory.first.id_source
20
+ end
21
+
22
+ it "can insert into a table with output inserted - with a uniqueidentifier value" do
23
+ exclude_output_inserted_table_names["sst_table_with_uuid_trigger"] = "uniqueidentifier"
24
+ assert SSTestTriggerHistory.all.empty?
25
+ obj = SSTestTriggerUuid.create! event_name: "test uuid trigger"
26
+ _(obj.id.class.name).must_equal "String"
27
+ _(obj.event_name).must_equal "test uuid trigger"
28
+ _(obj.id).must_be :present?
29
+ _(obj.id.to_s).must_equal SSTestTriggerHistory.first.id_source
30
+ end
31
+
32
+ it "can insert into a table with composite pk with output inserted - with a true setting for table name" do
33
+ exclude_output_inserted_table_names["sst_table_with_composite_pk_trigger"] = true
34
+ assert SSTestTriggerHistory.all.empty?
35
+ obj = SSTestTriggerCompositePk.create! pk_col_one: 123, pk_col_two: 42, event_name: "test trigger"
36
+ _(obj.event_name).must_equal "test trigger"
37
+ _(obj.pk_col_one).must_equal 123
38
+ _(obj.pk_col_two).must_equal 42
39
+ _(obj.pk_col_one.to_s).must_equal SSTestTriggerHistory.first.id_source
40
+ end
41
+
42
+ it "can insert into a table with composite pk with different data type with output inserted - with a hash setting for table name" do
43
+ exclude_output_inserted_table_names["sst_table_with_composite_pk_trigger_with_different_data_type"] = { pk_col_one: "uniqueidentifier", pk_col_two: "int" }
44
+ assert SSTestTriggerHistory.all.empty?
45
+ obj = SSTestTriggerCompositePkWithDefferentDataType.create! pk_col_two: 123, event_name: "test trigger"
46
+ _(obj.event_name).must_equal "test trigger"
47
+ _(obj.pk_col_one).must_be :present?
48
+ _(obj.pk_col_two).must_equal 123
49
+ _(obj.pk_col_one.to_s).must_equal SSTestTriggerHistory.first.id_source
50
+ end
51
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+
5
+ class UtilsTestSQLServer < ActiveRecord::TestCase
6
+ it ".quote_string" do
7
+ _(SQLServer::Utils.quote_string("I'll store this in C:\\Users")).must_equal "I''ll store this in C:\\Users"
8
+ end
9
+
10
+ it ".unquote_string" do
11
+ _(SQLServer::Utils.unquote_string("I''ll store this in C:\\Users")).must_equal "I'll store this in C:\\Users"
12
+ end
13
+
14
+ it ".quoted_raw" do
15
+ _(SQLServer::Utils.quoted_raw("some.Name")).must_equal "[some.Name]"
16
+ end
17
+
18
+ describe ".extract_identifiers constructor and thus SQLServer::Utils::Name value object" do
19
+ let(:valid_names) { valid_names_unquoted + valid_names_quoted }
20
+
21
+ let(:valid_names_unquoted) {
22
+ [
23
+ "server.database.schema.object",
24
+ "server.database..object",
25
+ "server..schema.object",
26
+ "server...object",
27
+ "database.schema.object",
28
+ "database..object",
29
+ "schema.object",
30
+ "object"
31
+ ]
32
+ }
33
+
34
+ let(:valid_names_quoted) {
35
+ [
36
+ "[server].[database].[schema].[object]",
37
+ "[server].[database]..[object]",
38
+ "[server]..[schema].[object]",
39
+ "[server]...[object]",
40
+ "[database].[schema].[object]",
41
+ "[database]..[object]",
42
+ "[schema].[object]",
43
+ "[object]"
44
+ ]
45
+ }
46
+
47
+ let(:server_names) { valid_names.partition { |name| name =~ /server/ } }
48
+ let(:database_names) { valid_names.partition { |name| name =~ /database/ } }
49
+ let(:schema_names) { valid_names.partition { |name| name =~ /schema/ } }
50
+
51
+ it "extracts and returns #object identifier unquoted by default or quoted as needed" do
52
+ valid_names.each do |n|
53
+ name = extract_identifiers(n)
54
+ _(name.object).must_equal "object", "With #{n.inspect} for #object"
55
+ _(name.object_quoted).must_equal "[object]", "With #{n.inspect} for #object_quoted"
56
+ end
57
+ end
58
+
59
+ [:schema, :database, :server].each do |part|
60
+ it "extracts and returns #{part} identifier unquoted by default or quoted as needed" do
61
+ present, blank = send(:"#{part}_names")
62
+ present.each do |n|
63
+ name = extract_identifiers(n)
64
+ _(name.send(:"#{part}")).must_equal "#{part}", "With #{n.inspect} for ##{part} method"
65
+ _(name.send(:"#{part}_quoted")).must_equal "[#{part}]", "With #{n.inspect} for ##{part}_quoted method"
66
+ end
67
+ blank.each do |n|
68
+ name = extract_identifiers(n)
69
+ _(name.send(:"#{part}")).must_be_nil "With #{n.inspect} for ##{part} method"
70
+ _(name.send(:"#{part}_quoted")).must_be_nil "With #{n.inspect} for ##{part}_quoted method"
71
+ end
72
+ end
73
+ end
74
+
75
+ it "does not blow up on nil or blank string name" do
76
+ _(extract_identifiers(nil).object).must_be_nil
77
+ _(extract_identifiers(" ").object).must_be_nil
78
+ end
79
+
80
+ it "has a #quoted that returns a fully quoted name with all identifiers as originally passed in" do
81
+ _(extract_identifiers("object").quoted).must_equal "[object]"
82
+ _(extract_identifiers("server.database..object").quoted).must_equal "[server].[database]..[object]"
83
+ _(extract_identifiers("[server]...[object]").quoted).must_equal "[server]...[object]"
84
+ end
85
+
86
+ it "can take a symbol argument" do
87
+ _(extract_identifiers(:object).object).must_equal "object"
88
+ end
89
+
90
+ it "allows identifiers with periods to work" do
91
+ _(extract_identifiers("[obj.name]").quoted).must_equal "[obj.name]"
92
+ _(extract_identifiers("[obj.name].[foo]").quoted).must_equal "[obj.name].[foo]"
93
+ end
94
+
95
+ it "should indicate if a name is fully qualified" do
96
+ _(extract_identifiers("object").fully_qualified?).must_equal false
97
+ _(extract_identifiers("schema.object").fully_qualified?).must_equal false
98
+ _(extract_identifiers("database.schema.object").fully_qualified?).must_equal false
99
+ _(extract_identifiers("database.object").fully_qualified?).must_equal false
100
+ _(extract_identifiers("server...object").fully_qualified?).must_equal false
101
+ _(extract_identifiers("server.database..object").fully_qualified?).must_equal false
102
+ _(extract_identifiers("server.database.schema.object").fully_qualified?).must_equal true
103
+ _(extract_identifiers("server.database.schema.").fully_qualified?).must_equal true
104
+ _(extract_identifiers("[obj.name]").fully_qualified?).must_equal false
105
+ _(extract_identifiers("[schema].[obj.name]").fully_qualified?).must_equal false
106
+ _(extract_identifiers("[database].[schema].[obj.name]").fully_qualified?).must_equal false
107
+ _(extract_identifiers("[database].[obj.name]").fully_qualified?).must_equal false
108
+ _(extract_identifiers("[server.name]...[obj.name]").fully_qualified?).must_equal false
109
+ _(extract_identifiers("[server.name].[database]..[obj.name]").fully_qualified?).must_equal false
110
+ _(extract_identifiers("[server.name].[database].[schema].[obj.name]").fully_qualified?).must_equal true
111
+ _(extract_identifiers("[server.name].[database].[schema].").fully_qualified?).must_equal true
112
+ end
113
+
114
+ it "can return fully qualified quoted table name" do
115
+ name = extract_identifiers("[my.server].db.schema.")
116
+ _(name.fully_qualified_database_quoted).must_equal "[my.server].[db]"
117
+ name = extract_identifiers("[server.name].[database].[schema].[object]")
118
+ _(name.fully_qualified_database_quoted).must_equal "[server.name].[database]"
119
+ name = extract_identifiers("server.database.schema.object")
120
+ _(name.fully_qualified_database_quoted).must_equal "[server].[database]"
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def extract_identifiers(name)
127
+ SQLServer::Utils.extract_identifiers(name)
128
+ end
129
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+
5
+ class SQLServerUuidTest < ActiveRecord::TestCase
6
+ let(:acceptable_uuid) { ActiveRecord::ConnectionAdapters::SQLServer::Type::Uuid::ACCEPTABLE_UUID }
7
+
8
+ it "has a uuid primary key" do
9
+ _(SSTestUuid.columns_hash["id"].type).must_equal :uuid
10
+ assert SSTestUuid.primary_key
11
+ end
12
+
13
+ it "can create with a new pk" do
14
+ obj = SSTestUuid.create!
15
+ _(obj.id).must_be :present?
16
+ _(obj.id).must_match acceptable_uuid
17
+ end
18
+
19
+ it "can create other uuid column on reload" do
20
+ obj = SSTestUuid.create!
21
+ obj.reload
22
+ _(obj.other_uuid).must_match acceptable_uuid
23
+ end
24
+
25
+ it "can find uuid pk via connection" do
26
+ _(connection.primary_key(SSTestUuid.table_name)).must_equal "id"
27
+ end
28
+
29
+ it "changing column default" do
30
+ table_name = SSTestUuid.table_name
31
+ connection.add_column table_name, :thingy, :uuid, null: false, default: "NEWSEQUENTIALID()"
32
+ SSTestUuid.reset_column_information
33
+ column = SSTestUuid.columns_hash["thingy"]
34
+ _(column.default_function).must_equal "newsequentialid()"
35
+ # Now to a different function.
36
+ connection.change_column table_name, :thingy, :uuid, null: false, default: "NEWID()"
37
+ SSTestUuid.reset_column_information
38
+ column = SSTestUuid.columns_hash["thingy"]
39
+ _(column.default_function).must_equal "newid()"
40
+ end
41
+
42
+ it "can insert even when use_output_inserted to false " do
43
+ obj = with_use_output_inserted_disabled { SSTestUuid.create!(name: "😢") }
44
+ _(obj.id).must_be :nil?
45
+ end
46
+
47
+ it "can add column with proc as default" do
48
+ table_name = SSTestUuid.table_name
49
+ connection.add_column table_name, :thingy, :uuid, null: false, default: -> { "NEWSEQUENTIALID()" }
50
+ SSTestUuid.reset_column_information
51
+ column = SSTestUuid.columns_hash["thingy"]
52
+ _(column.default_function).must_equal "newsequentialid()"
53
+ end
54
+ end