diff --git a/P3-Tests/P3ClientTest.class.st b/P3-Tests/P3ClientTest.class.st index a7ccb93..64c9162 100644 --- a/P3-Tests/P3ClientTest.class.st +++ b/P3-Tests/P3ClientTest.class.st @@ -265,6 +265,48 @@ P3ClientTest >> testConnection [ self assert: client isWorking. ] +{ #category : #tests } +P3ClientTest >> testConstraints [ + | statement constraints constraint | + client execute: 'DROP TABLE IF EXISTS orders'. + client execute: 'DROP TABLE IF EXISTS suppliers'. + client execute: 'CREATE TABLE suppliers (id INTEGER, name TEXT)'. + client execute: 'CREATE TABLE orders (id INTEGER, description TEXT, supplier_id INTEGER)'. + client execute: 'ALTER TABLE suppliers ADD CONSTRAINT suppliers_pkey PRIMARY KEY (id)'. + client execute: 'ALTER TABLE orders ADD CONSTRAINT orders_pkey PRIMARY KEY (id)'. + client execute: 'ALTER TABLE orders ADD CONSTRAINT orders_suppliers_fkey FOREIGN KEY (supplier_id) REFERENCES suppliers (id)'. + statement := client format: 'INSERT INTO suppliers (id, name) VALUES ($1, $2)'. + statement executeBatch: #((1 'Foo')(2 'Bar')). + statement := client format: 'INSERT INTO orders (id, description, supplier_id) VALUES ($1, $2, $3)'. + statement executeBatch: #((1 'AAA' 1)(2 'BBB' 1)(3 'CCC' 2)). + constraints := P3Constraint allForTable: 'suppliers' in: 'public' using: client. + constraint := constraints + detect: [ :each | each constraintName = 'suppliers_pkey' ] + ifNone: [ self fail ]. + self assert: constraint isPrimaryKey. + self assert: constraint tableName equals: 'suppliers'. + self assert: constraint constraintColumns equals: #(id). + constraints := P3Constraint allForTable: 'orders' in: 'public' using: client. + constraint := constraints + detect: [ :each | each constraintName = 'orders_pkey' ] + ifNone: [ self fail ]. + self assert: constraint isPrimaryKey. + self assert: constraint tableName equals: 'orders'. + self assert: constraint constraintColumns equals: #(id). + constraint := constraints + detect: [ :each | each constraintName = 'orders_suppliers_fkey' ] + ifNone: [ self fail ]. + self assert: constraint isForeignKey. + self assert: constraint tableName equals: 'orders'. + self assert: constraint constraintColumns equals: #(supplier_id). + self assert: constraint foreignKeyTable equals: 'suppliers'. + self assert: constraint foreignKeyColumns equals: #(id). + constraints := P3Constraint referencingConstraintNamesForTable: 'suppliers' in: 'public' using: client. + self assert: constraints equals: #(('orders' 'orders_suppliers_fkey')). + client execute: 'DROP TABLE orders'. + client execute: 'DROP TABLE suppliers' +] + { #category : #tests } P3ClientTest >> testConvenienceMetaAccess [ self deny: client listDatabases isEmpty. diff --git a/P3/P3CheckConstraint.class.st b/P3/P3CheckConstraint.class.st new file mode 100644 index 0000000..bd2f230 --- /dev/null +++ b/P3/P3CheckConstraint.class.st @@ -0,0 +1,53 @@ +" +I am P3CheckConstraint. +I am a P3Constraint. + +I implement SQL CHECK. + +I know my check clause. +" +Class { + #name : #P3CheckConstraint, + #superclass : #P3Constraint, + #instVars : [ + 'checkClause' + ], + #category : #'P3-Support' +} + +{ #category : #accessing } +P3CheckConstraint class >> handlesType: type [ + ^ type = 'CHECK' +] + +{ #category : #accessing } +P3CheckConstraint >> checkClause [ + ^ checkClause +] + +{ #category : #accessing } +P3CheckConstraint >> checkClause: anObject [ + checkClause := anObject +] + +{ #category : #accessing } +P3CheckConstraint >> constraintType [ + ^ 'CHECK' +] + +{ #category : #accessing } +P3CheckConstraint >> loadDetailsUsing: client [ + | statement result | + super loadDetailsUsing: client. + statement := client format: 'SELECT check_clause FROM information_schema.check_constraints WHERE constraint_schema = $1 AND constraint_name = $2'. + result := statement query: { self constraintSchema . self constraintName }. + self checkClause: result firstFieldOfFirstRecord +] + +{ #category : #accessing } +P3CheckConstraint >> sqlDescription [ + ^ String streamContents: [ :out | + out nextPutAll: 'CHECK'. + self checkClause ifNotNil: [ :clause | + out space; nextPutAll: clause ] ] +] diff --git a/P3/P3Constraint.class.st b/P3/P3Constraint.class.st new file mode 100644 index 0000000..caa4e54 --- /dev/null +++ b/P3/P3Constraint.class.st @@ -0,0 +1,202 @@ +" +I am P3Constraint. +I am an abstract class. + +I represent an SQL constraint. + +I have +- a name +- a schem:table that I am defined on +- a list of columns on which I am applicable + +I can reproduce my SQL definition. + +Use me to get all constraints for a given table. + +My concrete subclasses implement actual constraint types. +" +Class { + #name : #P3Constraint, + #superclass : #Object, + #instVars : [ + 'constraintSchema', + 'constraintName', + 'tableSchema', + 'tableName', + 'isDeferrable', + 'initiallyDeferred', + 'enforced', + 'constraintColumns' + ], + #category : #'P3-Support' +} + +{ #category : #accessing } +P3Constraint class >> allForTable: tableName in: schemaName using: client [ + | statement result specificClass constraint | + statement := client format: 'SELECT constraint_schema, constraint_name, constraint_type, is_deferrable, initially_deferred, enforced FROM information_schema.table_constraints WHERE table_schema = $1 AND table_name= $2'. + result := statement query: { schemaName . tableName }. + ^ result data collect: [ :row | + specificClass := self subclasses + detect: [ :each | each handlesType: row third ] + ifNone: [ self error: 'unknown contraint type' ]. + constraint := specificClass new. + constraint + tableSchema: schemaName; + tableName: tableName; + constraintSchema: row first; + constraintName: row second; + isDeferrable: row fourth = 'YES'; + initiallyDeferred: row fifth = 'YES'; + enforced: row = 'YES'. + constraint loadDetailsUsing: client. + constraint ] +] + +{ #category : #accessing } +P3Constraint class >> handlesType: type [ + self subclassResponsibility +] + +{ #category : #accessing } +P3Constraint class >> referencingConstraintNamesForTable: tableName in: schemaName using: client [ + "Return (table_name, constraint_name) pairs where the given schemaName:tableName is referenced. + Do not return the actual constraint object as this might be costly to compute." + + | statement result | + statement := client format: 'SELECT tc.table_name, ctu.constraint_name +FROM information_schema.constraint_table_usage AS ctu, information_schema.table_constraints AS tc +WHERE ctu.table_schema = $1 AND ctu.table_name = $2 AND ctu.constraint_name = tc.constraint_name AND tc.constraint_type = ''FOREIGN KEY'''. + result := statement query: { schemaName . tableName }. + ^ result data +] + +{ #category : #accessing } +P3Constraint >> constraintColumns [ + ^ constraintColumns +] + +{ #category : #accessing } +P3Constraint >> constraintColumns: anObject [ + constraintColumns := anObject +] + +{ #category : #accessing } +P3Constraint >> constraintName [ + ^ constraintName +] + +{ #category : #accessing } +P3Constraint >> constraintName: anObject [ + constraintName := anObject +] + +{ #category : #accessing } +P3Constraint >> constraintSchema [ + ^ constraintSchema +] + +{ #category : #accessing } +P3Constraint >> constraintSchema: anObject [ + constraintSchema := anObject +] + +{ #category : #accessing } +P3Constraint >> constraintType [ + self subclassResponsibility +] + +{ #category : #accessing } +P3Constraint >> enforced [ + ^ enforced +] + +{ #category : #accessing } +P3Constraint >> enforced: anObject [ + enforced := anObject +] + +{ #category : #accessing } +P3Constraint >> initiallyDeferred [ + ^ initiallyDeferred +] + +{ #category : #accessing } +P3Constraint >> initiallyDeferred: anObject [ + initiallyDeferred := anObject +] + +{ #category : #testing } +P3Constraint >> isCheck [ + ^ self constraintType = 'CHECK' +] + +{ #category : #accessing } +P3Constraint >> isDeferrable [ + ^ isDeferrable +] + +{ #category : #accessing } +P3Constraint >> isDeferrable: anObject [ + isDeferrable := anObject +] + +{ #category : #testing } +P3Constraint >> isForeignKey [ + ^ self constraintType = 'FOREIGN KEY' +] + +{ #category : #testing } +P3Constraint >> isPrimaryKey [ + ^ self constraintType = 'PRIMARY KEY' +] + +{ #category : #testing } +P3Constraint >> isUnique [ + ^ self constraintType = 'UNIQUE' +] + +{ #category : #accessing } +P3Constraint >> loadDetailsUsing: client [ + | statement result | + statement := client format: 'SELECT column_name FROM information_schema.key_column_usage WHERE constraint_schema = $1 AND constraint_name = $2'. + result := statement query: { self constraintSchema . self constraintName }. + self constraintColumns: result firstColumnData +] + +{ #category : #accessing } +P3Constraint >> printOn: stream [ + super printOn: stream. + stream nextPut: $(. + self constraintName ifNotNil: [ :name | + stream nextPutAll: name ]. + self sqlDescription ifNotNil: [ :description | + self constraintName ifNotNil: [ stream space ]. + stream nextPutAll: description ]. + stream nextPut: $) +] + +{ #category : #accessing } +P3Constraint >> sqlDescription [ + self subclassResponsibility +] + +{ #category : #accessing } +P3Constraint >> tableName [ + ^ tableName +] + +{ #category : #accessing } +P3Constraint >> tableName: anObject [ + tableName := anObject +] + +{ #category : #accessing } +P3Constraint >> tableSchema [ + ^ tableSchema +] + +{ #category : #accessing } +P3Constraint >> tableSchema: anObject [ + tableSchema := anObject +] diff --git a/P3/P3ForeignKeyConstraint.class.st b/P3/P3ForeignKeyConstraint.class.st new file mode 100644 index 0000000..271096c --- /dev/null +++ b/P3/P3ForeignKeyConstraint.class.st @@ -0,0 +1,76 @@ +" +I am P3ForeignKeyConstraint. +I am a P3Constraint. + +I implement SQL FOREIGN KEY. + +I know to table and columns that I reference. +" +Class { + #name : #P3ForeignKeyConstraint, + #superclass : #P3Constraint, + #instVars : [ + 'foreignKeyTable', + 'foreignKeyColumns' + ], + #category : #'P3-Support' +} + +{ #category : #accessing } +P3ForeignKeyConstraint class >> handlesType: type [ + ^ type = 'FOREIGN KEY' +] + +{ #category : #accessing } +P3ForeignKeyConstraint >> constraintType [ + ^ 'FOREIGN KEY' +] + +{ #category : #accessing } +P3ForeignKeyConstraint >> foreignKeyColumns [ + ^ foreignKeyColumns +] + +{ #category : #accessing } +P3ForeignKeyConstraint >> foreignKeyColumns: anObject [ + foreignKeyColumns := anObject +] + +{ #category : #accessing } +P3ForeignKeyConstraint >> foreignKeyTable [ + ^ foreignKeyTable +] + +{ #category : #accessing } +P3ForeignKeyConstraint >> foreignKeyTable: anObject [ + foreignKeyTable := anObject +] + +{ #category : #accessing } +P3ForeignKeyConstraint >> loadDetailsUsing: client [ + | statement result | + super loadDetailsUsing: client. + statement := client format: 'SELECT column_name FROM information_schema.constraint_column_usage WHERE constraint_schema = $1 AND constraint_name = $2'. + result := statement query: { self constraintSchema . self constraintName }. + self foreignKeyColumns: result firstColumnData. + statement := client format: 'SELECT table_name FROM information_schema.constraint_table_usage WHERE constraint_schema = $1 AND constraint_name = $2'. + result := statement query: { self constraintSchema . self constraintName }. + self foreignKeyTable: result firstFieldOfFirstRecord +] + +{ #category : #accessing } +P3ForeignKeyConstraint >> sqlDescription [ + ^ String streamContents: [ :out | + out nextPutAll: 'FOREIGN KEY'. + (self constraintColumns isEmptyOrNil + or: [ self foreignKeyColumns isEmptyOrNil + or: [ self foreignKeyTable isNil ] ]) + ifFalse: [ + out nextPutAll: ' ('; + nextPutAll: ($, join: self constraintColumns); + nextPutAll: ') REFERENCES '; + nextPutAll: self foreignKeyTable; + nextPut: $(; + nextPutAll: ($, join: self foreignKeyColumns); + nextPut: $) ] ] +] diff --git a/P3/P3PrimaryKeyConstraint.class.st b/P3/P3PrimaryKeyConstraint.class.st new file mode 100644 index 0000000..1ce7c14 --- /dev/null +++ b/P3/P3PrimaryKeyConstraint.class.st @@ -0,0 +1,26 @@ +Class { + #name : #P3PrimaryKeyConstraint, + #superclass : #P3Constraint, + #category : #'P3-Support' +} + +{ #category : #accessing } +P3PrimaryKeyConstraint class >> handlesType: type [ + ^ type = 'PRIMARY KEY' +] + +{ #category : #accessing } +P3PrimaryKeyConstraint >> constraintType [ + ^ 'PRIMARY KEY' +] + +{ #category : #accessing } +P3PrimaryKeyConstraint >> sqlDescription [ + ^ String streamContents: [ :out | + out nextPutAll: 'PRIMARY KEY'. + self constraintColumns isEmptyOrNil ifFalse: [ + out + nextPutAll: ' ('; + nextPutAll: ($, join: self constraintColumns); + nextPut: $) ] ] +] diff --git a/P3/P3UniqueConstraint.class.st b/P3/P3UniqueConstraint.class.st new file mode 100644 index 0000000..2e95464 --- /dev/null +++ b/P3/P3UniqueConstraint.class.st @@ -0,0 +1,26 @@ +Class { + #name : #P3UniqueConstraint, + #superclass : #P3Constraint, + #category : #'P3-Support' +} + +{ #category : #accessing } +P3UniqueConstraint class >> handlesType: type [ + ^ type = 'UNIQUE' +] + +{ #category : #accessing } +P3UniqueConstraint >> constraintType [ + ^ 'UNIQUE' +] + +{ #category : #accessing } +P3UniqueConstraint >> sqlDescription [ + ^ String streamContents: [ :out | + out nextPutAll: 'UNIQUE'. + self constraintColumns isEmptyOrNil ifFalse: [ + out + nextPutAll: ' ('; + nextPutAll: ($, join: self constraintColumns); + nextPut: $) ] ] +]