meadow-connection-sqlite 1.0.13 → 1.0.15

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.
@@ -10,6 +10,7 @@ const libFS = require('fs');
10
10
 
11
11
  const libFable = require('fable');
12
12
  const libMeadowConnectionSQLite = require('../source/Meadow-Connection-SQLite.js');
13
+ const libMeadowSchemaSQLite = require('../source/Meadow-Schema-SQLite.js');
13
14
 
14
15
  const _FableConfig = (
15
16
  {
@@ -68,6 +69,145 @@ const _AuthorTableSchema =
68
69
  Description: 'A table of authors'
69
70
  };
70
71
 
72
+ const _BookTableSchemaWithColumnIndexed =
73
+ {
74
+ TableName: 'BookIndexed',
75
+ Columns:
76
+ [
77
+ { Column: 'IDBookIndexed', DataType: 'ID' },
78
+ { Column: 'GUIDBookIndexed', DataType: 'GUID' },
79
+ { Column: 'Title', DataType: 'String', Indexed: true },
80
+ { Column: 'Description', DataType: 'Text' },
81
+ { Column: 'ISBN', DataType: 'String', Indexed: 'unique' },
82
+ { Column: 'IDPublisher', DataType: 'ForeignKey' }
83
+ ]
84
+ };
85
+
86
+ const _BookTableSchemaWithIndexName =
87
+ {
88
+ TableName: 'BookCustomIdx',
89
+ Columns:
90
+ [
91
+ { Column: 'IDBookCustomIdx', DataType: 'ID' },
92
+ { Column: 'GUIDBookCustomIdx', DataType: 'GUID' },
93
+ { Column: 'Title', DataType: 'String', Indexed: true, IndexName: 'IX_Custom_Title' },
94
+ { Column: 'ISBN', DataType: 'String', Indexed: 'unique', IndexName: 'UQ_BookCustomIdx_ISBN' },
95
+ { Column: 'YearPublished', DataType: 'Numeric', Indexed: true },
96
+ { Column: 'IDEditor', DataType: 'ForeignKey' }
97
+ ]
98
+ };
99
+
100
+ const _ChinookSQL = `
101
+ CREATE TABLE Artist (
102
+ ArtistId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
103
+ Name NVARCHAR(120)
104
+ );
105
+
106
+ CREATE TABLE Album (
107
+ AlbumId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
108
+ Title NVARCHAR(160) NOT NULL,
109
+ ArtistId INTEGER NOT NULL,
110
+ FOREIGN KEY (ArtistId) REFERENCES Artist (ArtistId)
111
+ );
112
+
113
+ CREATE TABLE Employee (
114
+ EmployeeId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
115
+ LastName NVARCHAR(20) NOT NULL,
116
+ FirstName NVARCHAR(20) NOT NULL,
117
+ Title NVARCHAR(30),
118
+ ReportsTo INTEGER,
119
+ BirthDate DATETIME,
120
+ HireDate DATETIME,
121
+ Address NVARCHAR(70),
122
+ City NVARCHAR(40),
123
+ State NVARCHAR(40),
124
+ Country NVARCHAR(40),
125
+ PostalCode NVARCHAR(10),
126
+ Phone NVARCHAR(24),
127
+ Fax NVARCHAR(24),
128
+ Email NVARCHAR(60),
129
+ FOREIGN KEY (ReportsTo) REFERENCES Employee (EmployeeId)
130
+ );
131
+
132
+ CREATE TABLE Customer (
133
+ CustomerId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
134
+ FirstName NVARCHAR(40) NOT NULL,
135
+ LastName NVARCHAR(20) NOT NULL,
136
+ Company NVARCHAR(80),
137
+ Address NVARCHAR(70),
138
+ City NVARCHAR(40),
139
+ State NVARCHAR(40),
140
+ Country NVARCHAR(40),
141
+ PostalCode NVARCHAR(10),
142
+ Phone NVARCHAR(24),
143
+ Fax NVARCHAR(24),
144
+ Email NVARCHAR(60) NOT NULL,
145
+ SupportRepId INTEGER,
146
+ FOREIGN KEY (SupportRepId) REFERENCES Employee (EmployeeId)
147
+ );
148
+
149
+ CREATE TABLE Genre (
150
+ GenreId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
151
+ Name NVARCHAR(120)
152
+ );
153
+
154
+ CREATE TABLE MediaType (
155
+ MediaTypeId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
156
+ Name NVARCHAR(120)
157
+ );
158
+
159
+ CREATE TABLE Playlist (
160
+ PlaylistId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
161
+ Name NVARCHAR(120)
162
+ );
163
+
164
+ CREATE TABLE Track (
165
+ TrackId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
166
+ Name NVARCHAR(200) NOT NULL,
167
+ AlbumId INTEGER,
168
+ MediaTypeId INTEGER NOT NULL,
169
+ GenreId INTEGER,
170
+ Composer NVARCHAR(220),
171
+ Milliseconds INTEGER NOT NULL,
172
+ Bytes INTEGER,
173
+ UnitPrice NUMERIC(10,2) NOT NULL,
174
+ FOREIGN KEY (AlbumId) REFERENCES Album (AlbumId),
175
+ FOREIGN KEY (MediaTypeId) REFERENCES MediaType (MediaTypeId),
176
+ FOREIGN KEY (GenreId) REFERENCES Genre (GenreId)
177
+ );
178
+
179
+ CREATE TABLE Invoice (
180
+ InvoiceId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
181
+ CustomerId INTEGER NOT NULL,
182
+ InvoiceDate DATETIME NOT NULL,
183
+ BillingAddress NVARCHAR(70),
184
+ BillingCity NVARCHAR(40),
185
+ BillingState NVARCHAR(40),
186
+ BillingCountry NVARCHAR(40),
187
+ BillingPostalCode NVARCHAR(10),
188
+ Total NUMERIC(10,2) NOT NULL,
189
+ FOREIGN KEY (CustomerId) REFERENCES Customer (CustomerId)
190
+ );
191
+
192
+ CREATE TABLE InvoiceLine (
193
+ InvoiceLineId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
194
+ InvoiceId INTEGER NOT NULL,
195
+ TrackId INTEGER NOT NULL,
196
+ UnitPrice NUMERIC(10,2) NOT NULL,
197
+ Quantity INTEGER NOT NULL,
198
+ FOREIGN KEY (InvoiceId) REFERENCES Invoice (InvoiceId),
199
+ FOREIGN KEY (TrackId) REFERENCES Track (TrackId)
200
+ );
201
+
202
+ CREATE TABLE PlaylistTrack (
203
+ PlaylistId INTEGER NOT NULL,
204
+ TrackId INTEGER NOT NULL,
205
+ PRIMARY KEY (PlaylistId, TrackId),
206
+ FOREIGN KEY (PlaylistId) REFERENCES Playlist (PlaylistId),
207
+ FOREIGN KEY (TrackId) REFERENCES Track (TrackId)
208
+ );
209
+ `;
210
+
71
211
  suite
72
212
  (
73
213
  'Connection',
@@ -425,5 +565,857 @@ suite
425
565
  );
426
566
  }
427
567
  );
568
+
569
+ suite
570
+ (
571
+ 'Index Generation',
572
+ ()=>
573
+ {
574
+ let libSchemaSQLite = null;
575
+
576
+ setup(
577
+ (fDone) =>
578
+ {
579
+ let _Fable = new libFable(_FableConfig);
580
+ libSchemaSQLite = _Fable.serviceManager.addServiceType('MeadowSchemaSQLite', libMeadowSchemaSQLite);
581
+ libSchemaSQLite = _Fable.serviceManager.instantiateServiceProvider('MeadowSchemaSQLite');
582
+ _Fable.serviceManager.addServiceType('MeadowSQLiteProvider', libMeadowConnectionSQLite);
583
+ _Fable.serviceManager.instantiateServiceProvider('MeadowSQLiteProvider');
584
+ _Fable.MeadowSQLiteProvider.connectAsync(
585
+ (pError) =>
586
+ {
587
+ libSchemaSQLite.setDatabase(_Fable.MeadowSQLiteProvider.db);
588
+ return fDone();
589
+ });
590
+ });
591
+
592
+ test
593
+ (
594
+ 'auto-detect GUID and ForeignKey indices',
595
+ () =>
596
+ {
597
+ let tmpIndices = libSchemaSQLite.getIndexDefinitionsFromSchema(_BookTableSchema);
598
+ Expect(tmpIndices).to.be.an('array');
599
+ Expect(tmpIndices.length).to.equal(2);
600
+ Expect(tmpIndices[0].Name).to.equal('AK_M_GUIDBook');
601
+ Expect(tmpIndices[0].Unique).to.equal(true);
602
+ Expect(tmpIndices[1].Name).to.equal('IX_M_IDAuthor');
603
+ Expect(tmpIndices[1].Unique).to.equal(false);
604
+ }
605
+ );
606
+
607
+ test
608
+ (
609
+ 'generate idempotent index script with IF NOT EXISTS',
610
+ () =>
611
+ {
612
+ let tmpScript = libSchemaSQLite.generateCreateIndexScript(_BookTableSchema);
613
+ Expect(tmpScript).to.contain('CREATE UNIQUE INDEX IF NOT EXISTS AK_M_GUIDBook ON Book(GUIDBook)');
614
+ Expect(tmpScript).to.contain('CREATE INDEX IF NOT EXISTS IX_M_IDAuthor ON Book(IDAuthor)');
615
+ }
616
+ );
617
+
618
+ test
619
+ (
620
+ 'generate individual index statements with sqlite_master check',
621
+ () =>
622
+ {
623
+ let tmpStatements = libSchemaSQLite.generateCreateIndexStatements(_BookTableSchema);
624
+ Expect(tmpStatements).to.be.an('array');
625
+ Expect(tmpStatements.length).to.equal(2);
626
+ Expect(tmpStatements[0].Name).to.equal('AK_M_GUIDBook');
627
+ Expect(tmpStatements[0].Statement).to.contain('CREATE UNIQUE INDEX AK_M_GUIDBook');
628
+ Expect(tmpStatements[0].CheckStatement).to.contain("sqlite_master");
629
+ Expect(tmpStatements[1].Name).to.equal('IX_M_IDAuthor');
630
+ }
631
+ );
632
+
633
+ test
634
+ (
635
+ 'create indices on a live SQLite database',
636
+ (fDone) =>
637
+ {
638
+ let tmpCreateTableStatement = libSchemaSQLite.generateCreateTableStatement(_BookTableSchema);
639
+ libSchemaSQLite._Database.exec(tmpCreateTableStatement);
640
+ libSchemaSQLite.createIndices(_BookTableSchema,
641
+ (pError) =>
642
+ {
643
+ Expect(pError).to.not.exist;
644
+ let tmpResult = libSchemaSQLite._Database.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='Book' ORDER BY name").all();
645
+ let tmpNames = tmpResult.map(r => r.name);
646
+ Expect(tmpNames).to.include('AK_M_GUIDBook');
647
+ Expect(tmpNames).to.include('IX_M_IDAuthor');
648
+ return fDone();
649
+ });
650
+ }
651
+ );
652
+
653
+ test
654
+ (
655
+ 'create indices idempotently (run twice)',
656
+ (fDone) =>
657
+ {
658
+ let tmpCreateTableStatement = libSchemaSQLite.generateCreateTableStatement(_BookTableSchema);
659
+ libSchemaSQLite._Database.exec(tmpCreateTableStatement);
660
+ libSchemaSQLite.createIndices(_BookTableSchema,
661
+ (pError) =>
662
+ {
663
+ Expect(pError).to.not.exist;
664
+ libSchemaSQLite.createIndices(_BookTableSchema,
665
+ (pError2) =>
666
+ {
667
+ Expect(pError2).to.not.exist;
668
+ return fDone();
669
+ });
670
+ });
671
+ }
672
+ );
673
+
674
+ test
675
+ (
676
+ 'createAllIndices creates indices for all tables in schema',
677
+ (fDone) =>
678
+ {
679
+ let tmpCreateBook = libSchemaSQLite.generateCreateTableStatement(_BookTableSchema);
680
+ let tmpCreateAuthor = libSchemaSQLite.generateCreateTableStatement(_AuthorTableSchema);
681
+ libSchemaSQLite._Database.exec(tmpCreateBook);
682
+ libSchemaSQLite._Database.exec(tmpCreateAuthor);
683
+ let tmpSchema = { Tables: [_BookTableSchema, _AuthorTableSchema] };
684
+ libSchemaSQLite.createAllIndices(tmpSchema,
685
+ (pError) =>
686
+ {
687
+ Expect(pError).to.not.exist;
688
+ return fDone();
689
+ });
690
+ }
691
+ );
692
+
693
+ test
694
+ (
695
+ 'column-level Indexed property generates consistently named indices',
696
+ () =>
697
+ {
698
+ let tmpIndices = libSchemaSQLite.getIndexDefinitionsFromSchema(_BookTableSchemaWithColumnIndexed);
699
+ Expect(tmpIndices).to.be.an('array');
700
+ Expect(tmpIndices.length).to.equal(4);
701
+ Expect(tmpIndices[0].Name).to.equal('AK_M_GUIDBookIndexed');
702
+ Expect(tmpIndices[1].Name).to.equal('IX_M_T_BookIndexed_C_Title');
703
+ Expect(tmpIndices[1].Unique).to.equal(false);
704
+ Expect(tmpIndices[2].Name).to.equal('AK_M_T_BookIndexed_C_ISBN');
705
+ Expect(tmpIndices[2].Unique).to.equal(true);
706
+ Expect(tmpIndices[3].Name).to.equal('IX_M_IDPublisher');
707
+ }
708
+ );
709
+
710
+ test
711
+ (
712
+ 'create column-level Indexed indices on a live SQLite database',
713
+ (fDone) =>
714
+ {
715
+ let tmpCreateTableStatement = libSchemaSQLite.generateCreateTableStatement(_BookTableSchemaWithColumnIndexed);
716
+ libSchemaSQLite._Database.exec(tmpCreateTableStatement);
717
+ libSchemaSQLite.createIndices(_BookTableSchemaWithColumnIndexed,
718
+ (pError) =>
719
+ {
720
+ Expect(pError).to.not.exist;
721
+ let tmpResult = libSchemaSQLite._Database.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='BookIndexed' ORDER BY name").all();
722
+ let tmpNames = tmpResult.map(r => r.name);
723
+ Expect(tmpNames).to.include('AK_M_GUIDBookIndexed');
724
+ Expect(tmpNames).to.include('IX_M_T_BookIndexed_C_Title');
725
+ Expect(tmpNames).to.include('AK_M_T_BookIndexed_C_ISBN');
726
+ Expect(tmpNames).to.include('IX_M_IDPublisher');
727
+ return fDone();
728
+ });
729
+ }
730
+ );
731
+
732
+ test
733
+ (
734
+ 'IndexName property overrides auto-generated index name',
735
+ () =>
736
+ {
737
+ let tmpIndices = libSchemaSQLite.getIndexDefinitionsFromSchema(_BookTableSchemaWithIndexName);
738
+ Expect(tmpIndices).to.be.an('array');
739
+ Expect(tmpIndices.length).to.equal(5);
740
+ Expect(tmpIndices[0].Name).to.equal('AK_M_GUIDBookCustomIdx');
741
+ Expect(tmpIndices[0].Unique).to.equal(true);
742
+ Expect(tmpIndices[1].Name).to.equal('IX_Custom_Title');
743
+ Expect(tmpIndices[1].Unique).to.equal(false);
744
+ Expect(tmpIndices[2].Name).to.equal('UQ_BookCustomIdx_ISBN');
745
+ Expect(tmpIndices[2].Unique).to.equal(true);
746
+ Expect(tmpIndices[3].Name).to.equal('IX_M_T_BookCustomIdx_C_YearPublished');
747
+ Expect(tmpIndices[3].Unique).to.equal(false);
748
+ Expect(tmpIndices[4].Name).to.equal('IX_M_IDEditor');
749
+ }
750
+ );
751
+
752
+ test
753
+ (
754
+ 'create IndexName-overridden indices on a live SQLite database',
755
+ (fDone) =>
756
+ {
757
+ let tmpCreateTableStatement = libSchemaSQLite.generateCreateTableStatement(_BookTableSchemaWithIndexName);
758
+ libSchemaSQLite._Database.exec(tmpCreateTableStatement);
759
+ libSchemaSQLite.createIndices(_BookTableSchemaWithIndexName,
760
+ (pError) =>
761
+ {
762
+ Expect(pError).to.not.exist;
763
+ let tmpResult = libSchemaSQLite._Database.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='BookCustomIdx' ORDER BY name").all();
764
+ let tmpNames = tmpResult.map(r => r.name);
765
+ Expect(tmpNames).to.include('IX_Custom_Title');
766
+ Expect(tmpNames).to.include('UQ_BookCustomIdx_ISBN');
767
+ Expect(tmpNames).to.include('IX_M_T_BookCustomIdx_C_YearPublished');
768
+ Expect(tmpNames).to.include('AK_M_GUIDBookCustomIdx');
769
+ Expect(tmpNames).to.include('IX_M_IDEditor');
770
+ return fDone();
771
+ });
772
+ }
773
+ );
774
+ }
775
+ );
776
+
777
+ test
778
+ (
779
+ 'schema provider is accessible from connection provider',
780
+ (fDone) =>
781
+ {
782
+ let _Fable = new libFable(_FableConfig);
783
+ _Fable.serviceManager.addServiceType('MeadowSQLiteProvider', libMeadowConnectionSQLite);
784
+ _Fable.serviceManager.instantiateServiceProvider('MeadowSQLiteProvider');
785
+ Expect(_Fable.MeadowSQLiteProvider.schemaProvider).to.be.an('object');
786
+ return fDone();
787
+ }
788
+ );
789
+
790
+ suite
791
+ (
792
+ 'Database Introspection',
793
+ ()=>
794
+ {
795
+ let libSchemaSQLite = null;
796
+
797
+ setup(
798
+ (fDone) =>
799
+ {
800
+ let _Fable = new libFable(_FableConfig);
801
+ libSchemaSQLite = _Fable.serviceManager.addServiceType('MeadowSchemaSQLite', libMeadowSchemaSQLite);
802
+ libSchemaSQLite = _Fable.serviceManager.instantiateServiceProvider('MeadowSchemaSQLite');
803
+ _Fable.serviceManager.addServiceType('MeadowSQLiteProvider', libMeadowConnectionSQLite);
804
+ _Fable.serviceManager.instantiateServiceProvider('MeadowSQLiteProvider');
805
+ _Fable.MeadowSQLiteProvider.connectAsync(
806
+ (pError) =>
807
+ {
808
+ libSchemaSQLite.setDatabase(_Fable.MeadowSQLiteProvider.db);
809
+ // Create tables and indices for introspection tests
810
+ let tmpCreateBook = libSchemaSQLite.generateCreateTableStatement(_BookTableSchema);
811
+ libSchemaSQLite._Database.exec(tmpCreateBook);
812
+ libSchemaSQLite.createIndices(_BookTableSchema,
813
+ (pIndexError) =>
814
+ {
815
+ let tmpCreateIndexed = libSchemaSQLite.generateCreateTableStatement(_BookTableSchemaWithColumnIndexed);
816
+ libSchemaSQLite._Database.exec(tmpCreateIndexed);
817
+ libSchemaSQLite.createIndices(_BookTableSchemaWithColumnIndexed,
818
+ (pIndexError2) =>
819
+ {
820
+ let tmpCreateCustom = libSchemaSQLite.generateCreateTableStatement(_BookTableSchemaWithIndexName);
821
+ libSchemaSQLite._Database.exec(tmpCreateCustom);
822
+ libSchemaSQLite.createIndices(_BookTableSchemaWithIndexName,
823
+ (pIndexError3) =>
824
+ {
825
+ return fDone();
826
+ });
827
+ });
828
+ });
829
+ });
830
+ });
831
+
832
+ test
833
+ (
834
+ 'listTables returns all user tables',
835
+ (fDone) =>
836
+ {
837
+ libSchemaSQLite.listTables(
838
+ (pError, pTables) =>
839
+ {
840
+ Expect(pError).to.not.exist;
841
+ Expect(pTables).to.be.an('array');
842
+ Expect(pTables.length).to.be.greaterThan(0);
843
+ Expect(pTables).to.include('Book');
844
+ Expect(pTables).to.include('BookIndexed');
845
+ Expect(pTables).to.include('BookCustomIdx');
846
+ return fDone();
847
+ });
848
+ }
849
+ );
850
+
851
+ test
852
+ (
853
+ 'introspectTableColumns returns column definitions for Book',
854
+ (fDone) =>
855
+ {
856
+ libSchemaSQLite.introspectTableColumns('Book',
857
+ (pError, pColumns) =>
858
+ {
859
+ Expect(pError).to.not.exist;
860
+ Expect(pColumns).to.be.an('array');
861
+ Expect(pColumns.length).to.equal(9);
862
+
863
+ // ID column
864
+ Expect(pColumns[0].Column).to.equal('IDBook');
865
+ Expect(pColumns[0].DataType).to.equal('ID');
866
+
867
+ // GUID column
868
+ Expect(pColumns[1].Column).to.equal('GUIDBook');
869
+ Expect(pColumns[1].DataType).to.equal('GUID');
870
+
871
+ // String column
872
+ Expect(pColumns[2].Column).to.equal('Title');
873
+ // Title is TEXT NOT NULL DEFAULT '' → maps to String
874
+ Expect(pColumns[2].DataType).to.equal('String');
875
+
876
+ // Text column
877
+ Expect(pColumns[3].Column).to.equal('Description');
878
+ Expect(pColumns[3].DataType).to.equal('Text');
879
+
880
+ // Decimal column
881
+ Expect(pColumns[4].Column).to.equal('Price');
882
+ Expect(pColumns[4].DataType).to.equal('Decimal');
883
+
884
+ // Numeric column
885
+ Expect(pColumns[5].Column).to.equal('PageCount');
886
+ Expect(pColumns[5].DataType).to.equal('Numeric');
887
+
888
+ // DateTime column (stored as TEXT in SQLite)
889
+ Expect(pColumns[6].Column).to.equal('PublishDate');
890
+ Expect(pColumns[6].DataType).to.equal('Text');
891
+
892
+ // Boolean column (stored as INTEGER, detected by "In" prefix + NOT NULL DEFAULT 0)
893
+ Expect(pColumns[7].Column).to.equal('InPrint');
894
+ Expect(pColumns[7].DataType).to.equal('Boolean');
895
+
896
+ // ForeignKey column
897
+ Expect(pColumns[8].Column).to.equal('IDAuthor');
898
+ Expect(pColumns[8].DataType).to.equal('Numeric');
899
+
900
+ return fDone();
901
+ });
902
+ }
903
+ );
904
+
905
+ test
906
+ (
907
+ 'introspectTableIndices returns index definitions for Book',
908
+ (fDone) =>
909
+ {
910
+ libSchemaSQLite.introspectTableIndices('Book',
911
+ (pError, pIndices) =>
912
+ {
913
+ Expect(pError).to.not.exist;
914
+ Expect(pIndices).to.be.an('array');
915
+ Expect(pIndices.length).to.equal(2);
916
+
917
+ // Should have AK_M_GUIDBook and IX_M_IDAuthor
918
+ let tmpNames = pIndices.map((pIdx) => { return pIdx.Name; });
919
+ Expect(tmpNames).to.include('AK_M_GUIDBook');
920
+ Expect(tmpNames).to.include('IX_M_IDAuthor');
921
+
922
+ let tmpGUIDIndex = pIndices.find((pIdx) => { return pIdx.Name === 'AK_M_GUIDBook'; });
923
+ Expect(tmpGUIDIndex.Unique).to.equal(true);
924
+ Expect(tmpGUIDIndex.Columns).to.deep.equal(['GUIDBook']);
925
+
926
+ return fDone();
927
+ });
928
+ }
929
+ );
930
+
931
+ test
932
+ (
933
+ 'introspectTableForeignKeys returns empty for table without FK constraints',
934
+ (fDone) =>
935
+ {
936
+ // SQLite tables created without REFERENCES have no FK constraints
937
+ libSchemaSQLite.introspectTableForeignKeys('Book',
938
+ (pError, pFKs) =>
939
+ {
940
+ Expect(pError).to.not.exist;
941
+ Expect(pFKs).to.be.an('array');
942
+ // Our Book table was created without REFERENCES clauses
943
+ Expect(pFKs.length).to.equal(0);
944
+ return fDone();
945
+ });
946
+ }
947
+ );
948
+
949
+ test
950
+ (
951
+ 'introspectTableSchema combines columns and indices for BookIndexed',
952
+ (fDone) =>
953
+ {
954
+ libSchemaSQLite.introspectTableSchema('BookIndexed',
955
+ (pError, pSchema) =>
956
+ {
957
+ Expect(pError).to.not.exist;
958
+ Expect(pSchema).to.be.an('object');
959
+ Expect(pSchema.TableName).to.equal('BookIndexed');
960
+ Expect(pSchema.Columns).to.be.an('array');
961
+
962
+ // Check that column-level Indexed properties are folded in
963
+ let tmpTitleCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'Title'; });
964
+ Expect(tmpTitleCol.Indexed).to.equal(true);
965
+ Expect(tmpTitleCol).to.not.have.property('IndexName');
966
+
967
+ let tmpISBNCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'ISBN'; });
968
+ Expect(tmpISBNCol.Indexed).to.equal('unique');
969
+ Expect(tmpISBNCol).to.not.have.property('IndexName');
970
+
971
+ return fDone();
972
+ });
973
+ }
974
+ );
975
+
976
+ test
977
+ (
978
+ 'introspectTableSchema preserves IndexName for custom-named indices',
979
+ (fDone) =>
980
+ {
981
+ libSchemaSQLite.introspectTableSchema('BookCustomIdx',
982
+ (pError, pSchema) =>
983
+ {
984
+ Expect(pError).to.not.exist;
985
+ Expect(pSchema.TableName).to.equal('BookCustomIdx');
986
+
987
+ // Title has custom IndexName IX_Custom_Title
988
+ let tmpTitleCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'Title'; });
989
+ Expect(tmpTitleCol.Indexed).to.equal(true);
990
+ Expect(tmpTitleCol.IndexName).to.equal('IX_Custom_Title');
991
+
992
+ // ISBN has custom IndexName UQ_BookCustomIdx_ISBN
993
+ let tmpISBNCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'ISBN'; });
994
+ Expect(tmpISBNCol.Indexed).to.equal('unique');
995
+ Expect(tmpISBNCol.IndexName).to.equal('UQ_BookCustomIdx_ISBN');
996
+
997
+ // YearPublished has auto-generated name → no IndexName
998
+ let tmpYearCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'YearPublished'; });
999
+ Expect(tmpYearCol.Indexed).to.equal(true);
1000
+ Expect(tmpYearCol).to.not.have.property('IndexName');
1001
+
1002
+ return fDone();
1003
+ });
1004
+ }
1005
+ );
1006
+
1007
+ test
1008
+ (
1009
+ 'introspectDatabaseSchema returns schemas for all tables',
1010
+ (fDone) =>
1011
+ {
1012
+ libSchemaSQLite.introspectDatabaseSchema(
1013
+ (pError, pSchema) =>
1014
+ {
1015
+ Expect(pError).to.not.exist;
1016
+ Expect(pSchema).to.be.an('object');
1017
+ Expect(pSchema.Tables).to.be.an('array');
1018
+ Expect(pSchema.Tables.length).to.be.greaterThan(0);
1019
+
1020
+ let tmpTableNames = pSchema.Tables.map((pT) => { return pT.TableName; });
1021
+ Expect(tmpTableNames).to.include('Book');
1022
+ Expect(tmpTableNames).to.include('BookIndexed');
1023
+ Expect(tmpTableNames).to.include('BookCustomIdx');
1024
+
1025
+ return fDone();
1026
+ });
1027
+ }
1028
+ );
1029
+
1030
+ test
1031
+ (
1032
+ 'generateMeadowPackageFromTable produces Meadow package JSON',
1033
+ (fDone) =>
1034
+ {
1035
+ libSchemaSQLite.generateMeadowPackageFromTable('Book',
1036
+ (pError, pPackage) =>
1037
+ {
1038
+ Expect(pError).to.not.exist;
1039
+ Expect(pPackage).to.be.an('object');
1040
+ Expect(pPackage.Scope).to.equal('Book');
1041
+ Expect(pPackage.DefaultIdentifier).to.equal('IDBook');
1042
+ Expect(pPackage.Schema).to.be.an('array');
1043
+ Expect(pPackage.DefaultObject).to.be.an('object');
1044
+
1045
+ // Verify schema entries
1046
+ let tmpIDEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'IDBook'; });
1047
+ Expect(tmpIDEntry.Type).to.equal('AutoIdentity');
1048
+
1049
+ let tmpGUIDEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'GUIDBook'; });
1050
+ Expect(tmpGUIDEntry.Type).to.equal('AutoGUID');
1051
+
1052
+ let tmpTitleEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'Title'; });
1053
+ Expect(tmpTitleEntry.Type).to.equal('String');
1054
+
1055
+ // Verify default object
1056
+ Expect(pPackage.DefaultObject.IDBook).to.equal(0);
1057
+ Expect(pPackage.DefaultObject.GUIDBook).to.equal('');
1058
+ Expect(pPackage.DefaultObject.Title).to.equal('');
1059
+
1060
+ return fDone();
1061
+ });
1062
+ }
1063
+ );
1064
+
1065
+ test
1066
+ (
1067
+ 'round-trip: introspect BookIndexed and regenerate matching indices',
1068
+ (fDone) =>
1069
+ {
1070
+ libSchemaSQLite.introspectTableSchema('BookIndexed',
1071
+ (pError, pSchema) =>
1072
+ {
1073
+ Expect(pError).to.not.exist;
1074
+
1075
+ // Use the introspected schema to generate index definitions
1076
+ let tmpIndices = libSchemaSQLite.getIndexDefinitionsFromSchema(pSchema);
1077
+
1078
+ // The original BookIndexed had:
1079
+ // AK_M_GUIDBookIndexed (GUID auto)
1080
+ // IX_M_T_BookIndexed_C_Title (Indexed: true)
1081
+ // AK_M_T_BookIndexed_C_ISBN (Indexed: 'unique')
1082
+ // IX_M_IDPublisher (FK auto)
1083
+ let tmpNames = tmpIndices.map((pIdx) => { return pIdx.Name; });
1084
+ Expect(tmpNames).to.include('AK_M_GUIDBookIndexed');
1085
+ Expect(tmpNames).to.include('IX_M_T_BookIndexed_C_Title');
1086
+ Expect(tmpNames).to.include('AK_M_T_BookIndexed_C_ISBN');
1087
+ Expect(tmpNames).to.include('IX_M_IDPublisher');
1088
+
1089
+ return fDone();
1090
+ });
1091
+ }
1092
+ );
1093
+
1094
+ test
1095
+ (
1096
+ 'round-trip: introspect BookCustomIdx and regenerate matching index names',
1097
+ (fDone) =>
1098
+ {
1099
+ libSchemaSQLite.introspectTableSchema('BookCustomIdx',
1100
+ (pError, pSchema) =>
1101
+ {
1102
+ Expect(pError).to.not.exist;
1103
+
1104
+ // Use the introspected schema to generate index definitions
1105
+ let tmpIndices = libSchemaSQLite.getIndexDefinitionsFromSchema(pSchema);
1106
+
1107
+ // The original BookCustomIdx had:
1108
+ // AK_M_GUIDBookCustomIdx (GUID auto)
1109
+ // IX_Custom_Title (IndexName override)
1110
+ // UQ_BookCustomIdx_ISBN (IndexName override, unique)
1111
+ // IX_M_T_BookCustomIdx_C_YearPublished (auto)
1112
+ // IX_M_IDEditor (FK auto)
1113
+ let tmpNames = tmpIndices.map((pIdx) => { return pIdx.Name; });
1114
+ Expect(tmpNames).to.include('AK_M_GUIDBookCustomIdx');
1115
+ Expect(tmpNames).to.include('IX_Custom_Title');
1116
+ Expect(tmpNames).to.include('UQ_BookCustomIdx_ISBN');
1117
+ Expect(tmpNames).to.include('IX_M_T_BookCustomIdx_C_YearPublished');
1118
+ Expect(tmpNames).to.include('IX_M_IDEditor');
1119
+
1120
+ return fDone();
1121
+ });
1122
+ }
1123
+ );
1124
+
1125
+ suite
1126
+ (
1127
+ 'Chinook Database Introspection',
1128
+ ()=>
1129
+ {
1130
+ setup(
1131
+ (fDone) =>
1132
+ {
1133
+ // Create Chinook tables (runs once before the suite)
1134
+ libSchemaSQLite._Database.exec(_ChinookSQL);
1135
+ return fDone();
1136
+ });
1137
+
1138
+ test
1139
+ (
1140
+ 'listTables includes all 11 Chinook tables',
1141
+ (fDone) =>
1142
+ {
1143
+ libSchemaSQLite.listTables(
1144
+ (pError, pTables) =>
1145
+ {
1146
+ Expect(pError).to.not.exist;
1147
+ Expect(pTables).to.be.an('array');
1148
+
1149
+ let tmpChinookTables = ['Album', 'Artist', 'Customer', 'Employee',
1150
+ 'Genre', 'Invoice', 'InvoiceLine', 'MediaType',
1151
+ 'Playlist', 'PlaylistTrack', 'Track'];
1152
+
1153
+ tmpChinookTables.forEach(
1154
+ (pTableName) =>
1155
+ {
1156
+ Expect(pTables).to.include(pTableName);
1157
+ });
1158
+
1159
+ return fDone();
1160
+ });
1161
+ }
1162
+ );
1163
+
1164
+ test
1165
+ (
1166
+ 'introspectTableColumns on Track detects all 9 columns',
1167
+ (fDone) =>
1168
+ {
1169
+ libSchemaSQLite.introspectTableColumns('Track',
1170
+ (pError, pColumns) =>
1171
+ {
1172
+ Expect(pError).to.not.exist;
1173
+ Expect(pColumns).to.be.an('array');
1174
+ Expect(pColumns.length).to.equal(9);
1175
+
1176
+ let tmpTrackId = pColumns.find((pCol) => { return pCol.Column === 'TrackId'; });
1177
+ Expect(tmpTrackId.DataType).to.equal('ID');
1178
+
1179
+ let tmpName = pColumns.find((pCol) => { return pCol.Column === 'Name'; });
1180
+ Expect(tmpName.DataType).to.equal('String');
1181
+
1182
+ let tmpUnitPrice = pColumns.find((pCol) => { return pCol.Column === 'UnitPrice'; });
1183
+ Expect(tmpUnitPrice.DataType).to.equal('Decimal');
1184
+
1185
+ let tmpMilliseconds = pColumns.find((pCol) => { return pCol.Column === 'Milliseconds'; });
1186
+ Expect(tmpMilliseconds.DataType).to.equal('Numeric');
1187
+
1188
+ return fDone();
1189
+ });
1190
+ }
1191
+ );
1192
+
1193
+ test
1194
+ (
1195
+ 'introspectTableColumns on Employee detects 15 columns',
1196
+ (fDone) =>
1197
+ {
1198
+ libSchemaSQLite.introspectTableColumns('Employee',
1199
+ (pError, pColumns) =>
1200
+ {
1201
+ Expect(pError).to.not.exist;
1202
+ Expect(pColumns.length).to.equal(15);
1203
+
1204
+ let tmpEmployeeId = pColumns.find((pCol) => { return pCol.Column === 'EmployeeId'; });
1205
+ Expect(tmpEmployeeId.DataType).to.equal('ID');
1206
+
1207
+ let tmpLastName = pColumns.find((pCol) => { return pCol.Column === 'LastName'; });
1208
+ Expect(tmpLastName.DataType).to.equal('String');
1209
+
1210
+ return fDone();
1211
+ });
1212
+ }
1213
+ );
1214
+
1215
+ test
1216
+ (
1217
+ 'introspectTableForeignKeys on Track detects 3 FK relationships',
1218
+ (fDone) =>
1219
+ {
1220
+ libSchemaSQLite.introspectTableForeignKeys('Track',
1221
+ (pError, pFKs) =>
1222
+ {
1223
+ Expect(pError).to.not.exist;
1224
+ Expect(pFKs).to.be.an('array');
1225
+ Expect(pFKs.length).to.equal(3);
1226
+
1227
+ let tmpAlbumFK = pFKs.find((pFK) => { return pFK.Column === 'AlbumId'; });
1228
+ Expect(tmpAlbumFK).to.exist;
1229
+ Expect(tmpAlbumFK.ReferencedTable).to.equal('Album');
1230
+ Expect(tmpAlbumFK.ReferencedColumn).to.equal('AlbumId');
1231
+
1232
+ let tmpMediaTypeFK = pFKs.find((pFK) => { return pFK.Column === 'MediaTypeId'; });
1233
+ Expect(tmpMediaTypeFK).to.exist;
1234
+ Expect(tmpMediaTypeFK.ReferencedTable).to.equal('MediaType');
1235
+
1236
+ let tmpGenreFK = pFKs.find((pFK) => { return pFK.Column === 'GenreId'; });
1237
+ Expect(tmpGenreFK).to.exist;
1238
+ Expect(tmpGenreFK.ReferencedTable).to.equal('Genre');
1239
+
1240
+ return fDone();
1241
+ });
1242
+ }
1243
+ );
1244
+
1245
+ test
1246
+ (
1247
+ 'introspectTableForeignKeys on Employee detects self-referential FK',
1248
+ (fDone) =>
1249
+ {
1250
+ libSchemaSQLite.introspectTableForeignKeys('Employee',
1251
+ (pError, pFKs) =>
1252
+ {
1253
+ Expect(pError).to.not.exist;
1254
+ Expect(pFKs).to.be.an('array');
1255
+ Expect(pFKs.length).to.equal(1);
1256
+
1257
+ Expect(pFKs[0].Column).to.equal('ReportsTo');
1258
+ Expect(pFKs[0].ReferencedTable).to.equal('Employee');
1259
+ Expect(pFKs[0].ReferencedColumn).to.equal('EmployeeId');
1260
+
1261
+ return fDone();
1262
+ });
1263
+ }
1264
+ );
1265
+
1266
+ test
1267
+ (
1268
+ 'introspectTableForeignKeys on PlaylistTrack detects 2 FKs',
1269
+ (fDone) =>
1270
+ {
1271
+ libSchemaSQLite.introspectTableForeignKeys('PlaylistTrack',
1272
+ (pError, pFKs) =>
1273
+ {
1274
+ Expect(pError).to.not.exist;
1275
+ Expect(pFKs).to.be.an('array');
1276
+ Expect(pFKs.length).to.equal(2);
1277
+
1278
+ let tmpPlaylistFK = pFKs.find((pFK) => { return pFK.Column === 'PlaylistId'; });
1279
+ Expect(tmpPlaylistFK).to.exist;
1280
+ Expect(tmpPlaylistFK.ReferencedTable).to.equal('Playlist');
1281
+
1282
+ let tmpTrackFK = pFKs.find((pFK) => { return pFK.Column === 'TrackId'; });
1283
+ Expect(tmpTrackFK).to.exist;
1284
+ Expect(tmpTrackFK.ReferencedTable).to.equal('Track');
1285
+
1286
+ return fDone();
1287
+ });
1288
+ }
1289
+ );
1290
+
1291
+ test
1292
+ (
1293
+ 'introspectTableSchema on Track combines columns with FK detection',
1294
+ (fDone) =>
1295
+ {
1296
+ libSchemaSQLite.introspectTableSchema('Track',
1297
+ (pError, pSchema) =>
1298
+ {
1299
+ Expect(pError).to.not.exist;
1300
+ Expect(pSchema.TableName).to.equal('Track');
1301
+ Expect(pSchema.Columns).to.be.an('array');
1302
+ Expect(pSchema.ForeignKeys).to.be.an('array');
1303
+ Expect(pSchema.ForeignKeys.length).to.equal(3);
1304
+
1305
+ // FK columns should be upgraded to ForeignKey DataType
1306
+ let tmpAlbumIdCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'AlbumId'; });
1307
+ Expect(tmpAlbumIdCol.DataType).to.equal('ForeignKey');
1308
+
1309
+ let tmpMediaTypeIdCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'MediaTypeId'; });
1310
+ Expect(tmpMediaTypeIdCol.DataType).to.equal('ForeignKey');
1311
+
1312
+ let tmpGenreIdCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'GenreId'; });
1313
+ Expect(tmpGenreIdCol.DataType).to.equal('ForeignKey');
1314
+
1315
+ return fDone();
1316
+ });
1317
+ }
1318
+ );
1319
+
1320
+ test
1321
+ (
1322
+ 'introspectTableSchema on Album shows FK to Artist',
1323
+ (fDone) =>
1324
+ {
1325
+ libSchemaSQLite.introspectTableSchema('Album',
1326
+ (pError, pSchema) =>
1327
+ {
1328
+ Expect(pError).to.not.exist;
1329
+ Expect(pSchema.TableName).to.equal('Album');
1330
+ Expect(pSchema.ForeignKeys.length).to.equal(1);
1331
+ Expect(pSchema.ForeignKeys[0].Column).to.equal('ArtistId');
1332
+ Expect(pSchema.ForeignKeys[0].ReferencedTable).to.equal('Artist');
1333
+
1334
+ let tmpArtistIdCol = pSchema.Columns.find((pCol) => { return pCol.Column === 'ArtistId'; });
1335
+ Expect(tmpArtistIdCol.DataType).to.equal('ForeignKey');
1336
+
1337
+ return fDone();
1338
+ });
1339
+ }
1340
+ );
1341
+
1342
+ test
1343
+ (
1344
+ 'introspectDatabaseSchema includes all Chinook tables',
1345
+ (fDone) =>
1346
+ {
1347
+ libSchemaSQLite.introspectDatabaseSchema(
1348
+ (pError, pSchema) =>
1349
+ {
1350
+ Expect(pError).to.not.exist;
1351
+ Expect(pSchema.Tables).to.be.an('array');
1352
+
1353
+ let tmpTableNames = pSchema.Tables.map((pT) => { return pT.TableName; });
1354
+ Expect(tmpTableNames).to.include('Track');
1355
+ Expect(tmpTableNames).to.include('Album');
1356
+ Expect(tmpTableNames).to.include('Artist');
1357
+ Expect(tmpTableNames).to.include('Invoice');
1358
+ Expect(tmpTableNames).to.include('InvoiceLine');
1359
+ Expect(tmpTableNames).to.include('PlaylistTrack');
1360
+ Expect(tmpTableNames).to.include('Employee');
1361
+ Expect(tmpTableNames).to.include('Customer');
1362
+
1363
+ // Verify Track schema has FKs detected
1364
+ let tmpTrack = pSchema.Tables.find((pT) => { return pT.TableName === 'Track'; });
1365
+ Expect(tmpTrack.ForeignKeys.length).to.equal(3);
1366
+
1367
+ return fDone();
1368
+ });
1369
+ }
1370
+ );
1371
+
1372
+ test
1373
+ (
1374
+ 'generateMeadowPackageFromTable on Album produces valid package',
1375
+ (fDone) =>
1376
+ {
1377
+ libSchemaSQLite.generateMeadowPackageFromTable('Album',
1378
+ (pError, pPackage) =>
1379
+ {
1380
+ Expect(pError).to.not.exist;
1381
+ Expect(pPackage.Scope).to.equal('Album');
1382
+ Expect(pPackage.DefaultIdentifier).to.equal('AlbumId');
1383
+ Expect(pPackage.Schema).to.be.an('array');
1384
+ Expect(pPackage.DefaultObject).to.be.an('object');
1385
+
1386
+ let tmpIDEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'AlbumId'; });
1387
+ Expect(tmpIDEntry.Type).to.equal('AutoIdentity');
1388
+
1389
+ let tmpTitleEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'Title'; });
1390
+ Expect(tmpTitleEntry.Type).to.equal('String');
1391
+
1392
+ return fDone();
1393
+ });
1394
+ }
1395
+ );
1396
+
1397
+ test
1398
+ (
1399
+ 'generateMeadowPackageFromTable on Track handles FKs and Decimal',
1400
+ (fDone) =>
1401
+ {
1402
+ libSchemaSQLite.generateMeadowPackageFromTable('Track',
1403
+ (pError, pPackage) =>
1404
+ {
1405
+ Expect(pError).to.not.exist;
1406
+ Expect(pPackage.Scope).to.equal('Track');
1407
+ Expect(pPackage.DefaultIdentifier).to.equal('TrackId');
1408
+
1409
+ let tmpUnitPriceEntry = pPackage.Schema.find((pEntry) => { return pEntry.Column === 'UnitPrice'; });
1410
+ Expect(tmpUnitPriceEntry).to.exist;
1411
+
1412
+ return fDone();
1413
+ });
1414
+ }
1415
+ );
1416
+ }
1417
+ );
1418
+ }
1419
+ );
428
1420
  }
429
1421
  );